agent-accelerator 0.5.9 → 0.6.5

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