laohuang 0.4.2 → 0.5.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 +10 -6
- package/dist/agent.d.ts +7 -27
- package/dist/agent.js +53 -567
- package/dist/agent.js.map +1 -1
- package/dist/capabilities.d.ts +19 -0
- package/dist/capabilities.js +29 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli.d.ts +9 -6
- package/dist/cli.js +83 -70
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +20 -3
- package/dist/commands.js +39 -38
- package/dist/commands.js.map +1 -1
- package/dist/keybindings/default-keybindings.d.ts +2 -0
- package/dist/keybindings/default-keybindings.js +10 -0
- package/dist/keybindings/default-keybindings.js.map +1 -0
- package/dist/keybindings/hints.d.ts +3 -0
- package/dist/keybindings/hints.js +7 -0
- package/dist/keybindings/hints.js.map +1 -0
- package/dist/keybindings/key-id.d.ts +20 -0
- package/dist/keybindings/key-id.js +11 -0
- package/dist/keybindings/key-id.js.map +1 -0
- package/dist/keybindings/key-parser.d.ts +7 -0
- package/dist/keybindings/key-parser.js +92 -0
- package/dist/keybindings/key-parser.js.map +1 -0
- package/dist/keybindings/keybindings.d.ts +21 -0
- package/dist/keybindings/keybindings.js +65 -0
- package/dist/keybindings/keybindings.js.map +1 -0
- package/dist/routing.d.ts +2 -57
- package/dist/routing.js +3 -161
- package/dist/routing.js.map +1 -1
- package/dist/runtime/agent-step-runner.d.ts +51 -0
- package/dist/runtime/agent-step-runner.js +361 -0
- package/dist/runtime/agent-step-runner.js.map +1 -0
- package/dist/runtime/agent-turn-loop.d.ts +36 -0
- package/dist/runtime/agent-turn-loop.js +71 -0
- package/dist/runtime/agent-turn-loop.js.map +1 -0
- package/dist/runtime/guard-policy.d.ts +21 -0
- package/dist/runtime/guard-policy.js +93 -0
- package/dist/runtime/guard-policy.js.map +1 -0
- package/dist/runtime/history-committer.d.ts +34 -0
- package/dist/runtime/history-committer.js +77 -0
- package/dist/runtime/history-committer.js.map +1 -0
- package/dist/runtime/human-intent-router.d.ts +5 -0
- package/dist/runtime/human-intent-router.js +38 -0
- package/dist/runtime/human-intent-router.js.map +1 -0
- package/dist/runtime/model-runtime.d.ts +33 -0
- package/dist/runtime/model-runtime.js +70 -0
- package/dist/runtime/model-runtime.js.map +1 -0
- package/dist/runtime/queue-bridge.d.ts +11 -0
- package/dist/runtime/queue-bridge.js +2 -0
- package/dist/runtime/queue-bridge.js.map +1 -0
- package/dist/runtime/queue-dispatcher.d.ts +45 -0
- package/dist/runtime/queue-dispatcher.js +120 -0
- package/dist/runtime/queue-dispatcher.js.map +1 -0
- package/dist/runtime/runtime-events.d.ts +26 -0
- package/dist/runtime/runtime-events.js +21 -0
- package/dist/runtime/runtime-events.js.map +1 -0
- package/dist/runtime/session-action-protocol.d.ts +10 -0
- package/dist/runtime/session-action-protocol.js +64 -0
- package/dist/runtime/session-action-protocol.js.map +1 -0
- package/dist/runtime/session-action.d.ts +42 -0
- package/dist/runtime/session-action.js +3 -0
- package/dist/runtime/session-action.js.map +1 -0
- package/dist/runtime/task-lifecycle.d.ts +73 -0
- package/dist/runtime/task-lifecycle.js +267 -0
- package/dist/runtime/task-lifecycle.js.map +1 -0
- package/dist/runtime/tool-runtime.d.ts +33 -0
- package/dist/runtime/tool-runtime.js +89 -0
- package/dist/runtime/tool-runtime.js.map +1 -0
- package/dist/runtime/user-intent.d.ts +48 -0
- package/dist/runtime/user-intent.js +17 -0
- package/dist/runtime/user-intent.js.map +1 -0
- package/dist/session.d.ts +19 -9
- package/dist/session.js +119 -169
- package/dist/session.js.map +1 -1
- package/dist/terminal/editor.d.ts +16 -5
- package/dist/terminal/editor.js +99 -4
- package/dist/terminal/editor.js.map +1 -1
- package/dist/terminal/input.d.ts +2 -0
- package/dist/terminal/input.js +32 -7
- package/dist/terminal/input.js.map +1 -1
- package/dist/terminal/ui.d.ts +32 -21
- package/dist/terminal/ui.js +413 -252
- package/dist/terminal/ui.js.map +1 -1
- package/dist/tools.d.ts +7 -0
- package/dist/tools.js.map +1 -1
- package/dist/tui/components.d.ts +5 -0
- package/dist/tui/components.js +12 -0
- package/dist/tui/components.js.map +1 -0
- package/dist/tui/focus-manager.d.ts +7 -0
- package/dist/tui/focus-manager.js +13 -0
- package/dist/tui/focus-manager.js.map +1 -0
- package/dist/tui/overlay-manager.d.ts +12 -0
- package/dist/tui/overlay-manager.js +27 -0
- package/dist/tui/overlay-manager.js.map +1 -0
- package/dist/ui/display-actions.d.ts +23 -0
- package/dist/ui/display-actions.js +15 -0
- package/dist/ui/display-actions.js.map +1 -0
- package/dist/ui/display-policy.d.ts +38 -0
- package/dist/ui/display-policy.js +97 -0
- package/dist/ui/display-policy.js.map +1 -0
- package/dist/ui/frame-builder.d.ts +39 -0
- package/dist/ui/frame-builder.js +111 -0
- package/dist/ui/frame-builder.js.map +1 -0
- package/dist/ui/transcript-store.d.ts +35 -0
- package/dist/ui/transcript-store.js +172 -0
- package/dist/ui/transcript-store.js.map +1 -0
- package/dist/ui-state.js +6 -6
- package/dist/ui-state.js.map +1 -1
- package/dist/web.js +14 -1
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -11,18 +11,17 @@
|
|
|
11
11
|
* duration budgets. A triggered guard disables tools and asks the model for
|
|
12
12
|
* one final answer from the information already gathered.
|
|
13
13
|
*/
|
|
14
|
-
import { randomUUID } from "node:crypto";
|
|
15
14
|
import path from "node:path";
|
|
16
15
|
import { EventKind, EventSource, } from "./events.js";
|
|
17
|
-
import {
|
|
18
|
-
import { defaultAdapterRegistry, modelErrorKind, portableMessage, } from "./model-adapter.js";
|
|
19
|
-
import { touchedPathOf } from "./tools.js";
|
|
16
|
+
import { defaultAdapterRegistry, portableMessage, } from "./model-adapter.js";
|
|
20
17
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
21
18
|
import { discoverInstructions, loadBaselineInstructions, realpathOrSelf, renderAdditionalInstructions, scopeChain, ProjectInstructionState, } from "./project-instructions.js";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
import { AgentStepRunner, FORCED_FINAL_PROMPT, } from "./runtime/agent-step-runner.js";
|
|
20
|
+
import { GuardPolicy } from "./runtime/guard-policy.js";
|
|
21
|
+
import { HistoryCommitter } from "./runtime/history-committer.js";
|
|
22
|
+
import { ModelRuntime } from "./runtime/model-runtime.js";
|
|
23
|
+
import { ToolRuntime } from "./runtime/tool-runtime.js";
|
|
24
|
+
export { FORCED_FINAL_PROMPT };
|
|
26
25
|
/** Raised when the model response cannot drive the agent loop. */
|
|
27
26
|
export class AgentError extends Error {
|
|
28
27
|
constructor(message, options = {}) {
|
|
@@ -58,6 +57,8 @@ export class CodingAgent {
|
|
|
58
57
|
provider;
|
|
59
58
|
/** Provider-neutral model access; resolved from `provider`. */
|
|
60
59
|
adapter;
|
|
60
|
+
modelRuntime;
|
|
61
|
+
toolRuntime;
|
|
61
62
|
tools;
|
|
62
63
|
maxTotalTokens;
|
|
63
64
|
maxElapsedSeconds;
|
|
@@ -94,7 +95,11 @@ export class CodingAgent {
|
|
|
94
95
|
this.onAgentEvent = options.onAgentEvent ?? null;
|
|
95
96
|
this.provider = options.provider ?? null;
|
|
96
97
|
this.adapter = defaultAdapterRegistry.resolve(this.provider);
|
|
98
|
+
this.modelRuntime = new ModelRuntime(this.adapter);
|
|
97
99
|
this.toolExecution = options.toolExecution ?? "parallel";
|
|
100
|
+
this.toolRuntime = new ToolRuntime(this.tools, {
|
|
101
|
+
createExecutionContext: (toolCallId, cancelToken) => makeToolContext(this.activeContext, toolCallId, cancelToken),
|
|
102
|
+
});
|
|
98
103
|
// Canonicalize so instruction scopes line up with the registry's
|
|
99
104
|
// realpath-resolved touched paths even when the cwd contains symlinks.
|
|
100
105
|
this.instructionRoot =
|
|
@@ -116,6 +121,7 @@ export class CodingAgent {
|
|
|
116
121
|
this.model = options.model;
|
|
117
122
|
this.provider = options.provider;
|
|
118
123
|
this.adapter = defaultAdapterRegistry.resolve(this.provider);
|
|
124
|
+
this.modelRuntime = new ModelRuntime(this.adapter);
|
|
119
125
|
this.emit("model_switched", {
|
|
120
126
|
provider: options.provider,
|
|
121
127
|
model: options.model,
|
|
@@ -131,444 +137,52 @@ export class CodingAgent {
|
|
|
131
137
|
}
|
|
132
138
|
this.activeContext = context;
|
|
133
139
|
this.turn += 1;
|
|
134
|
-
let toolRounds = 0;
|
|
135
|
-
let modelRound = 0;
|
|
136
|
-
let modelRequests = 0;
|
|
137
|
-
let totalTokens = 0;
|
|
138
|
-
const startedAt = performance.now();
|
|
139
|
-
const repeatedCalls = new Map();
|
|
140
|
-
let guardReason = null;
|
|
141
|
-
let guardEmitted = false;
|
|
142
140
|
try {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
modelRequests += 1;
|
|
183
|
-
const currentRequestId = modelRound === 1 && options.requestId
|
|
184
|
-
? options.requestId
|
|
185
|
-
: randomUUID();
|
|
186
|
-
this.activeRequestId = currentRequestId;
|
|
187
|
-
this.emit("model_request", {
|
|
188
|
-
round: modelRound,
|
|
189
|
-
request_id: currentRequestId,
|
|
190
|
-
message_count: this.messages.length,
|
|
191
|
-
tool_rounds: toolRounds,
|
|
192
|
-
model_requests: modelRequests,
|
|
193
|
-
total_tokens: totalTokens,
|
|
194
|
-
force_final: forceFinal,
|
|
195
|
-
guard_reason: guardReason,
|
|
196
|
-
});
|
|
197
|
-
const requestMessages = [...this.messages];
|
|
198
|
-
if (forceFinal) {
|
|
199
|
-
const systemMessage = {
|
|
200
|
-
...(requestMessages[0] ?? {}),
|
|
201
|
-
};
|
|
202
|
-
systemMessage["content"] =
|
|
203
|
-
`${String(systemMessage["content"] ?? "")}\n\n${FORCED_FINAL_PROMPT}`;
|
|
204
|
-
requestMessages[0] = systemMessage;
|
|
205
|
-
}
|
|
206
|
-
let result;
|
|
207
|
-
try {
|
|
208
|
-
const modelRequestOpened = context?.modelRequestOpened;
|
|
209
|
-
const isRequestActive = options.isRequestActive ??
|
|
210
|
-
((requestId) => this.activeRequestId === requestId);
|
|
211
|
-
result = await this.adapter.complete(this.client, {
|
|
212
|
-
model: this.model,
|
|
213
|
-
messages: requestMessages,
|
|
214
|
-
tools: this.tools.definitions,
|
|
215
|
-
toolChoice: forceFinal ? "none" : "auto",
|
|
216
|
-
requestId: currentRequestId,
|
|
217
|
-
cancelToken,
|
|
218
|
-
isRequestActive,
|
|
219
|
-
onDelta: (kind, payload) => {
|
|
220
|
-
this.emit(kind, { round: modelRound, ...payload });
|
|
221
|
-
},
|
|
222
|
-
onRequestOpened: modelRequestOpened
|
|
223
|
-
? () => modelRequestOpened.call(context)
|
|
224
|
-
: null,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
catch (error) {
|
|
228
|
-
if (error instanceof ModelStreamCancelled) {
|
|
229
|
-
// Covers StaleModelRequest as well.
|
|
230
|
-
this.emit("model_response_aborted", {
|
|
231
|
-
round: modelRound,
|
|
232
|
-
request_id: currentRequestId,
|
|
233
|
-
reason: errorMessage(error),
|
|
234
|
-
});
|
|
235
|
-
throw new AgentCancelled(errorMessage(error), { cause: error });
|
|
236
|
-
}
|
|
237
|
-
const errorPayload = {
|
|
238
|
-
round: modelRound,
|
|
239
|
-
request_id: currentRequestId,
|
|
240
|
-
error: errorMessage(error),
|
|
241
|
-
};
|
|
242
|
-
if (error instanceof ModelStreamError && error.hadDelta) {
|
|
243
|
-
this.emit("model_response_aborted", errorPayload);
|
|
244
|
-
this.emitLegacy("model_error", errorPayload);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
this.emit("model_error", errorPayload);
|
|
248
|
-
}
|
|
249
|
-
if (forceFinal) {
|
|
250
|
-
const payload = this.guardPayload({
|
|
251
|
-
reason: guardReason ?? "runtime safety guard",
|
|
252
|
-
toolRounds,
|
|
253
|
-
modelRequests,
|
|
254
|
-
totalTokens,
|
|
255
|
-
startedAt,
|
|
256
|
-
finalError: errorMessage(error),
|
|
257
|
-
});
|
|
258
|
-
this.emit("agent_guard_failed", payload);
|
|
259
|
-
let message = guardErrorMessage(payload);
|
|
260
|
-
if (this.provider &&
|
|
261
|
-
modelErrorKind(error) === "authentication") {
|
|
262
|
-
message +=
|
|
263
|
-
` Authentication failed for ${this.provider}. ` +
|
|
264
|
-
`Run /login ${this.provider} to update your API key.`;
|
|
265
|
-
}
|
|
266
|
-
throw new AgentError(message, { cause: error });
|
|
267
|
-
}
|
|
268
|
-
let message = `Model request failed: ${errorMessage(error)}`;
|
|
269
|
-
if (this.provider && modelErrorKind(error) === "authentication") {
|
|
270
|
-
message +=
|
|
271
|
-
`\nAuthentication failed for ${this.provider}. ` +
|
|
272
|
-
`Run /login ${this.provider} to update your API key.`;
|
|
273
|
-
}
|
|
274
|
-
throw new AgentError(message, { cause: error });
|
|
275
|
-
}
|
|
276
|
-
const toolCalls = [...result.toolCalls];
|
|
277
|
-
let requestTokens = usageTotalTokens(result.usage);
|
|
278
|
-
const tokensEstimated = requestTokens === 0;
|
|
279
|
-
if (tokensEstimated) {
|
|
280
|
-
requestTokens = estimateRequestTokens(requestMessages, result.messageDict());
|
|
281
|
-
}
|
|
282
|
-
totalTokens += requestTokens;
|
|
283
|
-
this.emit("model_response", {
|
|
284
|
-
round: modelRound,
|
|
285
|
-
request_id: currentRequestId,
|
|
286
|
-
finish_reason: result.finishReason,
|
|
287
|
-
tool_call_count: toolCalls.length,
|
|
288
|
-
tool_names: toolCalls.map((call) => call.function.name),
|
|
289
|
-
tool_call_ids: toolCalls.map((call) => call.id),
|
|
290
|
-
usage: result.usage,
|
|
291
|
-
request_tokens: requestTokens,
|
|
292
|
-
tokens_estimated: tokensEstimated,
|
|
293
|
-
total_tokens: totalTokens,
|
|
294
|
-
tool_rounds: toolRounds,
|
|
295
|
-
model_requests: modelRequests,
|
|
296
|
-
force_final: forceFinal,
|
|
297
|
-
});
|
|
298
|
-
if (forceFinal && toolCalls.length > 0) {
|
|
299
|
-
this.emit("model_response_aborted", {
|
|
300
|
-
round: modelRound,
|
|
301
|
-
request_id: currentRequestId,
|
|
302
|
-
reason: "tool call returned while tools were disabled",
|
|
303
|
-
});
|
|
304
|
-
const payload = this.guardPayload({
|
|
305
|
-
reason: guardReason ?? "runtime safety guard",
|
|
306
|
-
toolRounds,
|
|
307
|
-
modelRequests,
|
|
308
|
-
totalTokens,
|
|
309
|
-
startedAt,
|
|
310
|
-
});
|
|
311
|
-
this.emit("agent_guard_failed", payload);
|
|
312
|
-
throw new AgentError(guardErrorMessage(payload));
|
|
313
|
-
}
|
|
314
|
-
const postResponseGuard = this.budgetGuardReason(totalTokens, (performance.now() - startedAt) / 1000);
|
|
315
|
-
if (toolCalls.length > 0 && postResponseGuard !== null) {
|
|
316
|
-
this.emit("model_response_aborted", {
|
|
317
|
-
round: modelRound,
|
|
318
|
-
request_id: currentRequestId,
|
|
319
|
-
reason: postResponseGuard,
|
|
320
|
-
});
|
|
321
|
-
guardReason = postResponseGuard;
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
// The complete assistant message is committed only if cancellation
|
|
325
|
-
// has not won the Session coordination race.
|
|
326
|
-
const assistantMessage = result.messageDict();
|
|
327
|
-
const commitIfActive = context?.commitIfActive;
|
|
328
|
-
if (typeof commitIfActive === "function") {
|
|
329
|
-
committed = commitIfActive.call(context, () => {
|
|
330
|
-
this.messages.push(assistantMessage);
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
raiseIfCancelled(cancelToken);
|
|
335
|
-
this.messages.push(assistantMessage);
|
|
336
|
-
committed = true;
|
|
337
|
-
}
|
|
338
|
-
if (!committed) {
|
|
339
|
-
this.emit("model_response_aborted", {
|
|
340
|
-
round: modelRound,
|
|
341
|
-
request_id: currentRequestId,
|
|
342
|
-
reason: "cancelled before history commit",
|
|
343
|
-
});
|
|
344
|
-
throw new AgentCancelled("cancelled before history commit");
|
|
345
|
-
}
|
|
346
|
-
this.emit("model_response_committed", {
|
|
347
|
-
round: modelRound,
|
|
348
|
-
request_id: currentRequestId,
|
|
349
|
-
});
|
|
350
|
-
if (toolCalls.length === 0) {
|
|
351
|
-
const content = result.content;
|
|
352
|
-
if (content === null) {
|
|
353
|
-
throw new AgentError("Model response had no content");
|
|
354
|
-
}
|
|
355
|
-
this.emit("assistant_response", {
|
|
356
|
-
round: modelRound,
|
|
357
|
-
content: truncateForEvent(content),
|
|
358
|
-
});
|
|
359
|
-
return content;
|
|
360
|
-
}
|
|
361
|
-
toolRounds += 1;
|
|
362
|
-
context?.toolsStarted?.();
|
|
363
|
-
const toolResults = await this.executeToolBatch(toolCalls, modelRound, cancelToken, context);
|
|
364
|
-
const repeated = this.recordRepeatedToolCalls(toolCalls, toolResults, repeatedCalls);
|
|
365
|
-
// Every committed assistant tool call must receive one paired tool
|
|
366
|
-
// result, including calls cancelled before they start.
|
|
367
|
-
for (let index = 0; index < toolCalls.length; index += 1) {
|
|
368
|
-
this.messages.push({
|
|
369
|
-
role: "tool",
|
|
370
|
-
tool_call_id: toolCalls[index]?.id,
|
|
371
|
-
content: JSON.stringify(toolResults[index]),
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
raiseIfCancelled(cancelToken);
|
|
375
|
-
// Dynamic descendant discovery runs only after every paired tool
|
|
376
|
-
// result is committed, so reminders land between the tool results
|
|
377
|
-
// and the next model request without touching earlier history.
|
|
378
|
-
const touchedPaths = [];
|
|
379
|
-
for (const result of toolResults) {
|
|
380
|
-
const touched = touchedPathOf(result);
|
|
381
|
-
if (typeof touched === "string") {
|
|
382
|
-
touchedPaths.push(touched);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
this.discoverForTouchedPaths(touchedPaths);
|
|
386
|
-
if (repeated !== null) {
|
|
387
|
-
guardReason =
|
|
388
|
-
`repeated tool call detected (${repeated.name} repeated ` +
|
|
389
|
-
`${repeated.count} times with the same arguments and result)`;
|
|
390
|
-
}
|
|
391
|
-
const safePoint = context?.safePoint;
|
|
392
|
-
if (context !== null && typeof safePoint === "function") {
|
|
393
|
-
const pendingBatch = safePoint.call(context);
|
|
394
|
-
const pendingContent = pendingBatch?.content ?? "";
|
|
395
|
-
if (pendingBatch != null && pendingContent) {
|
|
396
|
-
const pendingMessage = {
|
|
397
|
-
role: "user",
|
|
398
|
-
content: pendingContent,
|
|
399
|
-
};
|
|
400
|
-
const commitPending = context.commitPending;
|
|
401
|
-
let committedPending;
|
|
402
|
-
if (typeof commitPending === "function") {
|
|
403
|
-
committedPending = this.commitContextMessage((append, rollback) => commitPending.call(context, pendingBatch, append, rollback), pendingMessage);
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
raiseIfCancelled(cancelToken);
|
|
407
|
-
this.messages.push(pendingMessage);
|
|
408
|
-
committedPending = true;
|
|
409
|
-
}
|
|
410
|
-
if (!committedPending) {
|
|
411
|
-
throw new AgentCancelled("cancelled before pending input commit");
|
|
412
|
-
}
|
|
413
|
-
this.emit("user_message", {
|
|
414
|
-
content: pendingContent,
|
|
415
|
-
pending_event_ids: [...(pendingBatch.eventIds ?? [])],
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
141
|
+
const runner = new AgentStepRunner({
|
|
142
|
+
client: this.client,
|
|
143
|
+
model: this.model,
|
|
144
|
+
provider: this.provider,
|
|
145
|
+
modelRuntime: this.modelRuntime,
|
|
146
|
+
toolRuntime: this.toolRuntime,
|
|
147
|
+
toolDefinitions: this.tools.definitions,
|
|
148
|
+
toolExecution: this.toolExecution,
|
|
149
|
+
history: new HistoryCommitter({
|
|
150
|
+
messages: this.messages,
|
|
151
|
+
context,
|
|
152
|
+
cancelToken,
|
|
153
|
+
createCancelled: (message) => new AgentCancelled(message),
|
|
154
|
+
}),
|
|
155
|
+
guardPolicy: new GuardPolicy({
|
|
156
|
+
maxTotalTokens: this.maxTotalTokens,
|
|
157
|
+
maxElapsedSeconds: this.maxElapsedSeconds,
|
|
158
|
+
repeatedToolCallLimit: this.repeatedToolCallLimit,
|
|
159
|
+
}),
|
|
160
|
+
userInput,
|
|
161
|
+
context,
|
|
162
|
+
cancelToken,
|
|
163
|
+
requestId: options.requestId ?? null,
|
|
164
|
+
isRequestActive: options.isRequestActive ??
|
|
165
|
+
((requestId) => this.activeRequestId === requestId),
|
|
166
|
+
onRequestId: (requestId) => {
|
|
167
|
+
this.activeRequestId = requestId;
|
|
168
|
+
},
|
|
169
|
+
emit: (eventType, payload) => this.emit(eventType, payload),
|
|
170
|
+
emitLegacy: (eventType, payload) => this.emitLegacy(eventType, payload),
|
|
171
|
+
injectBaselineInstructions: (token) => this.injectBaselineInstructions(token),
|
|
172
|
+
discoverForTouchedPaths: (paths) => this.discoverForTouchedPaths(paths),
|
|
173
|
+
onToolEvent: (name, args, result) => {
|
|
174
|
+
this.onToolEvent?.(name, args, result);
|
|
175
|
+
},
|
|
176
|
+
createError: (message, cause) => new AgentError(message, { cause }),
|
|
177
|
+
createCancelled: (message, cause) => new AgentCancelled(message, { cause }),
|
|
178
|
+
});
|
|
179
|
+
return await runner.run();
|
|
420
180
|
}
|
|
421
181
|
finally {
|
|
422
182
|
this.activeRequestId = null;
|
|
423
183
|
this.activeContext = null;
|
|
424
184
|
}
|
|
425
185
|
}
|
|
426
|
-
// --- Guard rails -----------------------------------------------------------
|
|
427
|
-
budgetGuardReason(totalTokens, elapsedSeconds) {
|
|
428
|
-
if (totalTokens >= this.maxTotalTokens) {
|
|
429
|
-
return `token budget reached (${this.maxTotalTokens})`;
|
|
430
|
-
}
|
|
431
|
-
if (elapsedSeconds >= this.maxElapsedSeconds) {
|
|
432
|
-
return (`elapsed time budget reached (${String(this.maxElapsedSeconds)} seconds)`);
|
|
433
|
-
}
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
436
|
-
guardPayload(options) {
|
|
437
|
-
const payload = {
|
|
438
|
-
reason: options.reason,
|
|
439
|
-
tool_rounds: options.toolRounds,
|
|
440
|
-
model_requests: options.modelRequests,
|
|
441
|
-
total_tokens: options.totalTokens,
|
|
442
|
-
elapsed_ms: Math.round(performance.now() - options.startedAt),
|
|
443
|
-
};
|
|
444
|
-
if (options.finalError) {
|
|
445
|
-
payload["final_error"] = options.finalError;
|
|
446
|
-
}
|
|
447
|
-
return payload;
|
|
448
|
-
}
|
|
449
|
-
recordRepeatedToolCalls(toolCalls, toolResults, counts) {
|
|
450
|
-
let repeated = null;
|
|
451
|
-
const seen = new Set();
|
|
452
|
-
for (let index = 0; index < toolCalls.length; index += 1) {
|
|
453
|
-
const toolCall = toolCalls[index];
|
|
454
|
-
if (toolCall === undefined) {
|
|
455
|
-
continue;
|
|
456
|
-
}
|
|
457
|
-
let parsedArguments;
|
|
458
|
-
try {
|
|
459
|
-
parsedArguments = JSON.parse(toolCall.function.arguments);
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
parsedArguments = toolCall.function.arguments;
|
|
463
|
-
}
|
|
464
|
-
const fingerprint = stableStringify({
|
|
465
|
-
name: toolCall.function.name,
|
|
466
|
-
arguments: parsedArguments,
|
|
467
|
-
result: stableToolResult(toolResults[index]),
|
|
468
|
-
});
|
|
469
|
-
const count = (counts.get(fingerprint) ?? 0) + 1;
|
|
470
|
-
counts.set(fingerprint, count);
|
|
471
|
-
seen.add(fingerprint);
|
|
472
|
-
if (repeated === null || count > repeated.count) {
|
|
473
|
-
repeated = { name: toolCall.function.name, count };
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
for (const fingerprint of [...counts.keys()]) {
|
|
477
|
-
if (!seen.has(fingerprint)) {
|
|
478
|
-
counts.delete(fingerprint);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return repeated !== null && repeated.count >= this.repeatedToolCallLimit
|
|
482
|
-
? repeated
|
|
483
|
-
: null;
|
|
484
|
-
}
|
|
485
|
-
// --- Tool batch execution ----------------------------------------------------
|
|
486
|
-
async executeToolBatch(toolCalls, modelRound, cancelToken, context) {
|
|
487
|
-
const results = new Array(toolCalls.length).fill(undefined);
|
|
488
|
-
const prepared = [];
|
|
489
|
-
for (let offset = 0; offset < toolCalls.length; offset += 1) {
|
|
490
|
-
const toolCall = toolCalls[offset];
|
|
491
|
-
if (toolCall === undefined) {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
let args;
|
|
495
|
-
let result;
|
|
496
|
-
try {
|
|
497
|
-
const decoded = JSON.parse(toolCall.function.arguments);
|
|
498
|
-
if (typeof decoded !== "object" ||
|
|
499
|
-
decoded === null ||
|
|
500
|
-
Array.isArray(decoded)) {
|
|
501
|
-
throw new Error("Tool arguments must be a JSON object");
|
|
502
|
-
}
|
|
503
|
-
args = decoded;
|
|
504
|
-
}
|
|
505
|
-
catch (error) {
|
|
506
|
-
args = { _raw: toolCall.function.arguments };
|
|
507
|
-
result = { ok: false, error: errorMessage(error) };
|
|
508
|
-
}
|
|
509
|
-
const eventContext = {
|
|
510
|
-
round: modelRound,
|
|
511
|
-
index: offset + 1,
|
|
512
|
-
batch_size: toolCalls.length,
|
|
513
|
-
tool_call_id: toolCall.id,
|
|
514
|
-
name: toolCall.function.name,
|
|
515
|
-
};
|
|
516
|
-
this.emit("tool_start", {
|
|
517
|
-
...eventContext,
|
|
518
|
-
arguments: safeArguments(args),
|
|
519
|
-
});
|
|
520
|
-
if (result === undefined) {
|
|
521
|
-
prepared.push({ offset, toolCall, args, eventContext });
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
results[offset] = result;
|
|
525
|
-
this.finishToolEvent(toolCall.function.name, args, result, eventContext);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
const sequentialBatch = this.toolExecution === "sequential" ||
|
|
529
|
-
toolCalls.some((call) => this.tools.executionMode(call.function.name) === "sequential") ||
|
|
530
|
-
toolCalls.some((call) => call.function.name === "write" || call.function.name === "edit");
|
|
531
|
-
const runOne = async (item) => {
|
|
532
|
-
const result = isCancelled(cancelToken)
|
|
533
|
-
? cancelledToolResult(cancelToken)
|
|
534
|
-
: await this.executeTool(item.toolCall.function.name, item.args, item.toolCall.id, cancelToken, context);
|
|
535
|
-
results[item.offset] = result;
|
|
536
|
-
// Finish events fire in actual completion order; the returned results
|
|
537
|
-
// array keeps the original call order for the model.
|
|
538
|
-
this.finishToolEvent(item.toolCall.function.name, item.args, result, item.eventContext);
|
|
539
|
-
};
|
|
540
|
-
if (sequentialBatch || prepared.length === 1) {
|
|
541
|
-
for (const item of prepared) {
|
|
542
|
-
await runOne(item);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
else if (prepared.length > 0) {
|
|
546
|
-
await Promise.all(prepared.map((item) => runOne(item)));
|
|
547
|
-
}
|
|
548
|
-
return results.map((result) => result ?? { ok: false, error: "Tool execution produced no result" });
|
|
549
|
-
}
|
|
550
|
-
async executeTool(name, args, toolCallId, cancelToken, context) {
|
|
551
|
-
if (isCancelled(cancelToken)) {
|
|
552
|
-
return cancelledToolResult(cancelToken);
|
|
553
|
-
}
|
|
554
|
-
try {
|
|
555
|
-
const toolContext = makeToolContext(context, toolCallId, cancelToken);
|
|
556
|
-
return await this.tools.execute(name, args, toolContext);
|
|
557
|
-
}
|
|
558
|
-
catch (error) {
|
|
559
|
-
return { ok: false, error: errorMessage(error) };
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
finishToolEvent(name, args, result, eventContext) {
|
|
563
|
-
this.onToolEvent?.(name, args, result);
|
|
564
|
-
const status = result["status"];
|
|
565
|
-
this.emit("tool_result", {
|
|
566
|
-
...eventContext,
|
|
567
|
-
status: (typeof status === "string" && status) ||
|
|
568
|
-
(result["ok"] ? "completed" : "failed"),
|
|
569
|
-
result: safeResult(result),
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
186
|
// --- History commits ---------------------------------------------------------
|
|
573
187
|
/**
|
|
574
188
|
* Append the rendered baseline project instructions once per session,
|
|
@@ -630,17 +244,6 @@ export class CodingAgent {
|
|
|
630
244
|
}
|
|
631
245
|
this.messages.push({ role: "user", content: rendered });
|
|
632
246
|
}
|
|
633
|
-
commitContextMessage(commit, message) {
|
|
634
|
-
const append = () => {
|
|
635
|
-
this.messages.push(message);
|
|
636
|
-
};
|
|
637
|
-
const rollback = () => {
|
|
638
|
-
if (this.messages.at(-1) === message) {
|
|
639
|
-
this.messages.pop();
|
|
640
|
-
}
|
|
641
|
-
};
|
|
642
|
-
return Boolean(commit(append, rollback));
|
|
643
|
-
}
|
|
644
247
|
// --- Events --------------------------------------------------------------------
|
|
645
248
|
emit(eventType, payload) {
|
|
646
249
|
const fullPayload = { turn: this.turn, ...payload };
|
|
@@ -738,121 +341,4 @@ function raiseIfCancelled(token) {
|
|
|
738
341
|
throw new AgentCancelled(token?.reason || "cancelled");
|
|
739
342
|
}
|
|
740
343
|
}
|
|
741
|
-
function cancelledToolResult(token) {
|
|
742
|
-
return {
|
|
743
|
-
ok: false,
|
|
744
|
-
status: "cancelled",
|
|
745
|
-
error: token?.reason || "cancelled",
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
function usageTotalTokens(usage) {
|
|
749
|
-
if (usage === null || usage === undefined || typeof usage !== "object") {
|
|
750
|
-
return 0;
|
|
751
|
-
}
|
|
752
|
-
const record = usage;
|
|
753
|
-
const total = record["total_tokens"];
|
|
754
|
-
if (isJsonInteger(total)) {
|
|
755
|
-
return Math.max(0, total);
|
|
756
|
-
}
|
|
757
|
-
const input = record["prompt_tokens"] ?? record["input_tokens"] ?? 0;
|
|
758
|
-
const output = record["completion_tokens"] ?? record["output_tokens"] ?? 0;
|
|
759
|
-
let sum = 0;
|
|
760
|
-
for (const value of [input, output]) {
|
|
761
|
-
if (isJsonInteger(value) && value > 0) {
|
|
762
|
-
sum += value;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
return sum;
|
|
766
|
-
}
|
|
767
|
-
function isJsonInteger(value) {
|
|
768
|
-
return typeof value === "number" && Number.isInteger(value);
|
|
769
|
-
}
|
|
770
|
-
/** Rough 4-bytes-per-token estimate when the API returns no usage. */
|
|
771
|
-
function estimateRequestTokens(messages, response) {
|
|
772
|
-
const serialized = JSON.stringify([...messages, response]);
|
|
773
|
-
return Math.max(1, Math.floor((Buffer.byteLength(serialized, "utf8") + 3) / 4));
|
|
774
|
-
}
|
|
775
|
-
/** Recursively drop the volatile duration_ms field from tool results. */
|
|
776
|
-
function stableToolResult(value) {
|
|
777
|
-
if (Array.isArray(value)) {
|
|
778
|
-
return value.map((item) => stableToolResult(item));
|
|
779
|
-
}
|
|
780
|
-
if (value !== null && typeof value === "object") {
|
|
781
|
-
const result = {};
|
|
782
|
-
for (const [key, item] of Object.entries(value)) {
|
|
783
|
-
if (key !== "duration_ms") {
|
|
784
|
-
result[key] = stableToolResult(item);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
return result;
|
|
788
|
-
}
|
|
789
|
-
return value;
|
|
790
|
-
}
|
|
791
|
-
/** Compact JSON with sorted object keys (Python json.dumps sort_keys). */
|
|
792
|
-
function stableStringify(value) {
|
|
793
|
-
if (value === null) {
|
|
794
|
-
return "null";
|
|
795
|
-
}
|
|
796
|
-
if (Array.isArray(value)) {
|
|
797
|
-
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
798
|
-
}
|
|
799
|
-
if (typeof value === "object") {
|
|
800
|
-
const record = value;
|
|
801
|
-
const parts = Object.keys(record)
|
|
802
|
-
.filter((key) => record[key] !== undefined)
|
|
803
|
-
.sort()
|
|
804
|
-
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
|
|
805
|
-
return `{${parts.join(",")}}`;
|
|
806
|
-
}
|
|
807
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
808
|
-
return JSON.stringify(value);
|
|
809
|
-
}
|
|
810
|
-
return JSON.stringify(String(value));
|
|
811
|
-
}
|
|
812
|
-
function guardErrorMessage(payload) {
|
|
813
|
-
let message = "Agent safety guard stopped tool use but could not produce a final " +
|
|
814
|
-
`answer: ${String(payload["reason"])}. ` +
|
|
815
|
-
`Tool rounds: ${String(payload["tool_rounds"])}; ` +
|
|
816
|
-
`model requests: ${String(payload["model_requests"])}; ` +
|
|
817
|
-
`tokens counted: ${String(payload["total_tokens"])}; ` +
|
|
818
|
-
`elapsed: ${String(payload["elapsed_ms"])}ms.`;
|
|
819
|
-
if (payload["final_error"]) {
|
|
820
|
-
message += ` Final request failed: ${String(payload["final_error"])}`;
|
|
821
|
-
}
|
|
822
|
-
return message;
|
|
823
|
-
}
|
|
824
|
-
function safeArguments(args) {
|
|
825
|
-
const safe = { ...args };
|
|
826
|
-
for (const key of ["content", "old_text", "new_text"]) {
|
|
827
|
-
const value = safe[key];
|
|
828
|
-
if (typeof value === "string") {
|
|
829
|
-
safe[key] = `<${value.length} chars>`;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
const edits = safe["edits"];
|
|
833
|
-
if (Array.isArray(edits)) {
|
|
834
|
-
safe["edits"] = `<${edits.length} edits>`;
|
|
835
|
-
}
|
|
836
|
-
const result = {};
|
|
837
|
-
for (const [key, value] of Object.entries(safe)) {
|
|
838
|
-
result[key] = typeof value === "string" ? truncateForEvent(value) : value;
|
|
839
|
-
}
|
|
840
|
-
return result;
|
|
841
|
-
}
|
|
842
|
-
function safeResult(result) {
|
|
843
|
-
const safe = {};
|
|
844
|
-
for (const [key, value] of Object.entries(result)) {
|
|
845
|
-
safe[key] = typeof value === "string" ? truncateForEvent(value) : value;
|
|
846
|
-
}
|
|
847
|
-
return safe;
|
|
848
|
-
}
|
|
849
|
-
function truncateForEvent(value, limit = 4_000) {
|
|
850
|
-
if (value.length <= limit) {
|
|
851
|
-
return value;
|
|
852
|
-
}
|
|
853
|
-
return `${value.slice(0, limit)}\n...[truncated ${value.length - limit} chars]`;
|
|
854
|
-
}
|
|
855
|
-
function errorMessage(error) {
|
|
856
|
-
return error instanceof Error ? error.message : String(error);
|
|
857
|
-
}
|
|
858
344
|
//# sourceMappingURL=agent.js.map
|