Skip to content

Commit b319493

Browse files
authored
improve: let Code Mode use declared tool result shapes (#109813)
* feat(agents): add Code Mode output contracts * perf(agents): defer untrusted tool schemas * chore(plugin-sdk): refresh API baseline * fix(agents): preserve deferred schema markers * fix(agents): preserve policy-blocked tool results * fix(agents): preserve nullable output contracts * fix(agents): reject policy blocks from success contracts * fix(agents): preserve output contracts through normalization * test(agents): type normalized tool fixtures * fix(agents): validate catalog results before projection * fix(agents): keep result snapshots internal * fix(agents): fail closed on unrenderable unions * fix(agents): preserve empty result markers * test(agents): keep result fixture discriminator literal * test(agents): use native own-property check
1 parent fee4490 commit b319493

33 files changed

Lines changed: 1711 additions & 244 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
5bab8bc957d81266c1c8cb760ff989e03ca8ce9c4057c990137f0ec2de2f003e plugin-sdk-api-baseline.json
2-
c5f6cd4cb5919af53c6d59765aa133740f00ab6002c65761bc3eedabc6c3ba70 plugin-sdk-api-baseline.jsonl
1+
dc2bf410e15972389b8e60d772f92731130148845dff24294fae4786d3b9f41a plugin-sdk-api-baseline.json
2+
ce076f7e71bb1ffbd0eead42a178dd069e15693348c4fd69b2ed291d51a8fcd7 plugin-sdk-api-baseline.jsonl

docs/docs.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@
164164
"source": "/tools/capability-cookbook",
165165
"destination": "/plugins/adding-capabilities"
166166
},
167+
{
168+
"source": "/reference/code-mode",
169+
"destination": "/tools/code-mode"
170+
},
167171
{
168172
"source": "/brave-search",
169173
"destination": "/tools/brave-search"
@@ -1356,6 +1360,7 @@
13561360
"tools/apply-patch",
13571361
"tools/btw",
13581362
"tools/code-execution",
1363+
"tools/code-mode",
13591364
"tools/diffs",
13601365
"tools/show-widget",
13611366
"tools/elevated",
@@ -1870,7 +1875,6 @@
18701875
"pages": [
18711876
"reference/rpc",
18721877
"gateway/external-apps",
1873-
"reference/code-mode",
18741878
"reference/device-models"
18751879
]
18761880
},

docs/docs_map.md

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7541,6 +7541,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
75417541
- H2: Write a tool
75427542
- H2: Optional and factory tools
75437543
- H2: Return values
7544+
- H2: Output contracts
75447545
- H2: Configuration
75457546
- H2: Generated metadata
75467547
- H2: Package metadata
@@ -8623,48 +8624,6 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
86238624
- H3: Skills (third-party APIs)
86248625
- H2: Related
86258626

8626-
## reference/code-mode.md
8627-
8628-
- Route: /reference/code-mode
8629-
- Headings:
8630-
- H2: What it does
8631-
- H2: Why use it
8632-
- H2: Enable it
8633-
- H2: Technical tour
8634-
- H2: Runtime status
8635-
- H2: Scope
8636-
- H2: Terms
8637-
- H2: Configuration
8638-
- H2: Activation
8639-
- H2: Model-visible tools
8640-
- H2: exec
8641-
- H2: wait
8642-
- H2: Guest runtime API
8643-
- H2: Internal namespaces
8644-
- H3: Registry lifecycle
8645-
- H3: Registration shape
8646-
- H3: Ownership and visibility
8647-
- H3: Scope serialization rules
8648-
- H3: Prompts
8649-
- H3: Cleanup
8650-
- H3: Test checklist
8651-
- H2: Output API
8652-
- H2: Tool catalog
8653-
- H2: Tool Search interaction
8654-
- H2: Tool names and collisions
8655-
- H2: Nested tool execution
8656-
- H2: Run and snapshot lifecycle
8657-
- H2: QuickJS-WASI runtime
8658-
- H2: TypeScript
8659-
- H2: Security boundary
8660-
- H2: Error codes
8661-
- H2: Telemetry
8662-
- H2: Debugging
8663-
- H2: Implementation layout
8664-
- H2: Validation checklist
8665-
- H2: E2E test plan
8666-
- H2: Related
8667-
86688627
## reference/credits.md
86698628

86708629
- Route: /reference/credits
@@ -9674,6 +9633,52 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
96749633
- H2: Errors
96759634
- H2: Related
96769635

9636+
## tools/code-mode.md
9637+
9638+
- Route: /tools/code-mode
9639+
- Headings:
9640+
- H2: What it does
9641+
- H2: Why use it
9642+
- H2: Quickstart
9643+
- H3: Enable Code Mode
9644+
- H3: What the model does
9645+
- H3: Verify the active surface
9646+
- H2: Technical tour
9647+
- H2: Runtime status
9648+
- H2: Scope
9649+
- H2: Terms
9650+
- H2: Configuration
9651+
- H2: Activation
9652+
- H2: Model-visible tools
9653+
- H2: exec
9654+
- H2: wait
9655+
- H2: Guest runtime API
9656+
- H2: Declared output contracts
9657+
- H2: Internal namespaces
9658+
- H3: Registry lifecycle
9659+
- H3: Registration shape
9660+
- H3: Ownership and visibility
9661+
- H3: Scope serialization rules
9662+
- H3: Prompts
9663+
- H3: Cleanup
9664+
- H3: Test checklist
9665+
- H2: Output API
9666+
- H2: Tool catalog
9667+
- H2: Tool Search interaction
9668+
- H2: Tool names and collisions
9669+
- H2: Nested tool execution
9670+
- H2: Run and snapshot lifecycle
9671+
- H2: QuickJS-WASI runtime
9672+
- H2: TypeScript
9673+
- H2: Security boundary
9674+
- H2: Error codes
9675+
- H2: Telemetry
9676+
- H2: Debugging
9677+
- H2: Implementation layout
9678+
- H2: Validation checklist
9679+
- H2: E2E test plan
9680+
- H2: Related
9681+
96779682
## tools/creating-skills.md
96789683

96799684
- Route: /tools/creating-skills

docs/gateway/config-tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ The shorthand is also accepted:
111111
MCP declarations are exposed through the read-only virtual API file surface in
112112
code mode. Guest code can call `API.list("mcp")` and
113113
`API.read("mcp/<server>.d.ts")` to inspect TypeScript-style signatures before
114-
calling `MCP.<server>.<tool>()`. See [Code mode](/reference/code-mode) for the
114+
calling `MCP.<server>.<tool>()`. See [Code Mode](/tools/code-mode) for the
115115
runtime contract, limits, and debugging steps.
116116

117117
### `tools.allow` / `tools.deny`

docs/gateway/restart-recovery.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ approval), the session is not blindly re-run; the agent instead posts a short
107107
notice asking the user to resend the last request. For WebChat, that notice is
108108
written directly to the session history so it remains visible after reconnect.
109109

110-
OpenClaw can also reconstruct interrupted read-only [Code Mode](/reference/code-mode)
110+
OpenClaw can also reconstruct interrupted read-only [Code Mode](/tools/code-mode)
111111
work. Code Mode marks these runs as restart-safe and rejects side-effecting
112112
catalog tools or plugin namespaces before they execute. If a restart lands on
113113
the `wait` control, the new gateway reconstructs the turn from its transcript

docs/plugins/building-plugins.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,15 @@ local proof.
137137
name: "my_tool",
138138
description: "Echo one input value",
139139
parameters: Type.Object({ input: Type.String() }),
140+
outputSchema: Type.Object(
141+
{ input: Type.String() },
142+
{ additionalProperties: false },
143+
),
140144
async execute(_id, params) {
145+
const details = { input: params.input };
141146
return {
142147
content: [{ type: "text", text: `Got: ${params.input}` }],
148+
details,
143149
};
144150
},
145151
});
@@ -247,15 +253,28 @@ register(api) {
247253
name: "workflow_tool",
248254
description: "Run a workflow",
249255
parameters: Type.Object({ pipeline: Type.String() }),
256+
outputSchema: Type.Object(
257+
{ pipeline: Type.String() },
258+
{ additionalProperties: false },
259+
),
250260
async execute(_id, params) {
251-
return { content: [{ type: "text", text: params.pipeline }] };
261+
return {
262+
content: [{ type: "text", text: params.pipeline }],
263+
details: { pipeline: params.pipeline },
264+
};
252265
},
253266
},
254267
{ optional: true },
255268
);
256269
}
257270
```
258271

272+
`outputSchema` is optional. It describes the structured `details` value used by
273+
[Code Mode](/tools/code-mode) and [Tool Search](/tools/tool-search). Catalog
274+
calls reject invalid schemas before execution and validate the final value after
275+
tool hooks. Omit it for tools without a stable JSON result. See
276+
[Tool plugins](/plugins/tool-plugins#output-contracts) for the full contract.
277+
259278
Every tool registered with `api.registerTool(...)` must also be declared in the
260279
plugin manifest:
261280

docs/plugins/codex-harness.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ execution environment, OpenClaw keeps its policy-filtered `exec` and `process`
4545
tools available instead for direct, unsandboxed execution.
4646

4747
This Codex-native feature is separate from
48-
[OpenClaw code mode](/reference/code-mode), an opt-in QuickJS-WASI runtime
48+
[OpenClaw Code Mode](/tools/code-mode), an opt-in QuickJS-WASI runtime
4949
for generic OpenClaw runs with a different `exec` input shape. For the
5050
broader model/provider/runtime split, start with
5151
[Agent runtimes](/concepts/agent-runtimes): `openai/gpt-5.6-sol` is the model

docs/plugins/sdk-entrypoints.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ export default defineToolPlugin({
8080
parameters: Type.Object({
8181
symbol: Type.String({ description: "Ticker symbol." }),
8282
}),
83+
outputSchema: Type.Object(
84+
{
85+
symbol: Type.String(),
86+
hasKey: Type.Boolean(),
87+
},
88+
{ additionalProperties: false },
89+
),
8390
execute: async ({ symbol }, config) => ({ symbol, hasKey: Boolean(config.apiKey) }),
8491
}),
8592
],
@@ -91,6 +98,9 @@ export default defineToolPlugin({
9198
- `execute` returns a plain string or JSON-serializable value; the helper
9299
wraps it as a text tool result with `details` set to the original
93100
(unstringified) return value.
101+
- `outputSchema` optionally describes that original `details` value for Code
102+
Mode and Tool Search. Catalog calls reject an invalid schema before execution
103+
and validate the final value before returning it.
94104
- For custom tool results, `openclaw/plugin-sdk/tool-results` exports
95105
`textResult` and `jsonResult`.
96106
- Tool names are static, so `openclaw plugins build` derives

docs/plugins/tool-plugins.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ export default defineToolPlugin({
9494
parameters: Type.Object({
9595
symbol: Type.String({ description: "Ticker symbol, for example OPEN." }),
9696
}),
97+
outputSchema: Type.Object(
98+
{
99+
symbol: Type.String(),
100+
configured: Type.Boolean(),
101+
baseUrl: Type.String(),
102+
},
103+
{ additionalProperties: false },
104+
),
97105
async execute({ symbol }, config, context) {
98106
context.signal?.throwIfAborted();
99107
return {
@@ -185,6 +193,54 @@ tool({
185193
Use a factory tool when you need a custom `AgentToolResult` or want to reuse an
186194
existing `api.registerTool` implementation.
187195

196+
## Output contracts
197+
198+
Add `outputSchema` when a tool returns stable JSON-compatible data. It describes
199+
the original value stored in `AgentToolResult.details`, not the formatted text
200+
in `content`:
201+
202+
```typescript
203+
tool({
204+
name: "shipment_list",
205+
description: "List shipments.",
206+
parameters: Type.Object({
207+
buyer: Type.Optional(Type.String()),
208+
}),
209+
outputSchema: Type.Array(
210+
Type.Object(
211+
{
212+
id: Type.String(),
213+
buyer: Type.String(),
214+
paid: Type.Boolean(),
215+
tons: Type.Number(),
216+
},
217+
{ additionalProperties: false },
218+
),
219+
),
220+
execute: ({ buyer }) => listShipments(buyer),
221+
});
222+
```
223+
224+
[Code Mode](/tools/code-mode) and [Tool Search](/tools/tool-search) turn this
225+
schema into a bounded TypeScript-style output hint. That lets a model call and
226+
transform a known result in one program instead of spending another model turn
227+
observing its shape.
228+
229+
OpenClaw compiles the schema before executing a catalog call, then validates the
230+
final `details` value after tool hooks before returning it through the bridge.
231+
An invalid schema cannot run the tool; a result mismatch fails the completed
232+
call. Include every non-throwing result variant, including structured error
233+
variants, or omit the schema when the result is not stable. Do not put secrets
234+
or sensitive values in schema descriptions because trusted output metadata can
235+
become model-visible.
236+
Use `{ additionalProperties: false }` on object layers when you want a complete
237+
compact output hint; open or truncated schemas remain available through
238+
`tools.describe(...)` but are not advertised as complete quick-index contracts.
239+
240+
Factory tools declare `outputSchema` on the concrete `AnyAgentTool` they
241+
return. The static `tool({ factory })` declaration does not accept a separate
242+
output schema because it could drift from the runtime tool.
243+
188244
## Configuration
189245

190246
`configSchema` is optional. Omit it and OpenClaw applies a strict empty object

0 commit comments

Comments
 (0)