diff --git a/src/agents/agent-tools.deferred-followup-guidance.test.ts b/src/agents/agent-tools.deferred-followup-guidance.test.ts index 2a8acbaea796..8cf81665d142 100644 --- a/src/agents/agent-tools.deferred-followup-guidance.test.ts +++ b/src/agents/agent-tools.deferred-followup-guidance.test.ts @@ -12,11 +12,11 @@ import { } from "./tool-description-presets.js"; import { createConversationsSendTool } from "./tools/conversation-tools.js"; -function findToolDescription(toolName: string, includeCron: boolean) { +function findToolDescription(toolName: string, schedulerToolName?: "automations" | "cron") { const tools = applyToolAvailabilityDescriptions([ { name: "exec", description: "exec base" }, { name: "process", description: "process base" }, - ...(includeCron ? [{ name: "cron", description: "cron base" }] : []), + ...(schedulerToolName ? [{ name: schedulerToolName, description: "scheduler base" }] : []), ] as AnyAgentTool[]); const tool = tools.find((entry) => entry.name === toolName); return { @@ -26,22 +26,25 @@ function findToolDescription(toolName: string, includeCron: boolean) { } describe("createOpenClawCodingTools availability guidance", () => { - it("keeps cron-specific guidance when cron survives filtering", () => { - const exec = findToolDescription("exec", true); - const process = findToolDescription("process", true); + it.each(["automations", "cron"] as const)( + "uses canonical automation guidance when %s survives filtering", + (schedulerToolName) => { + const exec = findToolDescription("exec", schedulerToolName); + const process = findToolDescription("process", schedulerToolName); - expect(exec.toolNames).toEqual(["exec", "process", "cron"]); - expect(exec.description).toBe( - "Run shell now; background continuation supported. Use yieldMs/background, then process for logs/status/input/intervention. Long run: automatic completion wake when enabled and output/failure occurs; otherwise process confirms completion. No sleep/delay loops for reminders/follow-ups; use cron. TTY CLI/UI/coding agent: pty=true.", - ); - expect(process.description).toBe( - "Control existing exec: list, poll, log, write, send-keys, submit, paste, kill. poll/log: status, output, quiet success, completion without auto-wake, input hints. Others: input/intervention. No polling as timer/reminder; scheduled follow-up uses cron.", - ); - }); + expect(exec.toolNames).toEqual(["exec", "process", schedulerToolName]); + expect(exec.description).toBe( + "Run shell now; background continuation supported. Use yieldMs/background, then process for logs/status/input/intervention. Long run: automatic completion wake when enabled and output/failure occurs; otherwise process confirms completion. No sleep loops for reminders/follow-ups; use automations. TTY CLI/UI/coding agent: pty=true.", + ); + expect(process.description).toBe( + "Control existing exec: list, poll, log, write, send-keys, submit, paste, kill. poll/log: status, output, quiet success, completion without auto-wake, input hints. Others: input/intervention. No polling as timer/reminder; scheduled follow-up uses automations.", + ); + }, + ); - it("drops cron-specific guidance when cron is unavailable", () => { - const exec = findToolDescription("exec", false); - const process = findToolDescription("process", false); + it("drops automation guidance when the scheduler is unavailable", () => { + const exec = findToolDescription("exec"); + const process = findToolDescription("process"); expect(exec.toolNames).toEqual(["exec", "process"]); expect(exec.description).toBe( diff --git a/src/agents/bash-tools.descriptions.ts b/src/agents/bash-tools.descriptions.ts index 1c26c23e32e7..b858d4cb3ed9 100644 --- a/src/agents/bash-tools.descriptions.ts +++ b/src/agents/bash-tools.descriptions.ts @@ -34,7 +34,7 @@ export function describeExecTool(params?: { ]; const base = [ ...continuation, - params?.hasCronTool ? "No sleep/delay loops for reminders/follow-ups; use cron." : undefined, + params?.hasCronTool ? "No sleep loops for reminders/follow-ups; use automations." : undefined, "TTY CLI/UI/coding agent: pty=true.", ] .filter(Boolean) @@ -83,7 +83,7 @@ export function describeProcessTool(params?: { hasCronTool?: boolean }): string "Control existing exec: list, poll, log, write, send-keys, submit, paste, kill.", "poll/log: status, output, quiet success, completion without auto-wake, input hints. Others: input/intervention.", params?.hasCronTool - ? "No polling as timer/reminder; scheduled follow-up uses cron." + ? "No polling as timer/reminder; scheduled follow-up uses automations." : undefined, ] .filter(Boolean) diff --git a/src/agents/bash-tools.test.ts b/src/agents/bash-tools.test.ts index a3deb0c25dec..91510da8cda1 100644 --- a/src/agents/bash-tools.test.ts +++ b/src/agents/bash-tools.test.ts @@ -688,7 +688,7 @@ const runNotifyNoopCase = async ({ label, defaults, expectNotification }: Notify }; describe("tool descriptions", () => { - it("adds cron-specific deferred follow-up guidance only when cron is available", () => { + it("adds automation follow-up guidance only when the scheduler is available", () => { const execWithCron = createTestExecTool({ hasCronTool: true }); const processWithCron = createProcessTool({ hasCronTool: true }); @@ -698,10 +698,10 @@ describe("tool descriptions", () => { expect(processWithCron.description).toContain("completion without auto-wake"); expect(processWithCron.description).toContain("write, send-keys, submit, paste, kill"); expect(execWithCron.description).toContain( - "No sleep/delay loops for reminders/follow-ups; use cron.", + "No sleep loops for reminders/follow-ups; use automations.", ); expect(processWithCron.description).toContain( - "No polling as timer/reminder; scheduled follow-up uses cron.", + "No polling as timer/reminder; scheduled follow-up uses automations.", ); expect(execTool.description).not.toContain("use cron instead"); expect(processTool.description).not.toContain("scheduled follow-ups"); diff --git a/src/agents/tools/heartbeat-response-tool.test.ts b/src/agents/tools/heartbeat-response-tool.test.ts index e10dc82c4ef8..7648a5a03071 100644 --- a/src/agents/tools/heartbeat-response-tool.test.ts +++ b/src/agents/tools/heartbeat-response-tool.test.ts @@ -32,7 +32,13 @@ describe("createHeartbeatResponseTool", () => { const outcome = readSchemaProperty(tool.parameters, "outcome"); const priority = readSchemaProperty(tool.parameters, "priority"); + const scratch = readSchemaProperty(tool.parameters, "scratch"); + expect(tool.catalogMode).toBe("direct-only"); + expect(tool.description).toContain("Scratch is monitor prose only."); + expect(tool.description).not.toMatch(/\b(?:cron|automations)\b/); + expect(JSON.stringify(tool.parameters)).not.toMatch(/\b(?:cron|automations)\b/); + expect(scratch.description).toContain("not a recurring schedule"); expect(outcome.type).toBe("string"); expect(outcome.enum).toEqual(["no_change", "progress", "done", "blocked", "needs_attention"]); expect(priority.type).toBe("string"); diff --git a/src/agents/tools/heartbeat-response-tool.ts b/src/agents/tools/heartbeat-response-tool.ts index a1bc152d07fd..6ce52ff0e99d 100644 --- a/src/agents/tools/heartbeat-response-tool.ts +++ b/src/agents/tools/heartbeat-response-tool.ts @@ -29,8 +29,7 @@ const HeartbeatResponseToolSchema = Type.Object( nextCheck: Type.Optional(Type.String()), scratch: Type.Optional( Type.String({ - description: - "Complete replacement for heartbeat monitor prose. Recurring schedules belong in automations, not scratch.", + description: "Complete replacement for heartbeat monitor prose; not a recurring schedule.", }), ), }, @@ -56,7 +55,7 @@ export function createHeartbeatResponseTool(): AnyAgentTool { catalogMode: "direct-only", displaySummary: "Accept heartbeat outcome/notify choice.", description: - "Accept heartbeat result for post-turn handling. `notify=false` no visible send. `notify=true` needs concise notificationText. Scratch is monitor prose only; recurring work belongs in automations.", + "Accept heartbeat result for post-turn handling. `notify=false` no visible send. `notify=true` needs concise notificationText. Scratch is monitor prose only.", parameters: HeartbeatResponseToolSchema, execute: async (_toolCallId, args) => { if (!isRecord(args)) { 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 d49a6fd676f7..5dc5f6a13073 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 @@ -6,7 +6,7 @@ "name": "openclaw_direct", "tools": [ { - "description": "Accept heartbeat result for post-turn handling. `notify=false` no visible send. `notify=true` needs concise notificationText. Scratch is monitor prose only; recurring work belongs in automations.", + "description": "Accept heartbeat result for post-turn handling. `notify=false` no visible send. `notify=true` needs concise notificationText. Scratch is monitor prose only.", "inputSchema": { "additionalProperties": false, "properties": { @@ -31,7 +31,7 @@ "type": "string" }, "scratch": { - "description": "Complete replacement for heartbeat monitor prose. Recurring schedules belong in automations, not scratch.", + "description": "Complete replacement for heartbeat monitor prose; not a recurring schedule.", "type": "string" }, "summary": { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md.diff b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md.diff index 75e1969561ca..241b6268b3d4 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md.diff +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md.diff @@ -1,5 +1,5 @@ --- telegram-direct-codex-message-tool.md sha256=0a75703590d0c2198fdadbf2e427bd7f3242ebfb7d10ae339585e17f566be58c -+++ telegram-heartbeat-codex-tool.md sha256=c2205edcd94a0c5735c4ef1d6250a10ae111b3df58d53583a13d6bb197104fa0 ++++ telegram-heartbeat-codex-tool.md sha256=3d557f5b83324ad5ff9ab39c3fed596c3aaf935f084674a0a620d452a1102489 @@ -1,1 +1,1 @@ -# Telegram Direct Codex Message Tool Turn +# Telegram Direct Codex Heartbeat Tool Turn @@ -34,8 +34,8 @@ @@ -235,2 +230,2 @@ - "chars": 54893, - "roughTokens": 13724 -+ "chars": 56455, -+ "roughTokens": 14114 ++ "chars": 56386, ++ "roughTokens": 14097 @@ -243,2 +238,2 @@ - "chars": 27336, - "roughTokens": 6834 @@ -44,8 +44,8 @@ @@ -247,2 +242,2 @@ - "chars": 82231, - "roughTokens": 20558 -+ "chars": 84135, -+ "roughTokens": 21034 ++ "chars": 84066, ++ "roughTokens": 21017 @@ -251,2 +246,2 @@ - "chars": 863, - "roughTokens": 216 @@ -65,7 +65,7 @@ @@ -699,0 +696,39 @@ + }, + { -+ "description": "Accept heartbeat result for post-turn handling. `notify=false` no visible send. `notify=true` needs concise notificationText. Scratch is monitor prose only; recurring work belongs in automations.", ++ "description": "Accept heartbeat result for post-turn handling. `notify=false` no visible send. `notify=true` needs concise notificationText. Scratch is monitor prose only.", + "inputSchema": { + "additionalProperties": false, + "properties": { @@ -90,7 +90,7 @@ + "type": "string" + }, + "scratch": { -+ "description": "Complete replacement for heartbeat monitor prose. Recurring schedules belong in automations, not scratch.", ++ "description": "Complete replacement for heartbeat monitor prose; not a recurring schedule.", + "type": "string" + }, + "summary": {