agent-accelerator 0.5.9 → 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 +3 -1
- 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 +366 -161
- package/src/agent/architecture.md +13 -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 +444 -365
- 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 +56 -27
- 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 +39 -30
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +147 -59
- package/src/providers/openai-compat.ts +57 -49
- package/src/providers/openai.ts +54 -37
- package/src/providers/openrouter.ts +85 -30
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +56 -115
- package/src/scheduler/architecture.md +7 -6
- 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 +6 -14
- 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 };
|
|
@@ -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
|
|
|
@@ -402,7 +439,9 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
|
|
|
402
439
|
timer = setTimeout(() => {
|
|
403
440
|
reject(new TimeoutError(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`));
|
|
404
441
|
}, timeoutMs);
|
|
405
|
-
try {
|
|
442
|
+
try {
|
|
443
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
444
|
+
} catch {}
|
|
406
445
|
});
|
|
407
446
|
return await Promise.race([task(), timeoutP]);
|
|
408
447
|
} finally {
|
|
@@ -521,7 +560,13 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
521
560
|
agentName?: string;
|
|
522
561
|
signal?: AbortSignal;
|
|
523
562
|
sessionId?: string;
|
|
524
|
-
onSubagentEvent?: (event: {
|
|
563
|
+
onSubagentEvent?: (event: {
|
|
564
|
+
trackingId: string;
|
|
565
|
+
delta?: string;
|
|
566
|
+
thinkingDelta?: string;
|
|
567
|
+
partialText?: string;
|
|
568
|
+
partialThinking?: string;
|
|
569
|
+
}) => void;
|
|
525
570
|
}): Promise<{ results: ToolResultRecord[]; fingerprints: Set<string> }> {
|
|
526
571
|
const { toolCalls, previousFingerprints, repeatCounts } = options;
|
|
527
572
|
const currentFingerprints = new Set<string>();
|
|
@@ -538,7 +583,13 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
538
583
|
}
|
|
539
584
|
if (!previousFingerprints.has(fingerprint)) repeatCounts.delete(fingerprint); // streak broken
|
|
540
585
|
if (shouldBlockRepeat(previousFingerprints, call, repeatCounts)) {
|
|
541
|
-
traceToolEvent(
|
|
586
|
+
traceToolEvent(
|
|
587
|
+
"call_blocked",
|
|
588
|
+
"warn",
|
|
589
|
+
call,
|
|
590
|
+
{ agentName: options.agentName, sessionId: options.sessionId },
|
|
591
|
+
{ reason: "repeat_blocked" }
|
|
592
|
+
);
|
|
542
593
|
blocked.set(call.id, makeRepeatBlockedResult(call));
|
|
543
594
|
} else {
|
|
544
595
|
executable.push(call);
|
|
@@ -547,19 +598,20 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
547
598
|
// Streaks only survive while the fingerprint keeps appearing every turn.
|
|
548
599
|
for (const fp of [...repeatCounts.keys()]) if (!currentFingerprints.has(fp)) repeatCounts.delete(fp);
|
|
549
600
|
|
|
550
|
-
const executed =
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
+
: [];
|
|
563
615
|
const byId = new Map<string, ToolResultRecord[]>();
|
|
564
616
|
for (const result of executed) {
|
|
565
617
|
const queue = byId.get(result.id) ?? [];
|
|
@@ -577,9 +629,7 @@ async function executeToolCallsWithRepeatGuard(options: {
|
|
|
577
629
|
// id for several distinct calls in a turn, and a plain id->result map
|
|
578
630
|
// would hand every call the last result. Queues keep each result aligned
|
|
579
631
|
// with its own call.
|
|
580
|
-
results: toolCalls
|
|
581
|
-
.map((call) => byId.get(call.id)?.shift())
|
|
582
|
-
.filter((r): r is ToolResultRecord => Boolean(r)),
|
|
632
|
+
results: toolCalls.map((call) => byId.get(call.id)?.shift()).filter((r): r is ToolResultRecord => Boolean(r)),
|
|
583
633
|
fingerprints: currentFingerprints,
|
|
584
634
|
};
|
|
585
635
|
}
|
|
@@ -603,7 +653,13 @@ export function unfinishedToolCalls(messages: AgentContext["messages"]): ToolCal
|
|
|
603
653
|
const open: ToolCallRecord[] = [];
|
|
604
654
|
for (const p of calls) {
|
|
605
655
|
if (p.type === "tool_call" && !answered.has(p.id)) {
|
|
606
|
-
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
|
+
});
|
|
607
663
|
}
|
|
608
664
|
}
|
|
609
665
|
return open;
|
|
@@ -698,295 +754,287 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
698
754
|
const repeatCounts = new Map<string, number>();
|
|
699
755
|
|
|
700
756
|
try {
|
|
701
|
-
|
|
757
|
+
const standardTools = toStandardToolDeclarations(tools);
|
|
702
758
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
+
}
|
|
713
770
|
}
|
|
714
771
|
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
while (!stoppedByTool && turns < maxTurns) {
|
|
718
|
-
// Fail fast on abort (throws AbortError with partial).
|
|
719
|
-
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
720
|
-
checkDeadline(startTime, limits.deadlineMs);
|
|
721
|
-
config.enforceBudget?.();
|
|
722
|
-
// Repair orphan tool pairs before every request.
|
|
723
|
-
ensureToolPairing(context);
|
|
724
|
-
if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
|
|
725
|
-
turns++;
|
|
726
|
-
|
|
727
|
-
const spec = getModelFromCatalog(provider.id, modelId);
|
|
728
|
-
|
|
729
|
-
const providerOptions: ProviderRequestOptions = {
|
|
730
|
-
...options,
|
|
731
|
-
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
732
|
-
cache: options?.cache,
|
|
733
|
-
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
734
|
-
signal: runOptions?.signal,
|
|
735
|
-
toolChoice: toolChoiceForTurn(options?.toolChoice, turns),
|
|
736
|
-
...(loopOutput.spec ? { output: loopOutput.spec } : {}),
|
|
737
|
-
};
|
|
738
|
-
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
739
|
-
try {
|
|
740
|
-
if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
|
|
741
|
-
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
|
|
742
|
-
}
|
|
743
|
-
} catch {}
|
|
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
772
|
|
|
777
|
-
|
|
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 {}
|
|
778
800
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
+
);
|
|
784
815
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
+
}
|
|
796
837
|
}
|
|
797
838
|
}
|
|
798
|
-
}
|
|
799
839
|
|
|
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) {
|
|
840
|
+
accumulateUsage(accumulatedUsage, genResult.usage, spec, options?.serviceTier);
|
|
818
841
|
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
|
-
});
|
|
842
|
+
config.onTurnUsage?.(genResult.usage, spec);
|
|
831
843
|
} catch {}
|
|
832
|
-
|
|
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
|
+
);
|
|
833
858
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
+
}
|
|
840
878
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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;
|
|
847
897
|
}
|
|
848
|
-
emitProgress(config);
|
|
849
|
-
if (noToolSteered.length > 0) continue;
|
|
850
|
-
break;
|
|
851
|
-
}
|
|
852
898
|
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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);
|
|
891
940
|
}
|
|
892
|
-
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
893
|
-
config.enforceBudget?.(s.usage);
|
|
894
941
|
}
|
|
942
|
+
sanitizedResults.push(sanitized);
|
|
895
943
|
}
|
|
896
|
-
sanitizedResults.push(sanitized);
|
|
897
|
-
}
|
|
898
944
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
+
}
|
|
915
973
|
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
974
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
975
|
+
// Abort always throws AbortError with partial attached (never
|
|
976
|
+
// silently breaks).
|
|
977
|
+
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
978
|
+
}
|
|
931
979
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
+
}
|
|
944
992
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
+
}
|
|
951
999
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
+
});
|
|
972
1023
|
} catch (err) {
|
|
973
1024
|
// Run-level onFailure rollback (default restores pre-run
|
|
974
1025
|
// checkpoint; opt out with (config as any).onFailure === "keep").
|
|
975
1026
|
// Aborts are excluded so partial history survives interruption.
|
|
976
|
-
const isAbort =
|
|
977
|
-
(err as { name?: string } | null)?.name === "AbortError" ||
|
|
978
|
-
!!runOptions?.signal?.aborted;
|
|
1027
|
+
const isAbort = (err as { name?: string } | null)?.name === "AbortError" || !!runOptions?.signal?.aborted;
|
|
979
1028
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
980
|
-
try {
|
|
1029
|
+
try {
|
|
1030
|
+
context.rollback(preRunCp, sessionIdForRollback);
|
|
1031
|
+
} catch {
|
|
981
1032
|
// why: rollback is best-effort — the original error takes precedence.
|
|
982
1033
|
}
|
|
983
1034
|
}
|
|
984
1035
|
// Ensure aborts surface as AbortError with partial attached.
|
|
985
1036
|
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
|
-
);
|
|
1037
|
+
throw makeAbortError({ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 }, err);
|
|
990
1038
|
}
|
|
991
1039
|
// Every failure carries its partial progress for observability.
|
|
992
1040
|
try {
|
|
@@ -1019,17 +1067,29 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1019
1067
|
const linked = new AbortController();
|
|
1020
1068
|
const userSignal = config.runOptions?.signal;
|
|
1021
1069
|
const forwardUserAbort = () => {
|
|
1022
|
-
try {
|
|
1070
|
+
try {
|
|
1071
|
+
linked.abort((userSignal as any)?.reason);
|
|
1072
|
+
} catch {
|
|
1073
|
+
try {
|
|
1074
|
+
linked.abort();
|
|
1075
|
+
} catch {}
|
|
1076
|
+
}
|
|
1023
1077
|
};
|
|
1024
1078
|
if (userSignal?.aborted) forwardUserAbort();
|
|
1025
1079
|
else userSignal?.addEventListener("abort", forwardUserAbort, { once: true });
|
|
1026
1080
|
let currentInner: AssistantMessageEventStream | null = null;
|
|
1027
1081
|
const removeOuterCancel = outerStream.onCancel(() => {
|
|
1028
|
-
try {
|
|
1029
|
-
|
|
1082
|
+
try {
|
|
1083
|
+
linked.abort();
|
|
1084
|
+
} catch {}
|
|
1085
|
+
try {
|
|
1086
|
+
currentInner?.cancel();
|
|
1087
|
+
} catch {}
|
|
1030
1088
|
});
|
|
1031
1089
|
linked.signal.addEventListener("abort", () => {
|
|
1032
|
-
try {
|
|
1090
|
+
try {
|
|
1091
|
+
currentInner?.cancel();
|
|
1092
|
+
} catch {}
|
|
1033
1093
|
});
|
|
1034
1094
|
const throwIfCancelled = (partial?: { usage: TokenUsage; turns: number; text?: string }) => {
|
|
1035
1095
|
if (linked.signal.aborted || outerStream.isCancelled()) {
|
|
@@ -1082,16 +1142,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1082
1142
|
}
|
|
1083
1143
|
}
|
|
1084
1144
|
}
|
|
1085
|
-
const {
|
|
1086
|
-
agentName,
|
|
1087
|
-
provider,
|
|
1088
|
-
modelId,
|
|
1089
|
-
context,
|
|
1090
|
-
tools,
|
|
1091
|
-
options,
|
|
1092
|
-
runOptions,
|
|
1093
|
-
maxTurns: rawMaxTurns = Infinity,
|
|
1094
|
-
} = config;
|
|
1145
|
+
const { agentName, provider, modelId, context, tools, options, runOptions } = config;
|
|
1095
1146
|
const maxTurns = streamLimits.maxTurns;
|
|
1096
1147
|
|
|
1097
1148
|
const standardTools = toStandardToolDeclarations(tools);
|
|
@@ -1149,20 +1200,6 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1149
1200
|
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = streamLimits.requestTimeoutMs;
|
|
1150
1201
|
}
|
|
1151
1202
|
} 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
1203
|
|
|
1167
1204
|
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
1168
1205
|
const innerStream = provider.stream(
|
|
@@ -1184,22 +1221,35 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1184
1221
|
let doneIter = false;
|
|
1185
1222
|
while (!doneIter) {
|
|
1186
1223
|
// Long-running streams pause for minutes between chunks; the adapter guards stalls and reconnects.
|
|
1187
|
-
const waitMs = longRunning
|
|
1224
|
+
const waitMs = longRunning
|
|
1225
|
+
? Infinity
|
|
1226
|
+
: first
|
|
1227
|
+
? streamLimits.firstByteTimeoutMs
|
|
1228
|
+
: streamLimits.streamIdleTimeoutMs;
|
|
1188
1229
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1189
1230
|
const timeoutP =
|
|
1190
1231
|
Number.isFinite(waitMs) && waitMs !== Infinity && waitMs > 0
|
|
1191
1232
|
? new Promise<never>((_, reject) => {
|
|
1192
1233
|
timer = setTimeout(() => {
|
|
1193
|
-
reject(
|
|
1234
|
+
reject(
|
|
1235
|
+
new TimeoutError(
|
|
1236
|
+
`[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`
|
|
1237
|
+
)
|
|
1238
|
+
);
|
|
1194
1239
|
}, waitMs);
|
|
1195
|
-
try {
|
|
1240
|
+
try {
|
|
1241
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
1242
|
+
} catch {}
|
|
1196
1243
|
})
|
|
1197
1244
|
: null;
|
|
1198
1245
|
try {
|
|
1199
1246
|
const nextP = it.next();
|
|
1200
1247
|
const res = timeoutP ? await Promise.race([nextP, timeoutP]) : await nextP;
|
|
1201
1248
|
if (timer) clearTimeout(timer);
|
|
1202
|
-
if (res.done) {
|
|
1249
|
+
if (res.done) {
|
|
1250
|
+
doneIter = true;
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1203
1253
|
first = false;
|
|
1204
1254
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: lastResponse?.text });
|
|
1205
1255
|
const event = res.value as StreamEvent;
|
|
@@ -1215,7 +1265,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1215
1265
|
if (timer) clearTimeout(timer);
|
|
1216
1266
|
// Timeout: cancel the hung inner stream before surfacing.
|
|
1217
1267
|
if ((e as Error)?.name === "TimeoutError") {
|
|
1218
|
-
try {
|
|
1268
|
+
try {
|
|
1269
|
+
currentInner?.cancel();
|
|
1270
|
+
} catch {}
|
|
1219
1271
|
}
|
|
1220
1272
|
throw e;
|
|
1221
1273
|
}
|
|
@@ -1227,15 +1279,16 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1227
1279
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: (turnResponse as { text?: string })?.text });
|
|
1228
1280
|
lastResponse = turnResponse;
|
|
1229
1281
|
// Canonical session routing: lets adapters detect provider switches.
|
|
1230
|
-
noteProviderTurn(
|
|
1231
|
-
runOptions?.sessionId || options?.sessionId,
|
|
1232
|
-
provider.id
|
|
1233
|
-
);
|
|
1282
|
+
noteProviderTurn(runOptions?.sessionId || options?.sessionId, provider.id);
|
|
1234
1283
|
|
|
1235
1284
|
// Thinking stays on events/traces + onTurn (default exposed). Promoting
|
|
1236
1285
|
// it to answer text is gated (default OFF): without the opt-in
|
|
1237
1286
|
// the turn keeps the provider's empty text and finishReason.
|
|
1238
|
-
if (
|
|
1287
|
+
if (
|
|
1288
|
+
(!turnResponse.text || turnResponse.text.trim() === "") &&
|
|
1289
|
+
(!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) &&
|
|
1290
|
+
turnResponse.thinking
|
|
1291
|
+
) {
|
|
1239
1292
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
1240
1293
|
if (turnResponse.thinking.includes("</think>")) {
|
|
1241
1294
|
const parts = turnResponse.thinking.split(/<\/(?:think|thought)>/i);
|
|
@@ -1249,8 +1302,13 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1249
1302
|
}
|
|
1250
1303
|
|
|
1251
1304
|
accumulateUsage(accumulatedUsage, turnResponse.usage, spec2, options?.serviceTier);
|
|
1252
|
-
try {
|
|
1253
|
-
|
|
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
|
+
});
|
|
1254
1312
|
checkTokenLimit(accumulatedUsage, streamLimits.maxTotalTokens);
|
|
1255
1313
|
|
|
1256
1314
|
context.addAssistantMessage(
|
|
@@ -1280,9 +1338,10 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1280
1338
|
} catch {}
|
|
1281
1339
|
}
|
|
1282
1340
|
|
|
1283
|
-
const noToolSteered =
|
|
1284
|
-
|
|
1285
|
-
|
|
1341
|
+
const noToolSteered =
|
|
1342
|
+
!turnResponse.toolCalls || turnResponse.toolCalls.length === 0
|
|
1343
|
+
? drainSteerInbox(context, config.steerInbox)
|
|
1344
|
+
: [];
|
|
1286
1345
|
if (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) {
|
|
1287
1346
|
emitTurn(runOptions, { turns, text: turnResponse.text, thinking: turnResponse.thinking });
|
|
1288
1347
|
if (noToolSteered.length > 0) {
|
|
@@ -1293,7 +1352,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1293
1352
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1294
1353
|
}
|
|
1295
1354
|
}
|
|
1296
|
-
try {
|
|
1355
|
+
try {
|
|
1356
|
+
config.onSteerInjected?.(noToolSteered);
|
|
1357
|
+
} catch {
|
|
1297
1358
|
// why: user callback — a throw must not fail the run.
|
|
1298
1359
|
}
|
|
1299
1360
|
}
|
|
@@ -1321,7 +1382,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1321
1382
|
tools,
|
|
1322
1383
|
toolCalls: turnResponse.toolCalls,
|
|
1323
1384
|
previousFingerprints: previousToolCallFingerprints,
|
|
1324
|
-
|
|
1385
|
+
repeatCounts,
|
|
1325
1386
|
agentName,
|
|
1326
1387
|
signal: linked.signal,
|
|
1327
1388
|
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
@@ -1407,7 +1468,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1407
1468
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1408
1469
|
}
|
|
1409
1470
|
}
|
|
1410
|
-
try {
|
|
1471
|
+
try {
|
|
1472
|
+
config.onSteerInjected?.(toolSteered);
|
|
1473
|
+
} catch {
|
|
1411
1474
|
// why: user callback — a throw must not fail the run.
|
|
1412
1475
|
}
|
|
1413
1476
|
}
|
|
@@ -1424,7 +1487,10 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1424
1487
|
);
|
|
1425
1488
|
}
|
|
1426
1489
|
const truncatedFinish = stoppedByTool
|
|
1427
|
-
? {
|
|
1490
|
+
? {
|
|
1491
|
+
finishReason: "stopped" as const,
|
|
1492
|
+
...(lastResponse?.finishReason ? { rawFinishReason: lastResponse.finishReason } : {}),
|
|
1493
|
+
}
|
|
1428
1494
|
: canonicalFinishReason(lastResponse?.finishReason, truncatedByMaxTurns);
|
|
1429
1495
|
|
|
1430
1496
|
let streamParsed: unknown | undefined;
|
|
@@ -1433,15 +1499,17 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1433
1499
|
streamParsed = parseFinalStructuredOutput(lastResponse?.text || "", streamOutputRaw, streamOutputSpec);
|
|
1434
1500
|
} catch (err) {
|
|
1435
1501
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1436
|
-
try {
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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 {}
|
|
1445
1513
|
outerStream.fail(raw);
|
|
1446
1514
|
return;
|
|
1447
1515
|
}
|
|
@@ -1478,15 +1546,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1478
1546
|
outerStream.end(finalAgentResponse);
|
|
1479
1547
|
} catch (err: any) {
|
|
1480
1548
|
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";
|
|
1549
|
+
const isAbort = linked.signal.aborted || outerStream.isCancelled() || (raw as any)?.name === "AbortError";
|
|
1485
1550
|
// Streaming onFailure rollback (same policy as runAgentLoop;
|
|
1486
1551
|
// aborts excluded so partial history survives).
|
|
1487
1552
|
try {
|
|
1488
1553
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
1489
|
-
try {
|
|
1554
|
+
try {
|
|
1555
|
+
config.context.rollback(streamPreCp, streamSessionId);
|
|
1556
|
+
} catch {
|
|
1490
1557
|
// why: rollback is best-effort — the original error takes precedence.
|
|
1491
1558
|
}
|
|
1492
1559
|
}
|
|
@@ -1505,26 +1572,35 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1505
1572
|
};
|
|
1506
1573
|
// Aborts always surface as AbortError with partial attached.
|
|
1507
1574
|
if (isAbort) {
|
|
1508
|
-
const abortErr =
|
|
1509
|
-
|
|
1510
|
-
|
|
1575
|
+
const abortErr = raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
|
|
1576
|
+
try {
|
|
1577
|
+
(abortErr as unknown as { partial?: unknown }).partial ??= partial;
|
|
1578
|
+
} catch {
|
|
1511
1579
|
// why: partial attachment is observability-only and must never mask the error.
|
|
1512
1580
|
}
|
|
1513
1581
|
outerStream.fail(abortErr);
|
|
1514
1582
|
} else {
|
|
1515
|
-
try {
|
|
1583
|
+
try {
|
|
1584
|
+
(raw as unknown as { partial?: unknown }).partial ??= partial;
|
|
1585
|
+
} catch {
|
|
1516
1586
|
// why: partial attachment is observability-only and must never mask the error.
|
|
1517
1587
|
}
|
|
1518
1588
|
outerStream.fail(raw);
|
|
1519
1589
|
}
|
|
1520
1590
|
} finally {
|
|
1521
|
-
try {
|
|
1591
|
+
try {
|
|
1592
|
+
currentInner?.cancel();
|
|
1593
|
+
} catch {
|
|
1522
1594
|
// why: inner streams tolerate double-cancel; cleanup must not throw.
|
|
1523
1595
|
}
|
|
1524
|
-
try {
|
|
1596
|
+
try {
|
|
1597
|
+
userSignal?.removeEventListener("abort", forwardUserAbort);
|
|
1598
|
+
} catch {
|
|
1525
1599
|
// why: listener removal is best-effort cleanup.
|
|
1526
1600
|
}
|
|
1527
|
-
try {
|
|
1601
|
+
try {
|
|
1602
|
+
removeOuterCancel();
|
|
1603
|
+
} catch {
|
|
1528
1604
|
// why: unsubscribe is best-effort cleanup.
|
|
1529
1605
|
}
|
|
1530
1606
|
}
|
|
@@ -1550,10 +1626,13 @@ export function shouldPromoteThinkingToAnswer(config?: unknown, runOptions?: unk
|
|
|
1550
1626
|
if ((runOptions as { thinkingAsAnswerFallback?: unknown } | null | undefined)?.thinkingAsAnswerFallback === true) {
|
|
1551
1627
|
return true;
|
|
1552
1628
|
}
|
|
1553
|
-
const c = config as
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1629
|
+
const c = config as
|
|
1630
|
+
| {
|
|
1631
|
+
thinkingAsAnswerFallback?: unknown;
|
|
1632
|
+
runOptions?: { thinkingAsAnswerFallback?: unknown };
|
|
1633
|
+
}
|
|
1634
|
+
| null
|
|
1635
|
+
| undefined;
|
|
1557
1636
|
if (c?.thinkingAsAnswerFallback === true) return true;
|
|
1558
1637
|
if (c?.runOptions?.thinkingAsAnswerFallback === true) return true;
|
|
1559
1638
|
return false;
|