@mikode13/harness 5.0.0 → 5.2.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 +24 -2
- package/dist/agent/domain/agent.d.ts +63 -1
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/engines/domain/model/llmAgent.d.ts +2 -0
- package/dist/engines/domain/model/llmAgent.js +101 -26
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/engines/domain/model/stepContext.d.ts +16 -0
- package/dist/engines/domain/model/stepContext.js +60 -0
- package/dist/engines/domain/model/stepContext.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/llm/domain/llm.d.ts +7 -0
- package/dist/llm/domain/llm.js.map +1 -1
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +2 -1
- package/dist/llm/infrastructure/claudeLLMClient.js +1 -0
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/llm/infrastructure/openAILLMClient.d.ts +2 -1
- package/dist/llm/infrastructure/openAILLMClient.js +2 -1
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +10 -5
- package/dist/orchestration/domain/model/orchestratorAgent.js +24 -6
- package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +14 -1
- package/dist/tools/domain/preparedCall.js +12 -0
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/domain/tool.d.ts +14 -0
- package/dist/tools/domain/tool.js +7 -1
- package/dist/tools/domain/tool.js.map +1 -1
- package/dist/tools/infrastructure/commandTool.d.ts +1 -1
- package/dist/tools/infrastructure/commandTool.js +32 -1
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/defineTool.d.ts +5 -3
- package/dist/tools/infrastructure/defineTool.js +14 -2
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/editFormats.js +26 -1
- package/dist/tools/infrastructure/editFormats.js.map +1 -1
- package/dist/tools/infrastructure/showChangesTool.js +1 -0
- package/dist/tools/infrastructure/showChangesTool.js.map +1 -1
- package/dist/tools/infrastructure/trackedReadFile.js +2 -1
- package/dist/tools/infrastructure/trackedReadFile.js.map +1 -1
- package/dist/tools/infrastructure/validatedSummarizer.d.ts +7 -0
- package/dist/tools/infrastructure/validatedSummarizer.js +15 -0
- package/dist/tools/infrastructure/validatedSummarizer.js.map +1 -0
- package/dist/tools/infrastructure/workspaceTools.d.ts +6 -0
- package/dist/tools/infrastructure/workspaceTools.js +16 -0
- package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,6 +153,15 @@ releases, so render the ones you know and ignore the rest. Swapping the agent fo
|
|
|
153
153
|
reviewer workflow, changes nothing else in the snippet above.
|
|
154
154
|
`createOrchestrator({ provider: 'anthropic' })` runs every role on one provider, for
|
|
155
155
|
example when the other one is out of quota.
|
|
156
|
+
|
|
157
|
+
The optional `agent` field on an event identifies the orchestrator role that produced
|
|
158
|
+
it (`planner`, `executor`, or `reviewer`). The optional `summary` on a `tool` event
|
|
159
|
+
is narration only: it describes the model's call and is not the tool result. A failed
|
|
160
|
+
configured command may also carry its optional `exitCode`; its output and changes
|
|
161
|
+
still arrive in the tool result. A role label is applied by the orchestrator,
|
|
162
|
+
including retries and later rounds, so a nested agent cannot accidentally attribute
|
|
163
|
+
its events to another role. Consumers should continue to handle events without these
|
|
164
|
+
optional fields.
|
|
156
165
|
`systemPrompts: { planner, executor, reviewer }` replaces a role's instructions;
|
|
157
166
|
a role left out keeps the harness's own, and the reviewer must still answer with
|
|
158
167
|
the JSON decision described on the option.
|
|
@@ -241,7 +250,10 @@ bills the key instead of the subscription.
|
|
|
241
250
|
`createLLMAgent` takes a system prompt and the tools the model may call, and
|
|
242
251
|
returns the same `Agent`. `defineTool` builds a tool from a Zod 4 schema; a field the
|
|
243
252
|
model may leave out is `.nullable()`, not optional, because both APIs call tools in
|
|
244
|
-
strict mode
|
|
253
|
+
strict mode. An optional `summarize` function receives the same input after Zod
|
|
254
|
+
validation and supplies the tool's short progress narration. It is called once per
|
|
255
|
+
model call; its output is never sent to the provider, and a throwing summarizer
|
|
256
|
+
ends the run as an `UnrecoverableError` before the tool is prepared or executed:
|
|
245
257
|
|
|
246
258
|
```ts
|
|
247
259
|
import {
|
|
@@ -257,6 +269,7 @@ const now = defineTool({
|
|
|
257
269
|
description: 'The current time, in ISO 8601.',
|
|
258
270
|
input: z.object({}),
|
|
259
271
|
risk: 'safe',
|
|
272
|
+
summarize: () => 'current time',
|
|
260
273
|
execute: async () => new Date().toISOString(),
|
|
261
274
|
});
|
|
262
275
|
|
|
@@ -530,7 +543,16 @@ nor the agent decides what runs, or for how long:
|
|
|
530
543
|
```
|
|
531
544
|
|
|
532
545
|
The CLI shows each command as the line it will run before asking, and "always" allows
|
|
533
|
-
that command again, not every command.
|
|
546
|
+
that command again, not every command. Tool calls finish as one dimmed line with an
|
|
547
|
+
icon, role, tool name, summary and duration; a failed configured command includes
|
|
548
|
+
`exit N` beside its duration, while a command event also includes its exit code.
|
|
549
|
+
A denied call is marked `denied`. Messages remain normal text under a bold role
|
|
550
|
+
heading. Reasoning is dim italic and complete by default; pass
|
|
551
|
+
`--thinking-headings` to show only Markdown bold headings as `∴ …`. A tool start
|
|
552
|
+
updates the spinner rather than printing a second line, and cancelling a turn clears
|
|
553
|
+
its pending spinner state. Pass `--steps <file>` to append one JSON line per call to the
|
|
554
|
+
model to that file: the call's tokens and what its context held, by origin, for measuring
|
|
555
|
+
what a run spends.
|
|
534
556
|
|
|
535
557
|
`cli/cli.ts` currently enables `autoApprove` for its Agent SDK agents, not for `--llm`.
|
|
536
558
|
This maps to each provider's permission-bypass mode and grants those processes
|
|
@@ -22,7 +22,10 @@ export interface AgentResponse {
|
|
|
22
22
|
runId?: string;
|
|
23
23
|
}
|
|
24
24
|
export type Callback = (item: ProgressEvent) => void;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* What a run needs besides its prompt. RetryingAgent and InstructedAgent pass it on whole;
|
|
27
|
+
* OrchestratorAgent copies its own enumerable fields and replaces only `onProgress` per role.
|
|
28
|
+
*/
|
|
26
29
|
export interface RunOptions {
|
|
27
30
|
/** Cancels the run, which then rejects with the signal's `AbortError`. */
|
|
28
31
|
signal: AbortSignal;
|
|
@@ -46,32 +49,43 @@ export type ProgressEvent = {
|
|
|
46
49
|
type: 'command';
|
|
47
50
|
command: string;
|
|
48
51
|
exitCode?: number;
|
|
52
|
+
agent?: ProgressAgent;
|
|
49
53
|
} | {
|
|
50
54
|
type: 'reasoning';
|
|
51
55
|
message: string;
|
|
56
|
+
agent?: ProgressAgent;
|
|
52
57
|
} | {
|
|
53
58
|
type: 'search';
|
|
54
59
|
query: string;
|
|
60
|
+
agent?: ProgressAgent;
|
|
55
61
|
} | {
|
|
56
62
|
type: 'fileChange';
|
|
57
63
|
changes: {
|
|
58
64
|
path: string;
|
|
59
65
|
kind: 'add' | 'update' | 'delete';
|
|
60
66
|
}[];
|
|
67
|
+
agent?: ProgressAgent;
|
|
61
68
|
} | {
|
|
62
69
|
type: 'mcpTool';
|
|
63
70
|
server: string;
|
|
64
71
|
tool: string;
|
|
65
72
|
status: string;
|
|
73
|
+
agent?: ProgressAgent;
|
|
66
74
|
} | {
|
|
67
75
|
type: 'agentMessage';
|
|
68
76
|
message: string;
|
|
77
|
+
agent?: ProgressAgent;
|
|
69
78
|
}
|
|
70
79
|
/** `id` pairs a call's end with its start when one step calls the same tool more than once. */
|
|
71
80
|
| {
|
|
72
81
|
type: 'tool';
|
|
73
82
|
id: string;
|
|
74
83
|
name: string;
|
|
84
|
+
agent?: ProgressAgent;
|
|
85
|
+
/** A short, tool-owned description of the call. Narration only. */
|
|
86
|
+
summary?: string;
|
|
87
|
+
/** The exit code of a configured command that ended with an error. Narration only. */
|
|
88
|
+
exitCode?: number;
|
|
75
89
|
/**
|
|
76
90
|
* `in_progress` only once the call is about to run, after any approval. `denied` when
|
|
77
91
|
* it never ran because no one allowed it, with no `in_progress` before it.
|
|
@@ -89,11 +103,59 @@ export type ProgressEvent = {
|
|
|
89
103
|
text: string;
|
|
90
104
|
completed: boolean;
|
|
91
105
|
}[];
|
|
106
|
+
agent?: ProgressAgent;
|
|
92
107
|
} | {
|
|
93
108
|
type: 'turnStarted';
|
|
109
|
+
agent?: ProgressAgent;
|
|
94
110
|
} | {
|
|
95
111
|
type: 'turnEnded';
|
|
112
|
+
agent?: ProgressAgent;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* One call to the model by an agent on the model APIs whose answer the client returned,
|
|
116
|
+
* even one the model stopped early: what the call sent and what it cost. Narration only:
|
|
117
|
+
* a call the client turned into an error is not a step, so the run's total is
|
|
118
|
+
* `AgentResponse.tokens`, or the failure's `tokens`.
|
|
119
|
+
*/
|
|
120
|
+
| {
|
|
121
|
+
type: 'step';
|
|
122
|
+
/** 1 for the first call of the run. */
|
|
123
|
+
step: number;
|
|
124
|
+
/** Missing when the provider did not report this call's usage. */
|
|
125
|
+
tokens?: Tokens;
|
|
126
|
+
context: StepContext;
|
|
127
|
+
agent?: ProgressAgent;
|
|
96
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* What one call to the model sent, in characters by where it came from: an estimate of size
|
|
131
|
+
* for finding what fills a context, not a token count.
|
|
132
|
+
*/
|
|
133
|
+
export interface StepContext {
|
|
134
|
+
/** The system prompt. */
|
|
135
|
+
instructions: number;
|
|
136
|
+
/** The tool definitions. */
|
|
137
|
+
tools: number;
|
|
138
|
+
/** The text of the user's prompts and the model's answers. */
|
|
139
|
+
messages: number;
|
|
140
|
+
/** Reasoning the model produced earlier and is sent back: its provider's own signed or encrypted blocks. */
|
|
141
|
+
reasoning: number;
|
|
142
|
+
/** The input of every tool call so far. */
|
|
143
|
+
toolCalls: number;
|
|
144
|
+
/** Every tool result so far, by tool, largest first. */
|
|
145
|
+
toolResults: {
|
|
146
|
+
tool: string;
|
|
147
|
+
characters: number;
|
|
148
|
+
calls: number;
|
|
149
|
+
}[];
|
|
150
|
+
/** The largest single results, each with its call's summary when the tool gives one. */
|
|
151
|
+
largestResults: {
|
|
152
|
+
tool: string;
|
|
153
|
+
characters: number;
|
|
154
|
+
summary?: string;
|
|
155
|
+
}[];
|
|
156
|
+
}
|
|
157
|
+
/** A role in the fixed planner → executor → reviewer workflow. */
|
|
158
|
+
export type ProgressAgent = 'planner' | 'executor' | 'reviewer';
|
|
97
159
|
/**
|
|
98
160
|
* The contract every engine (CodexAgent, ClaudeAgent, future providers) and every
|
|
99
161
|
* decorator (RetryingAgent, OrchestratorAgent) is built against.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AA4CA,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';\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. RetryingAgent and InstructedAgent pass it on whole;\n * OrchestratorAgent copies its own enumerable fields and replaces only `onProgress` per role.\n */\nexport interface RunOptions {\n\t/** Cancels the run, which then rejects with the signal's `AbortError`. */\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.\n\t */\n\tapprove?: Approver | 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 * `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 call to the model by an agent on the model APIs whose answer the client returned,\n\t * even one the model stopped early: what the call sent and what it cost. Narration only:\n\t * a call the client turned into an error is not a step, so the run's total is\n\t * `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\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/** The text of the user's prompts and the model's answers. */\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` 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"]}
|
|
@@ -16,6 +16,7 @@ export declare class LLMAgent implements Agent {
|
|
|
16
16
|
private readonly conversation;
|
|
17
17
|
private readonly tools;
|
|
18
18
|
private readonly toolDefinitions;
|
|
19
|
+
private readonly callSummaries;
|
|
19
20
|
private readonly maxSteps;
|
|
20
21
|
private readonly autoApprove;
|
|
21
22
|
private readonly logger;
|
|
@@ -53,6 +54,7 @@ export declare class LLMAgent implements Agent {
|
|
|
53
54
|
private summarizeUndone;
|
|
54
55
|
private warnSummaryFailed;
|
|
55
56
|
private llmCall;
|
|
57
|
+
private narrateStep;
|
|
56
58
|
/**
|
|
57
59
|
* Undefined when the call may run; otherwise why it may not, which the model receives in its
|
|
58
60
|
* place. Only a `destructive` call is asked about: git can undo a `mutating` one. A throwing
|
|
@@ -5,10 +5,12 @@ import { InvalidAgentConfigError, RecoverableError, UnrecoverableError, withSpen
|
|
|
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 } from '#src/tools/domain/preparedCall';
|
|
8
|
+
import { prepareCall, ToolExecutionError, } from '#src/tools/domain/preparedCall';
|
|
9
9
|
import { ReadRegistry } from '#src/tools/domain/readRegistry';
|
|
10
|
+
import { InvalidToolInputError } from '#src/tools/domain/tool';
|
|
10
11
|
import { isAbortError } from '#src/shared/domain/isAbortError';
|
|
11
12
|
import { addTokens } from '#src/shared/domain/tokens';
|
|
13
|
+
import { measureContext } from "./stepContext.js";
|
|
12
14
|
import { followHistory, transcriptOf, undoNote, } from "./historyFollower.js";
|
|
13
15
|
function describePart(part) {
|
|
14
16
|
switch (part.type) {
|
|
@@ -30,10 +32,17 @@ function describePart(part) {
|
|
|
30
32
|
return undefined;
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
|
-
function narrate(part, callback) {
|
|
35
|
+
function narrate(part, callback, summary, exitCode) {
|
|
34
36
|
const event = describePart(part);
|
|
35
|
-
if (event)
|
|
36
|
-
|
|
37
|
+
if (!event)
|
|
38
|
+
return;
|
|
39
|
+
if (event.type === 'tool') {
|
|
40
|
+
if (summary !== undefined)
|
|
41
|
+
event.summary = summary;
|
|
42
|
+
if (exitCode !== undefined)
|
|
43
|
+
event.exitCode = exitCode;
|
|
44
|
+
}
|
|
45
|
+
emit(event, callback);
|
|
37
46
|
}
|
|
38
47
|
function emit(event, callback) {
|
|
39
48
|
treatErrors(() => {
|
|
@@ -78,6 +87,9 @@ export class LLMAgent {
|
|
|
78
87
|
conversation;
|
|
79
88
|
tools;
|
|
80
89
|
toolDefinitions;
|
|
90
|
+
// Each call's summary, as its tool gave it when the call ran, so measuring a step that
|
|
91
|
+
// resends the call never runs a consumer's summarizer again.
|
|
92
|
+
callSummaries = new Map();
|
|
81
93
|
maxSteps;
|
|
82
94
|
autoApprove;
|
|
83
95
|
logger;
|
|
@@ -172,26 +184,30 @@ export class LLMAgent {
|
|
|
172
184
|
}, classifyHostFailure, 'LLM agent logger failed');
|
|
173
185
|
}
|
|
174
186
|
async llmCall(runMessages, after, signal) {
|
|
187
|
+
// Sent as a copy, so what the run records is what the agent built, whatever the client does.
|
|
188
|
+
const context = [...this.conversation.contextAfter(after), ...structuredClone(runMessages)];
|
|
175
189
|
let response;
|
|
176
190
|
try {
|
|
177
|
-
|
|
178
|
-
response = await this.llmClient.send({
|
|
179
|
-
context: [...this.conversation.contextAfter(after), ...structuredClone(runMessages)],
|
|
180
|
-
tools: this.toolDefinitions,
|
|
181
|
-
}, signal);
|
|
191
|
+
response = await this.llmClient.send({ context, tools: this.toolDefinitions }, signal);
|
|
182
192
|
}
|
|
183
193
|
catch (error) {
|
|
184
194
|
throw classifyProviderFailure(error, 'The LLM call failed');
|
|
185
195
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
return { response, context };
|
|
197
|
+
}
|
|
198
|
+
narrateStep(step, response, sent, callback) {
|
|
199
|
+
emit({
|
|
200
|
+
type: 'step',
|
|
201
|
+
step,
|
|
202
|
+
...(response.usage ? { tokens: response.usage } : {}),
|
|
203
|
+
context: measureContext({
|
|
204
|
+
instructions: this.llmClient.systemPrompt,
|
|
205
|
+
providerDataSource: this.llmClient.providerDataSource,
|
|
206
|
+
tools: this.toolDefinitions,
|
|
207
|
+
context: sent,
|
|
208
|
+
summarize: call => this.callSummaries.get(call.id),
|
|
209
|
+
}),
|
|
210
|
+
}, callback);
|
|
195
211
|
}
|
|
196
212
|
/**
|
|
197
213
|
* Undefined when the call may run; otherwise why it may not, which the model receives in its
|
|
@@ -239,6 +255,7 @@ export class LLMAgent {
|
|
|
239
255
|
*/
|
|
240
256
|
async runTool(call, { signal, onProgress: callback = ignoreProgress, approve }, effects, toolCall) {
|
|
241
257
|
const result = { type: 'toolResult', callId: call.id, name: call.name };
|
|
258
|
+
signal.throwIfAborted();
|
|
242
259
|
const tool = this.tools.get(call.name);
|
|
243
260
|
if (!tool) {
|
|
244
261
|
return {
|
|
@@ -250,6 +267,25 @@ export class LLMAgent {
|
|
|
250
267
|
denied: false,
|
|
251
268
|
};
|
|
252
269
|
}
|
|
270
|
+
let summary;
|
|
271
|
+
try {
|
|
272
|
+
// This is deliberately before preparation and approval. A consumer-supplied
|
|
273
|
+
// summarizer is host code, and a failed one must never leave an ambiguous call that
|
|
274
|
+
// could be replayed by RetryingAgent.
|
|
275
|
+
summary = tool.summarize?.(call.input);
|
|
276
|
+
if (summary !== undefined)
|
|
277
|
+
this.callSummaries.set(call.id, summary);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
signal.throwIfAborted();
|
|
281
|
+
if (error instanceof InvalidToolInputError) {
|
|
282
|
+
return {
|
|
283
|
+
result: { ...result, output: describeFailure(error), isError: true },
|
|
284
|
+
denied: false,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
throw classifyHostFailure(error, 'The tool call summarizer failed');
|
|
288
|
+
}
|
|
253
289
|
let prepared;
|
|
254
290
|
try {
|
|
255
291
|
prepared = await prepareCall(tool, call.input, signal, toolCall);
|
|
@@ -261,6 +297,7 @@ export class LLMAgent {
|
|
|
261
297
|
return {
|
|
262
298
|
result: { ...result, output: describeFailure(error), isError: true },
|
|
263
299
|
denied: false,
|
|
300
|
+
...(summary === undefined ? {} : { summary }),
|
|
264
301
|
};
|
|
265
302
|
}
|
|
266
303
|
// Preparing may have finished after a cancellation it did not notice; nobody is asked to
|
|
@@ -268,12 +305,22 @@ export class LLMAgent {
|
|
|
268
305
|
signal.throwIfAborted();
|
|
269
306
|
const denial = await this.denial(call, prepared, signal, approve);
|
|
270
307
|
if (denial !== undefined) {
|
|
271
|
-
return {
|
|
308
|
+
return {
|
|
309
|
+
result: { ...result, output: denial, isError: true },
|
|
310
|
+
denied: true,
|
|
311
|
+
...(summary === undefined ? {} : { summary }),
|
|
312
|
+
};
|
|
272
313
|
}
|
|
273
314
|
// Deciding took at least one await, and the run may have been cancelled meanwhile.
|
|
274
315
|
signal.throwIfAborted();
|
|
275
316
|
// Announced only once it will run: a missing, failing-to-judge or denied call never is.
|
|
276
|
-
|
|
317
|
+
emit({
|
|
318
|
+
type: 'tool',
|
|
319
|
+
id: call.id,
|
|
320
|
+
name: call.name,
|
|
321
|
+
status: 'in_progress',
|
|
322
|
+
...(summary === undefined ? {} : { summary }),
|
|
323
|
+
}, callback);
|
|
277
324
|
// Checked again at the last moment: the consumer may cancel on the announcement itself.
|
|
278
325
|
signal.throwIfAborted();
|
|
279
326
|
// Set before `execute`, so a tool that fails halfway still counts: it may have had effects.
|
|
@@ -282,6 +329,7 @@ export class LLMAgent {
|
|
|
282
329
|
return {
|
|
283
330
|
result: { ...result, output: await prepared.run(signal), isError: false },
|
|
284
331
|
denied: false,
|
|
332
|
+
...(summary === undefined ? {} : { summary }),
|
|
285
333
|
...(prepared.diff === undefined ? {} : { diff: prepared.diff }),
|
|
286
334
|
};
|
|
287
335
|
}
|
|
@@ -290,6 +338,10 @@ export class LLMAgent {
|
|
|
290
338
|
return {
|
|
291
339
|
result: { ...result, output: describeFailure(error), isError: true },
|
|
292
340
|
denied: false,
|
|
341
|
+
...(summary === undefined ? {} : { summary }),
|
|
342
|
+
...(error instanceof ToolExecutionError && error.exitCode !== undefined
|
|
343
|
+
? { exitCode: error.exitCode }
|
|
344
|
+
: {}),
|
|
293
345
|
};
|
|
294
346
|
}
|
|
295
347
|
}
|
|
@@ -303,15 +355,29 @@ export class LLMAgent {
|
|
|
303
355
|
signal.throwIfAborted();
|
|
304
356
|
// `runTool` announces the call only once it is allowed to run, so a consumer never
|
|
305
357
|
// shows a call as running early, nor while the user is still being asked.
|
|
306
|
-
const { result, denied, diff } = await this.runTool(call, options, effects, toolCall);
|
|
307
|
-
if (denied)
|
|
308
|
-
emit({
|
|
358
|
+
const { result, denied, diff, summary, exitCode } = await this.runTool(call, options, effects, toolCall);
|
|
359
|
+
if (denied) {
|
|
360
|
+
emit({
|
|
361
|
+
type: 'tool',
|
|
362
|
+
id: call.id,
|
|
363
|
+
name: call.name,
|
|
364
|
+
status: 'denied',
|
|
365
|
+
...(summary === undefined ? {} : { summary }),
|
|
366
|
+
}, callback);
|
|
367
|
+
}
|
|
309
368
|
// The diff of a change it made, for the person watching; only once it was made.
|
|
310
369
|
else if (diff !== undefined) {
|
|
311
|
-
emit({
|
|
370
|
+
emit({
|
|
371
|
+
type: 'tool',
|
|
372
|
+
id: call.id,
|
|
373
|
+
name: call.name,
|
|
374
|
+
status: 'completed',
|
|
375
|
+
diff,
|
|
376
|
+
...(summary === undefined ? {} : { summary }),
|
|
377
|
+
}, callback);
|
|
312
378
|
}
|
|
313
379
|
else
|
|
314
|
-
narrate(result, callback);
|
|
380
|
+
narrate(result, callback, summary, exitCode);
|
|
315
381
|
results.push(result);
|
|
316
382
|
}
|
|
317
383
|
return { role: 'tool', content: results };
|
|
@@ -396,9 +462,18 @@ export class LLMAgent {
|
|
|
396
462
|
const reads = this.reads.begin();
|
|
397
463
|
const repeats = new RepeatedCalls();
|
|
398
464
|
for (let step = 1; step <= this.maxSteps; step++) {
|
|
399
|
-
const response = await this.llmCall(runMessages, after, signal);
|
|
465
|
+
const { response, context: sent } = await this.llmCall(runMessages, after, signal);
|
|
400
466
|
tokens = addTokens(tokens, response.usage);
|
|
401
467
|
unreported ||= !response.usage;
|
|
468
|
+
// After the tokens are counted, so a throwing callback still reports them, and for an
|
|
469
|
+
// unusable answer too: the call was sent, and billed.
|
|
470
|
+
this.narrateStep(step, response, sent, callback);
|
|
471
|
+
if (response.stopReason !== 'completed') {
|
|
472
|
+
// Its tokens are already counted with the run's, which the failure carries.
|
|
473
|
+
throw new UnrecoverableError('The LLM stopped before completing its answer', {
|
|
474
|
+
cause: `The model stopped with "${response.stopReason}".`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
402
477
|
runMessages.push(response.message);
|
|
403
478
|
// Tool calls are narrated as they start, in `runTools`, and never if they do not run.
|
|
404
479
|
for (const part of response.message.content) {
|