agent-accelerator 0.5.6 → 0.5.8

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 (57) hide show
  1. package/README.md +10 -17
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +92 -57
  4. package/src/agent/architecture.md +25 -0
  5. package/src/agent/context.ts +7 -3
  6. package/src/agent/delegation.ts +30 -96
  7. package/src/agent/loop.ts +54 -56
  8. package/src/agent/subagent.ts +4 -4
  9. package/src/agent/worker.ts +1 -3
  10. package/src/architecture.md +28 -0
  11. package/src/index.ts +13 -80
  12. package/src/models/architecture.md +14 -0
  13. package/src/models/catalog-cache.ts +10 -7
  14. package/src/models/catalog.ts +20 -13
  15. package/src/providers/architecture.md +24 -0
  16. package/src/providers/google.ts +24 -4
  17. package/src/providers/openai-compat.ts +29 -7
  18. package/src/providers/openai.ts +22 -6
  19. package/src/providers/openrouter.ts +14 -2
  20. package/src/providers/registry.ts +1 -0
  21. package/src/providers/shared.ts +4 -4
  22. package/src/providers.ts +9 -53
  23. package/src/scheduler/architecture.md +22 -0
  24. package/src/scheduler/scheduler.ts +131 -56
  25. package/src/scheduler/store.ts +274 -54
  26. package/src/scheduler/time.ts +2 -0
  27. package/src/session/architecture.md +19 -0
  28. package/src/session/store.ts +37 -22
  29. package/src/session/usage.ts +1 -0
  30. package/src/streaming/architecture.md +13 -0
  31. package/src/streaming/event-stream.ts +16 -11
  32. package/src/telemetry-file.ts +74 -24
  33. package/src/telemetry.ts +3 -2
  34. package/src/tools/architecture.md +19 -0
  35. package/src/tools/executor.ts +14 -1
  36. package/src/tools/schema.ts +1 -0
  37. package/src/tools/subagent-result.ts +1 -0
  38. package/src/tools/tool.ts +8 -8
  39. package/src/types/agent.ts +22 -20
  40. package/src/types/architecture.md +20 -0
  41. package/src/types/response.ts +8 -8
  42. package/src/types/scheduler.ts +2 -2
  43. package/src/types/tool.ts +5 -5
  44. package/src/update-models.ts +2 -2
  45. package/src/utils/architecture.md +25 -0
  46. package/src/utils/base64.ts +2 -0
  47. package/src/utils/cache.ts +6 -4
  48. package/src/utils/durable.ts +78 -3
  49. package/src/utils/env.ts +1 -1
  50. package/src/utils/errors.ts +4 -0
  51. package/src/utils/headers.ts +7 -4
  52. package/src/utils/home.ts +1 -1
  53. package/src/utils/media.ts +1 -0
  54. package/src/utils/raw-capture.ts +3 -0
  55. package/src/utils/retry.ts +2 -1
  56. package/src/utils/safe-io.ts +4 -0
  57. package/src/utils/session.ts +10 -166
package/src/agent/loop.ts CHANGED
@@ -25,6 +25,7 @@ import { enforceFileAccess } from "../utils/safe-io.ts";
25
25
  import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-result.ts";
26
26
  import { BudgetExceededError } from "../types/errors.ts";
27
27
  import { applyCaptureRaw, type CaptureRawMode } from "../utils/raw-capture.ts";
28
+ import { previewPrompt } from "../utils/session.ts";
28
29
  import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
29
30
 
30
31
  /** One mid-session steer request buffered while a run is active. */
@@ -99,20 +100,6 @@ export interface AgentLoopConfig {
99
100
  output?: unknown;
100
101
  }
101
102
 
102
- /** Short preview for `steer_injected` / `queued` event payloads (bounded, display-only). */
103
- function steerPreview(prompt: string | ContentPart[]): string {
104
- if (typeof prompt === "string") return prompt.slice(0, 500);
105
- try {
106
- const texts = prompt
107
- .filter((p) => (p as { type?: unknown }).type === "text")
108
- .map((p) => (p as { text?: string }).text ?? "")
109
- .join("\n");
110
- return (texts || "[multipart]").slice(0, 500);
111
- } catch {
112
- return "[multipart]";
113
- }
114
- }
115
-
116
103
  /**
117
104
  * Drains pending steer entries into the conversation as new user messages.
118
105
  * Returns display previews for events/telemetry. Never throws: invalid
@@ -133,7 +120,7 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
133
120
  else prompt = [{ type: "text", text: prefix } as ContentPart, ...prompt];
134
121
  }
135
122
  context.addUserMessage(prompt);
136
- injected.push(steerPreview(entry.prompt));
123
+ injected.push(previewPrompt(entry.prompt));
137
124
  } catch {
138
125
  continue;
139
126
  }
@@ -141,6 +128,7 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
141
128
  return injected;
142
129
  }
143
130
 
131
+ // Reports one finished turn to the caller's onTurn hook (ids only), never throwing; in: both loops, out: AgentRunOptions.onTurn.
144
132
  function emitTurn(
145
133
  runOptions: AgentRunOptions | undefined,
146
134
  turn: { turns: number; text?: string; thinking?: string; toolCalls?: ToolCallRecord[]; toolResults?: ToolResultRecord[] }
@@ -183,6 +171,7 @@ function shouldRollbackOnFailure(
183
171
  return !toolsExecuted;
184
172
  }
185
173
 
174
+ // Calls the persistence checkpoint hook, never throwing; in: both loops after each step, out: Agent.persistNow.
186
175
  function emitProgress(config: { onProgress?: () => void }): void {
187
176
  try { config.onProgress?.(); } catch {}
188
177
  }
@@ -191,6 +180,7 @@ function emitProgress(config: { onProgress?: () => void }): void {
191
180
  // Structured outputs (canonical output wiring for the loop).
192
181
  // ---------------------------------------------------------------------------
193
182
 
183
+ // Normalizes the structured-output option once per run (raw value + provider spec); in: AgentLoopConfig.output, out: the final-answer validation.
194
184
  function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: import("../types/model.ts").StructuredOutputSpec | undefined } {
195
185
  const raw = (config as { output?: unknown }).output;
196
186
  const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)?.output;
@@ -204,6 +194,7 @@ function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: impor
204
194
  return { raw: undefined, spec: undefined };
205
195
  }
206
196
 
197
+ // Validates the final text against the structured-output option, throwing ValidationError on mismatch; in: the last model text, out: AgentResponse.parsed.
207
198
  function parseFinalStructuredOutput(text: string, raw: unknown, spec: import("../types/model.ts").StructuredOutputSpec | undefined): unknown | undefined {
208
199
  if (raw === undefined && spec === undefined) return undefined;
209
200
  // Strict-fail when opted in: invalid JSON/schema throws ValidationError.
@@ -232,7 +223,7 @@ function normalizeMaxTurns(value?: number): number {
232
223
  function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendingTools: string[]): void {
233
224
  const who = agentName ? `"${agentName}"` : "Agent";
234
225
  const tools = pendingTools.length > 0 ? ` Pending tool calls dropped: ${pendingTools.join(", ")}.` : "";
235
- // Q-41: no console.* in library code — route via the telemetry bus
226
+ // No console.* in library code — route via the telemetry bus
236
227
  // (default no-op; CLIs attach a pretty logger via telemetryBus.onEvent).
237
228
  try {
238
229
  telemetryBus.emitWarning(
@@ -246,11 +237,11 @@ function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendin
246
237
  }
247
238
 
248
239
  // ---------------------------------------------------------------------------
249
- // Q-19: tool-call/result pairing repair.
240
+ // Tool-call/result pairing repair.
250
241
  // ---------------------------------------------------------------------------
251
242
 
252
243
  /**
253
- * Repairs tool call/result pairing before each provider request (Q-19).
244
+ * Repairs tool call/result pairing before each provider request.
254
245
  *
255
246
  * Providers reject a `tool_result` without its `tool_call`, a `tool_call`
256
247
  * without its result, AND (Chat-Completions style APIs) results that are not
@@ -315,7 +306,7 @@ export function ensureToolPairing(context: AgentContext): void {
315
306
  }
316
307
 
317
308
  // ---------------------------------------------------------------------------
318
- // Q-21/Q-22: abort + limits helpers.
309
+ // Abort + limits helpers.
319
310
  // ---------------------------------------------------------------------------
320
311
 
321
312
  export interface LoopLimits {
@@ -328,7 +319,7 @@ export interface LoopLimits {
328
319
  streamIdleTimeoutMs: number;
329
320
  }
330
321
 
331
- /** Resolves `(config as any).limits` with Q-22 defaults (timeouts tolerate reasoning). */
322
+ /** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
332
323
  function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): LoopLimits & { maxTurns: number; deadlineMs: number; maxToolCallsPerRun: number; maxTotalTokens: number } {
333
324
  const raw = ((config as unknown as { limits?: Record<string, unknown> }).limits ?? {}) as Record<string, unknown>;
334
325
  const num = (v: unknown): number | undefined =>
@@ -350,6 +341,7 @@ function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): L
350
341
  return { maxTurns, deadlineMs, maxToolCallsPerRun, maxTotalTokens, requestTimeoutMs, firstByteTimeoutMs, streamIdleTimeoutMs };
351
342
  }
352
343
 
344
+ // The session id a run's provider calls use (run override, else agent default); in: AgentLoopConfig, out: rollback and routing.
353
345
  function loopSessionId(config: AgentLoopConfig): string | undefined {
354
346
  try {
355
347
  return (
@@ -359,6 +351,7 @@ function loopSessionId(config: AgentLoopConfig): string | undefined {
359
351
  } catch { return undefined; }
360
352
  }
361
353
 
354
+ // Builds an AbortError that carries the run's partial progress; in: an abort reason + partial usage, out: the error the run rejects with.
362
355
  function makeAbortError(
363
356
  partial: { usage: TokenUsage; turns: number; text?: string },
364
357
  reason?: unknown
@@ -373,6 +366,7 @@ function makeAbortError(
373
366
  return base;
374
367
  }
375
368
 
369
+ // Throws the run's AbortError as soon as its signal is aborted; in: loop checkpoints, out: nothing or a thrown abort.
376
370
  function throwIfAborted(
377
371
  signal: AbortSignal | undefined,
378
372
  partial: { usage: TokenUsage; turns: number; text?: string }
@@ -382,6 +376,7 @@ function throwIfAborted(
382
376
  }
383
377
  }
384
378
 
379
+ // Throws BudgetExceededError when the run's tokens pass limits.maxTotalTokens; in: both loops after usage changes, out: nothing or a thrown error.
385
380
  function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
386
381
  if (Number.isFinite(maxTotalTokens) && usage.totalTokens > maxTotalTokens) {
387
382
  throw new BudgetExceededError(
@@ -391,12 +386,14 @@ function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
391
386
  }
392
387
  }
393
388
 
389
+ // Throws when the run has been going longer than limits.deadlineMs; in: both loops each turn, out: nothing or a thrown error.
394
390
  function checkDeadline(startTime: number, deadlineMs: number): void {
395
391
  if (Number.isFinite(deadlineMs) && deadlineMs !== Infinity && Date.now() - startTime > deadlineMs) {
396
392
  throw new Error(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
397
393
  }
398
394
  }
399
395
 
396
+ // Races one provider call against the request timeout (a TimeoutError), clearing its timer either way; in: runAgentLoop, out: the call's result.
400
397
  async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number): Promise<T> {
401
398
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs === Infinity) return task();
402
399
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -439,7 +436,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
439
436
  }
440
437
 
441
438
  // ---------------------------------------------------------------------------
442
- // Q-35: canonical usage accumulation (pure w.r.t. source).
439
+ // Canonical usage accumulation (pure w.r.t. source).
443
440
  // Invariants (see assertUsageInvariants in session/store.ts):
444
441
  // - token counts are finite integers >= 0;
445
442
  // - cachedTokens/cacheReadTokens are a SUBSET of inputTokens (clamped below
@@ -447,7 +444,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
447
444
  // - thinkingTokens is a SUBSET of outputTokens when both are reported
448
445
  // (reasoning is generated output; enforced in the test helper, documented
449
446
  // here because providers vary on which side they omit).
450
- // This function never mutates `source` (Q-35): the priced cost is cloned
447
+ // This function never mutates `source`: the priced cost is cloned
451
448
  // into a local before folding into the target.
452
449
  // ---------------------------------------------------------------------------
453
450
  export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
@@ -468,7 +465,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
468
465
  target.cachedTokens = Math.min(preCached, target.inputTokens);
469
466
  target.cacheReadTokens = Math.min(preRead, target.inputTokens);
470
467
  if ((target.cachedTokens ?? 0) < preCached || (target.cacheReadTokens ?? 0) < preRead) {
471
- // Q-35: no console — report via the anomaly hook + telemetry bus.
468
+ // No console — report via the anomaly hook + telemetry bus.
472
469
  reportUsageAnomaly({
473
470
  type: "cache_clamp",
474
471
  inputTokens: target.inputTokens,
@@ -478,7 +475,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
478
475
  }
479
476
 
480
477
  const priced = computeCostFromPricing(source, spec, serviceTier);
481
- // Q-35: pure — read `source` only; never assign back to `source.cost`.
478
+ // Pure — read `source` only; never assign back to `source.cost`.
482
479
  // Clone the priced cost so later target folds cannot alias source state.
483
480
  const sourceCost = priced ? { ...priced } : undefined;
484
481
 
@@ -493,6 +490,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
493
490
  }
494
491
  }
495
492
 
493
+ // Turns a tool result into what the model sees: a branded delegation envelope becomes its text and yields validated worker metadata, anything else passes through; in: executed results, out: history + usage roll-up.
496
494
  function sanitizeToolResult(r: ToolResultRecord): { sanitized: ToolResultRecord; metas: SubAgentExecutionMetadata[] } {
497
495
  // Only SDK-branded delegation envelopes are trusted. A plain object that
498
496
  // merely carries a `_subagentMetadata` key (e.g. scraped JSON) is ordinary
@@ -675,11 +673,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
675
673
  const maxTurns = limits.maxTurns;
676
674
  // Structured outputs: normalize once (throws ValidationError for bad shapes).
677
675
  const loopOutput = resolveLoopOutput(config);
678
- // Q-19: pre-run checkpoint for onFailure rollback.
676
+ // Pre-run checkpoint for onFailure rollback.
679
677
  const preRunCp = context.checkpoint();
680
678
  const sessionIdForRollback = loopSessionId(config);
681
679
 
682
- // Q-37: partial-run state hoisted outside try so the catch block can attach
680
+ // Partial-run state hoisted outside try so the catch block can attach
683
681
  // {usage,cost,turns,toolCalls,toolResults,text} to ANY thrown error.
684
682
  const startTime = Date.now();
685
683
  const accumulatedUsage: TokenUsage = {
@@ -719,11 +717,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
719
717
  }
720
718
 
721
719
  while (!stoppedByTool && turns < maxTurns) {
722
- // Q-21: fail fast on abort (throws AbortError with partial).
720
+ // Fail fast on abort (throws AbortError with partial).
723
721
  throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
724
722
  checkDeadline(startTime, limits.deadlineMs);
725
723
  config.enforceBudget?.();
726
- // Q-19: repair orphan tool pairs before every request.
724
+ // Repair orphan tool pairs before every request.
727
725
  ensureToolPairing(context);
728
726
  if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
729
727
  turns++;
@@ -739,13 +737,13 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
739
737
  toolChoice: toolChoiceForTurn(options?.toolChoice, turns),
740
738
  ...(loopOutput.spec ? { output: loopOutput.spec } : {}),
741
739
  };
742
- // Q-22: per-request timeout pass-through (providers honor timeoutMs).
740
+ // Per-request timeout pass-through (providers honor timeoutMs).
743
741
  try {
744
742
  if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
745
743
  (providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
746
744
  }
747
745
  } catch {}
748
- // Q-25-partial: google:{store:false} opt-out pass-through (provider
746
+ // google:{store:false} opt-out pass-through (provider
749
747
  // already respects body.store===false for chaining).
750
748
  try {
751
749
  const gStore =
@@ -786,8 +784,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
786
784
  provider.id
787
785
  );
788
786
 
789
- // Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (Q-15,
790
- // default OFF): without the opt-in the run returns the provider's empty text and finishReason.
787
+ // Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (default
788
+ // OFF): without the opt-in the run returns the provider's empty text and finishReason.
791
789
  if ((!genResult.text || genResult.text.trim() === "") && (!genResult.toolCalls || genResult.toolCalls.length === 0) && genResult.thinking) {
792
790
  if (shouldPromoteThinkingToAnswer(config, runOptions)) {
793
791
  if (genResult.thinking.includes("</think>")) {
@@ -855,11 +853,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
855
853
  }
856
854
 
857
855
  // Execute tool calls — always parallel (model-driven, bloatfree DX7)
858
- // Q-21: check abort before tools (throws AbortError with partial).
856
+ // Check abort before tools (throws AbortError with partial).
859
857
  throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
860
858
  emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
861
859
  allToolCalls.push(...genResult.toolCalls);
862
- // Q-22: per-run tool-call budget.
860
+ // Per-run tool-call budget.
863
861
  if (allToolCalls.length > limits.maxToolCallsPerRun) {
864
862
  throw new Error(
865
863
  `[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
@@ -903,7 +901,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
903
901
  allToolResults.push(...sanitizedResults);
904
902
  context.addToolResults(sanitizedResults);
905
903
  checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
906
- // Q-19: results are stored BEFORE the cancellation check so an abort
904
+ // Results are stored BEFORE the cancellation check so an abort
907
905
  // never drops executed tool work from history.
908
906
  throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
909
907
  emitTurn(runOptions, {
@@ -928,7 +926,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
928
926
  }
929
927
  emitProgress(config);
930
928
 
931
- // Q-21: abort always throws AbortError with partial attached (never
929
+ // Abort always throws AbortError with partial attached (never
932
930
  // silently breaks).
933
931
  throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
934
932
  }
@@ -965,7 +963,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
965
963
  responseId: finalResult?.responseId,
966
964
  model: modelId,
967
965
  provider: provider.id,
968
- // Q-04: canonical finish reason + raw vendor passthrough.
966
+ // Canonical finish reason + raw vendor passthrough.
969
967
  ...(stoppedByTool
970
968
  ? { finishReason: "stopped" as const, ...(finalResult?.finishReason ? { rawFinishReason: finalResult.finishReason } : {}) }
971
969
  : canonicalFinishReason(finalResult?.finishReason, truncatedByMaxTurns)),
@@ -974,7 +972,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
974
972
  turns,
975
973
  });
976
974
  } catch (err) {
977
- // Q-19: run-level onFailure rollback (default restores pre-run
975
+ // Run-level onFailure rollback (default restores pre-run
978
976
  // checkpoint; opt out with (config as any).onFailure === "keep").
979
977
  // Aborts are excluded so partial history survives interruption.
980
978
  const isAbort =
@@ -985,14 +983,14 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
985
983
  // why: rollback is best-effort — the original error takes precedence.
986
984
  }
987
985
  }
988
- // Q-21: ensure aborts surface as AbortError with partial attached.
986
+ // Ensure aborts surface as AbortError with partial attached.
989
987
  if (isAbort && (err as { name?: string } | null)?.name !== "AbortError") {
990
988
  throw makeAbortError(
991
989
  { usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 },
992
990
  err
993
991
  );
994
992
  }
995
- // Q-37: every failure carries its partial progress for observability.
993
+ // Every failure carries its partial progress for observability.
996
994
  try {
997
995
  (err as unknown as { partial?: unknown }).partial ??= {
998
996
  usage: accumulatedUsage,
@@ -1044,7 +1042,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1044
1042
  }
1045
1043
  };
1046
1044
 
1047
- // Q-19/Q-22: per-run checkpoint + limits for the streaming loop.
1045
+ // Per-run checkpoint + limits for the streaming loop.
1048
1046
  const streamPreCp = config.context.checkpoint();
1049
1047
  const streamSessionId = loopSessionId(config);
1050
1048
  const streamFallbackMax = normalizeMaxTurns((config as { maxTurns?: number }).maxTurns);
@@ -1052,7 +1050,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1052
1050
  const streamStartTime = startTime;
1053
1051
 
1054
1052
  (async () => {
1055
- // Hoisted for catch-block partial reporting (Q-21/Q-37).
1053
+ // Hoisted for catch-block partial reporting.
1056
1054
  let accumulatedUsage: TokenUsage = {
1057
1055
  inputTokens: 0,
1058
1056
  outputTokens: 0,
@@ -1064,7 +1062,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1064
1062
  };
1065
1063
  let lastResponse: AgentResponse | null = null;
1066
1064
  let turns = 0;
1067
- // Q-37: tool progress hoisted so failures carry toolCalls/toolResults.
1065
+ // Tool progress hoisted so failures carry toolCalls/toolResults.
1068
1066
  const allToolCalls: ToolCallRecord[] = [];
1069
1067
  const allToolResults: ToolResultRecord[] = [];
1070
1068
  const allSubagents: SubAgentExecutionMetadata[] = [];
@@ -1120,7 +1118,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1120
1118
  thinkingTokens: 0,
1121
1119
  };
1122
1120
 
1123
- // Q-37: allToolCalls/allToolResults/allSubagents are hoisted above (fresh
1121
+ // allToolCalls/allToolResults/allSubagents are hoisted above (fresh
1124
1122
  // per stream run); just reset turn counters here.
1125
1123
  lastResponse = null;
1126
1124
  turns = 0;
@@ -1178,7 +1176,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1178
1176
  );
1179
1177
  currentInner = innerStream as AssistantMessageEventStream;
1180
1178
 
1181
- // Q-22: first-byte + idle timeouts around SSE (tolerate reasoning).
1179
+ // First-byte + idle timeouts around SSE (tolerate reasoning).
1182
1180
  // Implemented as races on the inner iterator so a hung provider
1183
1181
  // surfaces as TimeoutError instead of hanging the run forever.
1184
1182
  {
@@ -1240,7 +1238,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1240
1238
  );
1241
1239
 
1242
1240
  // Thinking stays on events/traces + onTurn (default exposed). Promoting
1243
- // it to answer text is gated (Q-15, default OFF): without the opt-in
1241
+ // it to answer text is gated (default OFF): without the opt-in
1244
1242
  // the turn keeps the provider's empty text and finishReason.
1245
1243
  if ((!turnResponse.text || turnResponse.text.trim() === "") && (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) && turnResponse.thinking) {
1246
1244
  if (shouldPromoteThinkingToAnswer(config, runOptions)) {
@@ -1317,7 +1315,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1317
1315
  `[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${streamLimits.maxToolCallsPerRun}).`
1318
1316
  );
1319
1317
  }
1320
- // Q-40: typed tool lifecycle for streaming consumers.
1318
+ // Typed tool lifecycle for streaming consumers.
1321
1319
  try {
1322
1320
  outerStream.push({ type: "tool_start", toolCalls: turnResponse.toolCalls });
1323
1321
  } catch {
@@ -1381,14 +1379,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1381
1379
  toolResult: res,
1382
1380
  });
1383
1381
  }
1384
- // Q-40: close the tool lifecycle opened by `tool_start` above.
1382
+ // Close the tool lifecycle opened by `tool_start` above.
1385
1383
  try {
1386
1384
  outerStream.push({ type: "tool_end", toolResults: sanitizedResults });
1387
1385
  } catch {
1388
1386
  // why: stream push never throws, but guard anyway to protect the loop.
1389
1387
  }
1390
1388
 
1391
- // Q-19: store tool results BEFORE the cancellation check.
1389
+ // Store tool results BEFORE the cancellation check.
1392
1390
  allToolResults.push(...sanitizedResults);
1393
1391
  context.addToolResults(sanitizedResults);
1394
1392
  throwIfCancelled({ usage: accumulatedUsage, turns, text: turnResponse.text });
@@ -1465,7 +1463,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1465
1463
  responseId: lastResponse?.responseId,
1466
1464
  model: modelId,
1467
1465
  provider: provider.id,
1468
- // Q-04: canonical finish reason + raw vendor passthrough.
1466
+ // Canonical finish reason + raw vendor passthrough.
1469
1467
  ...truncatedFinish,
1470
1468
  durationMs: Date.now() - startTime,
1471
1469
  raw: applyCaptureRaw(lastResponse?.raw, config.captureRaw) as ProviderRawData,
@@ -1488,7 +1486,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1488
1486
  linked.signal.aborted ||
1489
1487
  outerStream.isCancelled() ||
1490
1488
  (raw as any)?.name === "AbortError";
1491
- // Q-19: streaming onFailure rollback (same policy as runAgentLoop;
1489
+ // Streaming onFailure rollback (same policy as runAgentLoop;
1492
1490
  // aborts excluded so partial history survives).
1493
1491
  try {
1494
1492
  if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
@@ -1499,7 +1497,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1499
1497
  } catch {
1500
1498
  // why: rollback policy itself must never mask the original error.
1501
1499
  }
1502
- // Q-37: every failure carries its partial progress for observability.
1500
+ // Every failure carries its partial progress for observability.
1503
1501
  const partial = {
1504
1502
  usage: accumulatedUsage,
1505
1503
  cost: accumulatedUsage.cost,
@@ -1508,7 +1506,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1508
1506
  toolResults: allToolResults,
1509
1507
  text: lastResponse?.text,
1510
1508
  };
1511
- // Q-21: aborts always surface as AbortError with partial attached.
1509
+ // Aborts always surface as AbortError with partial attached.
1512
1510
  if (isAbort) {
1513
1511
  const abortErr =
1514
1512
  raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
@@ -1539,12 +1537,12 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1539
1537
  }
1540
1538
 
1541
1539
  // ---------------------------------------------------------------------------
1542
- // Q-15: thinking-promotion policy + Q-04 canonical finish helper.
1540
+ // Thinking-promotion policy + canonical finish helper.
1543
1541
  // (Appended here to avoid conflicts with other loop sections.)
1544
1542
  // ---------------------------------------------------------------------------
1545
1543
 
1546
1544
  /**
1547
- * Whether an empty-text turn may promote `thinking` to answer `text` (Q-15).
1545
+ * Whether an empty-text turn may promote `thinking` to answer `text`.
1548
1546
  * Default false: the run returns the provider's empty text with its
1549
1547
  * finishReason instead of fabricating an answer from reasoning. Opt in via
1550
1548
  * `(runOptions as any).thinkingAsAnswerFallback === true` or
@@ -1569,7 +1567,7 @@ export function shouldPromoteThinkingToAnswer(config?: unknown, runOptions?: unk
1569
1567
  }
1570
1568
 
1571
1569
  /**
1572
- * Canonicalizes a provider finish reason for AgentResponse (Q-04).
1570
+ * Canonicalizes a provider finish reason for AgentResponse.
1573
1571
  * When `truncated` (maxTurns exhausted with pending tools), forces
1574
1572
  * `"max_turns"` and preserves the provider's reason in `rawFinishReason`.
1575
1573
  * Otherwise delegates to `normalizeFinishReason` (unknown vendor strings map
@@ -9,7 +9,7 @@ import { getModel, getSubModel } from "../utils/env.ts";
9
9
  export interface SubAgentConfig extends AgentConfig {}
10
10
 
11
11
  /**
12
- * Canonical env-based construction (Q-56): builds an `Agent` reading
12
+ * Canonical env-based construction: builds an `Agent` reading
13
13
  * `MODEL`/`SUB_AGENT_MODEL` only for the fields the caller omits, via
14
14
  * `getModel()`/`getSubModel()`. Explicit config always wins; constructing an
15
15
  * `Agent` with an explicit `model` reads no env for routing. The static
@@ -36,12 +36,12 @@ export function agentFromEnv(
36
36
  * It inherits 100% of the capabilities of Agent, defaults to SUB_AGENT_MODEL,
37
37
  * and provides .asTool() and .toTool() for seamless registration into parent agents.
38
38
  *
39
- * Model fallback chain (Q-56, canonical): explicit `config.model` ??
39
+ * Model fallback chain (canonical): explicit `config.model` ??
40
40
  * `SUB_AGENT_MODEL` (`getSubModel()`) ?? `MODEL` (`getModel()`). Missing
41
41
  * everything throws. Constructing any `Agent`/`SubAgent` with an explicit
42
42
  * `model` reads no env for routing.
43
43
  *
44
- * Q-47 decomposition boundary: this class stays a thin wrapper. The run/stream
44
+ * Decomposition boundary: this class stays a thin wrapper. The run/stream
45
45
  * loops live in `agent.ts`/`loop.ts`, spawning in `delegation.ts`
46
46
  * (`agentToTool`), and model parsing in `utils/session.ts` (`parseModelRef`).
47
47
  * No loop, transport, or formatting logic is duplicated here.
@@ -104,7 +104,7 @@ export class SubAgent extends Agent {
104
104
  }
105
105
  }
106
106
 
107
- // Q-56: `Agent.fromEnv()` static without touching `agent.ts` (owned by
107
+ // `Agent.fromEnv()` static without touching `agent.ts` (owned by
108
108
  // another lane). Wired at module load; typed via the declaration below so
109
109
  // `Agent.fromEnv({...})` typechecks wherever this module is imported.
110
110
  declare module "./agent.ts" {
@@ -3,12 +3,9 @@ import type { SubAgentStep } from "../types/response.ts";
3
3
  import type { AgentConfig } from "../types/agent.ts";
4
4
  import type { Agent } from "./agent.ts";
5
5
  import { normalizeToolName } from "../tools/executor.ts";
6
- import { escapeXml } from "../utils/serialization.ts";
7
6
  import { getModel, getSubModel } from "../utils/env.ts";
8
7
  import { resolveModel } from "../providers/registry.ts";
9
8
 
10
- export { escapeXml };
11
-
12
9
  /** Minimal live-trace shape shared by fixed + dynamic workers (structural subset of `SubAgentTrace`). */
13
10
  export interface LiveWorkerTrace {
14
11
  trackingId: string;
@@ -200,6 +197,7 @@ export function grantWorkerTools(
200
197
  return grantedTools;
201
198
  }
202
199
 
200
+ // The provider id a model string resolves to, or undefined when it cannot be resolved; in: resolveWorkerCredentials, out: the provider match check.
203
201
  function providerOf(modelStr: string | unknown | undefined): string | undefined {
204
202
  if (!modelStr) return undefined;
205
203
  try {
@@ -0,0 +1,28 @@
1
+ # src/
2
+
3
+ The whole SDK. `index.ts` is the only public entry (`main`/`module`/`types`); everything else is reached through it.
4
+
5
+ | File / folder | What it is |
6
+ | :-- | :-- |
7
+ | `index.ts` | The public barrel. Adding or removing an export is a public API change. |
8
+ | `telemetry.ts` | The in-process event bus (`telemetryBus`). Imports nothing from the SDK, so any module may use it. |
9
+ | `telemetry-file.ts` | Opt-in NDJSON sink for the bus: a persisted session's events go to `<session folder>/telemetry.ndjson`, the rest to `<home>/telemetry/<date>.ndjson`; never outside a home. |
10
+ | `update-models.ts` | CLI behind `bun run update-models` (refreshes the model catalog cache). |
11
+ | `providers.ts` | Provider-agnostic mappers (thinking, cache, tool choice, structured output, generation params). No HTTP. |
12
+ | `agent/` | `Agent`, the model-to-tools loop, workers, delegation. |
13
+ | `providers/` | One adapter per wire (Google, OpenAI, OpenRouter, OpenAI-compatible), the registry, shared helpers. |
14
+ | `models/` | The model catalog (models.dev) and its cache. |
15
+ | `tools/` | `tool()`, schemas, the executor. |
16
+ | `streaming/` | The event stream and the SSE parser. |
17
+ | `session/` | Persistence (snapshot + journal), usage accounting, tool logs. |
18
+ | `scheduler/` | Scheduled actions. |
19
+ | `types/` | Shared contracts (no logic except small helpers). |
20
+ | `utils/` | Small shared helpers (durable writes, home, env, media, retry, safe I/O). |
21
+
22
+ ## Rules for every file here
23
+ - Imports use `.ts` extensions; type-only imports use `import type`.
24
+ - No `console.*`: warnings and traces go to `telemetryBus` (exceptions: the `update-models.ts` CLI and one compat `console.warn` in `providers.ts`).
25
+ - All state is written under the one home folder (`utils/home.ts`). Files are replaced through `utils/durable.ts`; the append-only ones are the session journal and tool logs (fsynced per line) and the opt-in telemetry file (not fsynced).
26
+ - Provider differences never leave `providers/` and `providers.ts`.
27
+ - Every function has one comment line above it (how it works, where input comes from, where output goes).
28
+ - Change a public option, export, env var or telemetry code: update `README.md` and `docs/` in the same change.