agent-accelerator 0.6.5 → 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.
@@ -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";
@@ -43,6 +42,18 @@ import { persistTelemetry, registerSessionFolder, unregisterSessionFolder } from
43
42
  import { lockFolder } from "../utils/durable.ts";
44
43
  import { createScheduledActionsTool, getDatetimeTool } from "../scheduler/tool.ts";
45
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";
46
57
  import { UsageLimit, checkUsageLimitConfig } from "./usage-limit.ts";
47
58
  import { mergeCap } from "./defaults.ts";
48
59
  import { applyEditable, assertModelAllowed, modelKey, toEditableConfig } from "./config-blocks.ts";
@@ -98,16 +109,6 @@ import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalo
98
109
  import { buildSessionData, type PersistedAgentSession, type SessionTotals } from "../session/store.ts";
99
110
  import { SessionTelemetry } from "../session/store.ts";
100
111
 
101
- /**
102
- * Resolves a per-run thinking override without mutating agent config.
103
- */
104
- export function resolveEffectiveThinking(base?: ThinkingConfig, overrideLevel?: string): ThinkingConfig | undefined {
105
- if (!overrideLevel) return base;
106
- if (overrideLevel === "none") return { enabled: false, level: "none", budgetTokens: 0 };
107
- if (overrideLevel === "dynamic") return { enabled: true, level: "dynamic", budgetTokens: -1 };
108
- return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
109
- }
110
-
111
112
  import { ConfigError, SessionLoadError, ValidationError, isAbortError } from "../types/errors.ts";
112
113
 
113
114
  /**
@@ -120,43 +121,6 @@ const mergeDisposers = new WeakMap<AbortController, () => void>();
120
121
  /** Waits before run retry 1, 2, 3 ... (the last one repeats). Module-level so tests can shrink it; not public API. */
121
122
  export const RUN_RETRY_DELAYS_MS: number[] = [5_000, 15_000, 45_000, 60_000];
122
123
 
123
- /** Max named sessions kept per Agent (LRU). */
124
- const SESSION_LRU_MAX = 100;
125
- /** Idle TTL for named sessions. */
126
- const SESSION_TTL_MS = 60 * 60 * 1000;
127
-
128
- /** Normalized per-run limits. Sub-agents inherit these unless overridden. */
129
- export interface AgentRunLimits {
130
- maxTurns?: number;
131
- deadlineMs?: number;
132
- maxToolCallsPerRun?: number;
133
- maxTotalTokens?: number;
134
- requestTimeoutMs: number;
135
- firstByteTimeoutMs: number;
136
- streamIdleTimeoutMs: number;
137
- }
138
-
139
- /** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
140
- function resolveAgentLimits(config: { limits?: Record<string, unknown> }): AgentRunLimits {
141
- const raw = (config as { limits?: Record<string, unknown> }).limits ?? {};
142
- const num = (v: unknown): number | undefined => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
143
- // A network timeout: `0` or `Infinity` turns it off, any other number is milliseconds, anything else is unset (the default applies).
144
- const timeout = (v: unknown): number | undefined => (typeof v === "number" && !Number.isNaN(v) ? v : undefined);
145
- return {
146
- ...(num(raw.maxTurns) !== undefined ? { maxTurns: Math.max(1, Math.floor(num(raw.maxTurns)!)) } : {}),
147
- ...(num(raw.deadlineMs) !== undefined ? { deadlineMs: num(raw.deadlineMs)! } : {}),
148
- ...(num(raw.maxToolCallsPerRun) !== undefined
149
- ? { maxToolCallsPerRun: Math.max(1, Math.floor(num(raw.maxToolCallsPerRun)!)) }
150
- : {}),
151
- ...(num(raw.maxTotalTokens) !== undefined
152
- ? { maxTotalTokens: Math.max(1, Math.floor(num(raw.maxTotalTokens)!)) }
153
- : {}),
154
- requestTimeoutMs: timeout(raw.requestTimeoutMs) ?? 300_000,
155
- firstByteTimeoutMs: timeout(raw.firstByteTimeoutMs) ?? 120_000,
156
- streamIdleTimeoutMs: timeout(raw.streamIdleTimeoutMs) ?? 300_000,
157
- };
158
- }
159
-
160
124
  /** Thrown when dynamic sub-agent spawning is enabled without an explicit model. */
161
125
  export class SubAgentModelError extends ConfigError {
162
126
  readonly parentModel?: string;
@@ -297,24 +261,14 @@ export class Agent {
297
261
  * request 300s / first-byte 120s / idle 300s (tolerate reasoning).
298
262
  */
299
263
  readonly runLimits: AgentRunLimits;
300
- private currentRun: {
301
- steerInbox: SteerEntry[];
302
- outerStream?: AssistantMessageEventStream;
303
- runPromise?: Promise<AgentResponse>;
304
- abortController: AbortController;
305
- } | null = null;
306
- private pendingQueue: Array<{
307
- prompt: string | ContentPart[];
308
- options?: AgentRunOptions;
309
- isStream: boolean;
310
- deferredStream?: AssistantMessageEventStream;
311
- resolve: (res: AgentResponse) => void;
312
- reject: (err: unknown) => void;
313
- /** Put here by `interrupt(message)`: runs before every other waiting prompt. */
314
- urgent?: boolean;
315
- }> = [];
264
+ /** The active run and the waiting queue (see `run-control.ts`). */
265
+ private readonly rc: RunControl;
316
266
  /** Named session contexts for `sessionId` runs (LRU 100 + TTL). */
317
- 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
+ );
318
272
  /** Coalesced persistence single-flight flag. */
319
273
  private persistScheduled = false;
320
274
  /** Step journal of the persisted session folder (dir mode only). */
@@ -493,6 +447,17 @@ export class Agent {
493
447
  };
494
448
  // Bound on buffered steer entries (default 20).
495
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
+ });
496
461
  // Run limits (sub-agents inherit unless overridden).
497
462
  this.runLimits = resolveAgentLimits(config as unknown as { limits?: Record<string, unknown> });
498
463
 
@@ -655,7 +620,7 @@ export class Agent {
655
620
  const open = new Set(unfinishedToolCalls(this.context.messages).map((c) => c.id));
656
621
  this.toolRecords.splice(0, this.toolRecords.length, ...(session.tools ?? []).filter((t) => open.has(t.id)));
657
622
  for (const w of waiting) {
658
- this.pendingQueue.push({
623
+ this.rc.queue.push({
659
624
  prompt: w.prompt,
660
625
  options: w.additionalContext ? { additionalContext: w.additionalContext } : undefined,
661
626
  isStream: false,
@@ -745,7 +710,7 @@ export class Agent {
745
710
  */
746
711
  resume(): Promise<AgentResponse | undefined> {
747
712
  if (this.resuming) return this.resuming;
748
- if (this.currentRun)
713
+ if (this.rc.current)
749
714
  throw new ValidationError("[Agent Accelerator] resume() needs an idle agent: a run is already active.");
750
715
  const mark = this.runMarks.filter((m) => !m.end && m !== this.activeMark).pop();
751
716
  const last = this.context.messages[this.context.messages.length - 1];
@@ -753,7 +718,7 @@ export class Agent {
753
718
  if (!mark || (nothingOpen && (last?.role === "assistant" || endsWithStop(this.context.messages)))) {
754
719
  // The answer was complete (or a tool had ended the run); only the note that it ended was lost.
755
720
  if (mark) this.endRun(mark, endsWithStop(this.context.messages) ? "stopped" : "done");
756
- this.pumpQueue();
721
+ this.rc.pumpQueue();
757
722
  return Promise.resolve(undefined);
758
723
  }
759
724
  telemetryBus.emit({
@@ -859,88 +824,25 @@ export class Agent {
859
824
  /** Drops one named session. No-op for unknown ids. */
860
825
  disposeSession(id: string): void {
861
826
  unregisterSessionFolder(id);
862
- try {
863
- this.sessions.delete(id);
864
- } catch {}
865
- try {
866
- clearInteractionChains(id);
867
- } catch {}
868
- try {
869
- clearSessionRouting(id);
870
- } catch {}
827
+ this.named.dispose(id);
871
828
  }
872
829
 
873
830
  /** Drops all named sessions except the default context. */
874
831
  clearSessions(): void {
875
- try {
876
- for (const id of [...this.sessions.keys()]) {
877
- try {
878
- clearInteractionChains(id);
879
- } catch {}
880
- try {
881
- clearSessionRouting(id);
882
- } catch {}
883
- }
884
- this.sessions.clear();
885
- } catch {}
832
+ this.named.clear();
886
833
  }
887
834
 
888
- /** 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.
889
836
  private getSessionContext(sessionId?: string): AgentContext {
890
- const sid = sessionId || this.sessionId;
891
- if (sid === this.sessionId) return this.context;
892
- const now = Date.now();
893
- // TTL eviction.
894
- try {
895
- for (const [key, entry] of [...this.sessions.entries()]) {
896
- if (now - entry.lastAccess > SESSION_TTL_MS) {
897
- this.sessions.delete(key);
898
- try {
899
- clearInteractionChains(key);
900
- } catch {}
901
- try {
902
- clearSessionRouting(key);
903
- } catch {}
904
- }
905
- }
906
- } catch {}
907
- const existing = this.sessions.get(sid);
908
- if (existing) {
909
- existing.lastAccess = now;
910
- return existing.context;
911
- }
912
- const fresh = new AgentContext(this.getFullInstructions());
913
- this.sessions.set(sid, { context: fresh, lastAccess: now });
914
- // LRU eviction (oldest lastAccess first).
915
- try {
916
- while (this.sessions.size > SESSION_LRU_MAX) {
917
- let oldest: string | undefined;
918
- let oldestTs = Infinity;
919
- for (const [key, entry] of this.sessions) {
920
- if (entry.lastAccess < oldestTs) {
921
- oldestTs = entry.lastAccess;
922
- oldest = key;
923
- }
924
- }
925
- if (!oldest) break;
926
- this.sessions.delete(oldest);
927
- try {
928
- clearInteractionChains(oldest);
929
- } catch {}
930
- try {
931
- clearSessionRouting(oldest);
932
- } catch {}
933
- }
934
- } catch {}
935
- return fresh;
837
+ return this.named.contextFor(sessionId);
936
838
  }
937
839
 
938
840
  /** Rejects + drops all queued follow-ups. Returns the cleared count. */
939
841
  clearQueue(): number {
940
- const n = this.pendingQueue.length;
842
+ const n = this.rc.queue.length;
941
843
  if (n === 0) return 0;
942
844
  const err = Object.assign(new Error("Queue cleared"), { name: "AbortError" });
943
- const pending = this.pendingQueue.splice(0);
845
+ const pending = this.rc.queue.splice(0);
944
846
  for (const entry of pending) {
945
847
  try {
946
848
  entry.reject(err);
@@ -953,32 +855,22 @@ export class Agent {
953
855
  return n;
954
856
  }
955
857
 
956
- /** Throws when the steer inbox is at its bound. */
957
- private assertSteerRoom(inbox: SteerEntry[]): void {
958
- const max = this.maxSteers ?? 20;
959
- if (inbox.length >= max) {
960
- throw new ValidationError(
961
- `[Agent Accelerator] Steer inbox is full (${inbox.length}/${max}). Wait for the active run to drain or increase midSession.maxSteers.`
962
- );
963
- }
964
- }
965
-
966
858
  /**
967
859
  * True while a `run()`/`stream()` turn is active on this agent.
968
860
  * Use with `steer()` (current-turn redirect) vs `queue()` (next-turn follow-up).
969
861
  */
970
862
  get isBusy(): boolean {
971
- return this.currentRun !== null;
863
+ return this.rc.current !== null;
972
864
  }
973
865
 
974
866
  /** Number of queued follow-up requests waiting for the active run to finish. */
975
867
  get pendingCount(): number {
976
- return this.pendingQueue.length;
868
+ return this.rc.queue.length;
977
869
  }
978
870
 
979
871
  /** Number of steer messages buffered for injection into the active turn. */
980
872
  get steerPendingCount(): number {
981
- return this.currentRun?.steerInbox.length ?? 0;
873
+ return this.rc.current?.steerInbox.length ?? 0;
982
874
  }
983
875
 
984
876
  /** Effective mid-session policy (`steer` | `queue` | `auto`). */
@@ -986,13 +878,6 @@ export class Agent {
986
878
  return this.midSessionConfig.mode;
987
879
  }
988
880
 
989
- // 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.
990
- private resolveEnqueueMode(requested?: "steer" | "queue"): "steer" | "queue" {
991
- const enforced = this.midSessionConfig.mode;
992
- if (enforced === "steer" || enforced === "queue") return enforced;
993
- return requested === "steer" ? "steer" : "queue";
994
- }
995
-
996
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.
997
882
  private assertPromptValid(prompt: string | ContentPart[]): void {
998
883
  if (Array.isArray(prompt) && prompt.length === 0) {
@@ -1021,28 +906,7 @@ export class Agent {
1021
906
  }
1022
907
  return this.run(arg, { ...options, enqueue: "interrupt" });
1023
908
  }
1024
- this.abortActive(arg instanceof Error ? arg : undefined);
1025
- }
1026
-
1027
- // 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.
1028
- private abortActive(reason?: Error): void {
1029
- const active = this.currentRun;
1030
- if (!active) return;
1031
- try {
1032
- active.abortController.abort(reason ?? new Error("Agent interrupted"));
1033
- } catch {}
1034
- try {
1035
- active.outerStream?.cancel(reason);
1036
- } catch {}
1037
- }
1038
-
1039
- // 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.
1040
- private queueUrgent(entry: Agent["pendingQueue"][number]): void {
1041
- entry.urgent = true;
1042
- const firstOrdinary = this.pendingQueue.findIndex((e) => !e.urgent);
1043
- this.pendingQueue.splice(firstOrdinary < 0 ? this.pendingQueue.length : firstOrdinary, 0, entry);
1044
- this.persistNow();
1045
- this.abortActive();
909
+ this.rc.abortActive(arg instanceof Error ? arg : undefined);
1046
910
  }
1047
911
 
1048
912
  /**
@@ -1068,41 +932,9 @@ export class Agent {
1068
932
  '[Agent Accelerator] steer() is disabled by midSession.mode "queue". Use queue() or set mode to "auto"/"steer".'
1069
933
  );
1070
934
  }
1071
- const active = this.currentRun;
935
+ const active = this.rc.current;
1072
936
  if (!active) return this.run(prompt, options) as Promise<AgentResponse>;
1073
- return this.steerInto(active, prompt, options);
1074
- }
1075
-
1076
- /**
1077
- * Buffers a steer prompt into the active run and returns a promise for ITS
1078
- * reply: the active run's response when the prompt is injected, or (if the
1079
- * run ends first and the prompt becomes a queued follow-up) the follow-up
1080
- * run's response, so a late steer's answer is never lost.
1081
- */
1082
- private steerInto(
1083
- active: NonNullable<Agent["currentRun"]>,
1084
- prompt: string | ContentPart[],
1085
- options?: AgentRunOptions
1086
- ): Promise<AgentResponse> {
1087
- this.assertSteerRoom(active.steerInbox);
1088
- const runDone = active.runPromise ?? active.outerStream?.result();
1089
- if (!runDone) return this.run(prompt, options) as Promise<AgentResponse>;
1090
- const entry: SteerEntry = { prompt, options };
1091
- const reply = new Promise<AgentResponse>((resolve, reject) => {
1092
- entry.settle = { resolve, reject };
1093
- });
1094
- reply.catch(() => {}); // a fire-and-forget caller must not trigger an unhandled rejection
1095
- active.steerInbox.push(entry);
1096
- this.persistNow();
1097
- runDone.then(
1098
- (res) => {
1099
- if (!entry.settle?.deferredToQueue) entry.settle?.resolve(res);
1100
- },
1101
- (err) => {
1102
- if (!entry.settle?.deferredToQueue) entry.settle?.reject(err);
1103
- }
1104
- );
1105
- return reply;
937
+ return this.rc.steerInto(active, prompt, options);
1106
938
  }
1107
939
 
1108
940
  /**
@@ -1126,157 +958,26 @@ export class Agent {
1126
958
  '[Agent Accelerator] queue() is disabled by midSession.mode "steer". Use steer() or set mode to "auto"/"queue".'
1127
959
  );
1128
960
  }
1129
- if (!this.currentRun) return this.run(prompt, options) as Promise<AgentResponse>;
961
+ if (!this.rc.current) return this.run(prompt, options) as Promise<AgentResponse>;
1130
962
  const max = this.midSessionConfig.maxQueued ?? 20;
1131
- if (this.pendingQueue.length >= max) {
963
+ if (this.rc.queue.length >= max) {
1132
964
  throw new ValidationError(
1133
- `[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.`
1134
966
  );
1135
967
  }
1136
968
  return new Promise<AgentResponse>((resolve, reject) => {
1137
- this.pendingQueue.push({ prompt, options, isStream: false, resolve, reject });
969
+ this.rc.queue.push({ prompt, options, isStream: false, resolve, reject });
1138
970
  this.persistNow();
1139
971
  try {
1140
- this.currentRun?.outerStream?.push({
972
+ this.rc.current?.outerStream?.push({
1141
973
  type: "queued",
1142
974
  queuedPrompt: previewPrompt(prompt),
1143
- queueLength: this.pendingQueue.length,
975
+ queueLength: this.rc.queue.length,
1144
976
  } as any);
1145
977
  } catch {}
1146
978
  });
1147
979
  }
1148
980
 
1149
- // 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.
1150
- private pumpQueue(): void {
1151
- if (this.currentRun !== null) return;
1152
- const next = this.pendingQueue.shift();
1153
- if (!next) return;
1154
- this.persistNow();
1155
- // Reject queued items whose signal aborted before start.
1156
- try {
1157
- if ((next.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
1158
- const reason = ((next.options as { signal?: AbortSignal }).signal as unknown as { reason?: unknown })?.reason;
1159
- const abortErr = Object.assign(
1160
- reason instanceof Error ? reason : new Error("Queued run aborted before start"),
1161
- { name: "AbortError" }
1162
- );
1163
- try {
1164
- next.reject(abortErr);
1165
- } catch {}
1166
- try {
1167
- next.deferredStream?.fail(abortErr instanceof Error ? abortErr : new Error(String(abortErr)));
1168
- } catch {}
1169
- queueMicrotask(() => this.pumpQueue());
1170
- return;
1171
- }
1172
- } catch {}
1173
- if (next.isStream && next.deferredStream) {
1174
- const deferred = next.deferredStream;
1175
- try {
1176
- const real = this.startStreamingRun(next.prompt, next.options);
1177
- try {
1178
- (deferred as any).__piped = true;
1179
- } catch {}
1180
- const forward = (e: StreamEvent) => {
1181
- try {
1182
- deferred.push(e);
1183
- } catch {}
1184
- };
1185
- real.on("*", forward as any);
1186
- try {
1187
- deferred.onCancel(() => {
1188
- try {
1189
- real.cancel();
1190
- } catch {}
1191
- });
1192
- } catch {}
1193
- real.result().then(
1194
- (res) => {
1195
- try {
1196
- next.resolve(res);
1197
- } catch {}
1198
- try {
1199
- deferred.end(res);
1200
- } catch {}
1201
- },
1202
- (err) => {
1203
- try {
1204
- next.reject(err);
1205
- } catch {}
1206
- // Pipe failures into the deferred stream so for-await sees them.
1207
- try {
1208
- deferred.fail(err instanceof Error ? err : new Error(String(err)));
1209
- } catch {}
1210
- }
1211
- );
1212
- } catch (err) {
1213
- try {
1214
- next.reject(err);
1215
- } catch {}
1216
- try {
1217
- deferred.fail(err instanceof Error ? err : new Error(String(err)));
1218
- } catch {}
1219
- // Keep draining even if starting failed.
1220
- queueMicrotask(() => this.pumpQueue());
1221
- }
1222
- return;
1223
- }
1224
- this.startNonStreamingRun(next.prompt, next.options).then(next.resolve, next.reject);
1225
- }
1226
-
1227
- // 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.
1228
- private finishCurrentRun(): void {
1229
- // Leftover steer entries become queued follow-ups instead of dropping.
1230
- let leftover: SteerEntry[] = [];
1231
- try {
1232
- leftover = this.currentRun?.steerInbox?.splice(0) ?? [];
1233
- } catch {
1234
- leftover = [];
1235
- }
1236
- this.currentRun = null;
1237
- if (leftover.length > 0) {
1238
- try {
1239
- const max = this.midSessionConfig.maxQueued ?? 20;
1240
- // Late steers were typed BEFORE anything already queued: they go to the front, in the order they arrived.
1241
- const lateSteers: Agent["pendingQueue"] = [];
1242
- for (const entry of leftover) {
1243
- try {
1244
- if ((entry.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
1245
- entry.settle?.reject(
1246
- Object.assign(new Error("Steer aborted before it could run"), { name: "AbortError" })
1247
- );
1248
- continue;
1249
- }
1250
- } catch {
1251
- continue;
1252
- }
1253
- if (this.pendingQueue.length + lateSteers.length >= max) {
1254
- entry.settle?.reject(
1255
- new ValidationError("[Agent Accelerator] Steer dropped: the follow-up queue is full.")
1256
- );
1257
- continue;
1258
- }
1259
- // The steer was never injected: it now runs as a follow-up, and the
1260
- // caller's promise resolves with that run's response.
1261
- if (entry.settle) entry.settle.deferredToQueue = true;
1262
- lateSteers.push({
1263
- prompt: entry.prompt,
1264
- options: entry.options,
1265
- isStream: false,
1266
- resolve: entry.settle?.resolve ?? (() => {}),
1267
- reject: entry.settle?.reject ?? (() => {}),
1268
- });
1269
- }
1270
- // ...but behind an interrupt(message), which is the newest and most urgent instruction.
1271
- const firstOrdinary = this.pendingQueue.findIndex((e) => !e.urgent);
1272
- this.pendingQueue.splice(firstOrdinary < 0 ? this.pendingQueue.length : firstOrdinary, 0, ...lateSteers);
1273
- } catch {}
1274
- }
1275
- this.persistNow();
1276
- // Defer pump so the completing run's .then handlers settle first.
1277
- queueMicrotask(() => this.pumpQueue());
1278
- }
1279
-
1280
981
  // Removes the abort listener a run's merged signal fallback installed; in: a settled run's controller, out: nothing.
1281
982
  private cleanupMerge(runController: AbortController): void {
1282
983
  try {
@@ -1435,38 +1136,20 @@ export class Agent {
1435
1136
  return level ? { ...runOptions, thinkingLevel: level } : runOptions;
1436
1137
  }
1437
1138
 
1438
- /**
1439
- * Single place that turns agent config + per-run options into the options
1440
- * every provider call receives. Per-run values override agent defaults
1441
- * (objects such as `retry` and `env` merge key by key).
1442
- */
1443
- private buildProviderOptions(runOptions: AgentRunOptions | undefined): ProviderRequestOptions {
1444
- const d = this.callDefaults;
1445
- const sessionId = runOptions?.sessionId || this.sessionId;
1446
- const toolChoice = normalizeToolChoiceOption(runOptions?.toolChoice ?? d.toolChoice);
1447
- const retry = { ...d.retry, ...runOptions?.retry };
1448
- const env = { ...d.env, ...runOptions?.env };
1449
- const maxOutputTokens = runOptions?.maxOutputTokens ?? d.maxOutputTokens;
1450
- 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 {
1451
1141
  return {
1452
1142
  apiKey: this.apiKey,
1453
1143
  baseUrl: this.baseUrl,
1454
- headers: { ...(this.customHeaders ?? {}), ...(runOptions?.headers ?? {}) },
1455
- thinking: resolveEffectiveThinking(this.thinkingConfig, runOptions?.thinkingLevel),
1456
- ...(this.cache ? { cache: this.cache } : {}),
1144
+ customHeaders: this.customHeaders,
1145
+ thinkingConfig: this.thinkingConfig,
1146
+ cache: this.cache,
1457
1147
  serviceTier: this.serviceTier,
1458
- sessionId,
1459
- ...(this.outputSpec ? { output: this.outputSpec } : {}),
1460
- prompts: { structuredOutput: this.prompts.structuredOutput, toolBridge: this.prompts.toolBridge },
1461
- ...(this.googleTier ? { googleTier: this.googleTier } : {}),
1462
- ...(toolChoice !== undefined ? { toolChoice } : {}),
1463
- ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
1464
- ...(Object.keys(env).length > 0 ? { env } : {}),
1465
- ...(fetchImpl ? { fetch: fetchImpl } : {}),
1466
- ...(retry.maxRetries !== undefined ? { maxRetries: retry.maxRetries } : {}),
1467
- ...(retry.maxDelayMs !== undefined ? { maxRetryDelayMs: retry.maxDelayMs } : {}),
1468
- ...(retry.baseDelayMs !== undefined ? { retryBaseDelayMs: retry.baseDelayMs } : {}),
1469
- ...(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,
1470
1153
  };
1471
1154
  }
1472
1155
 
@@ -1501,7 +1184,7 @@ export class Agent {
1501
1184
  }
1502
1185
  this.persistNow();
1503
1186
 
1504
- const providerOptions = this.buildProviderOptions(runOptions);
1187
+ const providerOptions = buildProviderOptions(this.optionSource(), runOptions);
1505
1188
 
1506
1189
  const loopConfig = {
1507
1190
  agentName: this.name,
@@ -1565,7 +1248,7 @@ export class Agent {
1565
1248
  return res;
1566
1249
  })();
1567
1250
 
1568
- this.currentRun = { steerInbox, abortController: runController, runPromise: task };
1251
+ this.rc.current = { steerInbox, abortController: runController, runPromise: task };
1569
1252
  task.then(
1570
1253
  (res) => this.endRun(mark, res.finishReason === "stopped" ? "stopped" : "done"),
1571
1254
  (err) => this.endRun(mark, isAbortError(err) ? "aborted" : "failed")
@@ -1574,7 +1257,7 @@ export class Agent {
1574
1257
  try {
1575
1258
  this.cleanupMerge(runController);
1576
1259
  } catch {}
1577
- if (this.currentRun?.runPromise === task) this.finishCurrentRun();
1260
+ if (this.rc.current?.runPromise === task) this.rc.finishCurrentRun();
1578
1261
  };
1579
1262
  task.then(cleanup, cleanup);
1580
1263
  return task;
@@ -1598,7 +1281,7 @@ export class Agent {
1598
1281
  const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
1599
1282
  const steerInbox: SteerEntry[] = [];
1600
1283
 
1601
- const providerOptions = this.buildProviderOptions(runOptions);
1284
+ const providerOptions = buildProviderOptions(this.optionSource(), runOptions);
1602
1285
 
1603
1286
  const loopConfig = {
1604
1287
  agentName: this.name,
@@ -1626,7 +1309,7 @@ export class Agent {
1626
1309
  };
1627
1310
 
1628
1311
  const s = streamAgentLoop(loopConfig);
1629
- this.currentRun = { steerInbox, outerStream: s, abortController: runController };
1312
+ this.rc.current = { steerInbox, outerStream: s, abortController: runController };
1630
1313
  s.result().then(
1631
1314
  (res) => {
1632
1315
  this.endRun(mark, res.finishReason === "stopped" ? "stopped" : "done");
@@ -1641,14 +1324,14 @@ export class Agent {
1641
1324
  targetContext.messages = [];
1642
1325
  targetContext.thoughtSignatures = [];
1643
1326
  }
1644
- if (this.currentRun?.outerStream === s) this.finishCurrentRun();
1327
+ if (this.rc.current?.outerStream === s) this.rc.finishCurrentRun();
1645
1328
  },
1646
1329
  (err) => {
1647
1330
  this.endRun(mark, isAbortError(err) ? "aborted" : "failed");
1648
1331
  try {
1649
1332
  this.cleanupMerge(runController);
1650
1333
  } catch {}
1651
- if (this.currentRun?.outerStream === s) this.finishCurrentRun();
1334
+ if (this.rc.current?.outerStream === s) this.rc.finishCurrentRun();
1652
1335
  }
1653
1336
  );
1654
1337
  return s;
@@ -1675,7 +1358,7 @@ export class Agent {
1675
1358
 
1676
1359
  /** Session ids whose spawned workers this agent may inspect (default + named sessions). */
1677
1360
  private ownedSessionIds(): ReadonlySet<string> {
1678
- return new Set<string>([this.sessionId, ...this.sessions.keys()]);
1361
+ return new Set<string>([this.sessionId, ...this.named.ids()]);
1679
1362
  }
1680
1363
 
1681
1364
  /** Lists TrackingIDs of worker traces visible to this agent (deduped). */
@@ -1998,7 +1681,7 @@ export class Agent {
1998
1681
  prompt: e.prompt,
1999
1682
  ...(e.options?.additionalContext ? { additionalContext: e.options.additionalContext } : {}),
2000
1683
  });
2001
- 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) };
2002
1685
  }
2003
1686
 
2004
1687
  /**
@@ -2042,10 +1725,10 @@ export class Agent {
2042
1725
  }
2043
1726
 
2044
1727
  // Mid-session: busy agent serializes via steer (same turn) or queue (next turn), or stops the active run and starts this prompt (interrupt).
2045
- if (this.currentRun) {
1728
+ if (this.rc.current) {
2046
1729
  if (options?.enqueue === "interrupt") {
2047
1730
  return new Promise<AgentResponse>((resolve, reject) =>
2048
- this.queueUrgent({ prompt, options, isStream: false, resolve, reject })
1731
+ this.rc.queueUrgent({ prompt, options, isStream: false, resolve, reject })
2049
1732
  );
2050
1733
  }
2051
1734
  const enforced = this.midSessionConfig.mode;
@@ -2055,10 +1738,10 @@ export class Agent {
2055
1738
  `[Agent Accelerator] enqueue "${requested}" is disabled by midSession.mode "${enforced}". Use ${enforced}() or set mode to "auto"/"${requested}".`
2056
1739
  );
2057
1740
  }
2058
- const mode = this.resolveEnqueueMode(requested);
1741
+ const mode = this.rc.resolveEnqueueMode(requested);
2059
1742
  if (mode === "steer") {
2060
1743
  this.assertPromptValid(prompt);
2061
- return this.steerInto(this.currentRun, prompt, options);
1744
+ return this.rc.steerInto(this.rc.current, prompt, options);
2062
1745
  } else {
2063
1746
  if (this.midSessionConfig.mode === "steer") {
2064
1747
  throw new ConfigError(
@@ -2067,19 +1750,19 @@ export class Agent {
2067
1750
  }
2068
1751
  this.assertPromptValid(prompt);
2069
1752
  const max = this.midSessionConfig.maxQueued ?? 20;
2070
- if (this.pendingQueue.length >= max) {
1753
+ if (this.rc.queue.length >= max) {
2071
1754
  throw new ValidationError(
2072
- `[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.`
2073
1756
  );
2074
1757
  }
2075
1758
  return new Promise<AgentResponse>((resolve, reject) => {
2076
- this.pendingQueue.push({ prompt, options, isStream: false, resolve, reject });
1759
+ this.rc.queue.push({ prompt, options, isStream: false, resolve, reject });
2077
1760
  this.persistNow();
2078
1761
  try {
2079
- this.currentRun?.outerStream?.push({
1762
+ this.rc.current?.outerStream?.push({
2080
1763
  type: "queued",
2081
1764
  queuedPrompt: previewPrompt(prompt),
2082
- queueLength: this.pendingQueue.length,
1765
+ queueLength: this.rc.queue.length,
2083
1766
  } as any);
2084
1767
  } catch {}
2085
1768
  });
@@ -2153,7 +1836,7 @@ export class Agent {
2153
1836
  // Same sync validation timing as run().
2154
1837
  this.assertPromptValid(prompt);
2155
1838
  // Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
2156
- if (this.currentRun) {
1839
+ if (this.rc.current) {
2157
1840
  const enforcedStream = this.midSessionConfig.mode;
2158
1841
  const urgent = options?.enqueue === "interrupt"; // stops the active run and goes first, in every mode and whatever the queue holds
2159
1842
  const requestedStream = options?.enqueue === "interrupt" ? undefined : options?.enqueue;
@@ -2166,13 +1849,13 @@ export class Agent {
2166
1849
  `[Agent Accelerator] enqueue "${requestedStream}" is disabled by midSession.mode "${enforcedStream}". Use ${enforcedStream}() or set mode to "auto"/"${requestedStream}".`
2167
1850
  );
2168
1851
  }
2169
- const mode = urgent ? "queue" : this.resolveEnqueueMode(requestedStream);
1852
+ const mode = urgent ? "queue" : this.rc.resolveEnqueueMode(requestedStream);
2170
1853
  if (mode === "steer") {
2171
1854
  this.assertPromptValid(prompt);
2172
- this.assertSteerRoom(this.currentRun.steerInbox);
2173
- this.currentRun.steerInbox.push({ prompt, options });
1855
+ this.rc.assertSteerRoom(this.rc.current.steerInbox);
1856
+ this.rc.current.steerInbox.push({ prompt, options });
2174
1857
  this.persistNow();
2175
- const activeStream = this.currentRun.outerStream;
1858
+ const activeStream = this.rc.current.outerStream;
2176
1859
  if (activeStream) {
2177
1860
  this.wireStreamCallbacks(activeStream, options);
2178
1861
  return activeStream;
@@ -2181,7 +1864,7 @@ export class Agent {
2181
1864
  // with the same final response so streaming callers still get events.
2182
1865
  const stub = new AssistantMessageEventStream();
2183
1866
  this.wireStreamCallbacks(stub, options);
2184
- const activePromise = this.currentRun.runPromise;
1867
+ const activePromise = this.rc.current.runPromise;
2185
1868
  if (activePromise) {
2186
1869
  activePromise.then(
2187
1870
  (res) => {
@@ -2218,9 +1901,9 @@ export class Agent {
2218
1901
  }
2219
1902
  this.assertPromptValid(prompt);
2220
1903
  const max = this.midSessionConfig.maxQueued ?? 20;
2221
- if (!urgent && this.pendingQueue.length >= max) {
1904
+ if (!urgent && this.rc.queue.length >= max) {
2222
1905
  throw new ValidationError(
2223
- `[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.`
2224
1907
  );
2225
1908
  }
2226
1909
  const deferred = new AssistantMessageEventStream();
@@ -2228,10 +1911,10 @@ export class Agent {
2228
1911
  // Notify the active stream (if any) that a follow-up was queued.
2229
1912
  if (!urgent) {
2230
1913
  try {
2231
- this.currentRun.outerStream?.push({
1914
+ this.rc.current.outerStream?.push({
2232
1915
  type: "queued",
2233
1916
  queuedPrompt: previewPrompt(prompt),
2234
- queueLength: this.pendingQueue.length + 1,
1917
+ queueLength: this.rc.queue.length + 1,
2235
1918
  } as any);
2236
1919
  } catch {}
2237
1920
  }
@@ -2262,9 +1945,9 @@ export class Agent {
2262
1945
  // Forward cancellation before start: drop from queue and fail.
2263
1946
  deferred.onCancel(() => {
2264
1947
  try {
2265
- const idx = this.pendingQueue.indexOf(queuedEntry as any);
1948
+ const idx = this.rc.queue.indexOf(queuedEntry as any);
2266
1949
  if (idx >= 0) {
2267
- this.pendingQueue.splice(idx, 1);
1950
+ this.rc.queue.splice(idx, 1);
2268
1951
  queuedEntry.reject(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
2269
1952
  try {
2270
1953
  deferred.fail(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
@@ -2273,9 +1956,9 @@ export class Agent {
2273
1956
  } catch {}
2274
1957
  });
2275
1958
  if (urgent) {
2276
- this.queueUrgent(queuedEntry as Agent["pendingQueue"][number]);
1959
+ this.rc.queueUrgent(queuedEntry as QueuedPrompt);
2277
1960
  } else {
2278
- this.pendingQueue.push(queuedEntry as any);
1961
+ this.rc.queue.push(queuedEntry as any);
2279
1962
  this.persistNow();
2280
1963
  }
2281
1964
  // Attach deferred completion to queued promise without exposing it.
@@ -2313,213 +1996,11 @@ export class Agent {
2313
1996
  return s;
2314
1997
  }
2315
1998
 
2316
- // 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.
2317
2000
  private wireStreamCallbacks(s: AssistantMessageEventStream, options?: AgentRunOptions): void {
2318
- // Wire one-liner callbacks so `stream:true` + onDelta is enough — no manual for-await needed
2319
- if (options?.wrapThinking) {
2320
- // Auto-wrap reasoning as <think>…</think> per-turn — clean boundaries across multi-turn agent runs.
2321
- //
2322
- // Display-only whitespace hygiene (cache-safe): provider thought
2323
- // summaries routinely carry edge blank lines (observed `"...\n\n\n"`),
2324
- // and the wrapper itself adds newlines around the span. Without care
2325
- // this renders as blank lines after `<think>`, double blanks between
2326
- // sections, and a stray blank line before `</think>`. The buffering
2327
- // below touches ONLY the presented strings/callback args and synthetic
2328
- // tag events — never provider accumulation, AgentContext history, or
2329
- // request bodies — so prefix-cache affinity is untouched.
2330
- //
2331
- // Trailing-newline buffering: each chunk's trailing `\n` run is
2332
- // withheld and prepended to the next chunk (or dropped at close), so
2333
- // emitted display text never ends with `\n` and `</think>` never gets
2334
- // a preceding blank line. Interior breaks are preserved (collapsed to
2335
- // a single blank line at most).
2336
- let isThinking = false;
2337
- let emittedAny = false;
2338
- let pendingNewlines = "";
2339
- // True once text has been emitted without a trailing newline: the next
2340
- // <think> open tag then needs its own leading newline, otherwise it
2341
- // glues onto the streamed text mid-line. False at stream start and
2342
- // after every tag (both end with newlines), preserving the exact
2343
- // "<think>\n...\n</think>\n" contract for thinking-first streams.
2344
- let needThinkNewline = false;
2345
- // Set on every think close; consumed by the next text_delta(s) to strip
2346
- // model-leading blank lines display-only (cache-safe: outer events only).
2347
- // Stays set until non-empty text is emitted so split "\n" | "\nText"
2348
- // chunks cannot leak a blank line.
2349
- let justClosedThink = false;
2350
- const collapseBlankLines = (s: string): string => s.replace(/\n{3,}/g, "\n\n");
2351
- const userOnThinking = options.onThinkingDelta;
2352
- const userOnDelta = options.onDelta;
2353
- const userOnEvent = options.onEvent;
2354
- if (userOnEvent) s.on("*", userOnEvent as any);
2355
-
2356
- const openThink = (e?: any) => {
2357
- if (!isThinking) {
2358
- isThinking = true;
2359
- emittedAny = false;
2360
- pendingNewlines = "";
2361
- const tag = (needThinkNewline ? "\n" : "") + "<think>\n";
2362
- needThinkNewline = false;
2363
- if (userOnThinking) userOnThinking(tag, e);
2364
- else if (userOnDelta) userOnDelta(tag, e as any);
2365
- }
2366
- };
2367
-
2368
- const closeThink = (e?: any) => {
2369
- if (isThinking) {
2370
- isThinking = false;
2371
- // Drop any withheld trailing newlines: the span ends cleanly and
2372
- // the tag supplies its own single line break. Empty spans still close.
2373
- pendingNewlines = "";
2374
- const close = emittedAny ? "\n</think>\n" : "</think>\n";
2375
- emittedAny = false;
2376
- justClosedThink = true;
2377
- if (userOnThinking) userOnThinking(close, e);
2378
- else if (userOnDelta) userOnDelta(close, e as any);
2379
- }
2380
- };
2381
-
2382
- /**
2383
- * Strips model-leading blank lines after a think close, display-only.
2384
- * Mutates `e.delta` in place so `for-await` consumers see the same clean
2385
- * text; safe because loop context derives from the provider result, not
2386
- * these outer events. Returns the display string (`""` when nothing
2387
- * displayable yet — caller skips the callback but keeps the flag set so
2388
- * a split newline-only chunk cannot leak a blank line).
2389
- */
2390
- const stripLeadingAfterClose = (e: any): string => {
2391
- const raw: string = e.delta ?? "";
2392
- if (!justClosedThink) return raw;
2393
- const stripped = raw.replace(/^\n+/, "");
2394
- try {
2395
- e.delta = stripped;
2396
- } catch {}
2397
- if (!stripped) return "";
2398
- justClosedThink = false;
2399
- return stripped;
2400
- };
2401
-
2402
- /** Buffers one raw thinking chunk, returning the display string to
2403
- * emit now (`""` when nothing displayable yet). Mutates the event in
2404
- * place so manual `for-await` consumers see the same clean text; safe
2405
- * because loop context derives from the provider result, not these
2406
- * outer events. */
2407
- const bufferThinkingEvent = (e: any): string => {
2408
- const raw: string = e.thinkingDelta ?? "";
2409
- let chunk = collapseBlankLines(pendingNewlines + raw);
2410
- pendingNewlines = "";
2411
- if (!emittedAny) {
2412
- chunk = chunk.replace(/^\n+/, "");
2413
- }
2414
- if (!chunk) return "";
2415
- const m = chunk.match(/\n+$/);
2416
- if (m) {
2417
- pendingNewlines = m[0];
2418
- chunk = chunk.slice(0, -m[0].length);
2419
- if (!chunk) return "";
2420
- }
2421
- emittedAny = true;
2422
- e.thinkingDelta = chunk;
2423
- if (typeof e.partialThinking === "string") {
2424
- e.partialThinking = collapseBlankLines(e.partialThinking);
2425
- }
2426
- return chunk;
2427
- };
2428
-
2429
- if (userOnThinking || userOnDelta) {
2430
- s.on("thinking_delta", (e: any) => {
2431
- openThink(e);
2432
- const chunk = bufferThinkingEvent(e);
2433
- if (!chunk) return;
2434
- if (userOnThinking) userOnThinking(chunk, e);
2435
- else if (userOnDelta) userOnDelta(chunk, e as any);
2436
- });
2437
- s.on("text_delta", (e: any) => {
2438
- closeThink(e);
2439
- const display = stripLeadingAfterClose(e);
2440
- if (!display) {
2441
- needThinkNewline = false;
2442
- return;
2443
- }
2444
- needThinkNewline = !display.endsWith("\n");
2445
- if (userOnDelta) userOnDelta(display, e);
2446
- else if (userOnThinking) userOnThinking(display, e as any);
2447
- });
2448
- // Close thinking tag and reset turn state before tool/subagent execution or results
2449
- s.on("tool_call_start" as any, (e: any) => closeThink(e));
2450
- s.on("tool_call_complete" as any, (e: any) => closeThink(e));
2451
- s.on("tool_result" as any, (e: any) => closeThink(e));
2452
- s.on("subagent_complete" as any, (e: any) => closeThink(e));
2453
- s.on("done", (e: any) => {
2454
- closeThink(e || ({ type: "done" } as any));
2455
- });
2456
- } else {
2457
- // No callbacks but wrapThinking true — still emit tags as events for manual iteration.
2458
- // Same display hygiene as above (in-place event normalization is safe:
2459
- // loop context derives from the provider result, not outer events).
2460
- const closeSynthetic = () => {
2461
- if (isThinking) {
2462
- isThinking = false;
2463
- pendingNewlines = "";
2464
- const tag = emittedAny ? "\n</think>\n" : "</think>\n";
2465
- emittedAny = false;
2466
- justClosedThink = true;
2467
- s.push({ type: "thinking_delta", thinkingDelta: tag, __synthetic: true } as any);
2468
- }
2469
- };
2470
- s.on("thinking_delta", (e: any) => {
2471
- // Ignore our own synthetic tags: push() redelivers events to
2472
- // listeners synchronously, so without this guard the handler
2473
- // re-enters on every tag it emits (duplicate <think> spans and
2474
- // mutated queued events).
2475
- if ((e as any).__synthetic) return;
2476
- if (!isThinking) {
2477
- isThinking = true;
2478
- emittedAny = false;
2479
- pendingNewlines = "";
2480
- const openTag = (needThinkNewline ? "\n" : "") + "<think>\n";
2481
- needThinkNewline = false;
2482
- s.push({ type: "thinking_delta", thinkingDelta: openTag, __synthetic: true } as any);
2483
- }
2484
- const chunk = bufferThinkingEvent(e);
2485
- if (!chunk) {
2486
- e.thinkingDelta = "";
2487
- }
2488
- });
2489
- s.on("text_delta", (e: any) => {
2490
- closeSynthetic();
2491
- if (justClosedThink) {
2492
- const stripped: string = (e.delta ?? "").replace(/^\n+/, "");
2493
- try {
2494
- e.delta = stripped;
2495
- } catch {}
2496
- if (!stripped) return;
2497
- justClosedThink = false;
2498
- }
2499
- needThinkNewline = !(e.delta ?? "").endsWith("\n");
2500
- });
2501
- s.on("tool_call_start" as any, () => {
2502
- closeSynthetic();
2503
- });
2504
- s.on("tool_call_complete" as any, () => {
2505
- closeSynthetic();
2506
- });
2507
- s.on("tool_result" as any, () => {
2508
- closeSynthetic();
2509
- });
2510
- s.on("subagent_complete" as any, () => {
2511
- closeSynthetic();
2512
- });
2513
- s.on("done", () => {
2514
- closeSynthetic();
2515
- });
2516
- if (userOnEvent) s.on("*", userOnEvent as any);
2517
- }
2518
- } else {
2519
- if (options?.onEvent) s.on("*", options.onEvent as any);
2520
- if (options?.onDelta) s.on("text_delta", (e: any) => options.onDelta!(e.delta!, e));
2521
- if (options?.onThinkingDelta) s.on("thinking_delta", (e: any) => options.onThinkingDelta!(e.thinkingDelta!, e));
2522
- }
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));
2523
2004
  }
2524
2005
  }
2525
2006
 
@@ -2551,57 +2032,3 @@ function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentR
2551
2032
  providerSteps: res.providerSteps,
2552
2033
  });
2553
2034
  }
2554
-
2555
- /** Normalizes the public `thinkingLevel` flag into provider-neutral thinking settings. */
2556
- export function normalizeThinking(config: AgentConfig, fallbackLevel?: string): ThinkingConfig | undefined {
2557
- // DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh, max
2558
- // `thinking` (canonical) wins over legacy `thinkingLevel`; `{ level }` objects are unwrapped.
2559
- const rawLevel = normalizeThinkingV2(config.thinking, config.thinkingLevel) ?? fallbackLevel;
2560
- if (typeof config.thinking === "object" && config.thinking?.budgetTokens !== undefined) {
2561
- // No provider maps an explicit token budget yet: say so instead of ignoring it silently.
2562
- telemetryBus.emitWarning(
2563
- "config.thinking_budget_ignored",
2564
- "[Agent Accelerator] thinking.budgetTokens is not forwarded by any provider adapter yet; the level alone is used.",
2565
- { budgetTokens: config.thinking.budgetTokens }
2566
- );
2567
- }
2568
- const level = rawLevel as ThinkingLevel | undefined;
2569
-
2570
- if (!level) return undefined;
2571
-
2572
- // Validate model thinking from catalog if model is configured
2573
- if (config.model) {
2574
- try {
2575
- const rawModel: any = (config.model as any)?.model ? (config.model as any).model : config.model;
2576
- const modelStr = typeof rawModel === "string" ? rawModel : rawModel?.id || "";
2577
- const provStr =
2578
- typeof rawModel === "object" && rawModel?.provider
2579
- ? rawModel.provider
2580
- : modelStr.includes("/")
2581
- ? modelStr.split("/")[0]
2582
- : "";
2583
- const actualModelId = modelStr.includes("/") ? modelStr.split("/").slice(1).join("/") : modelStr;
2584
- if (actualModelId) {
2585
- validateModelThinking(provStr || "openrouter", actualModelId, level);
2586
- }
2587
- } catch (e) {
2588
- throw e;
2589
- }
2590
- }
2591
-
2592
- if (level === "none") {
2593
- return { enabled: false, level: "none", budgetTokens: 0 };
2594
- }
2595
- if (level === "dynamic") {
2596
- return { enabled: true, level: "dynamic", budgetTokens: -1 };
2597
- }
2598
- return { enabled: true, level };
2599
- }
2600
-
2601
- /** Maps the public `toolChoice` option onto the provider wire-neutral form. */
2602
- function normalizeToolChoiceOption(choice: ToolChoiceOption | undefined): ProviderRequestOptions["toolChoice"] {
2603
- if (!choice) return undefined;
2604
- if (typeof choice === "string") return choice;
2605
- if (choice.tool) return { type: "function", function: { name: choice.tool } };
2606
- return choice.mode;
2607
- }