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.
Files changed (67) hide show
  1. package/README.md +3 -1
  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 +366 -161
  7. package/src/agent/architecture.md +13 -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 +444 -365
  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 +56 -27
  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 +39 -30
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +147 -59
  25. package/src/providers/openai-compat.ts +57 -49
  26. package/src/providers/openai.ts +54 -37
  27. package/src/providers/openrouter.ts +85 -30
  28. package/src/providers/registry.ts +6 -21
  29. package/src/providers/shared.ts +87 -33
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +7 -6
  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 +6 -14
  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 +15 -3
  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 };
@@ -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
 
@@ -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 { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
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: { 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;
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("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
+ );
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 = executable.length > 0
551
- ? await executeToolCalls({
552
- tools: options.tools,
553
- toolCalls: executable,
554
- agentName: options.agentName,
555
- parallel: true,
556
- signal: options.signal,
557
- sessionId: options.sessionId,
558
- onSubagentEvent: options.onSubagentEvent,
559
- onResult: options.onResult,
560
- openToolLog: options.openToolLog,
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({ 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
+ });
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
- const standardTools = toStandardToolDeclarations(tools);
757
+ const standardTools = toStandardToolDeclarations(tools);
702
758
 
703
- if (config.resume) {
704
- const finished = await finishInterruptedTools(config);
705
- stoppedByTool = finished.results.some((r) => r.stopped) || endsWithStop(context.messages);
706
- if (finished.calls.length > 0) {
707
- toolsExecuted = true;
708
- allToolCalls.push(...finished.calls);
709
- allToolResults.push(...finished.results);
710
- for (const meta of finished.subagents) {
711
- allSubagents.push(meta);
712
- 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
+ }
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
- 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 {}
778
800
 
779
- // Canonical session routing: lets adapters detect provider switches.
780
- noteProviderTurn(
781
- runOptions?.sessionId || options?.sessionId,
782
- provider.id
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
- // Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (default
786
- // OFF): without the opt-in the run returns the provider's empty text and finishReason.
787
- if ((!genResult.text || genResult.text.trim() === "") && (!genResult.toolCalls || genResult.toolCalls.length === 0) && genResult.thinking) {
788
- if (shouldPromoteThinkingToAnswer(config, runOptions)) {
789
- if (genResult.thinking.includes("</think>")) {
790
- const parts = genResult.thinking.split(/<\/(?:think|thought)>/i);
791
- genResult.thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
792
- genResult.text = parts.slice(1).join("").trim();
793
- } else {
794
- genResult.text = genResult.thinking;
795
- 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
+ }
796
837
  }
797
838
  }
798
- }
799
839
 
800
- accumulateUsage(accumulatedUsage, genResult.usage, spec, options?.serviceTier);
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
- telemetryBus.emit({
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
- // Mid-session steer: if new queries arrived during this provider turn,
835
- // inject them now. Tool turns still execute first (see below) so the
836
- // current action finishes before redirection.
837
- const noToolSteered = (!genResult.toolCalls || genResult.toolCalls.length === 0)
838
- ? drainSteerInbox(context, config.steerInbox)
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
- // If no tool calls, generation is complete — unless steered, in which
842
- // case the injected message extends the SAME run (no new response).
843
- if (!genResult.toolCalls || genResult.toolCalls.length === 0) {
844
- emitTurn(runOptions, { turns, text: genResult.text, thinking: genResult.thinking });
845
- if (noToolSteered.length > 0) {
846
- 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;
847
897
  }
848
- emitProgress(config);
849
- if (noToolSteered.length > 0) continue;
850
- break;
851
- }
852
898
 
853
- // Execute tool calls — always parallel (model-driven, bloatfree DX7)
854
- // Check abort before tools (throws AbortError with partial).
855
- throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
856
- emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
857
- allToolCalls.push(...genResult.toolCalls);
858
- // Per-run tool-call budget.
859
- if (allToolCalls.length > limits.maxToolCallsPerRun) {
860
- throw new BudgetExceededError(
861
- `[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
862
- );
863
- }
864
- toolsExecuted = true;
865
- const guarded = await executeToolCallsWithRepeatGuard({
866
- tools,
867
- toolCalls: genResult.toolCalls,
868
- previousFingerprints: previousToolCallFingerprints,
869
- repeatCounts,
870
- agentName,
871
- signal: runOptions?.signal,
872
- sessionId: runOptions?.sessionId || options?.sessionId,
873
- onResult: config.onToolResult,
874
- openToolLog: config.openToolLog,
875
- });
876
- const results = guarded.results;
877
- previousToolCallFingerprints = guarded.fingerprints;
878
-
879
- // Process results and extract any sub-agent execution metadata
880
- const sanitizedResults: ToolResultRecord[] = [];
881
-
882
- for (const r of results) {
883
- const { sanitized, metas } = sanitizeToolResult(r);
884
- if (metas.length > 0) {
885
- allSubagents.push(...metas);
886
- for (const s of metas) {
887
- if (s.raw) s.raw = applyCaptureRaw(s.raw, config.captureRaw);
888
- const subagentSpec = getModelFromCatalog(s.provider, s.model);
889
- if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
890
- 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);
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
- allToolResults.push(...sanitizedResults);
900
- context.addToolResults(sanitizedResults);
901
- checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
902
- // Results are stored BEFORE the cancellation check so an abort
903
- // never drops executed tool work from history.
904
- throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
905
- emitTurn(runOptions, {
906
- turns,
907
- text: genResult.text,
908
- thinking: genResult.thinking,
909
- toolCalls: genResult.toolCalls,
910
- toolResults: sanitizedResults,
911
- });
912
- // A tool ended the run (`ctx.stop`): the whole batch has finished, so stop here. Waiting steers become follow-ups.
913
- if (sanitizedResults.some((r) => r.stopped)) {
914
- 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
+ }
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
- // Abort always throws AbortError with partial attached (never
928
- // silently breaks).
929
- throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
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
- // maxTurns exhausted while the model still requested tools: the tool
933
- // results above were never sent back, so the run is truncated — surface
934
- // it via finishReason + warning instead of silently returning.
935
- const truncatedByMaxTurns =
936
- !stoppedByTool && turns >= maxTurns && !!finalResult?.toolCalls && finalResult.toolCalls.length > 0;
937
- if (truncatedByMaxTurns) {
938
- warnMaxTurnsHit(
939
- agentName,
940
- maxTurns,
941
- finalResult.toolCalls.map((c: ToolCallRecord) => c.name)
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
- // Structured outputs: validate final text only (tool turns skip this).
946
- // Strict-fail throws ValidationError with partial attached via catch below.
947
- let parsedOutput: unknown | undefined;
948
- if (loopOutput.raw !== undefined && !truncatedByMaxTurns && !stoppedByTool) {
949
- parsedOutput = parseFinalStructuredOutput(finalResult?.text || "", loopOutput.raw, loopOutput.spec);
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
- return new AgentResponse({
953
- text: finalResult?.text || "",
954
- ...(parsedOutput !== undefined ? { parsed: parsedOutput } : {}),
955
- thinking: finalResult?.thinking,
956
- thoughtSignature: finalResult?.thoughtSignature,
957
- toolCalls: allToolCalls,
958
- toolResults: allToolResults,
959
- subagents: allSubagents,
960
- usage: accumulatedUsage,
961
- responseId: finalResult?.responseId,
962
- model: modelId,
963
- provider: provider.id,
964
- // Canonical finish reason + raw vendor passthrough.
965
- ...(stoppedByTool
966
- ? { finishReason: "stopped" as const, ...(finalResult?.finishReason ? { rawFinishReason: finalResult.finishReason } : {}) }
967
- : canonicalFinishReason(finalResult?.finishReason, truncatedByMaxTurns)),
968
- durationMs: Date.now() - startTime,
969
- raw: applyCaptureRaw(finalResult?.raw, config.captureRaw) as ProviderRawData,
970
- turns,
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 { context.rollback(preRunCp, sessionIdForRollback); } catch {
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 { 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
+ }
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 { linked.abort(); } catch {}
1029
- try { currentInner?.cancel(); } catch {}
1082
+ try {
1083
+ linked.abort();
1084
+ } catch {}
1085
+ try {
1086
+ currentInner?.cancel();
1087
+ } catch {}
1030
1088
  });
1031
1089
  linked.signal.addEventListener("abort", () => {
1032
- try { currentInner?.cancel(); } catch {}
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 ? Infinity : first ? streamLimits.firstByteTimeoutMs : streamLimits.streamIdleTimeoutMs;
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(new TimeoutError(`[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
+ )
1238
+ );
1194
1239
  }, waitMs);
1195
- try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
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) { doneIter = true; break; }
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 { currentInner?.cancel(); } catch {}
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 ((!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
+ ) {
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 { config.onTurnUsage?.(turnResponse.usage, spec2); } catch {}
1253
- 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
+ });
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 = (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0)
1284
- ? drainSteerInbox(context, config.steerInbox)
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 { config.onSteerInjected?.(noToolSteered); } catch {
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
- repeatCounts,
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 { config.onSteerInjected?.(toolSteered); } catch {
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
- ? { finishReason: "stopped" as const, ...(lastResponse?.finishReason ? { rawFinishReason: lastResponse.finishReason } : {}) }
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 { (raw as unknown as { partial?: unknown }).partial ??= {
1437
- usage: accumulatedUsage,
1438
- cost: accumulatedUsage.cost,
1439
- turns,
1440
- toolCalls: allToolCalls,
1441
- toolResults: allToolResults,
1442
- subagents: allSubagents,
1443
- text: lastResponse?.text,
1444
- }; } 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 {}
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 { config.context.rollback(streamPreCp, streamSessionId); } catch {
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
- raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
1510
- 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 {
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 { (raw as unknown as { partial?: unknown }).partial ??= partial; } catch {
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 { currentInner?.cancel(); } catch {
1591
+ try {
1592
+ currentInner?.cancel();
1593
+ } catch {
1522
1594
  // why: inner streams tolerate double-cancel; cleanup must not throw.
1523
1595
  }
1524
- try { userSignal?.removeEventListener("abort", forwardUserAbort); } catch {
1596
+ try {
1597
+ userSignal?.removeEventListener("abort", forwardUserAbort);
1598
+ } catch {
1525
1599
  // why: listener removal is best-effort cleanup.
1526
1600
  }
1527
- try { removeOuterCancel(); } catch {
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
- thinkingAsAnswerFallback?: unknown;
1555
- runOptions?: { thinkingAsAnswerFallback?: unknown };
1556
- } | null | undefined;
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;