@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.
- package/README.md +81 -10
- package/dist/agent/domain/agent.d.ts +31 -7
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +2 -1
- package/dist/agent/domain/approval.js +2 -1
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/question.d.ts +5 -0
- package/dist/agent/domain/question.js +11 -0
- package/dist/agent/domain/question.js.map +1 -1
- package/dist/agent/domain/runContext.d.ts +17 -0
- package/dist/agent/domain/runContext.js +9 -0
- package/dist/agent/domain/runContext.js.map +1 -1
- package/dist/agent/domain/transcript.d.ts +10 -0
- package/dist/agent/domain/transcript.js.map +1 -1
- package/dist/engines/domain/model/askUser.d.ts +0 -5
- package/dist/engines/domain/model/askUser.js +0 -9
- package/dist/engines/domain/model/askUser.js.map +1 -1
- package/dist/engines/domain/model/backgroundAgents.d.ts +60 -0
- package/dist/engines/domain/model/backgroundAgents.js +165 -0
- package/dist/engines/domain/model/backgroundAgents.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +12 -3
- package/dist/engines/domain/model/llmAgent.js +184 -74
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +7 -3
- package/dist/factory/infrastructure/agentLLMFactory.js +3 -3
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +52 -2
- package/dist/tools/domain/preparedCall.js +15 -0
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/domain/readRegistry.d.ts +7 -0
- package/dist/tools/domain/readRegistry.js.map +1 -1
- package/dist/tools/infrastructure/agentAsTool.d.ts +63 -0
- package/dist/tools/infrastructure/agentAsTool.js +143 -0
- package/dist/tools/infrastructure/agentAsTool.js.map +1 -0
- package/dist/tools/infrastructure/commandTool.js +6 -2
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/defineTool.d.ts +14 -1
- package/dist/tools/infrastructure/defineTool.js +24 -0
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.d.ts +2 -2
- package/dist/tools/infrastructure/exploreTool.js +36 -56
- package/dist/tools/infrastructure/exploreTool.js.map +1 -1
- package/dist/tools/infrastructure/fileEditor.d.ts +2 -0
- package/dist/tools/infrastructure/fileEditor.js +5 -1
- package/dist/tools/infrastructure/fileEditor.js.map +1 -1
- package/dist/tools/infrastructure/fileFormatter.d.ts +1 -0
- package/dist/tools/infrastructure/fileFormatter.js +5 -1
- package/dist/tools/infrastructure/fileFormatter.js.map +1 -1
- package/dist/tools/infrastructure/subagents.d.ts +19 -0
- package/dist/tools/infrastructure/subagents.js +29 -0
- package/dist/tools/infrastructure/subagents.js.map +1 -0
- package/dist/tools/infrastructure/writeSession.d.ts +8 -0
- package/dist/tools/infrastructure/writeSession.js +37 -0
- package/dist/tools/infrastructure/writeSession.js.map +1 -1
- 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,
|
|
71
|
-
*
|
|
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
|
-
/**
|
|
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 {
|
|
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,
|
|
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,
|
|
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,
|
|
309
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
|
470
|
-
|
|
471
|
-
const alike
|
|
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({
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
}
|