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.
- package/README.md +0 -2
- package/package.json +1 -1
- package/src/agent/agent.ts +97 -670
- package/src/agent/architecture.md +4 -1
- package/src/agent/loop.ts +208 -213
- package/src/agent/named-sessions.ts +93 -0
- package/src/agent/options.ts +166 -0
- package/src/agent/run-control.ts +248 -0
- package/src/index.ts +2 -2
- package/src/providers/google.ts +1 -2
- package/src/streaming/event-stream.ts +1 -1
- package/src/types/agent.ts +0 -2
- package/src/utils/headers.ts +1 -1
package/src/agent/agent.ts
CHANGED
|
@@ -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
|
-
|
|
301
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
961
|
+
if (!this.rc.current) return this.run(prompt, options) as Promise<AgentResponse>;
|
|
1130
962
|
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
1131
|
-
if (this.
|
|
963
|
+
if (this.rc.queue.length >= max) {
|
|
1132
964
|
throw new ValidationError(
|
|
1133
|
-
`[Agent Accelerator] Queue is full (${this.
|
|
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.
|
|
969
|
+
this.rc.queue.push({ prompt, options, isStream: false, resolve, reject });
|
|
1138
970
|
this.persistNow();
|
|
1139
971
|
try {
|
|
1140
|
-
this.
|
|
972
|
+
this.rc.current?.outerStream?.push({
|
|
1141
973
|
type: "queued",
|
|
1142
974
|
queuedPrompt: previewPrompt(prompt),
|
|
1143
|
-
queueLength: this.
|
|
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
|
-
|
|
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
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1144
|
+
customHeaders: this.customHeaders,
|
|
1145
|
+
thinkingConfig: this.thinkingConfig,
|
|
1146
|
+
cache: this.cache,
|
|
1457
1147
|
serviceTier: this.serviceTier,
|
|
1458
|
-
sessionId,
|
|
1459
|
-
|
|
1460
|
-
prompts:
|
|
1461
|
-
|
|
1462
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1753
|
+
if (this.rc.queue.length >= max) {
|
|
2071
1754
|
throw new ValidationError(
|
|
2072
|
-
`[Agent Accelerator] Queue is full (${this.
|
|
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.
|
|
1759
|
+
this.rc.queue.push({ prompt, options, isStream: false, resolve, reject });
|
|
2077
1760
|
this.persistNow();
|
|
2078
1761
|
try {
|
|
2079
|
-
this.
|
|
1762
|
+
this.rc.current?.outerStream?.push({
|
|
2080
1763
|
type: "queued",
|
|
2081
1764
|
queuedPrompt: previewPrompt(prompt),
|
|
2082
|
-
queueLength: this.
|
|
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.
|
|
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.
|
|
2173
|
-
this.
|
|
1855
|
+
this.rc.assertSteerRoom(this.rc.current.steerInbox);
|
|
1856
|
+
this.rc.current.steerInbox.push({ prompt, options });
|
|
2174
1857
|
this.persistNow();
|
|
2175
|
-
const activeStream = this.
|
|
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.
|
|
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.
|
|
1904
|
+
if (!urgent && this.rc.queue.length >= max) {
|
|
2222
1905
|
throw new ValidationError(
|
|
2223
|
-
`[Agent Accelerator] Queue is full (${this.
|
|
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.
|
|
1914
|
+
this.rc.current.outerStream?.push({
|
|
2232
1915
|
type: "queued",
|
|
2233
1916
|
queuedPrompt: previewPrompt(prompt),
|
|
2234
|
-
queueLength: this.
|
|
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.
|
|
1948
|
+
const idx = this.rc.queue.indexOf(queuedEntry as any);
|
|
2266
1949
|
if (idx >= 0) {
|
|
2267
|
-
this.
|
|
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
|
|
1959
|
+
this.rc.queueUrgent(queuedEntry as QueuedPrompt);
|
|
2277
1960
|
} else {
|
|
2278
|
-
this.
|
|
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
|
|
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
|
-
|
|
2319
|
-
if (options?.
|
|
2320
|
-
|
|
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
|
-
}
|