fix(agents): keep automation tool guidance consistent (#129953)

This commit is contained in:
Peter Steinberger
2026-08-26 01:36:13 -07:00
committed by GitHub
parent 4708c0b607
commit 3316bfd5ab
7 changed files with 41 additions and 33 deletions
@@ -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(
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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");
@@ -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");
+2 -3
View File
@@ -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)) {
@@ -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": {
@@ -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": {