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,