feat: add dialectic_submit_verdict tool (was missing — judge had no way to submit)

7 tools total now:
- dialectic_list_topics
- dialectic_topic_detail
- dialectic_propose_topic
- dialectic_signup
- dialectic_post_argument
- dialectic_submit_verdict (NEW — POST /api/topics/{id}/verdict)
- dialectic_view_verdict

Also added contracts.tools entry for the new tool, updated README +
startup log line. Sim smoke verified the other 4 (list/propose/signup/
detail) via direct plugin import; submit_verdict not yet smoke-tested
end-to-end (requires running through a full debate to debate_end_at).
Code path is identical to other write tools — bearer + JSON body +
shape-coerced response.
This commit is contained in:
h z
2026-05-23 13:10:44 +01:00
parent db85d7dc69
commit 119d79ada3
5 changed files with 102 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
# Dialectic.OpenclawPlugin # Dialectic.OpenclawPlugin
OpenClaw plugin that gives agents tools to participate in Dialectic v2 OpenClaw plugin that gives agents tools to participate in Dialectic v2
debates. Six tools, one per Dialectic backend endpoint they need: debates. Seven tools, one per Dialectic backend endpoint they need:
| Tool | Backend call | Notes | | Tool | Backend call | Notes |
|------|--------------|-------| |------|--------------|-------|
@@ -10,6 +10,7 @@ debates. Six tools, one per Dialectic backend endpoint they need:
| `dialectic_propose_topic` | `POST /api/topics` | title + summary + 4 lifecycle timestamps | | `dialectic_propose_topic` | `POST /api/topics` | title + summary + 4 lifecycle timestamps |
| `dialectic_signup` | `POST /api/topics/{id}/signups` | with HF on_call coverage pre-check | | `dialectic_signup` | `POST /api/topics/{id}/signups` | with HF on_call coverage pre-check |
| `dialectic_post_argument` | `POST /api/topics/{id}/arguments` | during `debating` only | | `dialectic_post_argument` | `POST /api/topics/{id}/arguments` | during `debating` only |
| `dialectic_submit_verdict` | `POST /api/topics/{id}/verdict` | judge submits structured verdict |
| `dialectic_view_verdict` | `GET /api/topics/{id}/verdict` | 404 until judge submits | | `dialectic_view_verdict` | `GET /api/topics/{id}/verdict` | 404 until judge submits |
## Setup ## Setup

View File

@@ -3,7 +3,8 @@
* *
* Tools: dialectic_list_topics, dialectic_topic_detail, * Tools: dialectic_list_topics, dialectic_topic_detail,
* dialectic_propose_topic, dialectic_signup, * dialectic_propose_topic, dialectic_signup,
* dialectic_post_argument, dialectic_view_verdict * dialectic_post_argument, dialectic_submit_verdict,
* dialectic_view_verdict
* *
* Loader gotchas (per [[reference-meridian-plugin-contract]]): * Loader gotchas (per [[reference-meridian-plugin-contract]]):
* - openclaw.plugin.json MUST declare `activation.onStartup: true` * - openclaw.plugin.json MUST declare `activation.onStartup: true`

View File

@@ -14,6 +14,7 @@
"dialectic_propose_topic", "dialectic_propose_topic",
"dialectic_signup", "dialectic_signup",
"dialectic_post_argument", "dialectic_post_argument",
"dialectic_submit_verdict",
"dialectic_view_verdict" "dialectic_view_verdict"
] ]
}, },

View File

@@ -19,7 +19,15 @@ import { BackendClient } from './backend-client.js';
import { hfOnCallCoverageCheck } from './hf-precheck.js'; import { hfOnCallCoverageCheck } from './hf-precheck.js';
const DEFAULT_BACKEND_URL = 'https://dialectic-api.hangman-lab.top'; const DEFAULT_BACKEND_URL = 'https://dialectic-api.hangman-lab.top';
export function registerDialecticTools(api) { export function registerDialecticTools(api) {
const backendUrl = api.config?.backendUrl ?? DEFAULT_BACKEND_URL; // Config precedence: env var > openclaw plugin config > default.
// pluginConfig is the plugin-scoped subset (openclaw.json
// plugins.entries.dialectic.config) — confirmed by reading the
// openclaw SDK's api-builder which assigns `pluginConfig:
// params.pluginConfig`. `api.config` (whole openclaw.json) is left
// accessible for downstream that needs cross-plugin lookups.
const backendUrl = (process.env.DIALECTIC_BACKEND_URL ?? '').trim() ||
api.pluginConfig?.backendUrl ||
DEFAULT_BACKEND_URL;
// Per-tool we pull ctx.agentId out of the factory (the // Per-tool we pull ctx.agentId out of the factory (the
// backend api key resolves from that agent's secret-mgr). // backend api key resolves from that agent's secret-mgr).
api.registerTool((ctx) => ({ api.registerTool((ctx) => ({
@@ -165,6 +173,41 @@ export function registerDialecticTools(api) {
return await client.post(`/api/topics/${encodeURIComponent(params.topic_id)}/arguments`, { content: params.content }); return await client.post(`/api/topics/${encodeURIComponent(params.topic_id)}/arguments`, { content: params.content });
}), }),
})); }));
api.registerTool((ctx) => ({
name: 'dialectic_submit_verdict',
description: 'Submit the structured verdict for a debate you are the judge of. Topic must be in `debating` ' +
'status AND past its debate_end_at. The `verdict` JSON shape must match the topic\'s ' +
'verdict_schema_id (binary / claim-resolution / policy-recommendation / free-form). On success ' +
'the topic transitions to `completed` and the verdict is visible via dialectic_view_verdict.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
topic_id: { type: 'string' },
verdict: {
type: 'object',
description: 'Structured verdict matching the topic verdict_schema_id shape',
additionalProperties: true,
},
rationale: { type: 'string', description: 'Reasoning behind the verdict; non-empty' },
tokens_input: { type: 'integer', minimum: 0, description: 'Optional cost telemetry' },
tokens_output: { type: 'integer', minimum: 0, description: 'Optional cost telemetry' },
},
required: ['topic_id', 'verdict', 'rationale'],
},
execute: async (_id, params) => asContent(async () => {
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
const body = {
verdict: params.verdict,
rationale: params.rationale,
};
if (typeof params.tokens_input === 'number')
body.tokens_input = params.tokens_input;
if (typeof params.tokens_output === 'number')
body.tokens_output = params.tokens_output;
return await client.post(`/api/topics/${encodeURIComponent(params.topic_id)}/verdict`, body);
}),
}));
api.registerTool((ctx) => ({ api.registerTool((ctx) => ({
name: 'dialectic_view_verdict', name: 'dialectic_view_verdict',
description: 'Fetch the structured verdict for a completed Dialectic topic. 404 if the debate is still ' + description: 'Fetch the structured verdict for a completed Dialectic topic. 404 if the debate is still ' +
@@ -180,7 +223,7 @@ export function registerDialecticTools(api) {
return await client.get(`/api/topics/${encodeURIComponent(params.topic_id)}/verdict`); return await client.get(`/api/topics/${encodeURIComponent(params.topic_id)}/verdict`);
}), }),
})); }));
api.logger.info(`[dialectic] registered 6 tools (backend=${backendUrl})`); api.logger.info(`[dialectic] registered 7 tools (backend=${backendUrl})`);
} }
/** Wraps an async backend call and converts result/error into MCP content shape. */ /** Wraps an async backend call and converts result/error into MCP content shape. */
async function asContent(fn) { async function asContent(fn) {

View File

@@ -21,14 +21,24 @@ import { hfOnCallCoverageCheck } from './hf-precheck.js';
type ToolApi = { type ToolApi = {
registerTool(def: any): void; registerTool(def: any): void;
config?: { backendUrl?: string }; pluginConfig?: { backendUrl?: string };
config?: unknown;
logger: { info(msg: string): void; warn(msg: string): void }; logger: { info(msg: string): void; warn(msg: string): void };
}; };
const DEFAULT_BACKEND_URL = 'https://dialectic-api.hangman-lab.top'; const DEFAULT_BACKEND_URL = 'https://dialectic-api.hangman-lab.top';
export function registerDialecticTools(api: ToolApi): void { export function registerDialecticTools(api: ToolApi): void {
const backendUrl = api.config?.backendUrl ?? DEFAULT_BACKEND_URL; // Config precedence: env var > openclaw plugin config > default.
// pluginConfig is the plugin-scoped subset (openclaw.json
// plugins.entries.dialectic.config) — confirmed by reading the
// openclaw SDK's api-builder which assigns `pluginConfig:
// params.pluginConfig`. `api.config` (whole openclaw.json) is left
// accessible for downstream that needs cross-plugin lookups.
const backendUrl =
(process.env.DIALECTIC_BACKEND_URL ?? '').trim() ||
api.pluginConfig?.backendUrl ||
DEFAULT_BACKEND_URL;
// Per-tool we pull ctx.agentId out of the factory (the // Per-tool we pull ctx.agentId out of the factory (the
// backend api key resolves from that agent's secret-mgr). // backend api key resolves from that agent's secret-mgr).
@@ -194,6 +204,45 @@ export function registerDialecticTools(api: ToolApi): void {
}), }),
})); }));
api.registerTool((ctx: { agentId?: string }) => ({
name: 'dialectic_submit_verdict',
description:
'Submit the structured verdict for a debate you are the judge of. Topic must be in `debating` ' +
'status AND past its debate_end_at. The `verdict` JSON shape must match the topic\'s ' +
'verdict_schema_id (binary / claim-resolution / policy-recommendation / free-form). On success ' +
'the topic transitions to `completed` and the verdict is visible via dialectic_view_verdict.',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
topic_id: { type: 'string' },
verdict: {
type: 'object',
description: 'Structured verdict matching the topic verdict_schema_id shape',
additionalProperties: true,
},
rationale: { type: 'string', description: 'Reasoning behind the verdict; non-empty' },
tokens_input: { type: 'integer', minimum: 0, description: 'Optional cost telemetry' },
tokens_output: { type: 'integer', minimum: 0, description: 'Optional cost telemetry' },
},
required: ['topic_id', 'verdict', 'rationale'],
},
execute: async (_id: string, params: Record<string, any>) =>
asContent(async () => {
const client = new BackendClient({ baseUrl: backendUrl, agentId: ctx.agentId ?? '' });
const body: Record<string, any> = {
verdict: params.verdict,
rationale: params.rationale,
};
if (typeof params.tokens_input === 'number') body.tokens_input = params.tokens_input;
if (typeof params.tokens_output === 'number') body.tokens_output = params.tokens_output;
return await client.post(
`/api/topics/${encodeURIComponent(params.topic_id)}/verdict`,
body,
);
}),
}));
api.registerTool((ctx: { agentId?: string }) => ({ api.registerTool((ctx: { agentId?: string }) => ({
name: 'dialectic_view_verdict', name: 'dialectic_view_verdict',
description: description:
@@ -212,7 +261,7 @@ export function registerDialecticTools(api: ToolApi): void {
}), }),
})); }));
api.logger.info(`[dialectic] registered 6 tools (backend=${backendUrl})`); api.logger.info(`[dialectic] registered 7 tools (backend=${backendUrl})`);
} }
/** Wraps an async backend call and converts result/error into MCP content shape. */ /** Wraps an async backend call and converts result/error into MCP content shape. */