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.
Files changed (67) hide show
  1. package/README.md +26 -5
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +374 -167
  7. package/src/agent/architecture.md +14 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +8 -3
  10. package/src/agent/delegation.ts +82 -29
  11. package/src/agent/loop.ts +449 -373
  12. package/src/agent/subagent.ts +1 -3
  13. package/src/agent/subagents-tool.ts +45 -7
  14. package/src/agent/subagents.md +3 -0
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/worker.ts +58 -28
  17. package/src/agent/workers.ts +113 -20
  18. package/src/architecture.md +16 -15
  19. package/src/index.ts +34 -10
  20. package/src/models/architecture.md +4 -3
  21. package/src/models/catalog-cache.ts +6 -5
  22. package/src/models/catalog.ts +46 -49
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +150 -62
  25. package/src/providers/openai-compat.ts +60 -58
  26. package/src/providers/openai.ts +57 -39
  27. package/src/providers/openrouter.ts +87 -32
  28. package/src/providers/registry.ts +12 -26
  29. package/src/providers/shared.ts +89 -39
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +9 -8
  32. package/src/scheduler/scheduled-actions.md +4 -0
  33. package/src/scheduler/scheduler.ts +186 -60
  34. package/src/scheduler/store.ts +23 -10
  35. package/src/scheduler/time.ts +17 -3
  36. package/src/scheduler/tool.ts +33 -7
  37. package/src/session/architecture.md +7 -6
  38. package/src/session/journal.ts +38 -12
  39. package/src/session/store.ts +122 -61
  40. package/src/session/usage.ts +58 -9
  41. package/src/streaming/architecture.md +4 -3
  42. package/src/streaming/event-stream.ts +26 -12
  43. package/src/telemetry.ts +6 -2
  44. package/src/tools/architecture.md +8 -7
  45. package/src/tools/executor.ts +50 -48
  46. package/src/tools/schema.ts +10 -6
  47. package/src/tools/subagent-result.ts +6 -1
  48. package/src/tools/tool.ts +9 -11
  49. package/src/types/agent.ts +2 -21
  50. package/src/types/architecture.md +11 -10
  51. package/src/types/core.ts +1 -9
  52. package/src/types/errors.ts +26 -4
  53. package/src/types/model.ts +0 -4
  54. package/src/types/tool.ts +12 -2
  55. package/src/update-models.ts +5 -5
  56. package/src/utils/architecture.md +16 -15
  57. package/src/utils/cache.ts +1 -3
  58. package/src/utils/durable.ts +17 -5
  59. package/src/utils/env.ts +9 -7
  60. package/src/utils/errors.ts +50 -44
  61. package/src/utils/headers.ts +3 -10
  62. package/src/utils/media.ts +23 -8
  63. package/src/utils/raw-capture.ts +9 -2
  64. package/src/utils/retry.ts +17 -31
  65. package/src/utils/safe-io.ts +29 -7
  66. package/src/utils/serialization.ts +1 -4
  67. 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 { AgentResponse, normalizeFinishReason, type FinishReason, type SubAgentExecutionMetadata, type StreamEvent } from "../types/response.ts";
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: { turns: number; text?: string; thinking?: string; toolCalls?: ToolCallRecord[]; toolResults?: ToolResultRecord[] }
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 { config.onProgress?.(); } catch {}
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): { raw: unknown; spec: import("../types/model.ts").StructuredOutputSpec | undefined } {
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)?.output;
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(text: string, raw: unknown, spec: import("../types/model.ts").StructuredOutputSpec | undefined): unknown | undefined {
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(hit.message.content.length === 1 ? hit.message : { role: "tool", name: hit.part.name, content: [hit.part] });
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(config: AgentLoopConfig, fallbackMaxTurns: number): LoopLimits & { maxTurns: number; deadlineMs: number; maxToolCallsPerRun: number; maxTotalTokens: number } {
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 { maxTurns, deadlineMs, maxToolCallsPerRun, maxTotalTokens, requestTimeoutMs, firstByteTimeoutMs, streamIdleTimeoutMs };
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 { return undefined; }
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 { (base as { name: string }).name = "AbortError"; } catch {}
365
- try { (base as unknown as { partial: unknown }).partial = partial; } catch {}
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 Error(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
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
- const e = new Error(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`);
404
- e.name = "TimeoutError";
405
- reject(e);
440
+ reject(new TimeoutError(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`));
406
441
  }, timeoutMs);
407
- try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
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: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
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("call_blocked", "warn", call, { agentName: options.agentName, sessionId: options.sessionId }, { reason: "repeat_blocked" });
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 = executable.length > 0
553
- ? await executeToolCalls({
554
- tools: options.tools,
555
- toolCalls: executable,
556
- agentName: options.agentName,
557
- parallel: true,
558
- signal: options.signal,
559
- sessionId: options.sessionId,
560
- onSubagentEvent: options.onSubagentEvent,
561
- onResult: options.onResult,
562
- openToolLog: options.openToolLog,
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({ id: p.id, name: p.name, arguments: p.arguments, ...(p.rawArguments ? { rawArguments: p.rawArguments } : {}), ...(p.callId ? { callId: p.callId } : {}) });
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
- const standardTools = toStandardToolDeclarations(tools);
757
+ const standardTools = toStandardToolDeclarations(tools);
704
758
 
705
- if (config.resume) {
706
- const finished = await finishInterruptedTools(config);
707
- stoppedByTool = finished.results.some((r) => r.stopped) || endsWithStop(context.messages);
708
- if (finished.calls.length > 0) {
709
- toolsExecuted = true;
710
- allToolCalls.push(...finished.calls);
711
- allToolResults.push(...finished.results);
712
- for (const meta of finished.subagents) {
713
- allSubagents.push(meta);
714
- accumulateUsage(accumulatedUsage, meta.usage, getModelFromCatalog(meta.provider, meta.model));
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
- finalResult = genResult;
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
- // Canonical session routing: lets adapters detect provider switches.
782
- noteProviderTurn(
783
- runOptions?.sessionId || options?.sessionId,
784
- provider.id
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
- // Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (default
788
- // OFF): without the opt-in the run returns the provider's empty text and finishReason.
789
- if ((!genResult.text || genResult.text.trim() === "") && (!genResult.toolCalls || genResult.toolCalls.length === 0) && genResult.thinking) {
790
- if (shouldPromoteThinkingToAnswer(config, runOptions)) {
791
- if (genResult.thinking.includes("</think>")) {
792
- const parts = genResult.thinking.split(/<\/(?:think|thought)>/i);
793
- genResult.thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
794
- genResult.text = parts.slice(1).join("").trim();
795
- } else {
796
- genResult.text = genResult.thinking;
797
- genResult.thinking = undefined;
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
- accumulateUsage(accumulatedUsage, genResult.usage, spec, options?.serviceTier);
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
- telemetryBus.emit({
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
- // Mid-session steer: if new queries arrived during this provider turn,
837
- // inject them now. Tool turns still execute first (see below) so the
838
- // current action finishes before redirection.
839
- const noToolSteered = (!genResult.toolCalls || genResult.toolCalls.length === 0)
840
- ? drainSteerInbox(context, config.steerInbox)
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
- // If no tool calls, generation is complete — unless steered, in which
844
- // case the injected message extends the SAME run (no new response).
845
- if (!genResult.toolCalls || genResult.toolCalls.length === 0) {
846
- emitTurn(runOptions, { turns, text: genResult.text, thinking: genResult.thinking });
847
- if (noToolSteered.length > 0) {
848
- try { config.onSteerInjected?.(noToolSteered); } catch {}
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
- // Execute tool calls — always parallel (model-driven, bloatfree DX7)
856
- // Check abort before tools (throws AbortError with partial).
857
- throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
858
- emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
859
- allToolCalls.push(...genResult.toolCalls);
860
- // Per-run tool-call budget.
861
- if (allToolCalls.length > limits.maxToolCallsPerRun) {
862
- throw new Error(
863
- `[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
864
- );
865
- }
866
- toolsExecuted = true;
867
- const guarded = await executeToolCallsWithRepeatGuard({
868
- tools,
869
- toolCalls: genResult.toolCalls,
870
- previousFingerprints: previousToolCallFingerprints,
871
- repeatCounts,
872
- agentName,
873
- signal: runOptions?.signal,
874
- sessionId: runOptions?.sessionId || options?.sessionId,
875
- onResult: config.onToolResult,
876
- openToolLog: config.openToolLog,
877
- });
878
- const results = guarded.results;
879
- previousToolCallFingerprints = guarded.fingerprints;
880
-
881
- // Process results and extract any sub-agent execution metadata
882
- const sanitizedResults: ToolResultRecord[] = [];
883
-
884
- for (const r of results) {
885
- const { sanitized, metas } = sanitizeToolResult(r);
886
- if (metas.length > 0) {
887
- allSubagents.push(...metas);
888
- for (const s of metas) {
889
- if (s.raw) s.raw = applyCaptureRaw(s.raw, config.captureRaw);
890
- const subagentSpec = getModelFromCatalog(s.provider, s.model);
891
- if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
892
- s.usage.cost = computeCostFromPricing(s.usage, subagentSpec);
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
- allToolResults.push(...sanitizedResults);
902
- context.addToolResults(sanitizedResults);
903
- checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
904
- // Results are stored BEFORE the cancellation check so an abort
905
- // never drops executed tool work from history.
906
- throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
907
- emitTurn(runOptions, {
908
- turns,
909
- text: genResult.text,
910
- thinking: genResult.thinking,
911
- toolCalls: genResult.toolCalls,
912
- toolResults: sanitizedResults,
913
- });
914
- // A tool ended the run (`ctx.stop`): the whole batch has finished, so stop here. Waiting steers become follow-ups.
915
- if (sanitizedResults.some((r) => r.stopped)) {
916
- stoppedByTool = true;
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
- // Abort always throws AbortError with partial attached (never
930
- // silently breaks).
931
- throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
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
- // maxTurns exhausted while the model still requested tools: the tool
935
- // results above were never sent back, so the run is truncated — surface
936
- // it via finishReason + warning instead of silently returning.
937
- const truncatedByMaxTurns =
938
- !stoppedByTool && turns >= maxTurns && !!finalResult?.toolCalls && finalResult.toolCalls.length > 0;
939
- if (truncatedByMaxTurns) {
940
- warnMaxTurnsHit(
941
- agentName,
942
- maxTurns,
943
- finalResult.toolCalls.map((c: ToolCallRecord) => c.name)
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
- // Structured outputs: validate final text only (tool turns skip this).
948
- // Strict-fail throws ValidationError with partial attached via catch below.
949
- let parsedOutput: unknown | undefined;
950
- if (loopOutput.raw !== undefined && !truncatedByMaxTurns && !stoppedByTool) {
951
- parsedOutput = parseFinalStructuredOutput(finalResult?.text || "", loopOutput.raw, loopOutput.spec);
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
- return new AgentResponse({
955
- text: finalResult?.text || "",
956
- ...(parsedOutput !== undefined ? { parsed: parsedOutput } : {}),
957
- thinking: finalResult?.thinking,
958
- thoughtSignature: finalResult?.thoughtSignature,
959
- toolCalls: allToolCalls,
960
- toolResults: allToolResults,
961
- subagents: allSubagents,
962
- usage: accumulatedUsage,
963
- responseId: finalResult?.responseId,
964
- model: modelId,
965
- provider: provider.id,
966
- // Canonical finish reason + raw vendor passthrough.
967
- ...(stoppedByTool
968
- ? { finishReason: "stopped" as const, ...(finalResult?.finishReason ? { rawFinishReason: finalResult.finishReason } : {}) }
969
- : canonicalFinishReason(finalResult?.finishReason, truncatedByMaxTurns)),
970
- durationMs: Date.now() - startTime,
971
- raw: applyCaptureRaw(finalResult?.raw, config.captureRaw) as ProviderRawData,
972
- turns,
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 { context.rollback(preRunCp, sessionIdForRollback); } catch {
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 { linked.abort((userSignal as any)?.reason); } catch { try { linked.abort(); } catch {} }
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 { linked.abort(); } catch {}
1030
- try { currentInner?.cancel(); } catch {}
1082
+ try {
1083
+ linked.abort();
1084
+ } catch {}
1085
+ try {
1086
+ currentInner?.cancel();
1087
+ } catch {}
1031
1088
  });
1032
1089
  linked.signal.addEventListener("abort", () => {
1033
- try { currentInner?.cancel(); } catch {}
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 ? Infinity : first ? streamLimits.firstByteTimeoutMs : streamLimits.streamIdleTimeoutMs;
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
- const e = new Error(
1195
- `[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`
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 { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
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) { doneIter = true; break; }
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 { currentInner?.cancel(); } catch {}
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 ((!turnResponse.text || turnResponse.text.trim() === "") && (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) && turnResponse.thinking) {
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 { config.onTurnUsage?.(turnResponse.usage, spec2); } catch {}
1258
- config.enforceBudget?.({ ...turnResponse.usage, cost: computeCostFromPricing(turnResponse.usage, spec2, options?.serviceTier) });
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 = (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0)
1289
- ? drainSteerInbox(context, config.steerInbox)
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 { config.onSteerInjected?.(noToolSteered); } catch {
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 Error(
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
- repeatCounts,
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 { config.onSteerInjected?.(toolSteered); } catch {
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
- ? { finishReason: "stopped" as const, ...(lastResponse?.finishReason ? { rawFinishReason: lastResponse.finishReason } : {}) }
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 { (raw as unknown as { partial?: unknown }).partial ??= {
1442
- usage: accumulatedUsage,
1443
- cost: accumulatedUsage.cost,
1444
- turns,
1445
- toolCalls: allToolCalls,
1446
- toolResults: allToolResults,
1447
- text: lastResponse?.text,
1448
- }; } catch {}
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 { config.context.rollback(streamPreCp, streamSessionId); } catch {
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
- raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
1513
- try { (abortErr as unknown as { partial?: unknown }).partial ??= partial; } catch {
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 { (raw as unknown as { partial?: unknown }).partial ??= partial; } catch {
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 { currentInner?.cancel(); } catch {
1591
+ try {
1592
+ currentInner?.cancel();
1593
+ } catch {
1525
1594
  // why: inner streams tolerate double-cancel; cleanup must not throw.
1526
1595
  }
1527
- try { userSignal?.removeEventListener("abort", forwardUserAbort); } catch {
1596
+ try {
1597
+ userSignal?.removeEventListener("abort", forwardUserAbort);
1598
+ } catch {
1528
1599
  // why: listener removal is best-effort cleanup.
1529
1600
  }
1530
- try { removeOuterCancel(); } catch {
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
- thinkingAsAnswerFallback?: unknown;
1558
- runOptions?: { thinkingAsAnswerFallback?: unknown };
1559
- } | null | undefined;
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;