agent-accelerator 0.5.9 → 0.6.5
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 +64 -16
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +447 -172
- package/src/agent/architecture.md +16 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +9 -3
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +86 -28
- package/src/agent/loop.ts +474 -374
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +50 -19
- package/src/agent/turns.ts +3 -1
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +73 -30
- package/src/agent/workers.ts +126 -21
- package/src/architecture.md +17 -15
- package/src/index.ts +43 -19
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/prompts/additional-context.md +2 -0
- package/src/prompts/architecture.md +19 -0
- package/src/prompts/prompts.ts +81 -0
- package/src/prompts/reasoning-directive.md +5 -0
- package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
- package/src/prompts/tool-bridge.md +1 -0
- package/src/prompts/tool-interrupted.md +1 -0
- package/src/prompts/tool-not-found.md +1 -0
- package/src/prompts/tool-repeat-blocked.md +1 -0
- package/src/prompts/tool-schema-error.md +1 -0
- package/src/prompts/tool-truncated-arguments.md +1 -0
- package/src/prompts/worker-failed.md +1 -0
- package/src/prompts/worker-replied.md +2 -0
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +228 -69
- package/src/providers/openai-compat.ts +74 -56
- package/src/providers/openai.ts +64 -41
- package/src/providers/openrouter.ts +270 -48
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +115 -124
- package/src/scheduler/architecture.md +8 -7
- package/src/scheduler/scheduler.ts +226 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +38 -16
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +39 -12
- package/src/session/store.ts +126 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +9 -7
- package/src/tools/executor.ts +109 -62
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool-content.ts +32 -0
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +37 -22
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +5 -9
- package/src/types/errors.ts +26 -4
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -5
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +14 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +9 -17
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +15 -3
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
package/src/agent/loop.ts
CHANGED
|
@@ -3,7 +3,13 @@ import type { ToolDefinition, ToolCallRecord, ToolResultRecord } from "../types/
|
|
|
3
3
|
import type { TokenUsage } from "../types/core.ts";
|
|
4
4
|
import type { AgentRunOptions } from "../types/agent.ts";
|
|
5
5
|
import type { ContentPart } from "../types/message.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AgentResponse,
|
|
8
|
+
normalizeFinishReason,
|
|
9
|
+
type FinishReason,
|
|
10
|
+
type SubAgentExecutionMetadata,
|
|
11
|
+
type StreamEvent,
|
|
12
|
+
} from "../types/response.ts";
|
|
7
13
|
import { priceUsage, reportUsageAnomaly } from "../session/store.ts";
|
|
8
14
|
import { telemetryBus } from "../telemetry.ts";
|
|
9
15
|
export { telemetryBus };
|
|
@@ -26,6 +32,7 @@ import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-r
|
|
|
26
32
|
import { BudgetExceededError, TimeoutError } from "../types/errors.ts";
|
|
27
33
|
import { applyCaptureRaw, type CaptureRawMode } from "../utils/raw-capture.ts";
|
|
28
34
|
import { previewPrompt } from "../utils/session.ts";
|
|
35
|
+
import { DEFAULT_PROMPTS, fillPrompt, type Prompts } from "../prompts/prompts.ts";
|
|
29
36
|
import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
|
|
30
37
|
|
|
31
38
|
/** One mid-session steer request buffered while a run is active. */
|
|
@@ -92,6 +99,8 @@ export interface AgentLoopConfig {
|
|
|
92
99
|
steerInbox?: SteerEntry[];
|
|
93
100
|
/** Called after steer entries are injected (streaming loops also push `steer_injected` events). */
|
|
94
101
|
onSteerInjected?: (injectedPrompts: string[]) => void;
|
|
102
|
+
/** The SDK prompts for this run (`AgentConfig.prompts` resolved); the shipped ones when unset. */
|
|
103
|
+
prompts?: Prompts;
|
|
95
104
|
/**
|
|
96
105
|
* Canonical structured-output request (raw `AgentConfig.output`).
|
|
97
106
|
* Normalized per turn for the provider wire; validated once at the end
|
|
@@ -105,7 +114,7 @@ export interface AgentLoopConfig {
|
|
|
105
114
|
* Returns display previews for events/telemetry. Never throws: invalid
|
|
106
115
|
* entries are skipped so one bad steer cannot fail the active turn.
|
|
107
116
|
*/
|
|
108
|
-
function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[] {
|
|
117
|
+
function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[], prompts: Prompts = DEFAULT_PROMPTS): string[] {
|
|
109
118
|
if (!inbox || inbox.length === 0) return [];
|
|
110
119
|
const injected: string[] = [];
|
|
111
120
|
while (inbox.length > 0) {
|
|
@@ -115,7 +124,7 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
|
|
|
115
124
|
const opts = entry.options;
|
|
116
125
|
if (Array.isArray(prompt) && prompt.length === 0) continue;
|
|
117
126
|
if (opts?.additionalContext) {
|
|
118
|
-
const prefix =
|
|
127
|
+
const prefix = `${fillPrompt(prompts.additionalContext, { context: opts.additionalContext })}\n\n`;
|
|
119
128
|
if (typeof prompt === "string") prompt = prefix + prompt;
|
|
120
129
|
else prompt = [{ type: "text", text: prefix } as ContentPart, ...prompt];
|
|
121
130
|
}
|
|
@@ -131,7 +140,13 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
|
|
|
131
140
|
// Reports one finished turn to the caller's onTurn hook (ids only), never throwing; in: both loops, out: AgentRunOptions.onTurn.
|
|
132
141
|
function emitTurn(
|
|
133
142
|
runOptions: AgentRunOptions | undefined,
|
|
134
|
-
turn: {
|
|
143
|
+
turn: {
|
|
144
|
+
turns: number;
|
|
145
|
+
text?: string;
|
|
146
|
+
thinking?: string;
|
|
147
|
+
toolCalls?: ToolCallRecord[];
|
|
148
|
+
toolResults?: ToolResultRecord[];
|
|
149
|
+
}
|
|
135
150
|
): void {
|
|
136
151
|
try {
|
|
137
152
|
runOptions?.onTurn?.({
|
|
@@ -145,7 +160,7 @@ function emitTurn(
|
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
/**
|
|
148
|
-
* Per-turn `toolChoice`. `"none"`
|
|
163
|
+
* Per-turn `toolChoice`. `"none"` and `"validated"` (which forces nothing) hold for the whole run; a forced choice
|
|
149
164
|
* (`"required"` or a pinned function) applies to the FIRST model turn only:
|
|
150
165
|
* forcing a tool call on every turn would keep the model calling tools until
|
|
151
166
|
* `maxTurns`, so the run could never produce a final answer.
|
|
@@ -155,7 +170,7 @@ function toolChoiceForTurn(
|
|
|
155
170
|
turn: number
|
|
156
171
|
): ProviderRequestOptions["toolChoice"] {
|
|
157
172
|
if (choice === undefined || choice === "auto") return undefined;
|
|
158
|
-
if (choice === "none") return
|
|
173
|
+
if (choice === "none" || choice === "validated") return choice;
|
|
159
174
|
return turn === 1 ? choice : undefined;
|
|
160
175
|
}
|
|
161
176
|
|
|
@@ -173,7 +188,9 @@ function shouldRollbackOnFailure(
|
|
|
173
188
|
|
|
174
189
|
// Calls the persistence checkpoint hook, never throwing; in: both loops after each step, out: Agent.persistNow.
|
|
175
190
|
function emitProgress(config: { onProgress?: () => void }): void {
|
|
176
|
-
try {
|
|
191
|
+
try {
|
|
192
|
+
config.onProgress?.();
|
|
193
|
+
} catch {}
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
// ---------------------------------------------------------------------------
|
|
@@ -181,9 +198,13 @@ function emitProgress(config: { onProgress?: () => void }): void {
|
|
|
181
198
|
// ---------------------------------------------------------------------------
|
|
182
199
|
|
|
183
200
|
// Normalizes the structured-output option once per run (raw value + provider spec); in: AgentLoopConfig.output, out: the final-answer validation.
|
|
184
|
-
function resolveLoopOutput(config: AgentLoopConfig): {
|
|
201
|
+
function resolveLoopOutput(config: AgentLoopConfig): {
|
|
202
|
+
raw: unknown;
|
|
203
|
+
spec: import("../types/model.ts").StructuredOutputSpec | undefined;
|
|
204
|
+
} {
|
|
185
205
|
const raw = (config as { output?: unknown }).output;
|
|
186
|
-
const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)
|
|
206
|
+
const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)
|
|
207
|
+
?.output;
|
|
187
208
|
if (raw !== undefined && raw !== null) {
|
|
188
209
|
// Throws ValidationError for invalid shapes — caught by run/stream catch
|
|
189
210
|
// with partial attached, same as other config validation.
|
|
@@ -195,7 +216,11 @@ function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: impor
|
|
|
195
216
|
}
|
|
196
217
|
|
|
197
218
|
// Validates the final text against the structured-output option, throwing ValidationError on mismatch; in: the last model text, out: AgentResponse.parsed.
|
|
198
|
-
function parseFinalStructuredOutput(
|
|
219
|
+
function parseFinalStructuredOutput(
|
|
220
|
+
text: string,
|
|
221
|
+
raw: unknown,
|
|
222
|
+
spec: import("../types/model.ts").StructuredOutputSpec | undefined
|
|
223
|
+
): unknown | undefined {
|
|
199
224
|
if (raw === undefined && spec === undefined) return undefined;
|
|
200
225
|
// Strict-fail when opted in: invalid JSON/schema throws ValidationError.
|
|
201
226
|
// Tool turns skip this (called only for final no-tool text).
|
|
@@ -253,7 +278,7 @@ function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendin
|
|
|
253
278
|
* - drops results whose id the model never issued.
|
|
254
279
|
* Open-weight models that reuse one id for several calls keep one result per call.
|
|
255
280
|
*/
|
|
256
|
-
export function ensureToolPairing(context: AgentContext): void {
|
|
281
|
+
export function ensureToolPairing(context: AgentContext, prompts: Prompts = DEFAULT_PROMPTS): void {
|
|
257
282
|
try {
|
|
258
283
|
type ToolResultPart = Extract<ContentPart, { type: "tool_result" }>;
|
|
259
284
|
const messages = context.messages;
|
|
@@ -280,7 +305,9 @@ export function ensureToolPairing(context: AgentContext): void {
|
|
|
280
305
|
const hit = results.get(p.id)?.shift();
|
|
281
306
|
if (hit) {
|
|
282
307
|
// Reuse the original message object when it carried exactly this result.
|
|
283
|
-
rebuilt.push(
|
|
308
|
+
rebuilt.push(
|
|
309
|
+
hit.message.content.length === 1 ? hit.message : { role: "tool", name: hit.part.name, content: [hit.part] }
|
|
310
|
+
);
|
|
284
311
|
} else {
|
|
285
312
|
const name = typeof p.name === "string" ? p.name : "tool";
|
|
286
313
|
traceToolEvent("call_interrupted", "warn", { id: p.id, name }, {});
|
|
@@ -292,7 +319,7 @@ export function ensureToolPairing(context: AgentContext): void {
|
|
|
292
319
|
type: "tool_result",
|
|
293
320
|
id: p.id,
|
|
294
321
|
name,
|
|
295
|
-
result:
|
|
322
|
+
result: fillPrompt(prompts.toolInterrupted, { tool: name, id: p.id }),
|
|
296
323
|
isError: true,
|
|
297
324
|
},
|
|
298
325
|
],
|
|
@@ -320,10 +347,12 @@ export interface LoopLimits {
|
|
|
320
347
|
}
|
|
321
348
|
|
|
322
349
|
/** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
|
|
323
|
-
function resolveLoopLimits(
|
|
350
|
+
function resolveLoopLimits(
|
|
351
|
+
config: AgentLoopConfig,
|
|
352
|
+
fallbackMaxTurns: number
|
|
353
|
+
): LoopLimits & { maxTurns: number; deadlineMs: number; maxToolCallsPerRun: number; maxTotalTokens: number } {
|
|
324
354
|
const raw = ((config as unknown as { limits?: Record<string, unknown> }).limits ?? {}) as Record<string, unknown>;
|
|
325
|
-
const num = (v: unknown): number | undefined =>
|
|
326
|
-
typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
355
|
+
const num = (v: unknown): number | undefined => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
|
327
356
|
let maxTurns = fallbackMaxTurns;
|
|
328
357
|
const limMax = num(raw.maxTurns);
|
|
329
358
|
if (limMax !== undefined) {
|
|
@@ -338,7 +367,15 @@ function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): L
|
|
|
338
367
|
const requestTimeoutMs = timeout(raw.requestTimeoutMs) ?? 300_000;
|
|
339
368
|
const firstByteTimeoutMs = timeout(raw.firstByteTimeoutMs) ?? 120_000;
|
|
340
369
|
const streamIdleTimeoutMs = timeout(raw.streamIdleTimeoutMs) ?? 300_000;
|
|
341
|
-
return {
|
|
370
|
+
return {
|
|
371
|
+
maxTurns,
|
|
372
|
+
deadlineMs,
|
|
373
|
+
maxToolCallsPerRun,
|
|
374
|
+
maxTotalTokens,
|
|
375
|
+
requestTimeoutMs,
|
|
376
|
+
firstByteTimeoutMs,
|
|
377
|
+
streamIdleTimeoutMs,
|
|
378
|
+
};
|
|
342
379
|
}
|
|
343
380
|
|
|
344
381
|
// The session id a run's provider calls use (run override, else agent default); in: AgentLoopConfig, out: rollback and routing.
|
|
@@ -348,21 +385,24 @@ function loopSessionId(config: AgentLoopConfig): string | undefined {
|
|
|
348
385
|
(config.runOptions as { sessionId?: string } | undefined)?.sessionId ||
|
|
349
386
|
(config.options as { sessionId?: string } | undefined)?.sessionId
|
|
350
387
|
);
|
|
351
|
-
} catch {
|
|
388
|
+
} catch {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
352
391
|
}
|
|
353
392
|
|
|
354
393
|
// Builds an AbortError that carries the run's partial progress; in: an abort reason + partial usage, out: the error the run rejects with.
|
|
355
|
-
function makeAbortError(
|
|
356
|
-
partial: { usage: TokenUsage; turns: number; text?: string },
|
|
357
|
-
reason?: unknown
|
|
358
|
-
): Error {
|
|
394
|
+
function makeAbortError(partial: { usage: TokenUsage; turns: number; text?: string }, reason?: unknown): Error {
|
|
359
395
|
let base: Error;
|
|
360
396
|
if (reason instanceof Error && reason.name === "AbortError") base = reason;
|
|
361
397
|
else if (reason instanceof Error) base = new Error(reason.message || "Aborted");
|
|
362
398
|
else if (typeof reason === "string" && reason) base = new Error(reason);
|
|
363
399
|
else base = new Error("Aborted");
|
|
364
|
-
try {
|
|
365
|
-
|
|
400
|
+
try {
|
|
401
|
+
(base as { name: string }).name = "AbortError";
|
|
402
|
+
} catch {}
|
|
403
|
+
try {
|
|
404
|
+
(base as unknown as { partial: unknown }).partial = partial;
|
|
405
|
+
} catch {}
|
|
366
406
|
return base;
|
|
367
407
|
}
|
|
368
408
|
|
|
@@ -402,7 +442,9 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
|
|
|
402
442
|
timer = setTimeout(() => {
|
|
403
443
|
reject(new TimeoutError(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`));
|
|
404
444
|
}, timeoutMs);
|
|
405
|
-
try {
|
|
445
|
+
try {
|
|
446
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
447
|
+
} catch {}
|
|
406
448
|
});
|
|
407
449
|
return await Promise.race([task(), timeoutP]);
|
|
408
450
|
} finally {
|
|
@@ -433,6 +475,14 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
|
|
|
433
475
|
}
|
|
434
476
|
}
|
|
435
477
|
|
|
478
|
+
// Adds the media token details of `source` into `target` (kept only when something was reported); in: accumulateUsage, out: target.inputDetails / outputDetails.
|
|
479
|
+
function sumDetails(target: TokenUsage, source: TokenUsage, key: "inputDetails" | "outputDetails"): void {
|
|
480
|
+
const from = source[key] as Record<string, number | undefined> | undefined;
|
|
481
|
+
if (!from) return;
|
|
482
|
+
const into = ((target[key] as Record<string, number> | undefined) ??= {});
|
|
483
|
+
for (const [k, v] of Object.entries(from)) if (typeof v === "number") into[k] = (into[k] ?? 0) + v;
|
|
484
|
+
}
|
|
485
|
+
|
|
436
486
|
// ---------------------------------------------------------------------------
|
|
437
487
|
// Canonical usage accumulation (pure w.r.t. source).
|
|
438
488
|
// Invariants (see assertUsageInvariants in session/store.ts):
|
|
@@ -445,6 +495,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
|
|
|
445
495
|
// This function never mutates `source`: the priced cost is cloned
|
|
446
496
|
// into a local before folding into the target.
|
|
447
497
|
// ---------------------------------------------------------------------------
|
|
498
|
+
// Adds one turn's usage (and its priced cost) into a running total without touching the source; in: both loops per model turn and finished worker, out: the run's accumulatedUsage.
|
|
448
499
|
export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
|
|
449
500
|
target.inputTokens += source.inputTokens || 0;
|
|
450
501
|
target.outputTokens += source.outputTokens || 0;
|
|
@@ -453,6 +504,8 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
453
504
|
target.cacheReadTokens = (target.cacheReadTokens ?? 0) + (source.cacheReadTokens ?? 0);
|
|
454
505
|
target.cacheWriteTokens = (target.cacheWriteTokens ?? 0) + (source.cacheWriteTokens ?? 0);
|
|
455
506
|
target.thinkingTokens = (target.thinkingTokens ?? 0) + (source.thinkingTokens ?? 0);
|
|
507
|
+
sumDetails(target, source, "inputDetails");
|
|
508
|
+
sumDetails(target, source, "outputDetails");
|
|
456
509
|
|
|
457
510
|
// Canonical invariant (provider-agnostic): cache hits are a subset of
|
|
458
511
|
// input. Some providers occasionally report cached > input on long chained
|
|
@@ -518,10 +571,17 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
518
571
|
openToolLog?: ExecuteToolsOptions["openToolLog"];
|
|
519
572
|
previousFingerprints: Set<string>;
|
|
520
573
|
repeatCounts: Map<string, number>;
|
|
574
|
+
prompts?: Prompts;
|
|
521
575
|
agentName?: string;
|
|
522
576
|
signal?: AbortSignal;
|
|
523
577
|
sessionId?: string;
|
|
524
|
-
onSubagentEvent?: (event: {
|
|
578
|
+
onSubagentEvent?: (event: {
|
|
579
|
+
trackingId: string;
|
|
580
|
+
delta?: string;
|
|
581
|
+
thinkingDelta?: string;
|
|
582
|
+
partialText?: string;
|
|
583
|
+
partialThinking?: string;
|
|
584
|
+
}) => void;
|
|
525
585
|
}): Promise<{ results: ToolResultRecord[]; fingerprints: Set<string> }> {
|
|
526
586
|
const { toolCalls, previousFingerprints, repeatCounts } = options;
|
|
527
587
|
const currentFingerprints = new Set<string>();
|
|
@@ -538,8 +598,14 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
538
598
|
}
|
|
539
599
|
if (!previousFingerprints.has(fingerprint)) repeatCounts.delete(fingerprint); // streak broken
|
|
540
600
|
if (shouldBlockRepeat(previousFingerprints, call, repeatCounts)) {
|
|
541
|
-
traceToolEvent(
|
|
542
|
-
|
|
601
|
+
traceToolEvent(
|
|
602
|
+
"call_blocked",
|
|
603
|
+
"warn",
|
|
604
|
+
call,
|
|
605
|
+
{ agentName: options.agentName, sessionId: options.sessionId },
|
|
606
|
+
{ reason: "repeat_blocked" }
|
|
607
|
+
);
|
|
608
|
+
blocked.set(call.id, makeRepeatBlockedResult(call, options.prompts?.toolRepeatBlocked));
|
|
543
609
|
} else {
|
|
544
610
|
executable.push(call);
|
|
545
611
|
}
|
|
@@ -547,19 +613,21 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
547
613
|
// Streaks only survive while the fingerprint keeps appearing every turn.
|
|
548
614
|
for (const fp of [...repeatCounts.keys()]) if (!currentFingerprints.has(fp)) repeatCounts.delete(fp);
|
|
549
615
|
|
|
550
|
-
const executed =
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
616
|
+
const executed =
|
|
617
|
+
executable.length > 0
|
|
618
|
+
? await executeToolCalls({
|
|
619
|
+
tools: options.tools,
|
|
620
|
+
toolCalls: executable,
|
|
621
|
+
agentName: options.agentName,
|
|
622
|
+
parallel: true,
|
|
623
|
+
signal: options.signal,
|
|
624
|
+
sessionId: options.sessionId,
|
|
625
|
+
onSubagentEvent: options.onSubagentEvent,
|
|
626
|
+
onResult: options.onResult,
|
|
627
|
+
openToolLog: options.openToolLog,
|
|
628
|
+
prompts: options.prompts,
|
|
629
|
+
})
|
|
630
|
+
: [];
|
|
563
631
|
const byId = new Map<string, ToolResultRecord[]>();
|
|
564
632
|
for (const result of executed) {
|
|
565
633
|
const queue = byId.get(result.id) ?? [];
|
|
@@ -577,9 +645,7 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
577
645
|
// id for several distinct calls in a turn, and a plain id->result map
|
|
578
646
|
// would hand every call the last result. Queues keep each result aligned
|
|
579
647
|
// with its own call.
|
|
580
|
-
results: toolCalls
|
|
581
|
-
.map((call) => byId.get(call.id)?.shift())
|
|
582
|
-
.filter((r): r is ToolResultRecord => Boolean(r)),
|
|
648
|
+
results: toolCalls.map((call) => byId.get(call.id)?.shift()).filter((r): r is ToolResultRecord => Boolean(r)),
|
|
583
649
|
fingerprints: currentFingerprints,
|
|
584
650
|
};
|
|
585
651
|
}
|
|
@@ -603,7 +669,13 @@ export function unfinishedToolCalls(messages: AgentContext["messages"]): ToolCal
|
|
|
603
669
|
const open: ToolCallRecord[] = [];
|
|
604
670
|
for (const p of calls) {
|
|
605
671
|
if (p.type === "tool_call" && !answered.has(p.id)) {
|
|
606
|
-
open.push({
|
|
672
|
+
open.push({
|
|
673
|
+
id: p.id,
|
|
674
|
+
name: p.name,
|
|
675
|
+
arguments: p.arguments,
|
|
676
|
+
...(p.rawArguments ? { rawArguments: p.rawArguments } : {}),
|
|
677
|
+
...(p.callId ? { callId: p.callId } : {}),
|
|
678
|
+
});
|
|
607
679
|
}
|
|
608
680
|
}
|
|
609
681
|
return open;
|
|
@@ -637,6 +709,7 @@ async function finishInterruptedTools(
|
|
|
637
709
|
sessionId: config.runOptions?.sessionId || config.options?.sessionId,
|
|
638
710
|
onResult: config.onToolResult,
|
|
639
711
|
openToolLog: config.openToolLog,
|
|
712
|
+
prompts: config.prompts,
|
|
640
713
|
})
|
|
641
714
|
: [];
|
|
642
715
|
const ran = new Map(executed.map((r) => [r.id, r]));
|
|
@@ -698,295 +771,290 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
698
771
|
const repeatCounts = new Map<string, number>();
|
|
699
772
|
|
|
700
773
|
try {
|
|
701
|
-
|
|
774
|
+
const standardTools = toStandardToolDeclarations(tools);
|
|
702
775
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
776
|
+
if (config.resume) {
|
|
777
|
+
const finished = await finishInterruptedTools(config);
|
|
778
|
+
stoppedByTool = finished.results.some((r) => r.stopped) || endsWithStop(context.messages);
|
|
779
|
+
if (finished.calls.length > 0) {
|
|
780
|
+
toolsExecuted = true;
|
|
781
|
+
allToolCalls.push(...finished.calls);
|
|
782
|
+
allToolResults.push(...finished.results);
|
|
783
|
+
for (const meta of finished.subagents) {
|
|
784
|
+
allSubagents.push(meta);
|
|
785
|
+
accumulateUsage(accumulatedUsage, meta.usage, getModelFromCatalog(meta.provider, meta.model));
|
|
786
|
+
}
|
|
713
787
|
}
|
|
714
788
|
}
|
|
715
|
-
}
|
|
716
789
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
// google:{store:false} opt-out pass-through (provider
|
|
745
|
-
// already respects body.store===false for chaining).
|
|
746
|
-
try {
|
|
747
|
-
const gStore =
|
|
748
|
-
(runOptions as unknown as { googleStore?: unknown } | undefined)?.googleStore ??
|
|
749
|
-
(options as unknown as { googleStore?: unknown } | undefined)?.googleStore ??
|
|
750
|
-
(config as unknown as { googleStore?: unknown }).googleStore;
|
|
751
|
-
if (gStore === false) (providerOptions as unknown as { store?: boolean }).store = false;
|
|
752
|
-
} catch {}
|
|
753
|
-
// Google interaction storage tier for the chain-TTL gate
|
|
754
|
-
// (`free`: 23h local gate; `paid`: 54d). Defaults to `free` in adapter.
|
|
755
|
-
try {
|
|
756
|
-
const gTier =
|
|
757
|
-
(runOptions as unknown as { googleTier?: unknown } | undefined)?.googleTier ??
|
|
758
|
-
(options as unknown as { googleTier?: unknown } | undefined)?.googleTier ??
|
|
759
|
-
(config as unknown as { googleTier?: unknown }).googleTier;
|
|
760
|
-
if (gTier === "paid" || gTier === "free") providerOptions.googleTier = gTier;
|
|
761
|
-
} catch {}
|
|
762
|
-
|
|
763
|
-
// A long-running call (e.g. Google long decode) times each HTTP request itself: no whole-call race.
|
|
764
|
-
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
765
|
-
const genResult = await generateWithTimeout(
|
|
766
|
-
() => provider.generate(
|
|
767
|
-
modelId,
|
|
768
|
-
{
|
|
769
|
-
systemPrompt: context.systemPrompt,
|
|
770
|
-
messages: context.messages,
|
|
771
|
-
},
|
|
772
|
-
providerOptions
|
|
773
|
-
),
|
|
774
|
-
longRunning ? Infinity : limits.requestTimeoutMs
|
|
775
|
-
);
|
|
776
|
-
|
|
777
|
-
finalResult = genResult;
|
|
790
|
+
while (!stoppedByTool && turns < maxTurns) {
|
|
791
|
+
// Fail fast on abort (throws AbortError with partial).
|
|
792
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
793
|
+
checkDeadline(startTime, limits.deadlineMs);
|
|
794
|
+
config.enforceBudget?.();
|
|
795
|
+
// Repair orphan tool pairs before every request.
|
|
796
|
+
ensureToolPairing(context, config.prompts);
|
|
797
|
+
if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
|
|
798
|
+
turns++;
|
|
799
|
+
|
|
800
|
+
const spec = getModelFromCatalog(provider.id, modelId);
|
|
801
|
+
|
|
802
|
+
const providerOptions: ProviderRequestOptions = {
|
|
803
|
+
...options,
|
|
804
|
+
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
805
|
+
cache: options?.cache,
|
|
806
|
+
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
807
|
+
signal: runOptions?.signal,
|
|
808
|
+
toolChoice: toolChoiceForTurn(options?.toolChoice, turns),
|
|
809
|
+
...(loopOutput.spec ? { output: loopOutput.spec } : {}),
|
|
810
|
+
};
|
|
811
|
+
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
812
|
+
try {
|
|
813
|
+
if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
|
|
814
|
+
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
|
|
815
|
+
}
|
|
816
|
+
} catch {}
|
|
778
817
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
818
|
+
// A long-running call (e.g. Google long decode) times each HTTP request itself: no whole-call race.
|
|
819
|
+
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
820
|
+
const genResult = await generateWithTimeout(
|
|
821
|
+
() =>
|
|
822
|
+
provider.generate(
|
|
823
|
+
modelId,
|
|
824
|
+
{
|
|
825
|
+
systemPrompt: context.systemPrompt,
|
|
826
|
+
messages: context.messages,
|
|
827
|
+
},
|
|
828
|
+
providerOptions
|
|
829
|
+
),
|
|
830
|
+
longRunning ? Infinity : limits.requestTimeoutMs
|
|
831
|
+
);
|
|
784
832
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
833
|
+
finalResult = genResult;
|
|
834
|
+
|
|
835
|
+
// Canonical session routing: lets adapters detect provider switches.
|
|
836
|
+
noteProviderTurn(runOptions?.sessionId || options?.sessionId, provider.id);
|
|
837
|
+
|
|
838
|
+
// Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (default
|
|
839
|
+
// OFF): without the opt-in the run returns the provider's empty text and finishReason.
|
|
840
|
+
if (
|
|
841
|
+
(!genResult.text || genResult.text.trim() === "") &&
|
|
842
|
+
(!genResult.toolCalls || genResult.toolCalls.length === 0) &&
|
|
843
|
+
genResult.thinking
|
|
844
|
+
) {
|
|
845
|
+
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
846
|
+
if (genResult.thinking.includes("</think>")) {
|
|
847
|
+
const parts = genResult.thinking.split(/<\/(?:think|thought)>/i);
|
|
848
|
+
genResult.thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
|
|
849
|
+
genResult.text = parts.slice(1).join("").trim();
|
|
850
|
+
} else {
|
|
851
|
+
genResult.text = genResult.thinking;
|
|
852
|
+
genResult.thinking = undefined;
|
|
853
|
+
}
|
|
796
854
|
}
|
|
797
855
|
}
|
|
798
|
-
}
|
|
799
856
|
|
|
800
|
-
|
|
801
|
-
try { config.onTurnUsage?.(genResult.usage, spec); } catch {}
|
|
802
|
-
config.enforceBudget?.({ ...genResult.usage, cost: computeCostFromPricing(genResult.usage, spec, options?.serviceTier) });
|
|
803
|
-
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
804
|
-
|
|
805
|
-
// Record assistant turn in context
|
|
806
|
-
context.addAssistantMessage(
|
|
807
|
-
genResult.text,
|
|
808
|
-
genResult.toolCalls,
|
|
809
|
-
genResult.thinking,
|
|
810
|
-
genResult.thoughtSignature,
|
|
811
|
-
genResult.providerSteps
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
// Thinking-trace telemetry (observability-only, full text, no limit):
|
|
815
|
-
// one bus event per thinking turn carrying the text + thought signature.
|
|
816
|
-
// Never touches provider payloads, history, or persistence formats.
|
|
817
|
-
if (genResult.thinking) {
|
|
857
|
+
accumulateUsage(accumulatedUsage, genResult.usage, spec, options?.serviceTier);
|
|
818
858
|
try {
|
|
819
|
-
|
|
820
|
-
code: "turn.thinking",
|
|
821
|
-
message: `[Agent Accelerator] turn ${turns} thinking (${provider.id}/${modelId})`,
|
|
822
|
-
data: {
|
|
823
|
-
turns,
|
|
824
|
-
model: modelId,
|
|
825
|
-
provider: provider.id,
|
|
826
|
-
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
827
|
-
thinking: genResult.thinking,
|
|
828
|
-
...(genResult.thoughtSignature ? { thoughtSignature: genResult.thoughtSignature } : {}),
|
|
829
|
-
},
|
|
830
|
-
});
|
|
859
|
+
config.onTurnUsage?.(genResult.usage, spec);
|
|
831
860
|
} catch {}
|
|
832
|
-
|
|
861
|
+
config.enforceBudget?.({
|
|
862
|
+
...genResult.usage,
|
|
863
|
+
cost: computeCostFromPricing(genResult.usage, spec, options?.serviceTier),
|
|
864
|
+
});
|
|
865
|
+
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
866
|
+
|
|
867
|
+
// Record assistant turn in context
|
|
868
|
+
context.addAssistantMessage(
|
|
869
|
+
genResult.text,
|
|
870
|
+
genResult.toolCalls,
|
|
871
|
+
genResult.thinking,
|
|
872
|
+
genResult.thoughtSignature,
|
|
873
|
+
genResult.providerSteps
|
|
874
|
+
);
|
|
833
875
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
876
|
+
// Thinking-trace telemetry (observability-only, full text, no limit):
|
|
877
|
+
// one bus event per thinking turn carrying the text + thought signature.
|
|
878
|
+
// Never touches provider payloads, history, or persistence formats.
|
|
879
|
+
if (genResult.thinking) {
|
|
880
|
+
try {
|
|
881
|
+
telemetryBus.emit({
|
|
882
|
+
code: "turn.thinking",
|
|
883
|
+
message: `[Agent Accelerator] turn ${turns} thinking (${provider.id}/${modelId})`,
|
|
884
|
+
data: {
|
|
885
|
+
turns,
|
|
886
|
+
model: modelId,
|
|
887
|
+
provider: provider.id,
|
|
888
|
+
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
889
|
+
thinking: genResult.thinking,
|
|
890
|
+
...(genResult.thoughtSignature ? { thoughtSignature: genResult.thoughtSignature } : {}),
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
} catch {}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Mid-session steer: if new queries arrived during this provider turn,
|
|
897
|
+
// inject them now. Tool turns still execute first (see below) so the
|
|
898
|
+
// current action finishes before redirection.
|
|
899
|
+
const noToolSteered =
|
|
900
|
+
!genResult.toolCalls || genResult.toolCalls.length === 0
|
|
901
|
+
? drainSteerInbox(context, config.steerInbox, config.prompts)
|
|
902
|
+
: [];
|
|
840
903
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
904
|
+
// If no tool calls, generation is complete — unless steered, in which
|
|
905
|
+
// case the injected message extends the SAME run (no new response).
|
|
906
|
+
if (!genResult.toolCalls || genResult.toolCalls.length === 0) {
|
|
907
|
+
emitTurn(runOptions, { turns, text: genResult.text, thinking: genResult.thinking });
|
|
908
|
+
if (noToolSteered.length > 0) {
|
|
909
|
+
try {
|
|
910
|
+
config.onSteerInjected?.(noToolSteered);
|
|
911
|
+
} catch {}
|
|
912
|
+
}
|
|
913
|
+
emitProgress(config);
|
|
914
|
+
if (noToolSteered.length > 0) continue;
|
|
915
|
+
break;
|
|
847
916
|
}
|
|
848
|
-
emitProgress(config);
|
|
849
|
-
if (noToolSteered.length > 0) continue;
|
|
850
|
-
break;
|
|
851
|
-
}
|
|
852
917
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
s.usage
|
|
918
|
+
// Execute tool calls — always parallel (model-driven, bloatfree DX7)
|
|
919
|
+
// Check abort before tools (throws AbortError with partial).
|
|
920
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
921
|
+
emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
|
|
922
|
+
allToolCalls.push(...genResult.toolCalls);
|
|
923
|
+
// Per-run tool-call budget.
|
|
924
|
+
if (allToolCalls.length > limits.maxToolCallsPerRun) {
|
|
925
|
+
throw new BudgetExceededError(
|
|
926
|
+
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
toolsExecuted = true;
|
|
930
|
+
const guarded = await executeToolCallsWithRepeatGuard({
|
|
931
|
+
tools,
|
|
932
|
+
toolCalls: genResult.toolCalls,
|
|
933
|
+
previousFingerprints: previousToolCallFingerprints,
|
|
934
|
+
repeatCounts,
|
|
935
|
+
agentName,
|
|
936
|
+
signal: runOptions?.signal,
|
|
937
|
+
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
938
|
+
onResult: config.onToolResult,
|
|
939
|
+
openToolLog: config.openToolLog,
|
|
940
|
+
prompts: config.prompts,
|
|
941
|
+
});
|
|
942
|
+
const results = guarded.results;
|
|
943
|
+
previousToolCallFingerprints = guarded.fingerprints;
|
|
944
|
+
|
|
945
|
+
// Process results and extract any sub-agent execution metadata
|
|
946
|
+
const sanitizedResults: ToolResultRecord[] = [];
|
|
947
|
+
|
|
948
|
+
for (const r of results) {
|
|
949
|
+
const { sanitized, metas } = sanitizeToolResult(r);
|
|
950
|
+
if (metas.length > 0) {
|
|
951
|
+
allSubagents.push(...metas);
|
|
952
|
+
for (const s of metas) {
|
|
953
|
+
if (s.raw) s.raw = applyCaptureRaw(s.raw, config.captureRaw);
|
|
954
|
+
const subagentSpec = getModelFromCatalog(s.provider, s.model);
|
|
955
|
+
if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
|
|
956
|
+
s.usage.cost = computeCostFromPricing(s.usage, subagentSpec);
|
|
957
|
+
}
|
|
958
|
+
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
959
|
+
config.enforceBudget?.(s.usage);
|
|
891
960
|
}
|
|
892
|
-
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
893
|
-
config.enforceBudget?.(s.usage);
|
|
894
961
|
}
|
|
962
|
+
sanitizedResults.push(sanitized);
|
|
895
963
|
}
|
|
896
|
-
sanitizedResults.push(sanitized);
|
|
897
|
-
}
|
|
898
964
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
965
|
+
allToolResults.push(...sanitizedResults);
|
|
966
|
+
context.addToolResults(sanitizedResults);
|
|
967
|
+
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
968
|
+
// Results are stored BEFORE the cancellation check so an abort
|
|
969
|
+
// never drops executed tool work from history.
|
|
970
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
971
|
+
emitTurn(runOptions, {
|
|
972
|
+
turns,
|
|
973
|
+
text: genResult.text,
|
|
974
|
+
thinking: genResult.thinking,
|
|
975
|
+
toolCalls: genResult.toolCalls,
|
|
976
|
+
toolResults: sanitizedResults,
|
|
977
|
+
});
|
|
978
|
+
// A tool ended the run (`ctx.stop`): the whole batch has finished, so stop here. Waiting steers become follow-ups.
|
|
979
|
+
if (sanitizedResults.some((r) => r.stopped)) {
|
|
980
|
+
stoppedByTool = true;
|
|
981
|
+
emitProgress(config);
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
// Steer drain AFTER tool results so call/result pairs stay adjacent.
|
|
985
|
+
// The next loop iteration sends the injected user message to the model
|
|
986
|
+
// within the same run (same usage/turn budget).
|
|
987
|
+
const toolSteered = drainSteerInbox(context, config.steerInbox, config.prompts);
|
|
988
|
+
if (toolSteered.length > 0) {
|
|
989
|
+
try {
|
|
990
|
+
config.onSteerInjected?.(toolSteered);
|
|
991
|
+
} catch {}
|
|
992
|
+
}
|
|
915
993
|
emitProgress(config);
|
|
916
|
-
break;
|
|
917
|
-
}
|
|
918
|
-
// Steer drain AFTER tool results so call/result pairs stay adjacent.
|
|
919
|
-
// The next loop iteration sends the injected user message to the model
|
|
920
|
-
// within the same run (same usage/turn budget).
|
|
921
|
-
const toolSteered = drainSteerInbox(context, config.steerInbox);
|
|
922
|
-
if (toolSteered.length > 0) {
|
|
923
|
-
try { config.onSteerInjected?.(toolSteered); } catch {}
|
|
924
|
-
}
|
|
925
|
-
emitProgress(config);
|
|
926
994
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
995
|
+
// Abort always throws AbortError with partial attached (never
|
|
996
|
+
// silently breaks).
|
|
997
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
998
|
+
}
|
|
931
999
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1000
|
+
// maxTurns exhausted while the model still requested tools: the tool
|
|
1001
|
+
// results above were never sent back, so the run is truncated — surface
|
|
1002
|
+
// it via finishReason + warning instead of silently returning.
|
|
1003
|
+
const truncatedByMaxTurns =
|
|
1004
|
+
!stoppedByTool && turns >= maxTurns && !!finalResult?.toolCalls && finalResult.toolCalls.length > 0;
|
|
1005
|
+
if (truncatedByMaxTurns) {
|
|
1006
|
+
warnMaxTurnsHit(
|
|
1007
|
+
agentName,
|
|
1008
|
+
maxTurns,
|
|
1009
|
+
finalResult.toolCalls.map((c: ToolCallRecord) => c.name)
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
944
1012
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1013
|
+
// Structured outputs: validate final text only (tool turns skip this).
|
|
1014
|
+
// Strict-fail throws ValidationError with partial attached via catch below.
|
|
1015
|
+
let parsedOutput: unknown | undefined;
|
|
1016
|
+
if (loopOutput.raw !== undefined && !truncatedByMaxTurns && !stoppedByTool) {
|
|
1017
|
+
parsedOutput = parseFinalStructuredOutput(finalResult?.text || "", loopOutput.raw, loopOutput.spec);
|
|
1018
|
+
}
|
|
951
1019
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1020
|
+
return new AgentResponse({
|
|
1021
|
+
text: finalResult?.text || "",
|
|
1022
|
+
...(parsedOutput !== undefined ? { parsed: parsedOutput } : {}),
|
|
1023
|
+
thinking: finalResult?.thinking,
|
|
1024
|
+
thoughtSignature: finalResult?.thoughtSignature,
|
|
1025
|
+
toolCalls: allToolCalls,
|
|
1026
|
+
toolResults: allToolResults,
|
|
1027
|
+
subagents: allSubagents,
|
|
1028
|
+
usage: accumulatedUsage,
|
|
1029
|
+
responseId: finalResult?.responseId,
|
|
1030
|
+
model: modelId,
|
|
1031
|
+
provider: provider.id,
|
|
1032
|
+
// Canonical finish reason + raw vendor passthrough.
|
|
1033
|
+
...(stoppedByTool
|
|
1034
|
+
? {
|
|
1035
|
+
finishReason: "stopped" as const,
|
|
1036
|
+
...(finalResult?.finishReason ? { rawFinishReason: finalResult.finishReason } : {}),
|
|
1037
|
+
}
|
|
1038
|
+
: canonicalFinishReason(finalResult?.finishReason, truncatedByMaxTurns)),
|
|
1039
|
+
durationMs: Date.now() - startTime,
|
|
1040
|
+
raw: applyCaptureRaw(finalResult?.raw, config.captureRaw) as ProviderRawData,
|
|
1041
|
+
turns,
|
|
1042
|
+
});
|
|
972
1043
|
} catch (err) {
|
|
973
1044
|
// Run-level onFailure rollback (default restores pre-run
|
|
974
1045
|
// checkpoint; opt out with (config as any).onFailure === "keep").
|
|
975
1046
|
// Aborts are excluded so partial history survives interruption.
|
|
976
|
-
const isAbort =
|
|
977
|
-
(err as { name?: string } | null)?.name === "AbortError" ||
|
|
978
|
-
!!runOptions?.signal?.aborted;
|
|
1047
|
+
const isAbort = (err as { name?: string } | null)?.name === "AbortError" || !!runOptions?.signal?.aborted;
|
|
979
1048
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
980
|
-
try {
|
|
1049
|
+
try {
|
|
1050
|
+
context.rollback(preRunCp, sessionIdForRollback);
|
|
1051
|
+
} catch {
|
|
981
1052
|
// why: rollback is best-effort — the original error takes precedence.
|
|
982
1053
|
}
|
|
983
1054
|
}
|
|
984
1055
|
// Ensure aborts surface as AbortError with partial attached.
|
|
985
1056
|
if (isAbort && (err as { name?: string } | null)?.name !== "AbortError") {
|
|
986
|
-
throw makeAbortError(
|
|
987
|
-
{ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 },
|
|
988
|
-
err
|
|
989
|
-
);
|
|
1057
|
+
throw makeAbortError({ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 }, err);
|
|
990
1058
|
}
|
|
991
1059
|
// Every failure carries its partial progress for observability.
|
|
992
1060
|
try {
|
|
@@ -1019,17 +1087,29 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1019
1087
|
const linked = new AbortController();
|
|
1020
1088
|
const userSignal = config.runOptions?.signal;
|
|
1021
1089
|
const forwardUserAbort = () => {
|
|
1022
|
-
try {
|
|
1090
|
+
try {
|
|
1091
|
+
linked.abort((userSignal as any)?.reason);
|
|
1092
|
+
} catch {
|
|
1093
|
+
try {
|
|
1094
|
+
linked.abort();
|
|
1095
|
+
} catch {}
|
|
1096
|
+
}
|
|
1023
1097
|
};
|
|
1024
1098
|
if (userSignal?.aborted) forwardUserAbort();
|
|
1025
1099
|
else userSignal?.addEventListener("abort", forwardUserAbort, { once: true });
|
|
1026
1100
|
let currentInner: AssistantMessageEventStream | null = null;
|
|
1027
1101
|
const removeOuterCancel = outerStream.onCancel(() => {
|
|
1028
|
-
try {
|
|
1029
|
-
|
|
1102
|
+
try {
|
|
1103
|
+
linked.abort();
|
|
1104
|
+
} catch {}
|
|
1105
|
+
try {
|
|
1106
|
+
currentInner?.cancel();
|
|
1107
|
+
} catch {}
|
|
1030
1108
|
});
|
|
1031
1109
|
linked.signal.addEventListener("abort", () => {
|
|
1032
|
-
try {
|
|
1110
|
+
try {
|
|
1111
|
+
currentInner?.cancel();
|
|
1112
|
+
} catch {}
|
|
1033
1113
|
});
|
|
1034
1114
|
const throwIfCancelled = (partial?: { usage: TokenUsage; turns: number; text?: string }) => {
|
|
1035
1115
|
if (linked.signal.aborted || outerStream.isCancelled()) {
|
|
@@ -1082,16 +1162,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1082
1162
|
}
|
|
1083
1163
|
}
|
|
1084
1164
|
}
|
|
1085
|
-
const {
|
|
1086
|
-
agentName,
|
|
1087
|
-
provider,
|
|
1088
|
-
modelId,
|
|
1089
|
-
context,
|
|
1090
|
-
tools,
|
|
1091
|
-
options,
|
|
1092
|
-
runOptions,
|
|
1093
|
-
maxTurns: rawMaxTurns = Infinity,
|
|
1094
|
-
} = config;
|
|
1165
|
+
const { agentName, provider, modelId, context, tools, options, runOptions } = config;
|
|
1095
1166
|
const maxTurns = streamLimits.maxTurns;
|
|
1096
1167
|
|
|
1097
1168
|
const standardTools = toStandardToolDeclarations(tools);
|
|
@@ -1129,7 +1200,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1129
1200
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: lastResponse?.text });
|
|
1130
1201
|
checkDeadline(streamStartTime, streamLimits.deadlineMs);
|
|
1131
1202
|
config.enforceBudget?.();
|
|
1132
|
-
ensureToolPairing(context);
|
|
1203
|
+
ensureToolPairing(context, config.prompts);
|
|
1133
1204
|
if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
|
|
1134
1205
|
turns++;
|
|
1135
1206
|
|
|
@@ -1149,20 +1220,6 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1149
1220
|
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = streamLimits.requestTimeoutMs;
|
|
1150
1221
|
}
|
|
1151
1222
|
} catch {}
|
|
1152
|
-
try {
|
|
1153
|
-
const gStore =
|
|
1154
|
-
(runOptions as unknown as { googleStore?: unknown } | undefined)?.googleStore ??
|
|
1155
|
-
(options as unknown as { googleStore?: unknown } | undefined)?.googleStore ??
|
|
1156
|
-
(config as unknown as { googleStore?: unknown }).googleStore;
|
|
1157
|
-
if (gStore === false) (providerOptions as unknown as { store?: boolean }).store = false;
|
|
1158
|
-
} catch {}
|
|
1159
|
-
try {
|
|
1160
|
-
const gTier =
|
|
1161
|
-
(runOptions as unknown as { googleTier?: unknown } | undefined)?.googleTier ??
|
|
1162
|
-
(options as unknown as { googleTier?: unknown } | undefined)?.googleTier ??
|
|
1163
|
-
(config as unknown as { googleTier?: unknown }).googleTier;
|
|
1164
|
-
if (gTier === "paid" || gTier === "free") providerOptions.googleTier = gTier;
|
|
1165
|
-
} catch {}
|
|
1166
1223
|
|
|
1167
1224
|
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
1168
1225
|
const innerStream = provider.stream(
|
|
@@ -1184,22 +1241,35 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1184
1241
|
let doneIter = false;
|
|
1185
1242
|
while (!doneIter) {
|
|
1186
1243
|
// Long-running streams pause for minutes between chunks; the adapter guards stalls and reconnects.
|
|
1187
|
-
const waitMs = longRunning
|
|
1244
|
+
const waitMs = longRunning
|
|
1245
|
+
? Infinity
|
|
1246
|
+
: first
|
|
1247
|
+
? streamLimits.firstByteTimeoutMs
|
|
1248
|
+
: streamLimits.streamIdleTimeoutMs;
|
|
1188
1249
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1189
1250
|
const timeoutP =
|
|
1190
1251
|
Number.isFinite(waitMs) && waitMs !== Infinity && waitMs > 0
|
|
1191
1252
|
? new Promise<never>((_, reject) => {
|
|
1192
1253
|
timer = setTimeout(() => {
|
|
1193
|
-
reject(
|
|
1254
|
+
reject(
|
|
1255
|
+
new TimeoutError(
|
|
1256
|
+
`[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`
|
|
1257
|
+
)
|
|
1258
|
+
);
|
|
1194
1259
|
}, waitMs);
|
|
1195
|
-
try {
|
|
1260
|
+
try {
|
|
1261
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
1262
|
+
} catch {}
|
|
1196
1263
|
})
|
|
1197
1264
|
: null;
|
|
1198
1265
|
try {
|
|
1199
1266
|
const nextP = it.next();
|
|
1200
1267
|
const res = timeoutP ? await Promise.race([nextP, timeoutP]) : await nextP;
|
|
1201
1268
|
if (timer) clearTimeout(timer);
|
|
1202
|
-
if (res.done) {
|
|
1269
|
+
if (res.done) {
|
|
1270
|
+
doneIter = true;
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1203
1273
|
first = false;
|
|
1204
1274
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: lastResponse?.text });
|
|
1205
1275
|
const event = res.value as StreamEvent;
|
|
@@ -1215,7 +1285,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1215
1285
|
if (timer) clearTimeout(timer);
|
|
1216
1286
|
// Timeout: cancel the hung inner stream before surfacing.
|
|
1217
1287
|
if ((e as Error)?.name === "TimeoutError") {
|
|
1218
|
-
try {
|
|
1288
|
+
try {
|
|
1289
|
+
currentInner?.cancel();
|
|
1290
|
+
} catch {}
|
|
1219
1291
|
}
|
|
1220
1292
|
throw e;
|
|
1221
1293
|
}
|
|
@@ -1227,15 +1299,16 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1227
1299
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: (turnResponse as { text?: string })?.text });
|
|
1228
1300
|
lastResponse = turnResponse;
|
|
1229
1301
|
// Canonical session routing: lets adapters detect provider switches.
|
|
1230
|
-
noteProviderTurn(
|
|
1231
|
-
runOptions?.sessionId || options?.sessionId,
|
|
1232
|
-
provider.id
|
|
1233
|
-
);
|
|
1302
|
+
noteProviderTurn(runOptions?.sessionId || options?.sessionId, provider.id);
|
|
1234
1303
|
|
|
1235
1304
|
// Thinking stays on events/traces + onTurn (default exposed). Promoting
|
|
1236
1305
|
// it to answer text is gated (default OFF): without the opt-in
|
|
1237
1306
|
// the turn keeps the provider's empty text and finishReason.
|
|
1238
|
-
if (
|
|
1307
|
+
if (
|
|
1308
|
+
(!turnResponse.text || turnResponse.text.trim() === "") &&
|
|
1309
|
+
(!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) &&
|
|
1310
|
+
turnResponse.thinking
|
|
1311
|
+
) {
|
|
1239
1312
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
1240
1313
|
if (turnResponse.thinking.includes("</think>")) {
|
|
1241
1314
|
const parts = turnResponse.thinking.split(/<\/(?:think|thought)>/i);
|
|
@@ -1249,8 +1322,13 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1249
1322
|
}
|
|
1250
1323
|
|
|
1251
1324
|
accumulateUsage(accumulatedUsage, turnResponse.usage, spec2, options?.serviceTier);
|
|
1252
|
-
try {
|
|
1253
|
-
|
|
1325
|
+
try {
|
|
1326
|
+
config.onTurnUsage?.(turnResponse.usage, spec2);
|
|
1327
|
+
} catch {}
|
|
1328
|
+
config.enforceBudget?.({
|
|
1329
|
+
...turnResponse.usage,
|
|
1330
|
+
cost: computeCostFromPricing(turnResponse.usage, spec2, options?.serviceTier),
|
|
1331
|
+
});
|
|
1254
1332
|
checkTokenLimit(accumulatedUsage, streamLimits.maxTotalTokens);
|
|
1255
1333
|
|
|
1256
1334
|
context.addAssistantMessage(
|
|
@@ -1280,9 +1358,10 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1280
1358
|
} catch {}
|
|
1281
1359
|
}
|
|
1282
1360
|
|
|
1283
|
-
const noToolSteered =
|
|
1284
|
-
|
|
1285
|
-
|
|
1361
|
+
const noToolSteered =
|
|
1362
|
+
!turnResponse.toolCalls || turnResponse.toolCalls.length === 0
|
|
1363
|
+
? drainSteerInbox(context, config.steerInbox, config.prompts)
|
|
1364
|
+
: [];
|
|
1286
1365
|
if (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) {
|
|
1287
1366
|
emitTurn(runOptions, { turns, text: turnResponse.text, thinking: turnResponse.thinking });
|
|
1288
1367
|
if (noToolSteered.length > 0) {
|
|
@@ -1293,7 +1372,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1293
1372
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1294
1373
|
}
|
|
1295
1374
|
}
|
|
1296
|
-
try {
|
|
1375
|
+
try {
|
|
1376
|
+
config.onSteerInjected?.(noToolSteered);
|
|
1377
|
+
} catch {
|
|
1297
1378
|
// why: user callback — a throw must not fail the run.
|
|
1298
1379
|
}
|
|
1299
1380
|
}
|
|
@@ -1321,12 +1402,13 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1321
1402
|
tools,
|
|
1322
1403
|
toolCalls: turnResponse.toolCalls,
|
|
1323
1404
|
previousFingerprints: previousToolCallFingerprints,
|
|
1324
|
-
|
|
1405
|
+
repeatCounts,
|
|
1325
1406
|
agentName,
|
|
1326
1407
|
signal: linked.signal,
|
|
1327
1408
|
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
1328
1409
|
onResult: config.onToolResult,
|
|
1329
1410
|
openToolLog: config.openToolLog,
|
|
1411
|
+
prompts: config.prompts,
|
|
1330
1412
|
onSubagentEvent: (ev) => {
|
|
1331
1413
|
try {
|
|
1332
1414
|
outerStream.push({
|
|
@@ -1398,7 +1480,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1398
1480
|
emitProgress(config);
|
|
1399
1481
|
break;
|
|
1400
1482
|
}
|
|
1401
|
-
const toolSteered = drainSteerInbox(context, config.steerInbox);
|
|
1483
|
+
const toolSteered = drainSteerInbox(context, config.steerInbox, config.prompts);
|
|
1402
1484
|
if (toolSteered.length > 0) {
|
|
1403
1485
|
for (const preview of toolSteered) {
|
|
1404
1486
|
try {
|
|
@@ -1407,7 +1489,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1407
1489
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1408
1490
|
}
|
|
1409
1491
|
}
|
|
1410
|
-
try {
|
|
1492
|
+
try {
|
|
1493
|
+
config.onSteerInjected?.(toolSteered);
|
|
1494
|
+
} catch {
|
|
1411
1495
|
// why: user callback — a throw must not fail the run.
|
|
1412
1496
|
}
|
|
1413
1497
|
}
|
|
@@ -1424,7 +1508,10 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1424
1508
|
);
|
|
1425
1509
|
}
|
|
1426
1510
|
const truncatedFinish = stoppedByTool
|
|
1427
|
-
? {
|
|
1511
|
+
? {
|
|
1512
|
+
finishReason: "stopped" as const,
|
|
1513
|
+
...(lastResponse?.finishReason ? { rawFinishReason: lastResponse.finishReason } : {}),
|
|
1514
|
+
}
|
|
1428
1515
|
: canonicalFinishReason(lastResponse?.finishReason, truncatedByMaxTurns);
|
|
1429
1516
|
|
|
1430
1517
|
let streamParsed: unknown | undefined;
|
|
@@ -1433,15 +1520,17 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1433
1520
|
streamParsed = parseFinalStructuredOutput(lastResponse?.text || "", streamOutputRaw, streamOutputSpec);
|
|
1434
1521
|
} catch (err) {
|
|
1435
1522
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1436
|
-
try {
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1523
|
+
try {
|
|
1524
|
+
(raw as unknown as { partial?: unknown }).partial ??= {
|
|
1525
|
+
usage: accumulatedUsage,
|
|
1526
|
+
cost: accumulatedUsage.cost,
|
|
1527
|
+
turns,
|
|
1528
|
+
toolCalls: allToolCalls,
|
|
1529
|
+
toolResults: allToolResults,
|
|
1530
|
+
subagents: allSubagents,
|
|
1531
|
+
text: lastResponse?.text,
|
|
1532
|
+
};
|
|
1533
|
+
} catch {}
|
|
1445
1534
|
outerStream.fail(raw);
|
|
1446
1535
|
return;
|
|
1447
1536
|
}
|
|
@@ -1478,15 +1567,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1478
1567
|
outerStream.end(finalAgentResponse);
|
|
1479
1568
|
} catch (err: any) {
|
|
1480
1569
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1481
|
-
const isAbort =
|
|
1482
|
-
linked.signal.aborted ||
|
|
1483
|
-
outerStream.isCancelled() ||
|
|
1484
|
-
(raw as any)?.name === "AbortError";
|
|
1570
|
+
const isAbort = linked.signal.aborted || outerStream.isCancelled() || (raw as any)?.name === "AbortError";
|
|
1485
1571
|
// Streaming onFailure rollback (same policy as runAgentLoop;
|
|
1486
1572
|
// aborts excluded so partial history survives).
|
|
1487
1573
|
try {
|
|
1488
1574
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
1489
|
-
try {
|
|
1575
|
+
try {
|
|
1576
|
+
config.context.rollback(streamPreCp, streamSessionId);
|
|
1577
|
+
} catch {
|
|
1490
1578
|
// why: rollback is best-effort — the original error takes precedence.
|
|
1491
1579
|
}
|
|
1492
1580
|
}
|
|
@@ -1505,26 +1593,35 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1505
1593
|
};
|
|
1506
1594
|
// Aborts always surface as AbortError with partial attached.
|
|
1507
1595
|
if (isAbort) {
|
|
1508
|
-
const abortErr =
|
|
1509
|
-
|
|
1510
|
-
|
|
1596
|
+
const abortErr = raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
|
|
1597
|
+
try {
|
|
1598
|
+
(abortErr as unknown as { partial?: unknown }).partial ??= partial;
|
|
1599
|
+
} catch {
|
|
1511
1600
|
// why: partial attachment is observability-only and must never mask the error.
|
|
1512
1601
|
}
|
|
1513
1602
|
outerStream.fail(abortErr);
|
|
1514
1603
|
} else {
|
|
1515
|
-
try {
|
|
1604
|
+
try {
|
|
1605
|
+
(raw as unknown as { partial?: unknown }).partial ??= partial;
|
|
1606
|
+
} catch {
|
|
1516
1607
|
// why: partial attachment is observability-only and must never mask the error.
|
|
1517
1608
|
}
|
|
1518
1609
|
outerStream.fail(raw);
|
|
1519
1610
|
}
|
|
1520
1611
|
} finally {
|
|
1521
|
-
try {
|
|
1612
|
+
try {
|
|
1613
|
+
currentInner?.cancel();
|
|
1614
|
+
} catch {
|
|
1522
1615
|
// why: inner streams tolerate double-cancel; cleanup must not throw.
|
|
1523
1616
|
}
|
|
1524
|
-
try {
|
|
1617
|
+
try {
|
|
1618
|
+
userSignal?.removeEventListener("abort", forwardUserAbort);
|
|
1619
|
+
} catch {
|
|
1525
1620
|
// why: listener removal is best-effort cleanup.
|
|
1526
1621
|
}
|
|
1527
|
-
try {
|
|
1622
|
+
try {
|
|
1623
|
+
removeOuterCancel();
|
|
1624
|
+
} catch {
|
|
1528
1625
|
// why: unsubscribe is best-effort cleanup.
|
|
1529
1626
|
}
|
|
1530
1627
|
}
|
|
@@ -1550,10 +1647,13 @@ export function shouldPromoteThinkingToAnswer(config?: unknown, runOptions?: unk
|
|
|
1550
1647
|
if ((runOptions as { thinkingAsAnswerFallback?: unknown } | null | undefined)?.thinkingAsAnswerFallback === true) {
|
|
1551
1648
|
return true;
|
|
1552
1649
|
}
|
|
1553
|
-
const c = config as
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1650
|
+
const c = config as
|
|
1651
|
+
| {
|
|
1652
|
+
thinkingAsAnswerFallback?: unknown;
|
|
1653
|
+
runOptions?: { thinkingAsAnswerFallback?: unknown };
|
|
1654
|
+
}
|
|
1655
|
+
| null
|
|
1656
|
+
| undefined;
|
|
1557
1657
|
if (c?.thinkingAsAnswerFallback === true) return true;
|
|
1558
1658
|
if (c?.runOptions?.thinkingAsAnswerFallback === true) return true;
|
|
1559
1659
|
return false;
|