@mikode13/harness 9.3.1 → 11.0.0

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.
Files changed (58) hide show
  1. package/README.md +81 -10
  2. package/dist/agent/domain/agent.d.ts +31 -7
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/approval.d.ts +2 -1
  5. package/dist/agent/domain/approval.js +2 -1
  6. package/dist/agent/domain/approval.js.map +1 -1
  7. package/dist/agent/domain/question.d.ts +5 -0
  8. package/dist/agent/domain/question.js +11 -0
  9. package/dist/agent/domain/question.js.map +1 -1
  10. package/dist/agent/domain/runContext.d.ts +17 -0
  11. package/dist/agent/domain/runContext.js +9 -0
  12. package/dist/agent/domain/runContext.js.map +1 -1
  13. package/dist/agent/domain/transcript.d.ts +10 -0
  14. package/dist/agent/domain/transcript.js.map +1 -1
  15. package/dist/engines/domain/model/askUser.d.ts +0 -5
  16. package/dist/engines/domain/model/askUser.js +0 -9
  17. package/dist/engines/domain/model/askUser.js.map +1 -1
  18. package/dist/engines/domain/model/backgroundAgents.d.ts +60 -0
  19. package/dist/engines/domain/model/backgroundAgents.js +165 -0
  20. package/dist/engines/domain/model/backgroundAgents.js.map +1 -0
  21. package/dist/engines/domain/model/llmAgent.d.ts +12 -3
  22. package/dist/engines/domain/model/llmAgent.js +184 -74
  23. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  24. package/dist/factory/infrastructure/agentLLMFactory.d.ts +7 -3
  25. package/dist/factory/infrastructure/agentLLMFactory.js +3 -3
  26. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/tools/domain/preparedCall.d.ts +52 -2
  31. package/dist/tools/domain/preparedCall.js +15 -0
  32. package/dist/tools/domain/preparedCall.js.map +1 -1
  33. package/dist/tools/domain/readRegistry.d.ts +7 -0
  34. package/dist/tools/domain/readRegistry.js.map +1 -1
  35. package/dist/tools/infrastructure/agentAsTool.d.ts +63 -0
  36. package/dist/tools/infrastructure/agentAsTool.js +143 -0
  37. package/dist/tools/infrastructure/agentAsTool.js.map +1 -0
  38. package/dist/tools/infrastructure/commandTool.js +6 -2
  39. package/dist/tools/infrastructure/commandTool.js.map +1 -1
  40. package/dist/tools/infrastructure/defineTool.d.ts +14 -1
  41. package/dist/tools/infrastructure/defineTool.js +24 -0
  42. package/dist/tools/infrastructure/defineTool.js.map +1 -1
  43. package/dist/tools/infrastructure/exploreTool.d.ts +2 -2
  44. package/dist/tools/infrastructure/exploreTool.js +36 -56
  45. package/dist/tools/infrastructure/exploreTool.js.map +1 -1
  46. package/dist/tools/infrastructure/fileEditor.d.ts +2 -0
  47. package/dist/tools/infrastructure/fileEditor.js +5 -1
  48. package/dist/tools/infrastructure/fileEditor.js.map +1 -1
  49. package/dist/tools/infrastructure/fileFormatter.d.ts +1 -0
  50. package/dist/tools/infrastructure/fileFormatter.js +5 -1
  51. package/dist/tools/infrastructure/fileFormatter.js.map +1 -1
  52. package/dist/tools/infrastructure/subagents.d.ts +19 -0
  53. package/dist/tools/infrastructure/subagents.js +29 -0
  54. package/dist/tools/infrastructure/subagents.js.map +1 -0
  55. package/dist/tools/infrastructure/writeSession.d.ts +8 -0
  56. package/dist/tools/infrastructure/writeSession.js +37 -0
  57. package/dist/tools/infrastructure/writeSession.js.map +1 -1
  58. package/package.json +1 -1
@@ -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":"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","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"]}
@@ -67,8 +67,9 @@ export declare class LLMAgent implements Agent {
67
67
  private denial;
68
68
  /**
69
69
  * Always answers the call: a missing tool, a denied call or a failing one becomes an error
70
- * result, so the model can correct itself. Only a cancellation, or a failing approver,
71
- * escapes, whatever error the tool turned it into.
70
+ * result, so the model can correct itself. Only a cancellation, a failing approver, or host
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
  /**
@@ -76,8 +77,16 @@ export declare class LLMAgent implements Agent {
76
77
  * correct; an asker that fails or answers wrongly is host code, and ends the run.
77
78
  */
78
79
  private askUser;
79
- /** One at a time and in order, so their effects and their narration never interleave. */
80
+ /**
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
+ */
80
84
  private runTools;
85
+ /**
86
+ * Narrates how a call ended and notes its command's ending, with the warning for the model
87
+ * when that ending keeps coming back alike.
88
+ */
89
+ private settle;
81
90
  /**
82
91
  * On a loop found, the fifth ending alike of a command or the fifth answer in a row asking for
83
92
  * the same calls: the note for the model if the user lets the run go on, or the failure that
@@ -1,18 +1,20 @@
1
1
  import { readDecision, } from '#src/agent/domain/approval';
2
2
  import { ignoreProgress, } from '#src/agent/domain/agent';
3
- import { RunContext, runContextOf, withRunContext, } from '#src/agent/domain/runContext';
3
+ import { delegationOf, RunContext, runContextOf, withRunContext, } from '#src/agent/domain/runContext';
4
4
  import { InvalidAgentConfigError, RecoverableError, UnrecoverableError, withSpentTokens, } from '#src/shared/domain/errors';
5
5
  import { classifyHostFailure, classifyLocalFailure, classifyProviderFailure, describeFailure, treatErrors, } from '#src/shared/domain/providerFailure';
6
6
  import { Conversation } from '#src/llm/domain/conversation';
7
7
  import { asNative, nativeOf } from '#src/llm/domain/tool';
8
- import { prepareCall, ToolExecutionError, } from '#src/tools/domain/preparedCall';
8
+ import { readAnswers } from '#src/agent/domain/question';
9
+ import { isDelegating, prepareCall, RunEndingToolError, ToolExecutionError, } from '#src/tools/domain/preparedCall';
9
10
  import { ReadRegistry } from '#src/tools/domain/readRegistry';
10
11
  import { InvalidToolInputError } from '#src/tools/domain/tool';
11
12
  import { isAbortError } from '#src/shared/domain/isAbortError';
12
13
  import { addTokens } from '#src/shared/domain/tokens';
13
14
  import { anotherAttempt, askAt, CommandLoops, extensionNote, loopQuestion, loopStop, loopWarning, repeatedAnswersAt, repeatNote, repeatQuestion, repeatStop, stop, warnAt, } from "./commandLoops.js";
15
+ import { BackgroundAgents, sentNote } from "./backgroundAgents.js";
14
16
  import { measureContext } from "./stepContext.js";
15
- import { askUserDefinition, askUserName, describeAnswers, readAnswers, readQuestions, summarizeQuestions, } from "./askUser.js";
17
+ import { askUserDefinition, askUserName, describeAnswers, readQuestions, summarizeQuestions, } from "./askUser.js";
16
18
  import { followHistory, transcriptOf, undoNote, } from "./historyFollower.js";
17
19
  function describePart(part) {
18
20
  switch (part.type) {
@@ -269,9 +271,11 @@ export class LLMAgent {
269
271
  * place. Only a `destructive` call is asked about: git can undo a `mutating` one. A throwing
270
272
  * approver ends the run, as any host callback does.
271
273
  */
272
- async denial(call, { risk, command }, signal, approve) {
274
+ async denial(call, prepared, signal, approve) {
275
+ const { risk } = prepared;
273
276
  if (this.autoApprove || risk !== 'destructive')
274
277
  return undefined;
278
+ const command = 'command' in prepared ? prepared.command : undefined;
275
279
  const retry = 'Do not retry the same call; choose another way, or ask the user.';
276
280
  if (!approve) {
277
281
  return `The call to "${call.name}" was denied: it is destructive, and no one was asked to allow it. ${retry}`;
@@ -305,10 +309,11 @@ export class LLMAgent {
305
309
  }
306
310
  /**
307
311
  * Always answers the call: a missing tool, a denied call or a failing one becomes an error
308
- * result, so the model can correct itself. Only a cancellation, or a failing approver,
309
- * escapes, whatever error the tool turned it into.
312
+ * result, so the model can correct itself. Only a cancellation, a failing approver, or host
313
+ * code that failed inside the call escapes. A call that hands tasks to agents answers once
314
+ * it sent them, and `background` watches them from then on.
310
315
  */
311
- async runTool(call, { signal, onProgress: callback = ignoreProgress, approve, ask }, effects, toolCall) {
316
+ async runTool(call, { signal, onProgress: callback = ignoreProgress, approve, ask }, effects, toolCall, background) {
312
317
  const result = { type: 'toolResult', callId: call.id, name: call.name };
313
318
  signal.throwIfAborted();
314
319
  if (call.name === askUserName && ask && !this.tools.has(askUserName)) {
@@ -346,7 +351,10 @@ export class LLMAgent {
346
351
  }
347
352
  let prepared;
348
353
  try {
349
- prepared = await prepareCall(tool, call.input, signal, { ...toolCall, callId: call.id });
354
+ const context = { ...toolCall, callId: call.id };
355
+ prepared = isDelegating(tool)
356
+ ? await tool.delegate(call.input, signal, context)
357
+ : await prepareCall(tool, call.input, signal, context);
350
358
  }
351
359
  catch (error) {
352
360
  signal.throwIfAborted();
@@ -384,6 +392,25 @@ export class LLMAgent {
384
392
  // Set before `execute`, so a tool that fails halfway still counts: it may have had effects.
385
393
  effects.toolRan = true;
386
394
  const unrecorded = prepared.unrecorded === true && prepared.risk !== 'safe';
395
+ if ('start' in prepared) {
396
+ // Before its agents start, so the person watching reads it before anything they narrate.
397
+ emit({
398
+ type: 'tool',
399
+ id: call.id,
400
+ name: call.name,
401
+ status: 'sent',
402
+ ...(summary === undefined ? {} : { summary }),
403
+ }, callback);
404
+ const agents = prepared.start(background.signal);
405
+ background.add(call.id, agents, unrecorded);
406
+ return {
407
+ result: { ...result, output: sentNote(agents), isError: false },
408
+ denied: false,
409
+ sent: true,
410
+ unrecorded,
411
+ ...(summary === undefined ? {} : { summary }),
412
+ };
413
+ }
387
414
  try {
388
415
  const output = await prepared.run(signal);
389
416
  const ending = prepared.ending?.();
@@ -398,6 +425,8 @@ export class LLMAgent {
398
425
  }
399
426
  catch (error) {
400
427
  signal.throwIfAborted();
428
+ if (error instanceof RunEndingToolError)
429
+ throw error.failure;
401
430
  const ending = prepared.ending?.();
402
431
  return {
403
432
  result: { ...result, output: describeFailure(error), isError: true },
@@ -447,11 +476,14 @@ export class LLMAgent {
447
476
  summary,
448
477
  };
449
478
  }
450
- /** One at a time and in order, so their effects and their narration never interleave. */
479
+ /**
480
+ * One at a time and in order, so their effects and their narration never interleave. Agents a
481
+ * call hands tasks to keep working in the background once it answered.
482
+ */
451
483
  async runTools(calls, options, effects, toolCall,
452
484
  // Throws the failure of a progress callback a call narrated through.
453
- throwIfProgressFailed, loops) {
454
- const { signal, onProgress: callback = ignoreProgress } = options;
485
+ throwIfProgressFailed, loops, background) {
486
+ const { signal } = options;
455
487
  const results = [];
456
488
  for (const [index, call] of calls.entries()) {
457
489
  // A call before this one may have ignored the cancellation; this one must not start,
@@ -459,48 +491,15 @@ export class LLMAgent {
459
491
  signal.throwIfAborted();
460
492
  // `runTool` announces the call only once it is allowed to run, so a consumer never
461
493
  // shows a call as running early, nor while the user is still being asked.
462
- const ran = await this.runTool(call, options, effects, toolCall);
463
- const { denied, diff, summary, exitCode, ending } = ran;
464
- let { result } = ran;
494
+ const ran = await this.runTool(call, options, effects, toolCall, background);
465
495
  // A call that failed to narrate ends the run before the next one can start.
466
496
  throwIfProgressFailed();
467
- if (ran.unrecorded)
497
+ // An agent at work since the last call may have changed what the history cannot copy.
498
+ if (background.mayHaveChangedUnseen())
468
499
  loops.unrecordedCallRan();
469
- const key = callKey(call);
470
- const label = summary ?? call.name;
471
- const alike = ending === undefined ? undefined : loops.observe(key, ending);
472
- // A command that changed what the history cannot copy hides that change from the next.
473
- if (ending !== undefined && ending.state === undefined)
474
- loops.unrecordedCallRan();
475
- // Narrated with it, so the person watching reads what the model is told.
476
- if (alike?.times === warnAt) {
477
- result = {
478
- ...result,
479
- output: `${result.output}\n\n${loopWarning(label, alike, options.ask !== undefined && !loops.wentOn)}`,
480
- };
481
- }
482
- if (denied) {
483
- emit({
484
- type: 'tool',
485
- id: call.id,
486
- name: call.name,
487
- status: 'denied',
488
- ...(summary === undefined ? {} : { summary }),
489
- }, callback);
490
- }
491
- // The diff of a change it made, for the person watching; only once it was made.
492
- else if (diff !== undefined) {
493
- emit({
494
- type: 'tool',
495
- id: call.id,
496
- name: call.name,
497
- status: 'completed',
498
- diff,
499
- ...(summary === undefined ? {} : { summary }),
500
- }, callback);
501
- }
502
- else
503
- narrate(result, callback, summary, exitCode);
500
+ const settled = this.settle(call, ran, loops, options);
501
+ let { result } = settled;
502
+ const { alike, key, label } = settled;
504
503
  // After its result is narrated, so the user is asked about the ending they have seen.
505
504
  if (alike !== undefined && alike.times >= askAt) {
506
505
  const decision = await this.decideOnLoop({
@@ -526,6 +525,55 @@ export class LLMAgent {
526
525
  }
527
526
  return { message: { role: 'tool', content: results } };
528
527
  }
528
+ /**
529
+ * Narrates how a call ended and notes its command's ending, with the warning for the model
530
+ * when that ending keeps coming back alike.
531
+ */
532
+ settle(call, ran, loops, { onProgress: callback = ignoreProgress, ask }) {
533
+ const { denied, diff, summary, exitCode, ending, sent } = ran;
534
+ let { result } = ran;
535
+ if (ran.unrecorded)
536
+ loops.unrecordedCallRan();
537
+ const key = callKey(call);
538
+ const label = summary ?? call.name;
539
+ const alike = ending === undefined ? undefined : loops.observe(key, ending);
540
+ // A command that changed what the history cannot copy hides that change from the next.
541
+ if (ending !== undefined && ending.state === undefined)
542
+ loops.unrecordedCallRan();
543
+ // Narrated with it, so the person watching reads what the model is told.
544
+ if (alike?.times === warnAt) {
545
+ result = {
546
+ ...result,
547
+ output: `${result.output}\n\n${loopWarning(label, alike, ask !== undefined && !loops.wentOn)}`,
548
+ };
549
+ }
550
+ if (denied) {
551
+ emit({
552
+ type: 'tool',
553
+ id: call.id,
554
+ name: call.name,
555
+ status: 'denied',
556
+ ...(summary === undefined ? {} : { summary }),
557
+ }, callback);
558
+ }
559
+ else if (sent === true) {
560
+ // Narrated as it sent its agents, which go on after it until each one ends.
561
+ }
562
+ // The diff of a change it made, for the person watching; only once it was made.
563
+ else if (diff !== undefined) {
564
+ emit({
565
+ type: 'tool',
566
+ id: call.id,
567
+ name: call.name,
568
+ status: 'completed',
569
+ diff,
570
+ ...(summary === undefined ? {} : { summary }),
571
+ }, callback);
572
+ }
573
+ else
574
+ narrate(result, callback, summary, exitCode);
575
+ return { result, alike, key, label };
576
+ }
529
577
  /**
530
578
  * On a loop found, the fifth ending alike of a command or the fifth answer in a row asking for
531
579
  * the same calls: the note for the model if the user lets the run go on, or the failure that
@@ -607,16 +655,17 @@ export class LLMAgent {
607
655
  response = await this.takeSteps(prompt, options, context, seen);
608
656
  }
609
657
  catch (error) {
610
- await this.transcribe(seen, isAbortError(error) ? 'cancelled' : 'failed', error);
658
+ await this.transcribe(seen, options, isAbortError(error) ? 'cancelled' : 'failed', error);
611
659
  throw error;
612
660
  }
613
- await this.transcribe(seen, 'completed');
661
+ await this.transcribe(seen, options, 'completed');
614
662
  return response;
615
663
  }
616
664
  /** Hands the run's transcript to the consumer, whose failure is logged, not the run's. */
617
- async transcribe(seen, outcome, error) {
665
+ async transcribe(seen, options, outcome, error) {
618
666
  if (!this.onTranscript)
619
667
  return;
668
+ const delegation = delegationOf(options);
620
669
  try {
621
670
  // A copy, so a consumer that changes it cannot change the agent's tools or conversation.
622
671
  const content = structuredClone({
@@ -629,7 +678,19 @@ export class LLMAgent {
629
678
  messages: seen.messages(),
630
679
  ...(outcome === 'completed' ? {} : { error: describeFailure(error) }),
631
680
  });
632
- await this.onTranscript({ outcome, content });
681
+ await this.onTranscript({
682
+ ...(delegation
683
+ ? {
684
+ delegation: {
685
+ tool: delegation.tool,
686
+ callId: delegation.callId,
687
+ subagent: delegation.subagent,
688
+ },
689
+ }
690
+ : {}),
691
+ outcome,
692
+ content,
693
+ });
633
694
  }
634
695
  catch (failure) {
635
696
  treatErrors(() => {
@@ -639,6 +700,22 @@ export class LLMAgent {
639
700
  }
640
701
  async takeSteps(prompt, options, context, seen) {
641
702
  const { signal, onProgress: callback = ignoreProgress } = options;
703
+ // A tool narrates through this without ever failing: a failing callback would reach
704
+ // only the tool, which turns any failure into a result for the model. The run ends on
705
+ // it once that tool returns.
706
+ let progressFailure;
707
+ const onProgress = (event) => {
708
+ if (progressFailure)
709
+ return;
710
+ try {
711
+ emit(event, callback);
712
+ }
713
+ catch (error) {
714
+ progressFailure = { error };
715
+ }
716
+ };
717
+ // The agents the run's calls send to work in the background.
718
+ const background = new BackgroundAgents(signal, onProgress);
642
719
  const start = Date.now();
643
720
  const runMessages = [{ role: 'user', content: [{ type: 'text', text: prompt }] }];
644
721
  let tokens;
@@ -682,23 +759,11 @@ export class LLMAgent {
682
759
  tokens = addTokens(tokens, spent);
683
760
  unreported ||= unreportedCall;
684
761
  };
685
- // A tool narrates through this without ever failing: a failing callback would reach
686
- // only the tool, which turns any failure into a result for the model. The run ends on
687
- // it once that tool returns.
688
- let progressFailure;
689
- const onProgress = (event) => {
690
- if (progressFailure)
691
- return;
692
- try {
693
- emit(event, callback);
694
- }
695
- catch (error) {
696
- progressFailure = { error };
697
- }
698
- };
762
+ // Also ends the run at its next boundary when an agent at work failed in a way that ends it.
699
763
  const throwIfProgressFailed = () => {
700
764
  if (progressFailure)
701
765
  throw progressFailure.error;
766
+ background.throwIfEnded();
702
767
  };
703
768
  // The agent's own `askUser` exists only for a run someone can answer, and never beside
704
769
  // a consumer's tool of that name, which the consumer gave first and which stays theirs.
@@ -714,7 +779,18 @@ export class LLMAgent {
714
779
  if (followed)
715
780
  this.seenRevision = followed.latestRevision;
716
781
  };
782
+ // A run kept although it failed stops its agents first, and tells the next run what
783
+ // became of each one, which would otherwise wait for answers that never come.
784
+ const keepFailed = async () => {
785
+ await background.stop();
786
+ const ended = background.takeAnswers();
787
+ if (ended)
788
+ runMessages.push(ended);
789
+ remember();
790
+ };
717
791
  for (let step = 1; step <= this.maxSteps; step++) {
792
+ // An agent at work may have narrated through a callback that failed.
793
+ throwIfProgressFailed();
718
794
  const { response, context: sent } = await this.llmCall(runMessages, after, {
719
795
  signal,
720
796
  step,
@@ -726,6 +802,7 @@ export class LLMAgent {
726
802
  // After the tokens are counted, so a throwing callback still reports them, and for an
727
803
  // unusable answer too: the call was sent, and billed.
728
804
  this.narrateStep(step, response.usage, { context: sent, tools }, callback);
805
+ throwIfProgressFailed();
729
806
  if (response.stopReason === 'truncated') {
730
807
  // What it wrote before the output limit, kept with the run so the next run can
731
808
  // take it up. Not its calls, whose input may be cut, nor what only the provider
@@ -735,7 +812,7 @@ export class LLMAgent {
735
812
  narrate(part, callback);
736
813
  if (written.length > 0)
737
814
  runMessages.push({ role: 'assistant', content: written });
738
- remember();
815
+ await keepFailed();
739
816
  throw new UnrecoverableError('The LLM stopped before completing its answer', {
740
817
  cause: 'The model stopped with "truncated". The run, and what the answer held before the limit, are kept in the conversation.',
741
818
  });
@@ -755,6 +832,24 @@ export class LLMAgent {
755
832
  }
756
833
  const calls = response.message.content.filter(part => part.type === 'toolCall');
757
834
  if (calls.length === 0) {
835
+ // An answer given while agents work, or before the model read what they answered,
836
+ // is a wait: the model goes on with their answers.
837
+ let answers = background.takeAnswers();
838
+ if (!answers && background.working && step < this.maxSteps) {
839
+ await background.nextAnswer();
840
+ throwIfProgressFailed();
841
+ answers = background.takeAnswers();
842
+ }
843
+ if (answers || background.working) {
844
+ if (step === this.maxSteps) {
845
+ throw new UnrecoverableError('The LLM did not finish within its step limit', {
846
+ cause: `The model was still waiting for the agents it sent after ${String(this.maxSteps)} steps.`,
847
+ });
848
+ }
849
+ if (answers)
850
+ runMessages.push(answers);
851
+ continue;
852
+ }
758
853
  remember();
759
854
  return {
760
855
  // Only the final answer: text from earlier steps was narrated as it came.
@@ -762,7 +857,7 @@ export class LLMAgent {
762
857
  .filter(part => part.type === 'text')
763
858
  .map(part => part.text)
764
859
  .join('\n'),
765
- tokens: unreported ? undefined : tokens,
860
+ tokens: unreported || background.usageUnknown ? undefined : tokens,
766
861
  duration: (Date.now() - start) / 1000,
767
862
  };
768
863
  }
@@ -793,7 +888,7 @@ export class LLMAgent {
793
888
  ...rest,
794
889
  ],
795
890
  });
796
- remember();
891
+ await keepFailed();
797
892
  throw decision;
798
893
  }
799
894
  repeats.reset();
@@ -801,13 +896,25 @@ export class LLMAgent {
801
896
  }
802
897
  // On the last step no call is left to send the results to, so the tools do not run.
803
898
  if (step < this.maxSteps) {
804
- const { message, stopped } = await this.runTools(calls, options, effects, { run: context, reads, onProgress, spend }, throwIfProgressFailed, loops);
899
+ const { message, stopped } = await this.runTools(calls, options, effects, {
900
+ run: context,
901
+ reads,
902
+ onProgress,
903
+ spend,
904
+ depth: delegationOf(options)?.depth ?? 0,
905
+ ...(options.approve ? { approve: options.approve } : {}),
906
+ ...(options.ask ? { ask: options.ask } : {}),
907
+ }, throwIfProgressFailed, loops, background);
805
908
  runMessages.push(repeatedNote === undefined ? message : withNote(message, repeatedNote));
806
909
  // Kept like a run cut by the output limit: the user may take it up.
807
910
  if (stopped) {
808
- remember();
911
+ await keepFailed();
809
912
  throw stopped;
810
913
  }
914
+ // What agents answered meanwhile, after the results the model is waiting for.
915
+ const answers = background.takeAnswers();
916
+ if (answers)
917
+ runMessages.push(answers);
811
918
  }
812
919
  }
813
920
  throw new UnrecoverableError('The LLM did not finish within its step limit', {
@@ -815,6 +922,9 @@ export class LLMAgent {
815
922
  });
816
923
  }
817
924
  catch (error) {
925
+ // The run ends only after the agents still working, which stop with it: what they
926
+ // changed and spent is the run's.
927
+ await background.stop();
818
928
  let failure = error;
819
929
  if (effects.toolRan && error instanceof RecoverableError) {
820
930
  // RetryingAgent would run the whole prompt again, and with it every tool this run
@@ -828,7 +938,7 @@ export class LLMAgent {
828
938
  failure = unrecoverable;
829
939
  }
830
940
  // The run spent the tokens of every step before the one that failed.
831
- throw withSpentTokens(failure, tokens, unreported);
941
+ throw withSpentTokens(failure, tokens, unreported || background.usageUnknown);
832
942
  }
833
943
  }
834
944
  }