@bubblebrain-ai/bubble 0.0.13 → 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/tool-intent.js +1 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +589 -316
- 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 +28 -0
- 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 +12 -0
- package/dist/slash-commands/types.d.ts +2 -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/display-history.d.ts +1 -0
- package/dist/tui/display-history.js +5 -4
- package/dist/tui/edit-diff.js +6 -1
- package/dist/tui/model-picker-data.d.ts +10 -0
- package/dist/tui/model-picker-data.js +32 -0
- package/dist/tui/run.js +632 -89
- package/dist/tui/tool-renderers/fallback.js +1 -1
- package/dist/tui/tool-renderers/write-preview.js +2 -0
- package/dist/tui/trace-groups.js +10 -3
- package/dist/tui-ink/app.js +1 -4
- 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 +14 -8
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/tui-opentui/app.js +2 -0
- package/dist/tui-opentui/approval/approval-dialog.js +7 -1
- package/dist/tui-opentui/display-history.d.ts +1 -0
- package/dist/tui-opentui/display-history.js +5 -4
- package/dist/tui-opentui/edit-diff.js +6 -1
- package/dist/tui-opentui/message-list.js +6 -3
- package/dist/tui-opentui/trace-groups.js +10 -3
- package/dist/types.d.ts +12 -2
- package/package.json +1 -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);
|
|
@@ -237,6 +245,23 @@ export class Agent {
|
|
|
237
245
|
async *run(userInput, cwd, options = {}) {
|
|
238
246
|
const abortSignal = composeAbortSignals([options.abortSignal, this.budgetLedger?.signal]);
|
|
239
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);
|
|
240
265
|
throwIfAborted(abortSignal);
|
|
241
266
|
const hookBus = new HookBus();
|
|
242
267
|
for (const hooks of createDefaultHooks()) {
|
|
@@ -290,7 +315,7 @@ export class Agent {
|
|
|
290
315
|
};
|
|
291
316
|
if (this._todos.length > 0 && this._todos.every((t) => t.status === "completed")) {
|
|
292
317
|
this.setTodos([]);
|
|
293
|
-
yield { type: "todos_updated", todos: [] };
|
|
318
|
+
yield emit({ type: "todos_updated", todos: [] });
|
|
294
319
|
}
|
|
295
320
|
this.appendMessage({ role: "user", content: userInput });
|
|
296
321
|
await hookBus.runBeforeTurn({
|
|
@@ -303,350 +328,487 @@ export class Agent {
|
|
|
303
328
|
});
|
|
304
329
|
flushGovernorReminders();
|
|
305
330
|
let consecutiveOverflowRecoveries = 0;
|
|
331
|
+
let consecutiveEmptyAssistantRecoveries = 0;
|
|
306
332
|
let step = 0;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
hookState.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
queueReminder,
|
|
348
|
-
flushReminders: flushGovernorReminders,
|
|
349
|
-
toolEntries,
|
|
350
|
-
disableTools: (reason) => {
|
|
351
|
-
hookState.forceTextOnlyReason = reason;
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
await hookBus.runBeforeModelCall(beforeModelCallCtx);
|
|
355
|
-
toolEntries = beforeModelCallCtx.toolEntries;
|
|
356
|
-
if (this._mode !== "plan") {
|
|
357
|
-
toolEntries = toolEntries.filter((t) => t.name !== "exit_plan_mode");
|
|
358
|
-
}
|
|
359
|
-
flushGovernorReminders();
|
|
360
|
-
const toolDefinitions = ((hookState.forceTextOnlyReason ? [] : toolEntries))
|
|
361
|
-
.map((t) => ({
|
|
362
|
-
name: t.name,
|
|
363
|
-
description: t.description,
|
|
364
|
-
parameters: t.parameters,
|
|
365
|
-
}));
|
|
366
|
-
// LLM-driven compaction runs ahead of projector's algorithmic passes. If
|
|
367
|
-
// it succeeds, this.messages is replaced with [preserved system+meta] +
|
|
368
|
-
// [LLM summary] + [last user msg], and the projector becomes a no-op for
|
|
369
|
-
// budget. If it fails (network error, etc.), the projector's existing
|
|
370
|
-
// algorithmic fallback still kicks in.
|
|
371
|
-
await this.maybeCompactWithLLM();
|
|
372
|
-
try {
|
|
373
|
-
const projectedMessages = projectMessages(this.messages, {
|
|
374
|
-
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,
|
|
375
373
|
providerId: this.providerId,
|
|
376
374
|
modelId: this.apiModel,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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 });
|
|
465
|
+
}
|
|
466
|
+
if (!streamingToolCalls.has(chunk.id)) {
|
|
467
|
+
streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
|
|
421
468
|
}
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
}
|
|
424
489
|
}
|
|
425
|
-
if (chunk.
|
|
426
|
-
|
|
427
|
-
|
|
490
|
+
if (chunk.isEnd && currentToolCall) {
|
|
491
|
+
assistantMsg.toolCalls.push({
|
|
492
|
+
id: currentToolCall.id,
|
|
493
|
+
name: currentToolCall.name,
|
|
494
|
+
arguments: currentToolCall.args,
|
|
495
|
+
...(currentToolCall.argsCorrupt ? { argsCorrupt: true } : {}),
|
|
496
|
+
});
|
|
497
|
+
yield emit({
|
|
498
|
+
type: "tool_call_end",
|
|
428
499
|
id: currentToolCall.id,
|
|
429
500
|
name: currentToolCall.name,
|
|
430
|
-
argumentsDelta: chunk.arguments,
|
|
431
501
|
arguments: currentToolCall.args,
|
|
432
|
-
};
|
|
502
|
+
});
|
|
503
|
+
streamingToolCalls.delete(chunk.id);
|
|
433
504
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
arguments: currentToolCall.args,
|
|
447
|
-
};
|
|
448
|
-
streamingToolCalls.delete(chunk.id);
|
|
449
|
-
}
|
|
450
|
-
break;
|
|
451
|
-
case "usage":
|
|
452
|
-
turnUsage = chunk.usage;
|
|
453
|
-
this.budgetLedger?.recordUsage(chunk.usage, this.budgetSource);
|
|
454
|
-
this.lastInputTokens = chunk.usage.promptTokens;
|
|
455
|
-
this.lastAnchorMessageCount = this.messages.length;
|
|
456
|
-
if (hookState.taskBudget) {
|
|
457
|
-
hookState.taskBudget.spent += chunk.usage.promptTokens + chunk.usage.completionTokens;
|
|
458
|
-
if (hookState.taskBudget.spent >= hookState.taskBudget.total) {
|
|
459
|
-
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
|
+
}
|
|
460
517
|
}
|
|
461
|
-
|
|
462
|
-
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
521
|
+
yield emit(update);
|
|
463
522
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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;
|
|
480
548
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
549
|
+
catch (error) {
|
|
550
|
+
traceEvent("provider_stream_error", {
|
|
551
|
+
error: summarizeTraceError(error),
|
|
552
|
+
}, traceContext);
|
|
553
|
+
if (assistantAppended) {
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
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;
|
|
498
566
|
}
|
|
499
|
-
|
|
500
|
-
|
|
567
|
+
if (consecutiveOverflowRecoveries >= MAX_CONSECUTIVE_OVERFLOW_RECOVERIES) {
|
|
568
|
+
throw error;
|
|
501
569
|
}
|
|
570
|
+
const droppedMessages = await this.recoverFromOverflow(consecutiveOverflowRecoveries);
|
|
571
|
+
consecutiveOverflowRecoveries += 1;
|
|
572
|
+
yield emit({ type: "context_recovered", droppedMessages, reason: "overflow" });
|
|
573
|
+
continue;
|
|
502
574
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
tc
|
|
519
|
-
}
|
|
520
|
-
blockToolCall: (result) => {
|
|
521
|
-
blockedResult = result;
|
|
522
|
-
},
|
|
523
|
-
});
|
|
524
|
-
assistantMsg.toolCalls[index] = {
|
|
525
|
-
id: tc.id,
|
|
526
|
-
name: tc.name,
|
|
527
|
-
arguments: tc.arguments,
|
|
528
|
-
};
|
|
529
|
-
flushGovernorReminders();
|
|
530
|
-
yield { type: "tool_start", id: tc.id, name: tc.name, args: tc.parsedArgs };
|
|
531
|
-
const todosVersionBefore = this._todosVersion;
|
|
532
|
-
const modeVersionBefore = this._modeVersion;
|
|
533
|
-
const updateQueue = createUpdateQueue();
|
|
534
|
-
let result;
|
|
535
|
-
if (blockedResult) {
|
|
536
|
-
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
|
+
}
|
|
537
592
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
let
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
.
|
|
551
|
-
|
|
552
|
-
|
|
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
|
+
},
|
|
553
630
|
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
+
}
|
|
557
682
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (!settled) {
|
|
561
|
-
await updateQueue.wait();
|
|
683
|
+
if (cancelledByAbort) {
|
|
684
|
+
result = cancelledToolResult(tc.name);
|
|
562
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);
|
|
563
744
|
}
|
|
564
|
-
if (rejected)
|
|
565
|
-
throw rejected;
|
|
566
|
-
result = resolved ?? { content: `Error: Tool "${tc.name}" returned no result`, isError: true };
|
|
567
745
|
}
|
|
568
|
-
|
|
569
|
-
await hookBus.runAfterToolCall({
|
|
746
|
+
await hookBus.runBeforeContinuation({
|
|
570
747
|
agent: this,
|
|
571
748
|
cwd,
|
|
572
749
|
input: userInput,
|
|
573
750
|
state: hookState,
|
|
574
751
|
queueReminder,
|
|
575
752
|
flushReminders: flushGovernorReminders,
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
753
|
+
toolCalls: parsedCalls,
|
|
754
|
+
toolResults: executedResults,
|
|
755
|
+
requestTextOnlyTurn: (reason) => {
|
|
756
|
+
hookState.forceTextOnlyReason = reason;
|
|
580
757
|
},
|
|
581
758
|
});
|
|
582
|
-
// Honor the model's server-declared per-tool-output token cap (e.g.
|
|
583
|
-
// gpt-5.5 reports 10000). Without this, 4-5 large file reads in a row
|
|
584
|
-
// blow past the input window even though our local estimate looks fine.
|
|
585
|
-
const truncatedOutput = truncateToolOutputForModel(result.content, this.providerId, this.apiModel);
|
|
586
|
-
this.appendMessage({
|
|
587
|
-
role: "tool",
|
|
588
|
-
toolCallId: tc.id,
|
|
589
|
-
content: truncatedOutput.content,
|
|
590
|
-
metadata: result.metadata,
|
|
591
|
-
isError: result.isError,
|
|
592
|
-
});
|
|
593
|
-
this.compactResidentHistory();
|
|
594
759
|
flushGovernorReminders();
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
yield { type: "todos_updated", todos: this.getTodos() };
|
|
602
|
-
}
|
|
603
|
-
if (this._modeVersion !== modeVersionBefore) {
|
|
604
|
-
yield { type: "mode_changed", mode: this._mode };
|
|
605
|
-
}
|
|
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;
|
|
606
766
|
}
|
|
607
|
-
await hookBus.
|
|
767
|
+
await hookBus.runAfterTurn({
|
|
608
768
|
agent: this,
|
|
609
769
|
cwd,
|
|
610
770
|
input: userInput,
|
|
611
771
|
state: hookState,
|
|
612
772
|
queueReminder,
|
|
613
773
|
flushReminders: flushGovernorReminders,
|
|
614
|
-
toolCalls: parsedCalls,
|
|
615
|
-
toolResults: executedResults,
|
|
616
|
-
requestTextOnlyTurn: (reason) => {
|
|
617
|
-
hookState.forceTextOnlyReason = reason;
|
|
618
|
-
},
|
|
619
774
|
});
|
|
620
775
|
flushGovernorReminders();
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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;
|
|
627
785
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
+
}
|
|
642
803
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
await stopOwnedAutoServers();
|
|
808
|
+
traceEvent("agent_run_end", {
|
|
809
|
+
messageCount: this.messages.length,
|
|
810
|
+
}, traceContext);
|
|
646
811
|
}
|
|
647
|
-
for (const update of this.drainSubagentToolUpdates())
|
|
648
|
-
yield update;
|
|
649
|
-
yield { type: "agent_end" };
|
|
650
812
|
}
|
|
651
813
|
async recoverFromOverflow(attempt) {
|
|
652
814
|
const before = this.messages.length;
|
|
@@ -1274,8 +1436,43 @@ export class Agent {
|
|
|
1274
1436
|
}
|
|
1275
1437
|
appendMessage(message) {
|
|
1276
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
|
+
});
|
|
1277
1447
|
this.onMessageAppend?.(message);
|
|
1278
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
|
+
}
|
|
1279
1476
|
async executeTool(toolCall, cwd, abortSignal, emitUpdate) {
|
|
1280
1477
|
throwIfAborted(abortSignal);
|
|
1281
1478
|
if (toolCall.name === "exit_plan_mode" && this._mode !== "plan") {
|
|
@@ -1400,9 +1597,61 @@ function throwIfAborted(signal) {
|
|
|
1400
1597
|
throw reason;
|
|
1401
1598
|
throw new AgentAbortError(typeof reason === "string" ? reason : undefined);
|
|
1402
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
|
+
}
|
|
1403
1651
|
function createUpdateQueue() {
|
|
1404
1652
|
const items = [];
|
|
1405
1653
|
let waiter;
|
|
1654
|
+
let abortCleanup;
|
|
1406
1655
|
return {
|
|
1407
1656
|
push(item) {
|
|
1408
1657
|
items.push(item);
|
|
@@ -1414,17 +1663,36 @@ function createUpdateQueue() {
|
|
|
1414
1663
|
hasItems() {
|
|
1415
1664
|
return items.length > 0;
|
|
1416
1665
|
},
|
|
1417
|
-
wait() {
|
|
1666
|
+
wait(signal) {
|
|
1418
1667
|
if (items.length > 0)
|
|
1419
|
-
return Promise.resolve();
|
|
1668
|
+
return Promise.resolve("woken");
|
|
1669
|
+
if (signal?.aborted)
|
|
1670
|
+
return Promise.resolve("aborted");
|
|
1420
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
|
+
}
|
|
1421
1687
|
waiter = resolve;
|
|
1422
1688
|
});
|
|
1423
1689
|
},
|
|
1424
1690
|
wake() {
|
|
1425
1691
|
const resolve = waiter;
|
|
1426
1692
|
waiter = undefined;
|
|
1427
|
-
|
|
1693
|
+
abortCleanup?.();
|
|
1694
|
+
abortCleanup = undefined;
|
|
1695
|
+
resolve?.("woken");
|
|
1428
1696
|
},
|
|
1429
1697
|
};
|
|
1430
1698
|
}
|
|
@@ -1470,21 +1738,26 @@ function sanitizeSubagentSummary(value) {
|
|
|
1470
1738
|
return stripProviderProtocolArtifacts(value).trim();
|
|
1471
1739
|
}
|
|
1472
1740
|
function needsExplicitFinalSummary(record, executedAnyTool) {
|
|
1473
|
-
// If the subagent actually invoked any tool, always solicit an explicit final
|
|
1474
|
-
// summary. We cannot tell from the stream alone whether a tool-free trailing
|
|
1475
|
-
// turn was the real answer or mid-thought narration ("Let me try X next:").
|
|
1476
|
-
// Asking the model to restate its findings is cheap and yields predictable,
|
|
1477
|
-
// clean output. (Profile-validation notes in `toolNotes` do not count as
|
|
1478
|
-
// actual tool executions.)
|
|
1479
|
-
if (executedAnyTool)
|
|
1480
|
-
return true;
|
|
1481
1741
|
if (!record.summary)
|
|
1482
|
-
return
|
|
1742
|
+
return executedAnyTool;
|
|
1483
1743
|
if (isOnlyProviderProtocolArtifacts(record.summary))
|
|
1484
1744
|
return true;
|
|
1485
1745
|
if (/<\/?[||][^<>]*>/.test(record.summary))
|
|
1486
1746
|
return true;
|
|
1487
|
-
|
|
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);
|
|
1488
1761
|
}
|
|
1489
1762
|
function summarizeSubagentToolEnd(event) {
|
|
1490
1763
|
const metadata = (event.result.metadata ?? {});
|