agent-accelerator 0.5.6 → 0.5.7

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 (45) hide show
  1. package/README.md +6 -13
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +16 -18
  4. package/src/agent/architecture.md +25 -0
  5. package/src/agent/context.ts +4 -0
  6. package/src/agent/delegation.ts +18 -84
  7. package/src/agent/loop.ts +13 -15
  8. package/src/agent/worker.ts +1 -3
  9. package/src/architecture.md +28 -0
  10. package/src/index.ts +6 -73
  11. package/src/models/architecture.md +14 -0
  12. package/src/models/catalog-cache.ts +3 -0
  13. package/src/models/catalog.ts +7 -0
  14. package/src/providers/architecture.md +24 -0
  15. package/src/providers/google.ts +20 -0
  16. package/src/providers/openai-compat.ts +22 -0
  17. package/src/providers/openai.ts +17 -1
  18. package/src/providers/openrouter.ts +12 -0
  19. package/src/providers/registry.ts +1 -0
  20. package/src/providers.ts +9 -53
  21. package/src/scheduler/architecture.md +19 -0
  22. package/src/scheduler/scheduler.ts +21 -0
  23. package/src/scheduler/store.ts +6 -0
  24. package/src/scheduler/time.ts +2 -0
  25. package/src/session/architecture.md +19 -0
  26. package/src/session/store.ts +15 -0
  27. package/src/session/usage.ts +1 -0
  28. package/src/streaming/architecture.md +13 -0
  29. package/src/streaming/event-stream.ts +5 -0
  30. package/src/telemetry.ts +1 -0
  31. package/src/tools/architecture.md +19 -0
  32. package/src/tools/executor.ts +13 -0
  33. package/src/tools/schema.ts +1 -0
  34. package/src/tools/subagent-result.ts +1 -0
  35. package/src/types/architecture.md +20 -0
  36. package/src/utils/architecture.md +25 -0
  37. package/src/utils/base64.ts +2 -0
  38. package/src/utils/cache.ts +2 -0
  39. package/src/utils/errors.ts +4 -0
  40. package/src/utils/headers.ts +4 -1
  41. package/src/utils/media.ts +1 -0
  42. package/src/utils/raw-capture.ts +3 -0
  43. package/src/utils/retry.ts +1 -0
  44. package/src/utils/safe-io.ts +4 -0
  45. package/src/utils/session.ts +5 -161
package/README.md CHANGED
@@ -386,6 +386,8 @@ telemetryBus.onEvent((e) => {
386
386
  | `skipScheduledAction(id, { home? })` | Ignore a missed run and wait for the next scheduled time. |
387
387
  | `deleteScheduledAction(id, { force?, purge?, home? })` | Delete for good. While it is cooking nothing changes unless `force: true`; `purge: true` also deletes past run files. |
388
388
 
389
+ Every function except `createScheduledAction` finds the home folder through `setHome()`, `AGENT_ACCEL_HOME`, or `.agent-accel/` in the working directory. If you built the Agent with its own `home`, pass the same `{ home }` to these calls (the model's `scheduled_actions` tool already does).
390
+
389
391
  **How it behaves**
390
392
 
391
393
  * **One local clock.** A single timer per home folder (guarded by a one-process lock, `<home>/.lock`) ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
@@ -657,7 +659,7 @@ Implement the `Provider` interface when adding a fully custom transport.
657
659
 
658
660
  ### Google
659
661
 
660
- `GoogleInteractionsProvider` (aliased as `GoogleAIStudioProvider`) and `GOOGLE_MODELS` provide Google support over the Interactions API (`POST {baseUrl}/interactions`, streaming via `?alt=sse`).
662
+ `GoogleInteractionsProvider` and `GOOGLE_MODELS` provide Google support over the Interactions API (`POST {baseUrl}/interactions`, streaming via `?alt=sse`).
661
663
 
662
664
  Turns chain statefully through `previous_interaction_id` per session, falling back to stateless full-history sends when the session switches providers or models.
663
665
 
@@ -683,7 +685,7 @@ Helpers:
683
685
 
684
686
  ### OpenAI
685
687
 
686
- `OpenAIResponsesProvider` (aliased as `OpenAIProvider`) and `OPENAI_MODELS` provide OpenAI support over the Responses API (`POST {baseUrl}/responses`).
688
+ `OpenAIResponsesProvider` and `OPENAI_MODELS` provide OpenAI support over the Responses API (`POST {baseUrl}/responses`).
687
689
 
688
690
  Every turn is stateless: the full history is sent explicitly with `store: false`, so no `previous_response_id`, `background`, or conversation chaining is ever used.
689
691
 
@@ -702,7 +704,7 @@ It also:
702
704
 
703
705
  ### OpenRouter
704
706
 
705
- `OpenRouterChatCompletionsProvider` (aliased as `OpenRouterProvider` / `OpenRouter`) and `OPENROUTER_MODELS` provide OpenRouter support over Chat Completions (`POST {baseUrl}/chat/completions`).
707
+ `OpenRouterChatCompletionsProvider` and `OPENROUTER_MODELS` provide OpenRouter support over Chat Completions (`POST {baseUrl}/chat/completions`).
706
708
 
707
709
  Every turn is stateless: the full `messages[]` array is sent explicitly — no chaining primitives exist on this endpoint.
708
710
 
@@ -731,7 +733,7 @@ setAttribution({ referer: "https://my.app", title: "My App" });
731
733
 
732
734
  ### Custom
733
735
 
734
- `OpenAICompatibleProvider` is also exposed through the `CustomProvider`, `createCustomProvider`, and `createOpenAICompatibleProvider(prefix, opts?)` aliases.
736
+ `OpenAICompatibleChatProvider` is also exposed through the `CustomProvider`, `createCustomProvider`, and `createOpenAICompatibleProvider(prefix, opts?)` aliases.
735
737
 
736
738
  `CustomProviderOptions`:
737
739
 
@@ -931,15 +933,6 @@ Use:
931
933
  Breaking out of `for await` cancels the stream. Your `AbortSignal` and stream cancellation share one linked controller, so `controller.abort()` and `stream.cancel()` both stop the HTTP request. Abort rejects with `AbortError` (never retried) and never resolves partial results. `agent.run(prompt, { stream: true, signal })` exposes the same
932
934
  `cancel` handle.
933
935
 
934
- `SSEParser` provides:
935
-
936
- ```ts
937
- feed(chunk)
938
- flush()
939
- ```
940
-
941
- It parses provider SSE streams and is only needed when implementing custom transports.
942
-
943
936
  ---
944
937
  ## Response, Usage, and Raw Data
945
938
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-accelerator",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
5
5
  "license": "MIT",
6
6
  "author": "Sashvat Bharat",
@@ -29,7 +29,7 @@ import { applyEditable, assertModelAllowed, modelKey, toEditableConfig } from ".
29
29
  import { assertNoToolCollisions } from "../tools/executor.ts";
30
30
  import { runAgentLoop, streamAgentLoop, unfinishedToolCalls, endsWithStop, accumulateUsage, type SteerEntry } from "./loop.ts";
31
31
  import { isRetryableRun, readRetryAfterMs, sleep } from "../utils/retry.ts";
32
- import { createSessionId, nowMs, newRunId } from "../utils/session.ts";
32
+ import { createSessionId, nowMs, newRunId, previewPrompt } from "../utils/session.ts";
33
33
  import {
34
34
  chargeBudgetUsage,
35
35
  checkBudget,
@@ -590,7 +590,7 @@ export class Agent {
590
590
  .map((m) => ({
591
591
  runId: m.id,
592
592
  startedAt: m.at,
593
- prompt: this.previewPrompt((this.context.messages[m.i]?.content ?? "") as string | ContentPart[]),
593
+ prompt: previewPrompt((this.context.messages[m.i]?.content ?? "") as string | ContentPart[]),
594
594
  pendingToolCalls: calls,
595
595
  }));
596
596
  }
@@ -652,6 +652,7 @@ export class Agent {
652
652
  this.doPersist();
653
653
  }
654
654
 
655
+ // Builds the system prompt: the developer's instructions plus, when thinking is on, the fixed Reasoning Directive; in: this.instructions + thinkingConfig, out: every AgentContext.systemPrompt.
655
656
  private getFullInstructions(): string | undefined {
656
657
  const base = this.instructions || "";
657
658
  let out = base;
@@ -799,12 +800,14 @@ export class Agent {
799
800
  return this.midSessionConfig.mode;
800
801
  }
801
802
 
803
+ // Picks steer or queue for a call that arrives while busy: the developer's enforced midSession mode wins over the per-call request; in: run()/stream() `enqueue`, out: their busy branch.
802
804
  private resolveEnqueueMode(requested?: "steer" | "queue"): "steer" | "queue" {
803
805
  const enforced = this.midSessionConfig.mode;
804
806
  if (enforced === "steer" || enforced === "queue") return enforced;
805
807
  return requested === "steer" ? "steer" : "queue";
806
808
  }
807
809
 
810
+ // Throws synchronously on an empty content-part array so run() and stream() fail at the same moment; in: any prompt entry point, out: nothing.
808
811
  private assertPromptValid(prompt: string | ContentPart[]): void {
809
812
  if (Array.isArray(prompt) && prompt.length === 0) {
810
813
  throw new Error(
@@ -813,19 +816,6 @@ export class Agent {
813
816
  }
814
817
  }
815
818
 
816
- private previewPrompt(prompt: string | ContentPart[]): string {
817
- if (typeof prompt === "string") return prompt.slice(0, 500);
818
- try {
819
- const texts = prompt
820
- .filter((p) => (p as { type?: unknown }).type === "text")
821
- .map((p) => (p as { text?: string }).text ?? "")
822
- .join("\n");
823
- return (texts || "[multipart]").slice(0, 500);
824
- } catch {
825
- return "[multipart]";
826
- }
827
- }
828
-
829
819
  /**
830
820
  * Stops the active run now (STOP.) and, given a message, starts it next (STOP. Do this instead). The provider
831
821
  * request is aborted (connection closed, so the provider stops generating where it can), the stream is cancelled, and
@@ -961,13 +951,14 @@ export class Agent {
961
951
  try {
962
952
  this.currentRun?.outerStream?.push({
963
953
  type: "queued",
964
- queuedPrompt: this.previewPrompt(prompt),
954
+ queuedPrompt: previewPrompt(prompt),
965
955
  queueLength: this.pendingQueue.length,
966
956
  } as any);
967
957
  } catch {}
968
958
  });
969
959
  }
970
960
 
961
+ // Starts the next waiting prompt (urgent ones first, aborted ones rejected) as a streaming or non-streaming run once the agent is idle; in: finishCurrentRun and queue changes, out: a new run whose result settles the queued caller.
971
962
  private pumpQueue(): void {
972
963
  if (this.currentRun !== null) return;
973
964
  const next = this.pendingQueue.shift();
@@ -1023,6 +1014,7 @@ export class Agent {
1023
1014
  this.startNonStreamingRun(next.prompt, next.options).then(next.resolve, next.reject);
1024
1015
  }
1025
1016
 
1017
+ // Ends the active run slot: leftover steers become follow-ups at the FRONT of the queue (behind urgent ones), then the queue is pumped; in: a settled run, out: pendingQueue + persistNow.
1026
1018
  private finishCurrentRun(): void {
1027
1019
  // Q-20: leftover steer entries become queued follow-ups instead of dropping.
1028
1020
  let leftover: SteerEntry[] = [];
@@ -1065,6 +1057,7 @@ export class Agent {
1065
1057
  queueMicrotask(() => this.pumpQueue());
1066
1058
  }
1067
1059
 
1060
+ // Removes the abort listener a run's merged signal fallback installed; in: a settled run's controller, out: nothing.
1068
1061
  private cleanupMerge(runController: AbortController): void {
1069
1062
  try {
1070
1063
  const dispose = mergeDisposers.get(runController);
@@ -1075,6 +1068,7 @@ export class Agent {
1075
1068
  } catch {}
1076
1069
  }
1077
1070
 
1071
+ // Joins the caller's AbortSignal with the run's own controller (AbortSignal.any, else a manual forward) so both stop the run; in: AgentRunOptions.signal + the run controller, out: the signal handed to the loop.
1078
1072
  private mergeUserSignal(userSignal?: AbortSignal, runController?: AbortController): AbortSignal | undefined {
1079
1073
  if (!runController) return userSignal;
1080
1074
  if (!userSignal) return runController.signal;
@@ -1227,6 +1221,7 @@ export class Agent {
1227
1221
  };
1228
1222
  }
1229
1223
 
1224
+ // Runs one non-streaming run as a task: catalog refresh, model resolve, prompt/mark (or a resumed mark), loop with `runRetries` backoff, trace merge, run ending recorded; in: run()/queue/resume(), out: the AgentResponse promise.
1230
1225
  private startNonStreamingRun(
1231
1226
  prompt: string | ContentPart[],
1232
1227
  options?: AgentRunOptions,
@@ -1328,6 +1323,7 @@ export class Agent {
1328
1323
  return task;
1329
1324
  }
1330
1325
 
1326
+ // Starts one streaming run: validates the model and thinking level synchronously, adds the prompt, builds the loop config and returns the loop's event stream, recording the run ending when it settles; in: stream()/pumpQueue, out: an AssistantMessageEventStream.
1331
1327
  private startStreamingRun(
1332
1328
  prompt: string | ContentPart[],
1333
1329
  options?: AgentRunOptions
@@ -1655,6 +1651,7 @@ export class Agent {
1655
1651
  } catch {}
1656
1652
  }
1657
1653
 
1654
+ // Adds the user prompt to the target context (fresh context when stateless, stable system prompt, additionalContext as a user prefix) and marks the run start for the default session; in: a run start, out: the new SessionRunMark.
1658
1655
  private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): SessionRunMark | undefined {
1659
1656
  // Q-20: prompt validation throws synchronously in both run() and
1660
1657
  // stream() (same timing; documented here).
@@ -1791,7 +1788,7 @@ export class Agent {
1791
1788
  try {
1792
1789
  this.currentRun?.outerStream?.push({
1793
1790
  type: "queued",
1794
- queuedPrompt: this.previewPrompt(prompt),
1791
+ queuedPrompt: previewPrompt(prompt),
1795
1792
  queueLength: this.pendingQueue.length,
1796
1793
  } as any);
1797
1794
  } catch {}
@@ -1933,7 +1930,7 @@ export class Agent {
1933
1930
  try {
1934
1931
  this.currentRun.outerStream?.push({
1935
1932
  type: "queued",
1936
- queuedPrompt: this.previewPrompt(prompt),
1933
+ queuedPrompt: previewPrompt(prompt),
1937
1934
  queueLength: this.pendingQueue.length + 1,
1938
1935
  } as any);
1939
1936
  } catch {}
@@ -2008,6 +2005,7 @@ export class Agent {
2008
2005
  return s;
2009
2006
  }
2010
2007
 
2008
+ // Attaches onEvent/onDelta/onThinkingDelta to a stream and, with wrapThinking, adds display-only <think> tags and whitespace hygiene on the outer events; in: stream()/pumpQueue, out: listeners on the stream.
2011
2009
  private wireStreamCallbacks(
2012
2010
  s: AssistantMessageEventStream,
2013
2011
  options?: AgentRunOptions
@@ -0,0 +1,25 @@
1
+ # src/agent/
2
+
3
+ The agent and everything that runs under it.
4
+
5
+ | File | What it is | Used by |
6
+ | :-- | :-- | :-- |
7
+ | `agent.ts` | `Agent`: `run`/`stream`/`ask`, `steer`/`queue`/`interrupt`, persistence hooks, `resume()`/`interrupted()`, `usageReport()`, `fork()`, `toConfig()`/`fromConfig()`, the `workers` getter. | callers, workers, scheduler |
8
+ | `loop.ts` | `runAgentLoop` / `streamAgentLoop`: per turn budget, pairing repair, file access, provider call, tool execution, repeat guard. Also `ensureToolPairing`, `accumulateUsage`. | `Agent` |
9
+ | `context.ts` | `AgentContext`: the validated message list with checkpoint/rollback. | `Agent`, loops |
10
+ | `delegation.ts` | Fixed workers (`agentToTool`, `buildAgentTools`), the live worker trace store, session-id helpers, budget helpers, `openWorkerTrace`/`workerMetadata`. | `Agent`, `workers.ts` |
11
+ | `worker.ts` | Worker core: model/tool normalization, `buildDynamicWorkerConfig` (shared preset), `createLiveWorker`. | `Agent`, registry, scheduler |
12
+ | `workers.ts` | `WorkerRegistry` (`agent.workers`): persistent multi-turn workers. | `Agent`, `subagents` tool |
13
+ | `subagents-tool.ts` + `subagents.md` | The one model-facing `subagents` tool; the `.md` is its description. | `Agent` constructor |
14
+ | `subagent.ts` | `SubAgent` class and `agentFromEnv` (thin, kept for compatibility). | callers |
15
+ | `turns.ts` | Pure grouping of messages into finished turns (history cut for workers). | `workers.ts` |
16
+ | `config-blocks.ts` | RSI blocks: only `model`, `instructions`, `tools` are editable; `allowedModels` check. | `Agent` |
17
+
18
+ ## Invariants
19
+ - `runAgentLoop` and `streamAgentLoop` duplicate their turn logic: every loop change goes into both.
20
+ - An `Agent` runs one thing at a time; delegation forks a copy per call so parallel calls never queue.
21
+ - A worker is a plain `Agent` with a parent link and presets. Never duplicate logic between Agent and worker.
22
+ - A worker is never less constrained than its parent (`inheritableConfig()`); its children draw only from the tools it was granted.
23
+ - Exactly-once worker calls rest on the call key (parent run mark id + tool call id) and `meta.last`.
24
+ - Only results branded by `makeSubagentResult` have their usage metadata trusted.
25
+ - Each Agent records only its own model turns; never sum `AgentResponse.usage` for totals.
@@ -6,6 +6,7 @@ import { clearInteractionChains } from "../providers/google.ts";
6
6
 
7
7
  const USER_PART_TYPES = new Set(["text", "image", "audio", "video", "file"]);
8
8
 
9
+ // Throws the AgentContext validation error with the SDK prefix; in: the add* validators, out: a thrown Error.
9
10
  function fail(message: string): never {
10
11
  throw new Error(`[Agent Accelerator] AgentContext: ${message}`);
11
12
  }
@@ -32,6 +33,7 @@ export class AgentContext {
32
33
  this.systemPrompt = systemPrompt;
33
34
  }
34
35
 
36
+ // Validates a user prompt (string or media/text parts) and appends it; in: Agent.prepareTurn and drained steers, out: messages.
35
37
  addUserMessage(content: string | ContentPart[]): void {
36
38
  if (Array.isArray(content)) {
37
39
  if (content.length === 0) return;
@@ -120,6 +122,7 @@ export class AgentContext {
120
122
  }
121
123
  }
122
124
 
125
+ // Appends one tool message per result, keeping logFile and stopped marks; in: the loops after a tool batch, out: messages.
123
126
  addToolResults(results: ToolResultRecord[]): void {
124
127
  if (!results || results.length === 0) return;
125
128
  for (let i = 0; i < results.length; i++) {
@@ -145,6 +148,7 @@ export class AgentContext {
145
148
  }
146
149
  }
147
150
 
151
+ // Copies the context with media bytes preserved (structuredClone, else a deep copy); in: callers needing an isolated history, out: a new AgentContext.
148
152
  clone(): AgentContext {
149
153
  const next = new AgentContext(this.systemPrompt);
150
154
  // Preserve Uint8Array/ArrayBuffer media via structuredClone when available (M8 fix)
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
2
  import { tool } from "../tools/tool.ts";
3
- import { escapeXml } from "../utils/serialization.ts";
4
3
  import type { ToolDefinition } from "../types/tool.ts";
5
4
  import type { AgentResponse, SubAgentExecutionMetadata, SubAgentStep } from "../types/response.ts";
6
5
  import type { TokenUsage } from "../types/core.ts";
@@ -17,8 +16,7 @@ import { newRunId } from "../utils/session.ts";
17
16
  import { newTurnId } from "../utils/session.ts";
18
17
  import { sanitizeToolName, createLiveWorker } from "./worker.ts";
19
18
 
20
- export { escapeXml };
21
-
19
+ /** Builds a random child session id that fits the 64-char provider limit, embedding a hash of the parent when it must be cut; in: a parent id + tag from scheduled runs, out: the worker's session id. */
22
20
  export function createChildSessionId(parentId: string, tag: string): string {
23
21
  let rand: string;
24
22
  try {
@@ -104,17 +102,18 @@ const subagentTraces = new Map<string, SubAgentTrace>();
104
102
  const subagentListeners = new Map<string, Set<(trace: SubAgentTrace) => void>>();
105
103
 
106
104
  /** Max trace keys held (LRU, evicts oldest whole trace on insert) (Q-30). */
107
- export const MAX_SUBAGENT_TRACES = 500;
105
+ const MAX_SUBAGENT_TRACES = 500;
108
106
  /** Trace TTL: 1h wall-clock, lazily expired on read (Q-30). */
109
- export const SUBAGENT_TRACE_TTL_MS = 60 * 60 * 1000;
107
+ const SUBAGENT_TRACE_TTL_MS = 60 * 60 * 1000;
110
108
  /** Max steps retained per trace; oldest steps are dropped beyond this (Q-30). */
111
- export const MAX_SUBAGENT_STEPS_PER_TRACE = 100;
109
+ const MAX_SUBAGENT_STEPS_PER_TRACE = 100;
112
110
  /** Live subscriptions auto-expire after 5min (Q-30). */
113
- export const SUBAGENT_SUBSCRIBE_TTL_MS = 5 * 60 * 1000;
111
+ const SUBAGENT_SUBSCRIBE_TTL_MS = 5 * 60 * 1000;
114
112
 
115
113
  /** Last-write wall-clock per canonical trackingId (drives TTL; reads do not extend it). */
116
114
  const traceTouchedAt = new Map<string, number>();
117
115
 
116
+ // Removes a trace under all its keys plus its listeners and TTL stamp; in: pruneTraces/lookupTrace, out: the trace store.
118
117
  function deleteTraceByCanonical(canonical: string): void {
119
118
  for (const [key, t] of [...subagentTraces]) {
120
119
  if (t.trackingId === canonical) subagentTraces.delete(key);
@@ -123,12 +122,14 @@ function deleteTraceByCanonical(canonical: string): void {
123
122
  subagentListeners.delete(canonical);
124
123
  }
125
124
 
125
+ // True once a trace was not written for SUBAGENT_TRACE_TTL_MS; in: lookup and prune, out: their expiry decision.
126
126
  function isTraceExpired(canonical: string, now = Date.now()): boolean {
127
127
  const touched = traceTouchedAt.get(canonical);
128
128
  if (touched === undefined) return false;
129
129
  return now - touched > SUBAGENT_TRACE_TTL_MS;
130
130
  }
131
131
 
132
+ // Stamps a trace as just written, which restarts its TTL; in: insert and the live worker hooks, out: traceTouchedAt.
132
133
  function touchTrace(canonical: string, now = Date.now()): void {
133
134
  traceTouchedAt.set(canonical, now);
134
135
  }
@@ -138,7 +139,7 @@ function touchTrace(canonical: string, now = Date.now()): void {
138
139
  * Called on every insert. Whole-trace eviction removes both the canonical
139
140
  * key and its display-id alias.
140
141
  */
141
- export function pruneTraces(now = Date.now()): void {
142
+ function pruneTraces(now = Date.now()): void {
142
143
  for (const canonical of [...new Set([...subagentTraces.values()].map((t) => t.trackingId))]) {
143
144
  if (isTraceExpired(canonical, now)) deleteTraceByCanonical(canonical);
144
145
  }
@@ -155,12 +156,13 @@ export function pruneTraces(now = Date.now()): void {
155
156
  }
156
157
 
157
158
  /** Drops oldest steps beyond {@link MAX_SUBAGENT_STEPS_PER_TRACE} (Q-30). */
158
- export function trimTraceSteps(trace: SubAgentTrace): void {
159
+ function trimTraceSteps(trace: SubAgentTrace): void {
159
160
  if (trace.steps.length > MAX_SUBAGENT_STEPS_PER_TRACE) {
160
161
  trace.steps.splice(0, trace.steps.length - MAX_SUBAGENT_STEPS_PER_TRACE);
161
162
  }
162
163
  }
163
164
 
165
+ // Stores a trace under its tracking id and display id in LRU order, pruning expired and excess traces; in: openWorkerTrace, out: the trace store.
164
166
  function insertTrace(trace: SubAgentTrace, displayId: string, now = Date.now()): void {
165
167
  pruneTraces(now);
166
168
  trimTraceSteps(trace);
@@ -181,6 +183,7 @@ function insertTrace(trace: SubAgentTrace, displayId: string, now = Date.now()):
181
183
  pruneTraces(now);
182
184
  }
183
185
 
186
+ // Finds a trace by tracking id, display id or case-insensitive id, dropping expired ones and refreshing LRU order; in: the scoped getters and subscribe, out: the live trace or undefined.
184
187
  function lookupTrace(id: string): SubAgentTrace | undefined {
185
188
  if (!id) return undefined;
186
189
  const direct = subagentTraces.get(id);
@@ -209,10 +212,12 @@ function lookupTrace(id: string): SubAgentTrace | undefined {
209
212
  return undefined;
210
213
  }
211
214
 
215
+ // Deep-enough copy of a trace (steps and usage) so callers never see live mutation; in: the getters and notifyTrace, out: a detached SubAgentTrace.
212
216
  function snapshotTrace(t: SubAgentTrace): SubAgentTrace {
213
217
  return { ...t, steps: t.steps.map((s) => ({ ...s })), usage: t.usage ? { ...t.usage } : undefined };
214
218
  }
215
219
 
220
+ // Sends a snapshot of a trace to its live subscribers, ignoring listener errors; in: the live worker hooks, out: subscriber callbacks.
216
221
  function notifyTrace(trackingId: string): void {
217
222
  const t = subagentTraces.get(trackingId);
218
223
  if (!t) return;
@@ -224,45 +229,6 @@ function notifyTrace(trackingId: string): void {
224
229
  }
225
230
  }
226
231
 
227
- /**
228
- * Returns a snapshot of one worker's trace by tracking id
229
- * (`SUBAGENT-NAME-{32hex}` or the raw 32-hex suffix). Used by
230
- * `agent.track(id)` for realtime inspection.
231
- *
232
- * Prefer {@link getSubAgentTraceScoped} when the parent session is known:
233
- * this compat lookup matches any parent and can confuse sibling workers
234
- * whose display names collide across sessions.
235
- */
236
- export function getSubAgentTrace(id: string): SubAgentTrace | undefined {
237
- const t = lookupTrace(id);
238
- return t ? snapshotTrace(t) : undefined;
239
- }
240
-
241
- /**
242
- * Scoped trace lookup (Q-30, preferred): like {@link getSubAgentTrace} but
243
- * only matches when `trace.parentSessionId === parentSessionId`. Use this
244
- * from `Agent.track` (or any parent-bound caller) so truncated/aliased ids
245
- * cannot leak across sessions.
246
- */
247
- export function getSubAgentTraceScoped(id: string, parentSessionId: string): SubAgentTrace | undefined {
248
- if (!id || !parentSessionId) return undefined;
249
- const t = lookupTrace(id);
250
- if (!t) return undefined;
251
- if (t.parentSessionId !== parentSessionId) return undefined;
252
- return snapshotTrace(t);
253
- }
254
-
255
- /**
256
- * Lists tracking ids of EVERY worker trace in the process (all agents, all
257
- * sessions). Diagnostics only: `Agent` never uses it, because in a
258
- * multi-tenant host it would expose other tenants' workers. Use
259
- * {@link listSubAgentTraceIdsForSessions} for agent-bound callers.
260
- */
261
- export function listSubAgentTraceIds(): string[] {
262
- pruneTraces();
263
- return [...subagentTraces.keys()];
264
- }
265
-
266
232
  /** Scoped lookup across a set of parent session ids (an agent's default + named sessions). */
267
233
  export function getSubAgentTraceForSessions(
268
234
  id: string,
@@ -326,21 +292,6 @@ export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => vo
326
292
  };
327
293
  }
328
294
 
329
- /** Appends a step to a worker trace and notifies live subscribers. */
330
- export function appendSubAgentStep(trackingId: string, step: SubAgentStep): void {
331
- const t = subagentTraces.get(trackingId) ?? lookupTrace(trackingId);
332
- if (!t) return;
333
- if (isTraceExpired(t.trackingId)) {
334
- deleteTraceByCanonical(t.trackingId);
335
- return;
336
- }
337
- t.steps.push(step);
338
- trimTraceSteps(t);
339
- t.turns = Math.max(t.turns, step.turn);
340
- touchTrace(t.trackingId);
341
- notifyTrace(trackingId);
342
- }
343
-
344
295
  // ---------------------------------------------------------------------------
345
296
  // One worker run: live trace + execution metadata (shared by fixed workers and the worker registry)
346
297
  // ---------------------------------------------------------------------------
@@ -523,10 +474,12 @@ export function workerMetadata(a: {
523
474
  // clock; TTL timestamps above use wall-clock `Date.now()`.
524
475
  // ---------------------------------------------------------------------------
525
476
 
477
+ // Reads the agent's configured budget; in: budget helpers, out: Budget or undefined.
526
478
  function readBudget(parentAgent: Agent): Budget | undefined {
527
479
  return (parentAgent as unknown as { budget?: Budget }).budget;
528
480
  }
529
481
 
482
+ // Returns the agent's per-run budget counters, creating them on first use; in: charge and reserve helpers, out: BudgetState.
530
483
  function ensureBudgetState(parentAgent: Agent): BudgetState {
531
484
  const host = parentAgent as unknown as { budgetState?: BudgetState };
532
485
  if (!host.budgetState) {
@@ -535,6 +488,7 @@ function ensureBudgetState(parentAgent: Agent): BudgetState {
535
488
  return host.budgetState;
536
489
  }
537
490
 
491
+ // Total tokens of a usage value, defensively (total, else input + output); in: budget helpers, out: a non-negative integer.
538
492
  function usageTokens(usage: unknown): number {
539
493
  const u = usage as { totalTokens?: unknown; inputTokens?: unknown; outputTokens?: unknown } | undefined;
540
494
  if (!u || typeof u !== "object") return 0;
@@ -544,6 +498,7 @@ function usageTokens(usage: unknown): number {
544
498
  return Math.max(0, Math.floor(inp + out));
545
499
  }
546
500
 
501
+ // Reported total cost of a usage value in USD, 0 when absent; in: budget helpers, out: a non-negative number.
547
502
  function usageCostUsd(usage: unknown): number {
548
503
  const cost = (usage as { cost?: { totalCost?: unknown } } | undefined)?.cost;
549
504
  const v = cost?.totalCost;
@@ -634,27 +589,6 @@ export function reserveSpawn(parentAgent: Agent, count = 1): BudgetState {
634
589
  return state;
635
590
  }
636
591
 
637
- // ---------------------------------------------------------------------------
638
- // Single core loop note + streaming-loop guard (Q-44, partial).
639
- // The canonical run/stream loops live in `loop.ts` + `agent.ts` (other
640
- // owners) and are intentionally NOT duplicated here. This guard exists so
641
- // future consolidation can branch on streaming handles without importing
642
- // the loop.
643
- // ---------------------------------------------------------------------------
644
-
645
- /** Minimal shape of a streaming run handle (has subscribe + terminal promise). */
646
- export interface StreamingLoopHandle {
647
- on: (...args: unknown[]) => unknown;
648
- result: (...args: unknown[]) => Promise<unknown>;
649
- }
650
-
651
- /** Type guard for streaming-loop handles (Q-44). */
652
- export function isStreamingLoop(value: unknown): value is StreamingLoopHandle {
653
- if (!value || typeof value !== "object") return false;
654
- const r = value as Record<string, unknown>;
655
- return typeof r["on"] === "function" && typeof r["result"] === "function";
656
- }
657
-
658
592
  /** Optional name/description wrapper for converting an Agent into a tool. */
659
593
  export interface AgentAsToolTarget {
660
594
  name?: string;
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.
@@ -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;
@@ -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
@@ -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 {