agent-accelerator 0.2.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/src/agent/loop.ts CHANGED
@@ -22,6 +22,53 @@ export interface AgentLoopConfig {
22
22
  maxTurns?: number;
23
23
  /** Convert `file` parts client-side for pdf-incapable models (see Agent flag). */
24
24
  bypassInputFileModality?: boolean;
25
+ /** Called after every turn for durable persistence checkpoints (never fails a turn). */
26
+ onProgress?: () => void;
27
+ }
28
+
29
+ function emitTurn(
30
+ runOptions: AgentRunOptions | undefined,
31
+ turn: { turns: number; text?: string; thinking?: string; toolCalls?: ToolCallRecord[]; toolResults?: ToolResultRecord[] }
32
+ ): void {
33
+ try {
34
+ runOptions?.onTurn?.({
35
+ turns: turn.turns,
36
+ text: turn.text,
37
+ thinking: turn.thinking,
38
+ toolCalls: turn.toolCalls?.map((c) => ({ id: c.id, name: c.name })),
39
+ toolResults: turn.toolResults?.map((r) => ({ id: r.id, name: r.name, isError: r.isError })),
40
+ });
41
+ } catch {}
42
+ }
43
+
44
+ function emitProgress(config: { onProgress?: () => void }): void {
45
+ try { config.onProgress?.(); } catch {}
46
+ }
47
+
48
+ /**
49
+ * Normalizes `maxTurns` to a safe positive integer (default Infinity).
50
+ * `0` means infinite (no limit), matching the `timeout: 0` convention.
51
+ * Prevents silent zero-turn runs from negative values (clamped to 1).
52
+ */
53
+ function normalizeMaxTurns(value?: number): number {
54
+ if (value === undefined) return Infinity;
55
+ if (value === 0 || value === Infinity) return Infinity;
56
+ if (!Number.isFinite(value as number)) return Infinity;
57
+ return Math.max(1, Math.floor(value as number));
58
+ }
59
+
60
+ /**
61
+ * Warns when an agent run stops because `maxTurns` was exhausted while
62
+ * the model still wanted to call tools. Without this, the loop simply
63
+ * returns the last tool-turn text (often empty) with `finishReason:
64
+ * "tool_calls"`, which looks like success while dropping pending work.
65
+ */
66
+ function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendingTools: string[]): void {
67
+ const who = agentName ? `"${agentName}"` : "Agent";
68
+ const tools = pendingTools.length > 0 ? ` Pending tool calls dropped: ${pendingTools.join(", ")}.` : "";
69
+ // Leading newline: runs often end mid-line on streamed output; without it
70
+ // the warning glues onto the last streamed chunk.
71
+ console.warn(`\n[Agent Accelerator] WARNING [agent] ${who} hit maxTurns (${maxTurns}) with unfinished tool calls.${tools} Increase maxTurns or split the task.`);
25
72
  }
26
73
 
27
74
  export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec): TokenUsage["cost"] {
@@ -131,6 +178,7 @@ async function executeToolCallsWithRepeatGuard(options: {
131
178
  agentName?: string;
132
179
  signal?: AbortSignal;
133
180
  sessionId?: string;
181
+ onSubagentEvent?: (event: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
134
182
  }): Promise<{ results: ToolResultRecord[]; fingerprints: Set<string> }> {
135
183
  const { toolCalls, previousFingerprints } = options;
136
184
  const currentFingerprints = new Set<string>();
@@ -163,6 +211,7 @@ async function executeToolCallsWithRepeatGuard(options: {
163
211
  parallel: true,
164
212
  signal: options.signal,
165
213
  sessionId: options.sessionId,
214
+ onSubagentEvent: options.onSubagentEvent,
166
215
  })
167
216
  : [];
168
217
  const byId = new Map<string, ToolResultRecord[]>();
@@ -202,8 +251,9 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
202
251
  tools,
203
252
  options,
204
253
  runOptions,
205
- maxTurns = 10,
254
+ maxTurns: rawMaxTurns = Infinity,
206
255
  } = config;
256
+ const maxTurns = normalizeMaxTurns(rawMaxTurns);
207
257
 
208
258
  if (config.bypassInputFileModality) {
209
259
  await preprocessFilePartsForBypass(config.context.messages, {
@@ -289,6 +339,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
289
339
 
290
340
  // If no tool calls, generation is complete!
291
341
  if (!genResult.toolCalls || genResult.toolCalls.length === 0) {
342
+ emitTurn(runOptions, { turns, text: genResult.text, thinking: genResult.thinking });
343
+ emitProgress(config);
292
344
  break;
293
345
  }
294
346
 
@@ -325,10 +377,31 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
325
377
 
326
378
  allToolResults.push(...sanitizedResults);
327
379
  context.addToolResults(sanitizedResults);
380
+ emitTurn(runOptions, {
381
+ turns,
382
+ text: genResult.text,
383
+ thinking: genResult.thinking,
384
+ toolCalls: genResult.toolCalls,
385
+ toolResults: sanitizedResults,
386
+ });
387
+ emitProgress(config);
328
388
 
329
389
  if (runOptions?.signal?.aborted) break;
330
390
  }
331
391
 
392
+ // maxTurns exhausted while the model still requested tools: the tool
393
+ // results above were never sent back, so the run is truncated — surface
394
+ // it via finishReason + warning instead of silently returning.
395
+ const truncatedByMaxTurns =
396
+ turns >= maxTurns && !!finalResult?.toolCalls && finalResult.toolCalls.length > 0;
397
+ if (truncatedByMaxTurns) {
398
+ warnMaxTurnsHit(
399
+ agentName,
400
+ maxTurns,
401
+ finalResult.toolCalls.map((c: ToolCallRecord) => c.name)
402
+ );
403
+ }
404
+
332
405
  return new AgentResponse({
333
406
  text: finalResult?.text || "",
334
407
  thinking: finalResult?.thinking,
@@ -340,7 +413,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
340
413
  responseId: finalResult?.responseId,
341
414
  model: modelId,
342
415
  provider: provider.id,
343
- finishReason: finalResult?.finishReason,
416
+ finishReason: truncatedByMaxTurns ? "max_turns" : finalResult?.finishReason,
344
417
  durationMs: Date.now() - startTime,
345
418
  raw: finalResult?.raw,
346
419
  turns,
@@ -409,8 +482,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
409
482
  tools,
410
483
  options,
411
484
  runOptions,
412
- maxTurns = 10,
485
+ maxTurns: rawMaxTurns = Infinity,
413
486
  } = config;
487
+ const maxTurns = normalizeMaxTurns(rawMaxTurns);
414
488
 
415
489
  const standardTools = toStandardToolDeclarations(tools);
416
490
 
@@ -495,6 +569,8 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
495
569
  );
496
570
 
497
571
  if (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) {
572
+ emitTurn(runOptions, { turns, text: turnResponse.text, thinking: turnResponse.thinking });
573
+ emitProgress(config);
498
574
  break;
499
575
  }
500
576
 
@@ -506,6 +582,18 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
506
582
  agentName,
507
583
  signal: linked.signal,
508
584
  sessionId: runOptions?.sessionId || options?.sessionId || options?.cache?.sessionId,
585
+ onSubagentEvent: (ev) => {
586
+ try {
587
+ outerStream.push({
588
+ type: "subagent_delta",
589
+ subagentTrackingId: ev.trackingId,
590
+ delta: ev.delta,
591
+ thinkingDelta: ev.thinkingDelta,
592
+ partialText: ev.partialText,
593
+ partialThinking: ev.partialThinking,
594
+ });
595
+ } catch {}
596
+ },
509
597
  });
510
598
  throwIfCancelled();
511
599
  const results = guarded.results;
@@ -541,7 +629,26 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
541
629
 
542
630
  allToolResults.push(...sanitizedResults);
543
631
  context.addToolResults(sanitizedResults);
632
+ emitTurn(runOptions, {
633
+ turns,
634
+ text: turnResponse.text,
635
+ thinking: turnResponse.thinking,
636
+ toolCalls: turnResponse.toolCalls,
637
+ toolResults: sanitizedResults,
638
+ });
639
+ emitProgress(config);
640
+ }
641
+
642
+ const truncatedByMaxTurns =
643
+ turns >= maxTurns && !!lastResponse?.toolCalls && lastResponse.toolCalls.length > 0;
644
+ if (truncatedByMaxTurns) {
645
+ warnMaxTurnsHit(
646
+ agentName,
647
+ maxTurns,
648
+ lastResponse!.toolCalls.map((c) => c.name)
649
+ );
544
650
  }
651
+ const truncatedFinishReason = truncatedByMaxTurns ? "max_turns" : lastResponse?.finishReason;
545
652
 
546
653
  const finalAgentResponse = new AgentResponse({
547
654
  text: lastResponse?.text || "",
@@ -554,7 +661,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
554
661
  responseId: lastResponse?.responseId,
555
662
  model: modelId,
556
663
  provider: provider.id,
557
- finishReason: lastResponse?.finishReason,
664
+ finishReason: truncatedFinishReason,
558
665
  durationMs: Date.now() - startTime,
559
666
  raw: lastResponse?.raw as any,
560
667
  turns,
@@ -564,7 +671,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
564
671
  type: "done",
565
672
  delta: "",
566
673
  usage: accumulatedUsage,
567
- finishReason: lastResponse?.finishReason,
674
+ finishReason: truncatedFinishReason,
568
675
  responseId: lastResponse?.responseId,
569
676
  });
570
677
 
package/src/index.ts CHANGED
@@ -31,8 +31,14 @@ export {
31
31
  buildAgentTools,
32
32
  agentToTool,
33
33
  createSubagentSpawnTool,
34
+ createChildSessionId,
35
+ createFixedChildSessionId,
36
+ isSessionDescendant,
37
+ getSubAgentTrace,
38
+ listSubAgentTraceIds,
39
+ subscribeToSubAgent,
34
40
  } from "./agent/delegation.ts";
35
- export type { DynamicSubagentTask, AgentAsToolTarget } from "./agent/delegation.ts";
41
+ export type { DynamicSubagentTask, AgentAsToolTarget, SubAgentTrace } from "./agent/delegation.ts";
36
42
 
37
43
  // Providers & Registry — unified multi-provider layer, all native REST
38
44
  export {
@@ -160,8 +166,32 @@ export { SSEParser } from "./streaming/sse-parser.ts";
160
166
  // Responses
161
167
  export { AgentResponse } from "./types/response.ts";
162
168
 
169
+ // Session persistence & telemetry (conversation history + cost rollup)
170
+ export {
171
+ SessionTelemetry,
172
+ loadSessionFile,
173
+ saveSessionFile,
174
+ loadSessionDir,
175
+ saveSessionDir,
176
+ writeFileAtomic,
177
+ findLatestSessionDir,
178
+ sessionDirFor,
179
+ extractMediaToDir,
180
+ resolveMediaPaths,
181
+ buildSessionData,
182
+ serializeSession,
183
+ deserializeSession,
184
+ getSessionContextWindow,
185
+ computeSessionTurnCost,
186
+ formatSessionTokens,
187
+ formatSessionCost,
188
+ formatSessionBanner,
189
+ emptyTotals,
190
+ } from "./session/store.ts";
191
+ export type { PersistedAgentSession, SessionTotals, SessionAgentLike, LoadedSessionDir, PersistedSubAgentTrace } from "./session/store.ts";
192
+
163
193
  // Utilities
164
- export { createSessionId } from "./utils/session.ts";
194
+ export { createSessionId, hashSessionPart, createTrackingId } from "./utils/session.ts";
165
195
  export { getApiKey, getEnv } from "./utils/env.ts";
166
196
  export { buildSessionHeaders } from "./utils/headers.ts";
167
197
  export { normalizeMediaInput, inferMimeType } from "./utils/media.ts";
@@ -218,6 +248,7 @@ export type {
218
248
  export type {
219
249
  AgentResponseJSON,
220
250
  SubAgentExecutionMetadata,
251
+ SubAgentStep,
221
252
  StreamEvent,
222
253
  StreamEventType,
223
254
  } from "./types/response.ts";
@@ -225,6 +256,7 @@ export type {
225
256
  export type {
226
257
  AgentConfig,
227
258
  AgentRunOptions,
259
+ AgentTurnEvent,
228
260
  DynamicSubagentsConfig,
229
261
  } from "./types/agent.ts";
230
262
 
@@ -244,7 +244,7 @@ export function getModelThinkingInfo(provider: string, modelId: string): ModelTh
244
244
  if (!spec) {
245
245
  return {
246
246
  supportsThinking: true,
247
- allowedLevels: ["none", "minimal", "low", "medium", "high", "xhigh", "dynamic"],
247
+ allowedLevels: ["none", "minimal", "low", "medium", "high", "xhigh", "max", "dynamic"],
248
248
  supportsDisable: true,
249
249
  description: "Model not found in catalog; generic thinking levels allowed.",
250
250
  };
@@ -295,6 +295,7 @@ export function getModelThinkingInfo(provider: string, modelId: string): ModelTh
295
295
  allowed.add("medium");
296
296
  allowed.add("high");
297
297
  allowed.add("xhigh");
298
+ allowed.add("max");
298
299
  }
299
300
 
300
301
  // Fixed reasoning model (no reasoning_options) e.g. DeepSeek-R1, QwQ-32B
@@ -510,7 +511,7 @@ export {
510
511
 
511
512
  /**
512
513
  * Permissive placeholder so custom endpoints never fail preflight:
513
- * allows every ThinkingLevel (none -> xhigh + dynamic).
514
+ * allows every ThinkingLevel (none -> max + dynamic).
514
515
  * @example `const spec = createGenericModelSpec("groq", "llama-3.3-70b-versatile");`
515
516
  */
516
517
  export function createGenericModelSpec(provider: string, modelId: string): ModelSpec {
@@ -527,7 +528,7 @@ export function createGenericModelSpec(provider: string, modelId: string): Model
527
528
  reasoning: true,
528
529
  reasoning_options: [
529
530
  { type: "toggle" },
530
- { type: "effort", values: ["minimal", "low", "medium", "high", "xhigh"] },
531
+ { type: "effort", values: ["minimal", "low", "medium", "high", "xhigh", "max"] },
531
532
  { type: "budget_tokens", min: 0, max: 128000 },
532
533
  ],
533
534
  tool_call: true,
package/src/providers.ts CHANGED
@@ -108,7 +108,7 @@ export function clearSessionRouting(sessionId?: string): void {
108
108
 
109
109
  /**
110
110
  * Maps a canonical ThinkingLevel onto Google Interactions `thinking_level`.
111
- * Google values: minimal|low|medium|high (no off switch, no xhigh).
111
+ * Google values: minimal|low|medium|high (no off switch, no xhigh/max).
112
112
  */
113
113
  export function mapThinkingLevelToGoogle(
114
114
  level: ThinkingLevel | string | undefined
@@ -126,7 +126,7 @@ export function mapThinkingLevelToGoogle(
126
126
  });
127
127
  return {};
128
128
  }
129
- if (norm === "xhigh") {
129
+ if (norm === "xhigh" || norm === "max") {
130
130
  emitProviderWarning({
131
131
  provider: "google",
132
132
  capability: "thinking level",
@@ -155,7 +155,7 @@ export function mapServiceTierToGoogle(tier: ServiceTier | undefined): "flex" |
155
155
  * Completions documents `reasoning_effort: xhigh|high|medium|low|minimal|none`
156
156
  * (parameters.md) and live probes accept `xhigh` verbatim (`03`), so — unlike
157
157
  * the discontinued Responses skin, which clamped `xhigh` — everything passes
158
- * through. `dynamic` omits (server default).
158
+ * through. `max` rides the same verbatim path. `dynamic` omits (server default).
159
159
  */
160
160
  export function mapThinkingLevelToOpenRouterChat(
161
161
  level: ThinkingLevel | string | undefined
@@ -169,7 +169,8 @@ export function mapThinkingLevelToOpenRouterChat(
169
169
  norm === "low" ||
170
170
  norm === "medium" ||
171
171
  norm === "high" ||
172
- norm === "xhigh"
172
+ norm === "xhigh" ||
173
+ norm === "max"
173
174
  ) {
174
175
  return { effort: norm };
175
176
  }
@@ -179,8 +180,8 @@ export function mapThinkingLevelToOpenRouterChat(
179
180
  /**
180
181
  * Maps a canonical ThinkingLevel onto OpenRouter Responses `reasoning.effort`.
181
182
  * Documented efforts: minimal|low|medium|high (server default medium).
182
- * Wire-validated extras: `none` disables reasoning output; `xhigh` is echoed
183
- * but is NOT a documented level, so it clamps to `high` with a warning.
183
+ * Wire-validated extras: `none` disables reasoning output; `xhigh`/`max` are
184
+ * echoed but are NOT documented levels, so they clamp to `high` with a warning.
184
185
  *
185
186
  * @deprecated The Responses skin is discontinued for OpenRouter
186
187
  * (`src/providers/openrouter-responses.ts`, archived). Use
@@ -194,7 +195,7 @@ export function mapThinkingLevelToOpenRouter(
194
195
  const norm = String(level).toLowerCase().trim();
195
196
  if (norm === "dynamic") return {}; // server default (medium)
196
197
  if (norm === "none") return { effort: "none" };
197
- if (norm === "xhigh") {
198
+ if (norm === "xhigh" || norm === "max") {
198
199
  emitProviderWarning({
199
200
  provider: "openrouter",
200
201
  capability: "thinking level",
@@ -327,7 +328,7 @@ export function mapToolChoiceToOpenRouter(
327
328
  * Maps a canonical ThinkingLevel onto OpenAI Responses `reasoning.effort`.
328
329
  * Documented efforts: none|minimal|low|medium|high|xhigh|max (server default
329
330
  * medium). Unlike OpenRouter (which clamps xhigh), OpenAI documents xhigh
330
- * natively so it passes through verbatim. `dynamic` omits (server default).
331
+ * and max natively so they pass through verbatim. `dynamic` omits (server default).
331
332
  */
332
333
  export function mapThinkingLevelToOpenAI(
333
334
  level: ThinkingLevel | string | undefined
@@ -341,7 +342,8 @@ export function mapThinkingLevelToOpenAI(
341
342
  norm === "low" ||
342
343
  norm === "medium" ||
343
344
  norm === "high" ||
344
- norm === "xhigh"
345
+ norm === "xhigh" ||
346
+ norm === "max"
345
347
  ) {
346
348
  return { effort: norm };
347
349
  }