agent-accelerator 0.5.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -5
- 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 +374 -167
- package/src/agent/architecture.md +14 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +8 -3
- package/src/agent/delegation.ts +82 -29
- package/src/agent/loop.ts +449 -373
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +45 -7
- package/src/agent/subagents.md +3 -0
- package/src/agent/turns.ts +3 -1
- package/src/agent/worker.ts +58 -28
- package/src/agent/workers.ts +113 -20
- package/src/architecture.md +16 -15
- package/src/index.ts +34 -10
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +46 -49
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +150 -62
- package/src/providers/openai-compat.ts +60 -58
- package/src/providers/openai.ts +57 -39
- package/src/providers/openrouter.ts +87 -32
- package/src/providers/registry.ts +12 -26
- package/src/providers/shared.ts +89 -39
- package/src/providers.ts +56 -115
- package/src/scheduler/architecture.md +9 -8
- package/src/scheduler/scheduled-actions.md +4 -0
- package/src/scheduler/scheduler.ts +186 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +33 -7
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +38 -12
- package/src/session/store.ts +122 -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 +8 -7
- package/src/tools/executor.ts +50 -48
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +2 -21
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +1 -9
- package/src/types/errors.ts +26 -4
- package/src/types/model.ts +0 -4
- package/src/types/tool.ts +12 -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 +50 -44
- 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 +17 -31
- 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 };
|
|
@@ -23,7 +29,7 @@ import {
|
|
|
23
29
|
import { getModelFromCatalog, ensureModelCatalogFresh, validateModelThinking } from "../models/catalog.ts";
|
|
24
30
|
import { enforceFileAccess } from "../utils/safe-io.ts";
|
|
25
31
|
import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-result.ts";
|
|
26
|
-
import { BudgetExceededError } from "../types/errors.ts";
|
|
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";
|
|
29
35
|
import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
|
|
@@ -131,7 +137,13 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
|
|
|
131
137
|
// Reports one finished turn to the caller's onTurn hook (ids only), never throwing; in: both loops, out: AgentRunOptions.onTurn.
|
|
132
138
|
function emitTurn(
|
|
133
139
|
runOptions: AgentRunOptions | undefined,
|
|
134
|
-
turn: {
|
|
140
|
+
turn: {
|
|
141
|
+
turns: number;
|
|
142
|
+
text?: string;
|
|
143
|
+
thinking?: string;
|
|
144
|
+
toolCalls?: ToolCallRecord[];
|
|
145
|
+
toolResults?: ToolResultRecord[];
|
|
146
|
+
}
|
|
135
147
|
): void {
|
|
136
148
|
try {
|
|
137
149
|
runOptions?.onTurn?.({
|
|
@@ -173,7 +185,9 @@ function shouldRollbackOnFailure(
|
|
|
173
185
|
|
|
174
186
|
// Calls the persistence checkpoint hook, never throwing; in: both loops after each step, out: Agent.persistNow.
|
|
175
187
|
function emitProgress(config: { onProgress?: () => void }): void {
|
|
176
|
-
try {
|
|
188
|
+
try {
|
|
189
|
+
config.onProgress?.();
|
|
190
|
+
} catch {}
|
|
177
191
|
}
|
|
178
192
|
|
|
179
193
|
// ---------------------------------------------------------------------------
|
|
@@ -181,9 +195,13 @@ function emitProgress(config: { onProgress?: () => void }): void {
|
|
|
181
195
|
// ---------------------------------------------------------------------------
|
|
182
196
|
|
|
183
197
|
// 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): {
|
|
198
|
+
function resolveLoopOutput(config: AgentLoopConfig): {
|
|
199
|
+
raw: unknown;
|
|
200
|
+
spec: import("../types/model.ts").StructuredOutputSpec | undefined;
|
|
201
|
+
} {
|
|
185
202
|
const raw = (config as { output?: unknown }).output;
|
|
186
|
-
const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)
|
|
203
|
+
const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)
|
|
204
|
+
?.output;
|
|
187
205
|
if (raw !== undefined && raw !== null) {
|
|
188
206
|
// Throws ValidationError for invalid shapes — caught by run/stream catch
|
|
189
207
|
// with partial attached, same as other config validation.
|
|
@@ -195,7 +213,11 @@ function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: impor
|
|
|
195
213
|
}
|
|
196
214
|
|
|
197
215
|
// Validates the final text against the structured-output option, throwing ValidationError on mismatch; in: the last model text, out: AgentResponse.parsed.
|
|
198
|
-
function parseFinalStructuredOutput(
|
|
216
|
+
function parseFinalStructuredOutput(
|
|
217
|
+
text: string,
|
|
218
|
+
raw: unknown,
|
|
219
|
+
spec: import("../types/model.ts").StructuredOutputSpec | undefined
|
|
220
|
+
): unknown | undefined {
|
|
199
221
|
if (raw === undefined && spec === undefined) return undefined;
|
|
200
222
|
// Strict-fail when opted in: invalid JSON/schema throws ValidationError.
|
|
201
223
|
// Tool turns skip this (called only for final no-tool text).
|
|
@@ -280,7 +302,9 @@ export function ensureToolPairing(context: AgentContext): void {
|
|
|
280
302
|
const hit = results.get(p.id)?.shift();
|
|
281
303
|
if (hit) {
|
|
282
304
|
// Reuse the original message object when it carried exactly this result.
|
|
283
|
-
rebuilt.push(
|
|
305
|
+
rebuilt.push(
|
|
306
|
+
hit.message.content.length === 1 ? hit.message : { role: "tool", name: hit.part.name, content: [hit.part] }
|
|
307
|
+
);
|
|
284
308
|
} else {
|
|
285
309
|
const name = typeof p.name === "string" ? p.name : "tool";
|
|
286
310
|
traceToolEvent("call_interrupted", "warn", { id: p.id, name }, {});
|
|
@@ -320,10 +344,12 @@ export interface LoopLimits {
|
|
|
320
344
|
}
|
|
321
345
|
|
|
322
346
|
/** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
|
|
323
|
-
function resolveLoopLimits(
|
|
347
|
+
function resolveLoopLimits(
|
|
348
|
+
config: AgentLoopConfig,
|
|
349
|
+
fallbackMaxTurns: number
|
|
350
|
+
): LoopLimits & { maxTurns: number; deadlineMs: number; maxToolCallsPerRun: number; maxTotalTokens: number } {
|
|
324
351
|
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;
|
|
352
|
+
const num = (v: unknown): number | undefined => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
|
327
353
|
let maxTurns = fallbackMaxTurns;
|
|
328
354
|
const limMax = num(raw.maxTurns);
|
|
329
355
|
if (limMax !== undefined) {
|
|
@@ -338,7 +364,15 @@ function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): L
|
|
|
338
364
|
const requestTimeoutMs = timeout(raw.requestTimeoutMs) ?? 300_000;
|
|
339
365
|
const firstByteTimeoutMs = timeout(raw.firstByteTimeoutMs) ?? 120_000;
|
|
340
366
|
const streamIdleTimeoutMs = timeout(raw.streamIdleTimeoutMs) ?? 300_000;
|
|
341
|
-
return {
|
|
367
|
+
return {
|
|
368
|
+
maxTurns,
|
|
369
|
+
deadlineMs,
|
|
370
|
+
maxToolCallsPerRun,
|
|
371
|
+
maxTotalTokens,
|
|
372
|
+
requestTimeoutMs,
|
|
373
|
+
firstByteTimeoutMs,
|
|
374
|
+
streamIdleTimeoutMs,
|
|
375
|
+
};
|
|
342
376
|
}
|
|
343
377
|
|
|
344
378
|
// The session id a run's provider calls use (run override, else agent default); in: AgentLoopConfig, out: rollback and routing.
|
|
@@ -348,21 +382,24 @@ function loopSessionId(config: AgentLoopConfig): string | undefined {
|
|
|
348
382
|
(config.runOptions as { sessionId?: string } | undefined)?.sessionId ||
|
|
349
383
|
(config.options as { sessionId?: string } | undefined)?.sessionId
|
|
350
384
|
);
|
|
351
|
-
} catch {
|
|
385
|
+
} catch {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
352
388
|
}
|
|
353
389
|
|
|
354
390
|
// 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 {
|
|
391
|
+
function makeAbortError(partial: { usage: TokenUsage; turns: number; text?: string }, reason?: unknown): Error {
|
|
359
392
|
let base: Error;
|
|
360
393
|
if (reason instanceof Error && reason.name === "AbortError") base = reason;
|
|
361
394
|
else if (reason instanceof Error) base = new Error(reason.message || "Aborted");
|
|
362
395
|
else if (typeof reason === "string" && reason) base = new Error(reason);
|
|
363
396
|
else base = new Error("Aborted");
|
|
364
|
-
try {
|
|
365
|
-
|
|
397
|
+
try {
|
|
398
|
+
(base as { name: string }).name = "AbortError";
|
|
399
|
+
} catch {}
|
|
400
|
+
try {
|
|
401
|
+
(base as unknown as { partial: unknown }).partial = partial;
|
|
402
|
+
} catch {}
|
|
366
403
|
return base;
|
|
367
404
|
}
|
|
368
405
|
|
|
@@ -389,7 +426,7 @@ function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
|
|
|
389
426
|
// Throws when the run has been going longer than limits.deadlineMs; in: both loops each turn, out: nothing or a thrown error.
|
|
390
427
|
function checkDeadline(startTime: number, deadlineMs: number): void {
|
|
391
428
|
if (Number.isFinite(deadlineMs) && deadlineMs !== Infinity && Date.now() - startTime > deadlineMs) {
|
|
392
|
-
throw new
|
|
429
|
+
throw new BudgetExceededError(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
|
|
393
430
|
}
|
|
394
431
|
}
|
|
395
432
|
|
|
@@ -400,11 +437,11 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
|
|
|
400
437
|
try {
|
|
401
438
|
const timeoutP = new Promise<never>((_, reject) => {
|
|
402
439
|
timer = setTimeout(() => {
|
|
403
|
-
|
|
404
|
-
e.name = "TimeoutError";
|
|
405
|
-
reject(e);
|
|
440
|
+
reject(new TimeoutError(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`));
|
|
406
441
|
}, timeoutMs);
|
|
407
|
-
try {
|
|
442
|
+
try {
|
|
443
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
444
|
+
} catch {}
|
|
408
445
|
});
|
|
409
446
|
return await Promise.race([task(), timeoutP]);
|
|
410
447
|
} finally {
|
|
@@ -523,7 +560,13 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
523
560
|
agentName?: string;
|
|
524
561
|
signal?: AbortSignal;
|
|
525
562
|
sessionId?: string;
|
|
526
|
-
onSubagentEvent?: (event: {
|
|
563
|
+
onSubagentEvent?: (event: {
|
|
564
|
+
trackingId: string;
|
|
565
|
+
delta?: string;
|
|
566
|
+
thinkingDelta?: string;
|
|
567
|
+
partialText?: string;
|
|
568
|
+
partialThinking?: string;
|
|
569
|
+
}) => void;
|
|
527
570
|
}): Promise<{ results: ToolResultRecord[]; fingerprints: Set<string> }> {
|
|
528
571
|
const { toolCalls, previousFingerprints, repeatCounts } = options;
|
|
529
572
|
const currentFingerprints = new Set<string>();
|
|
@@ -540,7 +583,13 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
540
583
|
}
|
|
541
584
|
if (!previousFingerprints.has(fingerprint)) repeatCounts.delete(fingerprint); // streak broken
|
|
542
585
|
if (shouldBlockRepeat(previousFingerprints, call, repeatCounts)) {
|
|
543
|
-
traceToolEvent(
|
|
586
|
+
traceToolEvent(
|
|
587
|
+
"call_blocked",
|
|
588
|
+
"warn",
|
|
589
|
+
call,
|
|
590
|
+
{ agentName: options.agentName, sessionId: options.sessionId },
|
|
591
|
+
{ reason: "repeat_blocked" }
|
|
592
|
+
);
|
|
544
593
|
blocked.set(call.id, makeRepeatBlockedResult(call));
|
|
545
594
|
} else {
|
|
546
595
|
executable.push(call);
|
|
@@ -549,19 +598,20 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
549
598
|
// Streaks only survive while the fingerprint keeps appearing every turn.
|
|
550
599
|
for (const fp of [...repeatCounts.keys()]) if (!currentFingerprints.has(fp)) repeatCounts.delete(fp);
|
|
551
600
|
|
|
552
|
-
const executed =
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
601
|
+
const executed =
|
|
602
|
+
executable.length > 0
|
|
603
|
+
? await executeToolCalls({
|
|
604
|
+
tools: options.tools,
|
|
605
|
+
toolCalls: executable,
|
|
606
|
+
agentName: options.agentName,
|
|
607
|
+
parallel: true,
|
|
608
|
+
signal: options.signal,
|
|
609
|
+
sessionId: options.sessionId,
|
|
610
|
+
onSubagentEvent: options.onSubagentEvent,
|
|
611
|
+
onResult: options.onResult,
|
|
612
|
+
openToolLog: options.openToolLog,
|
|
613
|
+
})
|
|
614
|
+
: [];
|
|
565
615
|
const byId = new Map<string, ToolResultRecord[]>();
|
|
566
616
|
for (const result of executed) {
|
|
567
617
|
const queue = byId.get(result.id) ?? [];
|
|
@@ -579,9 +629,7 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
579
629
|
// id for several distinct calls in a turn, and a plain id->result map
|
|
580
630
|
// would hand every call the last result. Queues keep each result aligned
|
|
581
631
|
// with its own call.
|
|
582
|
-
results: toolCalls
|
|
583
|
-
.map((call) => byId.get(call.id)?.shift())
|
|
584
|
-
.filter((r): r is ToolResultRecord => Boolean(r)),
|
|
632
|
+
results: toolCalls.map((call) => byId.get(call.id)?.shift()).filter((r): r is ToolResultRecord => Boolean(r)),
|
|
585
633
|
fingerprints: currentFingerprints,
|
|
586
634
|
};
|
|
587
635
|
}
|
|
@@ -605,7 +653,13 @@ export function unfinishedToolCalls(messages: AgentContext["messages"]): ToolCal
|
|
|
605
653
|
const open: ToolCallRecord[] = [];
|
|
606
654
|
for (const p of calls) {
|
|
607
655
|
if (p.type === "tool_call" && !answered.has(p.id)) {
|
|
608
|
-
open.push({
|
|
656
|
+
open.push({
|
|
657
|
+
id: p.id,
|
|
658
|
+
name: p.name,
|
|
659
|
+
arguments: p.arguments,
|
|
660
|
+
...(p.rawArguments ? { rawArguments: p.rawArguments } : {}),
|
|
661
|
+
...(p.callId ? { callId: p.callId } : {}),
|
|
662
|
+
});
|
|
609
663
|
}
|
|
610
664
|
}
|
|
611
665
|
return open;
|
|
@@ -700,295 +754,287 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
700
754
|
const repeatCounts = new Map<string, number>();
|
|
701
755
|
|
|
702
756
|
try {
|
|
703
|
-
|
|
757
|
+
const standardTools = toStandardToolDeclarations(tools);
|
|
704
758
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
759
|
+
if (config.resume) {
|
|
760
|
+
const finished = await finishInterruptedTools(config);
|
|
761
|
+
stoppedByTool = finished.results.some((r) => r.stopped) || endsWithStop(context.messages);
|
|
762
|
+
if (finished.calls.length > 0) {
|
|
763
|
+
toolsExecuted = true;
|
|
764
|
+
allToolCalls.push(...finished.calls);
|
|
765
|
+
allToolResults.push(...finished.results);
|
|
766
|
+
for (const meta of finished.subagents) {
|
|
767
|
+
allSubagents.push(meta);
|
|
768
|
+
accumulateUsage(accumulatedUsage, meta.usage, getModelFromCatalog(meta.provider, meta.model));
|
|
769
|
+
}
|
|
715
770
|
}
|
|
716
771
|
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
while (!stoppedByTool && turns < maxTurns) {
|
|
720
|
-
// Fail fast on abort (throws AbortError with partial).
|
|
721
|
-
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
722
|
-
checkDeadline(startTime, limits.deadlineMs);
|
|
723
|
-
config.enforceBudget?.();
|
|
724
|
-
// Repair orphan tool pairs before every request.
|
|
725
|
-
ensureToolPairing(context);
|
|
726
|
-
if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
|
|
727
|
-
turns++;
|
|
728
|
-
|
|
729
|
-
const spec = getModelFromCatalog(provider.id, modelId);
|
|
730
|
-
|
|
731
|
-
const providerOptions: ProviderRequestOptions = {
|
|
732
|
-
...options,
|
|
733
|
-
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
734
|
-
cache: options?.cache,
|
|
735
|
-
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
736
|
-
signal: runOptions?.signal,
|
|
737
|
-
toolChoice: toolChoiceForTurn(options?.toolChoice, turns),
|
|
738
|
-
...(loopOutput.spec ? { output: loopOutput.spec } : {}),
|
|
739
|
-
};
|
|
740
|
-
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
741
|
-
try {
|
|
742
|
-
if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
|
|
743
|
-
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
|
|
744
|
-
}
|
|
745
|
-
} catch {}
|
|
746
|
-
// google:{store:false} opt-out pass-through (provider
|
|
747
|
-
// already respects body.store===false for chaining).
|
|
748
|
-
try {
|
|
749
|
-
const gStore =
|
|
750
|
-
(runOptions as unknown as { googleStore?: unknown } | undefined)?.googleStore ??
|
|
751
|
-
(options as unknown as { googleStore?: unknown } | undefined)?.googleStore ??
|
|
752
|
-
(config as unknown as { googleStore?: unknown }).googleStore;
|
|
753
|
-
if (gStore === false) (providerOptions as unknown as { store?: boolean }).store = false;
|
|
754
|
-
} catch {}
|
|
755
|
-
// Google interaction storage tier for the chain-TTL gate
|
|
756
|
-
// (`free`: 23h local gate; `paid`: 54d). Defaults to `free` in adapter.
|
|
757
|
-
try {
|
|
758
|
-
const gTier =
|
|
759
|
-
(runOptions as unknown as { googleTier?: unknown } | undefined)?.googleTier ??
|
|
760
|
-
(options as unknown as { googleTier?: unknown } | undefined)?.googleTier ??
|
|
761
|
-
(config as unknown as { googleTier?: unknown }).googleTier;
|
|
762
|
-
if (gTier === "paid" || gTier === "free") providerOptions.googleTier = gTier;
|
|
763
|
-
} catch {}
|
|
764
|
-
|
|
765
|
-
// A long-running call (e.g. Google long decode) times each HTTP request itself: no whole-call race.
|
|
766
|
-
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
767
|
-
const genResult = await generateWithTimeout(
|
|
768
|
-
() => provider.generate(
|
|
769
|
-
modelId,
|
|
770
|
-
{
|
|
771
|
-
systemPrompt: context.systemPrompt,
|
|
772
|
-
messages: context.messages,
|
|
773
|
-
},
|
|
774
|
-
providerOptions
|
|
775
|
-
),
|
|
776
|
-
longRunning ? Infinity : limits.requestTimeoutMs
|
|
777
|
-
);
|
|
778
772
|
|
|
779
|
-
|
|
773
|
+
while (!stoppedByTool && turns < maxTurns) {
|
|
774
|
+
// Fail fast on abort (throws AbortError with partial).
|
|
775
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
776
|
+
checkDeadline(startTime, limits.deadlineMs);
|
|
777
|
+
config.enforceBudget?.();
|
|
778
|
+
// Repair orphan tool pairs before every request.
|
|
779
|
+
ensureToolPairing(context);
|
|
780
|
+
if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
|
|
781
|
+
turns++;
|
|
782
|
+
|
|
783
|
+
const spec = getModelFromCatalog(provider.id, modelId);
|
|
784
|
+
|
|
785
|
+
const providerOptions: ProviderRequestOptions = {
|
|
786
|
+
...options,
|
|
787
|
+
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
788
|
+
cache: options?.cache,
|
|
789
|
+
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
790
|
+
signal: runOptions?.signal,
|
|
791
|
+
toolChoice: toolChoiceForTurn(options?.toolChoice, turns),
|
|
792
|
+
...(loopOutput.spec ? { output: loopOutput.spec } : {}),
|
|
793
|
+
};
|
|
794
|
+
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
795
|
+
try {
|
|
796
|
+
if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
|
|
797
|
+
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
|
|
798
|
+
}
|
|
799
|
+
} catch {}
|
|
780
800
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
801
|
+
// A long-running call (e.g. Google long decode) times each HTTP request itself: no whole-call race.
|
|
802
|
+
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
803
|
+
const genResult = await generateWithTimeout(
|
|
804
|
+
() =>
|
|
805
|
+
provider.generate(
|
|
806
|
+
modelId,
|
|
807
|
+
{
|
|
808
|
+
systemPrompt: context.systemPrompt,
|
|
809
|
+
messages: context.messages,
|
|
810
|
+
},
|
|
811
|
+
providerOptions
|
|
812
|
+
),
|
|
813
|
+
longRunning ? Infinity : limits.requestTimeoutMs
|
|
814
|
+
);
|
|
786
815
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
816
|
+
finalResult = genResult;
|
|
817
|
+
|
|
818
|
+
// Canonical session routing: lets adapters detect provider switches.
|
|
819
|
+
noteProviderTurn(runOptions?.sessionId || options?.sessionId, provider.id);
|
|
820
|
+
|
|
821
|
+
// Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (default
|
|
822
|
+
// OFF): without the opt-in the run returns the provider's empty text and finishReason.
|
|
823
|
+
if (
|
|
824
|
+
(!genResult.text || genResult.text.trim() === "") &&
|
|
825
|
+
(!genResult.toolCalls || genResult.toolCalls.length === 0) &&
|
|
826
|
+
genResult.thinking
|
|
827
|
+
) {
|
|
828
|
+
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
829
|
+
if (genResult.thinking.includes("</think>")) {
|
|
830
|
+
const parts = genResult.thinking.split(/<\/(?:think|thought)>/i);
|
|
831
|
+
genResult.thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
|
|
832
|
+
genResult.text = parts.slice(1).join("").trim();
|
|
833
|
+
} else {
|
|
834
|
+
genResult.text = genResult.thinking;
|
|
835
|
+
genResult.thinking = undefined;
|
|
836
|
+
}
|
|
798
837
|
}
|
|
799
838
|
}
|
|
800
|
-
}
|
|
801
839
|
|
|
802
|
-
|
|
803
|
-
try { config.onTurnUsage?.(genResult.usage, spec); } catch {}
|
|
804
|
-
config.enforceBudget?.({ ...genResult.usage, cost: computeCostFromPricing(genResult.usage, spec, options?.serviceTier) });
|
|
805
|
-
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
806
|
-
|
|
807
|
-
// Record assistant turn in context
|
|
808
|
-
context.addAssistantMessage(
|
|
809
|
-
genResult.text,
|
|
810
|
-
genResult.toolCalls,
|
|
811
|
-
genResult.thinking,
|
|
812
|
-
genResult.thoughtSignature,
|
|
813
|
-
genResult.providerSteps
|
|
814
|
-
);
|
|
815
|
-
|
|
816
|
-
// Thinking-trace telemetry (observability-only, full text, no limit):
|
|
817
|
-
// one bus event per thinking turn carrying the text + thought signature.
|
|
818
|
-
// Never touches provider payloads, history, or persistence formats.
|
|
819
|
-
if (genResult.thinking) {
|
|
840
|
+
accumulateUsage(accumulatedUsage, genResult.usage, spec, options?.serviceTier);
|
|
820
841
|
try {
|
|
821
|
-
|
|
822
|
-
code: "turn.thinking",
|
|
823
|
-
message: `[Agent Accelerator] turn ${turns} thinking (${provider.id}/${modelId})`,
|
|
824
|
-
data: {
|
|
825
|
-
turns,
|
|
826
|
-
model: modelId,
|
|
827
|
-
provider: provider.id,
|
|
828
|
-
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
829
|
-
thinking: genResult.thinking,
|
|
830
|
-
...(genResult.thoughtSignature ? { thoughtSignature: genResult.thoughtSignature } : {}),
|
|
831
|
-
},
|
|
832
|
-
});
|
|
842
|
+
config.onTurnUsage?.(genResult.usage, spec);
|
|
833
843
|
} catch {}
|
|
834
|
-
|
|
844
|
+
config.enforceBudget?.({
|
|
845
|
+
...genResult.usage,
|
|
846
|
+
cost: computeCostFromPricing(genResult.usage, spec, options?.serviceTier),
|
|
847
|
+
});
|
|
848
|
+
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
849
|
+
|
|
850
|
+
// Record assistant turn in context
|
|
851
|
+
context.addAssistantMessage(
|
|
852
|
+
genResult.text,
|
|
853
|
+
genResult.toolCalls,
|
|
854
|
+
genResult.thinking,
|
|
855
|
+
genResult.thoughtSignature,
|
|
856
|
+
genResult.providerSteps
|
|
857
|
+
);
|
|
835
858
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
859
|
+
// Thinking-trace telemetry (observability-only, full text, no limit):
|
|
860
|
+
// one bus event per thinking turn carrying the text + thought signature.
|
|
861
|
+
// Never touches provider payloads, history, or persistence formats.
|
|
862
|
+
if (genResult.thinking) {
|
|
863
|
+
try {
|
|
864
|
+
telemetryBus.emit({
|
|
865
|
+
code: "turn.thinking",
|
|
866
|
+
message: `[Agent Accelerator] turn ${turns} thinking (${provider.id}/${modelId})`,
|
|
867
|
+
data: {
|
|
868
|
+
turns,
|
|
869
|
+
model: modelId,
|
|
870
|
+
provider: provider.id,
|
|
871
|
+
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
872
|
+
thinking: genResult.thinking,
|
|
873
|
+
...(genResult.thoughtSignature ? { thoughtSignature: genResult.thoughtSignature } : {}),
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
} catch {}
|
|
877
|
+
}
|
|
842
878
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
879
|
+
// Mid-session steer: if new queries arrived during this provider turn,
|
|
880
|
+
// inject them now. Tool turns still execute first (see below) so the
|
|
881
|
+
// current action finishes before redirection.
|
|
882
|
+
const noToolSteered =
|
|
883
|
+
!genResult.toolCalls || genResult.toolCalls.length === 0 ? drainSteerInbox(context, config.steerInbox) : [];
|
|
884
|
+
|
|
885
|
+
// If no tool calls, generation is complete — unless steered, in which
|
|
886
|
+
// case the injected message extends the SAME run (no new response).
|
|
887
|
+
if (!genResult.toolCalls || genResult.toolCalls.length === 0) {
|
|
888
|
+
emitTurn(runOptions, { turns, text: genResult.text, thinking: genResult.thinking });
|
|
889
|
+
if (noToolSteered.length > 0) {
|
|
890
|
+
try {
|
|
891
|
+
config.onSteerInjected?.(noToolSteered);
|
|
892
|
+
} catch {}
|
|
893
|
+
}
|
|
894
|
+
emitProgress(config);
|
|
895
|
+
if (noToolSteered.length > 0) continue;
|
|
896
|
+
break;
|
|
849
897
|
}
|
|
850
|
-
emitProgress(config);
|
|
851
|
-
if (noToolSteered.length > 0) continue;
|
|
852
|
-
break;
|
|
853
|
-
}
|
|
854
898
|
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
899
|
+
// Execute tool calls — always parallel (model-driven, bloatfree DX7)
|
|
900
|
+
// Check abort before tools (throws AbortError with partial).
|
|
901
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
902
|
+
emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
|
|
903
|
+
allToolCalls.push(...genResult.toolCalls);
|
|
904
|
+
// Per-run tool-call budget.
|
|
905
|
+
if (allToolCalls.length > limits.maxToolCallsPerRun) {
|
|
906
|
+
throw new BudgetExceededError(
|
|
907
|
+
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
toolsExecuted = true;
|
|
911
|
+
const guarded = await executeToolCallsWithRepeatGuard({
|
|
912
|
+
tools,
|
|
913
|
+
toolCalls: genResult.toolCalls,
|
|
914
|
+
previousFingerprints: previousToolCallFingerprints,
|
|
915
|
+
repeatCounts,
|
|
916
|
+
agentName,
|
|
917
|
+
signal: runOptions?.signal,
|
|
918
|
+
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
919
|
+
onResult: config.onToolResult,
|
|
920
|
+
openToolLog: config.openToolLog,
|
|
921
|
+
});
|
|
922
|
+
const results = guarded.results;
|
|
923
|
+
previousToolCallFingerprints = guarded.fingerprints;
|
|
924
|
+
|
|
925
|
+
// Process results and extract any sub-agent execution metadata
|
|
926
|
+
const sanitizedResults: ToolResultRecord[] = [];
|
|
927
|
+
|
|
928
|
+
for (const r of results) {
|
|
929
|
+
const { sanitized, metas } = sanitizeToolResult(r);
|
|
930
|
+
if (metas.length > 0) {
|
|
931
|
+
allSubagents.push(...metas);
|
|
932
|
+
for (const s of metas) {
|
|
933
|
+
if (s.raw) s.raw = applyCaptureRaw(s.raw, config.captureRaw);
|
|
934
|
+
const subagentSpec = getModelFromCatalog(s.provider, s.model);
|
|
935
|
+
if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
|
|
936
|
+
s.usage.cost = computeCostFromPricing(s.usage, subagentSpec);
|
|
937
|
+
}
|
|
938
|
+
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
939
|
+
config.enforceBudget?.(s.usage);
|
|
893
940
|
}
|
|
894
|
-
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
895
|
-
config.enforceBudget?.(s.usage);
|
|
896
941
|
}
|
|
942
|
+
sanitizedResults.push(sanitized);
|
|
897
943
|
}
|
|
898
|
-
sanitizedResults.push(sanitized);
|
|
899
|
-
}
|
|
900
944
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
945
|
+
allToolResults.push(...sanitizedResults);
|
|
946
|
+
context.addToolResults(sanitizedResults);
|
|
947
|
+
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
948
|
+
// Results are stored BEFORE the cancellation check so an abort
|
|
949
|
+
// never drops executed tool work from history.
|
|
950
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
951
|
+
emitTurn(runOptions, {
|
|
952
|
+
turns,
|
|
953
|
+
text: genResult.text,
|
|
954
|
+
thinking: genResult.thinking,
|
|
955
|
+
toolCalls: genResult.toolCalls,
|
|
956
|
+
toolResults: sanitizedResults,
|
|
957
|
+
});
|
|
958
|
+
// A tool ended the run (`ctx.stop`): the whole batch has finished, so stop here. Waiting steers become follow-ups.
|
|
959
|
+
if (sanitizedResults.some((r) => r.stopped)) {
|
|
960
|
+
stoppedByTool = true;
|
|
961
|
+
emitProgress(config);
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
// Steer drain AFTER tool results so call/result pairs stay adjacent.
|
|
965
|
+
// The next loop iteration sends the injected user message to the model
|
|
966
|
+
// within the same run (same usage/turn budget).
|
|
967
|
+
const toolSteered = drainSteerInbox(context, config.steerInbox);
|
|
968
|
+
if (toolSteered.length > 0) {
|
|
969
|
+
try {
|
|
970
|
+
config.onSteerInjected?.(toolSteered);
|
|
971
|
+
} catch {}
|
|
972
|
+
}
|
|
917
973
|
emitProgress(config);
|
|
918
|
-
break;
|
|
919
|
-
}
|
|
920
|
-
// Steer drain AFTER tool results so call/result pairs stay adjacent.
|
|
921
|
-
// The next loop iteration sends the injected user message to the model
|
|
922
|
-
// within the same run (same usage/turn budget).
|
|
923
|
-
const toolSteered = drainSteerInbox(context, config.steerInbox);
|
|
924
|
-
if (toolSteered.length > 0) {
|
|
925
|
-
try { config.onSteerInjected?.(toolSteered); } catch {}
|
|
926
|
-
}
|
|
927
|
-
emitProgress(config);
|
|
928
974
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
975
|
+
// Abort always throws AbortError with partial attached (never
|
|
976
|
+
// silently breaks).
|
|
977
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
978
|
+
}
|
|
933
979
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
980
|
+
// maxTurns exhausted while the model still requested tools: the tool
|
|
981
|
+
// results above were never sent back, so the run is truncated — surface
|
|
982
|
+
// it via finishReason + warning instead of silently returning.
|
|
983
|
+
const truncatedByMaxTurns =
|
|
984
|
+
!stoppedByTool && turns >= maxTurns && !!finalResult?.toolCalls && finalResult.toolCalls.length > 0;
|
|
985
|
+
if (truncatedByMaxTurns) {
|
|
986
|
+
warnMaxTurnsHit(
|
|
987
|
+
agentName,
|
|
988
|
+
maxTurns,
|
|
989
|
+
finalResult.toolCalls.map((c: ToolCallRecord) => c.name)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
946
992
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
993
|
+
// Structured outputs: validate final text only (tool turns skip this).
|
|
994
|
+
// Strict-fail throws ValidationError with partial attached via catch below.
|
|
995
|
+
let parsedOutput: unknown | undefined;
|
|
996
|
+
if (loopOutput.raw !== undefined && !truncatedByMaxTurns && !stoppedByTool) {
|
|
997
|
+
parsedOutput = parseFinalStructuredOutput(finalResult?.text || "", loopOutput.raw, loopOutput.spec);
|
|
998
|
+
}
|
|
953
999
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1000
|
+
return new AgentResponse({
|
|
1001
|
+
text: finalResult?.text || "",
|
|
1002
|
+
...(parsedOutput !== undefined ? { parsed: parsedOutput } : {}),
|
|
1003
|
+
thinking: finalResult?.thinking,
|
|
1004
|
+
thoughtSignature: finalResult?.thoughtSignature,
|
|
1005
|
+
toolCalls: allToolCalls,
|
|
1006
|
+
toolResults: allToolResults,
|
|
1007
|
+
subagents: allSubagents,
|
|
1008
|
+
usage: accumulatedUsage,
|
|
1009
|
+
responseId: finalResult?.responseId,
|
|
1010
|
+
model: modelId,
|
|
1011
|
+
provider: provider.id,
|
|
1012
|
+
// Canonical finish reason + raw vendor passthrough.
|
|
1013
|
+
...(stoppedByTool
|
|
1014
|
+
? {
|
|
1015
|
+
finishReason: "stopped" as const,
|
|
1016
|
+
...(finalResult?.finishReason ? { rawFinishReason: finalResult.finishReason } : {}),
|
|
1017
|
+
}
|
|
1018
|
+
: canonicalFinishReason(finalResult?.finishReason, truncatedByMaxTurns)),
|
|
1019
|
+
durationMs: Date.now() - startTime,
|
|
1020
|
+
raw: applyCaptureRaw(finalResult?.raw, config.captureRaw) as ProviderRawData,
|
|
1021
|
+
turns,
|
|
1022
|
+
});
|
|
974
1023
|
} catch (err) {
|
|
975
1024
|
// Run-level onFailure rollback (default restores pre-run
|
|
976
1025
|
// checkpoint; opt out with (config as any).onFailure === "keep").
|
|
977
1026
|
// Aborts are excluded so partial history survives interruption.
|
|
978
|
-
const isAbort =
|
|
979
|
-
(err as { name?: string } | null)?.name === "AbortError" ||
|
|
980
|
-
!!runOptions?.signal?.aborted;
|
|
1027
|
+
const isAbort = (err as { name?: string } | null)?.name === "AbortError" || !!runOptions?.signal?.aborted;
|
|
981
1028
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
982
|
-
try {
|
|
1029
|
+
try {
|
|
1030
|
+
context.rollback(preRunCp, sessionIdForRollback);
|
|
1031
|
+
} catch {
|
|
983
1032
|
// why: rollback is best-effort — the original error takes precedence.
|
|
984
1033
|
}
|
|
985
1034
|
}
|
|
986
1035
|
// Ensure aborts surface as AbortError with partial attached.
|
|
987
1036
|
if (isAbort && (err as { name?: string } | null)?.name !== "AbortError") {
|
|
988
|
-
throw makeAbortError(
|
|
989
|
-
{ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 },
|
|
990
|
-
err
|
|
991
|
-
);
|
|
1037
|
+
throw makeAbortError({ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 }, err);
|
|
992
1038
|
}
|
|
993
1039
|
// Every failure carries its partial progress for observability.
|
|
994
1040
|
try {
|
|
@@ -998,6 +1044,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
998
1044
|
turns,
|
|
999
1045
|
toolCalls: allToolCalls,
|
|
1000
1046
|
toolResults: allToolResults,
|
|
1047
|
+
subagents: allSubagents,
|
|
1001
1048
|
text: finalResult?.text,
|
|
1002
1049
|
};
|
|
1003
1050
|
} catch {
|
|
@@ -1020,17 +1067,29 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1020
1067
|
const linked = new AbortController();
|
|
1021
1068
|
const userSignal = config.runOptions?.signal;
|
|
1022
1069
|
const forwardUserAbort = () => {
|
|
1023
|
-
try {
|
|
1070
|
+
try {
|
|
1071
|
+
linked.abort((userSignal as any)?.reason);
|
|
1072
|
+
} catch {
|
|
1073
|
+
try {
|
|
1074
|
+
linked.abort();
|
|
1075
|
+
} catch {}
|
|
1076
|
+
}
|
|
1024
1077
|
};
|
|
1025
1078
|
if (userSignal?.aborted) forwardUserAbort();
|
|
1026
1079
|
else userSignal?.addEventListener("abort", forwardUserAbort, { once: true });
|
|
1027
1080
|
let currentInner: AssistantMessageEventStream | null = null;
|
|
1028
1081
|
const removeOuterCancel = outerStream.onCancel(() => {
|
|
1029
|
-
try {
|
|
1030
|
-
|
|
1082
|
+
try {
|
|
1083
|
+
linked.abort();
|
|
1084
|
+
} catch {}
|
|
1085
|
+
try {
|
|
1086
|
+
currentInner?.cancel();
|
|
1087
|
+
} catch {}
|
|
1031
1088
|
});
|
|
1032
1089
|
linked.signal.addEventListener("abort", () => {
|
|
1033
|
-
try {
|
|
1090
|
+
try {
|
|
1091
|
+
currentInner?.cancel();
|
|
1092
|
+
} catch {}
|
|
1034
1093
|
});
|
|
1035
1094
|
const throwIfCancelled = (partial?: { usage: TokenUsage; turns: number; text?: string }) => {
|
|
1036
1095
|
if (linked.signal.aborted || outerStream.isCancelled()) {
|
|
@@ -1083,16 +1142,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1083
1142
|
}
|
|
1084
1143
|
}
|
|
1085
1144
|
}
|
|
1086
|
-
const {
|
|
1087
|
-
agentName,
|
|
1088
|
-
provider,
|
|
1089
|
-
modelId,
|
|
1090
|
-
context,
|
|
1091
|
-
tools,
|
|
1092
|
-
options,
|
|
1093
|
-
runOptions,
|
|
1094
|
-
maxTurns: rawMaxTurns = Infinity,
|
|
1095
|
-
} = config;
|
|
1145
|
+
const { agentName, provider, modelId, context, tools, options, runOptions } = config;
|
|
1096
1146
|
const maxTurns = streamLimits.maxTurns;
|
|
1097
1147
|
|
|
1098
1148
|
const standardTools = toStandardToolDeclarations(tools);
|
|
@@ -1150,20 +1200,6 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1150
1200
|
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = streamLimits.requestTimeoutMs;
|
|
1151
1201
|
}
|
|
1152
1202
|
} catch {}
|
|
1153
|
-
try {
|
|
1154
|
-
const gStore =
|
|
1155
|
-
(runOptions as unknown as { googleStore?: unknown } | undefined)?.googleStore ??
|
|
1156
|
-
(options as unknown as { googleStore?: unknown } | undefined)?.googleStore ??
|
|
1157
|
-
(config as unknown as { googleStore?: unknown }).googleStore;
|
|
1158
|
-
if (gStore === false) (providerOptions as unknown as { store?: boolean }).store = false;
|
|
1159
|
-
} catch {}
|
|
1160
|
-
try {
|
|
1161
|
-
const gTier =
|
|
1162
|
-
(runOptions as unknown as { googleTier?: unknown } | undefined)?.googleTier ??
|
|
1163
|
-
(options as unknown as { googleTier?: unknown } | undefined)?.googleTier ??
|
|
1164
|
-
(config as unknown as { googleTier?: unknown }).googleTier;
|
|
1165
|
-
if (gTier === "paid" || gTier === "free") providerOptions.googleTier = gTier;
|
|
1166
|
-
} catch {}
|
|
1167
1203
|
|
|
1168
1204
|
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
1169
1205
|
const innerStream = provider.stream(
|
|
@@ -1185,26 +1221,35 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1185
1221
|
let doneIter = false;
|
|
1186
1222
|
while (!doneIter) {
|
|
1187
1223
|
// Long-running streams pause for minutes between chunks; the adapter guards stalls and reconnects.
|
|
1188
|
-
const waitMs = longRunning
|
|
1224
|
+
const waitMs = longRunning
|
|
1225
|
+
? Infinity
|
|
1226
|
+
: first
|
|
1227
|
+
? streamLimits.firstByteTimeoutMs
|
|
1228
|
+
: streamLimits.streamIdleTimeoutMs;
|
|
1189
1229
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1190
1230
|
const timeoutP =
|
|
1191
1231
|
Number.isFinite(waitMs) && waitMs !== Infinity && waitMs > 0
|
|
1192
1232
|
? new Promise<never>((_, reject) => {
|
|
1193
1233
|
timer = setTimeout(() => {
|
|
1194
|
-
|
|
1195
|
-
|
|
1234
|
+
reject(
|
|
1235
|
+
new TimeoutError(
|
|
1236
|
+
`[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`
|
|
1237
|
+
)
|
|
1196
1238
|
);
|
|
1197
|
-
e.name = "TimeoutError";
|
|
1198
|
-
reject(e);
|
|
1199
1239
|
}, waitMs);
|
|
1200
|
-
try {
|
|
1240
|
+
try {
|
|
1241
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
1242
|
+
} catch {}
|
|
1201
1243
|
})
|
|
1202
1244
|
: null;
|
|
1203
1245
|
try {
|
|
1204
1246
|
const nextP = it.next();
|
|
1205
1247
|
const res = timeoutP ? await Promise.race([nextP, timeoutP]) : await nextP;
|
|
1206
1248
|
if (timer) clearTimeout(timer);
|
|
1207
|
-
if (res.done) {
|
|
1249
|
+
if (res.done) {
|
|
1250
|
+
doneIter = true;
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1208
1253
|
first = false;
|
|
1209
1254
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: lastResponse?.text });
|
|
1210
1255
|
const event = res.value as StreamEvent;
|
|
@@ -1220,7 +1265,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1220
1265
|
if (timer) clearTimeout(timer);
|
|
1221
1266
|
// Timeout: cancel the hung inner stream before surfacing.
|
|
1222
1267
|
if ((e as Error)?.name === "TimeoutError") {
|
|
1223
|
-
try {
|
|
1268
|
+
try {
|
|
1269
|
+
currentInner?.cancel();
|
|
1270
|
+
} catch {}
|
|
1224
1271
|
}
|
|
1225
1272
|
throw e;
|
|
1226
1273
|
}
|
|
@@ -1232,15 +1279,16 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1232
1279
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: (turnResponse as { text?: string })?.text });
|
|
1233
1280
|
lastResponse = turnResponse;
|
|
1234
1281
|
// Canonical session routing: lets adapters detect provider switches.
|
|
1235
|
-
noteProviderTurn(
|
|
1236
|
-
runOptions?.sessionId || options?.sessionId,
|
|
1237
|
-
provider.id
|
|
1238
|
-
);
|
|
1282
|
+
noteProviderTurn(runOptions?.sessionId || options?.sessionId, provider.id);
|
|
1239
1283
|
|
|
1240
1284
|
// Thinking stays on events/traces + onTurn (default exposed). Promoting
|
|
1241
1285
|
// it to answer text is gated (default OFF): without the opt-in
|
|
1242
1286
|
// the turn keeps the provider's empty text and finishReason.
|
|
1243
|
-
if (
|
|
1287
|
+
if (
|
|
1288
|
+
(!turnResponse.text || turnResponse.text.trim() === "") &&
|
|
1289
|
+
(!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) &&
|
|
1290
|
+
turnResponse.thinking
|
|
1291
|
+
) {
|
|
1244
1292
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
1245
1293
|
if (turnResponse.thinking.includes("</think>")) {
|
|
1246
1294
|
const parts = turnResponse.thinking.split(/<\/(?:think|thought)>/i);
|
|
@@ -1254,8 +1302,13 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1254
1302
|
}
|
|
1255
1303
|
|
|
1256
1304
|
accumulateUsage(accumulatedUsage, turnResponse.usage, spec2, options?.serviceTier);
|
|
1257
|
-
try {
|
|
1258
|
-
|
|
1305
|
+
try {
|
|
1306
|
+
config.onTurnUsage?.(turnResponse.usage, spec2);
|
|
1307
|
+
} catch {}
|
|
1308
|
+
config.enforceBudget?.({
|
|
1309
|
+
...turnResponse.usage,
|
|
1310
|
+
cost: computeCostFromPricing(turnResponse.usage, spec2, options?.serviceTier),
|
|
1311
|
+
});
|
|
1259
1312
|
checkTokenLimit(accumulatedUsage, streamLimits.maxTotalTokens);
|
|
1260
1313
|
|
|
1261
1314
|
context.addAssistantMessage(
|
|
@@ -1285,9 +1338,10 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1285
1338
|
} catch {}
|
|
1286
1339
|
}
|
|
1287
1340
|
|
|
1288
|
-
const noToolSteered =
|
|
1289
|
-
|
|
1290
|
-
|
|
1341
|
+
const noToolSteered =
|
|
1342
|
+
!turnResponse.toolCalls || turnResponse.toolCalls.length === 0
|
|
1343
|
+
? drainSteerInbox(context, config.steerInbox)
|
|
1344
|
+
: [];
|
|
1291
1345
|
if (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) {
|
|
1292
1346
|
emitTurn(runOptions, { turns, text: turnResponse.text, thinking: turnResponse.thinking });
|
|
1293
1347
|
if (noToolSteered.length > 0) {
|
|
@@ -1298,7 +1352,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1298
1352
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1299
1353
|
}
|
|
1300
1354
|
}
|
|
1301
|
-
try {
|
|
1355
|
+
try {
|
|
1356
|
+
config.onSteerInjected?.(noToolSteered);
|
|
1357
|
+
} catch {
|
|
1302
1358
|
// why: user callback — a throw must not fail the run.
|
|
1303
1359
|
}
|
|
1304
1360
|
}
|
|
@@ -1311,7 +1367,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1311
1367
|
emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
|
|
1312
1368
|
allToolCalls.push(...turnResponse.toolCalls);
|
|
1313
1369
|
if (allToolCalls.length > streamLimits.maxToolCallsPerRun) {
|
|
1314
|
-
throw new
|
|
1370
|
+
throw new BudgetExceededError(
|
|
1315
1371
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${streamLimits.maxToolCallsPerRun}).`
|
|
1316
1372
|
);
|
|
1317
1373
|
}
|
|
@@ -1326,7 +1382,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1326
1382
|
tools,
|
|
1327
1383
|
toolCalls: turnResponse.toolCalls,
|
|
1328
1384
|
previousFingerprints: previousToolCallFingerprints,
|
|
1329
|
-
|
|
1385
|
+
repeatCounts,
|
|
1330
1386
|
agentName,
|
|
1331
1387
|
signal: linked.signal,
|
|
1332
1388
|
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
@@ -1412,7 +1468,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1412
1468
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1413
1469
|
}
|
|
1414
1470
|
}
|
|
1415
|
-
try {
|
|
1471
|
+
try {
|
|
1472
|
+
config.onSteerInjected?.(toolSteered);
|
|
1473
|
+
} catch {
|
|
1416
1474
|
// why: user callback — a throw must not fail the run.
|
|
1417
1475
|
}
|
|
1418
1476
|
}
|
|
@@ -1429,7 +1487,10 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1429
1487
|
);
|
|
1430
1488
|
}
|
|
1431
1489
|
const truncatedFinish = stoppedByTool
|
|
1432
|
-
? {
|
|
1490
|
+
? {
|
|
1491
|
+
finishReason: "stopped" as const,
|
|
1492
|
+
...(lastResponse?.finishReason ? { rawFinishReason: lastResponse.finishReason } : {}),
|
|
1493
|
+
}
|
|
1433
1494
|
: canonicalFinishReason(lastResponse?.finishReason, truncatedByMaxTurns);
|
|
1434
1495
|
|
|
1435
1496
|
let streamParsed: unknown | undefined;
|
|
@@ -1438,14 +1499,17 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1438
1499
|
streamParsed = parseFinalStructuredOutput(lastResponse?.text || "", streamOutputRaw, streamOutputSpec);
|
|
1439
1500
|
} catch (err) {
|
|
1440
1501
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1441
|
-
try {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1502
|
+
try {
|
|
1503
|
+
(raw as unknown as { partial?: unknown }).partial ??= {
|
|
1504
|
+
usage: accumulatedUsage,
|
|
1505
|
+
cost: accumulatedUsage.cost,
|
|
1506
|
+
turns,
|
|
1507
|
+
toolCalls: allToolCalls,
|
|
1508
|
+
toolResults: allToolResults,
|
|
1509
|
+
subagents: allSubagents,
|
|
1510
|
+
text: lastResponse?.text,
|
|
1511
|
+
};
|
|
1512
|
+
} catch {}
|
|
1449
1513
|
outerStream.fail(raw);
|
|
1450
1514
|
return;
|
|
1451
1515
|
}
|
|
@@ -1482,15 +1546,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1482
1546
|
outerStream.end(finalAgentResponse);
|
|
1483
1547
|
} catch (err: any) {
|
|
1484
1548
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1485
|
-
const isAbort =
|
|
1486
|
-
linked.signal.aborted ||
|
|
1487
|
-
outerStream.isCancelled() ||
|
|
1488
|
-
(raw as any)?.name === "AbortError";
|
|
1549
|
+
const isAbort = linked.signal.aborted || outerStream.isCancelled() || (raw as any)?.name === "AbortError";
|
|
1489
1550
|
// Streaming onFailure rollback (same policy as runAgentLoop;
|
|
1490
1551
|
// aborts excluded so partial history survives).
|
|
1491
1552
|
try {
|
|
1492
1553
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
1493
|
-
try {
|
|
1554
|
+
try {
|
|
1555
|
+
config.context.rollback(streamPreCp, streamSessionId);
|
|
1556
|
+
} catch {
|
|
1494
1557
|
// why: rollback is best-effort — the original error takes precedence.
|
|
1495
1558
|
}
|
|
1496
1559
|
}
|
|
@@ -1504,30 +1567,40 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1504
1567
|
turns,
|
|
1505
1568
|
toolCalls: allToolCalls,
|
|
1506
1569
|
toolResults: allToolResults,
|
|
1570
|
+
subagents: allSubagents,
|
|
1507
1571
|
text: lastResponse?.text,
|
|
1508
1572
|
};
|
|
1509
1573
|
// Aborts always surface as AbortError with partial attached.
|
|
1510
1574
|
if (isAbort) {
|
|
1511
|
-
const abortErr =
|
|
1512
|
-
|
|
1513
|
-
|
|
1575
|
+
const abortErr = raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
|
|
1576
|
+
try {
|
|
1577
|
+
(abortErr as unknown as { partial?: unknown }).partial ??= partial;
|
|
1578
|
+
} catch {
|
|
1514
1579
|
// why: partial attachment is observability-only and must never mask the error.
|
|
1515
1580
|
}
|
|
1516
1581
|
outerStream.fail(abortErr);
|
|
1517
1582
|
} else {
|
|
1518
|
-
try {
|
|
1583
|
+
try {
|
|
1584
|
+
(raw as unknown as { partial?: unknown }).partial ??= partial;
|
|
1585
|
+
} catch {
|
|
1519
1586
|
// why: partial attachment is observability-only and must never mask the error.
|
|
1520
1587
|
}
|
|
1521
1588
|
outerStream.fail(raw);
|
|
1522
1589
|
}
|
|
1523
1590
|
} finally {
|
|
1524
|
-
try {
|
|
1591
|
+
try {
|
|
1592
|
+
currentInner?.cancel();
|
|
1593
|
+
} catch {
|
|
1525
1594
|
// why: inner streams tolerate double-cancel; cleanup must not throw.
|
|
1526
1595
|
}
|
|
1527
|
-
try {
|
|
1596
|
+
try {
|
|
1597
|
+
userSignal?.removeEventListener("abort", forwardUserAbort);
|
|
1598
|
+
} catch {
|
|
1528
1599
|
// why: listener removal is best-effort cleanup.
|
|
1529
1600
|
}
|
|
1530
|
-
try {
|
|
1601
|
+
try {
|
|
1602
|
+
removeOuterCancel();
|
|
1603
|
+
} catch {
|
|
1531
1604
|
// why: unsubscribe is best-effort cleanup.
|
|
1532
1605
|
}
|
|
1533
1606
|
}
|
|
@@ -1553,10 +1626,13 @@ export function shouldPromoteThinkingToAnswer(config?: unknown, runOptions?: unk
|
|
|
1553
1626
|
if ((runOptions as { thinkingAsAnswerFallback?: unknown } | null | undefined)?.thinkingAsAnswerFallback === true) {
|
|
1554
1627
|
return true;
|
|
1555
1628
|
}
|
|
1556
|
-
const c = config as
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1629
|
+
const c = config as
|
|
1630
|
+
| {
|
|
1631
|
+
thinkingAsAnswerFallback?: unknown;
|
|
1632
|
+
runOptions?: { thinkingAsAnswerFallback?: unknown };
|
|
1633
|
+
}
|
|
1634
|
+
| null
|
|
1635
|
+
| undefined;
|
|
1560
1636
|
if (c?.thinkingAsAnswerFallback === true) return true;
|
|
1561
1637
|
if (c?.runOptions?.thinkingAsAnswerFallback === true) return true;
|
|
1562
1638
|
return false;
|