fix(x): spawn-agent works from task alone — instructions now optional

First real-world use showed the model calling spawn-agent with
{name, tools, task} and no instructions — a complete spec by any
reasonable reading — and burning a correction round-trip on the
"exactly one of agent_id or instructions" rejection (both children
then succeeded on retry).

Requiring instructions bought nothing: the task is the spec. Omitting
both agent_id and instructions now spawns a general-purpose worker
with a default headless prompt; only supplying BOTH remains an error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-09 06:08:10 +05:30
parent b62ed6a2f4
commit 3822bda1f7
2 changed files with 40 additions and 16 deletions

View file

@ -140,20 +140,32 @@ describe("runSpawnedAgent", () => {
expect(started[0].maxModelCalls).toBe(5);
});
it("requires exactly one of agent_id and instructions", async () => {
it("rejects agent_id and instructions together", async () => {
const { services } = fakeServices({});
for (const input of [
{ task: "t" },
const result = await runSpawnedAgent(
{ task: "t", agent_id: "a", instructions: "b" },
]) {
const result = await runSpawnedAgent(input, {
parentTurnId: "parent-1",
signal,
services,
});
expect(result.isError).toBe(true);
expect(result.output).toMatch(/exactly one/);
}
{ parentTurnId: "parent-1", signal, services },
);
expect(result.isError).toBe(true);
expect(result.output).toMatch(/at most one/);
});
it("spawns a default worker when neither agent_id nor instructions is given", async () => {
// Models routinely treat task (+ name/tools) as a complete spec; the
// task-only form must work rather than cost a correction round-trip.
const { services, started } = fakeServices({});
const result = await runSpawnedAgent(
{ task: "find the weather", name: "london-weather", tools: ["web-search"] },
{ parentTurnId: "parent-1", signal, services },
);
expect(result.isError).toBe(false);
const agent = started[0].agent as {
inline: { name: string; instructions: string; tools?: string[] };
};
expect(agent.inline.name).toBe("london-weather");
expect(agent.inline.tools).toEqual(["web-search"]);
expect(agent.inline.instructions).toMatch(/london-weather/);
expect(agent.inline.instructions).toMatch(/headlessly/);
});
it("rejects a model-call budget above the cap at the schema boundary", async () => {

View file

@ -34,7 +34,7 @@ export const SpawnAgentInput = z.object({
.string()
.optional()
.describe(
"System instructions for an agent constructed on the fly. Mutually exclusive with `agent_id`.",
"System instructions for an agent constructed on the fly. Mutually exclusive with `agent_id`. Optional: omitting both `agent_id` and `instructions` spawns a general-purpose worker driven by `task` alone.",
),
model: z
.string()
@ -96,9 +96,9 @@ export async function runSpawnedAgent(
return spawnError(`invalid input: ${parsed.error.message}`);
}
const input = parsed.data;
if (!input.agent_id === !input.instructions) {
if (input.agent_id && input.instructions) {
return spawnError(
"provide exactly one of `agent_id` or `instructions`",
"provide at most one of `agent_id` or `instructions`",
);
}
@ -149,7 +149,11 @@ export async function runSpawnedAgent(
: {
inline: {
name: agentName,
instructions: input.instructions!,
// The task alone is usually a complete spec — models
// routinely omit `instructions` for ad-hoc workers, and
// rejecting that just costs a correction round-trip.
instructions:
input.instructions ?? defaultWorkerInstructions(agentName),
...(model ? { model } : {}),
...(input.tools ? { tools: input.tools } : {}),
},
@ -231,6 +235,14 @@ function spawnError(message: string): z.infer<typeof ToolResultData> {
return { output: `spawn-agent: ${message}`, isError: true };
}
function defaultWorkerInstructions(name: string): string {
return (
`You are ${name}, a sub-agent spawned to complete a single task. ` +
"You run headlessly: no user is available to clarify or approve, and your final message is your only output. " +
"Work autonomously with the tools you have, then end with a complete, self-contained answer to the task."
);
}
// True for any child-shaped parent: inline agents only exist as spawned
// children, and by-id children carry the subagent composition flag.
function hasSubagentFlag(