mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 21:32:53 +00:00
fix(agents): allow hyphenated subagent task names
Allow `sessions_spawn.taskName` to accept lowercase hyphenated task slugs while keeping the existing underscore support and invalid-name rejection. Update the tool schema, system prompt wording, docs, focused tests, and generated prompt snapshots so the user/model-facing contract matches the validator. Verification: - `pnpm prompt:snapshots:check` - `node scripts/run-vitest.mjs src/agents/tools/sessions-spawn-tool.test.ts src/agents/system-prompt.test.ts` - Real behavior proof gate: https://github.com/openclaw/openclaw/actions/runs/26628449324/job/78470916945 - PR CI: https://github.com/openclaw/openclaw/actions/runs/26628441940, with failures matching current `main` at https://github.com/openclaw/openclaw/actions/runs/26628128225 Co-authored-by: chenhaoqiang <chenhaoqiang@xiaomi.com> Co-authored-by: Lanzhi <lizhan3@xiaomi.com>
This commit is contained in:
@@ -178,7 +178,7 @@ Per-agent overrides use `agents.list[].subagents.delegationMode`.
|
||||
The task description for the sub-agent.
|
||||
</ParamField>
|
||||
<ParamField path="taskName" type="string">
|
||||
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`.
|
||||
</ParamField>
|
||||
<ParamField path="label" type="string">
|
||||
Optional human-readable label.
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user