@bubblebrain-ai/bubble 0.0.12 → 0.0.14
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/dist/agent/execution-governor.js +1 -1
- package/dist/agent/input-controller.d.ts +11 -0
- package/dist/agent/input-controller.js +30 -0
- package/dist/agent/tool-intent.js +1 -0
- package/dist/agent.d.ts +8 -4
- package/dist/agent.js +623 -312
- package/dist/approval/controller.d.ts +1 -0
- package/dist/approval/controller.js +20 -3
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +14 -1
- package/dist/context/compact.js +9 -3
- package/dist/context/projector.js +27 -12
- package/dist/debug-trace.d.ts +27 -0
- package/dist/debug-trace.js +385 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/serve.js +7 -1
- package/dist/main.js +86 -9
- package/dist/model-catalog.js +1 -0
- package/dist/orchestrator/default-hooks.js +19 -8
- package/dist/orchestrator/hooks.d.ts +1 -0
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.d.ts +5 -6
- package/dist/prompt/reminders.js +8 -9
- package/dist/prompt/runtime.js +2 -2
- package/dist/provider-openai-codex.d.ts +7 -0
- package/dist/provider-openai-codex.js +265 -124
- package/dist/provider-registry.d.ts +2 -0
- package/dist/provider-registry.js +58 -9
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +5 -1
- package/dist/session-log.js +13 -1
- package/dist/slash-commands/commands.js +39 -0
- package/dist/slash-commands/types.d.ts +12 -0
- package/dist/stats/usage.d.ts +52 -0
- package/dist/stats/usage.js +414 -0
- package/dist/tools/apply-patch.d.ts +9 -0
- package/dist/tools/apply-patch.js +330 -0
- package/dist/tools/bash.js +205 -44
- package/dist/tools/edit-apply.d.ts +5 -2
- package/dist/tools/edit-apply.js +221 -31
- package/dist/tools/edit.js +12 -3
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +12 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch-apply.d.ts +41 -0
- package/dist/tools/patch-apply.js +312 -0
- package/dist/tools/server-manager.d.ts +36 -0
- package/dist/tools/server-manager.js +234 -0
- package/dist/tools/server.d.ts +6 -0
- package/dist/tools/server.js +245 -0
- package/dist/tools/write.d.ts +3 -6
- package/dist/tools/write.js +26 -46
- package/dist/tui/clipboard.d.ts +1 -0
- package/dist/tui/clipboard.js +53 -0
- package/dist/tui/detect-theme.d.ts +2 -0
- package/dist/tui/detect-theme.js +87 -0
- package/dist/tui/display-history.d.ts +63 -0
- package/dist/tui/display-history.js +306 -0
- package/dist/tui/edit-diff.d.ts +11 -0
- package/dist/tui/edit-diff.js +57 -0
- package/dist/tui/escape-confirmation.d.ts +15 -0
- package/dist/tui/escape-confirmation.js +30 -0
- package/dist/tui/file-mentions.d.ts +29 -0
- package/dist/tui/file-mentions.js +174 -0
- package/dist/tui/global-key-router.d.ts +3 -0
- package/dist/tui/global-key-router.js +87 -0
- package/dist/tui/image-paste.d.ts +95 -0
- package/dist/tui/image-paste.js +505 -0
- package/dist/tui/input-history.d.ts +16 -0
- package/dist/tui/input-history.js +79 -0
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/markdown-theme-rules.d.ts +23 -0
- package/dist/tui/markdown-theme-rules.js +164 -0
- package/dist/tui/markdown-theme.d.ts +5 -0
- package/dist/tui/markdown-theme.js +27 -0
- package/dist/tui/model-picker-data.d.ts +10 -0
- package/dist/tui/model-picker-data.js +32 -0
- package/dist/tui/opencode-spinner.d.ts +22 -0
- package/dist/tui/opencode-spinner.js +216 -0
- package/dist/tui/prompt-keybindings.d.ts +42 -0
- package/dist/tui/prompt-keybindings.js +35 -0
- package/dist/tui/recent-activity.d.ts +8 -0
- package/dist/tui/recent-activity.js +71 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.d.ts +45 -0
- package/dist/tui/run.js +9359 -0
- package/dist/tui/session-display.d.ts +6 -0
- package/dist/tui/session-display.js +12 -0
- package/dist/tui/sidebar-mcp.d.ts +31 -0
- package/dist/tui/sidebar-mcp.js +62 -0
- package/dist/tui/sidebar-state.d.ts +12 -0
- package/dist/tui/sidebar-state.js +69 -0
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +135 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +32 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/tui/trace-groups.d.ts +27 -0
- package/dist/tui/trace-groups.js +419 -0
- package/dist/tui/wordmark.d.ts +15 -0
- package/dist/tui/wordmark.js +179 -0
- package/dist/tui-ink/app.js +45 -9
- package/dist/tui-ink/approval/approval-dialog.js +7 -1
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +5 -4
- package/dist/tui-ink/message-list.js +23 -9
- package/dist/tui-ink/theme.d.ts +3 -9
- package/dist/tui-ink/theme.js +39 -45
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/tui-ink/welcome.js +22 -78
- package/dist/tui-opentui/app.d.ts +54 -0
- package/dist/tui-opentui/app.js +1365 -0
- package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
- package/dist/tui-opentui/approval/approval-dialog.js +145 -0
- package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
- package/dist/tui-opentui/approval/diff-view.js +43 -0
- package/dist/tui-opentui/approval/select.d.ts +37 -0
- package/dist/tui-opentui/approval/select.js +91 -0
- package/dist/tui-opentui/detect-theme.d.ts +2 -0
- package/dist/tui-opentui/detect-theme.js +87 -0
- package/dist/tui-opentui/display-history.d.ts +56 -0
- package/dist/tui-opentui/display-history.js +130 -0
- package/dist/tui-opentui/edit-diff.d.ts +11 -0
- package/dist/tui-opentui/edit-diff.js +57 -0
- package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
- package/dist/tui-opentui/feedback-dialog.js +164 -0
- package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
- package/dist/tui-opentui/feishu-setup-picker.js +272 -0
- package/dist/tui-opentui/file-mentions.d.ts +29 -0
- package/dist/tui-opentui/file-mentions.js +174 -0
- package/dist/tui-opentui/footer.d.ts +26 -0
- package/dist/tui-opentui/footer.js +40 -0
- package/dist/tui-opentui/image-paste.d.ts +54 -0
- package/dist/tui-opentui/image-paste.js +288 -0
- package/dist/tui-opentui/input-box.d.ts +34 -0
- package/dist/tui-opentui/input-box.js +471 -0
- package/dist/tui-opentui/input-history.d.ts +16 -0
- package/dist/tui-opentui/input-history.js +79 -0
- package/dist/tui-opentui/markdown.d.ts +66 -0
- package/dist/tui-opentui/markdown.js +127 -0
- package/dist/tui-opentui/message-list.d.ts +31 -0
- package/dist/tui-opentui/message-list.js +128 -0
- package/dist/tui-opentui/model-picker.d.ts +63 -0
- package/dist/tui-opentui/model-picker.js +450 -0
- package/dist/tui-opentui/plan-confirm.d.ts +9 -0
- package/dist/tui-opentui/plan-confirm.js +124 -0
- package/dist/tui-opentui/question-dialog.d.ts +10 -0
- package/dist/tui-opentui/question-dialog.js +110 -0
- package/dist/tui-opentui/recent-activity.d.ts +8 -0
- package/dist/tui-opentui/recent-activity.js +71 -0
- package/dist/tui-opentui/run-session-picker.d.ts +10 -0
- package/dist/tui-opentui/run-session-picker.js +28 -0
- package/dist/tui-opentui/run.d.ts +38 -0
- package/dist/tui-opentui/run.js +48 -0
- package/dist/tui-opentui/session-picker.d.ts +12 -0
- package/dist/tui-opentui/session-picker.js +120 -0
- package/dist/tui-opentui/theme.d.ts +89 -0
- package/dist/tui-opentui/theme.js +157 -0
- package/dist/tui-opentui/todos.d.ts +9 -0
- package/dist/tui-opentui/todos.js +45 -0
- package/dist/tui-opentui/trace-groups.d.ts +27 -0
- package/dist/tui-opentui/trace-groups.js +419 -0
- package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
- package/dist/tui-opentui/use-terminal-size.js +5 -0
- package/dist/tui-opentui/welcome.d.ts +25 -0
- package/dist/tui-opentui/welcome.js +77 -0
- package/dist/types.d.ts +36 -2
- package/package.json +5 -1
package/dist/agent.js
CHANGED
|
@@ -22,6 +22,8 @@ import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subage
|
|
|
22
22
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
23
23
|
import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
|
|
24
24
|
import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
|
|
25
|
+
import { stopAutoServersForSession } from "./tools/server-manager.js";
|
|
26
|
+
import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceMessage, summarizeTraceToolResult, summarizeTraceValue, traceEvent, } from "./debug-trace.js";
|
|
25
27
|
const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
|
|
26
28
|
const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
|
|
27
29
|
const RESIDENT_HISTORY_MESSAGE_LIMIT = 160;
|
|
@@ -29,6 +31,12 @@ const RESIDENT_HISTORY_CHAR_SOFT_LIMIT = 256 * 1024;
|
|
|
29
31
|
const RESIDENT_HISTORY_CHAR_HARD_LIMIT = 512 * 1024;
|
|
30
32
|
const RESIDENT_HISTORY_HEAP_SOFT_LIMIT = 512 * 1024 * 1024;
|
|
31
33
|
const RESIDENT_HISTORY_HEAP_HARD_LIMIT = 768 * 1024 * 1024;
|
|
34
|
+
const MAX_EMPTY_ASSISTANT_RECOVERIES = 1;
|
|
35
|
+
const EMPTY_ASSISTANT_RECOVERY_REMINDER = "The previous model response contained no user-visible assistant content and no tool calls. " +
|
|
36
|
+
"Respond now with a concise, user-visible answer, or call an available tool if more work is required. " +
|
|
37
|
+
"Do not put the final answer only in hidden reasoning.";
|
|
38
|
+
const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
|
|
39
|
+
const INTERRUPTED_ASSISTANT_CONTENT = "Interrupted by user. The prior request was stopped and should not be resumed unless the user asks.";
|
|
32
40
|
export class AgentAbortError extends Error {
|
|
33
41
|
constructor(message = "Agent run cancelled.") {
|
|
34
42
|
super(message);
|
|
@@ -236,6 +244,24 @@ export class Agent {
|
|
|
236
244
|
}
|
|
237
245
|
async *run(userInput, cwd, options = {}) {
|
|
238
246
|
const abortSignal = composeAbortSignals([options.abortSignal, this.budgetLedger?.signal]);
|
|
247
|
+
const inputController = options.inputController;
|
|
248
|
+
const traceContext = {
|
|
249
|
+
cwd,
|
|
250
|
+
sessionFile: this.sessionID,
|
|
251
|
+
provider: this._providerId || "none",
|
|
252
|
+
model: this.apiModel || "none",
|
|
253
|
+
};
|
|
254
|
+
const emit = (event) => {
|
|
255
|
+
traceEvent("agent_event", summarizeAgentEventForTrace(event), traceContext);
|
|
256
|
+
return event;
|
|
257
|
+
};
|
|
258
|
+
traceEvent("agent_run_start", {
|
|
259
|
+
input: summarizeTraceValue(userInput),
|
|
260
|
+
mode: this._mode,
|
|
261
|
+
messageCount: this.messages.length,
|
|
262
|
+
toolCount: this.tools.size,
|
|
263
|
+
deferredUnlocked: this.unlockedDeferred.size,
|
|
264
|
+
}, traceContext);
|
|
239
265
|
throwIfAborted(abortSignal);
|
|
240
266
|
const hookBus = new HookBus();
|
|
241
267
|
for (const hooks of createDefaultHooks()) {
|
|
@@ -249,6 +275,39 @@ export class Agent {
|
|
|
249
275
|
const queueReminder = (reminder) => {
|
|
250
276
|
reminderQueue.push(reminder);
|
|
251
277
|
};
|
|
278
|
+
const pendingInputCount = () => inputController?.pendingInputCount() ?? 0;
|
|
279
|
+
const applyPendingInputs = () => {
|
|
280
|
+
const pendingInputs = inputController?.drainPendingInputs() ?? [];
|
|
281
|
+
if (pendingInputs.length === 0)
|
|
282
|
+
return [];
|
|
283
|
+
for (const input of pendingInputs) {
|
|
284
|
+
this.appendMessage({ role: "user", content: input.content });
|
|
285
|
+
}
|
|
286
|
+
return [
|
|
287
|
+
...pendingInputs.map((input) => ({
|
|
288
|
+
type: "input_applied",
|
|
289
|
+
id: input.id,
|
|
290
|
+
content: input.content,
|
|
291
|
+
target: "current_turn",
|
|
292
|
+
})),
|
|
293
|
+
{ type: "input_pending_changed", pending: pendingInputCount() },
|
|
294
|
+
];
|
|
295
|
+
};
|
|
296
|
+
const rejectPendingInputs = (reason) => {
|
|
297
|
+
const pendingInputs = inputController?.drainPendingInputs() ?? [];
|
|
298
|
+
if (pendingInputs.length === 0)
|
|
299
|
+
return [];
|
|
300
|
+
return [
|
|
301
|
+
...pendingInputs.map((input) => ({
|
|
302
|
+
type: "input_rejected",
|
|
303
|
+
id: input.id,
|
|
304
|
+
content: input.content,
|
|
305
|
+
reason,
|
|
306
|
+
target: "next_turn",
|
|
307
|
+
})),
|
|
308
|
+
{ type: "input_pending_changed", pending: pendingInputCount() },
|
|
309
|
+
];
|
|
310
|
+
};
|
|
252
311
|
const flushGovernorReminders = () => {
|
|
253
312
|
for (const reminder of reminderQueue.splice(0, reminderQueue.length)) {
|
|
254
313
|
this.injectSystemReminder(reminder);
|
|
@@ -256,7 +315,7 @@ export class Agent {
|
|
|
256
315
|
};
|
|
257
316
|
if (this._todos.length > 0 && this._todos.every((t) => t.status === "completed")) {
|
|
258
317
|
this.setTodos([]);
|
|
259
|
-
yield { type: "todos_updated", todos: [] };
|
|
318
|
+
yield emit({ type: "todos_updated", todos: [] });
|
|
260
319
|
}
|
|
261
320
|
this.appendMessage({ role: "user", content: userInput });
|
|
262
321
|
await hookBus.runBeforeTurn({
|
|
@@ -269,346 +328,487 @@ export class Agent {
|
|
|
269
328
|
});
|
|
270
329
|
flushGovernorReminders();
|
|
271
330
|
let consecutiveOverflowRecoveries = 0;
|
|
331
|
+
let consecutiveEmptyAssistantRecoveries = 0;
|
|
272
332
|
let step = 0;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
toolEntries,
|
|
314
|
-
disableTools: (reason) => {
|
|
315
|
-
hookState.forceTextOnlyReason = reason;
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
await hookBus.runBeforeModelCall(beforeModelCallCtx);
|
|
319
|
-
toolEntries = beforeModelCallCtx.toolEntries;
|
|
320
|
-
if (this._mode !== "plan") {
|
|
321
|
-
toolEntries = toolEntries.filter((t) => t.name !== "exit_plan_mode");
|
|
322
|
-
}
|
|
323
|
-
flushGovernorReminders();
|
|
324
|
-
const toolDefinitions = ((hookState.forceTextOnlyReason ? [] : toolEntries))
|
|
325
|
-
.map((t) => ({
|
|
326
|
-
name: t.name,
|
|
327
|
-
description: t.description,
|
|
328
|
-
parameters: t.parameters,
|
|
329
|
-
}));
|
|
330
|
-
// LLM-driven compaction runs ahead of projector's algorithmic passes. If
|
|
331
|
-
// it succeeds, this.messages is replaced with [preserved system+meta] +
|
|
332
|
-
// [LLM summary] + [last user msg], and the projector becomes a no-op for
|
|
333
|
-
// budget. If it fails (network error, etc.), the projector's existing
|
|
334
|
-
// algorithmic fallback still kicks in.
|
|
335
|
-
await this.maybeCompactWithLLM();
|
|
336
|
-
try {
|
|
337
|
-
const projectedMessages = projectMessages(this.messages, {
|
|
338
|
-
mode: "budgeted",
|
|
333
|
+
let autoServersStopped = false;
|
|
334
|
+
const stopOwnedAutoServers = async () => {
|
|
335
|
+
if (autoServersStopped)
|
|
336
|
+
return;
|
|
337
|
+
autoServersStopped = true;
|
|
338
|
+
await stopAutoServersForSession(this.sessionID);
|
|
339
|
+
};
|
|
340
|
+
let currentAssistantMsg;
|
|
341
|
+
let currentAssistantAppended = false;
|
|
342
|
+
try {
|
|
343
|
+
while (true) {
|
|
344
|
+
throwIfAborted(abortSignal);
|
|
345
|
+
flushGovernorReminders();
|
|
346
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
347
|
+
yield emit(update);
|
|
348
|
+
for (const event of applyPendingInputs())
|
|
349
|
+
yield emit(event);
|
|
350
|
+
yield emit({ type: "turn_start" });
|
|
351
|
+
step += 1;
|
|
352
|
+
hookState.turnCount = step;
|
|
353
|
+
if (this.taskBudget) {
|
|
354
|
+
hookState.taskBudget = {
|
|
355
|
+
total: this.taskBudget.total,
|
|
356
|
+
spent: hookState.taskBudget?.spent ?? 0,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
let forceTextOnlyReason = hookState.forceTextOnlyReason;
|
|
360
|
+
if (!forceTextOnlyReason && this.maxTurns !== undefined && step >= this.maxTurns) {
|
|
361
|
+
forceTextOnlyReason = "The configured maximum turns for this agent have been reached.";
|
|
362
|
+
hookState.forceTextOnlyReason = forceTextOnlyReason;
|
|
363
|
+
}
|
|
364
|
+
if (forceTextOnlyReason) {
|
|
365
|
+
this.injectSystemReminder(buildToolFreezeReminder(forceTextOnlyReason));
|
|
366
|
+
}
|
|
367
|
+
const assistantMsg = {
|
|
368
|
+
role: "assistant",
|
|
369
|
+
content: "",
|
|
370
|
+
reasoning: "",
|
|
371
|
+
toolCalls: [],
|
|
372
|
+
model: this._model,
|
|
339
373
|
providerId: this.providerId,
|
|
340
374
|
modelId: this.apiModel,
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
375
|
+
};
|
|
376
|
+
const streamingToolCalls = new Map();
|
|
377
|
+
let turnUsage;
|
|
378
|
+
let assistantAppended = false;
|
|
379
|
+
currentAssistantMsg = assistantMsg;
|
|
380
|
+
currentAssistantAppended = false;
|
|
381
|
+
let toolEntries = Array.from(this.tools.values())
|
|
382
|
+
.filter((t) => !t.deferred || this.unlockedDeferred.has(t.name));
|
|
383
|
+
const beforeModelCallCtx = {
|
|
384
|
+
agent: this,
|
|
385
|
+
cwd,
|
|
386
|
+
input: userInput,
|
|
387
|
+
state: hookState,
|
|
388
|
+
queueReminder,
|
|
389
|
+
flushReminders: flushGovernorReminders,
|
|
390
|
+
toolEntries,
|
|
391
|
+
disableTools: (reason) => {
|
|
392
|
+
hookState.forceTextOnlyReason = reason;
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
await hookBus.runBeforeModelCall(beforeModelCallCtx);
|
|
396
|
+
toolEntries = beforeModelCallCtx.toolEntries;
|
|
397
|
+
if (this._mode !== "plan") {
|
|
398
|
+
toolEntries = toolEntries.filter((t) => t.name !== "exit_plan_mode");
|
|
399
|
+
}
|
|
400
|
+
flushGovernorReminders();
|
|
401
|
+
const toolDefinitions = ((hookState.forceTextOnlyReason ? [] : toolEntries))
|
|
402
|
+
.map((t) => ({
|
|
403
|
+
name: t.name,
|
|
404
|
+
description: t.description,
|
|
405
|
+
parameters: t.parameters,
|
|
406
|
+
}));
|
|
407
|
+
// LLM-driven compaction runs ahead of projector's algorithmic passes. If
|
|
408
|
+
// it succeeds, this.messages is replaced with [preserved system+meta] +
|
|
409
|
+
// [LLM summary] + [last user msg], and the projector becomes a no-op for
|
|
410
|
+
// budget. If it fails (network error, etc.), the projector's existing
|
|
411
|
+
// algorithmic fallback still kicks in.
|
|
412
|
+
await this.maybeCompactWithLLM();
|
|
413
|
+
try {
|
|
414
|
+
const projectedMessages = projectMessages(this.messages, {
|
|
415
|
+
mode: "budgeted",
|
|
416
|
+
providerId: this.providerId,
|
|
417
|
+
modelId: this.apiModel,
|
|
418
|
+
usageAnchorTokens: this.lastInputTokens ?? undefined,
|
|
419
|
+
anchorMessageCount: this.lastAnchorMessageCount ?? undefined,
|
|
420
|
+
});
|
|
421
|
+
const providerStartedAt = Date.now();
|
|
422
|
+
let streamTextChars = 0;
|
|
423
|
+
let streamReasoningChars = 0;
|
|
424
|
+
let streamToolCallDeltas = 0;
|
|
425
|
+
traceEvent("provider_stream_start", {
|
|
426
|
+
residentMessageCount: this.messages.length,
|
|
427
|
+
projectedMessageCount: projectedMessages.length,
|
|
428
|
+
toolCount: toolDefinitions.length,
|
|
429
|
+
thinkingLevel: this.thinkingLevel,
|
|
430
|
+
mode: this._mode,
|
|
431
|
+
}, traceContext);
|
|
432
|
+
const stream = this.provider.streamChat(projectedMessages, {
|
|
433
|
+
model: this.apiModel,
|
|
434
|
+
tools: toolDefinitions,
|
|
435
|
+
temperature: this.temperature,
|
|
436
|
+
thinkingLevel: this.thinkingLevel,
|
|
437
|
+
abortSignal,
|
|
438
|
+
});
|
|
439
|
+
for await (const chunk of stream) {
|
|
440
|
+
throwIfAborted(abortSignal);
|
|
441
|
+
switch (chunk.type) {
|
|
442
|
+
case "text":
|
|
443
|
+
assistantMsg.content += chunk.content;
|
|
444
|
+
streamTextChars += chunk.content.length;
|
|
445
|
+
yield emit({ type: "text_delta", content: chunk.content });
|
|
446
|
+
break;
|
|
447
|
+
case "reasoning_delta":
|
|
448
|
+
debugReasoningStream({
|
|
449
|
+
stage: "agent_receive",
|
|
450
|
+
providerId: this._providerId,
|
|
451
|
+
modelId: this.apiModel,
|
|
452
|
+
turnStep: step,
|
|
453
|
+
beforeLength: assistantMsg.reasoning?.length ?? 0,
|
|
454
|
+
delta: summarizeDebugText(chunk.content),
|
|
455
|
+
afterLength: (assistantMsg.reasoning?.length ?? 0) + chunk.content.length,
|
|
456
|
+
});
|
|
457
|
+
assistantMsg.reasoning = (assistantMsg.reasoning || "") + chunk.content;
|
|
458
|
+
streamReasoningChars += chunk.content.length;
|
|
459
|
+
yield emit({ type: "reasoning_delta", content: chunk.content });
|
|
460
|
+
break;
|
|
461
|
+
case "tool_call":
|
|
462
|
+
if (chunk.isStart) {
|
|
463
|
+
streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
|
|
464
|
+
yield emit({ type: "tool_call_start", id: chunk.id, name: chunk.name });
|
|
385
465
|
}
|
|
386
|
-
if (chunk.
|
|
387
|
-
|
|
466
|
+
if (!streamingToolCalls.has(chunk.id)) {
|
|
467
|
+
streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
|
|
388
468
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
469
|
+
const currentToolCall = streamingToolCalls.get(chunk.id);
|
|
470
|
+
if (currentToolCall) {
|
|
471
|
+
currentToolCall.name = chunk.name || currentToolCall.name;
|
|
472
|
+
currentToolCall.args += chunk.arguments;
|
|
473
|
+
if (chunk.argumentsFull !== undefined) {
|
|
474
|
+
currentToolCall.args = chunk.argumentsFull;
|
|
475
|
+
}
|
|
476
|
+
if (chunk.argumentsCorrupt) {
|
|
477
|
+
currentToolCall.argsCorrupt = true;
|
|
478
|
+
}
|
|
479
|
+
if (chunk.arguments) {
|
|
480
|
+
streamToolCallDeltas += 1;
|
|
481
|
+
yield emit({
|
|
482
|
+
type: "tool_call_delta",
|
|
483
|
+
id: currentToolCall.id,
|
|
484
|
+
name: currentToolCall.name,
|
|
485
|
+
argumentsDelta: chunk.arguments,
|
|
486
|
+
arguments: currentToolCall.args,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (chunk.isEnd && currentToolCall) {
|
|
491
|
+
assistantMsg.toolCalls.push({
|
|
392
492
|
id: currentToolCall.id,
|
|
393
493
|
name: currentToolCall.name,
|
|
394
|
-
argumentsDelta: chunk.arguments,
|
|
395
494
|
arguments: currentToolCall.args,
|
|
396
|
-
|
|
495
|
+
...(currentToolCall.argsCorrupt ? { argsCorrupt: true } : {}),
|
|
496
|
+
});
|
|
497
|
+
yield emit({
|
|
498
|
+
type: "tool_call_end",
|
|
499
|
+
id: currentToolCall.id,
|
|
500
|
+
name: currentToolCall.name,
|
|
501
|
+
arguments: currentToolCall.args,
|
|
502
|
+
});
|
|
503
|
+
streamingToolCalls.delete(chunk.id);
|
|
397
504
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
arguments: currentToolCall.args,
|
|
411
|
-
};
|
|
412
|
-
streamingToolCalls.delete(chunk.id);
|
|
413
|
-
}
|
|
414
|
-
break;
|
|
415
|
-
case "usage":
|
|
416
|
-
turnUsage = chunk.usage;
|
|
417
|
-
this.budgetLedger?.recordUsage(chunk.usage, this.budgetSource);
|
|
418
|
-
this.lastInputTokens = chunk.usage.promptTokens;
|
|
419
|
-
this.lastAnchorMessageCount = this.messages.length;
|
|
420
|
-
if (hookState.taskBudget) {
|
|
421
|
-
hookState.taskBudget.spent += chunk.usage.promptTokens + chunk.usage.completionTokens;
|
|
422
|
-
if (hookState.taskBudget.spent >= hookState.taskBudget.total) {
|
|
423
|
-
hookState.forceTextOnlyReason = "The configured task budget for this agent has been exhausted.";
|
|
505
|
+
break;
|
|
506
|
+
case "usage":
|
|
507
|
+
turnUsage = chunk.usage;
|
|
508
|
+
assistantMsg.usage = chunk.usage;
|
|
509
|
+
this.budgetLedger?.recordUsage(chunk.usage, this.budgetSource);
|
|
510
|
+
this.lastInputTokens = chunk.usage.promptTokens;
|
|
511
|
+
this.lastAnchorMessageCount = this.messages.length;
|
|
512
|
+
if (hookState.taskBudget) {
|
|
513
|
+
hookState.taskBudget.spent += chunk.usage.promptTokens + chunk.usage.completionTokens;
|
|
514
|
+
if (hookState.taskBudget.spent >= hookState.taskBudget.total) {
|
|
515
|
+
hookState.forceTextOnlyReason = "The configured task budget for this agent has been exhausted.";
|
|
516
|
+
}
|
|
424
517
|
}
|
|
425
|
-
|
|
426
|
-
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
521
|
+
yield emit(update);
|
|
427
522
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
523
|
+
traceEvent("provider_stream_end", {
|
|
524
|
+
elapsedMs: Date.now() - providerStartedAt,
|
|
525
|
+
textChars: streamTextChars,
|
|
526
|
+
reasoningChars: streamReasoningChars,
|
|
527
|
+
toolCallDeltas: streamToolCallDeltas,
|
|
528
|
+
toolCalls: assistantMsg.toolCalls?.length ?? 0,
|
|
529
|
+
usage: turnUsage,
|
|
530
|
+
}, traceContext);
|
|
531
|
+
throwIfAborted(abortSignal);
|
|
532
|
+
const assistantHasContent = assistantMsg.content.trim().length > 0;
|
|
533
|
+
const assistantHasToolCalls = !!assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0;
|
|
534
|
+
if (!assistantHasContent && !assistantHasToolCalls) {
|
|
535
|
+
if (consecutiveEmptyAssistantRecoveries < MAX_EMPTY_ASSISTANT_RECOVERIES) {
|
|
536
|
+
consecutiveEmptyAssistantRecoveries += 1;
|
|
537
|
+
this.injectSystemReminder(EMPTY_ASSISTANT_RECOVERY_REMINDER);
|
|
538
|
+
yield emit({ type: "turn_end", usage: turnUsage, willContinue: true });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
assistantMsg.content = EMPTY_ASSISTANT_FALLBACK;
|
|
542
|
+
assistantMsg.reasoning = "";
|
|
543
|
+
yield emit({ type: "text_delta", content: assistantMsg.content });
|
|
544
|
+
}
|
|
545
|
+
this.appendMessage(assistantMsg);
|
|
546
|
+
assistantAppended = true;
|
|
547
|
+
currentAssistantAppended = true;
|
|
444
548
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// Execute tools if any
|
|
452
|
-
if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
|
|
453
|
-
const parsedCalls = [];
|
|
454
|
-
for (let index = 0; index < assistantMsg.toolCalls.length; index++) {
|
|
455
|
-
const tc = assistantMsg.toolCalls[index];
|
|
456
|
-
try {
|
|
457
|
-
parsedCalls.push({
|
|
458
|
-
...tc,
|
|
459
|
-
parsedArgs: JSON.parse(tc.arguments),
|
|
460
|
-
...(tc.argsCorrupt ? { argsCorrupt: true } : {}),
|
|
461
|
-
});
|
|
549
|
+
catch (error) {
|
|
550
|
+
traceEvent("provider_stream_error", {
|
|
551
|
+
error: summarizeTraceError(error),
|
|
552
|
+
}, traceContext);
|
|
553
|
+
if (assistantAppended) {
|
|
554
|
+
throw error;
|
|
462
555
|
}
|
|
463
|
-
|
|
464
|
-
|
|
556
|
+
if (!isContextOverflowError(error)) {
|
|
557
|
+
if (!isAbortLikeError(error, abortSignal) && shouldAppendModelInterruptedBoundary(this.messages)) {
|
|
558
|
+
this.appendMessage(createModelInterruptedMessage(error, {
|
|
559
|
+
model: this._model,
|
|
560
|
+
providerId: this.providerId,
|
|
561
|
+
modelId: this.apiModel,
|
|
562
|
+
}));
|
|
563
|
+
assistantAppended = true;
|
|
564
|
+
}
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
if (consecutiveOverflowRecoveries >= MAX_CONSECUTIVE_OVERFLOW_RECOVERIES) {
|
|
568
|
+
throw error;
|
|
465
569
|
}
|
|
570
|
+
const droppedMessages = await this.recoverFromOverflow(consecutiveOverflowRecoveries);
|
|
571
|
+
consecutiveOverflowRecoveries += 1;
|
|
572
|
+
yield emit({ type: "context_recovered", droppedMessages, reason: "overflow" });
|
|
573
|
+
continue;
|
|
466
574
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
tc
|
|
483
|
-
}
|
|
484
|
-
blockToolCall: (result) => {
|
|
485
|
-
blockedResult = result;
|
|
486
|
-
},
|
|
487
|
-
});
|
|
488
|
-
assistantMsg.toolCalls[index] = {
|
|
489
|
-
id: tc.id,
|
|
490
|
-
name: tc.name,
|
|
491
|
-
arguments: tc.arguments,
|
|
492
|
-
};
|
|
493
|
-
flushGovernorReminders();
|
|
494
|
-
yield { type: "tool_start", id: tc.id, name: tc.name, args: tc.parsedArgs };
|
|
495
|
-
const todosVersionBefore = this._todosVersion;
|
|
496
|
-
const modeVersionBefore = this._modeVersion;
|
|
497
|
-
const updateQueue = createUpdateQueue();
|
|
498
|
-
let result;
|
|
499
|
-
if (blockedResult) {
|
|
500
|
-
result = blockedResult;
|
|
575
|
+
consecutiveOverflowRecoveries = 0;
|
|
576
|
+
consecutiveEmptyAssistantRecoveries = 0;
|
|
577
|
+
// Execute tools if any
|
|
578
|
+
if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
|
|
579
|
+
const parsedCalls = [];
|
|
580
|
+
for (let index = 0; index < assistantMsg.toolCalls.length; index++) {
|
|
581
|
+
const tc = assistantMsg.toolCalls[index];
|
|
582
|
+
try {
|
|
583
|
+
parsedCalls.push({
|
|
584
|
+
...tc,
|
|
585
|
+
parsedArgs: JSON.parse(tc.arguments),
|
|
586
|
+
...(tc.argsCorrupt ? { argsCorrupt: true } : {}),
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
parsedCalls.push({ ...tc, parsedArgs: {}, argsCorrupt: true });
|
|
591
|
+
}
|
|
501
592
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
let
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
.
|
|
515
|
-
|
|
516
|
-
|
|
593
|
+
const executedResults = [];
|
|
594
|
+
const appendCancelledToolMessages = (startIndex) => {
|
|
595
|
+
for (let pendingIndex = startIndex; pendingIndex < parsedCalls.length; pendingIndex++) {
|
|
596
|
+
const pending = parsedCalls[pendingIndex];
|
|
597
|
+
const pendingResult = cancelledToolResult(pending.name);
|
|
598
|
+
this.appendMessage({
|
|
599
|
+
role: "tool",
|
|
600
|
+
toolCallId: pending.id,
|
|
601
|
+
content: pendingResult.content,
|
|
602
|
+
metadata: pendingResult.metadata,
|
|
603
|
+
isError: pendingResult.isError,
|
|
604
|
+
});
|
|
605
|
+
executedResults.push(pendingResult);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
for (let index = 0; index < parsedCalls.length; index++) {
|
|
609
|
+
if (abortSignal?.aborted) {
|
|
610
|
+
appendCancelledToolMessages(index);
|
|
611
|
+
throwIfAborted(abortSignal);
|
|
612
|
+
}
|
|
613
|
+
let tc = parsedCalls[index];
|
|
614
|
+
let blockedResult;
|
|
615
|
+
await hookBus.runBeforeToolCall({
|
|
616
|
+
agent: this,
|
|
617
|
+
cwd,
|
|
618
|
+
input: userInput,
|
|
619
|
+
state: hookState,
|
|
620
|
+
queueReminder,
|
|
621
|
+
flushReminders: flushGovernorReminders,
|
|
622
|
+
toolCall: tc,
|
|
623
|
+
blockedResult,
|
|
624
|
+
replaceToolCall: (toolCall) => {
|
|
625
|
+
tc = toolCall;
|
|
626
|
+
},
|
|
627
|
+
blockToolCall: (result) => {
|
|
628
|
+
blockedResult = result;
|
|
629
|
+
},
|
|
517
630
|
});
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
631
|
+
assistantMsg.toolCalls[index] = {
|
|
632
|
+
id: tc.id,
|
|
633
|
+
name: tc.name,
|
|
634
|
+
arguments: tc.arguments,
|
|
635
|
+
};
|
|
636
|
+
flushGovernorReminders();
|
|
637
|
+
const toolStartedAt = Date.now();
|
|
638
|
+
traceEvent("tool_execute_start", {
|
|
639
|
+
id: tc.id,
|
|
640
|
+
name: tc.name,
|
|
641
|
+
args: summarizeTraceValue(tc.parsedArgs),
|
|
642
|
+
argsCorrupt: tc.argsCorrupt,
|
|
643
|
+
}, traceContext);
|
|
644
|
+
yield emit({ type: "tool_start", id: tc.id, name: tc.name, args: tc.parsedArgs });
|
|
645
|
+
const todosVersionBefore = this._todosVersion;
|
|
646
|
+
const modeVersionBefore = this._modeVersion;
|
|
647
|
+
const updateQueue = createUpdateQueue();
|
|
648
|
+
let result;
|
|
649
|
+
if (blockedResult) {
|
|
650
|
+
result = blockedResult;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const toolExecution = this.executeTool(tc, cwd, abortSignal, (update) => updateQueue.push(update));
|
|
654
|
+
let settled = false;
|
|
655
|
+
let cancelledByAbort = false;
|
|
656
|
+
let resolved;
|
|
657
|
+
let rejected;
|
|
658
|
+
void toolExecution
|
|
659
|
+
.then((value) => {
|
|
660
|
+
resolved = value;
|
|
661
|
+
})
|
|
662
|
+
.catch((error) => {
|
|
663
|
+
rejected = error;
|
|
664
|
+
})
|
|
665
|
+
.finally(() => {
|
|
666
|
+
settled = true;
|
|
667
|
+
updateQueue.wake();
|
|
668
|
+
});
|
|
669
|
+
while (!settled || updateQueue.hasItems()) {
|
|
670
|
+
for (const update of updateQueue.drain()) {
|
|
671
|
+
yield emit({ type: "tool_update", id: tc.id, name: tc.name, update });
|
|
672
|
+
}
|
|
673
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
674
|
+
yield emit(update);
|
|
675
|
+
if (!settled) {
|
|
676
|
+
const waitStatus = await updateQueue.wait(abortSignal);
|
|
677
|
+
if (waitStatus === "aborted" && !settled) {
|
|
678
|
+
cancelledByAbort = true;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
521
682
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (!settled) {
|
|
525
|
-
await updateQueue.wait();
|
|
683
|
+
if (cancelledByAbort) {
|
|
684
|
+
result = cancelledToolResult(tc.name);
|
|
526
685
|
}
|
|
686
|
+
else {
|
|
687
|
+
if (rejected)
|
|
688
|
+
throw rejected;
|
|
689
|
+
result = resolved ?? { content: `Error: Tool "${tc.name}" returned no result`, isError: true };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
await hookBus.runAfterToolCall({
|
|
693
|
+
agent: this,
|
|
694
|
+
cwd,
|
|
695
|
+
input: userInput,
|
|
696
|
+
state: hookState,
|
|
697
|
+
queueReminder,
|
|
698
|
+
flushReminders: flushGovernorReminders,
|
|
699
|
+
toolCall: tc,
|
|
700
|
+
result,
|
|
701
|
+
replaceResult: (next) => {
|
|
702
|
+
result = next;
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
// Honor the model's server-declared per-tool-output token cap (e.g.
|
|
706
|
+
// gpt-5.5 reports 10000). Without this, 4-5 large file reads in a row
|
|
707
|
+
// blow past the input window even though our local estimate looks fine.
|
|
708
|
+
const truncatedOutput = truncateToolOutputForModel(result.content, this.providerId, this.apiModel);
|
|
709
|
+
traceEvent("tool_execute_end", {
|
|
710
|
+
id: tc.id,
|
|
711
|
+
name: tc.name,
|
|
712
|
+
elapsedMs: Date.now() - toolStartedAt,
|
|
713
|
+
result: summarizeTraceToolResult(result),
|
|
714
|
+
outputTruncation: {
|
|
715
|
+
truncated: truncatedOutput.truncated,
|
|
716
|
+
originalTokens: truncatedOutput.originalTokens,
|
|
717
|
+
finalTokens: truncatedOutput.finalTokens,
|
|
718
|
+
limit: truncatedOutput.limit,
|
|
719
|
+
},
|
|
720
|
+
}, traceContext);
|
|
721
|
+
this.appendMessage({
|
|
722
|
+
role: "tool",
|
|
723
|
+
toolCallId: tc.id,
|
|
724
|
+
content: truncatedOutput.content,
|
|
725
|
+
metadata: result.metadata,
|
|
726
|
+
isError: result.isError,
|
|
727
|
+
});
|
|
728
|
+
this.compactResidentHistory();
|
|
729
|
+
flushGovernorReminders();
|
|
730
|
+
this.onToolResult?.(tc.name, result);
|
|
731
|
+
executedResults.push(result);
|
|
732
|
+
yield emit({ type: "tool_end", id: tc.id, name: tc.name, result });
|
|
733
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
734
|
+
yield emit(update);
|
|
735
|
+
if (this._todosVersion !== todosVersionBefore) {
|
|
736
|
+
yield emit({ type: "todos_updated", todos: this.getTodos() });
|
|
737
|
+
}
|
|
738
|
+
if (this._modeVersion !== modeVersionBefore) {
|
|
739
|
+
yield emit({ type: "mode_changed", mode: this._mode });
|
|
740
|
+
}
|
|
741
|
+
if (abortSignal?.aborted) {
|
|
742
|
+
appendCancelledToolMessages(index + 1);
|
|
743
|
+
throwIfAborted(abortSignal);
|
|
527
744
|
}
|
|
528
|
-
if (rejected)
|
|
529
|
-
throw rejected;
|
|
530
|
-
result = resolved ?? { content: `Error: Tool "${tc.name}" returned no result`, isError: true };
|
|
531
745
|
}
|
|
532
|
-
|
|
533
|
-
await hookBus.runAfterToolCall({
|
|
746
|
+
await hookBus.runBeforeContinuation({
|
|
534
747
|
agent: this,
|
|
535
748
|
cwd,
|
|
536
749
|
input: userInput,
|
|
537
750
|
state: hookState,
|
|
538
751
|
queueReminder,
|
|
539
752
|
flushReminders: flushGovernorReminders,
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
753
|
+
toolCalls: parsedCalls,
|
|
754
|
+
toolResults: executedResults,
|
|
755
|
+
requestTextOnlyTurn: (reason) => {
|
|
756
|
+
hookState.forceTextOnlyReason = reason;
|
|
544
757
|
},
|
|
545
758
|
});
|
|
546
|
-
// Honor the model's server-declared per-tool-output token cap (e.g.
|
|
547
|
-
// gpt-5.5 reports 10000). Without this, 4-5 large file reads in a row
|
|
548
|
-
// blow past the input window even though our local estimate looks fine.
|
|
549
|
-
const truncatedOutput = truncateToolOutputForModel(result.content, this.providerId, this.apiModel);
|
|
550
|
-
this.appendMessage({
|
|
551
|
-
role: "tool",
|
|
552
|
-
toolCallId: tc.id,
|
|
553
|
-
content: truncatedOutput.content,
|
|
554
|
-
metadata: result.metadata,
|
|
555
|
-
isError: result.isError,
|
|
556
|
-
});
|
|
557
|
-
this.compactResidentHistory();
|
|
558
759
|
flushGovernorReminders();
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
yield { type: "todos_updated", todos: this.getTodos() };
|
|
566
|
-
}
|
|
567
|
-
if (this._modeVersion !== modeVersionBefore) {
|
|
568
|
-
yield { type: "mode_changed", mode: this._mode };
|
|
569
|
-
}
|
|
760
|
+
yield emit({ type: "turn_end", usage: turnUsage, willContinue: true });
|
|
761
|
+
// Auto-continue: if we have tool results, the LLM needs to respond to them.
|
|
762
|
+
// Emitting the turn boundary keeps UI renderers aligned with the persisted
|
|
763
|
+
// assistant/tool message sequence instead of merging the next answer into
|
|
764
|
+
// the tool-call turn.
|
|
765
|
+
continue;
|
|
570
766
|
}
|
|
571
|
-
await hookBus.
|
|
767
|
+
await hookBus.runAfterTurn({
|
|
572
768
|
agent: this,
|
|
573
769
|
cwd,
|
|
574
770
|
input: userInput,
|
|
575
771
|
state: hookState,
|
|
576
772
|
queueReminder,
|
|
577
773
|
flushReminders: flushGovernorReminders,
|
|
578
|
-
toolCalls: parsedCalls,
|
|
579
|
-
toolResults: executedResults,
|
|
580
|
-
requestTextOnlyTurn: (reason) => {
|
|
581
|
-
hookState.forceTextOnlyReason = reason;
|
|
582
|
-
},
|
|
583
774
|
});
|
|
584
775
|
flushGovernorReminders();
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
776
|
+
const willContinue = !!hookState.forceContinuationReason;
|
|
777
|
+
yield emit({ type: "turn_end", usage: turnUsage, willContinue });
|
|
778
|
+
if (willContinue) {
|
|
779
|
+
delete hookState.forceContinuationReason;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
for (const event of rejectPendingInputs("no_continuation"))
|
|
783
|
+
yield emit(event);
|
|
784
|
+
break;
|
|
591
785
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
786
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
787
|
+
yield emit(update);
|
|
788
|
+
await stopOwnedAutoServers();
|
|
789
|
+
yield emit({ type: "agent_end" });
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
if (isAbortError(error, abortSignal)) {
|
|
793
|
+
const appendedBoundary = this.appendInterruptedAssistantBoundary(currentAssistantMsg, currentAssistantAppended);
|
|
794
|
+
const clearedTodos = this.clearTodosAfterInterruptedRun();
|
|
795
|
+
traceEvent("agent_run_interrupted", {
|
|
796
|
+
appendedBoundary,
|
|
797
|
+
clearedTodos,
|
|
798
|
+
messageCount: this.messages.length,
|
|
799
|
+
}, traceContext);
|
|
800
|
+
if (clearedTodos) {
|
|
801
|
+
yield emit({ type: "todos_updated", todos: this.getTodos() });
|
|
802
|
+
}
|
|
606
803
|
}
|
|
607
|
-
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
await stopOwnedAutoServers();
|
|
808
|
+
traceEvent("agent_run_end", {
|
|
809
|
+
messageCount: this.messages.length,
|
|
810
|
+
}, traceContext);
|
|
608
811
|
}
|
|
609
|
-
for (const update of this.drainSubagentToolUpdates())
|
|
610
|
-
yield update;
|
|
611
|
-
yield { type: "agent_end" };
|
|
612
812
|
}
|
|
613
813
|
async recoverFromOverflow(attempt) {
|
|
614
814
|
const before = this.messages.length;
|
|
@@ -1236,8 +1436,43 @@ export class Agent {
|
|
|
1236
1436
|
}
|
|
1237
1437
|
appendMessage(message) {
|
|
1238
1438
|
this.messages.push(message);
|
|
1439
|
+
traceEvent("agent_message_append", {
|
|
1440
|
+
message: summarizeTraceMessage(message),
|
|
1441
|
+
messageCount: this.messages.length,
|
|
1442
|
+
}, {
|
|
1443
|
+
sessionFile: this.sessionID,
|
|
1444
|
+
provider: this._providerId || "none",
|
|
1445
|
+
model: this.apiModel || "none",
|
|
1446
|
+
});
|
|
1239
1447
|
this.onMessageAppend?.(message);
|
|
1240
1448
|
}
|
|
1449
|
+
appendInterruptedAssistantBoundary(currentAssistant, currentAssistantAppended) {
|
|
1450
|
+
const last = lastProviderMessage(this.messages);
|
|
1451
|
+
if (last?.role === "assistant" && last.error?.aborted) {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
const partialText = !currentAssistantAppended ? currentAssistant?.content.trim() : "";
|
|
1455
|
+
const content = partialText
|
|
1456
|
+
? `${partialText}\n\n${INTERRUPTED_ASSISTANT_CONTENT}`
|
|
1457
|
+
: INTERRUPTED_ASSISTANT_CONTENT;
|
|
1458
|
+
this.appendMessage({
|
|
1459
|
+
role: "assistant",
|
|
1460
|
+
content,
|
|
1461
|
+
reasoning: !currentAssistantAppended ? currentAssistant?.reasoning : undefined,
|
|
1462
|
+
error: {
|
|
1463
|
+
name: "MessageAbortedError",
|
|
1464
|
+
message: "Assistant response was interrupted by the user.",
|
|
1465
|
+
aborted: true,
|
|
1466
|
+
},
|
|
1467
|
+
});
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
clearTodosAfterInterruptedRun() {
|
|
1471
|
+
if (this._todos.length === 0)
|
|
1472
|
+
return false;
|
|
1473
|
+
this.setTodos([]);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1241
1476
|
async executeTool(toolCall, cwd, abortSignal, emitUpdate) {
|
|
1242
1477
|
throwIfAborted(abortSignal);
|
|
1243
1478
|
if (toolCall.name === "exit_plan_mode" && this._mode !== "plan") {
|
|
@@ -1362,9 +1597,61 @@ function throwIfAborted(signal) {
|
|
|
1362
1597
|
throw reason;
|
|
1363
1598
|
throw new AgentAbortError(typeof reason === "string" ? reason : undefined);
|
|
1364
1599
|
}
|
|
1600
|
+
function isAbortLikeError(error, signal) {
|
|
1601
|
+
if (signal?.aborted)
|
|
1602
|
+
return true;
|
|
1603
|
+
if (error instanceof AgentAbortError)
|
|
1604
|
+
return true;
|
|
1605
|
+
if (error instanceof DOMException && error.name === "AbortError")
|
|
1606
|
+
return true;
|
|
1607
|
+
if (typeof error === "object" && error !== null && error.name === "AbortError")
|
|
1608
|
+
return true;
|
|
1609
|
+
return false;
|
|
1610
|
+
}
|
|
1611
|
+
function isAbortError(error, signal) {
|
|
1612
|
+
return isAbortLikeError(error, signal);
|
|
1613
|
+
}
|
|
1614
|
+
function shouldAppendModelInterruptedBoundary(messages) {
|
|
1615
|
+
return messages.at(-1)?.role === "tool";
|
|
1616
|
+
}
|
|
1617
|
+
function createModelInterruptedMessage(error, metadata) {
|
|
1618
|
+
return {
|
|
1619
|
+
role: "assistant",
|
|
1620
|
+
content: `[model request interrupted before a final answer was produced: ${summarizeInterruptError(error)}]`,
|
|
1621
|
+
model: metadata.model,
|
|
1622
|
+
providerId: metadata.providerId,
|
|
1623
|
+
modelId: metadata.modelId,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
function summarizeInterruptError(error) {
|
|
1627
|
+
const message = error instanceof Error
|
|
1628
|
+
? error.message
|
|
1629
|
+
: typeof error === "string"
|
|
1630
|
+
? error
|
|
1631
|
+
: String(error);
|
|
1632
|
+
return message.replace(/\s+/g, " ").trim().slice(0, 240) || "unknown error";
|
|
1633
|
+
}
|
|
1634
|
+
function lastProviderMessage(messages) {
|
|
1635
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1636
|
+
const message = messages[i];
|
|
1637
|
+
if (message.role === "system" || message.role === "meta")
|
|
1638
|
+
continue;
|
|
1639
|
+
return message;
|
|
1640
|
+
}
|
|
1641
|
+
return undefined;
|
|
1642
|
+
}
|
|
1643
|
+
function cancelledToolResult(toolName) {
|
|
1644
|
+
return {
|
|
1645
|
+
content: `Tool "${toolName}" was cancelled.`,
|
|
1646
|
+
isError: true,
|
|
1647
|
+
status: "cancelled",
|
|
1648
|
+
metadata: { reason: "cancelled" },
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1365
1651
|
function createUpdateQueue() {
|
|
1366
1652
|
const items = [];
|
|
1367
1653
|
let waiter;
|
|
1654
|
+
let abortCleanup;
|
|
1368
1655
|
return {
|
|
1369
1656
|
push(item) {
|
|
1370
1657
|
items.push(item);
|
|
@@ -1376,17 +1663,36 @@ function createUpdateQueue() {
|
|
|
1376
1663
|
hasItems() {
|
|
1377
1664
|
return items.length > 0;
|
|
1378
1665
|
},
|
|
1379
|
-
wait() {
|
|
1666
|
+
wait(signal) {
|
|
1380
1667
|
if (items.length > 0)
|
|
1381
|
-
return Promise.resolve();
|
|
1668
|
+
return Promise.resolve("woken");
|
|
1669
|
+
if (signal?.aborted)
|
|
1670
|
+
return Promise.resolve("aborted");
|
|
1382
1671
|
return new Promise((resolve) => {
|
|
1672
|
+
abortCleanup?.();
|
|
1673
|
+
abortCleanup = undefined;
|
|
1674
|
+
const finish = (status) => {
|
|
1675
|
+
if (waiter !== resolve)
|
|
1676
|
+
return;
|
|
1677
|
+
waiter = undefined;
|
|
1678
|
+
abortCleanup?.();
|
|
1679
|
+
abortCleanup = undefined;
|
|
1680
|
+
resolve(status);
|
|
1681
|
+
};
|
|
1682
|
+
if (signal) {
|
|
1683
|
+
const onAbort = () => finish("aborted");
|
|
1684
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1685
|
+
abortCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1686
|
+
}
|
|
1383
1687
|
waiter = resolve;
|
|
1384
1688
|
});
|
|
1385
1689
|
},
|
|
1386
1690
|
wake() {
|
|
1387
1691
|
const resolve = waiter;
|
|
1388
1692
|
waiter = undefined;
|
|
1389
|
-
|
|
1693
|
+
abortCleanup?.();
|
|
1694
|
+
abortCleanup = undefined;
|
|
1695
|
+
resolve?.("woken");
|
|
1390
1696
|
},
|
|
1391
1697
|
};
|
|
1392
1698
|
}
|
|
@@ -1432,21 +1738,26 @@ function sanitizeSubagentSummary(value) {
|
|
|
1432
1738
|
return stripProviderProtocolArtifacts(value).trim();
|
|
1433
1739
|
}
|
|
1434
1740
|
function needsExplicitFinalSummary(record, executedAnyTool) {
|
|
1435
|
-
// If the subagent actually invoked any tool, always solicit an explicit final
|
|
1436
|
-
// summary. We cannot tell from the stream alone whether a tool-free trailing
|
|
1437
|
-
// turn was the real answer or mid-thought narration ("Let me try X next:").
|
|
1438
|
-
// Asking the model to restate its findings is cheap and yields predictable,
|
|
1439
|
-
// clean output. (Profile-validation notes in `toolNotes` do not count as
|
|
1440
|
-
// actual tool executions.)
|
|
1441
|
-
if (executedAnyTool)
|
|
1442
|
-
return true;
|
|
1443
1741
|
if (!record.summary)
|
|
1444
|
-
return
|
|
1742
|
+
return executedAnyTool;
|
|
1445
1743
|
if (isOnlyProviderProtocolArtifacts(record.summary))
|
|
1446
1744
|
return true;
|
|
1447
1745
|
if (/<\/?[||][^<>]*>/.test(record.summary))
|
|
1448
1746
|
return true;
|
|
1449
|
-
|
|
1747
|
+
if (!executedAnyTool)
|
|
1748
|
+
return false;
|
|
1749
|
+
if (record.summary === EMPTY_ASSISTANT_FALLBACK)
|
|
1750
|
+
return true;
|
|
1751
|
+
return isLikelyIntermediateSubagentSummary(record.summary);
|
|
1752
|
+
}
|
|
1753
|
+
function isLikelyIntermediateSubagentSummary(value) {
|
|
1754
|
+
const normalized = value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1755
|
+
if (!normalized)
|
|
1756
|
+
return false;
|
|
1757
|
+
if (/^(let me|i'll|i will|i need to|i should|i'm going to|now i'll|now i will)\b/.test(normalized)) {
|
|
1758
|
+
return true;
|
|
1759
|
+
}
|
|
1760
|
+
return /:\s*$/.test(normalized) && /\b(read|inspect|check|look|search|try|open)\b/.test(normalized);
|
|
1450
1761
|
}
|
|
1451
1762
|
function summarizeSubagentToolEnd(event) {
|
|
1452
1763
|
const metadata = (event.result.metadata ?? {});
|