agent-accelerator 0.1.0 → 0.3.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 +174 -123
- package/{SYSTEM_PROMPT_AGENT.md → examples/prompts/SYSTEM_PROMPT_AGENT.md} +1 -1
- package/package.json +14 -9
- package/src/agent/agent.ts +385 -73
- package/src/agent/context.ts +1 -0
- package/src/agent/delegation.ts +443 -30
- package/src/agent/loop.ts +183 -53
- package/src/data/README.md +6 -6
- package/src/index.ts +163 -46
- package/src/models/catalog-cache.ts +60 -9
- package/src/models/catalog.ts +55 -8
- package/src/providers/google.ts +926 -0
- package/src/providers/openai-compat.ts +1147 -0
- package/src/providers/openai.ts +959 -0
- package/src/providers/openrouter-responses.ts +949 -0
- package/src/providers/openrouter.ts +1037 -0
- package/src/{ai-sdk → providers}/registry.ts +43 -55
- package/src/providers.ts +492 -0
- package/src/session/store.ts +806 -0
- package/src/streaming/sse-parser.ts +6 -4
- package/src/tools/executor.ts +23 -7
- package/src/tools/schema.ts +21 -11
- package/src/types/agent.ts +33 -2
- package/src/types/core.ts +3 -2
- package/src/types/message.ts +5 -0
- package/src/types/model.ts +3 -7
- package/src/types/provider-payloads.ts +2 -84
- package/src/types/response.ts +24 -0
- package/src/types/tool.ts +8 -0
- package/src/update-models.ts +56 -0
- package/src/utils/cache.ts +1 -1
- package/src/utils/documents.ts +517 -0
- package/src/utils/env.ts +0 -7
- package/src/{ai-sdk → utils}/errors.ts +61 -2
- package/src/utils/headers.ts +10 -20
- package/src/utils/media.ts +5 -2
- package/src/utils/retry.ts +89 -0
- package/src/utils/serialization.ts +15 -0
- package/src/utils/session.ts +36 -0
- package/src/ai-sdk/converters.ts +0 -342
- package/src/ai-sdk/executor.ts +0 -454
- package/src/ai-sdk/index.ts +0 -55
- package/src/ai-sdk/model-provider.ts +0 -303
- package/src/ai-sdk/options.ts +0 -306
- package/src/ai-sdk/provider.ts +0 -415
- package/src/tokens/counter.ts +0 -136
- /package/{SYSTEM_PROMPT.md → examples/prompts/SYSTEM_PROMPT.md} +0 -0
- /package/{SYSTEM_PROMPT_TOOLS.md → examples/prompts/SYSTEM_PROMPT_TOOLS.md} +0 -0
package/src/agent/agent.ts
CHANGED
|
@@ -5,13 +5,30 @@ import type { ContentPart } from "../types/message.ts";
|
|
|
5
5
|
import { AgentResponse, type StreamEvent } from "../types/response.ts";
|
|
6
6
|
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
7
7
|
import { AgentContext } from "./context.ts";
|
|
8
|
-
import { resolveModel } from "../
|
|
8
|
+
import { resolveModel } from "../providers/registry.ts";
|
|
9
9
|
import { buildAgentTools, createSubagentSpawnTool } from "./delegation.ts";
|
|
10
|
+
import { convert_document_to_markdown } from "../utils/documents.ts";
|
|
10
11
|
import { runAgentLoop, streamAgentLoop } from "./loop.ts";
|
|
11
12
|
import { createSessionId } from "../utils/session.ts";
|
|
13
|
+
import { getSubAgentTrace, subscribeToSubAgent, listSubAgentTraceIds, type SubAgentTrace } from "./delegation.ts";
|
|
14
|
+
import { saveSessionDir, saveSessionFile } from "../session/store.ts";
|
|
12
15
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
13
|
-
import { validateModelThinking } from "../models/catalog.ts";
|
|
14
|
-
import {
|
|
16
|
+
import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalog.ts";
|
|
17
|
+
import { buildSessionData, type PersistedAgentSession, type SessionTotals,} from "../session/store.ts";
|
|
18
|
+
import { SessionTelemetry } from "../session/store.ts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a per-run thinking override without mutating agent config.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveEffectiveThinking(
|
|
24
|
+
base?: ThinkingConfig,
|
|
25
|
+
overrideLevel?: string
|
|
26
|
+
): ThinkingConfig | undefined {
|
|
27
|
+
if (!overrideLevel) return base;
|
|
28
|
+
if (overrideLevel === "none") return { enabled: false, level: "none", budgetTokens: 0 };
|
|
29
|
+
if (overrideLevel === "dynamic") return { enabled: true, level: "dynamic", budgetTokens: -1 };
|
|
30
|
+
return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
/** Thrown when dynamic sub-agent spawning is enabled without an explicit model. */
|
|
17
34
|
export class SubAgentModelError extends Error {
|
|
@@ -91,6 +108,8 @@ export class Agent {
|
|
|
91
108
|
readonly cacheConfig?: CacheConfig;
|
|
92
109
|
/** Provider service tier. */
|
|
93
110
|
readonly serviceTier?: ServiceTier;
|
|
111
|
+
/** Convert `file` parts client-side for pdf-incapable models. */
|
|
112
|
+
readonly bypassInputFileModality: boolean;
|
|
94
113
|
/** Maximum model/tool turns per run. */
|
|
95
114
|
readonly maxTurns: number;
|
|
96
115
|
/** Session ID used for history and cache affinity. */
|
|
@@ -105,6 +124,8 @@ export class Agent {
|
|
|
105
124
|
readonly context: AgentContext;
|
|
106
125
|
/** Whether history is cleared around each run. */
|
|
107
126
|
readonly stateless: boolean;
|
|
127
|
+
/** Durable persistence target: session file rewritten after every step. */
|
|
128
|
+
readonly persistConfig?: { dir?: string; file?: string };
|
|
108
129
|
|
|
109
130
|
/**
|
|
110
131
|
* Creates an agent and registers its model, tools, cache, and delegation settings.
|
|
@@ -182,8 +203,15 @@ export class Agent {
|
|
|
182
203
|
this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
|
|
183
204
|
this.baseUrl = (rawModel as any)?.baseUrl || config.baseUrl;
|
|
184
205
|
this.customHeaders = config.headers;
|
|
185
|
-
this.maxTurns =
|
|
206
|
+
this.maxTurns =
|
|
207
|
+
config.maxTurns === undefined || config.maxTurns === 0 || config.maxTurns === Infinity
|
|
208
|
+
? Infinity
|
|
209
|
+
: Number.isFinite(config.maxTurns as number)
|
|
210
|
+
? Math.max(1, Math.floor(config.maxTurns as number))
|
|
211
|
+
: Infinity;
|
|
186
212
|
this.sessionId = config.sessionId || config.cache?.sessionId || createSessionId();
|
|
213
|
+
(this as any).subagentTraces = {};
|
|
214
|
+
this.persistConfig = config.persist;
|
|
187
215
|
|
|
188
216
|
// DX4: single thinkingLevel flag — also inherit from ModelProviderInstance when omitted
|
|
189
217
|
const mpThinking = (rawModel as any)?.thinkingLevel;
|
|
@@ -198,6 +226,7 @@ export class Agent {
|
|
|
198
226
|
const initialCachedId = (this.cacheConfig as any)?.cachedContentId;
|
|
199
227
|
|
|
200
228
|
this.serviceTier = config.serviceTier;
|
|
229
|
+
this.bypassInputFileModality = config.bypassInputFileModality ?? false;
|
|
201
230
|
this.stateless = config.stateless ?? false;
|
|
202
231
|
|
|
203
232
|
// Tools registration
|
|
@@ -225,6 +254,10 @@ export class Agent {
|
|
|
225
254
|
}
|
|
226
255
|
}
|
|
227
256
|
|
|
257
|
+
if (this.bypassInputFileModality && !this.tools["convert_document_to_markdown"]) {
|
|
258
|
+
this.tools["convert_document_to_markdown"] = convert_document_to_markdown;
|
|
259
|
+
}
|
|
260
|
+
|
|
228
261
|
if (this.dynamicSubagents?.enabled === true) {
|
|
229
262
|
if (!this.subagentModel) {
|
|
230
263
|
throw new SubAgentModelError(
|
|
@@ -287,7 +320,179 @@ export class Agent {
|
|
|
287
320
|
this.context.systemPrompt = this.getFullInstructions();
|
|
288
321
|
}
|
|
289
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Returns a snapshot of one sub-agent worker's trace by tracking id
|
|
325
|
+
* (`SUBAGENT-NAME-{32hex}` or the raw 32-hex suffix). Sees live running
|
|
326
|
+
* state: steps appear as the worker acts, not just after it finishes.
|
|
327
|
+
*
|
|
328
|
+
* @example `const trace = agent.track("RAM-RESEARCH-AGENT-9f2c…");`
|
|
329
|
+
*/
|
|
330
|
+
track(trackingId: string): SubAgentTrace | undefined {
|
|
331
|
+
if (!trackingId) return undefined;
|
|
332
|
+
try {
|
|
333
|
+
const own = (this as any).subagentTraces as Record<string, SubAgentTrace> | undefined;
|
|
334
|
+
const hit = own?.[trackingId];
|
|
335
|
+
if (hit) return { ...hit, steps: hit.steps.map((s) => ({ ...s })) };
|
|
336
|
+
} catch {}
|
|
337
|
+
return getSubAgentTrace(trackingId);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Lists TrackingIDs of worker traces visible to this agent (deduped). */
|
|
341
|
+
listTrackedSubAgents(): string[] {
|
|
342
|
+
const rawOf = (id: string): string => {
|
|
343
|
+
const tail = id.split("-").pop() ?? "";
|
|
344
|
+
return /^[0-9a-f]{32}$/.test(tail) ? tail : id;
|
|
345
|
+
};
|
|
346
|
+
const seen = new Set<string>();
|
|
347
|
+
const out: string[] = [];
|
|
348
|
+
for (const id of [...Object.keys((this as any).subagentTraces ?? {}), ...listSubAgentTraceIds()]) {
|
|
349
|
+
const raw = rawOf(id);
|
|
350
|
+
if (!seen.has(raw)) {
|
|
351
|
+
seen.add(raw);
|
|
352
|
+
out.push(raw);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Subscribes to live updates of one worker trace. The callback fires on
|
|
360
|
+
* every logged step; returns an unsubscribe fn.
|
|
361
|
+
*
|
|
362
|
+
* @example `const off = agent.subscribeToSubAgent(id, (t) => render(t));`
|
|
363
|
+
*/
|
|
364
|
+
subscribeToSubAgent(trackingId: string, fn: (trace: SubAgentTrace) => void): () => void {
|
|
365
|
+
return subscribeToSubAgent(trackingId, fn);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Rewrites the configured session file now (when `persist` is set).
|
|
370
|
+
* Called automatically after every step during runs; safe to call
|
|
371
|
+
* manually. Never throws.
|
|
372
|
+
*/
|
|
373
|
+
persistNow(): void {
|
|
374
|
+
const cfg = this.persistConfig;
|
|
375
|
+
if (!cfg || (!cfg.dir && !cfg.file)) return;
|
|
376
|
+
try {
|
|
377
|
+
if (cfg.dir) saveSessionDir(cfg.dir, this as any, null);
|
|
378
|
+
else if (cfg.file) saveSessionFile(cfg.file, this as any, null);
|
|
379
|
+
} catch {}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Merges completed worker metadata (incl. steps) into this agent's trace registry. */
|
|
383
|
+
private mergeSubagentTraces(subagents: Array<{ trackingId?: string; name: string; sessionId?: string; parentSessionId?: string; task: string; steps?: SubAgentTrace["steps"]; [k: string]: unknown }>): void {
|
|
384
|
+
if (!subagents || subagents.length === 0) return;
|
|
385
|
+
const rawOf = (id?: string): string | undefined => {
|
|
386
|
+
if (!id) return undefined;
|
|
387
|
+
const tail = id.split("-").pop() ?? "";
|
|
388
|
+
return /^[0-9a-f]{32}$/.test(tail) ? tail : id;
|
|
389
|
+
};
|
|
390
|
+
try {
|
|
391
|
+
const registry = ((this as any).subagentTraces ??= {}) as Record<string, SubAgentTrace>;
|
|
392
|
+
for (const s of subagents) {
|
|
393
|
+
const raw = rawOf(s.trackingId);
|
|
394
|
+
const key = raw || s.name;
|
|
395
|
+
const existing = registry[key] ?? (raw ? registry[s.trackingId ?? ""] : undefined);
|
|
396
|
+
if (existing) {
|
|
397
|
+
existing.status = "done";
|
|
398
|
+
if (s.steps && s.steps.length > 0 && existing.steps.length === 0) {
|
|
399
|
+
existing.steps = s.steps.map((x) => ({ ...x }));
|
|
400
|
+
}
|
|
401
|
+
if (raw && !registry[raw]) registry[raw] = existing;
|
|
402
|
+
} else if (raw) {
|
|
403
|
+
registry[raw] = {
|
|
404
|
+
trackingId: raw,
|
|
405
|
+
name: s.name,
|
|
406
|
+
sessionId: s.sessionId || "",
|
|
407
|
+
parentSessionId: s.parentSessionId || this.sessionId,
|
|
408
|
+
status: "done",
|
|
409
|
+
task: s.task,
|
|
410
|
+
turns: 0,
|
|
411
|
+
steps: (s.steps ?? []).map((x) => ({ ...x })),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Exports this agent's conversation + config as a storable session snapshot.
|
|
420
|
+
* Pair with `importSession` / `loadSessionFile` / `saveSessionFile`.
|
|
421
|
+
*
|
|
422
|
+
* @example `saveSessionFile(".session.json", agent, telemetry)`
|
|
423
|
+
*/
|
|
424
|
+
exportSession(telemetry?: SessionTelemetry | SessionTotals | null): PersistedAgentSession {
|
|
425
|
+
return buildSessionData(this as any, telemetry);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Restores conversation + config from `exportSession` / `loadSessionFile`.
|
|
430
|
+
* Restores messages, system prompt, cached content id, sub-agent traces,
|
|
431
|
+
* and (when present) session id, model, thinking level, instructions,
|
|
432
|
+
* cache, and worker model. No-ops on nullish input.
|
|
433
|
+
*
|
|
434
|
+
* @example `const saved = loadSessionFile(".session.json"); if (saved) agent.importSession(saved);`
|
|
435
|
+
*/
|
|
436
|
+
importSession(data?: PersistedAgentSession | null): void {
|
|
437
|
+
if (!data) return;
|
|
438
|
+
if (data.sessionId) (this as any).sessionId = data.sessionId;
|
|
439
|
+
if ((data as any).parentSessionId) (this as any).parentSessionId = (data as any).parentSessionId;
|
|
440
|
+
if ((data as any).subagents && typeof (data as any).subagents === "object") {
|
|
441
|
+
try {
|
|
442
|
+
(this as any).subagentTraces = JSON.parse(JSON.stringify((data as any).subagents));
|
|
443
|
+
} catch {
|
|
444
|
+
(this as any).subagentTraces = { ...((data as any).subagents as object) };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (data.model) (this as any).modelStringOrSpec = data.model;
|
|
448
|
+
if (data.thinkingLevel) {
|
|
449
|
+
const lvl = String(data.thinkingLevel);
|
|
450
|
+
(this as any).thinkingConfig =
|
|
451
|
+
lvl === "none"
|
|
452
|
+
? { enabled: false, level: "none", budgetTokens: 0 }
|
|
453
|
+
: lvl === "dynamic"
|
|
454
|
+
? { enabled: true, level: "dynamic", budgetTokens: -1 }
|
|
455
|
+
: { enabled: true, level: lvl };
|
|
456
|
+
}
|
|
457
|
+
if (typeof data.instructions === "string") this.instructions = data.instructions;
|
|
458
|
+
if (data.cache) {
|
|
459
|
+
(this as any).cacheConfig = {
|
|
460
|
+
...this.cacheConfig,
|
|
461
|
+
...data.cache,
|
|
462
|
+
sessionId: data.sessionId ?? this.sessionId,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (data.subagentModel) {
|
|
466
|
+
(this as any).subagentModel = data.subagentModel;
|
|
467
|
+
const dyn = (this as any).dynamicSubagents;
|
|
468
|
+
if (dyn) dyn.model = data.subagentModel;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const g: any = globalThis as any;
|
|
472
|
+
this.context.messages = Array.isArray(data.messages)
|
|
473
|
+
? (typeof g.structuredClone === "function"
|
|
474
|
+
? g.structuredClone(data.messages)
|
|
475
|
+
: JSON.parse(JSON.stringify(data.messages)))
|
|
476
|
+
: [];
|
|
477
|
+
} catch {
|
|
478
|
+
this.context.messages = Array.isArray(data.messages) ? [...data.messages] : [];
|
|
479
|
+
}
|
|
480
|
+
this.context.thoughtSignatures = [];
|
|
481
|
+
if (data.systemPrompt) {
|
|
482
|
+
this.context.systemPrompt = data.systemPrompt;
|
|
483
|
+
} else {
|
|
484
|
+
this.context.systemPrompt = this.getFullInstructions();
|
|
485
|
+
}
|
|
486
|
+
this.context.cachedContentId =
|
|
487
|
+
data.cachedContentId ?? (this.cacheConfig as any)?.cachedContentId;
|
|
488
|
+
}
|
|
489
|
+
|
|
290
490
|
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions): void {
|
|
491
|
+
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
"[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
|
|
494
|
+
);
|
|
495
|
+
}
|
|
291
496
|
if (this.stateless) {
|
|
292
497
|
this.context.messages = [];
|
|
293
498
|
this.context.thoughtSignatures = [];
|
|
@@ -317,10 +522,26 @@ export class Agent {
|
|
|
317
522
|
* Runs one or more model/tool turns and resolves to a normalized AgentResponse.
|
|
318
523
|
* @example `const response = await agent.run("Summarize this document");`
|
|
319
524
|
*/
|
|
525
|
+
run(
|
|
526
|
+
prompt: string | ContentPart[],
|
|
527
|
+
options: AgentRunOptions & { stream: true } & (
|
|
528
|
+
| { onDelta: NonNullable<AgentRunOptions["onDelta"]> }
|
|
529
|
+
| { onThinkingDelta: NonNullable<AgentRunOptions["onThinkingDelta"]> }
|
|
530
|
+
| { onEvent: NonNullable<AgentRunOptions["onEvent"]> }
|
|
531
|
+
)
|
|
532
|
+
): Promise<AgentResponse> & AsyncIterable<StreamEvent>;
|
|
533
|
+
run(
|
|
534
|
+
prompt: string | ContentPart[],
|
|
535
|
+
options: AgentRunOptions & { stream: true }
|
|
536
|
+
): AssistantMessageEventStream;
|
|
320
537
|
run(
|
|
321
538
|
prompt: string | ContentPart[],
|
|
322
539
|
options?: AgentRunOptions
|
|
323
|
-
): Promise<AgentResponse
|
|
540
|
+
): Promise<AgentResponse>;
|
|
541
|
+
run(
|
|
542
|
+
prompt: string | ContentPart[],
|
|
543
|
+
options?: AgentRunOptions
|
|
544
|
+
): Promise<AgentResponse> | AssistantMessageEventStream {
|
|
324
545
|
const isStream = options?.stream === true;
|
|
325
546
|
if (isStream) {
|
|
326
547
|
const hasCallbacks = !!(options?.onDelta || options?.onThinkingDelta || options?.onEvent);
|
|
@@ -343,44 +564,52 @@ export class Agent {
|
|
|
343
564
|
return s;
|
|
344
565
|
}
|
|
345
566
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
567
|
+
return (async (): Promise<AgentResponse> => {
|
|
568
|
+
// Refresh the catalog BEFORE thinking validation so levels are checked
|
|
569
|
+
// against live data instead of a possibly-empty cold cache.
|
|
570
|
+
await ensureModelCatalogFresh();
|
|
571
|
+
const resolved = resolveModel(this.modelStringOrSpec);
|
|
572
|
+
const effectiveLevel = options?.thinkingLevel || this.thinkingConfig?.level;
|
|
573
|
+
if (effectiveLevel) {
|
|
574
|
+
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
575
|
+
}
|
|
576
|
+
this.prepareTurn(prompt, options);
|
|
577
|
+
this.persistNow();
|
|
578
|
+
|
|
579
|
+
const providerOptions = {
|
|
580
|
+
apiKey: this.apiKey,
|
|
581
|
+
baseUrl: this.baseUrl,
|
|
582
|
+
headers: { ...(this.customHeaders ?? {}), ...(options?.headers ?? {}) },
|
|
583
|
+
thinking: resolveEffectiveThinking(this.thinkingConfig, options?.thinkingLevel),
|
|
584
|
+
cache: this.stateless
|
|
585
|
+
? { sessionId: options?.sessionId || this.sessionId }
|
|
586
|
+
: { ...this.cacheConfig, sessionId: options?.sessionId || this.sessionId },
|
|
587
|
+
serviceTier: this.serviceTier,
|
|
588
|
+
sessionId: options?.sessionId || this.sessionId,
|
|
589
|
+
};
|
|
364
590
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
591
|
+
const loopConfig = {
|
|
592
|
+
agentName: this.name,
|
|
593
|
+
provider: resolved.provider,
|
|
594
|
+
modelId: resolved.modelId,
|
|
595
|
+
context: this.context,
|
|
596
|
+
tools: this.tools,
|
|
597
|
+
options: providerOptions,
|
|
598
|
+
runOptions: options,
|
|
599
|
+
maxTurns: this.maxTurns,
|
|
600
|
+
bypassInputFileModality: this.bypassInputFileModality,
|
|
601
|
+
onProgress: () => this.persistNow(),
|
|
602
|
+
};
|
|
375
603
|
|
|
376
|
-
|
|
604
|
+
const res = await runAgentLoop(loopConfig);
|
|
605
|
+
this.mergeSubagentTraces(res.subagents as any);
|
|
606
|
+
this.persistNow();
|
|
377
607
|
if (this.stateless) {
|
|
378
608
|
this.context.messages = [];
|
|
379
609
|
this.context.thoughtSignatures = [];
|
|
380
610
|
}
|
|
381
611
|
return res;
|
|
382
|
-
});
|
|
383
|
-
return promise as any;
|
|
612
|
+
})();
|
|
384
613
|
}
|
|
385
614
|
|
|
386
615
|
/**
|
|
@@ -409,10 +638,16 @@ export class Agent {
|
|
|
409
638
|
}
|
|
410
639
|
const stringStream: any = {
|
|
411
640
|
[Symbol.asyncIterator]: async function* () {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
641
|
+
try {
|
|
642
|
+
for await (const chunk of stream) {
|
|
643
|
+
if (chunk.type === "text_delta" && chunk.delta) {
|
|
644
|
+
yield chunk.delta;
|
|
645
|
+
}
|
|
415
646
|
}
|
|
647
|
+
} finally {
|
|
648
|
+
// Breaking out early must stop the underlying provider stream
|
|
649
|
+
// instead of leaving the HTTP connection running in the background.
|
|
650
|
+
stream.cancel();
|
|
416
651
|
}
|
|
417
652
|
},
|
|
418
653
|
result: () => stream.result(),
|
|
@@ -428,7 +663,7 @@ export class Agent {
|
|
|
428
663
|
return stringStream;
|
|
429
664
|
}
|
|
430
665
|
|
|
431
|
-
return this.run(prompt, runOpts);
|
|
666
|
+
return this.run(prompt, runOpts) as any;
|
|
432
667
|
}
|
|
433
668
|
|
|
434
669
|
/**
|
|
@@ -450,6 +685,7 @@ export class Agent {
|
|
|
450
685
|
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
451
686
|
}
|
|
452
687
|
this.prepareTurn(prompt, options);
|
|
688
|
+
this.persistNow();
|
|
453
689
|
|
|
454
690
|
const providerOptions = {
|
|
455
691
|
apiKey: this.apiKey,
|
|
@@ -472,9 +708,17 @@ export class Agent {
|
|
|
472
708
|
options: providerOptions,
|
|
473
709
|
runOptions: options,
|
|
474
710
|
maxTurns: this.maxTurns,
|
|
711
|
+
bypassInputFileModality: this.bypassInputFileModality,
|
|
712
|
+
onProgress: () => this.persistNow(),
|
|
475
713
|
};
|
|
476
714
|
|
|
477
715
|
const s = streamAgentLoop(loopConfig);
|
|
716
|
+
s.result().then((res) => {
|
|
717
|
+
try {
|
|
718
|
+
this.mergeSubagentTraces(res.subagents as any);
|
|
719
|
+
this.persistNow();
|
|
720
|
+
} catch {}
|
|
721
|
+
}).catch(() => {});
|
|
478
722
|
if (this.stateless) {
|
|
479
723
|
s.result().then(() => {
|
|
480
724
|
this.context.messages = [];
|
|
@@ -483,8 +727,32 @@ export class Agent {
|
|
|
483
727
|
}
|
|
484
728
|
// Wire one-liner callbacks so `stream:true` + onDelta is enough — no manual for-await needed
|
|
485
729
|
if (options?.wrapThinking) {
|
|
486
|
-
// Auto-wrap reasoning as <think
|
|
730
|
+
// Auto-wrap reasoning as <think>…</think> per-turn — clean boundaries across multi-turn agent runs.
|
|
731
|
+
//
|
|
732
|
+
// Display-only whitespace hygiene (cache-safe): provider thought
|
|
733
|
+
// summaries routinely carry edge blank lines (observed `"...\n\n\n"`),
|
|
734
|
+
// and the wrapper itself adds newlines around the span. Without care
|
|
735
|
+
// this renders as blank lines after `<think>`, double blanks between
|
|
736
|
+
// sections, and a stray blank line before `</think>`. The buffering
|
|
737
|
+
// below touches ONLY the presented strings/callback args and synthetic
|
|
738
|
+
// tag events — never provider accumulation, AgentContext history, or
|
|
739
|
+
// request bodies — so prefix-cache affinity is untouched.
|
|
740
|
+
//
|
|
741
|
+
// Trailing-newline buffering: each chunk's trailing `\n` run is
|
|
742
|
+
// withheld and prepended to the next chunk (or dropped at close), so
|
|
743
|
+
// emitted display text never ends with `\n` and `</think>` never gets
|
|
744
|
+
// a preceding blank line. Interior breaks are preserved (collapsed to
|
|
745
|
+
// a single blank line at most).
|
|
487
746
|
let isThinking = false;
|
|
747
|
+
let emittedAny = false;
|
|
748
|
+
let pendingNewlines = "";
|
|
749
|
+
// True once text has been emitted without a trailing newline: the next
|
|
750
|
+
// <think> open tag then needs its own leading newline, otherwise it
|
|
751
|
+
// glues onto the streamed text mid-line. False at stream start and
|
|
752
|
+
// after every tag (both end with newlines), preserving the exact
|
|
753
|
+
// "<think>\n...\n</think>\n\n" contract for thinking-first streams.
|
|
754
|
+
let needThinkNewline = false;
|
|
755
|
+
const collapseBlankLines = (s: string): string => s.replace(/\n{3,}/g, "\n\n");
|
|
488
756
|
const userOnThinking = options.onThinkingDelta;
|
|
489
757
|
const userOnDelta = options.onDelta;
|
|
490
758
|
const userOnEvent = options.onEvent;
|
|
@@ -493,7 +761,10 @@ export class Agent {
|
|
|
493
761
|
const openThink = (e?: any) => {
|
|
494
762
|
if (!isThinking) {
|
|
495
763
|
isThinking = true;
|
|
496
|
-
|
|
764
|
+
emittedAny = false;
|
|
765
|
+
pendingNewlines = "";
|
|
766
|
+
const tag = (needThinkNewline ? "\n" : "") + "<think>\n";
|
|
767
|
+
needThinkNewline = false;
|
|
497
768
|
if (userOnThinking) userOnThinking(tag, e);
|
|
498
769
|
else if (userOnDelta) userOnDelta(tag, e as any);
|
|
499
770
|
}
|
|
@@ -502,20 +773,54 @@ export class Agent {
|
|
|
502
773
|
const closeThink = (e?: any) => {
|
|
503
774
|
if (isThinking) {
|
|
504
775
|
isThinking = false;
|
|
505
|
-
|
|
776
|
+
// Drop any withheld trailing newlines: the span ends cleanly and
|
|
777
|
+
// the tag supplies its own line breaks. Empty spans still close.
|
|
778
|
+
pendingNewlines = "";
|
|
779
|
+
const close = emittedAny ? "\n</think>\n\n" : "</think>\n\n";
|
|
780
|
+
emittedAny = false;
|
|
506
781
|
if (userOnThinking) userOnThinking(close, e);
|
|
507
782
|
else if (userOnDelta) userOnDelta(close, e as any);
|
|
508
783
|
}
|
|
509
784
|
};
|
|
510
785
|
|
|
786
|
+
/** Buffers one raw thinking chunk, returning the display string to
|
|
787
|
+
* emit now (`""` when nothing displayable yet). Mutates the event in
|
|
788
|
+
* place so manual `for-await` consumers see the same clean text; safe
|
|
789
|
+
* because loop context derives from the provider result, not these
|
|
790
|
+
* outer events. */
|
|
791
|
+
const bufferThinkingEvent = (e: any): string => {
|
|
792
|
+
const raw: string = e.thinkingDelta ?? "";
|
|
793
|
+
let chunk = collapseBlankLines(pendingNewlines + raw);
|
|
794
|
+
pendingNewlines = "";
|
|
795
|
+
if (!emittedAny) {
|
|
796
|
+
chunk = chunk.replace(/^\n+/, "");
|
|
797
|
+
}
|
|
798
|
+
if (!chunk) return "";
|
|
799
|
+
const m = chunk.match(/\n+$/);
|
|
800
|
+
if (m) {
|
|
801
|
+
pendingNewlines = m[0];
|
|
802
|
+
chunk = chunk.slice(0, -m[0].length);
|
|
803
|
+
if (!chunk) return "";
|
|
804
|
+
}
|
|
805
|
+
emittedAny = true;
|
|
806
|
+
e.thinkingDelta = chunk;
|
|
807
|
+
if (typeof e.partialThinking === "string") {
|
|
808
|
+
e.partialThinking = collapseBlankLines(e.partialThinking);
|
|
809
|
+
}
|
|
810
|
+
return chunk;
|
|
811
|
+
};
|
|
812
|
+
|
|
511
813
|
if (userOnThinking || userOnDelta) {
|
|
512
814
|
s.on("thinking_delta", (e: any) => {
|
|
513
815
|
openThink(e);
|
|
514
|
-
|
|
515
|
-
|
|
816
|
+
const chunk = bufferThinkingEvent(e);
|
|
817
|
+
if (!chunk) return;
|
|
818
|
+
if (userOnThinking) userOnThinking(chunk, e);
|
|
819
|
+
else if (userOnDelta) userOnDelta(chunk, e as any);
|
|
516
820
|
});
|
|
517
821
|
s.on("text_delta", (e: any) => {
|
|
518
822
|
closeThink(e);
|
|
823
|
+
needThinkNewline = !((e.delta ?? "").endsWith("\n"));
|
|
519
824
|
if (userOnDelta) userOnDelta(e.delta!, e);
|
|
520
825
|
else if (userOnThinking) userOnThinking(e.delta!, e as any);
|
|
521
826
|
});
|
|
@@ -528,48 +833,55 @@ export class Agent {
|
|
|
528
833
|
closeThink(e || ({ type: "done" } as any));
|
|
529
834
|
});
|
|
530
835
|
} else {
|
|
531
|
-
// No callbacks but wrapThinking true — still emit tags as events for manual iteration
|
|
836
|
+
// No callbacks but wrapThinking true — still emit tags as events for manual iteration.
|
|
837
|
+
// Same display hygiene as above (in-place event normalization is safe:
|
|
838
|
+
// loop context derives from the provider result, not outer events).
|
|
839
|
+
const closeSynthetic = () => {
|
|
840
|
+
if (isThinking) {
|
|
841
|
+
isThinking = false;
|
|
842
|
+
pendingNewlines = "";
|
|
843
|
+
const tag = emittedAny ? "\n</think>\n\n" : "</think>\n\n";
|
|
844
|
+
emittedAny = false;
|
|
845
|
+
s.push({ type: "thinking_delta", thinkingDelta: tag, __synthetic: true } as any);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
532
848
|
s.on("thinking_delta", (e: any) => {
|
|
849
|
+
// Ignore our own synthetic tags: push() redelivers events to
|
|
850
|
+
// listeners synchronously, so without this guard the handler
|
|
851
|
+
// re-enters on every tag it emits (duplicate <think> spans and
|
|
852
|
+
// mutated queued events).
|
|
853
|
+
if ((e as any).__synthetic) return;
|
|
533
854
|
if (!isThinking) {
|
|
534
855
|
isThinking = true;
|
|
535
|
-
|
|
856
|
+
emittedAny = false;
|
|
857
|
+
pendingNewlines = "";
|
|
858
|
+
const openTag = (needThinkNewline ? "\n" : "") + "<think>\n";
|
|
859
|
+
needThinkNewline = false;
|
|
860
|
+
s.push({ type: "thinking_delta", thinkingDelta: openTag, __synthetic: true } as any);
|
|
861
|
+
}
|
|
862
|
+
const chunk = bufferThinkingEvent(e);
|
|
863
|
+
if (!chunk) {
|
|
864
|
+
e.thinkingDelta = "";
|
|
536
865
|
}
|
|
537
866
|
});
|
|
538
867
|
s.on("text_delta", (e: any) => {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
542
|
-
}
|
|
868
|
+
closeSynthetic();
|
|
869
|
+
needThinkNewline = !((e.delta ?? "").endsWith("\n"));
|
|
543
870
|
});
|
|
544
871
|
s.on("tool_call_start" as any, () => {
|
|
545
|
-
|
|
546
|
-
isThinking = false;
|
|
547
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
548
|
-
}
|
|
872
|
+
closeSynthetic();
|
|
549
873
|
});
|
|
550
874
|
s.on("tool_call_complete" as any, () => {
|
|
551
|
-
|
|
552
|
-
isThinking = false;
|
|
553
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
554
|
-
}
|
|
875
|
+
closeSynthetic();
|
|
555
876
|
});
|
|
556
877
|
s.on("tool_result" as any, () => {
|
|
557
|
-
|
|
558
|
-
isThinking = false;
|
|
559
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
560
|
-
}
|
|
878
|
+
closeSynthetic();
|
|
561
879
|
});
|
|
562
880
|
s.on("subagent_complete" as any, () => {
|
|
563
|
-
|
|
564
|
-
isThinking = false;
|
|
565
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
566
|
-
}
|
|
881
|
+
closeSynthetic();
|
|
567
882
|
});
|
|
568
883
|
s.on("done", () => {
|
|
569
|
-
|
|
570
|
-
isThinking = false;
|
|
571
|
-
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
572
|
-
}
|
|
884
|
+
closeSynthetic();
|
|
573
885
|
});
|
|
574
886
|
if (userOnEvent) s.on("*", userOnEvent as any);
|
|
575
887
|
}
|
|
@@ -584,7 +896,7 @@ export class Agent {
|
|
|
584
896
|
|
|
585
897
|
/** Normalizes the public `thinkingLevel` flag into provider-neutral thinking settings. */
|
|
586
898
|
export function normalizeThinking(config: AgentConfig, fallbackLevel?: string): ThinkingConfig | undefined {
|
|
587
|
-
// DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh
|
|
899
|
+
// DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh, max
|
|
588
900
|
const rawLevel = config.thinkingLevel ?? fallbackLevel;
|
|
589
901
|
const level = rawLevel as ThinkingLevel | undefined;
|
|
590
902
|
|
|
@@ -598,7 +910,7 @@ export function normalizeThinking(config: AgentConfig, fallbackLevel?: string):
|
|
|
598
910
|
const provStr = typeof rawModel === "object" && rawModel?.provider ? rawModel.provider : modelStr.includes("/") ? modelStr.split("/")[0] : "";
|
|
599
911
|
const actualModelId = modelStr.includes("/") ? modelStr.split("/").slice(1).join("/") : modelStr;
|
|
600
912
|
if (actualModelId) {
|
|
601
|
-
validateModelThinking(provStr || "
|
|
913
|
+
validateModelThinking(provStr || "openrouter", actualModelId, level);
|
|
602
914
|
}
|
|
603
915
|
} catch (e) {
|
|
604
916
|
throw e;
|
package/src/agent/context.ts
CHANGED