mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
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:
parent
b62ed6a2f4
commit
3822bda1f7
2 changed files with 40 additions and 16 deletions
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue