diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index 3a1151638b7..f98ef8cdeeb 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -178,7 +178,7 @@ Per-agent overrides use `agents.list[].subagents.delegationMode`. The task description for the sub-agent. - Optional stable handle for identifying a specific child in later status output. Must match `[a-z][a-z0-9_]{0,63}` and cannot be reserved targets such as `last` or `all`. + Optional stable handle for identifying a specific child in later status output. Must match `[a-z][a-z0-9_-]{0,63}` and cannot be reserved targets such as `last` or `all`. Optional human-readable label. diff --git a/src/agents/subagent-task-name.ts b/src/agents/subagent-task-name.ts index 28182bebf8b..d008cc2b517 100644 --- a/src/agents/subagent-task-name.ts +++ b/src/agents/subagent-task-name.ts @@ -1,6 +1,6 @@ import { normalizeOptionalString } from "../shared/string-coerce.js"; -const SUBAGENT_TASK_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/; +const SUBAGENT_TASK_NAME_RE = /^[a-z][a-z0-9_-]{0,63}$/; const RESERVED_SUBAGENT_TASK_NAMES = new Set(["all", "last"]); type NormalizeSubagentTaskNameResult = @@ -14,7 +14,7 @@ export function normalizeSubagentTaskName(value: unknown): NormalizeSubagentTask } if (!SUBAGENT_TASK_NAME_RE.test(taskName)) { return { - error: `Invalid taskName "${taskName}". Use 1-64 chars matching [a-z][a-z0-9_]*.`, + error: `Invalid taskName "${taskName}". Use 1-64 chars matching [a-z][a-z0-9_-]*.`, }; } if (RESERVED_SUBAGENT_TASK_NAMES.has(taskName)) { diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index b7d3372554e..0bf80c23e36 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -866,6 +866,7 @@ describe("buildAgentSystemPrompt", () => { "Anything requiring more work than a direct reply should go through `sessions_spawn`", ); expect(preferPrompt).toContain("objective, expected output, relevant files/inputs"); + expect(preferPrompt).toContain("keep it lowercase with underscores or hyphens"); expect(preferPrompt).toContain("Treat child outputs as reports/evidence"); expect(preferPrompt).toContain( "Use `subagents(action=list)` only when explicitly asked for sub-agent status", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 0dcb85bffd8..3cf952de2d3 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -96,7 +96,7 @@ function buildSubagentDelegationPreferenceSection(params: { "- Anything requiring more work than a direct reply should go through `sessions_spawn`; avoid doing expensive tool calls yourself.", "- Delegate file/code inspection, shell commands, web/browser use, long reads, debugging, coding, multi-step analysis, comparisons, non-trivial summarization, and background waiting.", "- Before spawning, decide what stays local and what is delegated. Give each child a clear objective, expected output, relevant files/inputs, write scope, verification ask, and whether it blocks your final answer.", - '- Set `taskName` when you will need a stable handle later; keep it lowercase with underscores. Omit `context` for isolated children; set `context:"fork"` only when current transcript details matter.', + '- Set `taskName` when you will need a stable handle later; keep it lowercase with underscores or hyphens. Omit `context` for isolated children; set `context:"fork"` only when current transcript details matter.', params.hasSessionsYield ? "- After spawning required work, call `sessions_yield` if you need completion events before answering. Do not poll for completion." : "- After spawning, do not poll for completion. Child completion is push-based and returns as a runtime event; synthesize that result for the user.", diff --git a/src/agents/tools/sessions-spawn-tool.test.ts b/src/agents/tools/sessions-spawn-tool.test.ts index bd67d8eab7d..91798b1aa04 100644 --- a/src/agents/tools/sessions-spawn-tool.test.ts +++ b/src/agents/tools/sessions-spawn-tool.test.ts @@ -355,7 +355,7 @@ describe("sessions_spawn tool", () => { const result = await tool.execute("call-task-name", { task: "review subagent handling", - taskName: "review_subagents", + taskName: "review-subagents", }); expectDetailFields(result.details, { @@ -364,24 +364,45 @@ describe("sessions_spawn tool", () => { }); const spawnArgs = mockCallArg(hoisted.spawnSubagentDirectMock, 0, 0, "spawnSubagentDirect"); expect(spawnArgs.task).toBe("review subagent handling"); - expect(spawnArgs.taskName).toBe("review_subagents"); + expect(spawnArgs.taskName).toBe("review-subagents"); }); - it("rejects invalid taskName before spawning", async () => { + it("accepts underscore taskName aliases", async () => { const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main", }); - const result = await tool.execute("call-bad-task-name", { + const result = await tool.execute("call-underscore-task-name", { task: "review subagent handling", - taskName: "Bad-Name", + taskName: "review_subagents", }); - expectDetailFields(result.details, { status: "error" }); - expect(JSON.stringify(result.details)).toContain("Invalid taskName"); - expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled(); + expectDetailFields(result.details, { + status: "accepted", + childSessionKey: "agent:main:subagent:1", + }); + const spawnArgs = mockCallArg(hoisted.spawnSubagentDirectMock, 0, 0, "spawnSubagentDirect"); + expect(spawnArgs.taskName).toBe("review_subagents"); }); + it.each(["Bad-Name", "code review", "-bad"])( + "rejects invalid taskName %s before spawning", + async (taskName) => { + const tool = createSessionsSpawnTool({ + agentSessionKey: "agent:main:main", + }); + + const result = await tool.execute("call-bad-task-name", { + task: "review subagent handling", + taskName, + }); + + expectDetailFields(result.details, { status: "error" }); + expect(JSON.stringify(result.details)).toContain("Invalid taskName"); + expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled(); + }, + ); + it.each(["last", "all"])("rejects reserved taskName %s before spawning", async (taskName) => { const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main", diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index fbafe150a3a..404f2557f9b 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -159,7 +159,7 @@ function createSessionsSpawnToolSchema(params: { taskName: Type.Optional( Type.String({ description: - "Stable alias for later targeting; lowercase letters/digits/underscores, starts letter.", + "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", }), ), label: Type.Optional(Type.String()), 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 aa58bb214d8..3d883992650 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 @@ -1064,7 +1064,7 @@ "type": "string" }, "taskName": { - "description": "Stable alias for later targeting; lowercase letters/digits/underscores, starts letter.", + "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", "type": "string" }, "thinking": { 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 e7d45490553..f59bcf23b07 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 @@ -1100,7 +1100,7 @@ "type": "string" }, "taskName": { - "description": "Stable alias for later targeting; lowercase letters/digits/underscores, starts letter.", + "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", "type": "string" }, "thinking": { 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 9cdcfdfb6e5..40b5c69e4bf 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 @@ -1064,7 +1064,7 @@ "type": "string" }, "taskName": { - "description": "Stable alias for later targeting; lowercase letters/digits/underscores, starts letter.", + "description": "Stable alias for later targeting; lowercase letters/digits/underscores/hyphens, starts letter.", "type": "string" }, "thinking": { 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 1795bffa4bc..d5c687f76ca 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 @@ -221,8 +221,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 41138, - "roughTokens": 10285 + "chars": 41146, + "roughTokens": 10287 }, "openClawDeveloperInstructions": { "chars": 2988, @@ -233,8 +233,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6925 }, "totalWithDynamicToolsJson": { - "chars": 68840, - "roughTokens": 17210 + "chars": 68848, + "roughTokens": 17212 }, "userInputText": { "chars": 1629, 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 5e3abd9655b..7d65f2bbc25 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 @@ -221,8 +221,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 40859, - "roughTokens": 10215 + "chars": 40867, + "roughTokens": 10217 }, "openClawDeveloperInstructions": { "chars": 1964, @@ -233,8 +233,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6544 }, "totalWithDynamicToolsJson": { - "chars": 67037, - "roughTokens": 16760 + "chars": 67045, + "roughTokens": 16762 }, "userInputText": { "chars": 1129, 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 54e9fa6b35f..200fbbff254 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 @@ -222,8 +222,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 41954, - "roughTokens": 10489 + "chars": 41962, + "roughTokens": 10491 }, "openClawDeveloperInstructions": { "chars": 1983, @@ -234,8 +234,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6780 }, "totalWithDynamicToolsJson": { - "chars": 69075, - "roughTokens": 17269 + "chars": 69083, + "roughTokens": 17271 }, "userInputText": { "chars": 1367,