diff --git a/extensions/codex/src/app-server/dynamic-tools.test.ts b/extensions/codex/src/app-server/dynamic-tools.test.ts index 9954c8633a14..2bd7ce28ffb9 100644 --- a/extensions/codex/src/app-server/dynamic-tools.test.ts +++ b/extensions/codex/src/app-server/dynamic-tools.test.ts @@ -178,12 +178,13 @@ afterEach(() => { }); describe("createCodexDynamicToolBridge", () => { - it("keeps turn-yield direct while deferring OpenClaw session spawn", () => { + it("keeps OpenClaw control-path tools direct while deferring broad tools", () => { const bridge = createCodexDynamicToolBridge({ tools: [ createTool({ name: "web_search" }), createTool({ name: "message" }), createTool({ name: HEARTBEAT_RESPONSE_TOOL_NAME }), + createTool({ name: "agents_list" }), createTool({ name: "sessions_spawn" }), createTool({ name: "sessions_yield" }), ], @@ -194,6 +195,7 @@ describe("createCodexDynamicToolBridge", () => { const webSearch = specs.find((tool) => tool.name === "web_search"); const message = specs.find((tool) => tool.name === "message"); const heartbeat = specs.find((tool) => tool.name === HEARTBEAT_RESPONSE_TOOL_NAME); + const agentsList = specs.find((tool) => tool.name === "agents_list"); const sessionsSpawn = specs.find((tool) => tool.name === "sessions_spawn"); const sessionsYield = specs.find((tool) => tool.name === "sessions_yield"); @@ -212,11 +214,8 @@ describe("createCodexDynamicToolBridge", () => { namespace: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE, deferLoading: true, }); - expectDynamicSpec(sessionsSpawn, { - name: "sessions_spawn", - namespace: CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE, - deferLoading: true, - }); + expectNoNamespace(agentsList); + expectNoNamespace(sessionsSpawn); expectNoNamespace(sessionsYield); }); diff --git a/extensions/codex/src/app-server/dynamic-tools.ts b/extensions/codex/src/app-server/dynamic-tools.ts index 605aaadb8adc..0dbb89ca0957 100644 --- a/extensions/codex/src/app-server/dynamic-tools.ts +++ b/extensions/codex/src/app-server/dynamic-tools.ts @@ -353,9 +353,14 @@ export type CodexDynamicToolBridge = { /** Namespace attached to OpenClaw-owned dynamic tools exposed to Codex. */ export const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw"; -// Keep OpenClaw session spawning searchable in Codex mode so Codex's native -// spawn_agent remains the primary Codex subagent surface. -const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = new Set(["sessions_yield"]); +// Keep OpenClaw control-path tools directly callable even when Codex tool_search +// is unavailable or resolves a connector-only universe. Developer instructions +// still steer normal Codex subagents to native spawn_agent. +const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = new Set([ + "agents_list", + "sessions_spawn", + "sessions_yield", +]); const EXPLICIT_MESSAGE_PROVIDER_KEYS = ["channel", "provider"]; const EXPLICIT_MESSAGE_TARGET_KEYS = ["target", "to", "channelId"]; const EXPLICIT_MESSAGE_THREAD_KEYS = ["threadId", "thread_id", "messageThreadId", "topicId"]; diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 70b206ba7f22..14be7527845e 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -1594,11 +1594,12 @@ describe("runCodexAppServerAttempt", () => { expect(dynamicToolNames).toEqual(["message"]); }); - it("keeps searchable OpenClaw dynamic tools when code-mode-only is enabled", () => { + it("keeps OpenClaw control-path tools direct when code-mode-only is enabled", () => { const tools = [ createRuntimeDynamicTool("message"), createRuntimeDynamicTool("web_search"), createRuntimeDynamicTool("heartbeat_respond"), + createRuntimeDynamicTool("agents_list"), createRuntimeDynamicTool("sessions_spawn"), createRuntimeDynamicTool("sessions_yield"), ]; @@ -1612,6 +1613,7 @@ describe("runCodexAppServerAttempt", () => { const message = specs.find((tool) => tool.name === "message"); const webSearch = specs.find((tool) => tool.name === "web_search"); const heartbeat = specs.find((tool) => tool.name === "heartbeat_respond"); + const agentsList = specs.find((tool) => tool.name === "agents_list"); const sessionsSpawn = specs.find((tool) => tool.name === "sessions_spawn"); const sessionsYield = specs.find((tool) => tool.name === "sessions_yield"); @@ -1621,13 +1623,15 @@ describe("runCodexAppServerAttempt", () => { expect(webSearch?.deferLoading).toBe(true); expect(heartbeat?.namespace).toBe(CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE); expect(heartbeat?.deferLoading).toBe(true); - expect(sessionsSpawn?.namespace).toBe(CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE); - expect(sessionsSpawn?.deferLoading).toBe(true); + expect(agentsList).not.toHaveProperty("namespace"); + expect(agentsList).not.toHaveProperty("deferLoading"); + expect(sessionsSpawn).not.toHaveProperty("namespace"); + expect(sessionsSpawn).not.toHaveProperty("deferLoading"); expect(sessionsYield).not.toHaveProperty("namespace"); expect(sessionsYield).not.toHaveProperty("deferLoading"); }); - it("registers heartbeat response durably without advertising it on normal turns", async () => { + it("keeps the heartbeat schema deferred and stable across normal and heartbeat turns", async () => { testing.setOpenClawCodingToolsFactoryForTests((options) => [ createRuntimeDynamicTool("message"), ...(options?.enableHeartbeatTool === true @@ -1640,12 +1644,10 @@ describe("runCodexAppServerAttempt", () => { const params = createParams(sessionFile, workspaceDir); params.disableTools = false; params.runtimePlan = createCodexRuntimePlanFixture(); + params.sourceReplyDeliveryMode = "message_tool_only"; if (trigger) { params.trigger = trigger; } - if (trigger === "heartbeat") { - params.sourceReplyDeliveryMode = "message_tool_only"; - } return params; }; @@ -1661,30 +1663,76 @@ describe("runCodexAppServerAttempt", () => { const normalInstructions = testing.buildDeveloperInstructions(createRunParams(), { dynamicTools: normalBridge.availableSpecs, }); + const heartbeatParams = createRunParams("heartbeat"); + const heartbeatBridge = createCodexToolBridgeForTest( + heartbeatParams, + [createRuntimeDynamicTool("message"), createRuntimeDynamicTool("heartbeat_respond")], + registeredTools, + ); + const heartbeatInstructions = testing.buildDeveloperInstructions(heartbeatParams, { + dynamicTools: heartbeatBridge.availableSpecs, + }); + const nextNormalParams = createRunParams(); + const nextNormalBridge = createCodexToolBridgeForTest( + nextNormalParams, + [createRuntimeDynamicTool("message")], + registeredTools, + ); const registeredToolNames = specNames(normalBridge.specs); expect(registeredToolNames).toContain("message"); expect(registeredToolNames).toContain("heartbeat_respond"); - expect(normalInstructions).toContain( - "Deferred searchable OpenClaw dynamic tools available: message.", - ); expect(normalInstructions).not.toContain( "Deferred searchable OpenClaw dynamic tools available: heartbeat_respond", ); - - const heartbeatBridge = createCodexToolBridgeForTest( - createRunParams("heartbeat"), - [createRuntimeDynamicTool("message"), createRuntimeDynamicTool("heartbeat_respond")], - registeredTools, + expect(heartbeatInstructions).toContain( + "Deferred searchable OpenClaw dynamic tools available: heartbeat_respond.", ); - const nextNormalBridge = createCodexToolBridgeForTest( - createRunParams(), - [createRuntimeDynamicTool("message")], - registeredTools, + for (const bridge of [normalBridge, heartbeatBridge, nextNormalBridge]) { + const heartbeat = flattenSpecsWithNamespace(bridge.specs).find( + (tool) => tool.name === "heartbeat_respond", + ); + expect(heartbeat?.namespace).toBe(CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE); + expect(heartbeat?.deferLoading).toBe(true); + } + expect(codexDynamicToolsFingerprint(heartbeatBridge.specs)).toBe( + codexDynamicToolsFingerprint(normalBridge.specs), + ); + expect(codexDynamicToolsFingerprint(nextNormalBridge.specs)).toBe( + codexDynamicToolsFingerprint(normalBridge.specs), ); - expect(specNames(heartbeatBridge.specs)).toEqual(registeredToolNames); - expect(specNames(nextNormalBridge.specs)).toEqual(registeredToolNames); + let startedThreadId: string | undefined; + const request = vi.fn(async (method: string) => { + if (method === "thread/start") { + startedThreadId = "thread-stable-heartbeat"; + return threadStartResult(startedThreadId); + } + if (method === "thread/resume") { + return threadStartResult(startedThreadId); + } + throw new Error(`unexpected method: ${method}`); + }); + const turns = [ + { params: createRunParams(), bridge: normalBridge }, + { params: heartbeatParams, bridge: heartbeatBridge }, + { params: nextNormalParams, bridge: nextNormalBridge }, + ]; + for (const turn of turns) { + await startOrResumeThread({ + client: { request } as never, + params: turn.params, + cwd: workspaceDir, + dynamicTools: turn.bridge.specs, + appServer: createThreadLifecycleAppServerOptions(), + }); + } + + expect(request.mock.calls.map(([method]) => method)).toEqual([ + "thread/start", + "thread/resume", + "thread/resume", + ]); }); it("keeps message in the registered schema when disabled for an internal turn", async () => { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json index 9e4ae09793c4..00260b8c5901 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json @@ -124,6 +124,108 @@ "name": "message", "type": "function" }, + { + "description": "List agent ids allowed for `sessions_spawn runtime=\"subagent\"`.", + "inputSchema": { + "properties": {}, + "type": "object" + }, + "name": "agents_list", + "type": "function" + }, + { + "description": "Spawn clean child session; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot; `mode=\"session\"` persistent/thread-bound, only when requester channel supports thread bindings. Subagents inherit parent workspace. Native subagents get task in first visible `[Subagent Task]` message. Native only: `context=\"fork\"` only when child needs current transcript; else omit or `isolated`. Use for fresh child-session work. Delegate sidecar/parallel tasks: batch file reads, multi-step searches, data collection. Avoid delegating quick lookups or single-file reads unless policy prefers delegation. After spawning, do non-overlapping work; run-mode results return, session-mode output stays in thread.", + "inputSchema": { + "properties": { + "agentId": { + "type": "string" + }, + "attachAs": { + "properties": { + "mountPath": { + "type": "string" + } + }, + "type": "object" + }, + "attachments": { + "items": { + "properties": { + "content": { + "type": "string" + }, + "encoding": { + "enum": ["utf8", "base64"], + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["name", "content"], + "type": "object" + }, + "maxItems": 50, + "type": "array" + }, + "cleanup": { + "enum": ["delete", "keep"], + "type": "string" + }, + "context": { + "description": "Native context. Omit/\"isolated\" for clean child; \"fork\" only when child needs requester transcript.", + "enum": ["isolated", "fork"], + "type": "string" + }, + "cwd": { + "type": "string" + }, + "label": { + "type": "string" + }, + "lightContext": { + "description": "Light bootstrap context; runtime=\"subagent\" only.", + "type": "boolean" + }, + "mode": { + "enum": ["run", "session"], + "type": "string" + }, + "model": { + "type": "string" + }, + "runtime": { + "enum": ["subagent"], + "type": "string" + }, + "sandbox": { + "enum": ["inherit", "require"], + "type": "string" + }, + "task": { + "type": "string" + }, + "taskName": { + "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", + "type": "string" + }, + "thinking": { + "type": "string" + }, + "thread": { + "description": "Bind spawn to new chat thread when supported. `thread=true` defaults mode=\"session\".", + "type": "boolean" + } + }, + "required": ["task"], + "type": "object" + }, + "name": "sessions_spawn", + "type": "function" + }, { "description": "End current turn. Use after spawning subagents; results arrive as next message.", "inputSchema": { @@ -1001,16 +1103,6 @@ "name": "gateway", "type": "function" }, - { - "deferLoading": true, - "description": "List agent ids allowed for `sessions_spawn runtime=\"subagent\"`.", - "inputSchema": { - "properties": {}, - "type": "object" - }, - "name": "agents_list", - "type": "function" - }, { "deferLoading": true, "description": "List visible sessions; filter by kind, label, agentId, search, activity. Use before sessions_history or sessions_send target selection.", @@ -1117,100 +1209,6 @@ "name": "sessions_send", "type": "function" }, - { - "deferLoading": true, - "description": "Spawn clean child session; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot; `mode=\"session\"` persistent/thread-bound, only when requester channel supports thread bindings. Subagents inherit parent workspace. Native subagents get task in first visible `[Subagent Task]` message. Native only: `context=\"fork\"` only when child needs current transcript; else omit or `isolated`. Use for fresh child-session work. Delegate sidecar/parallel tasks: batch file reads, multi-step searches, data collection. Avoid delegating quick lookups or single-file reads unless policy prefers delegation. After spawning, do non-overlapping work; run-mode results return, session-mode output stays in thread.", - "inputSchema": { - "properties": { - "agentId": { - "type": "string" - }, - "attachAs": { - "properties": { - "mountPath": { - "type": "string" - } - }, - "type": "object" - }, - "attachments": { - "items": { - "properties": { - "content": { - "type": "string" - }, - "encoding": { - "enum": ["utf8", "base64"], - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["name", "content"], - "type": "object" - }, - "maxItems": 50, - "type": "array" - }, - "cleanup": { - "enum": ["delete", "keep"], - "type": "string" - }, - "context": { - "description": "Native context. Omit/\"isolated\" for clean child; \"fork\" only when child needs requester transcript.", - "enum": ["isolated", "fork"], - "type": "string" - }, - "cwd": { - "type": "string" - }, - "label": { - "type": "string" - }, - "lightContext": { - "description": "Light bootstrap context; runtime=\"subagent\" only.", - "type": "boolean" - }, - "mode": { - "enum": ["run", "session"], - "type": "string" - }, - "model": { - "type": "string" - }, - "runtime": { - "enum": ["subagent"], - "type": "string" - }, - "sandbox": { - "enum": ["inherit", "require"], - "type": "string" - }, - "task": { - "type": "string" - }, - "taskName": { - "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", - "type": "string" - }, - "thinking": { - "type": "string" - }, - "thread": { - "description": "Bind spawn to new chat thread when supported. `thread=true` defaults mode=\"session\".", - "type": "boolean" - } - }, - "required": ["task"], - "type": "object" - }, - "name": "sessions_spawn", - "type": "function" - }, { "deferLoading": true, "description": "List active and recent subagents for the requester session. If sessions_yield exists, use it for completion; do not poll wait loops.", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json index 9051bdc5a940..247f80fb0286 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json @@ -124,6 +124,104 @@ "name": "message", "type": "function" }, + { + "description": "List agent ids allowed for `sessions_spawn runtime=\"subagent\"`.", + "inputSchema": { + "properties": {}, + "type": "object" + }, + "name": "agents_list", + "type": "function" + }, + { + "description": "Spawn clean child session; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background work. Subagents inherit parent workspace. Native subagents get task in first visible `[Subagent Task]` message. Native only: `context=\"fork\"` only when child needs current transcript; else omit or `isolated`. Use for fresh child-session work. Delegate sidecar/parallel tasks: batch file reads, multi-step searches, data collection. Avoid delegating quick lookups or single-file reads unless policy prefers delegation. After spawning, do non-overlapping work while run-mode results return.", + "inputSchema": { + "properties": { + "agentId": { + "type": "string" + }, + "attachAs": { + "properties": { + "mountPath": { + "type": "string" + } + }, + "type": "object" + }, + "attachments": { + "items": { + "properties": { + "content": { + "type": "string" + }, + "encoding": { + "enum": ["utf8", "base64"], + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["name", "content"], + "type": "object" + }, + "maxItems": 50, + "type": "array" + }, + "cleanup": { + "enum": ["delete", "keep"], + "type": "string" + }, + "context": { + "description": "Native context. Omit/\"isolated\" for clean child; \"fork\" only when child needs requester transcript.", + "enum": ["isolated", "fork"], + "type": "string" + }, + "cwd": { + "type": "string" + }, + "label": { + "type": "string" + }, + "lightContext": { + "description": "Light bootstrap context; runtime=\"subagent\" only.", + "type": "boolean" + }, + "mode": { + "enum": ["run"], + "type": "string" + }, + "model": { + "type": "string" + }, + "runtime": { + "enum": ["subagent"], + "type": "string" + }, + "sandbox": { + "enum": ["inherit", "require"], + "type": "string" + }, + "task": { + "type": "string" + }, + "taskName": { + "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", + "type": "string" + }, + "thinking": { + "type": "string" + } + }, + "required": ["task"], + "type": "object" + }, + "name": "sessions_spawn", + "type": "function" + }, { "description": "End current turn. Use after spawning subagents; results arrive as next message.", "inputSchema": { @@ -1037,16 +1135,6 @@ "name": "gateway", "type": "function" }, - { - "deferLoading": true, - "description": "List agent ids allowed for `sessions_spawn runtime=\"subagent\"`.", - "inputSchema": { - "properties": {}, - "type": "object" - }, - "name": "agents_list", - "type": "function" - }, { "deferLoading": true, "description": "List visible sessions; filter by kind, label, agentId, search, activity. Use before sessions_history or sessions_send target selection.", @@ -1153,96 +1241,6 @@ "name": "sessions_send", "type": "function" }, - { - "deferLoading": true, - "description": "Spawn clean child session; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background work. Subagents inherit parent workspace. Native subagents get task in first visible `[Subagent Task]` message. Native only: `context=\"fork\"` only when child needs current transcript; else omit or `isolated`. Use for fresh child-session work. Delegate sidecar/parallel tasks: batch file reads, multi-step searches, data collection. Avoid delegating quick lookups or single-file reads unless policy prefers delegation. After spawning, do non-overlapping work while run-mode results return.", - "inputSchema": { - "properties": { - "agentId": { - "type": "string" - }, - "attachAs": { - "properties": { - "mountPath": { - "type": "string" - } - }, - "type": "object" - }, - "attachments": { - "items": { - "properties": { - "content": { - "type": "string" - }, - "encoding": { - "enum": ["utf8", "base64"], - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["name", "content"], - "type": "object" - }, - "maxItems": 50, - "type": "array" - }, - "cleanup": { - "enum": ["delete", "keep"], - "type": "string" - }, - "context": { - "description": "Native context. Omit/\"isolated\" for clean child; \"fork\" only when child needs requester transcript.", - "enum": ["isolated", "fork"], - "type": "string" - }, - "cwd": { - "type": "string" - }, - "label": { - "type": "string" - }, - "lightContext": { - "description": "Light bootstrap context; runtime=\"subagent\" only.", - "type": "boolean" - }, - "mode": { - "enum": ["run"], - "type": "string" - }, - "model": { - "type": "string" - }, - "runtime": { - "enum": ["subagent"], - "type": "string" - }, - "sandbox": { - "enum": ["inherit", "require"], - "type": "string" - }, - "task": { - "type": "string" - }, - "taskName": { - "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", - "type": "string" - }, - "thinking": { - "type": "string" - } - }, - "required": ["task"], - "type": "object" - }, - "name": "sessions_spawn", - "type": "function" - }, { "deferLoading": true, "description": "List active and recent subagents for the requester session. If sessions_yield exists, use it for completion; do not poll wait loops.", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json index d2b6062e7012..3894a04d7894 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json @@ -124,6 +124,104 @@ "name": "message", "type": "function" }, + { + "description": "List agent ids allowed for `sessions_spawn runtime=\"subagent\"`.", + "inputSchema": { + "properties": {}, + "type": "object" + }, + "name": "agents_list", + "type": "function" + }, + { + "description": "Spawn clean child session; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background work. Subagents inherit parent workspace. Native subagents get task in first visible `[Subagent Task]` message. Native only: `context=\"fork\"` only when child needs current transcript; else omit or `isolated`. Use for fresh child-session work. Delegate sidecar/parallel tasks: batch file reads, multi-step searches, data collection. Avoid delegating quick lookups or single-file reads unless policy prefers delegation. After spawning, do non-overlapping work while run-mode results return.", + "inputSchema": { + "properties": { + "agentId": { + "type": "string" + }, + "attachAs": { + "properties": { + "mountPath": { + "type": "string" + } + }, + "type": "object" + }, + "attachments": { + "items": { + "properties": { + "content": { + "type": "string" + }, + "encoding": { + "enum": ["utf8", "base64"], + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["name", "content"], + "type": "object" + }, + "maxItems": 50, + "type": "array" + }, + "cleanup": { + "enum": ["delete", "keep"], + "type": "string" + }, + "context": { + "description": "Native context. Omit/\"isolated\" for clean child; \"fork\" only when child needs requester transcript.", + "enum": ["isolated", "fork"], + "type": "string" + }, + "cwd": { + "type": "string" + }, + "label": { + "type": "string" + }, + "lightContext": { + "description": "Light bootstrap context; runtime=\"subagent\" only.", + "type": "boolean" + }, + "mode": { + "enum": ["run"], + "type": "string" + }, + "model": { + "type": "string" + }, + "runtime": { + "enum": ["subagent"], + "type": "string" + }, + "sandbox": { + "enum": ["inherit", "require"], + "type": "string" + }, + "task": { + "type": "string" + }, + "taskName": { + "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", + "type": "string" + }, + "thinking": { + "type": "string" + } + }, + "required": ["task"], + "type": "object" + }, + "name": "sessions_spawn", + "type": "function" + }, { "description": "End current turn. Use after spawning subagents; results arrive as next message.", "inputSchema": { @@ -1001,16 +1099,6 @@ "name": "gateway", "type": "function" }, - { - "deferLoading": true, - "description": "List agent ids allowed for `sessions_spawn runtime=\"subagent\"`.", - "inputSchema": { - "properties": {}, - "type": "object" - }, - "name": "agents_list", - "type": "function" - }, { "deferLoading": true, "description": "List visible sessions; filter by kind, label, agentId, search, activity. Use before sessions_history or sessions_send target selection.", @@ -1117,96 +1205,6 @@ "name": "sessions_send", "type": "function" }, - { - "deferLoading": true, - "description": "Spawn clean child session; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background work. Subagents inherit parent workspace. Native subagents get task in first visible `[Subagent Task]` message. Native only: `context=\"fork\"` only when child needs current transcript; else omit or `isolated`. Use for fresh child-session work. Delegate sidecar/parallel tasks: batch file reads, multi-step searches, data collection. Avoid delegating quick lookups or single-file reads unless policy prefers delegation. After spawning, do non-overlapping work while run-mode results return.", - "inputSchema": { - "properties": { - "agentId": { - "type": "string" - }, - "attachAs": { - "properties": { - "mountPath": { - "type": "string" - } - }, - "type": "object" - }, - "attachments": { - "items": { - "properties": { - "content": { - "type": "string" - }, - "encoding": { - "enum": ["utf8", "base64"], - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": ["name", "content"], - "type": "object" - }, - "maxItems": 50, - "type": "array" - }, - "cleanup": { - "enum": ["delete", "keep"], - "type": "string" - }, - "context": { - "description": "Native context. Omit/\"isolated\" for clean child; \"fork\" only when child needs requester transcript.", - "enum": ["isolated", "fork"], - "type": "string" - }, - "cwd": { - "type": "string" - }, - "label": { - "type": "string" - }, - "lightContext": { - "description": "Light bootstrap context; runtime=\"subagent\" only.", - "type": "boolean" - }, - "mode": { - "enum": ["run"], - "type": "string" - }, - "model": { - "type": "string" - }, - "runtime": { - "enum": ["subagent"], - "type": "string" - }, - "sandbox": { - "enum": ["inherit", "require"], - "type": "string" - }, - "task": { - "type": "string" - }, - "taskName": { - "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", - "type": "string" - }, - "thinking": { - "type": "string" - } - }, - "required": ["task"], - "type": "object" - }, - "name": "sessions_spawn", - "type": "function" - }, { "deferLoading": true, "description": "List active and recent subagents for the requester session. If sessions_yield exists, use it for completion; do not poll wait loops.", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 6d66a29c7879..84ce7914e9b0 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -88,16 +88,16 @@ "developerInstructions": "", "dynamicTools": [ "message", + "agents_list", + "sessions_spawn", "sessions_yield", "nodes", "cron", "tts", "gateway", - "agents_list", "sessions_list", "sessions_history", "sessions_send", - "sessions_spawn", "subagents", "session_status", "web_search", @@ -227,20 +227,20 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 50951, - "roughTokens": 12738 + "chars": 50395, + "roughTokens": 12599 }, "openClawDeveloperInstructions": { - "chars": 3074, - "roughTokens": 769 + "chars": 3045, + "roughTokens": 762 }, "totalTextOnly": { - "chars": 27692, - "roughTokens": 6923 + "chars": 27663, + "roughTokens": 6916 }, "totalWithDynamicToolsJson": { - "chars": 78645, - "roughTokens": 19662 + "chars": 78060, + "roughTokens": 19515 }, "userInputText": { "chars": 1535, @@ -427,7 +427,7 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for ````text You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes. -Deferred searchable OpenClaw dynamic tools available: agents_list, cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_send, sessions_spawn, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. +Deferred searchable OpenClaw dynamic tools available: cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. Use Codex native `spawn_agent` for Codex subagents. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation. @@ -560,16 +560,16 @@ Full JSON: `codex-dynamic-tools.discord-group.json` ```json [ "message", + "agents_list", + "sessions_spawn", "sessions_yield", "nodes", "cron", "tts", "gateway", - "agents_list", "sessions_list", "sessions_history", "sessions_send", - "sessions_spawn", "subagents", "session_status", "web_search", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index b6bb3cf64458..7a82f23ce45a 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -88,16 +88,16 @@ "developerInstructions": "", "dynamicTools": [ "message", + "agents_list", + "sessions_spawn", "sessions_yield", "nodes", "cron", "tts", "gateway", - "agents_list", "sessions_list", "sessions_history", "sessions_send", - "sessions_spawn", "subagents", "session_status", "web_search", @@ -227,20 +227,20 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 50620, - "roughTokens": 12655 + "chars": 50084, + "roughTokens": 12521 }, "openClawDeveloperInstructions": { - "chars": 1965, - "roughTokens": 492 + "chars": 1936, + "roughTokens": 484 }, "totalTextOnly": { - "chars": 26081, - "roughTokens": 6521 + "chars": 26052, + "roughTokens": 6513 }, "totalWithDynamicToolsJson": { - "chars": 76703, - "roughTokens": 19176 + "chars": 76138, + "roughTokens": 19035 }, "userInputText": { "chars": 1033, @@ -427,7 +427,7 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for ````text You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes. -Deferred searchable OpenClaw dynamic tools available: agents_list, cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_send, sessions_spawn, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. +Deferred searchable OpenClaw dynamic tools available: cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. Use Codex native `spawn_agent` for Codex subagents. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation. @@ -537,16 +537,16 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` ```json [ "message", + "agents_list", + "sessions_spawn", "sessions_yield", "nodes", "cron", "tts", "gateway", - "agents_list", "sessions_list", "sessions_history", "sessions_send", - "sessions_spawn", "subagents", "session_status", "web_search", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 9139072ef71e..72a9e6275359 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -88,17 +88,17 @@ "developerInstructions": "", "dynamicTools": [ "message", + "agents_list", + "sessions_spawn", "sessions_yield", "nodes", "cron", "heartbeat_respond", "tts", "gateway", - "agents_list", "sessions_list", "sessions_history", "sessions_send", - "sessions_spawn", "subagents", "session_status", "web_search", @@ -228,20 +228,20 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 51910, - "roughTokens": 12978 + "chars": 51374, + "roughTokens": 12844 }, "openClawDeveloperInstructions": { - "chars": 1984, - "roughTokens": 496 + "chars": 1955, + "roughTokens": 489 }, "totalTextOnly": { - "chars": 27024, - "roughTokens": 6756 + "chars": 26995, + "roughTokens": 6749 }, "totalWithDynamicToolsJson": { - "chars": 78936, - "roughTokens": 19734 + "chars": 78371, + "roughTokens": 19593 }, "userInputText": { "chars": 1271, @@ -428,7 +428,7 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for ````text You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes. -Deferred searchable OpenClaw dynamic tools available: agents_list, cron, gateway, heartbeat_respond, nodes, session_status, sessions_history, sessions_list, sessions_send, sessions_spawn, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. +Deferred searchable OpenClaw dynamic tools available: cron, gateway, heartbeat_respond, nodes, session_status, sessions_history, sessions_list, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. Use Codex native `spawn_agent` for Codex subagents. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation. @@ -547,17 +547,17 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` ```json [ "message", + "agents_list", + "sessions_spawn", "sessions_yield", "nodes", "cron", "heartbeat_respond", "tts", "gateway", - "agents_list", "sessions_list", "sessions_history", "sessions_send", - "sessions_spawn", "subagents", "session_status", "web_search",