agent-accelerator 0.4.5 → 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 +401 -120
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +253 -98
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +39 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +21 -2
- package/src/providers/google.ts +282 -74
- package/src/providers/openai-compat.ts +61 -41
- package/src/providers/openai.ts +46 -31
- package/src/providers/openrouter.ts +42 -31
- package/src/providers/shared.ts +110 -16
- package/src/providers.ts +119 -6
- 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 +211 -104
- 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 +57 -20
- package/src/types/model.ts +18 -0
- package/src/types/response.ts +2 -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/errors.ts +11 -0
- 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,15 +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";
|
|
16
38
|
import type { StructuredOutputSpec } from "../types/model.ts";
|
|
17
39
|
import { clearInteractionChains } from "../providers/google.ts";
|
|
18
|
-
import { getModel, getSubModel } from "../utils/env.ts";
|
|
19
40
|
import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalog.ts";
|
|
20
41
|
import { buildSessionData, type PersistedAgentSession, type SessionTotals,} from "../session/store.ts";
|
|
21
42
|
import { SessionTelemetry } from "../session/store.ts";
|
|
@@ -33,7 +54,7 @@ export function resolveEffectiveThinking(
|
|
|
33
54
|
return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
|
|
34
55
|
}
|
|
35
56
|
|
|
36
|
-
import { ConfigError } from "../types/errors.ts";
|
|
57
|
+
import { ConfigError, SessionLoadError } from "../types/errors.ts";
|
|
37
58
|
|
|
38
59
|
/**
|
|
39
60
|
* Disposers for the manual user-signal forward listeners (Q-21 fallback when
|
|
@@ -52,6 +73,7 @@ export interface AgentRunLimits {
|
|
|
52
73
|
maxTurns?: number;
|
|
53
74
|
deadlineMs?: number;
|
|
54
75
|
maxToolCallsPerRun?: number;
|
|
76
|
+
maxTotalTokens?: number;
|
|
55
77
|
requestTimeoutMs: number;
|
|
56
78
|
firstByteTimeoutMs: number;
|
|
57
79
|
streamIdleTimeoutMs: number;
|
|
@@ -68,6 +90,7 @@ function resolveAgentLimits(config: { limits?: Record<string, unknown> }): Agent
|
|
|
68
90
|
...(num(raw.maxToolCallsPerRun) !== undefined
|
|
69
91
|
? { maxToolCallsPerRun: Math.max(1, Math.floor(num(raw.maxToolCallsPerRun)!)) }
|
|
70
92
|
: {}),
|
|
93
|
+
...(num(raw.maxTotalTokens) !== undefined ? { maxTotalTokens: Math.max(1, Math.floor(num(raw.maxTotalTokens)!)) } : {}),
|
|
71
94
|
requestTimeoutMs: num(raw.requestTimeoutMs) ?? 300_000,
|
|
72
95
|
firstByteTimeoutMs: num(raw.firstByteTimeoutMs) ?? 120_000,
|
|
73
96
|
streamIdleTimeoutMs: num(raw.streamIdleTimeoutMs) ?? 300_000,
|
|
@@ -135,7 +158,11 @@ export class Agent {
|
|
|
135
158
|
thinkingLevel?: ThinkingLevel;
|
|
136
159
|
tools: Record<string, ToolDefinition>;
|
|
137
160
|
timeout: number;
|
|
161
|
+
/** Scheduled actions (`scheduled_actions` + `get_datetime` tools) are on. */
|
|
162
|
+
allowScheduledActions: boolean;
|
|
138
163
|
};
|
|
164
|
+
/** Home folder for everything written to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). */
|
|
165
|
+
readonly home: string;
|
|
139
166
|
/** Registered model-callable tools. */
|
|
140
167
|
readonly tools: Record<string, ToolDefinition> = {};
|
|
141
168
|
/** Normalized reasoning configuration. */
|
|
@@ -150,6 +177,35 @@ export class Agent {
|
|
|
150
177
|
readonly serviceTier?: ServiceTier;
|
|
151
178
|
/** Convert `file` parts client-side for pdf-incapable models. */
|
|
152
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
|
+
};
|
|
153
209
|
/** Maximum model/tool turns per run. */
|
|
154
210
|
readonly maxTurns: number;
|
|
155
211
|
/** Session ID used for history and cache affinity. */
|
|
@@ -220,53 +276,29 @@ export class Agent {
|
|
|
220
276
|
this.description = config.description || "AI Agent powered by Agent Accelerator";
|
|
221
277
|
// DX1: only instructions (no systemPrompt alias)
|
|
222
278
|
this.instructions = config.instructions || "";
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
} else if (rawModel && typeof rawModel === "object" && "id" in rawModel && "provider" in rawModel) {
|
|
228
|
-
this.modelStringOrSpec = rawModel;
|
|
229
|
-
} else {
|
|
230
|
-
this.modelStringOrSpec = rawModel || getModel();
|
|
231
|
-
}
|
|
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;
|
|
232
283
|
const dynRaw = config.dynamicSubagents;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
dynModel = (dynModelRaw as any).model;
|
|
237
|
-
} else if (dynModelRaw && typeof dynModelRaw === "object" && "id" in dynModelRaw && "provider" in dynModelRaw) {
|
|
238
|
-
dynModel = dynModelRaw;
|
|
239
|
-
} else {
|
|
240
|
-
dynModel = (dynModelRaw as any) || getSubModel();
|
|
241
|
-
}
|
|
242
|
-
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;
|
|
243
287
|
const dynEnabled = dynRaw ? (dynRaw.enabled ?? true) : false;
|
|
244
288
|
if (dynRaw) {
|
|
245
|
-
const
|
|
246
|
-
const dynTools: Record<string, ToolDefinition> = {};
|
|
247
|
-
if (rawTools) {
|
|
248
|
-
if (Array.isArray(rawTools)) {
|
|
249
|
-
for (const t of rawTools) {
|
|
250
|
-
const tName = (t as any)?.name || `tool_${Object.keys(dynTools).length}`;
|
|
251
|
-
dynTools[tName] = { ...(t as any), name: tName };
|
|
252
|
-
}
|
|
253
|
-
} else {
|
|
254
|
-
for (const [key, def] of Object.entries(rawTools as Record<string, ToolDefinition>)) {
|
|
255
|
-
dynTools[key] = { ...(def as any), name: (def as any)?.name || key };
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
289
|
+
const dynTools = normalizeToolMap((dynRaw as { tools?: Record<string, ToolDefinition> | ToolDefinition[] }).tools);
|
|
259
290
|
const rawMax = (dynRaw as any)?.maxSpawn;
|
|
260
291
|
const maxSpawn = Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax as number)) : 4;
|
|
261
|
-
|
|
262
|
-
const timeout =
|
|
292
|
+
// `workerTimeoutMs ?? timeout`, or -1 when `modelMaySetTimeout` (see normalizeWorkerTimeout).
|
|
293
|
+
const timeout = normalizeWorkerTimeout(dynRaw as { timeout?: number; workerTimeoutMs?: number; modelMaySetTimeout?: boolean });
|
|
263
294
|
this.dynamicSubagents = {
|
|
264
295
|
enabled: dynEnabled,
|
|
265
|
-
model: dynModel,
|
|
296
|
+
model: dynModel as string | any | undefined,
|
|
266
297
|
maxSpawn,
|
|
267
298
|
thinkingLevel: (dynRaw as any)?.thinkingLevel,
|
|
268
299
|
tools: dynTools,
|
|
269
300
|
timeout,
|
|
301
|
+
allowScheduledActions: dynRaw.allowScheduledActions === true,
|
|
270
302
|
};
|
|
271
303
|
}
|
|
272
304
|
this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
|
|
@@ -280,9 +312,14 @@ export class Agent {
|
|
|
280
312
|
: Infinity;
|
|
281
313
|
this.sessionId = config.sessionId || config.cache?.sessionId || createSessionId();
|
|
282
314
|
(this as any).subagentTraces = {};
|
|
283
|
-
this.
|
|
284
|
-
|
|
285
|
-
|
|
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);
|
|
286
323
|
const rawMaxQueued = (config.midSession as any)?.maxQueued;
|
|
287
324
|
const rawMaxSteers = (config.midSession as any)?.maxSteers;
|
|
288
325
|
this.midSessionConfig = {
|
|
@@ -319,23 +356,40 @@ export class Agent {
|
|
|
319
356
|
|
|
320
357
|
this.serviceTier = config.serviceTier;
|
|
321
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;
|
|
322
388
|
this.stateless = config.stateless ?? false;
|
|
323
389
|
|
|
324
|
-
// Tools registration
|
|
390
|
+
// Tools registration (unified normalization shared with dynamic pool).
|
|
325
391
|
if (config.tools) {
|
|
326
|
-
|
|
327
|
-
for (const t of config.tools) {
|
|
328
|
-
const tName = t.name || `tool_${Object.keys(this.tools).length}`;
|
|
329
|
-
this.tools[tName] = { ...t, name: tName };
|
|
330
|
-
}
|
|
331
|
-
} else {
|
|
332
|
-
for (const [key, def] of Object.entries(config.tools)) {
|
|
333
|
-
this.tools[key] = {
|
|
334
|
-
...def,
|
|
335
|
-
name: def.name || key,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
}
|
|
392
|
+
Object.assign(this.tools, normalizeToolMap(config.tools));
|
|
339
393
|
}
|
|
340
394
|
|
|
341
395
|
|
|
@@ -347,23 +401,34 @@ export class Agent {
|
|
|
347
401
|
}
|
|
348
402
|
|
|
349
403
|
if (this.bypassInputFileModality && !this.tools["convert_document_to_markdown"]) {
|
|
350
|
-
this.tools["convert_document_to_markdown"] =
|
|
404
|
+
this.tools["convert_document_to_markdown"] = createConvertDocumentTool({ roots: this.fileAccess?.roots });
|
|
351
405
|
}
|
|
352
406
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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) {
|
|
359
414
|
const spawnTool = createSubagentSpawnTool(this);
|
|
360
415
|
this.tools[spawnTool.name || "spawn_subagents"] = spawnTool;
|
|
361
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);
|
|
362
426
|
|
|
363
427
|
this.context = new AgentContext(this.getFullInstructions());
|
|
364
428
|
if (initialCachedId) {
|
|
365
429
|
this.context.cachedContentId = initialCachedId;
|
|
366
430
|
}
|
|
431
|
+
if (dyn?.allowScheduledActions) registerAgent(this);
|
|
367
432
|
}
|
|
368
433
|
|
|
369
434
|
private getFullInstructions(): string | undefined {
|
|
@@ -605,12 +670,39 @@ export class Agent {
|
|
|
605
670
|
}
|
|
606
671
|
const active = this.currentRun;
|
|
607
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> {
|
|
608
687
|
this.assertSteerRoom(active.steerInbox);
|
|
609
|
-
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);
|
|
610
696
|
this.persistNow();
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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;
|
|
614
706
|
}
|
|
615
707
|
|
|
616
708
|
/**
|
|
@@ -717,15 +809,24 @@ export class Agent {
|
|
|
717
809
|
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
718
810
|
for (const entry of leftover) {
|
|
719
811
|
try {
|
|
720
|
-
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
|
+
}
|
|
721
816
|
} catch { continue; }
|
|
722
|
-
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;
|
|
723
824
|
this.pendingQueue.push({
|
|
724
825
|
prompt: entry.prompt,
|
|
725
826
|
options: entry.options,
|
|
726
827
|
isStream: false,
|
|
727
|
-
resolve: () => {},
|
|
728
|
-
reject: () => {},
|
|
828
|
+
resolve: entry.settle?.resolve ?? (() => {}),
|
|
829
|
+
reject: entry.settle?.reject ?? (() => {}),
|
|
729
830
|
});
|
|
730
831
|
}
|
|
731
832
|
} catch {}
|
|
@@ -779,14 +880,116 @@ export class Agent {
|
|
|
779
880
|
return runController.signal;
|
|
780
881
|
}
|
|
781
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
|
+
|
|
782
984
|
private startNonStreamingRun(
|
|
783
985
|
prompt: string | ContentPart[],
|
|
784
986
|
options?: AgentRunOptions
|
|
785
987
|
): Promise<AgentResponse> {
|
|
988
|
+
this.resetBudgetState();
|
|
786
989
|
const runController = new AbortController();
|
|
787
990
|
const steerInbox: SteerEntry[] = [];
|
|
788
991
|
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
789
|
-
const runOptions = { ...options, signal: mergedSignal };
|
|
992
|
+
const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
|
|
790
993
|
// Q-19a: route to the named session context (default = this.context).
|
|
791
994
|
const targetContext = this.getSessionContext(runOptions?.sessionId || this.sessionId);
|
|
792
995
|
const task = (async (): Promise<AgentResponse> => {
|
|
@@ -799,18 +1002,7 @@ export class Agent {
|
|
|
799
1002
|
this.prepareTurn(prompt, runOptions, targetContext);
|
|
800
1003
|
this.persistNow();
|
|
801
1004
|
|
|
802
|
-
const providerOptions =
|
|
803
|
-
apiKey: this.apiKey,
|
|
804
|
-
baseUrl: this.baseUrl,
|
|
805
|
-
headers: { ...(this.customHeaders ?? {}), ...(runOptions?.headers ?? {}) },
|
|
806
|
-
thinking: resolveEffectiveThinking(this.thinkingConfig, runOptions?.thinkingLevel),
|
|
807
|
-
cache: this.stateless
|
|
808
|
-
? { sessionId: runOptions?.sessionId || this.sessionId }
|
|
809
|
-
: { ...this.cacheConfig, sessionId: runOptions?.sessionId || this.sessionId },
|
|
810
|
-
serviceTier: this.serviceTier,
|
|
811
|
-
sessionId: runOptions?.sessionId || this.sessionId,
|
|
812
|
-
...(this.outputSpec ? { output: this.outputSpec } : {}),
|
|
813
|
-
};
|
|
1005
|
+
const providerOptions = this.buildProviderOptions(runOptions);
|
|
814
1006
|
|
|
815
1007
|
const loopConfig = {
|
|
816
1008
|
agentName: this.name,
|
|
@@ -822,6 +1014,10 @@ export class Agent {
|
|
|
822
1014
|
runOptions,
|
|
823
1015
|
maxTurns: this.maxTurns,
|
|
824
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) } : {}),
|
|
825
1021
|
onProgress: () => this.persistNow(),
|
|
826
1022
|
steerInbox,
|
|
827
1023
|
onSteerInjected: () => this.persistNow(),
|
|
@@ -854,7 +1050,7 @@ export class Agent {
|
|
|
854
1050
|
options?: AgentRunOptions
|
|
855
1051
|
): AssistantMessageEventStream {
|
|
856
1052
|
const resolved = resolveModel(this.modelStringOrSpec);
|
|
857
|
-
const effectiveLevel = options?.thinkingLevel || this.thinkingConfig?.level;
|
|
1053
|
+
const effectiveLevel = normalizeThinkingV2(options?.thinking, options?.thinkingLevel) || this.thinkingConfig?.level;
|
|
858
1054
|
if (effectiveLevel) {
|
|
859
1055
|
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
860
1056
|
}
|
|
@@ -863,23 +1059,13 @@ export class Agent {
|
|
|
863
1059
|
this.prepareTurn(prompt, options, targetContext);
|
|
864
1060
|
this.persistNow();
|
|
865
1061
|
|
|
1062
|
+
this.resetBudgetState();
|
|
866
1063
|
const runController = new AbortController();
|
|
867
1064
|
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
868
|
-
const runOptions = { ...options, signal: mergedSignal };
|
|
1065
|
+
const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
|
|
869
1066
|
const steerInbox: SteerEntry[] = [];
|
|
870
1067
|
|
|
871
|
-
const providerOptions =
|
|
872
|
-
apiKey: this.apiKey,
|
|
873
|
-
baseUrl: this.baseUrl,
|
|
874
|
-
headers: { ...(this.customHeaders ?? {}), ...(runOptions?.headers ?? {}) },
|
|
875
|
-
thinking: resolveEffectiveThinking(this.thinkingConfig, runOptions?.thinkingLevel),
|
|
876
|
-
cache: this.stateless
|
|
877
|
-
? { sessionId: runOptions?.sessionId || this.sessionId }
|
|
878
|
-
: { ...this.cacheConfig, sessionId: runOptions?.sessionId || this.sessionId },
|
|
879
|
-
serviceTier: this.serviceTier,
|
|
880
|
-
sessionId: runOptions?.sessionId || this.sessionId,
|
|
881
|
-
...(this.outputSpec ? { output: this.outputSpec } : {}),
|
|
882
|
-
};
|
|
1068
|
+
const providerOptions = this.buildProviderOptions(runOptions);
|
|
883
1069
|
|
|
884
1070
|
const loopConfig = {
|
|
885
1071
|
agentName: this.name,
|
|
@@ -891,6 +1077,10 @@ export class Agent {
|
|
|
891
1077
|
runOptions,
|
|
892
1078
|
maxTurns: this.maxTurns,
|
|
893
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) } : {}),
|
|
894
1084
|
onProgress: () => this.persistNow(),
|
|
895
1085
|
steerInbox,
|
|
896
1086
|
onSteerInjected: () => this.persistNow(),
|
|
@@ -936,7 +1126,14 @@ export class Agent {
|
|
|
936
1126
|
const hit = own?.[trackingId];
|
|
937
1127
|
if (hit) return { ...hit, steps: hit.steps.map((s) => ({ ...s })) };
|
|
938
1128
|
} catch {}
|
|
939
|
-
|
|
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()]);
|
|
940
1137
|
}
|
|
941
1138
|
|
|
942
1139
|
/** Lists TrackingIDs of worker traces visible to this agent (deduped). */
|
|
@@ -947,7 +1144,10 @@ export class Agent {
|
|
|
947
1144
|
};
|
|
948
1145
|
const seen = new Set<string>();
|
|
949
1146
|
const out: string[] = [];
|
|
950
|
-
for (const id of [
|
|
1147
|
+
for (const id of [
|
|
1148
|
+
...Object.keys((this as any).subagentTraces ?? {}),
|
|
1149
|
+
...listSubAgentTraceIdsForSessions(this.ownedSessionIds()),
|
|
1150
|
+
]) {
|
|
951
1151
|
const raw = rawOf(id);
|
|
952
1152
|
if (!seen.has(raw)) {
|
|
953
1153
|
seen.add(raw);
|
|
@@ -964,7 +1164,7 @@ export class Agent {
|
|
|
964
1164
|
* @example `const off = agent.subscribeToSubAgent(id, (t) => render(t));`
|
|
965
1165
|
*/
|
|
966
1166
|
subscribeToSubAgent(trackingId: string, fn: (trace: SubAgentTrace) => void): () => void {
|
|
967
|
-
return
|
|
1167
|
+
return subscribeToSubAgentForSessions(trackingId, this.ownedSessionIds(), fn);
|
|
968
1168
|
}
|
|
969
1169
|
|
|
970
1170
|
/**
|
|
@@ -1063,6 +1263,13 @@ export class Agent {
|
|
|
1063
1263
|
*/
|
|
1064
1264
|
importSession(data?: PersistedAgentSession | null): void {
|
|
1065
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 ?? []);
|
|
1066
1273
|
const prevSid = this.sessionId;
|
|
1067
1274
|
if (data.sessionId) (this as any).sessionId = data.sessionId;
|
|
1068
1275
|
if ((data as any).parentSessionId) (this as any).parentSessionId = (data as any).parentSessionId;
|
|
@@ -1122,6 +1329,32 @@ export class Agent {
|
|
|
1122
1329
|
try { clearSessionRouting(this.sessionId); } catch {}
|
|
1123
1330
|
}
|
|
1124
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
|
+
|
|
1125
1358
|
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): void {
|
|
1126
1359
|
// Q-20: prompt validation throws synchronously in both run() and
|
|
1127
1360
|
// stream() (same timing; documented here).
|
|
@@ -1218,12 +1451,7 @@ export class Agent {
|
|
|
1218
1451
|
const mode = this.resolveEnqueueMode(requested);
|
|
1219
1452
|
if (mode === "steer") {
|
|
1220
1453
|
this.assertPromptValid(prompt);
|
|
1221
|
-
this.
|
|
1222
|
-
this.currentRun.steerInbox.push({ prompt, options });
|
|
1223
|
-
this.persistNow();
|
|
1224
|
-
const active = this.currentRun;
|
|
1225
|
-
if (active.runPromise) return active.runPromise;
|
|
1226
|
-
if (active.outerStream) return active.outerStream.result();
|
|
1454
|
+
return this.steerInto(this.currentRun, prompt, options);
|
|
1227
1455
|
} else {
|
|
1228
1456
|
if (this.midSessionConfig.mode === "steer") {
|
|
1229
1457
|
throw new Error(
|
|
@@ -1480,8 +1708,13 @@ export class Agent {
|
|
|
1480
1708
|
// <think> open tag then needs its own leading newline, otherwise it
|
|
1481
1709
|
// glues onto the streamed text mid-line. False at stream start and
|
|
1482
1710
|
// after every tag (both end with newlines), preserving the exact
|
|
1483
|
-
// "<think>\n...\n</think>\n
|
|
1711
|
+
// "<think>\n...\n</think>\n" contract for thinking-first streams.
|
|
1484
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;
|
|
1485
1718
|
const collapseBlankLines = (s: string): string => s.replace(/\n{3,}/g, "\n\n");
|
|
1486
1719
|
const userOnThinking = options.onThinkingDelta;
|
|
1487
1720
|
const userOnDelta = options.onDelta;
|
|
@@ -1504,15 +1737,34 @@ export class Agent {
|
|
|
1504
1737
|
if (isThinking) {
|
|
1505
1738
|
isThinking = false;
|
|
1506
1739
|
// Drop any withheld trailing newlines: the span ends cleanly and
|
|
1507
|
-
// the tag supplies its own line
|
|
1740
|
+
// the tag supplies its own single line break. Empty spans still close.
|
|
1508
1741
|
pendingNewlines = "";
|
|
1509
|
-
const close = emittedAny ? "\n</think>\n
|
|
1742
|
+
const close = emittedAny ? "\n</think>\n" : "</think>\n";
|
|
1510
1743
|
emittedAny = false;
|
|
1744
|
+
justClosedThink = true;
|
|
1511
1745
|
if (userOnThinking) userOnThinking(close, e);
|
|
1512
1746
|
else if (userOnDelta) userOnDelta(close, e as any);
|
|
1513
1747
|
}
|
|
1514
1748
|
};
|
|
1515
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
|
+
|
|
1516
1768
|
/** Buffers one raw thinking chunk, returning the display string to
|
|
1517
1769
|
* emit now (`""` when nothing displayable yet). Mutates the event in
|
|
1518
1770
|
* place so manual `for-await` consumers see the same clean text; safe
|
|
@@ -1550,9 +1802,14 @@ export class Agent {
|
|
|
1550
1802
|
});
|
|
1551
1803
|
s.on("text_delta", (e: any) => {
|
|
1552
1804
|
closeThink(e);
|
|
1553
|
-
|
|
1554
|
-
if (
|
|
1555
|
-
|
|
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);
|
|
1556
1813
|
});
|
|
1557
1814
|
// Close thinking tag and reset turn state before tool/subagent execution or results
|
|
1558
1815
|
s.on("tool_call_start" as any, (e: any) => closeThink(e));
|
|
@@ -1570,8 +1827,9 @@ export class Agent {
|
|
|
1570
1827
|
if (isThinking) {
|
|
1571
1828
|
isThinking = false;
|
|
1572
1829
|
pendingNewlines = "";
|
|
1573
|
-
const tag = emittedAny ? "\n</think>\n
|
|
1830
|
+
const tag = emittedAny ? "\n</think>\n" : "</think>\n";
|
|
1574
1831
|
emittedAny = false;
|
|
1832
|
+
justClosedThink = true;
|
|
1575
1833
|
s.push({ type: "thinking_delta", thinkingDelta: tag, __synthetic: true } as any);
|
|
1576
1834
|
}
|
|
1577
1835
|
};
|
|
@@ -1596,6 +1854,12 @@ export class Agent {
|
|
|
1596
1854
|
});
|
|
1597
1855
|
s.on("text_delta", (e: any) => {
|
|
1598
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
|
+
}
|
|
1599
1863
|
needThinkNewline = !((e.delta ?? "").endsWith("\n"));
|
|
1600
1864
|
});
|
|
1601
1865
|
s.on("tool_call_start" as any, () => {
|
|
@@ -1626,7 +1890,16 @@ export class Agent {
|
|
|
1626
1890
|
/** Normalizes the public `thinkingLevel` flag into provider-neutral thinking settings. */
|
|
1627
1891
|
export function normalizeThinking(config: AgentConfig, fallbackLevel?: string): ThinkingConfig | undefined {
|
|
1628
1892
|
// DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh, max
|
|
1629
|
-
|
|
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
|
+
}
|
|
1630
1903
|
const level = rawLevel as ThinkingLevel | undefined;
|
|
1631
1904
|
|
|
1632
1905
|
if (!level) return undefined;
|
|
@@ -1654,3 +1927,11 @@ export function normalizeThinking(config: AgentConfig, fallbackLevel?: string):
|
|
|
1654
1927
|
}
|
|
1655
1928
|
return { enabled: true, level };
|
|
1656
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
|
+
}
|