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