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
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 |
|------|--------------|-------|
@@ -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_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_submit_verdict` | `POST /api/topics/{id}/verdict` | judge submits structured verdict |
| `dialectic_view_verdict` | `GET /api/topics/{id}/verdict` | 404 until judge submits |
## Setup

View File

@@ -3,7 +3,8 @@
*
* Tools: dialectic_list_topics, dialectic_topic_detail,
* 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]]):
* - openclaw.plugin.json MUST declare `activation.onStartup: true`

View File

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

View File

@@ -19,7 +19,15 @@ import { BackendClient } from './backend-client.js';
import { hfOnCallCoverageCheck } from './hf-precheck.js';
const DEFAULT_BACKEND_URL = 'https://dialectic-api.hangman-lab.top';
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
// backend api key resolves from that agent's secret-mgr).
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 });
}),
}));
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) => ({
name: 'dialectic_view_verdict',
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`);
}),
}));
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. */
async function asContent(fn) {

View File

@@ -21,14 +21,24 @@ import { hfOnCallCoverageCheck } from './hf-precheck.js';
type ToolApi = {
registerTool(def: any): void;
config?: { backendUrl?: string };
pluginConfig?: { backendUrl?: string };
config?: unknown;
logger: { info(msg: string): void; warn(msg: string): void };
};
const DEFAULT_BACKEND_URL = 'https://dialectic-api.hangman-lab.top';
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
// 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 }) => ({
name: 'dialectic_view_verdict',
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. */