agent-accelerator 0.6.0 → 0.6.6

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 (55) hide show
  1. package/README.md +62 -18
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +192 -695
  4. package/src/agent/architecture.md +19 -13
  5. package/src/agent/context.ts +1 -0
  6. package/src/agent/defaults.ts +88 -0
  7. package/src/agent/delegation.ts +8 -3
  8. package/src/agent/loop.ts +242 -226
  9. package/src/agent/named-sessions.ts +93 -0
  10. package/src/agent/options.ts +166 -0
  11. package/src/agent/run-control.ts +248 -0
  12. package/src/agent/subagents-tool.ts +14 -21
  13. package/src/agent/usage-limit.ts +92 -0
  14. package/src/agent/worker.ts +17 -3
  15. package/src/agent/workers.ts +13 -1
  16. package/src/architecture.md +1 -0
  17. package/src/index.ts +11 -11
  18. package/src/prompts/additional-context.md +2 -0
  19. package/src/prompts/architecture.md +19 -0
  20. package/src/prompts/prompts.ts +81 -0
  21. package/src/prompts/reasoning-directive.md +5 -0
  22. package/src/prompts/structured-output.md +2 -0
  23. package/src/prompts/tool-bridge.md +1 -0
  24. package/src/prompts/tool-interrupted.md +1 -0
  25. package/src/prompts/tool-not-found.md +1 -0
  26. package/src/prompts/tool-repeat-blocked.md +1 -0
  27. package/src/prompts/tool-schema-error.md +1 -0
  28. package/src/prompts/tool-truncated-arguments.md +1 -0
  29. package/src/prompts/worker-failed.md +1 -0
  30. package/src/prompts/worker-replied.md +2 -0
  31. package/src/providers/architecture.md +8 -8
  32. package/src/providers/google.ts +83 -13
  33. package/src/providers/openai-compat.ts +17 -7
  34. package/src/providers/openai.ts +10 -4
  35. package/src/providers/openrouter.ts +185 -18
  36. package/src/providers.ts +59 -9
  37. package/src/scheduler/architecture.md +7 -7
  38. package/src/scheduler/scheduler.ts +40 -0
  39. package/src/scheduler/tool.ts +5 -9
  40. package/src/session/journal.ts +1 -0
  41. package/src/session/store.ts +4 -0
  42. package/src/streaming/event-stream.ts +1 -1
  43. package/src/tools/architecture.md +7 -6
  44. package/src/tools/executor.ts +60 -15
  45. package/src/tools/tool-content.ts +32 -0
  46. package/src/types/agent.ts +40 -8
  47. package/src/types/core.ts +4 -0
  48. package/src/types/message.ts +7 -0
  49. package/src/types/model.ts +4 -1
  50. package/src/types/scheduler.ts +4 -4
  51. package/src/types/tool.ts +2 -0
  52. package/src/utils/errors.ts +3 -3
  53. package/src/utils/headers.ts +1 -1
  54. /package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +0 -0
  55. /package/src/{agent/subagents.md → prompts/subagents-tool.md} +0 -0
@@ -10,7 +10,6 @@ import type {
10
10
  InterruptedRun,
11
11
  } from "../types/agent.ts";
12
12
  import type { TokenUsage } from "../types/core.ts";
13
- import type { ProviderRequestOptions } from "../types/model.ts";
14
13
  import { normalizeThinkingV2, resolveWhenBusy } from "../tools/tool.ts";
15
14
  import type { ToolDefinition, ToolResultRecord } from "../types/tool.ts";
16
15
  import { isSubagentResult } from "../tools/subagent-result.ts";
@@ -28,14 +27,35 @@ import { AgentContext } from "./context.ts";
28
27
  import { resolveModel } from "../providers/registry.ts";
29
28
  import { buildAgentTools } from "./delegation.ts";
30
29
  import { createSubagentsTool } from "./subagents-tool.ts";
31
- import { WorkerRegistry } from "./workers.ts";
32
- import { DEFAULT_MAX_DEPTH, normalizeToolMap, resolveDynamicModelInput, resolveMainModel } from "./worker.ts";
30
+ import { WorkerRegistry, savedSpend } from "./workers.ts";
31
+ import {
32
+ DEFAULT_MAX_DEPTH,
33
+ isWorkerConfig,
34
+ normalizeToolMap,
35
+ resolveDynamicModelInput,
36
+ resolveMainModel,
37
+ } from "./worker.ts";
33
38
  import * as path from "node:path";
34
- import { registerAgent } from "../scheduler/scheduler.ts";
39
+ import { registerAgent, ScheduledActions } from "../scheduler/scheduler.ts";
35
40
  import { resolveHome } from "../utils/home.ts";
36
41
  import { persistTelemetry, registerSessionFolder, unregisterSessionFolder } from "../telemetry-file.ts";
37
42
  import { lockFolder } from "../utils/durable.ts";
38
43
  import { createScheduledActionsTool, getDatetimeTool } from "../scheduler/tool.ts";
44
+ import { fillPrompt, resolvePrompts, type Prompts } from "../prompts/prompts.ts";
45
+ import {
46
+ buildProviderOptions,
47
+ normalizeThinking,
48
+ type ProviderOptionSource,
49
+ resolveAgentLimits,
50
+ resolveEffectiveThinking,
51
+ type AgentRunLimits,
52
+ } from "./options.ts";
53
+ export { normalizeThinking, resolveEffectiveThinking };
54
+ export type { AgentRunLimits };
55
+ import { NamedSessions } from "./named-sessions.ts";
56
+ import { RunControl, type QueuedPrompt } from "./run-control.ts";
57
+ import { UsageLimit, checkUsageLimitConfig } from "./usage-limit.ts";
58
+ import { mergeCap } from "./defaults.ts";
39
59
  import { applyEditable, assertModelAllowed, modelKey, toEditableConfig } from "./config-blocks.ts";
40
60
  import { assertNoToolCollisions } from "../tools/executor.ts";
41
61
  import {
@@ -89,16 +109,6 @@ import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalo
89
109
  import { buildSessionData, type PersistedAgentSession, type SessionTotals } from "../session/store.ts";
90
110
  import { SessionTelemetry } from "../session/store.ts";
91
111
 
92
- /**
93
- * Resolves a per-run thinking override without mutating agent config.
94
- */
95
- export function resolveEffectiveThinking(base?: ThinkingConfig, overrideLevel?: string): ThinkingConfig | undefined {
96
- if (!overrideLevel) return base;
97
- if (overrideLevel === "none") return { enabled: false, level: "none", budgetTokens: 0 };
98
- if (overrideLevel === "dynamic") return { enabled: true, level: "dynamic", budgetTokens: -1 };
99
- return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
100
- }
101
-
102
112
  import { ConfigError, SessionLoadError, ValidationError, isAbortError } from "../types/errors.ts";
103
113
 
104
114
  /**
@@ -111,43 +121,6 @@ const mergeDisposers = new WeakMap<AbortController, () => void>();
111
121
  /** Waits before run retry 1, 2, 3 ... (the last one repeats). Module-level so tests can shrink it; not public API. */
112
122
  export const RUN_RETRY_DELAYS_MS: number[] = [5_000, 15_000, 45_000, 60_000];
113
123
 
114
- /** Max named sessions kept per Agent (LRU). */
115
- const SESSION_LRU_MAX = 100;
116
- /** Idle TTL for named sessions. */
117
- const SESSION_TTL_MS = 60 * 60 * 1000;
118
-
119
- /** Normalized per-run limits. Sub-agents inherit these unless overridden. */
120
- export interface AgentRunLimits {
121
- maxTurns?: number;
122
- deadlineMs?: number;
123
- maxToolCallsPerRun?: number;
124
- maxTotalTokens?: number;
125
- requestTimeoutMs: number;
126
- firstByteTimeoutMs: number;
127
- streamIdleTimeoutMs: number;
128
- }
129
-
130
- /** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
131
- function resolveAgentLimits(config: { limits?: Record<string, unknown> }): AgentRunLimits {
132
- const raw = (config as { limits?: Record<string, unknown> }).limits ?? {};
133
- const num = (v: unknown): number | undefined => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
134
- // A network timeout: `0` or `Infinity` turns it off, any other number is milliseconds, anything else is unset (the default applies).
135
- const timeout = (v: unknown): number | undefined => (typeof v === "number" && !Number.isNaN(v) ? v : undefined);
136
- return {
137
- ...(num(raw.maxTurns) !== undefined ? { maxTurns: Math.max(1, Math.floor(num(raw.maxTurns)!)) } : {}),
138
- ...(num(raw.deadlineMs) !== undefined ? { deadlineMs: num(raw.deadlineMs)! } : {}),
139
- ...(num(raw.maxToolCallsPerRun) !== undefined
140
- ? { maxToolCallsPerRun: Math.max(1, Math.floor(num(raw.maxToolCallsPerRun)!)) }
141
- : {}),
142
- ...(num(raw.maxTotalTokens) !== undefined
143
- ? { maxTotalTokens: Math.max(1, Math.floor(num(raw.maxTotalTokens)!)) }
144
- : {}),
145
- requestTimeoutMs: timeout(raw.requestTimeoutMs) ?? 300_000,
146
- firstByteTimeoutMs: timeout(raw.firstByteTimeoutMs) ?? 120_000,
147
- streamIdleTimeoutMs: timeout(raw.streamIdleTimeoutMs) ?? 300_000,
148
- };
149
- }
150
-
151
124
  /** Thrown when dynamic sub-agent spawning is enabled without an explicit model. */
152
125
  export class SubAgentModelError extends ConfigError {
153
126
  readonly parentModel?: string;
@@ -197,6 +170,8 @@ export class Agent {
197
170
  readonly description: string;
198
171
  /** Stable system instructions. */
199
172
  instructions: string;
173
+ /** The SDK prompts in use: the shipped defaults with `AgentConfig.prompts` applied. */
174
+ readonly prompts: Prompts;
200
175
  /** Original model string or catalog spec used for resolution. */
201
176
  readonly modelStringOrSpec: string | any;
202
177
  /** Configured dynamic sub-agent model, if enabled. */
@@ -217,6 +192,8 @@ export class Agent {
217
192
  maxDepth: number;
218
193
  /** Per-run spending cap of each worker (unset = unlimited). */
219
194
  workerBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
195
+ /** Per-run spending cap of each scheduled-action run (unset = unlimited). */
196
+ scheduledActionBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
220
197
  };
221
198
  /** Home folder for everything written to disk (`sessions/`, `scheduled/`, `telemetry/`). */
222
199
  readonly home: string;
@@ -284,24 +261,14 @@ export class Agent {
284
261
  * request 300s / first-byte 120s / idle 300s (tolerate reasoning).
285
262
  */
286
263
  readonly runLimits: AgentRunLimits;
287
- private currentRun: {
288
- steerInbox: SteerEntry[];
289
- outerStream?: AssistantMessageEventStream;
290
- runPromise?: Promise<AgentResponse>;
291
- abortController: AbortController;
292
- } | null = null;
293
- private pendingQueue: Array<{
294
- prompt: string | ContentPart[];
295
- options?: AgentRunOptions;
296
- isStream: boolean;
297
- deferredStream?: AssistantMessageEventStream;
298
- resolve: (res: AgentResponse) => void;
299
- reject: (err: unknown) => void;
300
- /** Put here by `interrupt(message)`: runs before every other waiting prompt. */
301
- urgent?: boolean;
302
- }> = [];
264
+ /** The active run and the waiting queue (see `run-control.ts`). */
265
+ private readonly rc: RunControl;
303
266
  /** Named session contexts for `sessionId` runs (LRU 100 + TTL). */
304
- private sessions = new Map<string, { context: AgentContext; lastAccess: number }>();
267
+ private readonly named: NamedSessions = new NamedSessions(
268
+ () => this.sessionId,
269
+ () => this.context,
270
+ () => new AgentContext(this.getFullInstructions())
271
+ );
305
272
  /** Coalesced persistence single-flight flag. */
306
273
  private persistScheduled = false;
307
274
  /** Step journal of the persisted session folder (dir mode only). */
@@ -324,6 +291,11 @@ export class Agent {
324
291
  /** The resume in flight, so a second `resume()` returns the same promise. */
325
292
  private resuming?: Promise<AgentResponse | undefined>;
326
293
  private workerRegistry?: WorkerRegistry;
294
+ /** The chat-wide spending cap this agent counts into and checks against: its own (`usageLimit`) or the root's, handed down to workers. @internal */
295
+ chat?: UsageLimit;
296
+ /** True for the agent that created `chat` from its own `usageLimit` (the root of the chat). */
297
+ private ownsChat = false;
298
+ private scheduledActions?: ScheduledActions;
327
299
 
328
300
  /**
329
301
  * Creates an agent and registers its model, tools, cache, and delegation settings.
@@ -349,6 +321,18 @@ export class Agent {
349
321
  this.description = config.description || "AI Agent powered by Agent Accelerator";
350
322
  // DX1: only instructions (no systemPrompt alias)
351
323
  this.instructions = config.instructions || "";
324
+ this.prompts = resolvePrompts(config.prompts);
325
+ // Workers and scheduled runs the SDK builds never take developer defaults (`configure()`, env vars).
326
+ const takesDefaults = !isWorkerConfig(config);
327
+ if (config.usageLimit !== undefined) {
328
+ const problem = checkUsageLimitConfig(config.usageLimit);
329
+ if (problem) throw new ConfigError(`[Agent Accelerator] Invalid usageLimit: ${problem}.`);
330
+ }
331
+ const chatCap = mergeCap("usageLimit", config.usageLimit, takesDefaults);
332
+ if (chatCap) {
333
+ this.chat = new UsageLimit(chatCap);
334
+ this.ownsChat = true;
335
+ }
352
336
  // Unified model resolution (shared with SubAgent/workers): supports
353
337
  // ModelProviderInstance {model,apiKey,...} and ModelSpec {id,provider}.
354
338
  const { modelStringOrSpec, rawModel } = resolveMainModel(config.model);
@@ -374,15 +358,25 @@ export class Agent {
374
358
  `[Agent Accelerator] Invalid dynamicSubagents.maxDepth ${JSON.stringify(rawDepth)}: expected a whole number >= 1, or omit it for no limit; set dynamicSubagents.enabled: false to turn workers off.`
375
359
  );
376
360
  }
377
- const rawBudget = (dynRaw as { workerBudget?: { maxCostUsd?: unknown; maxTotalTokens?: unknown } }).workerBudget;
378
- for (const key of ["maxCostUsd", "maxTotalTokens"] as const) {
379
- const v = rawBudget?.[key];
380
- if (v !== undefined && !(typeof v === "number" && Number.isFinite(v) && v > 0)) {
381
- throw new ConfigError(
382
- `[Agent Accelerator] Invalid dynamicSubagents.workerBudget.${key} ${JSON.stringify(v)}: expected a number greater than 0, or omit it for no limit.`
383
- );
361
+ type RawBudget = { maxCostUsd?: unknown; maxTotalTokens?: unknown } | undefined;
362
+ const checkBudgetKeys = (name: "workerBudget" | "scheduledActionBudget"): RawBudget => {
363
+ const raw = (dynRaw as Record<string, RawBudget>)[name];
364
+ for (const key of ["maxCostUsd", "maxTotalTokens"] as const) {
365
+ const v = raw?.[key];
366
+ if (v !== undefined && !(typeof v === "number" && Number.isFinite(v) && v > 0)) {
367
+ throw new ConfigError(
368
+ `[Agent Accelerator] Invalid dynamicSubagents.${name}.${key} ${JSON.stringify(v)}: expected a number greater than 0, or omit it for no limit.`
369
+ );
370
+ }
384
371
  }
385
- }
372
+ return raw;
373
+ };
374
+ const rawBudget = checkBudgetKeys("workerBudget");
375
+ const rawScheduledBudget = mergeCap(
376
+ "scheduledActionBudget",
377
+ checkBudgetKeys("scheduledActionBudget") as { maxCostUsd?: number; maxTotalTokens?: number } | undefined,
378
+ takesDefaults
379
+ );
386
380
  // Open workers are unlimited unless the developer sets a cap (a number >= 1; Infinity also means unlimited).
387
381
  const rawMax = (dynRaw as { maxSpawn?: unknown }).maxSpawn;
388
382
  if (rawMax !== undefined && (typeof rawMax !== "number" || Number.isNaN(rawMax))) {
@@ -416,6 +410,9 @@ export class Agent {
416
410
  allowScheduledActions: dynRaw.allowScheduledActions === true,
417
411
  maxDepth: (rawDepth as number | undefined) ?? DEFAULT_MAX_DEPTH,
418
412
  ...(rawBudget ? { workerBudget: { ...(rawBudget as { maxCostUsd?: number; maxTotalTokens?: number }) } } : {}),
413
+ ...(rawScheduledBudget
414
+ ? { scheduledActionBudget: { ...(rawScheduledBudget as { maxCostUsd?: number; maxTotalTokens?: number }) } }
415
+ : {}),
419
416
  };
420
417
  }
421
418
  this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
@@ -450,6 +447,17 @@ export class Agent {
450
447
  };
451
448
  // Bound on buffered steer entries (default 20).
452
449
  this.maxSteers = this.midSessionConfig.maxSteers ?? 20;
450
+ const self = this;
451
+ this.rc = new RunControl({
452
+ persistNow: () => this.persistNow(),
453
+ run: (prompt, options) => this.run(prompt, options),
454
+ startNonStreamingRun: (prompt, options) => this.startNonStreamingRun(prompt, options),
455
+ startStreamingRun: (prompt, options) => this.startStreamingRun(prompt, options),
456
+ get midSessionConfig() {
457
+ return self.midSessionConfig;
458
+ },
459
+ maxSteers: this.maxSteers,
460
+ });
453
461
  // Run limits (sub-agents inherit unless overridden).
454
462
  this.runLimits = resolveAgentLimits(config as unknown as { limits?: Record<string, unknown> });
455
463
 
@@ -519,7 +527,7 @@ export class Agent {
519
527
  }
520
528
 
521
529
  if (Array.isArray(config.subagents)) {
522
- const subagentTools = buildAgentTools(config.subagents);
530
+ const subagentTools = buildAgentTools(config.subagents, { chat: this.chat });
523
531
  for (const [toolName, toolDef] of Object.entries(subagentTools)) {
524
532
  this.tools[toolName] = toolDef;
525
533
  }
@@ -559,6 +567,11 @@ export class Agent {
559
567
  return (this.workerRegistry ??= new WorkerRegistry(this));
560
568
  }
561
569
 
570
+ /** The scheduled actions of this agent's home: `create`, `list`, `update`, `run`, `skip`, `delete` (see `ScheduledActions`). */
571
+ get scheduled(): ScheduledActions {
572
+ return (this.scheduledActions ??= new ScheduledActions(this));
573
+ }
574
+
562
575
  // Takes the one-process lock of the folder this Agent persists to: its own session folder, or the folder of a single `persist.file`; throws ConfigError when another live process holds it; in: the constructor and importSession (a new session id is a new folder), out: a `.lock` file there.
563
576
  private lockSessionFolder(): void {
564
577
  const cfg = this.persistConfig;
@@ -607,7 +620,7 @@ export class Agent {
607
620
  const open = new Set(unfinishedToolCalls(this.context.messages).map((c) => c.id));
608
621
  this.toolRecords.splice(0, this.toolRecords.length, ...(session.tools ?? []).filter((t) => open.has(t.id)));
609
622
  for (const w of waiting) {
610
- this.pendingQueue.push({
623
+ this.rc.queue.push({
611
624
  prompt: w.prompt,
612
625
  options: w.additionalContext ? { additionalContext: w.additionalContext } : undefined,
613
626
  isStream: false,
@@ -621,15 +634,14 @@ export class Agent {
621
634
  // Puts saved spending totals back after a restart or import (zeros when the file has none); in: a loaded session's `usage`, out: this.usageTotals.
622
635
  private restoreUsage(saved: unknown): void {
623
636
  Object.assign(this.usageTotals, readUsage(saved));
637
+ if (this.ownsChat) this.chat!.restart(); // a restored session changes the starting point of the chat total
624
638
  }
625
639
 
626
640
  // Records one model call of THIS agent (priced per turn, with the service tier); in: the loop's onTurnUsage, out: this.usageTotals (saved by the next persist).
627
641
  private recordTurnUsage(usage: TokenUsage, spec: Parameters<typeof priceUsage>[1]): void {
628
- addTurn(
629
- this.usageTotals,
630
- usage,
631
- priceUsage(usage, spec, this.serviceTier ? { serviceTier: this.serviceTier } : undefined)
632
- );
642
+ const priced = priceUsage(usage, spec, this.serviceTier ? { serviceTier: this.serviceTier } : undefined);
643
+ addTurn(this.usageTotals, usage, priced);
644
+ this.chat?.add((usage.inputTokens || 0) + (usage.outputTokens || 0), priced?.breakdown.totalMicroUSD ?? 0);
633
645
  }
634
646
 
635
647
  /** Adds another agent's spending to this one (a fixed worker's per-call copy into its template). @internal */
@@ -698,7 +710,7 @@ export class Agent {
698
710
  */
699
711
  resume(): Promise<AgentResponse | undefined> {
700
712
  if (this.resuming) return this.resuming;
701
- if (this.currentRun)
713
+ if (this.rc.current)
702
714
  throw new ValidationError("[Agent Accelerator] resume() needs an idle agent: a run is already active.");
703
715
  const mark = this.runMarks.filter((m) => !m.end && m !== this.activeMark).pop();
704
716
  const last = this.context.messages[this.context.messages.length - 1];
@@ -706,7 +718,7 @@ export class Agent {
706
718
  if (!mark || (nothingOpen && (last?.role === "assistant" || endsWithStop(this.context.messages)))) {
707
719
  // The answer was complete (or a tool had ended the run); only the note that it ended was lost.
708
720
  if (mark) this.endRun(mark, endsWithStop(this.context.messages) ? "stopped" : "done");
709
- this.pumpQueue();
721
+ this.rc.pumpQueue();
710
722
  return Promise.resolve(undefined);
711
723
  }
712
724
  telemetryBus.emit({
@@ -738,6 +750,7 @@ export class Agent {
738
750
  result: t.result,
739
751
  ...(t.isError ? { isError: true } : {}),
740
752
  ...(t.stopped ? { stopped: true } : {}),
753
+ ...(t.parts && t.parts.length > 0 ? { parts: t.parts } : {}),
741
754
  } as ToolResultRecord,
742
755
  ])
743
756
  );
@@ -759,6 +772,7 @@ export class Agent {
759
772
  result: isSubagentResult(result.result) ? result.result.xml : result.result,
760
773
  ...(result.isError ? { isError: true } : {}),
761
774
  ...(result.stopped ? { stopped: true } : {}),
775
+ ...(result.parts && result.parts.length > 0 ? { parts: result.parts } : {}),
762
776
  });
763
777
  this.doPersist();
764
778
  }
@@ -769,12 +783,8 @@ export class Agent {
769
783
  let out = base;
770
784
  const isThinkingEnabled =
771
785
  this.thinkingConfig?.enabled !== false && this.thinkingConfig?.level && this.thinkingConfig?.level !== "none";
772
- if (isThinkingEnabled && !out.includes("[Reasoning Directive]")) {
773
- const guidance =
774
- "[Reasoning Directive]\n" +
775
- "1. Use internal reasoning strictly for private planning and step-by-step thinking.\n" +
776
- "2. Never attempt to execute tools or output final user deliverables inside reasoning.\n" +
777
- "3. Once your reasoning is complete, output your final response or function calls directly in the standard response output.";
786
+ const guidance = this.prompts.reasoningDirective;
787
+ if (isThinkingEnabled && !out.includes(guidance)) {
778
788
  out = out ? `${out}\n\n${guidance}` : guidance;
779
789
  }
780
790
  return out || undefined;
@@ -814,88 +824,25 @@ export class Agent {
814
824
  /** Drops one named session. No-op for unknown ids. */
815
825
  disposeSession(id: string): void {
816
826
  unregisterSessionFolder(id);
817
- try {
818
- this.sessions.delete(id);
819
- } catch {}
820
- try {
821
- clearInteractionChains(id);
822
- } catch {}
823
- try {
824
- clearSessionRouting(id);
825
- } catch {}
827
+ this.named.dispose(id);
826
828
  }
827
829
 
828
830
  /** Drops all named sessions except the default context. */
829
831
  clearSessions(): void {
830
- try {
831
- for (const id of [...this.sessions.keys()]) {
832
- try {
833
- clearInteractionChains(id);
834
- } catch {}
835
- try {
836
- clearSessionRouting(id);
837
- } catch {}
838
- }
839
- this.sessions.clear();
840
- } catch {}
832
+ this.named.clear();
841
833
  }
842
834
 
843
- /** Resolves the context for a `sessionId` run (default session = `this.context`). */
835
+ // Resolves the context for a `sessionId` run (default session = `this.context`); in: the run starters and session(), out: an AgentContext.
844
836
  private getSessionContext(sessionId?: string): AgentContext {
845
- const sid = sessionId || this.sessionId;
846
- if (sid === this.sessionId) return this.context;
847
- const now = Date.now();
848
- // TTL eviction.
849
- try {
850
- for (const [key, entry] of [...this.sessions.entries()]) {
851
- if (now - entry.lastAccess > SESSION_TTL_MS) {
852
- this.sessions.delete(key);
853
- try {
854
- clearInteractionChains(key);
855
- } catch {}
856
- try {
857
- clearSessionRouting(key);
858
- } catch {}
859
- }
860
- }
861
- } catch {}
862
- const existing = this.sessions.get(sid);
863
- if (existing) {
864
- existing.lastAccess = now;
865
- return existing.context;
866
- }
867
- const fresh = new AgentContext(this.getFullInstructions());
868
- this.sessions.set(sid, { context: fresh, lastAccess: now });
869
- // LRU eviction (oldest lastAccess first).
870
- try {
871
- while (this.sessions.size > SESSION_LRU_MAX) {
872
- let oldest: string | undefined;
873
- let oldestTs = Infinity;
874
- for (const [key, entry] of this.sessions) {
875
- if (entry.lastAccess < oldestTs) {
876
- oldestTs = entry.lastAccess;
877
- oldest = key;
878
- }
879
- }
880
- if (!oldest) break;
881
- this.sessions.delete(oldest);
882
- try {
883
- clearInteractionChains(oldest);
884
- } catch {}
885
- try {
886
- clearSessionRouting(oldest);
887
- } catch {}
888
- }
889
- } catch {}
890
- return fresh;
837
+ return this.named.contextFor(sessionId);
891
838
  }
892
839
 
893
840
  /** Rejects + drops all queued follow-ups. Returns the cleared count. */
894
841
  clearQueue(): number {
895
- const n = this.pendingQueue.length;
842
+ const n = this.rc.queue.length;
896
843
  if (n === 0) return 0;
897
844
  const err = Object.assign(new Error("Queue cleared"), { name: "AbortError" });
898
- const pending = this.pendingQueue.splice(0);
845
+ const pending = this.rc.queue.splice(0);
899
846
  for (const entry of pending) {
900
847
  try {
901
848
  entry.reject(err);
@@ -908,32 +855,22 @@ export class Agent {
908
855
  return n;
909
856
  }
910
857
 
911
- /** Throws when the steer inbox is at its bound. */
912
- private assertSteerRoom(inbox: SteerEntry[]): void {
913
- const max = this.maxSteers ?? 20;
914
- if (inbox.length >= max) {
915
- throw new ValidationError(
916
- `[Agent Accelerator] Steer inbox is full (${inbox.length}/${max}). Wait for the active run to drain or increase midSession.maxSteers.`
917
- );
918
- }
919
- }
920
-
921
858
  /**
922
859
  * True while a `run()`/`stream()` turn is active on this agent.
923
860
  * Use with `steer()` (current-turn redirect) vs `queue()` (next-turn follow-up).
924
861
  */
925
862
  get isBusy(): boolean {
926
- return this.currentRun !== null;
863
+ return this.rc.current !== null;
927
864
  }
928
865
 
929
866
  /** Number of queued follow-up requests waiting for the active run to finish. */
930
867
  get pendingCount(): number {
931
- return this.pendingQueue.length;
868
+ return this.rc.queue.length;
932
869
  }
933
870
 
934
871
  /** Number of steer messages buffered for injection into the active turn. */
935
872
  get steerPendingCount(): number {
936
- return this.currentRun?.steerInbox.length ?? 0;
873
+ return this.rc.current?.steerInbox.length ?? 0;
937
874
  }
938
875
 
939
876
  /** Effective mid-session policy (`steer` | `queue` | `auto`). */
@@ -941,13 +878,6 @@ export class Agent {
941
878
  return this.midSessionConfig.mode;
942
879
  }
943
880
 
944
- // 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.
945
- private resolveEnqueueMode(requested?: "steer" | "queue"): "steer" | "queue" {
946
- const enforced = this.midSessionConfig.mode;
947
- if (enforced === "steer" || enforced === "queue") return enforced;
948
- return requested === "steer" ? "steer" : "queue";
949
- }
950
-
951
881
  // Throws synchronously on an empty content-part array so run() and stream() fail at the same moment; in: any prompt entry point, out: nothing.
952
882
  private assertPromptValid(prompt: string | ContentPart[]): void {
953
883
  if (Array.isArray(prompt) && prompt.length === 0) {
@@ -976,28 +906,7 @@ export class Agent {
976
906
  }
977
907
  return this.run(arg, { ...options, enqueue: "interrupt" });
978
908
  }
979
- this.abortActive(arg instanceof Error ? arg : undefined);
980
- }
981
-
982
- // Aborts the active run (provider request, tools, workers, stream) through its own controller; no-op when idle; in: interrupt and queueUrgent, out: the run settles as AbortError.
983
- private abortActive(reason?: Error): void {
984
- const active = this.currentRun;
985
- if (!active) return;
986
- try {
987
- active.abortController.abort(reason ?? new Error("Agent interrupted"));
988
- } catch {}
989
- try {
990
- active.outerStream?.cancel(reason);
991
- } catch {}
992
- }
993
-
994
- // Puts a prompt ahead of everything waiting (behind earlier interrupts) and aborts the active run, so it starts as soon as that run has settled; ignores midSession mode and maxQueued; in: run()/stream() with enqueue "interrupt", out: pendingQueue + the abort.
995
- private queueUrgent(entry: Agent["pendingQueue"][number]): void {
996
- entry.urgent = true;
997
- const firstOrdinary = this.pendingQueue.findIndex((e) => !e.urgent);
998
- this.pendingQueue.splice(firstOrdinary < 0 ? this.pendingQueue.length : firstOrdinary, 0, entry);
999
- this.persistNow();
1000
- this.abortActive();
909
+ this.rc.abortActive(arg instanceof Error ? arg : undefined);
1001
910
  }
1002
911
 
1003
912
  /**
@@ -1023,41 +932,9 @@ export class Agent {
1023
932
  '[Agent Accelerator] steer() is disabled by midSession.mode "queue". Use queue() or set mode to "auto"/"steer".'
1024
933
  );
1025
934
  }
1026
- const active = this.currentRun;
935
+ const active = this.rc.current;
1027
936
  if (!active) return this.run(prompt, options) as Promise<AgentResponse>;
1028
- return this.steerInto(active, prompt, options);
1029
- }
1030
-
1031
- /**
1032
- * Buffers a steer prompt into the active run and returns a promise for ITS
1033
- * reply: the active run's response when the prompt is injected, or (if the
1034
- * run ends first and the prompt becomes a queued follow-up) the follow-up
1035
- * run's response, so a late steer's answer is never lost.
1036
- */
1037
- private steerInto(
1038
- active: NonNullable<Agent["currentRun"]>,
1039
- prompt: string | ContentPart[],
1040
- options?: AgentRunOptions
1041
- ): Promise<AgentResponse> {
1042
- this.assertSteerRoom(active.steerInbox);
1043
- const runDone = active.runPromise ?? active.outerStream?.result();
1044
- if (!runDone) return this.run(prompt, options) as Promise<AgentResponse>;
1045
- const entry: SteerEntry = { prompt, options };
1046
- const reply = new Promise<AgentResponse>((resolve, reject) => {
1047
- entry.settle = { resolve, reject };
1048
- });
1049
- reply.catch(() => {}); // a fire-and-forget caller must not trigger an unhandled rejection
1050
- active.steerInbox.push(entry);
1051
- this.persistNow();
1052
- runDone.then(
1053
- (res) => {
1054
- if (!entry.settle?.deferredToQueue) entry.settle?.resolve(res);
1055
- },
1056
- (err) => {
1057
- if (!entry.settle?.deferredToQueue) entry.settle?.reject(err);
1058
- }
1059
- );
1060
- return reply;
937
+ return this.rc.steerInto(active, prompt, options);
1061
938
  }
1062
939
 
1063
940
  /**
@@ -1081,157 +958,26 @@ export class Agent {
1081
958
  '[Agent Accelerator] queue() is disabled by midSession.mode "steer". Use steer() or set mode to "auto"/"queue".'
1082
959
  );
1083
960
  }
1084
- if (!this.currentRun) return this.run(prompt, options) as Promise<AgentResponse>;
961
+ if (!this.rc.current) return this.run(prompt, options) as Promise<AgentResponse>;
1085
962
  const max = this.midSessionConfig.maxQueued ?? 20;
1086
- if (this.pendingQueue.length >= max) {
963
+ if (this.rc.queue.length >= max) {
1087
964
  throw new ValidationError(
1088
- `[Agent Accelerator] Queue is full (${this.pendingQueue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
965
+ `[Agent Accelerator] Queue is full (${this.rc.queue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
1089
966
  );
1090
967
  }
1091
968
  return new Promise<AgentResponse>((resolve, reject) => {
1092
- this.pendingQueue.push({ prompt, options, isStream: false, resolve, reject });
969
+ this.rc.queue.push({ prompt, options, isStream: false, resolve, reject });
1093
970
  this.persistNow();
1094
971
  try {
1095
- this.currentRun?.outerStream?.push({
972
+ this.rc.current?.outerStream?.push({
1096
973
  type: "queued",
1097
974
  queuedPrompt: previewPrompt(prompt),
1098
- queueLength: this.pendingQueue.length,
975
+ queueLength: this.rc.queue.length,
1099
976
  } as any);
1100
977
  } catch {}
1101
978
  });
1102
979
  }
1103
980
 
1104
- // 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.
1105
- private pumpQueue(): void {
1106
- if (this.currentRun !== null) return;
1107
- const next = this.pendingQueue.shift();
1108
- if (!next) return;
1109
- this.persistNow();
1110
- // Reject queued items whose signal aborted before start.
1111
- try {
1112
- if ((next.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
1113
- const reason = ((next.options as { signal?: AbortSignal }).signal as unknown as { reason?: unknown })?.reason;
1114
- const abortErr = Object.assign(
1115
- reason instanceof Error ? reason : new Error("Queued run aborted before start"),
1116
- { name: "AbortError" }
1117
- );
1118
- try {
1119
- next.reject(abortErr);
1120
- } catch {}
1121
- try {
1122
- next.deferredStream?.fail(abortErr instanceof Error ? abortErr : new Error(String(abortErr)));
1123
- } catch {}
1124
- queueMicrotask(() => this.pumpQueue());
1125
- return;
1126
- }
1127
- } catch {}
1128
- if (next.isStream && next.deferredStream) {
1129
- const deferred = next.deferredStream;
1130
- try {
1131
- const real = this.startStreamingRun(next.prompt, next.options);
1132
- try {
1133
- (deferred as any).__piped = true;
1134
- } catch {}
1135
- const forward = (e: StreamEvent) => {
1136
- try {
1137
- deferred.push(e);
1138
- } catch {}
1139
- };
1140
- real.on("*", forward as any);
1141
- try {
1142
- deferred.onCancel(() => {
1143
- try {
1144
- real.cancel();
1145
- } catch {}
1146
- });
1147
- } catch {}
1148
- real.result().then(
1149
- (res) => {
1150
- try {
1151
- next.resolve(res);
1152
- } catch {}
1153
- try {
1154
- deferred.end(res);
1155
- } catch {}
1156
- },
1157
- (err) => {
1158
- try {
1159
- next.reject(err);
1160
- } catch {}
1161
- // Pipe failures into the deferred stream so for-await sees them.
1162
- try {
1163
- deferred.fail(err instanceof Error ? err : new Error(String(err)));
1164
- } catch {}
1165
- }
1166
- );
1167
- } catch (err) {
1168
- try {
1169
- next.reject(err);
1170
- } catch {}
1171
- try {
1172
- deferred.fail(err instanceof Error ? err : new Error(String(err)));
1173
- } catch {}
1174
- // Keep draining even if starting failed.
1175
- queueMicrotask(() => this.pumpQueue());
1176
- }
1177
- return;
1178
- }
1179
- this.startNonStreamingRun(next.prompt, next.options).then(next.resolve, next.reject);
1180
- }
1181
-
1182
- // 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.
1183
- private finishCurrentRun(): void {
1184
- // Leftover steer entries become queued follow-ups instead of dropping.
1185
- let leftover: SteerEntry[] = [];
1186
- try {
1187
- leftover = this.currentRun?.steerInbox?.splice(0) ?? [];
1188
- } catch {
1189
- leftover = [];
1190
- }
1191
- this.currentRun = null;
1192
- if (leftover.length > 0) {
1193
- try {
1194
- const max = this.midSessionConfig.maxQueued ?? 20;
1195
- // Late steers were typed BEFORE anything already queued: they go to the front, in the order they arrived.
1196
- const lateSteers: Agent["pendingQueue"] = [];
1197
- for (const entry of leftover) {
1198
- try {
1199
- if ((entry.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
1200
- entry.settle?.reject(
1201
- Object.assign(new Error("Steer aborted before it could run"), { name: "AbortError" })
1202
- );
1203
- continue;
1204
- }
1205
- } catch {
1206
- continue;
1207
- }
1208
- if (this.pendingQueue.length + lateSteers.length >= max) {
1209
- entry.settle?.reject(
1210
- new ValidationError("[Agent Accelerator] Steer dropped: the follow-up queue is full.")
1211
- );
1212
- continue;
1213
- }
1214
- // The steer was never injected: it now runs as a follow-up, and the
1215
- // caller's promise resolves with that run's response.
1216
- if (entry.settle) entry.settle.deferredToQueue = true;
1217
- lateSteers.push({
1218
- prompt: entry.prompt,
1219
- options: entry.options,
1220
- isStream: false,
1221
- resolve: entry.settle?.resolve ?? (() => {}),
1222
- reject: entry.settle?.reject ?? (() => {}),
1223
- });
1224
- }
1225
- // ...but behind an interrupt(message), which is the newest and most urgent instruction.
1226
- const firstOrdinary = this.pendingQueue.findIndex((e) => !e.urgent);
1227
- this.pendingQueue.splice(firstOrdinary < 0 ? this.pendingQueue.length : firstOrdinary, 0, ...lateSteers);
1228
- } catch {}
1229
- }
1230
- this.persistNow();
1231
- // Defer pump so the completing run's .then handlers settle first.
1232
- queueMicrotask(() => this.pumpQueue());
1233
- }
1234
-
1235
981
  // Removes the abort listener a run's merged signal fallback installed; in: a settled run's controller, out: nothing.
1236
982
  private cleanupMerge(runController: AbortController): void {
1237
983
  try {
@@ -1299,6 +1045,7 @@ export class Agent {
1299
1045
  const d = this.callDefaults;
1300
1046
  return {
1301
1047
  limits: { ...this.runLimits },
1048
+ ...(this.forkConfig.prompts ? { prompts: { ...this.forkConfig.prompts } } : {}),
1302
1049
  ...(d.retry ? { retry: d.retry } : {}),
1303
1050
  ...(d.env ? { env: d.env } : {}),
1304
1051
  ...(d.fetch ? { fetch: d.fetch } : {}),
@@ -1350,13 +1097,34 @@ export class Agent {
1350
1097
  return new Agent(applyEditable(editable, base));
1351
1098
  }
1352
1099
 
1100
+ // Takes the chat total's starting point (what the saved sessions of this agent and its workers already spent) once, before the first turn of the chat's root agent; in: the run starters, out: this.chat.
1101
+ private beginChat(): void {
1102
+ if (!this.ownsChat) return;
1103
+ this.chat!.begin(() => {
1104
+ // A persisted chat starts from its saved sessions (this agent's and every worker's); a memory-only one from what it holds now.
1105
+ const root = this.persistConfig?.dir;
1106
+ if (root && !this.stateless) {
1107
+ try {
1108
+ const saved = savedSpend(sessionDirFor(root, this.sessionId));
1109
+ return { tokens: saved.inputTokens + saved.outputTokens, microUSD: saved.costMicroUSD.total };
1110
+ } catch {
1111
+ // not a usable session folder name: nothing was saved there
1112
+ }
1113
+ }
1114
+ const total = this.usageReport().total;
1115
+ return { tokens: total.tokens.total, microUSD: Math.round((total.cost.totalUsd ?? 0) * 1_000_000) };
1116
+ });
1117
+ }
1118
+
1353
1119
  /** Budgets are per run: counters restart whenever a new run begins. */
1354
1120
  private resetBudgetState(): void {
1121
+ this.beginChat();
1355
1122
  if (this.budget) this.budgetState = { spawned: 0, totalTokens: 0, totalCostUsd: 0, startedAtMs: nowMs() };
1356
1123
  }
1357
1124
 
1358
1125
  /** Loop hook: charge `usage` (when given) and throw `BudgetExceededError` once any cap is passed. */
1359
1126
  private enforceBudget(usage?: TokenUsage): void {
1127
+ this.chat?.check(); // the chat-wide cap: before every turn and right after each one
1360
1128
  if (!this.budget) return;
1361
1129
  if (usage) chargeBudgetUsage(this as unknown as Parameters<typeof chargeBudgetUsage>[0], usage);
1362
1130
  checkBudget(this.budget, this.budgetState);
@@ -1368,37 +1136,20 @@ export class Agent {
1368
1136
  return level ? { ...runOptions, thinkingLevel: level } : runOptions;
1369
1137
  }
1370
1138
 
1371
- /**
1372
- * Single place that turns agent config + per-run options into the options
1373
- * every provider call receives. Per-run values override agent defaults
1374
- * (objects such as `retry` and `env` merge key by key).
1375
- */
1376
- private buildProviderOptions(runOptions: AgentRunOptions | undefined): ProviderRequestOptions {
1377
- const d = this.callDefaults;
1378
- const sessionId = runOptions?.sessionId || this.sessionId;
1379
- const toolChoice = normalizeToolChoiceOption(runOptions?.toolChoice ?? d.toolChoice);
1380
- const retry = { ...d.retry, ...runOptions?.retry };
1381
- const env = { ...d.env, ...runOptions?.env };
1382
- const maxOutputTokens = runOptions?.maxOutputTokens ?? d.maxOutputTokens;
1383
- const fetchImpl = runOptions?.fetch ?? d.fetch;
1139
+ // The settings `buildProviderOptions` reads, as one object; in: both run starters, out: options.ts.
1140
+ private optionSource(): ProviderOptionSource {
1384
1141
  return {
1385
1142
  apiKey: this.apiKey,
1386
1143
  baseUrl: this.baseUrl,
1387
- headers: { ...(this.customHeaders ?? {}), ...(runOptions?.headers ?? {}) },
1388
- thinking: resolveEffectiveThinking(this.thinkingConfig, runOptions?.thinkingLevel),
1389
- ...(this.cache ? { cache: this.cache } : {}),
1144
+ customHeaders: this.customHeaders,
1145
+ thinkingConfig: this.thinkingConfig,
1146
+ cache: this.cache,
1390
1147
  serviceTier: this.serviceTier,
1391
- sessionId,
1392
- ...(this.outputSpec ? { output: this.outputSpec } : {}),
1393
- ...(this.googleTier ? { googleTier: this.googleTier } : {}),
1394
- ...(toolChoice !== undefined ? { toolChoice } : {}),
1395
- ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1396
- ...(Object.keys(env).length > 0 ? { env } : {}),
1397
- ...(fetchImpl ? { fetch: fetchImpl } : {}),
1398
- ...(retry.maxRetries !== undefined ? { maxRetries: retry.maxRetries } : {}),
1399
- ...(retry.maxDelayMs !== undefined ? { maxRetryDelayMs: retry.maxDelayMs } : {}),
1400
- ...(retry.baseDelayMs !== undefined ? { retryBaseDelayMs: retry.baseDelayMs } : {}),
1401
- ...(retry.budgetMs !== undefined ? { retryBudgetMs: retry.budgetMs } : {}),
1148
+ sessionId: this.sessionId,
1149
+ outputSpec: this.outputSpec,
1150
+ prompts: this.prompts,
1151
+ googleTier: this.googleTier,
1152
+ callDefaults: this.callDefaults,
1402
1153
  };
1403
1154
  }
1404
1155
 
@@ -1433,7 +1184,7 @@ export class Agent {
1433
1184
  }
1434
1185
  this.persistNow();
1435
1186
 
1436
- const providerOptions = this.buildProviderOptions(runOptions);
1187
+ const providerOptions = buildProviderOptions(this.optionSource(), runOptions);
1437
1188
 
1438
1189
  const loopConfig = {
1439
1190
  agentName: this.name,
@@ -1447,7 +1198,7 @@ export class Agent {
1447
1198
  ...(this.fileAccess ? { fileAccess: this.fileAccess } : {}),
1448
1199
  onFailure: this.onFailure,
1449
1200
  ...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
1450
- ...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1201
+ ...(this.budget || this.chat ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1451
1202
  onProgress: () => this.persistNow(),
1452
1203
  onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
1453
1204
  onToolResult: (r: ToolResultRecord) => this.recordTool(r),
@@ -1457,6 +1208,7 @@ export class Agent {
1457
1208
  onSteerInjected: () => this.persistNow(),
1458
1209
  // Loop enforces these (plus local timeout races).
1459
1210
  limits: { ...this.runLimits },
1211
+ prompts: this.prompts,
1460
1212
  ...(this.output !== undefined ? { output: this.output } : {}),
1461
1213
  };
1462
1214
 
@@ -1496,7 +1248,7 @@ export class Agent {
1496
1248
  return res;
1497
1249
  })();
1498
1250
 
1499
- this.currentRun = { steerInbox, abortController: runController, runPromise: task };
1251
+ this.rc.current = { steerInbox, abortController: runController, runPromise: task };
1500
1252
  task.then(
1501
1253
  (res) => this.endRun(mark, res.finishReason === "stopped" ? "stopped" : "done"),
1502
1254
  (err) => this.endRun(mark, isAbortError(err) ? "aborted" : "failed")
@@ -1505,7 +1257,7 @@ export class Agent {
1505
1257
  try {
1506
1258
  this.cleanupMerge(runController);
1507
1259
  } catch {}
1508
- if (this.currentRun?.runPromise === task) this.finishCurrentRun();
1260
+ if (this.rc.current?.runPromise === task) this.rc.finishCurrentRun();
1509
1261
  };
1510
1262
  task.then(cleanup, cleanup);
1511
1263
  return task;
@@ -1529,7 +1281,7 @@ export class Agent {
1529
1281
  const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
1530
1282
  const steerInbox: SteerEntry[] = [];
1531
1283
 
1532
- const providerOptions = this.buildProviderOptions(runOptions);
1284
+ const providerOptions = buildProviderOptions(this.optionSource(), runOptions);
1533
1285
 
1534
1286
  const loopConfig = {
1535
1287
  agentName: this.name,
@@ -1543,7 +1295,7 @@ export class Agent {
1543
1295
  ...(this.fileAccess ? { fileAccess: this.fileAccess } : {}),
1544
1296
  onFailure: this.onFailure,
1545
1297
  ...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
1546
- ...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1298
+ ...(this.budget || this.chat ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1547
1299
  onProgress: () => this.persistNow(),
1548
1300
  onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
1549
1301
  onToolResult: (r: ToolResultRecord) => this.recordTool(r),
@@ -1552,11 +1304,12 @@ export class Agent {
1552
1304
  onSteerInjected: () => this.persistNow(),
1553
1305
  // Loop enforces these (plus local timeout races).
1554
1306
  limits: { ...this.runLimits },
1307
+ prompts: this.prompts,
1555
1308
  ...(this.output !== undefined ? { output: this.output } : {}),
1556
1309
  };
1557
1310
 
1558
1311
  const s = streamAgentLoop(loopConfig);
1559
- this.currentRun = { steerInbox, outerStream: s, abortController: runController };
1312
+ this.rc.current = { steerInbox, outerStream: s, abortController: runController };
1560
1313
  s.result().then(
1561
1314
  (res) => {
1562
1315
  this.endRun(mark, res.finishReason === "stopped" ? "stopped" : "done");
@@ -1571,14 +1324,14 @@ export class Agent {
1571
1324
  targetContext.messages = [];
1572
1325
  targetContext.thoughtSignatures = [];
1573
1326
  }
1574
- if (this.currentRun?.outerStream === s) this.finishCurrentRun();
1327
+ if (this.rc.current?.outerStream === s) this.rc.finishCurrentRun();
1575
1328
  },
1576
1329
  (err) => {
1577
1330
  this.endRun(mark, isAbortError(err) ? "aborted" : "failed");
1578
1331
  try {
1579
1332
  this.cleanupMerge(runController);
1580
1333
  } catch {}
1581
- if (this.currentRun?.outerStream === s) this.finishCurrentRun();
1334
+ if (this.rc.current?.outerStream === s) this.rc.finishCurrentRun();
1582
1335
  }
1583
1336
  );
1584
1337
  return s;
@@ -1605,7 +1358,7 @@ export class Agent {
1605
1358
 
1606
1359
  /** Session ids whose spawned workers this agent may inspect (default + named sessions). */
1607
1360
  private ownedSessionIds(): ReadonlySet<string> {
1608
- return new Set<string>([this.sessionId, ...this.sessions.keys()]);
1361
+ return new Set<string>([this.sessionId, ...this.named.ids()]);
1609
1362
  }
1610
1363
 
1611
1364
  /** Lists TrackingIDs of worker traces visible to this agent (deduped). */
@@ -1898,7 +1651,7 @@ export class Agent {
1898
1651
  if (options?.additionalContext) {
1899
1652
  // Preserve stable instructions as systemPrompt
1900
1653
  ctx.systemPrompt = fullInstructions;
1901
- const prefix = `[Additional Context]\n${options.additionalContext}\n\n`;
1654
+ const prefix = `${fillPrompt(this.prompts.additionalContext, { context: options.additionalContext })}\n\n`;
1902
1655
  if (typeof prompt === "string") {
1903
1656
  prompt = prefix + prompt;
1904
1657
  } else if (Array.isArray(prompt)) {
@@ -1928,7 +1681,7 @@ export class Agent {
1928
1681
  prompt: e.prompt,
1929
1682
  ...(e.options?.additionalContext ? { additionalContext: e.options.additionalContext } : {}),
1930
1683
  });
1931
- return { steer: (this.currentRun?.steerInbox ?? []).map(pack), queue: this.pendingQueue.map(pack) };
1684
+ return { steer: (this.rc.current?.steerInbox ?? []).map(pack), queue: this.rc.queue.map(pack) };
1932
1685
  }
1933
1686
 
1934
1687
  /**
@@ -1972,10 +1725,10 @@ export class Agent {
1972
1725
  }
1973
1726
 
1974
1727
  // Mid-session: busy agent serializes via steer (same turn) or queue (next turn), or stops the active run and starts this prompt (interrupt).
1975
- if (this.currentRun) {
1728
+ if (this.rc.current) {
1976
1729
  if (options?.enqueue === "interrupt") {
1977
1730
  return new Promise<AgentResponse>((resolve, reject) =>
1978
- this.queueUrgent({ prompt, options, isStream: false, resolve, reject })
1731
+ this.rc.queueUrgent({ prompt, options, isStream: false, resolve, reject })
1979
1732
  );
1980
1733
  }
1981
1734
  const enforced = this.midSessionConfig.mode;
@@ -1985,10 +1738,10 @@ export class Agent {
1985
1738
  `[Agent Accelerator] enqueue "${requested}" is disabled by midSession.mode "${enforced}". Use ${enforced}() or set mode to "auto"/"${requested}".`
1986
1739
  );
1987
1740
  }
1988
- const mode = this.resolveEnqueueMode(requested);
1741
+ const mode = this.rc.resolveEnqueueMode(requested);
1989
1742
  if (mode === "steer") {
1990
1743
  this.assertPromptValid(prompt);
1991
- return this.steerInto(this.currentRun, prompt, options);
1744
+ return this.rc.steerInto(this.rc.current, prompt, options);
1992
1745
  } else {
1993
1746
  if (this.midSessionConfig.mode === "steer") {
1994
1747
  throw new ConfigError(
@@ -1997,19 +1750,19 @@ export class Agent {
1997
1750
  }
1998
1751
  this.assertPromptValid(prompt);
1999
1752
  const max = this.midSessionConfig.maxQueued ?? 20;
2000
- if (this.pendingQueue.length >= max) {
1753
+ if (this.rc.queue.length >= max) {
2001
1754
  throw new ValidationError(
2002
- `[Agent Accelerator] Queue is full (${this.pendingQueue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
1755
+ `[Agent Accelerator] Queue is full (${this.rc.queue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
2003
1756
  );
2004
1757
  }
2005
1758
  return new Promise<AgentResponse>((resolve, reject) => {
2006
- this.pendingQueue.push({ prompt, options, isStream: false, resolve, reject });
1759
+ this.rc.queue.push({ prompt, options, isStream: false, resolve, reject });
2007
1760
  this.persistNow();
2008
1761
  try {
2009
- this.currentRun?.outerStream?.push({
1762
+ this.rc.current?.outerStream?.push({
2010
1763
  type: "queued",
2011
1764
  queuedPrompt: previewPrompt(prompt),
2012
- queueLength: this.pendingQueue.length,
1765
+ queueLength: this.rc.queue.length,
2013
1766
  } as any);
2014
1767
  } catch {}
2015
1768
  });
@@ -2083,7 +1836,7 @@ export class Agent {
2083
1836
  // Same sync validation timing as run().
2084
1837
  this.assertPromptValid(prompt);
2085
1838
  // Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
2086
- if (this.currentRun) {
1839
+ if (this.rc.current) {
2087
1840
  const enforcedStream = this.midSessionConfig.mode;
2088
1841
  const urgent = options?.enqueue === "interrupt"; // stops the active run and goes first, in every mode and whatever the queue holds
2089
1842
  const requestedStream = options?.enqueue === "interrupt" ? undefined : options?.enqueue;
@@ -2096,13 +1849,13 @@ export class Agent {
2096
1849
  `[Agent Accelerator] enqueue "${requestedStream}" is disabled by midSession.mode "${enforcedStream}". Use ${enforcedStream}() or set mode to "auto"/"${requestedStream}".`
2097
1850
  );
2098
1851
  }
2099
- const mode = urgent ? "queue" : this.resolveEnqueueMode(requestedStream);
1852
+ const mode = urgent ? "queue" : this.rc.resolveEnqueueMode(requestedStream);
2100
1853
  if (mode === "steer") {
2101
1854
  this.assertPromptValid(prompt);
2102
- this.assertSteerRoom(this.currentRun.steerInbox);
2103
- this.currentRun.steerInbox.push({ prompt, options });
1855
+ this.rc.assertSteerRoom(this.rc.current.steerInbox);
1856
+ this.rc.current.steerInbox.push({ prompt, options });
2104
1857
  this.persistNow();
2105
- const activeStream = this.currentRun.outerStream;
1858
+ const activeStream = this.rc.current.outerStream;
2106
1859
  if (activeStream) {
2107
1860
  this.wireStreamCallbacks(activeStream, options);
2108
1861
  return activeStream;
@@ -2111,7 +1864,7 @@ export class Agent {
2111
1864
  // with the same final response so streaming callers still get events.
2112
1865
  const stub = new AssistantMessageEventStream();
2113
1866
  this.wireStreamCallbacks(stub, options);
2114
- const activePromise = this.currentRun.runPromise;
1867
+ const activePromise = this.rc.current.runPromise;
2115
1868
  if (activePromise) {
2116
1869
  activePromise.then(
2117
1870
  (res) => {
@@ -2148,9 +1901,9 @@ export class Agent {
2148
1901
  }
2149
1902
  this.assertPromptValid(prompt);
2150
1903
  const max = this.midSessionConfig.maxQueued ?? 20;
2151
- if (!urgent && this.pendingQueue.length >= max) {
1904
+ if (!urgent && this.rc.queue.length >= max) {
2152
1905
  throw new ValidationError(
2153
- `[Agent Accelerator] Queue is full (${this.pendingQueue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
1906
+ `[Agent Accelerator] Queue is full (${this.rc.queue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
2154
1907
  );
2155
1908
  }
2156
1909
  const deferred = new AssistantMessageEventStream();
@@ -2158,10 +1911,10 @@ export class Agent {
2158
1911
  // Notify the active stream (if any) that a follow-up was queued.
2159
1912
  if (!urgent) {
2160
1913
  try {
2161
- this.currentRun.outerStream?.push({
1914
+ this.rc.current.outerStream?.push({
2162
1915
  type: "queued",
2163
1916
  queuedPrompt: previewPrompt(prompt),
2164
- queueLength: this.pendingQueue.length + 1,
1917
+ queueLength: this.rc.queue.length + 1,
2165
1918
  } as any);
2166
1919
  } catch {}
2167
1920
  }
@@ -2192,9 +1945,9 @@ export class Agent {
2192
1945
  // Forward cancellation before start: drop from queue and fail.
2193
1946
  deferred.onCancel(() => {
2194
1947
  try {
2195
- const idx = this.pendingQueue.indexOf(queuedEntry as any);
1948
+ const idx = this.rc.queue.indexOf(queuedEntry as any);
2196
1949
  if (idx >= 0) {
2197
- this.pendingQueue.splice(idx, 1);
1950
+ this.rc.queue.splice(idx, 1);
2198
1951
  queuedEntry.reject(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
2199
1952
  try {
2200
1953
  deferred.fail(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
@@ -2203,9 +1956,9 @@ export class Agent {
2203
1956
  } catch {}
2204
1957
  });
2205
1958
  if (urgent) {
2206
- this.queueUrgent(queuedEntry as Agent["pendingQueue"][number]);
1959
+ this.rc.queueUrgent(queuedEntry as QueuedPrompt);
2207
1960
  } else {
2208
- this.pendingQueue.push(queuedEntry as any);
1961
+ this.rc.queue.push(queuedEntry as any);
2209
1962
  this.persistNow();
2210
1963
  }
2211
1964
  // Attach deferred completion to queued promise without exposing it.
@@ -2243,213 +1996,11 @@ export class Agent {
2243
1996
  return s;
2244
1997
  }
2245
1998
 
2246
- // 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.
1999
+ // Attaches the caller's onEvent/onDelta/onThinkingDelta callbacks to a stream; in: stream()/pumpQueue, out: listeners on the stream.
2247
2000
  private wireStreamCallbacks(s: AssistantMessageEventStream, options?: AgentRunOptions): void {
2248
- // Wire one-liner callbacks so `stream:true` + onDelta is enough — no manual for-await needed
2249
- if (options?.wrapThinking) {
2250
- // Auto-wrap reasoning as <think>…</think> per-turn — clean boundaries across multi-turn agent runs.
2251
- //
2252
- // Display-only whitespace hygiene (cache-safe): provider thought
2253
- // summaries routinely carry edge blank lines (observed `"...\n\n\n"`),
2254
- // and the wrapper itself adds newlines around the span. Without care
2255
- // this renders as blank lines after `<think>`, double blanks between
2256
- // sections, and a stray blank line before `</think>`. The buffering
2257
- // below touches ONLY the presented strings/callback args and synthetic
2258
- // tag events — never provider accumulation, AgentContext history, or
2259
- // request bodies — so prefix-cache affinity is untouched.
2260
- //
2261
- // Trailing-newline buffering: each chunk's trailing `\n` run is
2262
- // withheld and prepended to the next chunk (or dropped at close), so
2263
- // emitted display text never ends with `\n` and `</think>` never gets
2264
- // a preceding blank line. Interior breaks are preserved (collapsed to
2265
- // a single blank line at most).
2266
- let isThinking = false;
2267
- let emittedAny = false;
2268
- let pendingNewlines = "";
2269
- // True once text has been emitted without a trailing newline: the next
2270
- // <think> open tag then needs its own leading newline, otherwise it
2271
- // glues onto the streamed text mid-line. False at stream start and
2272
- // after every tag (both end with newlines), preserving the exact
2273
- // "<think>\n...\n</think>\n" contract for thinking-first streams.
2274
- let needThinkNewline = false;
2275
- // Set on every think close; consumed by the next text_delta(s) to strip
2276
- // model-leading blank lines display-only (cache-safe: outer events only).
2277
- // Stays set until non-empty text is emitted so split "\n" | "\nText"
2278
- // chunks cannot leak a blank line.
2279
- let justClosedThink = false;
2280
- const collapseBlankLines = (s: string): string => s.replace(/\n{3,}/g, "\n\n");
2281
- const userOnThinking = options.onThinkingDelta;
2282
- const userOnDelta = options.onDelta;
2283
- const userOnEvent = options.onEvent;
2284
- if (userOnEvent) s.on("*", userOnEvent as any);
2285
-
2286
- const openThink = (e?: any) => {
2287
- if (!isThinking) {
2288
- isThinking = true;
2289
- emittedAny = false;
2290
- pendingNewlines = "";
2291
- const tag = (needThinkNewline ? "\n" : "") + "<think>\n";
2292
- needThinkNewline = false;
2293
- if (userOnThinking) userOnThinking(tag, e);
2294
- else if (userOnDelta) userOnDelta(tag, e as any);
2295
- }
2296
- };
2297
-
2298
- const closeThink = (e?: any) => {
2299
- if (isThinking) {
2300
- isThinking = false;
2301
- // Drop any withheld trailing newlines: the span ends cleanly and
2302
- // the tag supplies its own single line break. Empty spans still close.
2303
- pendingNewlines = "";
2304
- const close = emittedAny ? "\n</think>\n" : "</think>\n";
2305
- emittedAny = false;
2306
- justClosedThink = true;
2307
- if (userOnThinking) userOnThinking(close, e);
2308
- else if (userOnDelta) userOnDelta(close, e as any);
2309
- }
2310
- };
2311
-
2312
- /**
2313
- * Strips model-leading blank lines after a think close, display-only.
2314
- * Mutates `e.delta` in place so `for-await` consumers see the same clean
2315
- * text; safe because loop context derives from the provider result, not
2316
- * these outer events. Returns the display string (`""` when nothing
2317
- * displayable yet — caller skips the callback but keeps the flag set so
2318
- * a split newline-only chunk cannot leak a blank line).
2319
- */
2320
- const stripLeadingAfterClose = (e: any): string => {
2321
- const raw: string = e.delta ?? "";
2322
- if (!justClosedThink) return raw;
2323
- const stripped = raw.replace(/^\n+/, "");
2324
- try {
2325
- e.delta = stripped;
2326
- } catch {}
2327
- if (!stripped) return "";
2328
- justClosedThink = false;
2329
- return stripped;
2330
- };
2331
-
2332
- /** Buffers one raw thinking chunk, returning the display string to
2333
- * emit now (`""` when nothing displayable yet). Mutates the event in
2334
- * place so manual `for-await` consumers see the same clean text; safe
2335
- * because loop context derives from the provider result, not these
2336
- * outer events. */
2337
- const bufferThinkingEvent = (e: any): string => {
2338
- const raw: string = e.thinkingDelta ?? "";
2339
- let chunk = collapseBlankLines(pendingNewlines + raw);
2340
- pendingNewlines = "";
2341
- if (!emittedAny) {
2342
- chunk = chunk.replace(/^\n+/, "");
2343
- }
2344
- if (!chunk) return "";
2345
- const m = chunk.match(/\n+$/);
2346
- if (m) {
2347
- pendingNewlines = m[0];
2348
- chunk = chunk.slice(0, -m[0].length);
2349
- if (!chunk) return "";
2350
- }
2351
- emittedAny = true;
2352
- e.thinkingDelta = chunk;
2353
- if (typeof e.partialThinking === "string") {
2354
- e.partialThinking = collapseBlankLines(e.partialThinking);
2355
- }
2356
- return chunk;
2357
- };
2358
-
2359
- if (userOnThinking || userOnDelta) {
2360
- s.on("thinking_delta", (e: any) => {
2361
- openThink(e);
2362
- const chunk = bufferThinkingEvent(e);
2363
- if (!chunk) return;
2364
- if (userOnThinking) userOnThinking(chunk, e);
2365
- else if (userOnDelta) userOnDelta(chunk, e as any);
2366
- });
2367
- s.on("text_delta", (e: any) => {
2368
- closeThink(e);
2369
- const display = stripLeadingAfterClose(e);
2370
- if (!display) {
2371
- needThinkNewline = false;
2372
- return;
2373
- }
2374
- needThinkNewline = !display.endsWith("\n");
2375
- if (userOnDelta) userOnDelta(display, e);
2376
- else if (userOnThinking) userOnThinking(display, e as any);
2377
- });
2378
- // Close thinking tag and reset turn state before tool/subagent execution or results
2379
- s.on("tool_call_start" as any, (e: any) => closeThink(e));
2380
- s.on("tool_call_complete" as any, (e: any) => closeThink(e));
2381
- s.on("tool_result" as any, (e: any) => closeThink(e));
2382
- s.on("subagent_complete" as any, (e: any) => closeThink(e));
2383
- s.on("done", (e: any) => {
2384
- closeThink(e || ({ type: "done" } as any));
2385
- });
2386
- } else {
2387
- // No callbacks but wrapThinking true — still emit tags as events for manual iteration.
2388
- // Same display hygiene as above (in-place event normalization is safe:
2389
- // loop context derives from the provider result, not outer events).
2390
- const closeSynthetic = () => {
2391
- if (isThinking) {
2392
- isThinking = false;
2393
- pendingNewlines = "";
2394
- const tag = emittedAny ? "\n</think>\n" : "</think>\n";
2395
- emittedAny = false;
2396
- justClosedThink = true;
2397
- s.push({ type: "thinking_delta", thinkingDelta: tag, __synthetic: true } as any);
2398
- }
2399
- };
2400
- s.on("thinking_delta", (e: any) => {
2401
- // Ignore our own synthetic tags: push() redelivers events to
2402
- // listeners synchronously, so without this guard the handler
2403
- // re-enters on every tag it emits (duplicate <think> spans and
2404
- // mutated queued events).
2405
- if ((e as any).__synthetic) return;
2406
- if (!isThinking) {
2407
- isThinking = true;
2408
- emittedAny = false;
2409
- pendingNewlines = "";
2410
- const openTag = (needThinkNewline ? "\n" : "") + "<think>\n";
2411
- needThinkNewline = false;
2412
- s.push({ type: "thinking_delta", thinkingDelta: openTag, __synthetic: true } as any);
2413
- }
2414
- const chunk = bufferThinkingEvent(e);
2415
- if (!chunk) {
2416
- e.thinkingDelta = "";
2417
- }
2418
- });
2419
- s.on("text_delta", (e: any) => {
2420
- closeSynthetic();
2421
- if (justClosedThink) {
2422
- const stripped: string = (e.delta ?? "").replace(/^\n+/, "");
2423
- try {
2424
- e.delta = stripped;
2425
- } catch {}
2426
- if (!stripped) return;
2427
- justClosedThink = false;
2428
- }
2429
- needThinkNewline = !(e.delta ?? "").endsWith("\n");
2430
- });
2431
- s.on("tool_call_start" as any, () => {
2432
- closeSynthetic();
2433
- });
2434
- s.on("tool_call_complete" as any, () => {
2435
- closeSynthetic();
2436
- });
2437
- s.on("tool_result" as any, () => {
2438
- closeSynthetic();
2439
- });
2440
- s.on("subagent_complete" as any, () => {
2441
- closeSynthetic();
2442
- });
2443
- s.on("done", () => {
2444
- closeSynthetic();
2445
- });
2446
- if (userOnEvent) s.on("*", userOnEvent as any);
2447
- }
2448
- } else {
2449
- if (options?.onEvent) s.on("*", options.onEvent as any);
2450
- if (options?.onDelta) s.on("text_delta", (e: any) => options.onDelta!(e.delta!, e));
2451
- if (options?.onThinkingDelta) s.on("thinking_delta", (e: any) => options.onThinkingDelta!(e.thinkingDelta!, e));
2452
- }
2001
+ if (options?.onEvent) s.on("*", options.onEvent as any);
2002
+ if (options?.onDelta) s.on("text_delta", (e: any) => options.onDelta!(e.delta!, e));
2003
+ if (options?.onThinkingDelta) s.on("thinking_delta", (e: any) => options.onThinkingDelta!(e.thinkingDelta!, e));
2453
2004
  }
2454
2005
  }
2455
2006
 
@@ -2481,57 +2032,3 @@ function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentR
2481
2032
  providerSteps: res.providerSteps,
2482
2033
  });
2483
2034
  }
2484
-
2485
- /** Normalizes the public `thinkingLevel` flag into provider-neutral thinking settings. */
2486
- export function normalizeThinking(config: AgentConfig, fallbackLevel?: string): ThinkingConfig | undefined {
2487
- // DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh, max
2488
- // `thinking` (canonical) wins over legacy `thinkingLevel`; `{ level }` objects are unwrapped.
2489
- const rawLevel = normalizeThinkingV2(config.thinking, config.thinkingLevel) ?? fallbackLevel;
2490
- if (typeof config.thinking === "object" && config.thinking?.budgetTokens !== undefined) {
2491
- // No provider maps an explicit token budget yet: say so instead of ignoring it silently.
2492
- telemetryBus.emitWarning(
2493
- "config.thinking_budget_ignored",
2494
- "[Agent Accelerator] thinking.budgetTokens is not forwarded by any provider adapter yet; the level alone is used.",
2495
- { budgetTokens: config.thinking.budgetTokens }
2496
- );
2497
- }
2498
- const level = rawLevel as ThinkingLevel | undefined;
2499
-
2500
- if (!level) return undefined;
2501
-
2502
- // Validate model thinking from catalog if model is configured
2503
- if (config.model) {
2504
- try {
2505
- const rawModel: any = (config.model as any)?.model ? (config.model as any).model : config.model;
2506
- const modelStr = typeof rawModel === "string" ? rawModel : rawModel?.id || "";
2507
- const provStr =
2508
- typeof rawModel === "object" && rawModel?.provider
2509
- ? rawModel.provider
2510
- : modelStr.includes("/")
2511
- ? modelStr.split("/")[0]
2512
- : "";
2513
- const actualModelId = modelStr.includes("/") ? modelStr.split("/").slice(1).join("/") : modelStr;
2514
- if (actualModelId) {
2515
- validateModelThinking(provStr || "openrouter", actualModelId, level);
2516
- }
2517
- } catch (e) {
2518
- throw e;
2519
- }
2520
- }
2521
-
2522
- if (level === "none") {
2523
- return { enabled: false, level: "none", budgetTokens: 0 };
2524
- }
2525
- if (level === "dynamic") {
2526
- return { enabled: true, level: "dynamic", budgetTokens: -1 };
2527
- }
2528
- return { enabled: true, level };
2529
- }
2530
-
2531
- /** Maps the public `toolChoice` option onto the provider wire-neutral form. */
2532
- function normalizeToolChoiceOption(choice: ToolChoiceOption | undefined): ProviderRequestOptions["toolChoice"] {
2533
- if (!choice) return undefined;
2534
- if (typeof choice === "string") return choice;
2535
- if (choice.tool) return { type: "function", function: { name: choice.tool } };
2536
- return choice.mode;
2537
- }