diff --git a/src/agents/code-mode.test.ts b/src/agents/code-mode.test.ts index 69e4291bbb0d..2563b9ec8e61 100644 --- a/src/agents/code-mode.test.ts +++ b/src/agents/code-mode.test.ts @@ -245,24 +245,31 @@ describe("Code Mode catalog and model-visible surface", () => { execTool.description.lastIndexOf(nodesGuidance), ); + expect(parameters.properties?.code?.description).toContain("no Python, shell"); expect(parameters.properties?.code?.description).toContain( - "`tools.search` takes a query string, not an object", - ); - expect(parameters.properties?.command?.description).toContain("Not a shell command"); - expect(parameters.properties?.code?.description).toContain( - "Select exact ids from `ALL_TOOLS` or `tools.search`", + "a trailing expression is discarded and yields `null`", ); expect(parameters.properties?.code?.description).toContain( - "never put dependent calls in Promise.all", + 'tools.callValue("openclaw:core:read", { path: "notes.txt" })', + ); + expect(parameters.properties?.code?.description).toContain("Use `callValue`, not `call`"); + expect(parameters.properties?.code?.description).toContain("return file.content"); + expect(parameters.properties?.code?.description).toContain( + "return it first, then parse it in a later exec", + ); + expect(parameters.properties?.code?.description).toContain( + "exact ids from `ALL_TOOLS` or `tools.search(query)`", ); expect(parameters.properties?.code?.description).toContain("`ALL_TOOLS`"); - expect(parameters.properties?.code?.description).toContain("Node built-in modules are not"); + expect(parameters.properties?.code?.description).toContain("`require`, `import`"); expect(parameters.properties?.restartSafe?.description).toContain( "Leave unset for ordinary calls", ); expect(parameters.properties?.language?.description).toContain( 'Must be "javascript" or "typescript"', ); + expect(parameters).toMatchObject({ required: ["code"] }); + expect(parameters.properties).not.toHaveProperty("command"); }); it("keeps code-mode exec guidance compact without advertising unavailable namespaces", () => { @@ -285,7 +292,7 @@ describe("Code Mode catalog and model-visible surface", () => { expect(execTool.description.length).toBeLessThan(2_400); expect(execTool.description).toContain("parallelize independent work only"); expect(codeDescription).toEqual(expect.any(String)); - expect(String(codeDescription).length).toBeLessThan(320); + expect(String(codeDescription).length).toBeLessThan(620); expect(codeDescription).not.toContain("MCP namespace globals"); expect(codeDescription).not.toContain("`API` virtual declaration files"); }); diff --git a/src/agents/code-mode.ts b/src/agents/code-mode.ts index 082dff97d7da..dda368c0d6e5 100644 --- a/src/agents/code-mode.ts +++ b/src/agents/code-mode.ts @@ -169,7 +169,7 @@ function createCodeModeExecDescription( nodesGuidance + skillsGuidance + ' The `language` field accepts only "javascript" or "typescript"; do not pass "bash", "shell", or other values.' + - " Both `code` and `command` contain JavaScript or TypeScript, never a shell command. " + + " The `code` field contains JavaScript or TypeScript, never a shell command. " + "For shell or file operations, call the exact catalog tool from guest JavaScript; do not retry failed shell source." + (namespacePrompt ? `\n\n${namespacePrompt}` : "") + (catalogIndex ? `\n\n${catalogIndex}` : "") @@ -182,18 +182,12 @@ export function createCodeModeTools(ctx: CodeModeToolContext): AnyAgentTool[] { label: "exec", description: createCodeModeExecDescription(ctx), parameters: Type.Object({ - code: Type.Optional( - Type.String({ - description: - "JavaScript or TypeScript for one complete workflow. Select exact ids from `ALL_TOOLS` or `tools.search`; never invent ids. `tools.search` takes a query string, not an object. Keep dependent calls in order; never put dependent calls in Promise.all. Return the final value. Node built-in modules are not available.", - }), - ), - command: Type.Optional( - Type.String({ - description: - "Alias for JavaScript or TypeScript code, provided for exec-compatible hook policies. Not a shell command.", - }), - ), + // `command` stays runtime-only for hook compatibility. Requiring the sole + // model-facing field prevents schema-valid empty calls from constrained models. + code: Type.String({ + description: + 'Required JS/TS; no Python, shell, `require`, `import`. Use explicit `return value`; a trailing expression is discarded and yields `null`. Use `callValue`, not `call`, for data; `call` wraps it under `.result`. Core text reads: `{kind:"text",content:string}`; use `.content`. Unknown format: return it first, then parse it in a later exec; never guess separators. Example: `const file=await tools.callValue("openclaw:core:read", { path: "notes.txt" }); if(file.kind!=="text") return file; return file.content;`. Use exact ids from `ALL_TOOLS` or `tools.search(query)`; never invent ids or parallelize dependent calls.', + }), language: optionalStringEnum(["javascript", "typescript"] as const, { description: 'Source language. Must be "javascript" or "typescript". Defaults to javascript.', diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index cef51ae188b2..1cea61c8663f 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -451,6 +451,21 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("sessions_send"); }); + it("describes the actual Code Mode control surface", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + toolNames: ["exec", "wait"], + codeModeActive: true, + }); + + expect(prompt).toContain( + "- exec: Run JavaScript/TypeScript Code Mode; call exact catalog tools from code, never shell/Python/imports", + ); + expect(prompt).toContain("- wait: Resume a suspended Code Mode exec"); + expect(prompt).not.toContain("- exec: Run shell"); + expect(prompt).not.toContain("Use exec yieldMs"); + }); + it("uses provider-neutral web_search prompt metadata", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 22443a361fb2..d2c02cb31484 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -837,10 +837,12 @@ export function buildAgentSystemPrompt(params: { grep: "Search file contents", find: "Find files by glob", ls: "List directories", - exec: - promptSurface === "cli_backend" + exec: params.codeModeActive + ? "Run JavaScript/TypeScript Code Mode; call exact catalog tools from code, never shell/Python/imports" + : promptSurface === "cli_backend" ? "Run shell on connected node; sync; host=node" : "Run shell; pty for TTY CLIs", + wait: "Resume a suspended Code Mode exec", process: "Control background exec", web_search: "Web search", web_fetch: "Fetch/extract URL", @@ -961,10 +963,11 @@ export function buildAgentSystemPrompt(params: { toolLines.push(summary ? `- ${name}: ${summary}` : `- ${name}`); } const toolSchemaDirectoryPrompt = params.toolSchemaDirectoryPrompt?.trim(); - const renderOpenClawToolWorkflowHints = shouldRenderOpenClawToolWorkflowHints({ - surface: promptSurface, - hasToolList: toolLines.length > 0, - }); + const renderOpenClawToolWorkflowHints = + shouldRenderOpenClawToolWorkflowHints({ + surface: promptSurface, + hasToolList: toolLines.length > 0, + }) && params.codeModeActive !== true; const hasGateway = availableTools.has("gateway"); const hasOpenClaw = availableTools.has("openclaw");