agent-accelerator 0.2.0 → 0.4.0
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 +94 -14
- package/package.json +1 -1
- package/src/agent/agent.ts +1010 -104
- package/src/agent/context.ts +35 -0
- package/src/agent/delegation.ts +758 -40
- package/src/agent/loop.ts +811 -102
- package/src/agent/subagent.ts +59 -0
- package/src/index.ts +186 -59
- package/src/models/catalog-cache.ts +68 -25
- package/src/models/catalog.ts +101 -21
- package/src/providers/google.ts +99 -39
- package/src/providers/openai-compat.ts +140 -80
- package/src/providers/openai.ts +149 -69
- package/src/providers/openrouter.ts +106 -71
- package/src/providers/registry.ts +16 -12
- package/src/providers/shared.ts +132 -0
- package/src/providers.ts +14 -13
- package/src/session/store.ts +1285 -0
- package/src/streaming/event-stream.ts +156 -41
- package/src/tools/executor.ts +252 -16
- package/src/tools/tool.ts +147 -3
- package/src/types/agent.ts +253 -2
- package/src/types/core.ts +2 -1
- package/src/types/errors.ts +133 -0
- package/src/types/model.ts +1 -1
- package/src/types/response.ts +150 -3
- package/src/types/tool.ts +71 -0
- package/src/update-models.ts +8 -2
- package/src/utils/cache.ts +68 -1
- package/src/utils/env.ts +2 -6
- package/src/utils/headers.ts +153 -19
- package/src/utils/retry.ts +27 -13
- package/src/utils/session.ts +320 -0
- package/src/providers/openrouter-responses.ts +0 -949
- package/src/types/provider-payloads.ts +0 -352
package/src/agent/agent.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentConfig, AgentRunOptions } from "../types/agent.ts";
|
|
1
|
+
import type { AgentConfig, AgentRunOptions, MidSessionConfig } from "../types/agent.ts";
|
|
2
2
|
import type { ToolDefinition } from "../types/tool.ts";
|
|
3
3
|
import type { ThinkingLevel, ThinkingConfig, CacheConfig, ServiceTier } from "../types/core.ts";
|
|
4
4
|
import type { ContentPart } from "../types/message.ts";
|
|
@@ -8,10 +8,16 @@ import { AgentContext } from "./context.ts";
|
|
|
8
8
|
import { resolveModel } from "../providers/registry.ts";
|
|
9
9
|
import { buildAgentTools, createSubagentSpawnTool } from "./delegation.ts";
|
|
10
10
|
import { convert_document_to_markdown } from "../utils/documents.ts";
|
|
11
|
-
import { runAgentLoop, streamAgentLoop } from "./loop.ts";
|
|
11
|
+
import { runAgentLoop, streamAgentLoop, type SteerEntry } from "./loop.ts";
|
|
12
12
|
import { createSessionId } from "../utils/session.ts";
|
|
13
|
+
import { getSubAgentTrace, subscribeToSubAgent, listSubAgentTraceIds, type SubAgentTrace } from "./delegation.ts";
|
|
14
|
+
import { saveSessionDir, saveSessionFile } from "../session/store.ts";
|
|
15
|
+
import { clearSessionRouting } from "../providers.ts";
|
|
16
|
+
import { clearInteractionChains } from "../providers/google.ts";
|
|
13
17
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
14
18
|
import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalog.ts";
|
|
19
|
+
import { buildSessionData, type PersistedAgentSession, type SessionTotals,} from "../session/store.ts";
|
|
20
|
+
import { SessionTelemetry } from "../session/store.ts";
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* Resolves a per-run thinking override without mutating agent config.
|
|
@@ -26,8 +32,49 @@ export function resolveEffectiveThinking(
|
|
|
26
32
|
return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
import { ConfigError } from "../types/errors.ts";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Disposers for the manual user-signal forward listeners (Q-21 fallback when
|
|
39
|
+
* `AbortSignal.any` is unavailable). Keyed by run controller so no expando
|
|
40
|
+
* properties are attached to AbortController instances.
|
|
41
|
+
*/
|
|
42
|
+
const mergeDisposers = new WeakMap<AbortController, () => void>();
|
|
43
|
+
|
|
44
|
+
/** Max named sessions kept per Agent (Q-19a LRU). */
|
|
45
|
+
const SESSION_LRU_MAX = 100;
|
|
46
|
+
/** Idle TTL for named sessions (Q-19a). */
|
|
47
|
+
const SESSION_TTL_MS = 60 * 60 * 1000;
|
|
48
|
+
|
|
49
|
+
/** Normalized per-run limits (Q-22). Sub-agents inherit these unless overridden. */
|
|
50
|
+
export interface AgentRunLimits {
|
|
51
|
+
maxTurns?: number;
|
|
52
|
+
deadlineMs?: number;
|
|
53
|
+
maxToolCallsPerRun?: number;
|
|
54
|
+
requestTimeoutMs: number;
|
|
55
|
+
firstByteTimeoutMs: number;
|
|
56
|
+
streamIdleTimeoutMs: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Resolves `(config as any).limits` with Q-22 defaults (timeouts tolerate reasoning). */
|
|
60
|
+
function resolveAgentLimits(config: { limits?: Record<string, unknown> }): AgentRunLimits {
|
|
61
|
+
const raw = (config as { limits?: Record<string, unknown> }).limits ?? {};
|
|
62
|
+
const num = (v: unknown): number | undefined =>
|
|
63
|
+
typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
64
|
+
return {
|
|
65
|
+
...(num(raw.maxTurns) !== undefined ? { maxTurns: Math.max(1, Math.floor(num(raw.maxTurns)!)) } : {}),
|
|
66
|
+
...(num(raw.deadlineMs) !== undefined ? { deadlineMs: num(raw.deadlineMs)! } : {}),
|
|
67
|
+
...(num(raw.maxToolCallsPerRun) !== undefined
|
|
68
|
+
? { maxToolCallsPerRun: Math.max(1, Math.floor(num(raw.maxToolCallsPerRun)!)) }
|
|
69
|
+
: {}),
|
|
70
|
+
requestTimeoutMs: num(raw.requestTimeoutMs) ?? 300_000,
|
|
71
|
+
firstByteTimeoutMs: num(raw.firstByteTimeoutMs) ?? 120_000,
|
|
72
|
+
streamIdleTimeoutMs: num(raw.streamIdleTimeoutMs) ?? 300_000,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
29
76
|
/** Thrown when dynamic sub-agent spawning is enabled without an explicit model. */
|
|
30
|
-
export class SubAgentModelError extends
|
|
77
|
+
export class SubAgentModelError extends ConfigError {
|
|
31
78
|
readonly parentModel?: string;
|
|
32
79
|
|
|
33
80
|
/**
|
|
@@ -39,20 +86,12 @@ export class SubAgentModelError extends Error {
|
|
|
39
86
|
constructor(parentModel?: string) {
|
|
40
87
|
const parentName = parentModel || "your main model";
|
|
41
88
|
const formatted =
|
|
42
|
-
|
|
43
|
-
`
|
|
44
|
-
`
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
` const agent = new Agent({\n` +
|
|
49
|
-
` model: "${parentName}",\n` +
|
|
50
|
-
` dynamicSubagents: { enabled: true, model: "provider/model-id" },\n` +
|
|
51
|
-
` });\n\n` +
|
|
52
|
-
` 2. Or set the SUB_AGENT_MODEL environment variable in your .env or shell:\n` +
|
|
53
|
-
` SUB_AGENT_MODEL="provider/model-id"`;
|
|
54
|
-
|
|
55
|
-
super(formatted);
|
|
89
|
+
`[Agent Accelerator] Missing Configuration: a sub-agent model is required when dynamic sub-agents are enabled. ` +
|
|
90
|
+
`Main Agent Model: ${parentName}. ` +
|
|
91
|
+
`Dynamic sub-agent delegation was enabled (dynamicSubagents.enabled), but no model was assigned for sub-agents. ` +
|
|
92
|
+
`Fix: pass dynamicSubagents.model in Agent config or set SUB_AGENT_MODEL.`;
|
|
93
|
+
|
|
94
|
+
super(formatted, { parentModel: parentName });
|
|
56
95
|
this.name = "SubAgentModelError";
|
|
57
96
|
this.parentModel = parentModel;
|
|
58
97
|
|
|
@@ -120,6 +159,37 @@ export class Agent {
|
|
|
120
159
|
readonly context: AgentContext;
|
|
121
160
|
/** Whether history is cleared around each run. */
|
|
122
161
|
readonly stateless: boolean;
|
|
162
|
+
/** Durable persistence target: session file rewritten after every step. */
|
|
163
|
+
readonly persistConfig?: { dir?: string; file?: string };
|
|
164
|
+
/** Mid-session queue policy for new queries arriving while a run is active. */
|
|
165
|
+
readonly midSessionConfig: Required<Pick<MidSessionConfig, "mode">> & MidSessionConfig & { maxSteers?: number };
|
|
166
|
+
/** Max buffered steer entries per run (Q-20, default 20). */
|
|
167
|
+
readonly maxSteers: number;
|
|
168
|
+
/**
|
|
169
|
+
* Normalized run limits (Q-22). Read from `(config as any).limits`;
|
|
170
|
+
* sub-agents inherit these unless overridden. `maxTurns`/`deadlineMs`/
|
|
171
|
+
* `maxToolCallsPerRun` default to unlimited; timeouts default to
|
|
172
|
+
* request 300s / first-byte 120s / idle 300s (tolerate reasoning).
|
|
173
|
+
*/
|
|
174
|
+
readonly runLimits: AgentRunLimits;
|
|
175
|
+
private currentRun: {
|
|
176
|
+
steerInbox: SteerEntry[];
|
|
177
|
+
outerStream?: AssistantMessageEventStream;
|
|
178
|
+
runPromise?: Promise<AgentResponse>;
|
|
179
|
+
abortController: AbortController;
|
|
180
|
+
} | null = null;
|
|
181
|
+
private pendingQueue: Array<{
|
|
182
|
+
prompt: string | ContentPart[];
|
|
183
|
+
options?: AgentRunOptions;
|
|
184
|
+
isStream: boolean;
|
|
185
|
+
deferredStream?: AssistantMessageEventStream;
|
|
186
|
+
resolve: (res: AgentResponse) => void;
|
|
187
|
+
reject: (err: unknown) => void;
|
|
188
|
+
}> = [];
|
|
189
|
+
/** Named session contexts for `sessionId` runs (Q-19a, LRU 100 + TTL). */
|
|
190
|
+
private sessions = new Map<string, { context: AgentContext; lastAccess: number }>();
|
|
191
|
+
/** Coalesced persistence single-flight flag (Q-28). */
|
|
192
|
+
private persistScheduled = false;
|
|
123
193
|
|
|
124
194
|
/**
|
|
125
195
|
* Creates an agent and registers its model, tools, cache, and delegation settings.
|
|
@@ -197,8 +267,32 @@ export class Agent {
|
|
|
197
267
|
this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
|
|
198
268
|
this.baseUrl = (rawModel as any)?.baseUrl || config.baseUrl;
|
|
199
269
|
this.customHeaders = config.headers;
|
|
200
|
-
this.maxTurns =
|
|
270
|
+
this.maxTurns =
|
|
271
|
+
config.maxTurns === undefined || config.maxTurns === 0 || config.maxTurns === Infinity
|
|
272
|
+
? Infinity
|
|
273
|
+
: Number.isFinite(config.maxTurns as number)
|
|
274
|
+
? Math.max(1, Math.floor(config.maxTurns as number))
|
|
275
|
+
: Infinity;
|
|
201
276
|
this.sessionId = config.sessionId || config.cache?.sessionId || createSessionId();
|
|
277
|
+
(this as any).subagentTraces = {};
|
|
278
|
+
this.persistConfig = config.persist;
|
|
279
|
+
const rawMode = config.midSession?.mode ?? "auto";
|
|
280
|
+
const normalizedMode = rawMode === "steer" || rawMode === "queue" ? rawMode : "auto";
|
|
281
|
+
const rawMaxQueued = (config.midSession as any)?.maxQueued;
|
|
282
|
+
const rawMaxSteers = (config.midSession as any)?.maxSteers;
|
|
283
|
+
this.midSessionConfig = {
|
|
284
|
+
mode: normalizedMode,
|
|
285
|
+
...(Number.isFinite(rawMaxQueued) && (rawMaxQueued as number) >= 0
|
|
286
|
+
? { maxQueued: Math.floor(rawMaxQueued as number) }
|
|
287
|
+
: { maxQueued: 20 }),
|
|
288
|
+
...(Number.isFinite(rawMaxSteers) && (rawMaxSteers as number) >= 0
|
|
289
|
+
? { maxSteers: Math.floor(rawMaxSteers as number) }
|
|
290
|
+
: { maxSteers: 20 }),
|
|
291
|
+
};
|
|
292
|
+
// Q-20: bound on buffered steer entries (default 20).
|
|
293
|
+
this.maxSteers = this.midSessionConfig.maxSteers ?? 20;
|
|
294
|
+
// Q-22: run limits (sub-agents inherit unless overridden).
|
|
295
|
+
this.runLimits = resolveAgentLimits(config as unknown as { limits?: Record<string, unknown> });
|
|
202
296
|
|
|
203
297
|
// DX4: single thinkingLevel flag — also inherit from ModelProviderInstance when omitted
|
|
204
298
|
const mpThinking = (rawModel as any)?.thinkingLevel;
|
|
@@ -301,41 +395,751 @@ export class Agent {
|
|
|
301
395
|
|
|
302
396
|
/** Clears conversation messages and provider thought signatures while keeping configuration. */
|
|
303
397
|
reset(): void {
|
|
398
|
+
const sid = this.sessionId;
|
|
304
399
|
this.context.messages = [];
|
|
305
400
|
this.context.thoughtSignatures = [];
|
|
306
401
|
this.context.cachedContentId = (this.cacheConfig as any)?.cachedContentId;
|
|
307
402
|
this.context.systemPrompt = this.getFullInstructions();
|
|
403
|
+
// Q-25: reset also drops session-scoped provider chaining/routing.
|
|
404
|
+
try { clearInteractionChains(sid); } catch {}
|
|
405
|
+
try { clearSessionRouting(sid); } catch {}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Returns a named session handle (Q-19a). `session()` creates an isolated
|
|
410
|
+
* `AgentContext` bound to `id` (separate histories per id); `agent.run`
|
|
411
|
+
* with `{ sessionId }` selects/creates the same entry. Sessions are
|
|
412
|
+
* ephemeral in-memory (LRU 100 + 1h TTL). `agent.run`/`stream` without a
|
|
413
|
+
* `sessionId` remain sugar over the default session for compat.
|
|
414
|
+
*/
|
|
415
|
+
session(id?: string): { id: string; context: AgentContext; runController: AbortController | null } {
|
|
416
|
+
const sid = id || this.sessionId;
|
|
417
|
+
const ctx = this.getSessionContext(sid);
|
|
418
|
+
return { id: sid, context: ctx, runController: null };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Drops one named session (Q-19a). No-op for unknown ids. */
|
|
422
|
+
disposeSession(id: string): void {
|
|
423
|
+
try { this.sessions.delete(id); } catch {}
|
|
424
|
+
try { clearInteractionChains(id); } catch {}
|
|
425
|
+
try { clearSessionRouting(id); } catch {}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Drops all named sessions except the default context (Q-19a). */
|
|
429
|
+
clearSessions(): void {
|
|
430
|
+
try {
|
|
431
|
+
for (const id of [...this.sessions.keys()]) {
|
|
432
|
+
try { clearInteractionChains(id); } catch {}
|
|
433
|
+
try { clearSessionRouting(id); } catch {}
|
|
434
|
+
}
|
|
435
|
+
this.sessions.clear();
|
|
436
|
+
} catch {}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Resolves the context for a `sessionId` run (default session = `this.context`). */
|
|
440
|
+
private getSessionContext(sessionId?: string): AgentContext {
|
|
441
|
+
const sid = sessionId || this.sessionId;
|
|
442
|
+
if (sid === this.sessionId) return this.context;
|
|
443
|
+
const now = Date.now();
|
|
444
|
+
// TTL eviction.
|
|
445
|
+
try {
|
|
446
|
+
for (const [key, entry] of [...this.sessions.entries()]) {
|
|
447
|
+
if (now - entry.lastAccess > SESSION_TTL_MS) {
|
|
448
|
+
this.sessions.delete(key);
|
|
449
|
+
try { clearInteractionChains(key); } catch {}
|
|
450
|
+
try { clearSessionRouting(key); } catch {}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch {}
|
|
454
|
+
const existing = this.sessions.get(sid);
|
|
455
|
+
if (existing) {
|
|
456
|
+
existing.lastAccess = now;
|
|
457
|
+
return existing.context;
|
|
458
|
+
}
|
|
459
|
+
const fresh = new AgentContext(this.getFullInstructions());
|
|
460
|
+
try {
|
|
461
|
+
const cached = (this.cacheConfig as unknown as { cachedContentId?: string })?.cachedContentId;
|
|
462
|
+
if (cached) fresh.cachedContentId = cached;
|
|
463
|
+
} catch {}
|
|
464
|
+
this.sessions.set(sid, { context: fresh, lastAccess: now });
|
|
465
|
+
// LRU eviction (oldest lastAccess first).
|
|
466
|
+
try {
|
|
467
|
+
while (this.sessions.size > SESSION_LRU_MAX) {
|
|
468
|
+
let oldest: string | undefined;
|
|
469
|
+
let oldestTs = Infinity;
|
|
470
|
+
for (const [key, entry] of this.sessions) {
|
|
471
|
+
if (entry.lastAccess < oldestTs) { oldestTs = entry.lastAccess; oldest = key; }
|
|
472
|
+
}
|
|
473
|
+
if (!oldest) break;
|
|
474
|
+
this.sessions.delete(oldest);
|
|
475
|
+
try { clearInteractionChains(oldest); } catch {}
|
|
476
|
+
try { clearSessionRouting(oldest); } catch {}
|
|
477
|
+
}
|
|
478
|
+
} catch {}
|
|
479
|
+
return fresh;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Rejects + drops all queued follow-ups (Q-20). Returns the cleared count. */
|
|
483
|
+
clearQueue(): number {
|
|
484
|
+
const n = this.pendingQueue.length;
|
|
485
|
+
if (n === 0) return 0;
|
|
486
|
+
const err = Object.assign(new Error("Queue cleared"), { name: "AbortError" });
|
|
487
|
+
const pending = this.pendingQueue.splice(0);
|
|
488
|
+
for (const entry of pending) {
|
|
489
|
+
try { entry.reject(err); } catch {}
|
|
490
|
+
try { entry.deferredStream?.fail(err instanceof Error ? err : new Error(String(err))); } catch {}
|
|
491
|
+
}
|
|
492
|
+
return n;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Throws when the steer inbox is at its Q-20 bound. */
|
|
496
|
+
private assertSteerRoom(inbox: SteerEntry[]): void {
|
|
497
|
+
const max = this.maxSteers ?? 20;
|
|
498
|
+
if (inbox.length >= max) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`[Agent Accelerator] Steer inbox is full (${inbox.length}/${max}). Wait for the active run to drain or increase midSession.maxSteers.`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* True while a `run()`/`stream()` turn is active on this agent.
|
|
507
|
+
* Use with `steer()` (current-turn redirect) vs `queue()` (next-turn follow-up).
|
|
508
|
+
*/
|
|
509
|
+
get isBusy(): boolean {
|
|
510
|
+
return this.currentRun !== null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Number of queued follow-up requests waiting for the active run to finish. */
|
|
514
|
+
get pendingCount(): number {
|
|
515
|
+
return this.pendingQueue.length;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Number of steer messages buffered for injection into the active turn. */
|
|
519
|
+
get steerPendingCount(): number {
|
|
520
|
+
return this.currentRun?.steerInbox.length ?? 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Effective mid-session policy (`steer` | `queue` | `auto`). */
|
|
524
|
+
get midSessionMode(): "steer" | "queue" | "auto" {
|
|
525
|
+
return this.midSessionConfig.mode;
|
|
308
526
|
}
|
|
309
527
|
|
|
310
|
-
private
|
|
528
|
+
private resolveEnqueueMode(requested?: "steer" | "queue"): "steer" | "queue" {
|
|
529
|
+
const enforced = this.midSessionConfig.mode;
|
|
530
|
+
if (enforced === "steer" || enforced === "queue") return enforced;
|
|
531
|
+
return requested === "steer" ? "steer" : "queue";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private assertPromptValid(prompt: string | ContentPart[]): void {
|
|
311
535
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
312
536
|
throw new Error(
|
|
313
537
|
"[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
|
|
314
538
|
);
|
|
315
539
|
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private previewPrompt(prompt: string | ContentPart[]): string {
|
|
543
|
+
if (typeof prompt === "string") return prompt.slice(0, 500);
|
|
544
|
+
try {
|
|
545
|
+
const texts = prompt
|
|
546
|
+
.filter((p) => (p as { type?: unknown }).type === "text")
|
|
547
|
+
.map((p) => (p as { text?: string }).text ?? "")
|
|
548
|
+
.join("\n");
|
|
549
|
+
return (texts || "[multipart]").slice(0, 500);
|
|
550
|
+
} catch {
|
|
551
|
+
return "[multipart]";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Interrupts the active run immediately (STOP. Do this instead).
|
|
557
|
+
* Cancels the active stream and aborts provider/tool/sub-agent work via
|
|
558
|
+
* the run's internal `AbortController`. No-op when idle. The interrupted
|
|
559
|
+
* run rejects with `AbortError`; queued follow-ups still run afterwards.
|
|
560
|
+
*/
|
|
561
|
+
interrupt(reason?: unknown): void {
|
|
562
|
+
const active = this.currentRun;
|
|
563
|
+
if (!active) return;
|
|
564
|
+
try {
|
|
565
|
+
active.abortController.abort(reason instanceof Error ? reason : new Error("Agent interrupted"));
|
|
566
|
+
} catch {}
|
|
567
|
+
try {
|
|
568
|
+
active.outerStream?.cancel(reason);
|
|
569
|
+
} catch {}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Steers the active turn (While you're doing that, change direction).
|
|
574
|
+
* The current provider/tool work finishes first, then `prompt` is injected
|
|
575
|
+
* as a user message into the SAME run — same `AgentResponse`, same
|
|
576
|
+
* `maxTurns` budget. Resolves to the active run's final response.
|
|
577
|
+
* When idle, behaves like `run(prompt, options)`.
|
|
578
|
+
* Throws when the developer enforces `midSession: { mode: "queue" }`.
|
|
579
|
+
* Note: `options` only honors `additionalContext` mid-turn (thinking level,
|
|
580
|
+
* session, and streaming callbacks stay with the active run). For a
|
|
581
|
+
* streaming follow-up use `stream(prompt, { enqueue: "steer" })`.
|
|
582
|
+
*/
|
|
583
|
+
steer(prompt: string | ContentPart[], options?: AgentRunOptions): Promise<AgentResponse> {
|
|
584
|
+
this.assertPromptValid(prompt);
|
|
585
|
+
if (options?.stream === true) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
'[Agent Accelerator] steer() is non-streaming. Use stream(prompt, { enqueue: "steer" }) for a streaming steer.'
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (this.midSessionConfig.mode === "queue") {
|
|
591
|
+
throw new Error(
|
|
592
|
+
'[Agent Accelerator] steer() is disabled by midSession.mode "queue". Use queue() or set mode to "auto"/"steer".'
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
const active = this.currentRun;
|
|
596
|
+
if (!active) return this.run(prompt, options) as Promise<AgentResponse>;
|
|
597
|
+
this.assertSteerRoom(active.steerInbox);
|
|
598
|
+
active.steerInbox.push({ prompt, options });
|
|
599
|
+
this.persistNow();
|
|
600
|
+
if (active.runPromise) return active.runPromise;
|
|
601
|
+
if (active.outerStream) return active.outerStream.result();
|
|
602
|
+
return this.run(prompt, options) as Promise<AgentResponse>;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Queues a follow-up for the next turn (When you're done, do this next).
|
|
607
|
+
* The active run finishes completely first; this prompt then starts a new
|
|
608
|
+
* run with its own turn budget and `AgentResponse`. Resolves to that next
|
|
609
|
+
* turn's response. When idle, behaves like `run(prompt, options)`.
|
|
610
|
+
* Throws when the developer enforces `midSession: { mode: "steer" }` or
|
|
611
|
+
* when `maxQueued` is exceeded. For a streaming follow-up use
|
|
612
|
+
* `stream(prompt, { enqueue: "queue" })`.
|
|
613
|
+
*/
|
|
614
|
+
queue(prompt: string | ContentPart[], options?: AgentRunOptions): Promise<AgentResponse> {
|
|
615
|
+
this.assertPromptValid(prompt);
|
|
616
|
+
if (options?.stream === true) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
'[Agent Accelerator] queue() is non-streaming. Use stream(prompt, { enqueue: "queue" }) for a streaming follow-up.'
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
if (this.midSessionConfig.mode === "steer") {
|
|
622
|
+
throw new Error(
|
|
623
|
+
'[Agent Accelerator] queue() is disabled by midSession.mode "steer". Use steer() or set mode to "auto"/"queue".'
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (!this.currentRun) return this.run(prompt, options) as Promise<AgentResponse>;
|
|
627
|
+
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
628
|
+
if (this.pendingQueue.length >= max) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
`[Agent Accelerator] Queue is full (${this.pendingQueue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
return new Promise<AgentResponse>((resolve, reject) => {
|
|
634
|
+
this.pendingQueue.push({ prompt, options, isStream: false, resolve, reject });
|
|
635
|
+
try {
|
|
636
|
+
this.currentRun?.outerStream?.push({
|
|
637
|
+
type: "queued",
|
|
638
|
+
queuedPrompt: this.previewPrompt(prompt),
|
|
639
|
+
queueLength: this.pendingQueue.length,
|
|
640
|
+
} as any);
|
|
641
|
+
} catch {}
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private pumpQueue(): void {
|
|
646
|
+
if (this.currentRun !== null) return;
|
|
647
|
+
const next = this.pendingQueue.shift();
|
|
648
|
+
if (!next) return;
|
|
649
|
+
// Q-20: reject queued items whose signal aborted before start.
|
|
650
|
+
try {
|
|
651
|
+
if ((next.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
|
|
652
|
+
const reason = ((next.options as { signal?: AbortSignal }).signal as unknown as { reason?: unknown })?.reason;
|
|
653
|
+
const abortErr = Object.assign(
|
|
654
|
+
reason instanceof Error ? reason : new Error("Queued run aborted before start"),
|
|
655
|
+
{ name: "AbortError" }
|
|
656
|
+
);
|
|
657
|
+
try { next.reject(abortErr); } catch {}
|
|
658
|
+
try { next.deferredStream?.fail(abortErr instanceof Error ? abortErr : new Error(String(abortErr))); } catch {}
|
|
659
|
+
queueMicrotask(() => this.pumpQueue());
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
} catch {}
|
|
663
|
+
if (next.isStream && next.deferredStream) {
|
|
664
|
+
const deferred = next.deferredStream;
|
|
665
|
+
try {
|
|
666
|
+
const real = this.startStreamingRun(next.prompt, next.options);
|
|
667
|
+
try { (deferred as any).__piped = true; } catch {}
|
|
668
|
+
const forward = (e: StreamEvent) => {
|
|
669
|
+
try { deferred.push(e); } catch {}
|
|
670
|
+
};
|
|
671
|
+
real.on("*", forward as any);
|
|
672
|
+
try {
|
|
673
|
+
deferred.onCancel(() => {
|
|
674
|
+
try { real.cancel(); } catch {}
|
|
675
|
+
});
|
|
676
|
+
} catch {}
|
|
677
|
+
real.result().then(
|
|
678
|
+
(res) => {
|
|
679
|
+
try { next.resolve(res); } catch {}
|
|
680
|
+
try { deferred.end(res); } catch {}
|
|
681
|
+
},
|
|
682
|
+
(err) => {
|
|
683
|
+
try { next.reject(err); } catch {}
|
|
684
|
+
// Pipe failures into the deferred stream so for-await sees them.
|
|
685
|
+
try { deferred.fail(err instanceof Error ? err : new Error(String(err))); } catch {}
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
} catch (err) {
|
|
689
|
+
try { next.reject(err); } catch {}
|
|
690
|
+
try { deferred.fail(err instanceof Error ? err : new Error(String(err))); } catch {}
|
|
691
|
+
// Keep draining even if starting failed.
|
|
692
|
+
queueMicrotask(() => this.pumpQueue());
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
this.startNonStreamingRun(next.prompt, next.options).then(next.resolve, next.reject);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private finishCurrentRun(): void {
|
|
700
|
+
// Q-20: leftover steer entries become queued follow-ups instead of dropping.
|
|
701
|
+
let leftover: SteerEntry[] = [];
|
|
702
|
+
try { leftover = this.currentRun?.steerInbox?.splice(0) ?? []; } catch { leftover = []; }
|
|
703
|
+
this.currentRun = null;
|
|
704
|
+
if (leftover.length > 0) {
|
|
705
|
+
try {
|
|
706
|
+
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
707
|
+
for (const entry of leftover) {
|
|
708
|
+
try {
|
|
709
|
+
if ((entry.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) continue;
|
|
710
|
+
} catch { continue; }
|
|
711
|
+
if (this.pendingQueue.length >= max) break;
|
|
712
|
+
this.pendingQueue.push({
|
|
713
|
+
prompt: entry.prompt,
|
|
714
|
+
options: entry.options,
|
|
715
|
+
isStream: false,
|
|
716
|
+
resolve: () => {},
|
|
717
|
+
reject: () => {},
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
} catch {}
|
|
721
|
+
}
|
|
722
|
+
// Defer pump so the completing run's .then handlers settle first.
|
|
723
|
+
queueMicrotask(() => this.pumpQueue());
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private cleanupMerge(runController: AbortController): void {
|
|
727
|
+
try {
|
|
728
|
+
const dispose = mergeDisposers.get(runController);
|
|
729
|
+
if (dispose) {
|
|
730
|
+
try { dispose(); } catch {}
|
|
731
|
+
mergeDisposers.delete(runController);
|
|
732
|
+
}
|
|
733
|
+
} catch {}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private mergeUserSignal(userSignal?: AbortSignal, runController?: AbortController): AbortSignal | undefined {
|
|
737
|
+
if (!runController) return userSignal;
|
|
738
|
+
if (!userSignal) return runController.signal;
|
|
739
|
+
if (userSignal.aborted) {
|
|
740
|
+
try {
|
|
741
|
+
runController.abort((userSignal as any).reason);
|
|
742
|
+
} catch {
|
|
743
|
+
try { runController.abort(); } catch {}
|
|
744
|
+
}
|
|
745
|
+
return runController.signal;
|
|
746
|
+
}
|
|
747
|
+
// Q-21: prefer AbortSignal.any (no expando, no listener leak).
|
|
748
|
+
try {
|
|
749
|
+
const anyFn = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;
|
|
750
|
+
if (typeof anyFn === "function") {
|
|
751
|
+
return anyFn.call(AbortSignal, [userSignal, runController.signal]);
|
|
752
|
+
}
|
|
753
|
+
} catch {}
|
|
754
|
+
// Fallback: manual forward without expando (disposer in WeakMap).
|
|
755
|
+
const forward = () => {
|
|
756
|
+
try {
|
|
757
|
+
runController.abort((userSignal as any).reason);
|
|
758
|
+
} catch {
|
|
759
|
+
try { runController.abort(); } catch {}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
try { userSignal.addEventListener("abort", forward, { once: true }); } catch {}
|
|
763
|
+
try {
|
|
764
|
+
mergeDisposers.set(runController, () => {
|
|
765
|
+
try { userSignal.removeEventListener("abort", forward); } catch {}
|
|
766
|
+
});
|
|
767
|
+
} catch {}
|
|
768
|
+
return runController.signal;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private startNonStreamingRun(
|
|
772
|
+
prompt: string | ContentPart[],
|
|
773
|
+
options?: AgentRunOptions
|
|
774
|
+
): Promise<AgentResponse> {
|
|
775
|
+
const runController = new AbortController();
|
|
776
|
+
const steerInbox: SteerEntry[] = [];
|
|
777
|
+
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
778
|
+
const runOptions = { ...options, signal: mergedSignal };
|
|
779
|
+
// Q-19a: route to the named session context (default = this.context).
|
|
780
|
+
const targetContext = this.getSessionContext(runOptions?.sessionId || this.sessionId);
|
|
781
|
+
const task = (async (): Promise<AgentResponse> => {
|
|
782
|
+
await ensureModelCatalogFresh();
|
|
783
|
+
const resolved = resolveModel(this.modelStringOrSpec);
|
|
784
|
+
const effectiveLevel = runOptions?.thinkingLevel || this.thinkingConfig?.level;
|
|
785
|
+
if (effectiveLevel) {
|
|
786
|
+
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
787
|
+
}
|
|
788
|
+
this.prepareTurn(prompt, runOptions, targetContext);
|
|
789
|
+
this.persistNow();
|
|
790
|
+
|
|
791
|
+
const providerOptions = {
|
|
792
|
+
apiKey: this.apiKey,
|
|
793
|
+
baseUrl: this.baseUrl,
|
|
794
|
+
headers: { ...(this.customHeaders ?? {}), ...(runOptions?.headers ?? {}) },
|
|
795
|
+
thinking: resolveEffectiveThinking(this.thinkingConfig, runOptions?.thinkingLevel),
|
|
796
|
+
cache: this.stateless
|
|
797
|
+
? { sessionId: runOptions?.sessionId || this.sessionId }
|
|
798
|
+
: { ...this.cacheConfig, sessionId: runOptions?.sessionId || this.sessionId },
|
|
799
|
+
serviceTier: this.serviceTier,
|
|
800
|
+
sessionId: runOptions?.sessionId || this.sessionId,
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const loopConfig = {
|
|
804
|
+
agentName: this.name,
|
|
805
|
+
provider: resolved.provider,
|
|
806
|
+
modelId: resolved.modelId,
|
|
807
|
+
context: targetContext,
|
|
808
|
+
tools: this.tools,
|
|
809
|
+
options: providerOptions,
|
|
810
|
+
runOptions,
|
|
811
|
+
maxTurns: this.maxTurns,
|
|
812
|
+
bypassInputFileModality: this.bypassInputFileModality,
|
|
813
|
+
onProgress: () => this.persistNow(),
|
|
814
|
+
steerInbox,
|
|
815
|
+
onSteerInjected: () => this.persistNow(),
|
|
816
|
+
// Q-22: loop enforces these (plus local timeout races).
|
|
817
|
+
limits: { ...this.runLimits },
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
const res = await runAgentLoop(loopConfig);
|
|
821
|
+
this.mergeSubagentTraces(res.subagents as any);
|
|
822
|
+
this.persistNow();
|
|
823
|
+
if (this.stateless) {
|
|
824
|
+
targetContext.messages = [];
|
|
825
|
+
targetContext.thoughtSignatures = [];
|
|
826
|
+
}
|
|
827
|
+
return res;
|
|
828
|
+
})();
|
|
829
|
+
|
|
830
|
+
this.currentRun = { steerInbox, abortController: runController, runPromise: task };
|
|
831
|
+
const cleanup = () => {
|
|
832
|
+
try { this.cleanupMerge(runController); } catch {}
|
|
833
|
+
if (this.currentRun?.runPromise === task) this.finishCurrentRun();
|
|
834
|
+
};
|
|
835
|
+
task.then(cleanup, cleanup);
|
|
836
|
+
return task;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private startStreamingRun(
|
|
840
|
+
prompt: string | ContentPart[],
|
|
841
|
+
options?: AgentRunOptions
|
|
842
|
+
): AssistantMessageEventStream {
|
|
843
|
+
const resolved = resolveModel(this.modelStringOrSpec);
|
|
844
|
+
const effectiveLevel = options?.thinkingLevel || this.thinkingConfig?.level;
|
|
845
|
+
if (effectiveLevel) {
|
|
846
|
+
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
847
|
+
}
|
|
848
|
+
// Q-19a: named session routing (default session for compat).
|
|
849
|
+
const targetContext = this.getSessionContext(options?.sessionId || this.sessionId);
|
|
850
|
+
this.prepareTurn(prompt, options, targetContext);
|
|
851
|
+
this.persistNow();
|
|
852
|
+
|
|
853
|
+
const runController = new AbortController();
|
|
854
|
+
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
855
|
+
const runOptions = { ...options, signal: mergedSignal };
|
|
856
|
+
const steerInbox: SteerEntry[] = [];
|
|
857
|
+
|
|
858
|
+
const providerOptions = {
|
|
859
|
+
apiKey: this.apiKey,
|
|
860
|
+
baseUrl: this.baseUrl,
|
|
861
|
+
headers: { ...(this.customHeaders ?? {}), ...(runOptions?.headers ?? {}) },
|
|
862
|
+
thinking: resolveEffectiveThinking(this.thinkingConfig, runOptions?.thinkingLevel),
|
|
863
|
+
cache: this.stateless
|
|
864
|
+
? { sessionId: runOptions?.sessionId || this.sessionId }
|
|
865
|
+
: { ...this.cacheConfig, sessionId: runOptions?.sessionId || this.sessionId },
|
|
866
|
+
serviceTier: this.serviceTier,
|
|
867
|
+
sessionId: runOptions?.sessionId || this.sessionId,
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
const loopConfig = {
|
|
871
|
+
agentName: this.name,
|
|
872
|
+
provider: resolved.provider,
|
|
873
|
+
modelId: resolved.modelId,
|
|
874
|
+
context: targetContext,
|
|
875
|
+
tools: this.tools,
|
|
876
|
+
options: providerOptions,
|
|
877
|
+
runOptions,
|
|
878
|
+
maxTurns: this.maxTurns,
|
|
879
|
+
bypassInputFileModality: this.bypassInputFileModality,
|
|
880
|
+
onProgress: () => this.persistNow(),
|
|
881
|
+
steerInbox,
|
|
882
|
+
onSteerInjected: () => this.persistNow(),
|
|
883
|
+
// Q-22: loop enforces these (plus local timeout races).
|
|
884
|
+
limits: { ...this.runLimits },
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
const s = streamAgentLoop(loopConfig);
|
|
888
|
+
this.currentRun = { steerInbox, outerStream: s, abortController: runController };
|
|
889
|
+
s.result().then(
|
|
890
|
+
(res) => {
|
|
891
|
+
try {
|
|
892
|
+
this.mergeSubagentTraces(res.subagents as any);
|
|
893
|
+
this.persistNow();
|
|
894
|
+
} catch {}
|
|
895
|
+
try { this.cleanupMerge(runController); } catch {}
|
|
896
|
+
if (this.stateless) {
|
|
897
|
+
targetContext.messages = [];
|
|
898
|
+
targetContext.thoughtSignatures = [];
|
|
899
|
+
}
|
|
900
|
+
if (this.currentRun?.outerStream === s) this.finishCurrentRun();
|
|
901
|
+
},
|
|
902
|
+
() => {
|
|
903
|
+
try { this.cleanupMerge(runController); } catch {}
|
|
904
|
+
if (this.currentRun?.outerStream === s) this.finishCurrentRun();
|
|
905
|
+
}
|
|
906
|
+
);
|
|
907
|
+
return s;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Returns a snapshot of one sub-agent worker's trace by tracking id
|
|
912
|
+
* (`SUBAGENT-NAME-{32hex}` or the raw 32-hex suffix). Sees live running
|
|
913
|
+
* state: steps appear as the worker acts, not just after it finishes.
|
|
914
|
+
*
|
|
915
|
+
* @example `const trace = agent.track("RAM-RESEARCH-AGENT-9f2c…");`
|
|
916
|
+
*/
|
|
917
|
+
track(trackingId: string): SubAgentTrace | undefined {
|
|
918
|
+
if (!trackingId) return undefined;
|
|
919
|
+
try {
|
|
920
|
+
const own = (this as any).subagentTraces as Record<string, SubAgentTrace> | undefined;
|
|
921
|
+
const hit = own?.[trackingId];
|
|
922
|
+
if (hit) return { ...hit, steps: hit.steps.map((s) => ({ ...s })) };
|
|
923
|
+
} catch {}
|
|
924
|
+
return getSubAgentTrace(trackingId);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Lists TrackingIDs of worker traces visible to this agent (deduped). */
|
|
928
|
+
listTrackedSubAgents(): string[] {
|
|
929
|
+
const rawOf = (id: string): string => {
|
|
930
|
+
const tail = id.split("-").pop() ?? "";
|
|
931
|
+
return /^[0-9a-f]{32}$/.test(tail) ? tail : id;
|
|
932
|
+
};
|
|
933
|
+
const seen = new Set<string>();
|
|
934
|
+
const out: string[] = [];
|
|
935
|
+
for (const id of [...Object.keys((this as any).subagentTraces ?? {}), ...listSubAgentTraceIds()]) {
|
|
936
|
+
const raw = rawOf(id);
|
|
937
|
+
if (!seen.has(raw)) {
|
|
938
|
+
seen.add(raw);
|
|
939
|
+
out.push(raw);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return out;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Subscribes to live updates of one worker trace. The callback fires on
|
|
947
|
+
* every logged step; returns an unsubscribe fn.
|
|
948
|
+
*
|
|
949
|
+
* @example `const off = agent.subscribeToSubAgent(id, (t) => render(t));`
|
|
950
|
+
*/
|
|
951
|
+
subscribeToSubAgent(trackingId: string, fn: (trace: SubAgentTrace) => void): () => void {
|
|
952
|
+
return subscribeToSubAgent(trackingId, fn);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Rewrites the configured session file now (when `persist` is set).
|
|
957
|
+
* Called automatically after every step during runs; safe to call
|
|
958
|
+
* manually. Never throws (failures route to `onPersistError`).
|
|
959
|
+
*
|
|
960
|
+
* Q-28: stateless agents skip persistence unless
|
|
961
|
+
* `(persist as any).allowStateless` is set. Calls in the same tick are
|
|
962
|
+
* coalesced via a `queueMicrotask` single flight.
|
|
963
|
+
*/
|
|
964
|
+
persistNow(): void {
|
|
965
|
+
const cfg = this.persistConfig;
|
|
966
|
+
if (!cfg || (!cfg.dir && !cfg.file)) return;
|
|
967
|
+
// Q-28: no stateless persist by default.
|
|
968
|
+
try {
|
|
969
|
+
if (this.stateless && !(cfg as unknown as { allowStateless?: unknown }).allowStateless) return;
|
|
970
|
+
} catch {}
|
|
971
|
+
if (this.persistScheduled) return;
|
|
972
|
+
this.persistScheduled = true;
|
|
973
|
+
queueMicrotask(() => {
|
|
974
|
+
this.persistScheduled = false;
|
|
975
|
+
this.doPersist();
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** Synchronous persistence write behind the coalesced `persistNow()` gate. */
|
|
980
|
+
private doPersist(): void {
|
|
981
|
+
const cfg = this.persistConfig;
|
|
982
|
+
if (!cfg || (!cfg.dir && !cfg.file)) return;
|
|
983
|
+
try {
|
|
984
|
+
if (this.stateless && !(cfg as unknown as { allowStateless?: unknown }).allowStateless) return;
|
|
985
|
+
} catch {}
|
|
986
|
+
try {
|
|
987
|
+
if (cfg.dir) saveSessionDir(cfg.dir, this as any, null);
|
|
988
|
+
else if (cfg.file) saveSessionFile(cfg.file, this as any, null);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
// Q-28: surface via hook instead of swallowing silently.
|
|
991
|
+
try { (cfg as unknown as { onPersistError?: (e: unknown) => void }).onPersistError?.(err); } catch {}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Merges completed worker metadata (incl. steps) into this agent's trace registry. */
|
|
996
|
+
private mergeSubagentTraces(subagents: Array<{ trackingId?: string; name: string; sessionId?: string; parentSessionId?: string; task: string; steps?: SubAgentTrace["steps"]; [k: string]: unknown }>): void {
|
|
997
|
+
if (!subagents || subagents.length === 0) return;
|
|
998
|
+
const rawOf = (id?: string): string | undefined => {
|
|
999
|
+
if (!id) return undefined;
|
|
1000
|
+
const tail = id.split("-").pop() ?? "";
|
|
1001
|
+
return /^[0-9a-f]{32}$/.test(tail) ? tail : id;
|
|
1002
|
+
};
|
|
1003
|
+
try {
|
|
1004
|
+
const registry = ((this as any).subagentTraces ??= {}) as Record<string, SubAgentTrace>;
|
|
1005
|
+
for (const s of subagents) {
|
|
1006
|
+
const raw = rawOf(s.trackingId);
|
|
1007
|
+
const key = raw || s.name;
|
|
1008
|
+
const existing = registry[key] ?? (raw ? registry[s.trackingId ?? ""] : undefined);
|
|
1009
|
+
if (existing) {
|
|
1010
|
+
existing.status = "done";
|
|
1011
|
+
if (s.steps && s.steps.length > 0 && existing.steps.length === 0) {
|
|
1012
|
+
existing.steps = s.steps.map((x) => ({ ...x }));
|
|
1013
|
+
}
|
|
1014
|
+
if (raw && !registry[raw]) registry[raw] = existing;
|
|
1015
|
+
} else if (raw) {
|
|
1016
|
+
registry[raw] = {
|
|
1017
|
+
trackingId: raw,
|
|
1018
|
+
name: s.name,
|
|
1019
|
+
sessionId: s.sessionId || "",
|
|
1020
|
+
parentSessionId: s.parentSessionId || this.sessionId,
|
|
1021
|
+
status: "done",
|
|
1022
|
+
task: s.task,
|
|
1023
|
+
turns: 0,
|
|
1024
|
+
steps: (s.steps ?? []).map((x) => ({ ...x })),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} catch {}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Exports this agent's conversation + config as a storable session snapshot.
|
|
1033
|
+
* Pair with `importSession` / `loadSessionFile` / `saveSessionFile`.
|
|
1034
|
+
*
|
|
1035
|
+
* @example `saveSessionFile(".session.json", agent, telemetry)`
|
|
1036
|
+
*/
|
|
1037
|
+
exportSession(telemetry?: SessionTelemetry | SessionTotals | null): PersistedAgentSession {
|
|
1038
|
+
return buildSessionData(this as any, telemetry);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Restores conversation + config from `exportSession` / `loadSessionFile`.
|
|
1043
|
+
* Restores messages, system prompt, cached content id, sub-agent traces,
|
|
1044
|
+
* and (when present) session id, model, thinking level, instructions,
|
|
1045
|
+
* cache, and worker model. No-ops on nullish input.
|
|
1046
|
+
*
|
|
1047
|
+
* @example `const saved = loadSessionFile(".session.json"); if (saved) agent.importSession(saved);`
|
|
1048
|
+
*/
|
|
1049
|
+
importSession(data?: PersistedAgentSession | null): void {
|
|
1050
|
+
if (!data) return;
|
|
1051
|
+
const prevSid = this.sessionId;
|
|
1052
|
+
if (data.sessionId) (this as any).sessionId = data.sessionId;
|
|
1053
|
+
if ((data as any).parentSessionId) (this as any).parentSessionId = (data as any).parentSessionId;
|
|
1054
|
+
if ((data as any).subagents && typeof (data as any).subagents === "object") {
|
|
1055
|
+
try {
|
|
1056
|
+
(this as any).subagentTraces = JSON.parse(JSON.stringify((data as any).subagents));
|
|
1057
|
+
} catch {
|
|
1058
|
+
(this as any).subagentTraces = { ...((data as any).subagents as object) };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (data.model) (this as any).modelStringOrSpec = data.model;
|
|
1062
|
+
if (data.thinkingLevel) {
|
|
1063
|
+
const lvl = String(data.thinkingLevel);
|
|
1064
|
+
(this as any).thinkingConfig =
|
|
1065
|
+
lvl === "none"
|
|
1066
|
+
? { enabled: false, level: "none", budgetTokens: 0 }
|
|
1067
|
+
: lvl === "dynamic"
|
|
1068
|
+
? { enabled: true, level: "dynamic", budgetTokens: -1 }
|
|
1069
|
+
: { enabled: true, level: lvl };
|
|
1070
|
+
}
|
|
1071
|
+
if (typeof data.instructions === "string") this.instructions = data.instructions;
|
|
1072
|
+
if (data.cache) {
|
|
1073
|
+
(this as any).cacheConfig = {
|
|
1074
|
+
...this.cacheConfig,
|
|
1075
|
+
...data.cache,
|
|
1076
|
+
sessionId: data.sessionId ?? this.sessionId,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
if (data.subagentModel) {
|
|
1080
|
+
(this as any).subagentModel = data.subagentModel;
|
|
1081
|
+
const dyn = (this as any).dynamicSubagents;
|
|
1082
|
+
if (dyn) dyn.model = data.subagentModel;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const g: any = globalThis as any;
|
|
1086
|
+
this.context.messages = Array.isArray(data.messages)
|
|
1087
|
+
? (typeof g.structuredClone === "function"
|
|
1088
|
+
? g.structuredClone(data.messages)
|
|
1089
|
+
: JSON.parse(JSON.stringify(data.messages)))
|
|
1090
|
+
: [];
|
|
1091
|
+
} catch {
|
|
1092
|
+
this.context.messages = Array.isArray(data.messages) ? [...data.messages] : [];
|
|
1093
|
+
}
|
|
1094
|
+
this.context.thoughtSignatures = [];
|
|
1095
|
+
if (data.systemPrompt) {
|
|
1096
|
+
this.context.systemPrompt = data.systemPrompt;
|
|
1097
|
+
} else {
|
|
1098
|
+
this.context.systemPrompt = this.getFullInstructions();
|
|
1099
|
+
}
|
|
1100
|
+
this.context.cachedContentId =
|
|
1101
|
+
data.cachedContentId ?? (this.cacheConfig as any)?.cachedContentId;
|
|
1102
|
+
// Q-25: importing a snapshot invalidates prior provider chaining/routing
|
|
1103
|
+
// for both the previous and the restored session.
|
|
1104
|
+
try { clearInteractionChains(prevSid); } catch {}
|
|
1105
|
+
try { clearSessionRouting(prevSid); } catch {}
|
|
1106
|
+
try { clearInteractionChains(this.sessionId); } catch {}
|
|
1107
|
+
try { clearSessionRouting(this.sessionId); } catch {}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): void {
|
|
1111
|
+
// Q-20: prompt validation throws synchronously in both run() and
|
|
1112
|
+
// stream() (same timing; documented here).
|
|
1113
|
+
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
"[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
const ctx = target ?? this.getSessionContext(options?.sessionId || this.sessionId);
|
|
316
1119
|
if (this.stateless) {
|
|
317
|
-
|
|
318
|
-
|
|
1120
|
+
// Q-19a: stateless runs use a fresh ephemeral context (never shared).
|
|
1121
|
+
ctx.messages = [];
|
|
1122
|
+
ctx.thoughtSignatures = [];
|
|
319
1123
|
}
|
|
320
1124
|
const fullInstructions = this.getFullInstructions();
|
|
321
1125
|
// C13: keep systemPrompt stable for Google implicit cache; additionalContext goes as user prefix, not system mutation
|
|
322
1126
|
if (options?.additionalContext) {
|
|
323
1127
|
// Preserve stable instructions as systemPrompt
|
|
324
|
-
|
|
1128
|
+
ctx.systemPrompt = fullInstructions;
|
|
325
1129
|
const prefix = `[Additional Context]\n${options.additionalContext}\n\n`;
|
|
326
1130
|
if (typeof prompt === "string") {
|
|
327
1131
|
prompt = prefix + prompt;
|
|
328
1132
|
} else if (Array.isArray(prompt)) {
|
|
329
1133
|
prompt = [{ type: "text", text: prefix } as ContentPart, ...prompt];
|
|
330
1134
|
}
|
|
331
|
-
} else if (
|
|
332
|
-
|
|
1135
|
+
} else if (ctx.systemPrompt !== fullInstructions) {
|
|
1136
|
+
ctx.systemPrompt = fullInstructions;
|
|
333
1137
|
}
|
|
334
1138
|
// C11: keep context cachedContentId in sync with cacheConfig if updated via options
|
|
335
|
-
if (options?.headers && (this.cacheConfig as any)?.cachedContentId && !
|
|
336
|
-
|
|
1139
|
+
if (options?.headers && (this.cacheConfig as any)?.cachedContentId && !ctx.cachedContentId) {
|
|
1140
|
+
ctx.cachedContentId = (this.cacheConfig as any).cachedContentId;
|
|
337
1141
|
}
|
|
338
|
-
|
|
1142
|
+
ctx.addUserMessage(prompt);
|
|
339
1143
|
}
|
|
340
1144
|
|
|
341
1145
|
/**
|
|
@@ -362,6 +1166,9 @@ export class Agent {
|
|
|
362
1166
|
prompt: string | ContentPart[],
|
|
363
1167
|
options?: AgentRunOptions
|
|
364
1168
|
): Promise<AgentResponse> | AssistantMessageEventStream {
|
|
1169
|
+
// Q-20: prompt validation throws synchronously in both run() and
|
|
1170
|
+
// stream() (same timing; async work starts only after validation).
|
|
1171
|
+
this.assertPromptValid(prompt);
|
|
365
1172
|
const isStream = options?.stream === true;
|
|
366
1173
|
if (isStream) {
|
|
367
1174
|
const hasCallbacks = !!(options?.onDelta || options?.onThinkingDelta || options?.onEvent);
|
|
@@ -384,48 +1191,51 @@ export class Agent {
|
|
|
384
1191
|
return s;
|
|
385
1192
|
}
|
|
386
1193
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1194
|
+
// Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
|
|
1195
|
+
if (this.currentRun) {
|
|
1196
|
+
const enforced = this.midSessionConfig.mode;
|
|
1197
|
+
const requested = options?.enqueue;
|
|
1198
|
+
if (requested && (enforced === "steer" || enforced === "queue") && requested !== enforced) {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
`[Agent Accelerator] enqueue "${requested}" is disabled by midSession.mode "${enforced}". Use ${enforced}() or set mode to "auto"/"${requested}".`
|
|
1201
|
+
);
|
|
395
1202
|
}
|
|
396
|
-
this.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
1203
|
+
const mode = this.resolveEnqueueMode(requested);
|
|
1204
|
+
if (mode === "steer") {
|
|
1205
|
+
this.assertPromptValid(prompt);
|
|
1206
|
+
this.assertSteerRoom(this.currentRun.steerInbox);
|
|
1207
|
+
this.currentRun.steerInbox.push({ prompt, options });
|
|
1208
|
+
this.persistNow();
|
|
1209
|
+
const active = this.currentRun;
|
|
1210
|
+
if (active.runPromise) return active.runPromise;
|
|
1211
|
+
if (active.outerStream) return active.outerStream.result();
|
|
1212
|
+
} else {
|
|
1213
|
+
if (this.midSessionConfig.mode === "steer") {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
'[Agent Accelerator] enqueue "queue" is disabled by midSession.mode "steer". Use steer() or set mode to "auto"/"queue".'
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
this.assertPromptValid(prompt);
|
|
1219
|
+
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
1220
|
+
if (this.pendingQueue.length >= max) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`[Agent Accelerator] Queue is full (${this.pendingQueue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
return new Promise<AgentResponse>((resolve, reject) => {
|
|
1226
|
+
this.pendingQueue.push({ prompt, options, isStream: false, resolve, reject });
|
|
1227
|
+
try {
|
|
1228
|
+
this.currentRun?.outerStream?.push({
|
|
1229
|
+
type: "queued",
|
|
1230
|
+
queuedPrompt: this.previewPrompt(prompt),
|
|
1231
|
+
queueLength: this.pendingQueue.length,
|
|
1232
|
+
} as any);
|
|
1233
|
+
} catch {}
|
|
1234
|
+
});
|
|
426
1235
|
}
|
|
427
|
-
|
|
428
|
-
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return this.startNonStreamingRun(prompt, options);
|
|
429
1239
|
}
|
|
430
1240
|
|
|
431
1241
|
/**
|
|
@@ -495,44 +1305,141 @@ export class Agent {
|
|
|
495
1305
|
prompt: string | ContentPart[],
|
|
496
1306
|
options?: AgentRunOptions
|
|
497
1307
|
): AssistantMessageEventStream {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
1308
|
+
// Q-20: same sync validation timing as run().
|
|
1309
|
+
this.assertPromptValid(prompt);
|
|
1310
|
+
// Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
|
|
1311
|
+
if (this.currentRun) {
|
|
1312
|
+
const enforcedStream = this.midSessionConfig.mode;
|
|
1313
|
+
const requestedStream = options?.enqueue;
|
|
1314
|
+
if (requestedStream && (enforcedStream === "steer" || enforcedStream === "queue") && requestedStream !== enforcedStream) {
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
`[Agent Accelerator] enqueue "${requestedStream}" is disabled by midSession.mode "${enforcedStream}". Use ${enforcedStream}() or set mode to "auto"/"${requestedStream}".`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
const mode = this.resolveEnqueueMode(requestedStream);
|
|
1320
|
+
if (mode === "steer") {
|
|
1321
|
+
this.assertPromptValid(prompt);
|
|
1322
|
+
this.assertSteerRoom(this.currentRun.steerInbox);
|
|
1323
|
+
this.currentRun.steerInbox.push({ prompt, options });
|
|
1324
|
+
this.persistNow();
|
|
1325
|
+
const activeStream = this.currentRun.outerStream;
|
|
1326
|
+
if (activeStream) {
|
|
1327
|
+
this.wireStreamCallbacks(activeStream, options);
|
|
1328
|
+
return activeStream;
|
|
1329
|
+
}
|
|
1330
|
+
// Active run is non-streaming: attach a lightweight stream that ends
|
|
1331
|
+
// with the same final response so streaming callers still get events.
|
|
1332
|
+
const stub = new AssistantMessageEventStream();
|
|
1333
|
+
this.wireStreamCallbacks(stub, options);
|
|
1334
|
+
const activePromise = this.currentRun.runPromise;
|
|
1335
|
+
if (activePromise) {
|
|
1336
|
+
activePromise.then(
|
|
1337
|
+
(res) => {
|
|
1338
|
+
try {
|
|
1339
|
+
stub.push({ type: "done", delta: "", usage: res.usage, finishReason: res.finishReason, responseId: res.responseId } as any);
|
|
1340
|
+
} catch {}
|
|
1341
|
+
try { stub.end(res); } catch {}
|
|
1342
|
+
},
|
|
1343
|
+
(err) => {
|
|
1344
|
+
try { stub.fail(err instanceof Error ? err : new Error(String(err))); } catch {}
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
} else {
|
|
1348
|
+
try { stub.fail(new Error("[Agent Accelerator] No active run to steer.")); } catch {}
|
|
1349
|
+
}
|
|
1350
|
+
return stub;
|
|
1351
|
+
}
|
|
1352
|
+
if (this.midSessionConfig.mode === "steer") {
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
'[Agent Accelerator] enqueue "queue" is disabled by midSession.mode "steer". Use steer() or set mode to "auto"/"queue".'
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
this.assertPromptValid(prompt);
|
|
1358
|
+
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
1359
|
+
if (this.pendingQueue.length >= max) {
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
`[Agent Accelerator] Queue is full (${this.pendingQueue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
const deferred = new AssistantMessageEventStream();
|
|
1365
|
+
this.wireStreamCallbacks(deferred, options);
|
|
1366
|
+
// Notify the active stream (if any) that a follow-up was queued.
|
|
1367
|
+
try {
|
|
1368
|
+
this.currentRun.outerStream?.push({
|
|
1369
|
+
type: "queued",
|
|
1370
|
+
queuedPrompt: this.previewPrompt(prompt),
|
|
1371
|
+
queueLength: this.pendingQueue.length + 1,
|
|
1372
|
+
} as any);
|
|
1373
|
+
} catch {}
|
|
1374
|
+
const queuedEntry: {
|
|
1375
|
+
prompt: string | ContentPart[];
|
|
1376
|
+
options?: AgentRunOptions;
|
|
1377
|
+
isStream: boolean;
|
|
1378
|
+
deferredStream?: AssistantMessageEventStream;
|
|
1379
|
+
resolve: (res: AgentResponse) => void;
|
|
1380
|
+
reject: (err: unknown) => void;
|
|
1381
|
+
} = {
|
|
1382
|
+
prompt,
|
|
1383
|
+
options,
|
|
1384
|
+
isStream: true,
|
|
1385
|
+
deferredStream: deferred,
|
|
1386
|
+
resolve: () => {},
|
|
1387
|
+
reject: () => {},
|
|
1388
|
+
};
|
|
1389
|
+
const done = new Promise<AgentResponse>((resolve, reject) => {
|
|
1390
|
+
queuedEntry.resolve = resolve;
|
|
1391
|
+
queuedEntry.reject = reject;
|
|
1392
|
+
});
|
|
1393
|
+
// Keep result() in sync: deferred ends when the queued turn completes.
|
|
1394
|
+
done.then(
|
|
1395
|
+
() => {},
|
|
1396
|
+
() => {}
|
|
1397
|
+
);
|
|
1398
|
+
// Forward cancellation before start: drop from queue and fail.
|
|
1399
|
+
deferred.onCancel(() => {
|
|
1400
|
+
try {
|
|
1401
|
+
const idx = this.pendingQueue.indexOf(queuedEntry as any);
|
|
1402
|
+
if (idx >= 0) {
|
|
1403
|
+
this.pendingQueue.splice(idx, 1);
|
|
1404
|
+
queuedEntry.reject(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
|
|
1405
|
+
try { deferred.fail(Object.assign(new Error("Stream aborted"), { name: "AbortError" })); } catch {}
|
|
1406
|
+
}
|
|
1407
|
+
} catch {}
|
|
1408
|
+
});
|
|
1409
|
+
this.pendingQueue.push(queuedEntry as any);
|
|
1410
|
+
// Attach deferred completion to queued promise without exposing it.
|
|
1411
|
+
// Real events are piped in pumpQueue() when the turn starts.
|
|
1412
|
+
void done.then(
|
|
1413
|
+
(res) => {
|
|
1414
|
+
// If pump already piped via real stream, deferred is already ended.
|
|
1415
|
+
// Otherwise (edge), end it here.
|
|
1416
|
+
try {
|
|
1417
|
+
if ((deferred as any).__piped !== true) {
|
|
1418
|
+
deferred.push({ type: "done", delta: "", usage: res.usage, finishReason: res.finishReason, responseId: res.responseId } as any);
|
|
1419
|
+
deferred.end(res);
|
|
1420
|
+
}
|
|
1421
|
+
} catch {}
|
|
1422
|
+
},
|
|
1423
|
+
(err) => {
|
|
1424
|
+
try {
|
|
1425
|
+
if ((deferred as any).__piped !== true) {
|
|
1426
|
+
deferred.fail(err instanceof Error ? err : new Error(String(err)));
|
|
1427
|
+
}
|
|
1428
|
+
} catch {}
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
1431
|
+
return deferred;
|
|
502
1432
|
}
|
|
503
|
-
this.prepareTurn(prompt, options);
|
|
504
|
-
|
|
505
|
-
const providerOptions = {
|
|
506
|
-
apiKey: this.apiKey,
|
|
507
|
-
baseUrl: this.baseUrl,
|
|
508
|
-
headers: { ...(this.customHeaders ?? {}), ...(options?.headers ?? {}) },
|
|
509
|
-
thinking: resolveEffectiveThinking(this.thinkingConfig, options?.thinkingLevel),
|
|
510
|
-
cache: this.stateless
|
|
511
|
-
? { sessionId: options?.sessionId || this.sessionId }
|
|
512
|
-
: { ...this.cacheConfig, sessionId: options?.sessionId || this.sessionId },
|
|
513
|
-
serviceTier: this.serviceTier,
|
|
514
|
-
sessionId: options?.sessionId || this.sessionId,
|
|
515
|
-
};
|
|
516
1433
|
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
context: this.context,
|
|
522
|
-
tools: this.tools,
|
|
523
|
-
options: providerOptions,
|
|
524
|
-
runOptions: options,
|
|
525
|
-
maxTurns: this.maxTurns,
|
|
526
|
-
bypassInputFileModality: this.bypassInputFileModality,
|
|
527
|
-
};
|
|
1434
|
+
const s = this.startStreamingRun(prompt, options);
|
|
1435
|
+
this.wireStreamCallbacks(s, options);
|
|
1436
|
+
return s;
|
|
1437
|
+
}
|
|
528
1438
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
this.context.thoughtSignatures = [];
|
|
534
|
-
}).catch(() => {});
|
|
535
|
-
}
|
|
1439
|
+
private wireStreamCallbacks(
|
|
1440
|
+
s: AssistantMessageEventStream,
|
|
1441
|
+
options?: AgentRunOptions
|
|
1442
|
+
): void {
|
|
536
1443
|
// Wire one-liner callbacks so `stream:true` + onDelta is enough — no manual for-await needed
|
|
537
1444
|
if (options?.wrapThinking) {
|
|
538
1445
|
// Auto-wrap reasoning as <think>…</think> per-turn — clean boundaries across multi-turn agent runs.
|
|
@@ -698,13 +1605,12 @@ export class Agent {
|
|
|
698
1605
|
if (options?.onDelta) s.on("text_delta", (e: any) => options.onDelta!(e.delta!, e));
|
|
699
1606
|
if (options?.onThinkingDelta) s.on("thinking_delta", (e: any) => options.onThinkingDelta!(e.thinkingDelta!, e));
|
|
700
1607
|
}
|
|
701
|
-
return s;
|
|
702
1608
|
}
|
|
703
1609
|
}
|
|
704
1610
|
|
|
705
1611
|
/** Normalizes the public `thinkingLevel` flag into provider-neutral thinking settings. */
|
|
706
1612
|
export function normalizeThinking(config: AgentConfig, fallbackLevel?: string): ThinkingConfig | undefined {
|
|
707
|
-
// DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh
|
|
1613
|
+
// DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh, max
|
|
708
1614
|
const rawLevel = config.thinkingLevel ?? fallbackLevel;
|
|
709
1615
|
const level = rawLevel as ThinkingLevel | undefined;
|
|
710
1616
|
|