diff --git a/apps/x/packages/core/src/agents/spawn-agent.test.ts b/apps/x/packages/core/src/agents/spawn-agent.test.ts index 55e093f7..fc5b91a3 100644 --- a/apps/x/packages/core/src/agents/spawn-agent.test.ts +++ b/apps/x/packages/core/src/agents/spawn-agent.test.ts @@ -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 () => { diff --git a/apps/x/packages/core/src/agents/spawn-agent.ts b/apps/x/packages/core/src/agents/spawn-agent.ts index 4466e62c..432edaab 100644 --- a/apps/x/packages/core/src/agents/spawn-agent.ts +++ b/apps/x/packages/core/src/agents/spawn-agent.ts @@ -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 { 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(