@mikode13/harness 10.0.0 → 11.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -513,8 +513,10 @@ model APIs:
513
513
  explorers at once, one per question, on the role's provider's cheaper model at `medium`:
514
514
  Claude Haiku 5.5, or `gpt-5.6-luna`. Each starts from its question alone and reports where
515
515
  the answer is, the files and line ranges, as a librarian says where a book is; the role
516
- then reads those ranges itself, with `readFiles`, instead of searching the repository. Their
517
- usage counts in the run's, and their narration arrives as `subagent` events;
516
+ then reads those ranges itself, with `readFiles`, instead of searching the repository. They
517
+ work in the background, as the agents of `agentAsTool` do, and each report reaches the role
518
+ when its explorer finishes. Their usage counts in the run's, and their narration arrives as
519
+ `subagent` events;
518
520
  - the executor gets its provider's file tools. `autoApprove` reaches only the executor;
519
521
  - each role gets the commands whose `roles` name it; a command names only the executor unless
520
522
  it says otherwise.
@@ -557,8 +559,9 @@ orchestrator is one run of the history, every round included.
557
559
  ### Agents that hand work to other agents
558
560
 
559
561
  `agentAsTool` offers an agent to another one as a tool. The calling model writes the task, and
560
- a new agent from the `createAgent` option runs it; its answer is the call's result. Specialists can come
561
- from a package you reuse in every project, or be written in one project:
562
+ a new agent from the `createAgent` option works on it in the background while the caller goes
563
+ on. Specialists can come from a package you reuse in every project, or be written in one
564
+ project:
562
565
 
563
566
  ```ts
564
567
  import { agentAsTool, createFileTools, createLLMAgent } from '@mikode13/harness';
@@ -592,17 +595,26 @@ The agent works inside the caller's run:
592
595
  `createAgent` option gave it. Handing over a task asks no one unless you set
593
596
  `risk: 'destructive'`;
594
597
  - its progress arrives as `subagent` events of the call, with the tool's `name`, and its
595
- transcript names the call in `delegation`.
596
-
597
- Calls to these tools next to each other in one answer run at once; a call to another tool
598
- between them waits for those before it, and the ones after it wait for it. The history stays
599
- exact, because every change and command of a run is made one at a time, but agents working at
600
- once can see each other's unfinished work, and a long command holds the others' writes until it
601
- ends. An agent that fails
602
- is the call's error, for the model to act on; a cancellation, or your approver or asker failing
603
- inside it, ends the run. A call deeper than `maxDepth`, 2 by default, is refused. Only agents
604
- from `createLLMAgent` work this way: an Agent SDK agent from the `createAgent` factory runs
605
- as a run of its own.
598
+ transcript names the call in `delegation`. The call's own `tool` event ends with
599
+ `status: 'sent'`, the agent's events keep arriving after it, and a `subagentEnded` event says
600
+ when it ended and how: `answered`, `failed`, or `stopped` because the run ended.
601
+
602
+ The call answers at once, telling the model the agent works in the background. When the agent
603
+ finishes, only its answer reaches the caller's model, in a message of its own at the caller's
604
+ next step; nothing else of its work does. The caller reacts to each answer as it arrives. An
605
+ answer the caller gives while agents still work is a wait, narrated like any other message:
606
+ the run makes no call to the model until the next agent answers, and it ends only when the
607
+ caller answers with nothing pending. Waiting counts against the caller's `maxSteps`, and each
608
+ agent has its own.
609
+
610
+ Agents that write work at once too. The history stays exact, because every change and command
611
+ of a run is made one at a time, but agents working at once can see each other's unfinished
612
+ work, and a long command holds the others' writes until it ends. An agent that fails answers
613
+ with why, for the model to act on. A cancellation, or your approver or asker failing inside an
614
+ agent, ends the run: every agent still working is stopped, the run ends once they have, and
615
+ what they had changed stays in the run's history. A call deeper than `maxDepth`, 2 by default,
616
+ is refused. Only agents from `createLLMAgent` work this way: an Agent SDK agent from the
617
+ `createAgent` factory runs as a run of its own.
606
618
 
607
619
  ### Running the project's commands
608
620
 
@@ -798,7 +810,10 @@ The CLI shows each command as the line it will run before asking, and "always" a
798
810
  that command again, not every command. Tool calls finish as one dimmed line with an
799
811
  icon, role, tool name, summary and duration; a failed configured command includes
800
812
  `exit N` beside its duration, while a command event also includes its exit code.
801
- A denied call is marked `denied`. Messages remain normal text under a bold role
813
+ A denied call is marked `denied`. A call that sent agents to work in the background is marked
814
+ `↗ … | sent`, and each of its agents gets a line of its own when it ends, `✓` when it answered,
815
+ `✗` when it failed and `⊘` when it was stopped, with how long it worked; while no call runs,
816
+ the spinner names an agent still at work. Messages remain normal text under a bold role
802
817
  heading. Reasoning is dim italic and complete by default; pass
803
818
  `--thinking-headings` to show only Markdown bold headings as `∴ …`. A tool start
804
819
  updates the spinner rather than printing a second line, and cancelling a turn clears
@@ -107,9 +107,11 @@ export type ProgressEvent = {
107
107
  error?: string;
108
108
  /**
109
109
  * `in_progress` only once the call is about to run, after any approval. `denied` when
110
- * it never ran because no one allowed it, with no `in_progress` before it.
110
+ * it never ran because no one allowed it, with no `in_progress` before it. `sent` when
111
+ * it sent subagents to work in the background, such as an `explore` call: they go on
112
+ * after it, as `subagent` events, until a `subagentEnded` for each.
111
113
  */
112
- status: 'in_progress' | 'completed' | 'error' | 'denied';
114
+ status: 'in_progress' | 'completed' | 'error' | 'denied' | 'sent';
113
115
  /**
114
116
  * On `completed`, for a call that changed a file through the harness's write tools: the
115
117
  * change as a unified diff, for the person watching. Narration only; the run's whole
@@ -165,11 +167,13 @@ export type ProgressEvent = {
165
167
  agent?: ProgressAgent;
166
168
  }
167
169
  /**
168
- * An event of a subagent that a tool call started: one of the explorers an `explore` call runs
169
- * at once, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that
170
+ * An event of a subagent that a tool call started: one of the explorers an `explore` call
171
+ * sends, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that
170
172
  * call's `tool` events, and `subagent` numbers its subagents from 1, so the events of agents
171
- * running in parallel stay apart. A subagent's own subagents arrive as `subagent` events
172
- * inside this one's `event`.
173
+ * running in parallel stay apart. Subagents work in the background: the call's own `tool`
174
+ * event ends with `status: 'sent'`, and each subagent's events keep arriving after it, until
175
+ * its `subagentEnded`, whether it answered, failed or was stopped. A subagent's own subagents
176
+ * arrive as `subagent` events inside this one's `event`.
173
177
  */
174
178
  | {
175
179
  type: 'subagent';
@@ -181,6 +185,20 @@ export type ProgressEvent = {
181
185
  task: string;
182
186
  event: ProgressEvent;
183
187
  agent?: ProgressAgent;
188
+ }
189
+ /**
190
+ * A subagent that a call with `status: 'sent'` sent has ended: it `answered`, it `failed` and
191
+ * its answer says why, or it was `stopped` because the run ended. `callId`, `subagent` and
192
+ * `name` are those of its `subagent` events. Narration only: its answer reaches the model that
193
+ * sent it, not the consumer.
194
+ */
195
+ | {
196
+ type: 'subagentEnded';
197
+ callId: string;
198
+ subagent: number;
199
+ name: string;
200
+ outcome: 'answered' | 'failed' | 'stopped';
201
+ agent?: ProgressAgent;
184
202
  };
185
203
  /**
186
204
  * What one call to the model sent, in characters by where it came from: an estimate of size
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AA0DA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\nimport type { Asker } from './question.ts';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n\t/**\n\t * The run the workspace's history recorded, when this run wrote through the harness's write\n\t * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes\n\t * the history does not record.\n\t */\n\trunId?: string;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/**\n * What a run needs besides its prompt. InstructedAgent passes it on whole. OrchestratorAgent\n * copies its own enumerable fields and replaces only `onProgress` per role; RetryingAgent does\n * the same for a run with `ask`, replacing only `ask`. A field must therefore be an own\n * enumerable one to reach every agent.\n */\nexport interface RunOptions {\n\t/**\n\t * Cancels the run, which then rejects with the signal's reason: an `AbortError`, or a\n\t * `TimeoutError` from `AbortSignal.timeout`. `isAbortError` recognizes both.\n\t */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide. Agents a delegation runs at once may call it before\n\t * an earlier call has its answer.\n\t */\n\tapprove?: Approver | undefined;\n\t/**\n\t * Answers what a model-backed agent asks the user, through its `askUser` tool, when a\n\t * decision is the user's to make. Without it the agent has no such tool. The Agent SDK\n\t * engines ignore it. Agents a delegation runs at once may call it before an earlier call has\n\t * its answer.\n\t */\n\task?: Asker | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number; agent?: ProgressAgent }\n\t| { type: 'reasoning'; message: string; agent?: ProgressAgent }\n\t| { type: 'search'; query: string; agent?: ProgressAgent }\n\t| {\n\t\t\ttype: 'fileChange';\n\t\t\tchanges: { path: string; kind: 'add' | 'update' | 'delete' }[];\n\t\t\tagent?: ProgressAgent;\n\t }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string; agent?: ProgressAgent }\n\t| { type: 'agentMessage'; message: string; agent?: ProgressAgent }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tagent?: ProgressAgent;\n\t\t\t/** A short, tool-owned description of the call. Narration only. */\n\t\t\tsummary?: string;\n\t\t\t/** The exit code of a configured command that ended with an error. Narration only. */\n\t\t\texitCode?: number;\n\t\t\t/**\n\t\t\t * On `error`, for an agent on the model APIs: what the model was told went wrong, such\n\t\t\t * as a patch that did not apply or a failed command's output. Narration only.\n\t\t\t */\n\t\t\terror?: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied';\n\t\t\t/**\n\t\t\t * On `completed`, for a call that changed a file through the harness's write tools: the\n\t\t\t * change as a unified diff, for the person watching. Narration only; the run's whole\n\t\t\t * record is the history's `changes`.\n\t\t\t */\n\t\t\tdiff?: string;\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[]; agent?: ProgressAgent }\n\t| { type: 'turnStarted'; agent?: ProgressAgent }\n\t| { type: 'turnEnded'; agent?: ProgressAgent }\n\t/**\n\t * One billed call to the model by an agent on the model APIs, including one the model\n\t * stopped early or the client turned into an error: what the call sent and what it cost.\n\t * Narration only: the run's total is `AgentResponse.tokens`, or the failure's `tokens`.\n\t */\n\t| {\n\t\t\ttype: 'step';\n\t\t\t/** 1 for the first call of the run. */\n\t\t\tstep: number;\n\t\t\t/** Missing when the provider did not report this call's usage. */\n\t\t\ttokens?: Tokens;\n\t\t\tcontext: StepContext;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A workflow's stage asks its agent for an answer; `attempt` is 2 or more when the previous\n\t * answer was invalid. Stages run one at a time, so the events that follow, until the next\n\t * workflow event, are this stage's agent's.\n\t */\n\t| { type: 'workflowStage'; stage: string; attempt: number; agent?: ProgressAgent }\n\t/** The transition a stage's valid answer selected, and the stage it leads to. */\n\t| {\n\t\t\ttype: 'workflowTransition';\n\t\t\tstage: string;\n\t\t\ttransition: 'go' | 'stop' | 'complete';\n\t\t\t/** Absent after `stop` or `complete`. */\n\t\t\tnextStage?: string;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * An event of a subagent that a tool call started: one of the explorers an `explore` call runs\n\t * at once, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that\n\t * call's `tool` events, and `subagent` numbers its subagents from 1, so the events of agents\n\t * running in parallel stay apart. A subagent's own subagents arrive as `subagent` events\n\t * inside this one's `event`.\n\t */\n\t| {\n\t\t\ttype: 'subagent';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\t/** Which agent it is: `explorer`, or the name of the tool that delegates to it. */\n\t\t\tname: string;\n\t\t\t/** What the subagent was asked. */\n\t\t\ttask: string;\n\t\t\tevent: ProgressEvent;\n\t\t\tagent?: ProgressAgent;\n\t };\n\n/**\n * What one call to the model sent, in characters by where it came from: an estimate of size\n * for finding what fills a context, not a token count.\n */\nexport interface StepContext {\n\t/** The system prompt. */\n\tinstructions: number;\n\t/** The tool definitions. */\n\ttools: number;\n\t/**\n\t * The text of the user's prompts and the model's answers, and the provider's summary of the\n\t * turns before it once it has compacted them: only what the call sent.\n\t */\n\tmessages: number;\n\t/** Reasoning the model produced earlier and is sent back: its provider's own signed or encrypted blocks. */\n\treasoning: number;\n\t/** The input of every tool call so far. */\n\ttoolCalls: number;\n\t/** Every tool result so far, by tool, largest first. */\n\ttoolResults: { tool: string; characters: number; calls: number }[];\n\t/** The largest single results, each with its call's summary when the tool gives one. */\n\tlargestResults: { tool: string; characters: number; summary?: string }[];\n}\n\n/** A role in the fixed planner → executor → reviewer workflow. */\nexport type ProgressAgent = 'planner' | 'executor' | 'reviewer';\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError`, or the `TimeoutError` of\n * `AbortSignal.timeout`, must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AA0DA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\nimport type { Asker } from './question.ts';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n\t/**\n\t * The run the workspace's history recorded, when this run wrote through the harness's write\n\t * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes\n\t * the history does not record.\n\t */\n\trunId?: string;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/**\n * What a run needs besides its prompt. InstructedAgent passes it on whole. OrchestratorAgent\n * copies its own enumerable fields and replaces only `onProgress` per role; RetryingAgent does\n * the same for a run with `ask`, replacing only `ask`. A field must therefore be an own\n * enumerable one to reach every agent.\n */\nexport interface RunOptions {\n\t/**\n\t * Cancels the run, which then rejects with the signal's reason: an `AbortError`, or a\n\t * `TimeoutError` from `AbortSignal.timeout`. `isAbortError` recognizes both.\n\t */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide. Agents a delegation runs at once may call it before\n\t * an earlier call has its answer.\n\t */\n\tapprove?: Approver | undefined;\n\t/**\n\t * Answers what a model-backed agent asks the user, through its `askUser` tool, when a\n\t * decision is the user's to make. Without it the agent has no such tool. The Agent SDK\n\t * engines ignore it. Agents a delegation runs at once may call it before an earlier call has\n\t * its answer.\n\t */\n\task?: Asker | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number; agent?: ProgressAgent }\n\t| { type: 'reasoning'; message: string; agent?: ProgressAgent }\n\t| { type: 'search'; query: string; agent?: ProgressAgent }\n\t| {\n\t\t\ttype: 'fileChange';\n\t\t\tchanges: { path: string; kind: 'add' | 'update' | 'delete' }[];\n\t\t\tagent?: ProgressAgent;\n\t }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string; agent?: ProgressAgent }\n\t| { type: 'agentMessage'; message: string; agent?: ProgressAgent }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tagent?: ProgressAgent;\n\t\t\t/** A short, tool-owned description of the call. Narration only. */\n\t\t\tsummary?: string;\n\t\t\t/** The exit code of a configured command that ended with an error. Narration only. */\n\t\t\texitCode?: number;\n\t\t\t/**\n\t\t\t * On `error`, for an agent on the model APIs: what the model was told went wrong, such\n\t\t\t * as a patch that did not apply or a failed command's output. Narration only.\n\t\t\t */\n\t\t\terror?: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it. `sent` when\n\t\t\t * it sent subagents to work in the background, such as an `explore` call: they go on\n\t\t\t * after it, as `subagent` events, until a `subagentEnded` for each.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied' | 'sent';\n\t\t\t/**\n\t\t\t * On `completed`, for a call that changed a file through the harness's write tools: the\n\t\t\t * change as a unified diff, for the person watching. Narration only; the run's whole\n\t\t\t * record is the history's `changes`.\n\t\t\t */\n\t\t\tdiff?: string;\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[]; agent?: ProgressAgent }\n\t| { type: 'turnStarted'; agent?: ProgressAgent }\n\t| { type: 'turnEnded'; agent?: ProgressAgent }\n\t/**\n\t * One billed call to the model by an agent on the model APIs, including one the model\n\t * stopped early or the client turned into an error: what the call sent and what it cost.\n\t * Narration only: the run's total is `AgentResponse.tokens`, or the failure's `tokens`.\n\t */\n\t| {\n\t\t\ttype: 'step';\n\t\t\t/** 1 for the first call of the run. */\n\t\t\tstep: number;\n\t\t\t/** Missing when the provider did not report this call's usage. */\n\t\t\ttokens?: Tokens;\n\t\t\tcontext: StepContext;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A workflow's stage asks its agent for an answer; `attempt` is 2 or more when the previous\n\t * answer was invalid. Stages run one at a time, so the events that follow, until the next\n\t * workflow event, are this stage's agent's.\n\t */\n\t| { type: 'workflowStage'; stage: string; attempt: number; agent?: ProgressAgent }\n\t/** The transition a stage's valid answer selected, and the stage it leads to. */\n\t| {\n\t\t\ttype: 'workflowTransition';\n\t\t\tstage: string;\n\t\t\ttransition: 'go' | 'stop' | 'complete';\n\t\t\t/** Absent after `stop` or `complete`. */\n\t\t\tnextStage?: string;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * An event of a subagent that a tool call started: one of the explorers an `explore` call\n\t * sends, or the agent a tool from `agentAsTool` delegates to. `callId` is the `id` of that\n\t * call's `tool` events, and `subagent` numbers its subagents from 1, so the events of agents\n\t * running in parallel stay apart. Subagents work in the background: the call's own `tool`\n\t * event ends with `status: 'sent'`, and each subagent's events keep arriving after it, until\n\t * its `subagentEnded`, whether it answered, failed or was stopped. A subagent's own subagents\n\t * arrive as `subagent` events inside this one's `event`.\n\t */\n\t| {\n\t\t\ttype: 'subagent';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\t/** Which agent it is: `explorer`, or the name of the tool that delegates to it. */\n\t\t\tname: string;\n\t\t\t/** What the subagent was asked. */\n\t\t\ttask: string;\n\t\t\tevent: ProgressEvent;\n\t\t\tagent?: ProgressAgent;\n\t }\n\t/**\n\t * A subagent that a call with `status: 'sent'` sent has ended: it `answered`, it `failed` and\n\t * its answer says why, or it was `stopped` because the run ended. `callId`, `subagent` and\n\t * `name` are those of its `subagent` events. Narration only: its answer reaches the model that\n\t * sent it, not the consumer.\n\t */\n\t| {\n\t\t\ttype: 'subagentEnded';\n\t\t\tcallId: string;\n\t\t\tsubagent: number;\n\t\t\tname: string;\n\t\t\toutcome: 'answered' | 'failed' | 'stopped';\n\t\t\tagent?: ProgressAgent;\n\t };\n\n/**\n * What one call to the model sent, in characters by where it came from: an estimate of size\n * for finding what fills a context, not a token count.\n */\nexport interface StepContext {\n\t/** The system prompt. */\n\tinstructions: number;\n\t/** The tool definitions. */\n\ttools: number;\n\t/**\n\t * The text of the user's prompts and the model's answers, and the provider's summary of the\n\t * turns before it once it has compacted them: only what the call sent.\n\t */\n\tmessages: number;\n\t/** Reasoning the model produced earlier and is sent back: its provider's own signed or encrypted blocks. */\n\treasoning: number;\n\t/** The input of every tool call so far. */\n\ttoolCalls: number;\n\t/** Every tool result so far, by tool, largest first. */\n\ttoolResults: { tool: string; characters: number; calls: number }[];\n\t/** The largest single results, each with its call's summary when the tool gives one. */\n\tlargestResults: { tool: string; characters: number; summary?: string }[];\n}\n\n/** A role in the fixed planner → executor → reviewer workflow. */\nexport type ProgressAgent = 'planner' | 'executor' | 'reviewer';\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError`, or the `TimeoutError` of\n * `AbortSignal.timeout`, must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import type { ProgressEvent } from '#src/agent/domain/agent';
2
+ import type { Message } from '#src/llm/domain/message';
3
+ import { type Delegated } from '#src/tools/domain/preparedCall';
4
+ /** What the model is told of the agents a call sent, as that call's result. */
5
+ export declare function sentNote(agents: readonly Delegated[]): string;
6
+ /**
7
+ * The agents a run's calls sent to work in the background. Each one's answer waits here, in the
8
+ * order they finished, until the model's next step takes it. A failure that ends the run stops
9
+ * every agent still working at once, and the run at its next boundary.
10
+ */
11
+ export declare class BackgroundAgents {
12
+ private readonly stopping;
13
+ /** What the agents work under: the run's own signal, or the stop of the run's end. */
14
+ readonly signal: AbortSignal;
15
+ private readonly agents;
16
+ private readonly arrived;
17
+ private ended;
18
+ private unrecordedWorking;
19
+ private unrecordedEnded;
20
+ private woken;
21
+ /**
22
+ * What some agent spent is unknown: it was stopped while it worked, or failed in a way that
23
+ * carries no usage.
24
+ */
25
+ private unknownUsage;
26
+ private readonly narrate;
27
+ /** `narrate` tells the person watching when each agent ended; it must never throw. */
28
+ constructor(runSignal: AbortSignal, narrate: (event: ProgressEvent) => void);
29
+ /** Some agent is still working. */
30
+ get working(): boolean;
31
+ /**
32
+ * Whether an agent that may change files the history does not record worked since the last
33
+ * time this was asked: one is working now, or one ended meanwhile.
34
+ */
35
+ mayHaveChangedUnseen(): boolean;
36
+ get usageUnknown(): boolean;
37
+ /** Watches the agents call `callId` sent, until each one answers. */
38
+ add(callId: string, agents: readonly Delegated[], unrecorded: boolean): void;
39
+ /**
40
+ * The message holding every answer that arrived since the last one, if any did, and what
41
+ * became of each agent a stop interrupted.
42
+ */
43
+ takeAnswers(): Message | undefined;
44
+ /**
45
+ * Waits until an answer arrives, while agents are working. Rejects as cancelled when the run
46
+ * is cancelled meanwhile, and with the failure that ended it.
47
+ */
48
+ nextAnswer(): Promise<void>;
49
+ /** Throws the failure of an agent that ended the run, if one did. */
50
+ throwIfEnded(): void;
51
+ /**
52
+ * Stops every agent still working, and waits until each one has: what they changed and spent
53
+ * belongs to the run, which ends only after them.
54
+ */
55
+ stop(): Promise<void>;
56
+ private arrive;
57
+ /** A failure ends the run: every agent is stopped for it. */
58
+ private end;
59
+ private wake;
60
+ }
@@ -0,0 +1,165 @@
1
+ import { describeFailure } from '#src/shared/domain/providerFailure';
2
+ import { RunEndingToolError } from '#src/tools/domain/preparedCall';
3
+ // How much of a task the answer repeats, so the model can tell which one it is.
4
+ const shownTaskCharacters = 200;
5
+ /** How the model that sent an agent is told which one it is, such as `explorer 2`. */
6
+ function labelOf({ name, subagent }) {
7
+ return `${name} ${String(subagent)}`;
8
+ }
9
+ /** What the model is told of the agents a call sent, as that call's result. */
10
+ export function sentNote(agents) {
11
+ const names = agents.map(labelOf).join(', ');
12
+ return `Sent to work in the background: ${names}. Each one's answer reaches you in a message of its own once it finishes. Keep working meanwhile. When you have nothing else to do until they answer, say briefly that you are waiting, without calling a tool: the run waits for them, and you go on with their answers.`;
13
+ }
14
+ // What a model is told of an agent the end of its run stopped, in a conversation kept for the next.
15
+ const stoppedAnswer = 'None: it was stopped when the run ended, before it answered. Anything it changed stays as it left it.';
16
+ function shownTask(task) {
17
+ return task.length > shownTaskCharacters ? `${task.slice(0, shownTaskCharacters)}…` : task;
18
+ }
19
+ /**
20
+ * The agents a run's calls sent to work in the background. Each one's answer waits here, in the
21
+ * order they finished, until the model's next step takes it. A failure that ends the run stops
22
+ * every agent still working at once, and the run at its next boundary.
23
+ */
24
+ export class BackgroundAgents {
25
+ stopping = new AbortController();
26
+ /** What the agents work under: the run's own signal, or the stop of the run's end. */
27
+ signal;
28
+ agents = new Set();
29
+ arrived = [];
30
+ ended;
31
+ unrecordedWorking = 0;
32
+ // Such an agent ended since `mayHaveChangedUnseen` was last asked.
33
+ unrecordedEnded = false;
34
+ woken;
35
+ /**
36
+ * What some agent spent is unknown: it was stopped while it worked, or failed in a way that
37
+ * carries no usage.
38
+ */
39
+ unknownUsage = false;
40
+ narrate;
41
+ /** `narrate` tells the person watching when each agent ended; it must never throw. */
42
+ constructor(runSignal, narrate) {
43
+ this.signal = AbortSignal.any([runSignal, this.stopping.signal]);
44
+ this.narrate = narrate;
45
+ }
46
+ /** Some agent is still working. */
47
+ get working() {
48
+ return this.agents.size > 0;
49
+ }
50
+ /**
51
+ * Whether an agent that may change files the history does not record worked since the last
52
+ * time this was asked: one is working now, or one ended meanwhile.
53
+ */
54
+ mayHaveChangedUnseen() {
55
+ const worked = this.unrecordedWorking > 0 || this.unrecordedEnded;
56
+ this.unrecordedEnded = false;
57
+ return worked;
58
+ }
59
+ get usageUnknown() {
60
+ return this.unknownUsage;
61
+ }
62
+ /** Watches the agents call `callId` sent, until each one answers. */
63
+ add(callId, agents, unrecorded) {
64
+ for (const agent of agents) {
65
+ if (unrecorded)
66
+ this.unrecordedWorking++;
67
+ const ended = (outcome) => {
68
+ const { name, subagent } = agent;
69
+ this.narrate({ type: 'subagentEnded', callId, subagent, name, outcome });
70
+ };
71
+ const watched = agent.answer
72
+ .then(({ text, failed }) => {
73
+ ended(failed ? 'failed' : 'answered');
74
+ this.arrive(agent, callId, text);
75
+ }, (error) => {
76
+ if (this.signal.aborted) {
77
+ ended('stopped');
78
+ this.unknownUsage = true;
79
+ this.arrive(agent, callId, stoppedAnswer);
80
+ }
81
+ else if (error instanceof RunEndingToolError) {
82
+ ended('failed');
83
+ this.end(error.failure);
84
+ }
85
+ else {
86
+ // It broke the promise of answering, so what it spent is unknown.
87
+ ended('failed');
88
+ this.unknownUsage = true;
89
+ this.arrive(agent, callId, `It failed: ${describeFailure(error)}`);
90
+ }
91
+ })
92
+ .finally(() => {
93
+ if (unrecorded) {
94
+ this.unrecordedWorking--;
95
+ this.unrecordedEnded = true;
96
+ }
97
+ this.agents.delete(watched);
98
+ this.wake();
99
+ });
100
+ this.agents.add(watched);
101
+ }
102
+ }
103
+ /**
104
+ * The message holding every answer that arrived since the last one, if any did, and what
105
+ * became of each agent a stop interrupted.
106
+ */
107
+ takeAnswers() {
108
+ if (this.arrived.length === 0)
109
+ return undefined;
110
+ const answers = this.arrived.splice(0);
111
+ const text = [
112
+ 'This message is from the harness, not the user: agents you sent finished.',
113
+ ...answers,
114
+ ].join('\n\n');
115
+ return { role: 'user', content: [{ type: 'text', text }] };
116
+ }
117
+ /**
118
+ * Waits until an answer arrives, while agents are working. Rejects as cancelled when the run
119
+ * is cancelled meanwhile, and with the failure that ended it.
120
+ */
121
+ async nextAnswer() {
122
+ while (this.arrived.length === 0 && this.agents.size > 0) {
123
+ this.throwIfEnded();
124
+ this.signal.throwIfAborted();
125
+ await new Promise(resolve => {
126
+ const done = () => {
127
+ this.signal.removeEventListener('abort', done);
128
+ resolve();
129
+ };
130
+ this.woken = done;
131
+ this.signal.addEventListener('abort', done, { once: true });
132
+ });
133
+ }
134
+ this.throwIfEnded();
135
+ this.signal.throwIfAborted();
136
+ }
137
+ /** Throws the failure of an agent that ended the run, if one did. */
138
+ throwIfEnded() {
139
+ if (this.ended)
140
+ throw this.ended.failure;
141
+ }
142
+ /**
143
+ * Stops every agent still working, and waits until each one has: what they changed and spent
144
+ * belongs to the run, which ends only after them.
145
+ */
146
+ async stop() {
147
+ if (this.agents.size > 0)
148
+ this.stopping.abort();
149
+ await Promise.all(this.agents);
150
+ }
151
+ arrive(agent, callId, answer) {
152
+ this.arrived.push(`${labelOf(agent)}, sent by your call ${callId} with the task: ${shownTask(agent.task)}\nIts answer:\n${answer}`);
153
+ }
154
+ /** A failure ends the run: every agent is stopped for it. */
155
+ end(failure) {
156
+ this.ended ??= { failure };
157
+ this.stopping.abort();
158
+ }
159
+ wake() {
160
+ const woken = this.woken;
161
+ this.woken = undefined;
162
+ woken?.();
163
+ }
164
+ }
165
+ //# sourceMappingURL=backgroundAgents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backgroundAgents.js","sourceRoot":"","sources":["../../../../src/engines/domain/model/backgroundAgents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,EAAkB,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpF,gFAAgF;AAChF,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC,sFAAsF;AACtF,SAAS,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAa;IAC7C,OAAO,GAAG,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;AACtC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,QAAQ,CAAC,MAA4B;IACpD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,mCAAmC,KAAK,2QAA2Q,CAAC;AAC5T,CAAC;AAED,oGAAoG;AACpG,MAAM,aAAa,GAClB,uGAAuG,CAAC;AAEzG,SAAS,SAAS,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,MAAM,GAAG,mBAAmB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5F,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,gBAAgB;IACX,QAAQ,GAAG,IAAI,eAAe,EAAE,CAAC;IAClD,sFAAsF;IAC7E,MAAM,CAAc;IACZ,MAAM,GAAG,IAAI,GAAG,EAAiB,CAAC;IAClC,OAAO,GAAa,EAAE,CAAC;IAChC,KAAK,CAAmC;IACxC,iBAAiB,GAAG,CAAC,CAAC;IAC9B,mEAAmE;IAC3D,eAAe,GAAG,KAAK,CAAC;IACxB,KAAK,CAA2B;IACxC;;;OAGG;IACK,YAAY,GAAG,KAAK,CAAC;IAEZ,OAAO,CAAiC;IAEzD,sFAAsF;IACtF,YAAY,SAAsB,EAAE,OAAuC;QAC1E,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACjE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;IAED,mCAAmC;IACnC,IAAI,OAAO;QACV,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED;;;OAGG;IACH,oBAAoB;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,iBAAiB,GAAG,CAAC,IAAI,IAAI,CAAC,eAAe,CAAC;QAClE,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QAC7B,OAAO,MAAM,CAAC;IACf,CAAC;IAED,IAAI,YAAY;QACf,OAAO,IAAI,CAAC,YAAY,CAAC;IAC1B,CAAC;IAED,qEAAqE;IACrE,GAAG,CAAC,MAAc,EAAE,MAA4B,EAAE,UAAmB;QACpE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC5B,IAAI,UAAU;gBAAE,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,CAAC,OAA0C,EAAE,EAAE;gBAC5D,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;gBACjC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YAC1E,CAAC,CAAC;YACF,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM;iBAC1B,IAAI,CACJ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;gBACpB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;gBACtC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YAClC,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;gBAClB,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBACzB,KAAK,CAAC,SAAS,CAAC,CAAC;oBACjB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;oBACzB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;gBAC3C,CAAC;qBAAM,IAAI,KAAK,YAAY,kBAAkB,EAAE,CAAC;oBAChD,KAAK,CAAC,QAAQ,CAAC,CAAC;oBAChB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACzB,CAAC;qBAAM,CAAC;oBACP,kEAAkE;oBAClE,KAAK,CAAC,QAAQ,CAAC,CAAC;oBAChB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;oBACzB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBACpE,CAAC;YACF,CAAC,CACD;iBACA,OAAO,CAAC,GAAG,EAAE;gBACb,IAAI,UAAU,EAAE,CAAC;oBAChB,IAAI,CAAC,iBAAiB,EAAE,CAAC;oBACzB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;gBAC7B,CAAC;gBACD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;YACb,CAAC,CAAC,CAAC;YACJ,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACF,CAAC;IAED;;;OAGG;IACH,WAAW;QACV,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG;YACZ,2EAA2E;YAC3E,GAAG,OAAO;SACV,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACf,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC5D,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC1D,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;YAC7B,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;gBACjC,MAAM,IAAI,GAAG,GAAG,EAAE;oBACjB,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;oBAC/C,OAAO,EAAE,CAAC;gBACX,CAAC,CAAC;gBACF,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;gBAClB,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7D,CAAC,CAAC,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;IAC9B,CAAC;IAED,qEAAqE;IACrE,YAAY;QACX,IAAI,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI;QACT,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC;YAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAChD,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAEO,MAAM,CAAC,KAAgB,EAAE,MAAc,EAAE,MAAc;QAC9D,IAAI,CAAC,OAAO,CAAC,IAAI,CAChB,GAAG,OAAO,CAAC,KAAK,CAAC,uBAAuB,MAAM,mBAAmB,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,kBAAkB,MAAM,EAAE,CAChH,CAAC;IACH,CAAC;IAED,6DAA6D;IACrD,GAAG,CAAC,OAAgB;QAC3B,IAAI,CAAC,KAAK,KAAK,EAAE,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAEO,IAAI;QACX,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACvB,KAAK,EAAE,EAAE,CAAC;IACX,CAAC;CACD","sourcesContent":["import type { ProgressEvent } from '#src/agent/domain/agent';\nimport type { Message } from '#src/llm/domain/message';\nimport { describeFailure } from '#src/shared/domain/providerFailure';\nimport { type Delegated, RunEndingToolError } from '#src/tools/domain/preparedCall';\n\n// How much of a task the answer repeats, so the model can tell which one it is.\nconst shownTaskCharacters = 200;\n\n/** How the model that sent an agent is told which one it is, such as `explorer 2`. */\nfunction labelOf({ name, subagent }: Delegated): string {\n\treturn `${name} ${String(subagent)}`;\n}\n\n/** What the model is told of the agents a call sent, as that call's result. */\nexport function sentNote(agents: readonly Delegated[]): string {\n\tconst names = agents.map(labelOf).join(', ');\n\treturn `Sent to work in the background: ${names}. Each one's answer reaches you in a message of its own once it finishes. Keep working meanwhile. When you have nothing else to do until they answer, say briefly that you are waiting, without calling a tool: the run waits for them, and you go on with their answers.`;\n}\n\n// What a model is told of an agent the end of its run stopped, in a conversation kept for the next.\nconst stoppedAnswer =\n\t'None: it was stopped when the run ended, before it answered. Anything it changed stays as it left it.';\n\nfunction shownTask(task: string): string {\n\treturn task.length > shownTaskCharacters ? `${task.slice(0, shownTaskCharacters)}…` : task;\n}\n\n/**\n * The agents a run's calls sent to work in the background. Each one's answer waits here, in the\n * order they finished, until the model's next step takes it. A failure that ends the run stops\n * every agent still working at once, and the run at its next boundary.\n */\nexport class BackgroundAgents {\n\tprivate readonly stopping = new AbortController();\n\t/** What the agents work under: the run's own signal, or the stop of the run's end. */\n\treadonly signal: AbortSignal;\n\tprivate readonly agents = new Set<Promise<void>>();\n\tprivate readonly arrived: string[] = [];\n\tprivate ended: { failure: unknown } | undefined;\n\tprivate unrecordedWorking = 0;\n\t// Such an agent ended since `mayHaveChangedUnseen` was last asked.\n\tprivate unrecordedEnded = false;\n\tprivate woken: (() => void) | undefined;\n\t/**\n\t * What some agent spent is unknown: it was stopped while it worked, or failed in a way that\n\t * carries no usage.\n\t */\n\tprivate unknownUsage = false;\n\n\tprivate readonly narrate: (event: ProgressEvent) => void;\n\n\t/** `narrate` tells the person watching when each agent ended; it must never throw. */\n\tconstructor(runSignal: AbortSignal, narrate: (event: ProgressEvent) => void) {\n\t\tthis.signal = AbortSignal.any([runSignal, this.stopping.signal]);\n\t\tthis.narrate = narrate;\n\t}\n\n\t/** Some agent is still working. */\n\tget working(): boolean {\n\t\treturn this.agents.size > 0;\n\t}\n\n\t/**\n\t * Whether an agent that may change files the history does not record worked since the last\n\t * time this was asked: one is working now, or one ended meanwhile.\n\t */\n\tmayHaveChangedUnseen(): boolean {\n\t\tconst worked = this.unrecordedWorking > 0 || this.unrecordedEnded;\n\t\tthis.unrecordedEnded = false;\n\t\treturn worked;\n\t}\n\n\tget usageUnknown(): boolean {\n\t\treturn this.unknownUsage;\n\t}\n\n\t/** Watches the agents call `callId` sent, until each one answers. */\n\tadd(callId: string, agents: readonly Delegated[], unrecorded: boolean): void {\n\t\tfor (const agent of agents) {\n\t\t\tif (unrecorded) this.unrecordedWorking++;\n\t\t\tconst ended = (outcome: 'answered' | 'failed' | 'stopped') => {\n\t\t\t\tconst { name, subagent } = agent;\n\t\t\t\tthis.narrate({ type: 'subagentEnded', callId, subagent, name, outcome });\n\t\t\t};\n\t\t\tconst watched = agent.answer\n\t\t\t\t.then(\n\t\t\t\t\t({ text, failed }) => {\n\t\t\t\t\t\tended(failed ? 'failed' : 'answered');\n\t\t\t\t\t\tthis.arrive(agent, callId, text);\n\t\t\t\t\t},\n\t\t\t\t\t(error: unknown) => {\n\t\t\t\t\t\tif (this.signal.aborted) {\n\t\t\t\t\t\t\tended('stopped');\n\t\t\t\t\t\t\tthis.unknownUsage = true;\n\t\t\t\t\t\t\tthis.arrive(agent, callId, stoppedAnswer);\n\t\t\t\t\t\t} else if (error instanceof RunEndingToolError) {\n\t\t\t\t\t\t\tended('failed');\n\t\t\t\t\t\t\tthis.end(error.failure);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// It broke the promise of answering, so what it spent is unknown.\n\t\t\t\t\t\t\tended('failed');\n\t\t\t\t\t\t\tthis.unknownUsage = true;\n\t\t\t\t\t\t\tthis.arrive(agent, callId, `It failed: ${describeFailure(error)}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\t.finally(() => {\n\t\t\t\t\tif (unrecorded) {\n\t\t\t\t\t\tthis.unrecordedWorking--;\n\t\t\t\t\t\tthis.unrecordedEnded = true;\n\t\t\t\t\t}\n\t\t\t\t\tthis.agents.delete(watched);\n\t\t\t\t\tthis.wake();\n\t\t\t\t});\n\t\t\tthis.agents.add(watched);\n\t\t}\n\t}\n\n\t/**\n\t * The message holding every answer that arrived since the last one, if any did, and what\n\t * became of each agent a stop interrupted.\n\t */\n\ttakeAnswers(): Message | undefined {\n\t\tif (this.arrived.length === 0) return undefined;\n\t\tconst answers = this.arrived.splice(0);\n\t\tconst text = [\n\t\t\t'This message is from the harness, not the user: agents you sent finished.',\n\t\t\t...answers,\n\t\t].join('\\n\\n');\n\t\treturn { role: 'user', content: [{ type: 'text', text }] };\n\t}\n\n\t/**\n\t * Waits until an answer arrives, while agents are working. Rejects as cancelled when the run\n\t * is cancelled meanwhile, and with the failure that ended it.\n\t */\n\tasync nextAnswer(): Promise<void> {\n\t\twhile (this.arrived.length === 0 && this.agents.size > 0) {\n\t\t\tthis.throwIfEnded();\n\t\t\tthis.signal.throwIfAborted();\n\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\tconst done = () => {\n\t\t\t\t\tthis.signal.removeEventListener('abort', done);\n\t\t\t\t\tresolve();\n\t\t\t\t};\n\t\t\t\tthis.woken = done;\n\t\t\t\tthis.signal.addEventListener('abort', done, { once: true });\n\t\t\t});\n\t\t}\n\t\tthis.throwIfEnded();\n\t\tthis.signal.throwIfAborted();\n\t}\n\n\t/** Throws the failure of an agent that ended the run, if one did. */\n\tthrowIfEnded(): void {\n\t\tif (this.ended) throw this.ended.failure;\n\t}\n\n\t/**\n\t * Stops every agent still working, and waits until each one has: what they changed and spent\n\t * belongs to the run, which ends only after them.\n\t */\n\tasync stop(): Promise<void> {\n\t\tif (this.agents.size > 0) this.stopping.abort();\n\t\tawait Promise.all(this.agents);\n\t}\n\n\tprivate arrive(agent: Delegated, callId: string, answer: string): void {\n\t\tthis.arrived.push(\n\t\t\t`${labelOf(agent)}, sent by your call ${callId} with the task: ${shownTask(agent.task)}\\nIts answer:\\n${answer}`,\n\t\t);\n\t}\n\n\t/** A failure ends the run: every agent is stopped for it. */\n\tprivate end(failure: unknown): void {\n\t\tthis.ended ??= { failure };\n\t\tthis.stopping.abort();\n\t}\n\n\tprivate wake(): void {\n\t\tconst woken = this.woken;\n\t\tthis.woken = undefined;\n\t\twoken?.();\n\t}\n}\n"]}
@@ -68,7 +68,8 @@ export declare class LLMAgent implements Agent {
68
68
  /**
69
69
  * Always answers the call: a missing tool, a denied call or a failing one becomes an error
70
70
  * result, so the model can correct itself. Only a cancellation, a failing approver, or host
71
- * code that failed inside the call, such as an agent's approver, escapes.
71
+ * code that failed inside the call escapes. A call that hands tasks to agents answers once
72
+ * it sent them, and `background` watches them from then on.
72
73
  */
73
74
  private runTool;
74
75
  /**
@@ -77,18 +78,10 @@ export declare class LLMAgent implements Agent {
77
78
  */
78
79
  private askUser;
79
80
  /**
80
- * One at a time and in order, so their effects and their narration never interleave. Only
81
- * calls of a `parallel` tool next to each other run at once, such as agents being delegated
82
- * to, each narrated as it goes.
81
+ * One at a time and in order, so their effects and their narration never interleave. Agents a
82
+ * call hands tasks to keep working in the background once it answered.
83
83
  */
84
84
  private runTools;
85
- /** The calls from `index` on whose tool is `parallel`, up to the first that is not. */
86
- private parallelFrom;
87
- /**
88
- * Runs the calls at once, their results in the order of the calls. The first failure that
89
- * ends the run stops the others, and the run ends once every one has.
90
- */
91
- private runTogether;
92
85
  /**
93
86
  * Narrates how a call ended and notes its command's ending, with the warning for the model
94
87
  * when that ending keeps coming back alike.