agent-accelerator 0.4.0 → 0.5.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 +149 -57
- package/bunfig.toml +1 -0
- package/package.json +2 -3
- package/src/agent/agent.ts +414 -118
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +327 -99
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +58 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +47 -2
- package/src/providers/google.ts +338 -81
- package/src/providers/openai-compat.ts +171 -61
- package/src/providers/openai.ts +174 -44
- package/src/providers/openrouter.ts +129 -44
- package/src/providers/shared.ts +169 -17
- package/src/providers.ts +375 -11
- package/src/scheduler/scheduled-actions.md +41 -0
- package/src/scheduler/scheduler.ts +814 -0
- package/src/scheduler/store.ts +263 -0
- package/src/scheduler/time.ts +148 -0
- package/src/scheduler/tool.ts +151 -0
- package/src/session/store.ts +229 -113
- package/src/streaming/event-stream.ts +6 -4
- package/src/telemetry-file.ts +31 -0
- package/src/telemetry.ts +65 -0
- package/src/tools/executor.ts +163 -38
- package/src/tools/subagent-result.ts +60 -0
- package/src/types/agent.ts +74 -22
- package/src/types/model.ts +39 -0
- package/src/types/response.ts +9 -0
- package/src/types/scheduler.ts +81 -0
- package/src/types/tool.ts +15 -0
- package/src/utils/cache.ts +14 -5
- package/src/utils/documents.ts +112 -52
- package/src/utils/env.ts +21 -7
- package/src/utils/errors.ts +26 -1
- package/src/utils/headers.ts +15 -15
- package/src/utils/home.ts +30 -0
- package/src/utils/media.ts +57 -12
- package/src/utils/raw-capture.ts +76 -0
- package/src/utils/retry.ts +11 -3
- package/src/utils/safe-io.ts +252 -0
- package/src/utils/session.ts +7 -8
- package/src/data/README.md +0 -84
package/src/agent/agent.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { AgentConfig, AgentRunOptions, MidSessionConfig } from "../types/agent.ts";
|
|
1
|
+
import type { AgentConfig, AgentRunOptions, MidSessionConfig, ToolChoiceOption, RetryPolicyConfig, Budget, BudgetState } from "../types/agent.ts";
|
|
2
|
+
import type { TokenUsage } from "../types/core.ts";
|
|
3
|
+
import type { ProviderRequestOptions } from "../types/model.ts";
|
|
4
|
+
import { normalizeThinkingV2, resolveWhenBusy, normalizeWorkerTimeout } from "../tools/tool.ts";
|
|
2
5
|
import type { ToolDefinition } from "../types/tool.ts";
|
|
3
6
|
import type { ThinkingLevel, ThinkingConfig, CacheConfig, ServiceTier } from "../types/core.ts";
|
|
4
7
|
import type { ContentPart } from "../types/message.ts";
|
|
@@ -7,14 +10,33 @@ import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
|
7
10
|
import { AgentContext } from "./context.ts";
|
|
8
11
|
import { resolveModel } from "../providers/registry.ts";
|
|
9
12
|
import { buildAgentTools, createSubagentSpawnTool } from "./delegation.ts";
|
|
10
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
normalizeToolMap,
|
|
15
|
+
resolveDynamicModelInput,
|
|
16
|
+
resolveMainModel,
|
|
17
|
+
} from "./worker.ts";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { createConvertDocumentTool } from "../utils/documents.ts";
|
|
20
|
+
import { registerAgent } from "../scheduler/scheduler.ts";
|
|
21
|
+
import { resolveHome } from "../scheduler/store.ts";
|
|
22
|
+
import { persistTelemetry } from "../telemetry-file.ts";
|
|
23
|
+
import { createScheduledActionsTool, getDatetimeTool } from "../scheduler/tool.ts";
|
|
24
|
+
import { assertNoToolCollisions } from "../tools/executor.ts";
|
|
11
25
|
import { runAgentLoop, streamAgentLoop, type SteerEntry } from "./loop.ts";
|
|
12
|
-
import { createSessionId } from "../utils/session.ts";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
import { createSessionId, nowMs } from "../utils/session.ts";
|
|
27
|
+
import {
|
|
28
|
+
chargeBudgetUsage,
|
|
29
|
+
checkBudget,
|
|
30
|
+
getSubAgentTraceForSessions,
|
|
31
|
+
subscribeToSubAgentForSessions,
|
|
32
|
+
listSubAgentTraceIdsForSessions,
|
|
33
|
+
type SubAgentTrace,
|
|
34
|
+
} from "./delegation.ts";
|
|
35
|
+
import { saveSessionDir, saveSessionFile, findInvalidMessage, telemetryBus } from "../session/store.ts";
|
|
36
|
+
import { isLocalPathInput } from "../utils/safe-io.ts";
|
|
37
|
+
import { clearSessionRouting, normalizeStructuredOutput, normalizeStop } from "../providers.ts";
|
|
38
|
+
import type { StructuredOutputSpec } from "../types/model.ts";
|
|
16
39
|
import { clearInteractionChains } from "../providers/google.ts";
|
|
17
|
-
import { getModel, getSubModel } from "../utils/env.ts";
|
|
18
40
|
import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalog.ts";
|
|
19
41
|
import { buildSessionData, type PersistedAgentSession, type SessionTotals,} from "../session/store.ts";
|
|
20
42
|
import { SessionTelemetry } from "../session/store.ts";
|
|
@@ -32,7 +54,7 @@ export function resolveEffectiveThinking(
|
|
|
32
54
|
return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
import { ConfigError } from "../types/errors.ts";
|
|
57
|
+
import { ConfigError, SessionLoadError } from "../types/errors.ts";
|
|
36
58
|
|
|
37
59
|
/**
|
|
38
60
|
* Disposers for the manual user-signal forward listeners (Q-21 fallback when
|
|
@@ -51,6 +73,7 @@ export interface AgentRunLimits {
|
|
|
51
73
|
maxTurns?: number;
|
|
52
74
|
deadlineMs?: number;
|
|
53
75
|
maxToolCallsPerRun?: number;
|
|
76
|
+
maxTotalTokens?: number;
|
|
54
77
|
requestTimeoutMs: number;
|
|
55
78
|
firstByteTimeoutMs: number;
|
|
56
79
|
streamIdleTimeoutMs: number;
|
|
@@ -67,6 +90,7 @@ function resolveAgentLimits(config: { limits?: Record<string, unknown> }): Agent
|
|
|
67
90
|
...(num(raw.maxToolCallsPerRun) !== undefined
|
|
68
91
|
? { maxToolCallsPerRun: Math.max(1, Math.floor(num(raw.maxToolCallsPerRun)!)) }
|
|
69
92
|
: {}),
|
|
93
|
+
...(num(raw.maxTotalTokens) !== undefined ? { maxTotalTokens: Math.max(1, Math.floor(num(raw.maxTotalTokens)!)) } : {}),
|
|
70
94
|
requestTimeoutMs: num(raw.requestTimeoutMs) ?? 300_000,
|
|
71
95
|
firstByteTimeoutMs: num(raw.firstByteTimeoutMs) ?? 120_000,
|
|
72
96
|
streamIdleTimeoutMs: num(raw.streamIdleTimeoutMs) ?? 300_000,
|
|
@@ -134,17 +158,54 @@ export class Agent {
|
|
|
134
158
|
thinkingLevel?: ThinkingLevel;
|
|
135
159
|
tools: Record<string, ToolDefinition>;
|
|
136
160
|
timeout: number;
|
|
161
|
+
/** Scheduled actions (`scheduled_actions` + `get_datetime` tools) are on. */
|
|
162
|
+
allowScheduledActions: boolean;
|
|
137
163
|
};
|
|
164
|
+
/** Home folder for everything written to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). */
|
|
165
|
+
readonly home: string;
|
|
138
166
|
/** Registered model-callable tools. */
|
|
139
167
|
readonly tools: Record<string, ToolDefinition> = {};
|
|
140
168
|
/** Normalized reasoning configuration. */
|
|
141
169
|
readonly thinkingConfig?: ThinkingConfig;
|
|
170
|
+
/** Canonical structured-output request (raw `AgentConfig.output`). */
|
|
171
|
+
readonly output?: unknown;
|
|
172
|
+
/** Normalized structured-output spec sent to providers. */
|
|
173
|
+
readonly outputSpec?: StructuredOutputSpec;
|
|
142
174
|
/** Cache retention/session settings. */
|
|
143
175
|
readonly cacheConfig?: CacheConfig;
|
|
144
176
|
/** Provider service tier. */
|
|
145
177
|
readonly serviceTier?: ServiceTier;
|
|
146
178
|
/** Convert `file` parts client-side for pdf-incapable models. */
|
|
147
179
|
readonly bypassInputFileModality: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Local-file policy. `roots` set: every local-path media part and the
|
|
182
|
+
* built-in document tool must resolve inside them (`[]` denies all paths).
|
|
183
|
+
* Unset: developer-supplied parts are unrestricted and the model-callable
|
|
184
|
+
* tool is limited to the working directory.
|
|
185
|
+
*/
|
|
186
|
+
readonly fileAccess?: { roots?: string[] };
|
|
187
|
+
/** Failure policy for the conversation (see `AgentConfig.onFailure`). */
|
|
188
|
+
readonly onFailure: "auto" | "rollback" | "keep";
|
|
189
|
+
/** Wire-capture policy for `AgentResponse.raw` (default `"redacted"`: bodies replaced by a content-free summary). */
|
|
190
|
+
readonly captureRaw?: false | "redacted" | true;
|
|
191
|
+
/** Constructor config, kept so {@link Agent.fork} can build isolated copies. */
|
|
192
|
+
private readonly forkConfig: AgentConfig;
|
|
193
|
+
/** Opt-in per-run budget (tokens / cost / duration / sub-agent count), enforced by the loop. */
|
|
194
|
+
readonly budget?: Budget;
|
|
195
|
+
/** Live counters behind {@link Agent.budget}; reset at the start of every run. */
|
|
196
|
+
budgetState?: BudgetState;
|
|
197
|
+
/** Google chain-TTL tier (`free` 23h default / `paid` 54d). */
|
|
198
|
+
readonly googleTier?: "free" | "paid";
|
|
199
|
+
/** Per-call defaults forwarded to providers; per-run options override them. */
|
|
200
|
+
private readonly callDefaults: {
|
|
201
|
+
toolChoice?: ToolChoiceOption;
|
|
202
|
+
maxOutputTokens?: number;
|
|
203
|
+
stop?: string | string[];
|
|
204
|
+
seed?: number;
|
|
205
|
+
retry?: RetryPolicyConfig;
|
|
206
|
+
env?: Record<string, string>;
|
|
207
|
+
fetch?: typeof fetch;
|
|
208
|
+
};
|
|
148
209
|
/** Maximum model/tool turns per run. */
|
|
149
210
|
readonly maxTurns: number;
|
|
150
211
|
/** Session ID used for history and cache affinity. */
|
|
@@ -215,53 +276,29 @@ export class Agent {
|
|
|
215
276
|
this.description = config.description || "AI Agent powered by Agent Accelerator";
|
|
216
277
|
// DX1: only instructions (no systemPrompt alias)
|
|
217
278
|
this.instructions = config.instructions || "";
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
} else if (rawModel && typeof rawModel === "object" && "id" in rawModel && "provider" in rawModel) {
|
|
223
|
-
this.modelStringOrSpec = rawModel;
|
|
224
|
-
} else {
|
|
225
|
-
this.modelStringOrSpec = rawModel || getModel();
|
|
226
|
-
}
|
|
279
|
+
// Unified model resolution (shared with SubAgent/workers): supports
|
|
280
|
+
// ModelProviderInstance {model,apiKey,...} and ModelSpec {id,provider}.
|
|
281
|
+
const { modelStringOrSpec, rawModel } = resolveMainModel(config.model);
|
|
282
|
+
this.modelStringOrSpec = modelStringOrSpec as string | any;
|
|
227
283
|
const dynRaw = config.dynamicSubagents;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
dynModel = (dynModelRaw as any).model;
|
|
232
|
-
} else if (dynModelRaw && typeof dynModelRaw === "object" && "id" in dynModelRaw && "provider" in dynModelRaw) {
|
|
233
|
-
dynModel = dynModelRaw;
|
|
234
|
-
} else {
|
|
235
|
-
dynModel = (dynModelRaw as any) || getSubModel();
|
|
236
|
-
}
|
|
237
|
-
this.subagentModel = dynModel;
|
|
284
|
+
// Unified worker-model chain: dynamicSubagents.model > subagentModel > SUB_AGENT_MODEL.
|
|
285
|
+
const dynModel = resolveDynamicModelInput(dynRaw, config.subagentModel);
|
|
286
|
+
this.subagentModel = dynModel as string | any | undefined;
|
|
238
287
|
const dynEnabled = dynRaw ? (dynRaw.enabled ?? true) : false;
|
|
239
288
|
if (dynRaw) {
|
|
240
|
-
const
|
|
241
|
-
const dynTools: Record<string, ToolDefinition> = {};
|
|
242
|
-
if (rawTools) {
|
|
243
|
-
if (Array.isArray(rawTools)) {
|
|
244
|
-
for (const t of rawTools) {
|
|
245
|
-
const tName = (t as any)?.name || `tool_${Object.keys(dynTools).length}`;
|
|
246
|
-
dynTools[tName] = { ...(t as any), name: tName };
|
|
247
|
-
}
|
|
248
|
-
} else {
|
|
249
|
-
for (const [key, def] of Object.entries(rawTools as Record<string, ToolDefinition>)) {
|
|
250
|
-
dynTools[key] = { ...(def as any), name: (def as any)?.name || key };
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
289
|
+
const dynTools = normalizeToolMap((dynRaw as { tools?: Record<string, ToolDefinition> | ToolDefinition[] }).tools);
|
|
254
290
|
const rawMax = (dynRaw as any)?.maxSpawn;
|
|
255
291
|
const maxSpawn = Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax as number)) : 4;
|
|
256
|
-
|
|
257
|
-
const timeout =
|
|
292
|
+
// `workerTimeoutMs ?? timeout`, or -1 when `modelMaySetTimeout` (see normalizeWorkerTimeout).
|
|
293
|
+
const timeout = normalizeWorkerTimeout(dynRaw as { timeout?: number; workerTimeoutMs?: number; modelMaySetTimeout?: boolean });
|
|
258
294
|
this.dynamicSubagents = {
|
|
259
295
|
enabled: dynEnabled,
|
|
260
|
-
model: dynModel,
|
|
296
|
+
model: dynModel as string | any | undefined,
|
|
261
297
|
maxSpawn,
|
|
262
298
|
thinkingLevel: (dynRaw as any)?.thinkingLevel,
|
|
263
299
|
tools: dynTools,
|
|
264
300
|
timeout,
|
|
301
|
+
allowScheduledActions: dynRaw.allowScheduledActions === true,
|
|
265
302
|
};
|
|
266
303
|
}
|
|
267
304
|
this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
|
|
@@ -275,9 +312,14 @@ export class Agent {
|
|
|
275
312
|
: Infinity;
|
|
276
313
|
this.sessionId = config.sessionId || config.cache?.sessionId || createSessionId();
|
|
277
314
|
(this as any).subagentTraces = {};
|
|
278
|
-
this.
|
|
279
|
-
|
|
280
|
-
|
|
315
|
+
this.home = resolveHome(config.home);
|
|
316
|
+
// `persist: {}` (neither dir nor file) means the default sessions folder under home.
|
|
317
|
+
this.persistConfig =
|
|
318
|
+
config.persist && !config.persist.dir && !config.persist.file
|
|
319
|
+
? { ...config.persist, dir: path.join(this.home, "sessions") }
|
|
320
|
+
: config.persist;
|
|
321
|
+
// `whenBusy` (canonical) wins over legacy `mode`; anything else is "auto".
|
|
322
|
+
const normalizedMode = resolveWhenBusy(config.midSession);
|
|
281
323
|
const rawMaxQueued = (config.midSession as any)?.maxQueued;
|
|
282
324
|
const rawMaxSteers = (config.midSession as any)?.maxSteers;
|
|
283
325
|
this.midSessionConfig = {
|
|
@@ -298,6 +340,12 @@ export class Agent {
|
|
|
298
340
|
const mpThinking = (rawModel as any)?.thinkingLevel;
|
|
299
341
|
this.thinkingConfig = normalizeThinking(config, mpThinking);
|
|
300
342
|
|
|
343
|
+
// Structured outputs: validate early so invalid schemas fail fast at construction.
|
|
344
|
+
if (config.output !== undefined && config.output !== null) {
|
|
345
|
+
this.output = config.output;
|
|
346
|
+
this.outputSpec = normalizeStructuredOutput(config.output);
|
|
347
|
+
}
|
|
348
|
+
|
|
301
349
|
// DX5: cache retention short|medium|long, undefined = no explicit
|
|
302
350
|
this.cacheConfig = {
|
|
303
351
|
sessionId: this.sessionId,
|
|
@@ -308,23 +356,40 @@ export class Agent {
|
|
|
308
356
|
|
|
309
357
|
this.serviceTier = config.serviceTier;
|
|
310
358
|
this.bypassInputFileModality = config.bypassInputFileModality ?? false;
|
|
359
|
+
this.onFailure =
|
|
360
|
+
config.onFailure === "rollback" || config.onFailure === "keep" ? config.onFailure : "auto";
|
|
361
|
+
this.forkConfig = { ...config };
|
|
362
|
+
this.captureRaw = config.captureRaw;
|
|
363
|
+
this.budget = config.budget ? { ...config.budget } : undefined;
|
|
364
|
+
for (const key of ["hooks", "contextManagement", "logger"] as const) {
|
|
365
|
+
if (config[key] !== undefined) {
|
|
366
|
+
// Declared for forward compatibility only: say so instead of silently ignoring.
|
|
367
|
+
telemetryBus.emitWarning(
|
|
368
|
+
"config.unsupported_option",
|
|
369
|
+
`[Agent Accelerator] AgentConfig.${key} is accepted for forward compatibility but is not implemented yet; it has no effect. Attach to telemetryBus.onEvent for observability.`,
|
|
370
|
+
{ option: key }
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (config.telemetry?.persist) persistTelemetry(this.home);
|
|
375
|
+
this.googleTier = config.googleTier === "paid" || config.googleTier === "free" ? config.googleTier : undefined;
|
|
376
|
+
this.callDefaults = {
|
|
377
|
+
toolChoice: config.toolChoice,
|
|
378
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
379
|
+
stop: config.stop,
|
|
380
|
+
seed: config.seed,
|
|
381
|
+
retry: config.retry,
|
|
382
|
+
env: config.env,
|
|
383
|
+
fetch: config.fetch,
|
|
384
|
+
};
|
|
385
|
+
this.fileAccess = config.fileAccess
|
|
386
|
+
? { ...(config.fileAccess.roots ? { roots: [...config.fileAccess.roots] } : {}) }
|
|
387
|
+
: undefined;
|
|
311
388
|
this.stateless = config.stateless ?? false;
|
|
312
389
|
|
|
313
|
-
// Tools registration
|
|
390
|
+
// Tools registration (unified normalization shared with dynamic pool).
|
|
314
391
|
if (config.tools) {
|
|
315
|
-
|
|
316
|
-
for (const t of config.tools) {
|
|
317
|
-
const tName = t.name || `tool_${Object.keys(this.tools).length}`;
|
|
318
|
-
this.tools[tName] = { ...t, name: tName };
|
|
319
|
-
}
|
|
320
|
-
} else {
|
|
321
|
-
for (const [key, def] of Object.entries(config.tools)) {
|
|
322
|
-
this.tools[key] = {
|
|
323
|
-
...def,
|
|
324
|
-
name: def.name || key,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
}
|
|
392
|
+
Object.assign(this.tools, normalizeToolMap(config.tools));
|
|
328
393
|
}
|
|
329
394
|
|
|
330
395
|
|
|
@@ -336,23 +401,34 @@ export class Agent {
|
|
|
336
401
|
}
|
|
337
402
|
|
|
338
403
|
if (this.bypassInputFileModality && !this.tools["convert_document_to_markdown"]) {
|
|
339
|
-
this.tools["convert_document_to_markdown"] =
|
|
404
|
+
this.tools["convert_document_to_markdown"] = createConvertDocumentTool({ roots: this.fileAccess?.roots });
|
|
340
405
|
}
|
|
341
406
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
407
|
+
const dyn = this.dynamicSubagents;
|
|
408
|
+
if ((dyn?.enabled === true || dyn?.allowScheduledActions) && !this.subagentModel) {
|
|
409
|
+
throw new SubAgentModelError(
|
|
410
|
+
typeof this.modelStringOrSpec === "string" ? this.modelStringOrSpec : (this.modelStringOrSpec as any)?.id
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (dyn?.enabled === true) {
|
|
348
414
|
const spawnTool = createSubagentSpawnTool(this);
|
|
349
415
|
this.tools[spawnTool.name || "spawn_subagents"] = spawnTool;
|
|
350
416
|
}
|
|
417
|
+
if (dyn?.allowScheduledActions) {
|
|
418
|
+
// Fixed by config, so the tool set (and the prompt cache) stays stable.
|
|
419
|
+
const scheduleTool = createScheduledActionsTool(this);
|
|
420
|
+
this.tools[scheduleTool.name || "scheduled_actions"] = scheduleTool;
|
|
421
|
+
if (!this.tools["get_datetime"]) this.tools["get_datetime"] = getDatetimeTool;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Ambiguous tool names would only blow up mid-run, after a paid model turn: fail at construction.
|
|
425
|
+
assertNoToolCollisions(this.tools);
|
|
351
426
|
|
|
352
427
|
this.context = new AgentContext(this.getFullInstructions());
|
|
353
428
|
if (initialCachedId) {
|
|
354
429
|
this.context.cachedContentId = initialCachedId;
|
|
355
430
|
}
|
|
431
|
+
if (dyn?.allowScheduledActions) registerAgent(this);
|
|
356
432
|
}
|
|
357
433
|
|
|
358
434
|
private getFullInstructions(): string | undefined {
|
|
@@ -594,12 +670,39 @@ export class Agent {
|
|
|
594
670
|
}
|
|
595
671
|
const active = this.currentRun;
|
|
596
672
|
if (!active) return this.run(prompt, options) as Promise<AgentResponse>;
|
|
673
|
+
return this.steerInto(active, prompt, options);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Buffers a steer prompt into the active run and returns a promise for ITS
|
|
678
|
+
* reply: the active run's response when the prompt is injected, or (if the
|
|
679
|
+
* run ends first and the prompt becomes a queued follow-up) the follow-up
|
|
680
|
+
* run's response, so a late steer's answer is never lost.
|
|
681
|
+
*/
|
|
682
|
+
private steerInto(
|
|
683
|
+
active: NonNullable<Agent["currentRun"]>,
|
|
684
|
+
prompt: string | ContentPart[],
|
|
685
|
+
options?: AgentRunOptions
|
|
686
|
+
): Promise<AgentResponse> {
|
|
597
687
|
this.assertSteerRoom(active.steerInbox);
|
|
598
|
-
active.
|
|
688
|
+
const runDone = active.runPromise ?? active.outerStream?.result();
|
|
689
|
+
if (!runDone) return this.run(prompt, options) as Promise<AgentResponse>;
|
|
690
|
+
const entry: SteerEntry = { prompt, options };
|
|
691
|
+
const reply = new Promise<AgentResponse>((resolve, reject) => {
|
|
692
|
+
entry.settle = { resolve, reject };
|
|
693
|
+
});
|
|
694
|
+
reply.catch(() => {}); // a fire-and-forget caller must not trigger an unhandled rejection
|
|
695
|
+
active.steerInbox.push(entry);
|
|
599
696
|
this.persistNow();
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
697
|
+
runDone.then(
|
|
698
|
+
(res) => {
|
|
699
|
+
if (!entry.settle?.deferredToQueue) entry.settle?.resolve(res);
|
|
700
|
+
},
|
|
701
|
+
(err) => {
|
|
702
|
+
if (!entry.settle?.deferredToQueue) entry.settle?.reject(err);
|
|
703
|
+
}
|
|
704
|
+
);
|
|
705
|
+
return reply;
|
|
603
706
|
}
|
|
604
707
|
|
|
605
708
|
/**
|
|
@@ -706,15 +809,24 @@ export class Agent {
|
|
|
706
809
|
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
707
810
|
for (const entry of leftover) {
|
|
708
811
|
try {
|
|
709
|
-
if ((entry.options as { signal?: AbortSignal } | undefined)?.signal?.aborted)
|
|
812
|
+
if ((entry.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
|
|
813
|
+
entry.settle?.reject(Object.assign(new Error("Steer aborted before it could run"), { name: "AbortError" }));
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
710
816
|
} catch { continue; }
|
|
711
|
-
if (this.pendingQueue.length >= max)
|
|
817
|
+
if (this.pendingQueue.length >= max) {
|
|
818
|
+
entry.settle?.reject(new Error("[Agent Accelerator] Steer dropped: the follow-up queue is full."));
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
// The steer was never injected: it now runs as a follow-up, and the
|
|
822
|
+
// caller's promise resolves with that run's response.
|
|
823
|
+
if (entry.settle) entry.settle.deferredToQueue = true;
|
|
712
824
|
this.pendingQueue.push({
|
|
713
825
|
prompt: entry.prompt,
|
|
714
826
|
options: entry.options,
|
|
715
827
|
isStream: false,
|
|
716
|
-
resolve: () => {},
|
|
717
|
-
reject: () => {},
|
|
828
|
+
resolve: entry.settle?.resolve ?? (() => {}),
|
|
829
|
+
reject: entry.settle?.reject ?? (() => {}),
|
|
718
830
|
});
|
|
719
831
|
}
|
|
720
832
|
} catch {}
|
|
@@ -768,14 +880,116 @@ export class Agent {
|
|
|
768
880
|
return runController.signal;
|
|
769
881
|
}
|
|
770
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Config a spawned dynamic worker must inherit so it is never less
|
|
885
|
+
* constrained than its parent (run limits, retry policy, credentials/env,
|
|
886
|
+
* fetch, Google tier, file-access policy).
|
|
887
|
+
* @internal
|
|
888
|
+
*/
|
|
889
|
+
inheritableConfig(): Partial<AgentConfig> {
|
|
890
|
+
const d = this.callDefaults;
|
|
891
|
+
return {
|
|
892
|
+
limits: { ...this.runLimits },
|
|
893
|
+
...(d.retry ? { retry: d.retry } : {}),
|
|
894
|
+
...(d.env ? { env: d.env } : {}),
|
|
895
|
+
...(d.fetch ? { fetch: d.fetch } : {}),
|
|
896
|
+
...(this.googleTier ? { googleTier: this.googleTier } : {}),
|
|
897
|
+
...(this.fileAccess ? { fileAccess: this.fileAccess } : {}),
|
|
898
|
+
...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Isolated, stateless copy of this agent for ONE delegated call. An `Agent`
|
|
904
|
+
* runs one thing at a time (a second `stream()` while busy queues or steers
|
|
905
|
+
* into the first), so delegation tools fork per call: parallel calls to the
|
|
906
|
+
* same worker then truly run in parallel, each with its own context and
|
|
907
|
+
* session id, and never see each other's answers. The copy keeps this
|
|
908
|
+
* agent's model, instructions, tools and policies but not its persistence.
|
|
909
|
+
* @internal
|
|
910
|
+
*/
|
|
911
|
+
fork(sessionId: string, parentSessionId?: string): Agent {
|
|
912
|
+
// A fork is a short-lived worker copy: it must not schedule, nor replace the
|
|
913
|
+
// real agent in the scheduler's registry.
|
|
914
|
+
const dyn = this.forkConfig.dynamicSubagents;
|
|
915
|
+
const copy = new Agent({
|
|
916
|
+
...this.forkConfig,
|
|
917
|
+
sessionId,
|
|
918
|
+
stateless: true,
|
|
919
|
+
persist: undefined,
|
|
920
|
+
...(dyn?.allowScheduledActions ? { dynamicSubagents: { ...dyn, allowScheduledActions: false } } : {}),
|
|
921
|
+
});
|
|
922
|
+
if (parentSessionId) (copy as unknown as { parentSessionId?: string }).parentSessionId = parentSessionId;
|
|
923
|
+
return copy;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Budgets are per run: counters restart whenever a new run begins. */
|
|
927
|
+
private resetBudgetState(): void {
|
|
928
|
+
if (this.budget) this.budgetState = { spawned: 0, totalTokens: 0, totalCostUsd: 0, startedAtMs: nowMs() };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Loop hook: charge `usage` (when given) and throw `BudgetExceededError` once any cap is passed. */
|
|
932
|
+
private enforceBudget(usage?: TokenUsage): void {
|
|
933
|
+
if (!this.budget) return;
|
|
934
|
+
if (usage) chargeBudgetUsage(this as unknown as Parameters<typeof chargeBudgetUsage>[0], usage);
|
|
935
|
+
checkBudget(this.budget, this.budgetState);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** Folds the canonical per-run `thinking` option into `thinkingLevel` so every downstream reader agrees. */
|
|
939
|
+
private withResolvedThinking<T extends AgentRunOptions>(runOptions: T): T {
|
|
940
|
+
const level = normalizeThinkingV2(runOptions.thinking, runOptions.thinkingLevel);
|
|
941
|
+
return level ? { ...runOptions, thinkingLevel: level } : runOptions;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Single place that turns agent config + per-run options into the options
|
|
946
|
+
* every provider call receives. Per-run values override agent defaults
|
|
947
|
+
* (objects such as `retry` and `env` merge key by key).
|
|
948
|
+
*/
|
|
949
|
+
private buildProviderOptions(runOptions: AgentRunOptions | undefined): ProviderRequestOptions {
|
|
950
|
+
const d = this.callDefaults;
|
|
951
|
+
const sessionId = runOptions?.sessionId || this.sessionId;
|
|
952
|
+
const toolChoice = normalizeToolChoiceOption(runOptions?.toolChoice ?? d.toolChoice);
|
|
953
|
+
const retry = { ...d.retry, ...runOptions?.retry };
|
|
954
|
+
const env = { ...d.env, ...runOptions?.env };
|
|
955
|
+
const stop = normalizeStop(runOptions?.stop ?? d.stop);
|
|
956
|
+
const maxOutputTokens = runOptions?.maxOutputTokens ?? d.maxOutputTokens;
|
|
957
|
+
const seed = runOptions?.seed ?? d.seed;
|
|
958
|
+
const fetchImpl = runOptions?.fetch ?? d.fetch;
|
|
959
|
+
return {
|
|
960
|
+
apiKey: this.apiKey,
|
|
961
|
+
baseUrl: this.baseUrl,
|
|
962
|
+
headers: { ...(this.customHeaders ?? {}), ...(runOptions?.headers ?? {}) },
|
|
963
|
+
thinking: resolveEffectiveThinking(this.thinkingConfig, runOptions?.thinkingLevel),
|
|
964
|
+
cache: this.stateless
|
|
965
|
+
? { sessionId }
|
|
966
|
+
: { ...this.cacheConfig, sessionId },
|
|
967
|
+
serviceTier: this.serviceTier,
|
|
968
|
+
sessionId,
|
|
969
|
+
...(this.outputSpec ? { output: this.outputSpec } : {}),
|
|
970
|
+
...(this.googleTier ? { googleTier: this.googleTier } : {}),
|
|
971
|
+
...(toolChoice !== undefined ? { toolChoice } : {}),
|
|
972
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
973
|
+
...(stop ? { stop } : {}),
|
|
974
|
+
...(seed !== undefined ? { seed } : {}),
|
|
975
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
976
|
+
...(fetchImpl ? { fetch: fetchImpl } : {}),
|
|
977
|
+
...(retry.maxRetries !== undefined ? { maxRetries: retry.maxRetries } : {}),
|
|
978
|
+
...(retry.maxDelayMs !== undefined ? { maxRetryDelayMs: retry.maxDelayMs } : {}),
|
|
979
|
+
...(retry.baseDelayMs !== undefined ? { retryBaseDelayMs: retry.baseDelayMs } : {}),
|
|
980
|
+
...(retry.budgetMs !== undefined ? { retryBudgetMs: retry.budgetMs } : {}),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
771
984
|
private startNonStreamingRun(
|
|
772
985
|
prompt: string | ContentPart[],
|
|
773
986
|
options?: AgentRunOptions
|
|
774
987
|
): Promise<AgentResponse> {
|
|
988
|
+
this.resetBudgetState();
|
|
775
989
|
const runController = new AbortController();
|
|
776
990
|
const steerInbox: SteerEntry[] = [];
|
|
777
991
|
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
778
|
-
const runOptions = { ...options, signal: mergedSignal };
|
|
992
|
+
const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
|
|
779
993
|
// Q-19a: route to the named session context (default = this.context).
|
|
780
994
|
const targetContext = this.getSessionContext(runOptions?.sessionId || this.sessionId);
|
|
781
995
|
const task = (async (): Promise<AgentResponse> => {
|
|
@@ -788,17 +1002,7 @@ export class Agent {
|
|
|
788
1002
|
this.prepareTurn(prompt, runOptions, targetContext);
|
|
789
1003
|
this.persistNow();
|
|
790
1004
|
|
|
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
|
-
};
|
|
1005
|
+
const providerOptions = this.buildProviderOptions(runOptions);
|
|
802
1006
|
|
|
803
1007
|
const loopConfig = {
|
|
804
1008
|
agentName: this.name,
|
|
@@ -810,11 +1014,16 @@ export class Agent {
|
|
|
810
1014
|
runOptions,
|
|
811
1015
|
maxTurns: this.maxTurns,
|
|
812
1016
|
bypassInputFileModality: this.bypassInputFileModality,
|
|
1017
|
+
...(this.fileAccess ? { fileAccess: this.fileAccess } : {}),
|
|
1018
|
+
onFailure: this.onFailure,
|
|
1019
|
+
...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
|
|
1020
|
+
...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
|
|
813
1021
|
onProgress: () => this.persistNow(),
|
|
814
1022
|
steerInbox,
|
|
815
1023
|
onSteerInjected: () => this.persistNow(),
|
|
816
1024
|
// Q-22: loop enforces these (plus local timeout races).
|
|
817
1025
|
limits: { ...this.runLimits },
|
|
1026
|
+
...(this.output !== undefined ? { output: this.output } : {}),
|
|
818
1027
|
};
|
|
819
1028
|
|
|
820
1029
|
const res = await runAgentLoop(loopConfig);
|
|
@@ -841,7 +1050,7 @@ export class Agent {
|
|
|
841
1050
|
options?: AgentRunOptions
|
|
842
1051
|
): AssistantMessageEventStream {
|
|
843
1052
|
const resolved = resolveModel(this.modelStringOrSpec);
|
|
844
|
-
const effectiveLevel = options?.thinkingLevel || this.thinkingConfig?.level;
|
|
1053
|
+
const effectiveLevel = normalizeThinkingV2(options?.thinking, options?.thinkingLevel) || this.thinkingConfig?.level;
|
|
845
1054
|
if (effectiveLevel) {
|
|
846
1055
|
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
847
1056
|
}
|
|
@@ -850,22 +1059,13 @@ export class Agent {
|
|
|
850
1059
|
this.prepareTurn(prompt, options, targetContext);
|
|
851
1060
|
this.persistNow();
|
|
852
1061
|
|
|
1062
|
+
this.resetBudgetState();
|
|
853
1063
|
const runController = new AbortController();
|
|
854
1064
|
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
855
|
-
const runOptions = { ...options, signal: mergedSignal };
|
|
1065
|
+
const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
|
|
856
1066
|
const steerInbox: SteerEntry[] = [];
|
|
857
1067
|
|
|
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
|
-
};
|
|
1068
|
+
const providerOptions = this.buildProviderOptions(runOptions);
|
|
869
1069
|
|
|
870
1070
|
const loopConfig = {
|
|
871
1071
|
agentName: this.name,
|
|
@@ -877,11 +1077,16 @@ export class Agent {
|
|
|
877
1077
|
runOptions,
|
|
878
1078
|
maxTurns: this.maxTurns,
|
|
879
1079
|
bypassInputFileModality: this.bypassInputFileModality,
|
|
1080
|
+
...(this.fileAccess ? { fileAccess: this.fileAccess } : {}),
|
|
1081
|
+
onFailure: this.onFailure,
|
|
1082
|
+
...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
|
|
1083
|
+
...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
|
|
880
1084
|
onProgress: () => this.persistNow(),
|
|
881
1085
|
steerInbox,
|
|
882
1086
|
onSteerInjected: () => this.persistNow(),
|
|
883
1087
|
// Q-22: loop enforces these (plus local timeout races).
|
|
884
1088
|
limits: { ...this.runLimits },
|
|
1089
|
+
...(this.output !== undefined ? { output: this.output } : {}),
|
|
885
1090
|
};
|
|
886
1091
|
|
|
887
1092
|
const s = streamAgentLoop(loopConfig);
|
|
@@ -921,7 +1126,14 @@ export class Agent {
|
|
|
921
1126
|
const hit = own?.[trackingId];
|
|
922
1127
|
if (hit) return { ...hit, steps: hit.steps.map((s) => ({ ...s })) };
|
|
923
1128
|
} catch {}
|
|
924
|
-
|
|
1129
|
+
// Process-wide store, filtered to sessions THIS agent owns: another
|
|
1130
|
+
// agent's workers (e.g. a different tenant) are never reachable.
|
|
1131
|
+
return getSubAgentTraceForSessions(trackingId, this.ownedSessionIds());
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** Session ids whose spawned workers this agent may inspect (default + named sessions). */
|
|
1135
|
+
private ownedSessionIds(): ReadonlySet<string> {
|
|
1136
|
+
return new Set<string>([this.sessionId, ...this.sessions.keys()]);
|
|
925
1137
|
}
|
|
926
1138
|
|
|
927
1139
|
/** Lists TrackingIDs of worker traces visible to this agent (deduped). */
|
|
@@ -932,7 +1144,10 @@ export class Agent {
|
|
|
932
1144
|
};
|
|
933
1145
|
const seen = new Set<string>();
|
|
934
1146
|
const out: string[] = [];
|
|
935
|
-
for (const id of [
|
|
1147
|
+
for (const id of [
|
|
1148
|
+
...Object.keys((this as any).subagentTraces ?? {}),
|
|
1149
|
+
...listSubAgentTraceIdsForSessions(this.ownedSessionIds()),
|
|
1150
|
+
]) {
|
|
936
1151
|
const raw = rawOf(id);
|
|
937
1152
|
if (!seen.has(raw)) {
|
|
938
1153
|
seen.add(raw);
|
|
@@ -949,7 +1164,7 @@ export class Agent {
|
|
|
949
1164
|
* @example `const off = agent.subscribeToSubAgent(id, (t) => render(t));`
|
|
950
1165
|
*/
|
|
951
1166
|
subscribeToSubAgent(trackingId: string, fn: (trace: SubAgentTrace) => void): () => void {
|
|
952
|
-
return
|
|
1167
|
+
return subscribeToSubAgentForSessions(trackingId, this.ownedSessionIds(), fn);
|
|
953
1168
|
}
|
|
954
1169
|
|
|
955
1170
|
/**
|
|
@@ -1048,6 +1263,13 @@ export class Agent {
|
|
|
1048
1263
|
*/
|
|
1049
1264
|
importSession(data?: PersistedAgentSession | null): void {
|
|
1050
1265
|
if (!data) return;
|
|
1266
|
+
const badIndex = findInvalidMessage(data.messages ?? []);
|
|
1267
|
+
if (badIndex !== -1) {
|
|
1268
|
+
throw new SessionLoadError(
|
|
1269
|
+
`[Agent Accelerator] Cannot import session: messages[${badIndex}] is not a valid message (expected role system|user|assistant|tool and string or part-array content).`
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
this.warnOnLocalMediaPaths(data.messages ?? []);
|
|
1051
1273
|
const prevSid = this.sessionId;
|
|
1052
1274
|
if (data.sessionId) (this as any).sessionId = data.sessionId;
|
|
1053
1275
|
if ((data as any).parentSessionId) (this as any).parentSessionId = (data as any).parentSessionId;
|
|
@@ -1107,6 +1329,32 @@ export class Agent {
|
|
|
1107
1329
|
try { clearSessionRouting(this.sessionId); } catch {}
|
|
1108
1330
|
}
|
|
1109
1331
|
|
|
1332
|
+
/**
|
|
1333
|
+
* A loaded session may reference local files (`image: "/home/me/x.png"`)
|
|
1334
|
+
* that the next turn would read and upload. Without a `fileAccess.roots`
|
|
1335
|
+
* policy that is silent, so surface it once on the telemetry bus.
|
|
1336
|
+
*/
|
|
1337
|
+
private warnOnLocalMediaPaths(messages: ReadonlyArray<{ content?: unknown }>): void {
|
|
1338
|
+
if (this.fileAccess?.roots) return; // enforced every turn by the loop
|
|
1339
|
+
const paths: string[] = [];
|
|
1340
|
+
for (const m of messages) {
|
|
1341
|
+
if (!Array.isArray(m.content)) continue;
|
|
1342
|
+
for (const part of m.content as Array<Record<string, unknown>>) {
|
|
1343
|
+
const key = ["image", "audio", "video", "file"].find((k) => k === part?.type);
|
|
1344
|
+
const value = key ? part[key] : undefined;
|
|
1345
|
+
if (isLocalPathInput(value) && !value.startsWith("media/")) paths.push(value);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (paths.length === 0) return;
|
|
1349
|
+
try {
|
|
1350
|
+
telemetryBus.emitWarning(
|
|
1351
|
+
"session.local_media_paths",
|
|
1352
|
+
`[Agent Accelerator] Imported session references ${paths.length} local file path(s) that will be read and sent to the provider on the next turn. Set fileAccess.roots to restrict them.`,
|
|
1353
|
+
{ count: paths.length, examples: paths.slice(0, 3) }
|
|
1354
|
+
);
|
|
1355
|
+
} catch {}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1110
1358
|
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): void {
|
|
1111
1359
|
// Q-20: prompt validation throws synchronously in both run() and
|
|
1112
1360
|
// stream() (same timing; documented here).
|
|
@@ -1203,12 +1451,7 @@ export class Agent {
|
|
|
1203
1451
|
const mode = this.resolveEnqueueMode(requested);
|
|
1204
1452
|
if (mode === "steer") {
|
|
1205
1453
|
this.assertPromptValid(prompt);
|
|
1206
|
-
this.
|
|
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();
|
|
1454
|
+
return this.steerInto(this.currentRun, prompt, options);
|
|
1212
1455
|
} else {
|
|
1213
1456
|
if (this.midSessionConfig.mode === "steer") {
|
|
1214
1457
|
throw new Error(
|
|
@@ -1465,8 +1708,13 @@ export class Agent {
|
|
|
1465
1708
|
// <think> open tag then needs its own leading newline, otherwise it
|
|
1466
1709
|
// glues onto the streamed text mid-line. False at stream start and
|
|
1467
1710
|
// after every tag (both end with newlines), preserving the exact
|
|
1468
|
-
// "<think>\n...\n</think>\n
|
|
1711
|
+
// "<think>\n...\n</think>\n" contract for thinking-first streams.
|
|
1469
1712
|
let needThinkNewline = false;
|
|
1713
|
+
// Set on every think close; consumed by the next text_delta(s) to strip
|
|
1714
|
+
// model-leading blank lines display-only (cache-safe: outer events only).
|
|
1715
|
+
// Stays set until non-empty text is emitted so split "\n" | "\nText"
|
|
1716
|
+
// chunks cannot leak a blank line.
|
|
1717
|
+
let justClosedThink = false;
|
|
1470
1718
|
const collapseBlankLines = (s: string): string => s.replace(/\n{3,}/g, "\n\n");
|
|
1471
1719
|
const userOnThinking = options.onThinkingDelta;
|
|
1472
1720
|
const userOnDelta = options.onDelta;
|
|
@@ -1489,15 +1737,34 @@ export class Agent {
|
|
|
1489
1737
|
if (isThinking) {
|
|
1490
1738
|
isThinking = false;
|
|
1491
1739
|
// Drop any withheld trailing newlines: the span ends cleanly and
|
|
1492
|
-
// the tag supplies its own line
|
|
1740
|
+
// the tag supplies its own single line break. Empty spans still close.
|
|
1493
1741
|
pendingNewlines = "";
|
|
1494
|
-
const close = emittedAny ? "\n</think>\n
|
|
1742
|
+
const close = emittedAny ? "\n</think>\n" : "</think>\n";
|
|
1495
1743
|
emittedAny = false;
|
|
1744
|
+
justClosedThink = true;
|
|
1496
1745
|
if (userOnThinking) userOnThinking(close, e);
|
|
1497
1746
|
else if (userOnDelta) userOnDelta(close, e as any);
|
|
1498
1747
|
}
|
|
1499
1748
|
};
|
|
1500
1749
|
|
|
1750
|
+
/**
|
|
1751
|
+
* Strips model-leading blank lines after a think close, display-only.
|
|
1752
|
+
* Mutates `e.delta` in place so `for-await` consumers see the same clean
|
|
1753
|
+
* text; safe because loop context derives from the provider result, not
|
|
1754
|
+
* these outer events. Returns the display string (`""` when nothing
|
|
1755
|
+
* displayable yet — caller skips the callback but keeps the flag set so
|
|
1756
|
+
* a split newline-only chunk cannot leak a blank line).
|
|
1757
|
+
*/
|
|
1758
|
+
const stripLeadingAfterClose = (e: any): string => {
|
|
1759
|
+
const raw: string = e.delta ?? "";
|
|
1760
|
+
if (!justClosedThink) return raw;
|
|
1761
|
+
const stripped = raw.replace(/^\n+/, "");
|
|
1762
|
+
try { e.delta = stripped; } catch {}
|
|
1763
|
+
if (!stripped) return "";
|
|
1764
|
+
justClosedThink = false;
|
|
1765
|
+
return stripped;
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1501
1768
|
/** Buffers one raw thinking chunk, returning the display string to
|
|
1502
1769
|
* emit now (`""` when nothing displayable yet). Mutates the event in
|
|
1503
1770
|
* place so manual `for-await` consumers see the same clean text; safe
|
|
@@ -1535,9 +1802,14 @@ export class Agent {
|
|
|
1535
1802
|
});
|
|
1536
1803
|
s.on("text_delta", (e: any) => {
|
|
1537
1804
|
closeThink(e);
|
|
1538
|
-
|
|
1539
|
-
if (
|
|
1540
|
-
|
|
1805
|
+
const display = stripLeadingAfterClose(e);
|
|
1806
|
+
if (!display) {
|
|
1807
|
+
needThinkNewline = false;
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
needThinkNewline = !display.endsWith("\n");
|
|
1811
|
+
if (userOnDelta) userOnDelta(display, e);
|
|
1812
|
+
else if (userOnThinking) userOnThinking(display, e as any);
|
|
1541
1813
|
});
|
|
1542
1814
|
// Close thinking tag and reset turn state before tool/subagent execution or results
|
|
1543
1815
|
s.on("tool_call_start" as any, (e: any) => closeThink(e));
|
|
@@ -1555,8 +1827,9 @@ export class Agent {
|
|
|
1555
1827
|
if (isThinking) {
|
|
1556
1828
|
isThinking = false;
|
|
1557
1829
|
pendingNewlines = "";
|
|
1558
|
-
const tag = emittedAny ? "\n</think>\n
|
|
1830
|
+
const tag = emittedAny ? "\n</think>\n" : "</think>\n";
|
|
1559
1831
|
emittedAny = false;
|
|
1832
|
+
justClosedThink = true;
|
|
1560
1833
|
s.push({ type: "thinking_delta", thinkingDelta: tag, __synthetic: true } as any);
|
|
1561
1834
|
}
|
|
1562
1835
|
};
|
|
@@ -1581,6 +1854,12 @@ export class Agent {
|
|
|
1581
1854
|
});
|
|
1582
1855
|
s.on("text_delta", (e: any) => {
|
|
1583
1856
|
closeSynthetic();
|
|
1857
|
+
if (justClosedThink) {
|
|
1858
|
+
const stripped: string = (e.delta ?? "").replace(/^\n+/, "");
|
|
1859
|
+
try { e.delta = stripped; } catch {}
|
|
1860
|
+
if (!stripped) return;
|
|
1861
|
+
justClosedThink = false;
|
|
1862
|
+
}
|
|
1584
1863
|
needThinkNewline = !((e.delta ?? "").endsWith("\n"));
|
|
1585
1864
|
});
|
|
1586
1865
|
s.on("tool_call_start" as any, () => {
|
|
@@ -1611,7 +1890,16 @@ export class Agent {
|
|
|
1611
1890
|
/** Normalizes the public `thinkingLevel` flag into provider-neutral thinking settings. */
|
|
1612
1891
|
export function normalizeThinking(config: AgentConfig, fallbackLevel?: string): ThinkingConfig | undefined {
|
|
1613
1892
|
// DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh, max
|
|
1614
|
-
|
|
1893
|
+
// `thinking` (canonical) wins over legacy `thinkingLevel`; `{ level }` objects are unwrapped.
|
|
1894
|
+
const rawLevel = normalizeThinkingV2(config.thinking, config.thinkingLevel) ?? fallbackLevel;
|
|
1895
|
+
if (typeof config.thinking === "object" && config.thinking?.budgetTokens !== undefined) {
|
|
1896
|
+
// No provider maps an explicit token budget yet: say so instead of ignoring it silently.
|
|
1897
|
+
telemetryBus.emitWarning(
|
|
1898
|
+
"config.thinking_budget_ignored",
|
|
1899
|
+
"[Agent Accelerator] thinking.budgetTokens is not forwarded by any provider adapter yet; the level alone is used.",
|
|
1900
|
+
{ budgetTokens: config.thinking.budgetTokens }
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1615
1903
|
const level = rawLevel as ThinkingLevel | undefined;
|
|
1616
1904
|
|
|
1617
1905
|
if (!level) return undefined;
|
|
@@ -1639,3 +1927,11 @@ export function normalizeThinking(config: AgentConfig, fallbackLevel?: string):
|
|
|
1639
1927
|
}
|
|
1640
1928
|
return { enabled: true, level };
|
|
1641
1929
|
}
|
|
1930
|
+
|
|
1931
|
+
/** Maps the public `toolChoice` option onto the provider wire-neutral form. */
|
|
1932
|
+
function normalizeToolChoiceOption(choice: ToolChoiceOption | undefined): ProviderRequestOptions["toolChoice"] {
|
|
1933
|
+
if (!choice) return undefined;
|
|
1934
|
+
if (typeof choice === "string") return choice;
|
|
1935
|
+
if (choice.tool) return { type: "function", function: { name: choice.tool } };
|
|
1936
|
+
return choice.mode;
|
|
1937
|
+
}
|