workflows/plan¶
Source: src/workflows/plan.ts
Reading a plan an agent wrote: the prompt, the parser, the validation.
Split out from orchestrate so the prompt, the parser and the validation
can be read, and tested, apart from the fan-out that follows them.
The open question this file answers is how to read what an agent decided. Three candidates were weighed: a tool call, structured output, or a parsed convention. This is a parsed convention, and the reasoning is worth keeping:
A tool call is possible (
createAgentSessiontakescustomTools), and it would give validated arguments for free. It also means teachingSessionPortabout tool definitions, and betting the whole thing on a model that reliably emits tool calls. The weak models this library is routinely run against do not.Structured output is not uniformly available across providers, and pi’s
prompt()returns text either way.A parsed convention costs one function, works on every provider, and the plan is validated against the known agents regardless - which is the check that actually matters. A schema would not have caught a hallucinated agent name; the name lookup does.
{@link parsePlan} is therefore lenient, and only it: its input is a language model, not a caller. Everywhere else in this library a malformed input is an error.
parsePlan¶
function
export function parsePlan(output: string, workers: readonly Agent[]): PlannedTask[] { /* … */ }
Reads a plan out of whatever the planner actually wrote.
Lenient by design - the input is a model. Two shapes are accepted, because both are what models really produce when asked for the first:
any JSON objects carrying
agentandtask, in the order they appear: inside an array, alone, one per line, wrapped in a code fence or in prose. Asked for an array, a real planner answered with bare objects and no brackets - so the parser collects objects rather than requiring the shape;one subtask per line,
agent: task.
A step naming an unknown agent is dropped, never remapped: guessing which agent a hallucinated name meant is how a workflow silently does the wrong work. An empty result means “nothing usable was said”, which the caller reports rather than papers over.
PlannedTask¶
type
export type PlannedTask = {
/** Already resolved: an unknown name is dropped by the parser, never carried here. */
agent: Agent;
/** What that agent is asked to do, in the planner's own words. */
task: string;
};
One step of a plan: a resolved agent - an unknown name never gets this far - and its task.
planningPrompt¶
function
export function planningPrompt(input: string, workers: readonly Agent[], maxTasks: number): string { /* … */ }
The question put to the planner: who is available, and how to answer.
PlanOptions¶
type
export type PlanOptions = WorkflowOptions & {
/** The agent that decides the split. */
planner: Agent;
/** Who it may delegate to. Their `description` is what the planner reads. */
workers: Agent[];
/** What is to be split. The planner sees this and the roster, nothing else. */
input: string;
/**
* How many subtasks a plan may contain. Defaults to 8.
*
* Same reasoning as `loop`'s `maxIterations`: every subtask is a session and
* a bill, so "a plan of two hundred steps" must not be reachable by a
* hallucination. Exceeding it fails **before** anything is spawned - losing
* a run costs less than paying for a runaway one.
*/
maxTasks?: number;
/** Overrides how the plan is read. See {@link parsePlan}. */
parse?: (output: string, workers: readonly Agent[]) => PlannedTask[];
/** Overrides the question put to the planner. See {@link planningPrompt}. */
format?: (input: string, workers: readonly Agent[], maxTasks: number) => string;
};
Who plans, who may be delegated to, and how large a plan may get.
PlanOutcome¶
type
export type PlanOutcome = {
/** What the planner asked for, after validation. Empty when nothing is runnable. */
plan: PlannedTask[];
/** The planner's own turn. Kept whatever happened next. */
planning: Result;
/** False when the planner failed, or produced nothing runnable. */
ok: boolean;
/** Set if and only if `ok` is false. It carries what the planner actually wrote. */
error?: string;
};
A validated plan, or the reason there is none - decided before anything is spawned.