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.
Files changed (48) hide show
  1. package/README.md +174 -123
  2. package/{SYSTEM_PROMPT_AGENT.md → examples/prompts/SYSTEM_PROMPT_AGENT.md} +1 -1
  3. package/package.json +14 -9
  4. package/src/agent/agent.ts +385 -73
  5. package/src/agent/context.ts +1 -0
  6. package/src/agent/delegation.ts +443 -30
  7. package/src/agent/loop.ts +183 -53
  8. package/src/data/README.md +6 -6
  9. package/src/index.ts +163 -46
  10. package/src/models/catalog-cache.ts +60 -9
  11. package/src/models/catalog.ts +55 -8
  12. package/src/providers/google.ts +926 -0
  13. package/src/providers/openai-compat.ts +1147 -0
  14. package/src/providers/openai.ts +959 -0
  15. package/src/providers/openrouter-responses.ts +949 -0
  16. package/src/providers/openrouter.ts +1037 -0
  17. package/src/{ai-sdk → providers}/registry.ts +43 -55
  18. package/src/providers.ts +492 -0
  19. package/src/session/store.ts +806 -0
  20. package/src/streaming/sse-parser.ts +6 -4
  21. package/src/tools/executor.ts +23 -7
  22. package/src/tools/schema.ts +21 -11
  23. package/src/types/agent.ts +33 -2
  24. package/src/types/core.ts +3 -2
  25. package/src/types/message.ts +5 -0
  26. package/src/types/model.ts +3 -7
  27. package/src/types/provider-payloads.ts +2 -84
  28. package/src/types/response.ts +24 -0
  29. package/src/types/tool.ts +8 -0
  30. package/src/update-models.ts +56 -0
  31. package/src/utils/cache.ts +1 -1
  32. package/src/utils/documents.ts +517 -0
  33. package/src/utils/env.ts +0 -7
  34. package/src/{ai-sdk → utils}/errors.ts +61 -2
  35. package/src/utils/headers.ts +10 -20
  36. package/src/utils/media.ts +5 -2
  37. package/src/utils/retry.ts +89 -0
  38. package/src/utils/serialization.ts +15 -0
  39. package/src/utils/session.ts +36 -0
  40. package/src/ai-sdk/converters.ts +0 -342
  41. package/src/ai-sdk/executor.ts +0 -454
  42. package/src/ai-sdk/index.ts +0 -55
  43. package/src/ai-sdk/model-provider.ts +0 -303
  44. package/src/ai-sdk/options.ts +0 -306
  45. package/src/ai-sdk/provider.ts +0 -415
  46. package/src/tokens/counter.ts +0 -136
  47. /package/{SYSTEM_PROMPT.md → examples/prompts/SYSTEM_PROMPT.md} +0 -0
  48. /package/{SYSTEM_PROMPT_TOOLS.md → examples/prompts/SYSTEM_PROMPT_TOOLS.md} +0 -0
@@ -56,8 +56,9 @@ export class SSEParser {
56
56
  // Comment / ping
57
57
  continue;
58
58
  } else if (line.startsWith("data:")) {
59
- const val = line.slice(5);
60
- this.currentData.push(val.startsWith(" ") ? val.slice(1) : val.trimStart());
59
+ // Per WHATWG SSE, strip exactly one leading space — never tabs or
60
+ // further whitespace, so indented code/JSON payloads survive intact.
61
+ this.currentData.push(line.startsWith("data: ") ? line.slice(6) : line.slice(5));
61
62
  } else if (line.startsWith("event:")) {
62
63
  const val = line.slice(6);
63
64
  this.currentEvent = (val.startsWith(" ") ? val.slice(1) : val).trim();
@@ -76,8 +77,9 @@ export class SSEParser {
76
77
  const messages: SSEMessage[] = [];
77
78
  // S7: flush pending currentData first, then treat remaining buffer as final lines (avoid duplication)
78
79
  if (this.buffer.trim() !== "") {
79
- // Feed remaining buffer as if terminated
80
- const pending = this.feed(this.buffer + "\n\n");
80
+ // Terminate the leftover buffer in place: feed() appends its argument
81
+ // to this.buffer, so re-feeding the buffer itself would duplicate it.
82
+ const pending = this.feed("\n\n");
81
83
  messages.push(...pending);
82
84
  this.buffer = "";
83
85
  }
@@ -13,6 +13,8 @@ export interface ExecuteToolsOptions {
13
13
  parallel?: boolean;
14
14
  signal?: AbortSignal;
15
15
  sessionId?: string;
16
+ /** Forwards live sub-agent deltas (realtime worker thinking/text) to the caller. */
17
+ onSubagentEvent?: (event: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
16
18
  }
17
19
 
18
20
  const DEFAULT_TOOL_CONCURRENCY = 8;
@@ -36,7 +38,8 @@ function integerOption(value: number | string | undefined): number | undefined {
36
38
  return parsed === undefined ? undefined : Math.floor(parsed);
37
39
  }
38
40
 
39
- function normalizeToolName(name: string): string {
41
+ /** Normalizes a tool name for case/format-insensitive matching (shared with delegation grants). */
42
+ export function normalizeToolName(name: string): string {
40
43
  return name
41
44
  .trim()
42
45
  .replace(/^(?:functions?|tools?)\./i, "")
@@ -99,6 +102,7 @@ class Semaphore {
99
102
  resolve: (release: () => void) => void;
100
103
  reject: (error: Error) => void;
101
104
  signal?: AbortSignal;
105
+ onAbort?: () => void;
102
106
  }> = [];
103
107
 
104
108
  constructor(private readonly limit: number) {}
@@ -110,13 +114,19 @@ class Semaphore {
110
114
  return Promise.resolve(() => this.release());
111
115
  }
112
116
  return new Promise((resolve, reject) => {
113
- const waiter = { resolve, reject, signal };
114
- this.waiters.push(waiter);
117
+ const waiter: {
118
+ resolve: (release: () => void) => void;
119
+ reject: (error: Error) => void;
120
+ signal?: AbortSignal;
121
+ onAbort?: () => void;
122
+ } = { resolve, reject, signal };
115
123
  const onAbort = () => {
116
124
  const index = this.waiters.indexOf(waiter);
117
125
  if (index >= 0) this.waiters.splice(index, 1);
118
126
  reject(new Error("Tool execution aborted"));
119
127
  };
128
+ waiter.onAbort = onAbort;
129
+ this.waiters.push(waiter);
120
130
  signal?.addEventListener("abort", onAbort, { once: true });
121
131
  });
122
132
  }
@@ -130,6 +140,9 @@ class Semaphore {
130
140
  continue;
131
141
  }
132
142
  this.active++;
143
+ // The waiter is leaving the queue: drop its abort listener so long-lived
144
+ // session signals don't accumulate one listener per queued tool call.
145
+ if (waiter.onAbort) waiter.signal?.removeEventListener("abort", waiter.onAbort);
133
146
  waiter.resolve(() => this.release());
134
147
  return;
135
148
  }
@@ -222,7 +235,7 @@ async function executeAttempt(
222
235
  export async function executeToolCalls(
223
236
  options: ExecuteToolsOptions
224
237
  ): Promise<ToolResultRecord[]> {
225
- const { tools, toolCalls, agentName, parallel = true, signal, sessionId } = options;
238
+ const { tools, toolCalls, agentName, parallel = true, signal, sessionId, onSubagentEvent } = options;
226
239
  const globalSemaphore = new Semaphore(DEFAULT_TOOL_CONCURRENCY);
227
240
  const perToolSemaphores = new Map<string, Semaphore>();
228
241
 
@@ -271,6 +284,7 @@ export async function executeToolCalls(
271
284
  agentName,
272
285
  signal,
273
286
  sessionId,
287
+ onSubagentEvent,
274
288
  };
275
289
 
276
290
  // Validate input if Zod schema is provided
@@ -306,9 +320,11 @@ export async function executeToolCalls(
306
320
  }
307
321
 
308
322
  const configuredTries = integerOption(toolDef.maxTries);
309
- // A positive value is the total attempt count. 0/omitted deliberately
310
- // means there is no configured retry limit for transient failures.
311
- const maxAttempts = configuredTries && configuredTries > 0 ? configuredTries : Number.POSITIVE_INFINITY;
323
+ // A positive value is the total attempt count. 0/omitted falls back to
324
+ // a bounded default: an unbounded retry loop on a persistently failing
325
+ // dependency (e.g. steady 503/429) would hang the agent loop forever,
326
+ // so callers opt into more attempts explicitly via maxTries.
327
+ const maxAttempts = configuredTries && configuredTries > 0 ? configuredTries : 3;
312
328
  const timeoutMs = numericOption(toolDef.timeoutMs);
313
329
  // Telemetry starts when the tool body is about to run, excluding queue
314
330
  // wait and schema validation. Retries/backoff remain part of this call.
@@ -65,7 +65,7 @@ export function zodToJsonSchema(schema: unknown): Record<string, unknown> {
65
65
  *
66
66
  * @example `const clean = cleanJsonSchema(rawSchema);`
67
67
  */
68
- export function cleanJsonSchema(schema: any, rootDefs?: Record<string, any>): Record<string, unknown> {
68
+ export function cleanJsonSchema(schema: any, rootDefs?: Record<string, any>, seenRefs: Set<string> = new Set()): Record<string, unknown> {
69
69
  if (typeof schema !== "object" || schema === null) {
70
70
  return schema;
71
71
  }
@@ -78,8 +78,18 @@ export function cleanJsonSchema(schema: any, rootDefs?: Record<string, any>): Re
78
78
  if (target) {
79
79
  // Merge sibling props (e.g. description) with target
80
80
  const { $ref, ...siblings } = schema;
81
- const resolved = cleanJsonSchema(target, defs);
82
- return { ...resolved, ...cleanJsonSchema(siblings, defs) } as any;
81
+ if (seenRefs.has(refName)) {
82
+ // Recursive schema (AST nodes, trees, nested categories): stop
83
+ // expanding to avoid overflowing the stack; keep siblings.
84
+ return { type: "object", ...cleanJsonSchema(siblings, defs, seenRefs) } as any;
85
+ }
86
+ seenRefs.add(refName);
87
+ try {
88
+ const resolved = cleanJsonSchema(target, defs, seenRefs);
89
+ return { ...resolved, ...cleanJsonSchema(siblings, defs, seenRefs) } as any;
90
+ } finally {
91
+ seenRefs.delete(refName);
92
+ }
83
93
  }
84
94
  }
85
95
  const { $schema, $defs, definitions, ...rest } = schema;
@@ -94,22 +104,22 @@ export function cleanJsonSchema(schema: any, rootDefs?: Record<string, any>): Re
94
104
  if (rest.properties && typeof rest.properties === "object") {
95
105
  const cleanedProps: Record<string, unknown> = {};
96
106
  for (const [key, value] of Object.entries(rest.properties)) {
97
- cleanedProps[key] = cleanJsonSchema(value, defs);
107
+ cleanedProps[key] = cleanJsonSchema(value, defs, seenRefs);
98
108
  }
99
109
  rest.properties = cleanedProps;
100
110
  }
101
111
 
102
112
  if (rest.items) {
103
- rest.items = cleanJsonSchema(rest.items, defs);
113
+ rest.items = cleanJsonSchema(rest.items, defs, seenRefs);
104
114
  }
105
- if (rest.anyOf) rest.anyOf = (rest.anyOf as any[]).map((v: any) => cleanJsonSchema(v, defs));
106
- if (rest.oneOf) rest.oneOf = (rest.oneOf as any[]).map((v: any) => cleanJsonSchema(v, defs));
107
- if (rest.allOf) rest.allOf = (rest.allOf as any[]).map((v: any) => cleanJsonSchema(v, defs));
108
- if (rest.prefixItems) rest.prefixItems = (rest.prefixItems as any[]).map((v: any) => cleanJsonSchema(v, defs));
115
+ if (rest.anyOf) rest.anyOf = (rest.anyOf as any[]).map((v: any) => cleanJsonSchema(v, defs, seenRefs));
116
+ if (rest.oneOf) rest.oneOf = (rest.oneOf as any[]).map((v: any) => cleanJsonSchema(v, defs, seenRefs));
117
+ if (rest.allOf) rest.allOf = (rest.allOf as any[]).map((v: any) => cleanJsonSchema(v, defs, seenRefs));
118
+ if (rest.prefixItems) rest.prefixItems = (rest.prefixItems as any[]).map((v: any) => cleanJsonSchema(v, defs, seenRefs));
109
119
  // Recursively clean nested $ref inside properties that were not top-level
110
120
  for (const k of Object.keys(rest)) {
111
121
  if (rest[k] && typeof rest[k] === "object" && !Array.isArray(rest[k]) && (rest[k] as any).$ref) {
112
- rest[k] = cleanJsonSchema(rest[k], defs);
122
+ rest[k] = cleanJsonSchema(rest[k], defs, seenRefs);
113
123
  }
114
124
  }
115
125
  return rest;
@@ -167,7 +177,7 @@ function inferZodPropertyType(prop: any): Record<string, unknown> {
167
177
  }
168
178
  if (tn.includes("literal")) {
169
179
  const val = unwrapped._def?.value;
170
- return { type: typeof val, enum: [val], ...(description ? { description } : {}) };
180
+ return { type: val === null ? "null" : typeof val, enum: [val], ...(description ? { description } : {}) };
171
181
  }
172
182
  if (tn.includes("array") || tn === "zodarray") {
173
183
  const elem = unwrapped._def?.type || unwrapped._def?.element || unwrapped._def?.valueType || {};
@@ -5,7 +5,7 @@ import type {
5
5
  CacheConfig,
6
6
  ServiceTier,
7
7
  } from "./core.ts";
8
- import type { ModelProviderInstance } from "../ai-sdk/registry.ts";
8
+ import type { ModelProviderInstance } from "../providers/registry.ts";
9
9
  import type { Agent } from "../agent/agent.ts";
10
10
 
11
11
  /** Configuration used to construct an {@link Agent}. */
@@ -34,16 +34,31 @@ export interface AgentConfig {
34
34
  cache?: CacheConfig;
35
35
  /** Provider service tier, when supported. */
36
36
  serviceTier?: ServiceTier;
37
+ /**
38
+ * When true, `file` parts are converted client-side to `<Document>` Markdown
39
+ * for models lacking native support (capable models still receive files
40
+ * natively; unknown models count as capable). Also auto-registers the
41
+ * `convert_document_to_markdown` tool for path/URL mentions in plain text.
42
+ */
43
+ bypassInputFileModality?: boolean;
37
44
  /** Stable conversation/cache session ID. */
38
45
  sessionId?: string;
39
46
  /** Headers merged into every provider request. */
40
47
  headers?: Record<string, string>;
41
- /** Maximum model/tool turns per run. Defaults to 10. */
48
+ /** Maximum model/tool turns per run. `0` or omitted means infinite. */
42
49
  maxTurns?: number;
43
50
  /** Fixed worker agents exposed as delegation tools. */
44
51
  subagents?: (Agent | { name: string; description: string; agent: Agent })[];
45
52
  /** Clears conversation state before and after every run. */
46
53
  stateless?: boolean;
54
+ /**
55
+ * Durable session persistence: when set, the session file is rewritten
56
+ * after every step (user turn, assistant turn, tool batch, sub-agent
57
+ * step) so a crash loses at most the in-flight step. Pass `{ dir }` for
58
+ * `sessions/<sessionId>/session.json` layout or `{ file }` for a single
59
+ * pretty `.session.json` file. Writes are atomic and never fail a turn.
60
+ */
61
+ persist?: { dir?: string; file?: string };
47
62
  }
48
63
 
49
64
  /** Developer-configured policy for LLM-spawned dynamic sub-agents. */
@@ -84,4 +99,20 @@ export interface AgentRunOptions {
84
99
  onEvent?: (event: import("./response.ts").StreamEvent) => void;
85
100
  /** When true, wraps reasoning stream as <think>\n...\n</think>\n\n — no manual isThinking needed. */
86
101
  wrapThinking?: boolean;
102
+ /** Called after every model/tool turn with a turn summary (also drives sub-agent step logs + durable persistence). */
103
+ onTurn?: (turn: AgentTurnEvent) => void;
104
+ }
105
+
106
+ /** Turn summary emitted to `AgentRunOptions.onTurn` after each loop turn. */
107
+ export interface AgentTurnEvent {
108
+ /** 1-based turn count within this run. */
109
+ turns: number;
110
+ /** Assistant text produced this turn, if any. */
111
+ text?: string;
112
+ /** Reasoning produced this turn, if any. */
113
+ thinking?: string;
114
+ /** Tool calls requested this turn, if any. */
115
+ toolCalls?: Array<{ id: string; name: string }>;
116
+ /** Tool results executed this turn, if any. */
117
+ toolResults?: Array<{ id: string; name: string; isError?: boolean }>;
87
118
  }
package/src/types/core.ts CHANGED
@@ -9,7 +9,8 @@ export type ThinkingLevel =
9
9
  | "low"
10
10
  | "medium"
11
11
  | "high"
12
- | "xhigh";
12
+ | "xhigh"
13
+ | "max";
13
14
 
14
15
  // Internal normalized config (SDK-internal, not exposed as Agent flag)
15
16
  /** Internal normalized reasoning configuration passed to providers. */
@@ -49,7 +50,7 @@ export interface CacheConfig {
49
50
  */
50
51
  cachedContentId?: string;
51
52
  /**
52
- * Session ID for cache affinity routing (e.g. x-session-id, x-opencode-session)
53
+ * Session ID for cache affinity routing (e.g. x-session-id)
53
54
  */
54
55
  sessionId?: string;
55
56
  /**
@@ -64,6 +64,11 @@ export interface ToolCallPart {
64
64
  * Optional Gemini thought signature associated with this tool call
65
65
  */
66
66
  thoughtSignature?: string;
67
+ /**
68
+ * Provider pairing identifier (e.g. Responses `call_id`), preserved so
69
+ * history builders can pair results without inventing ids.
70
+ */
71
+ callId?: string;
67
72
  }
68
73
 
69
74
  /** Tool execution result returned to the model context. */
@@ -5,10 +5,10 @@ import type { AssistantMessageEventStream } from "../streaming/event-stream.ts";
5
5
 
6
6
  // ---------------------------------------------------------------------------
7
7
  // Provider identity — battle-tested: matches models.dev provider keys
8
- // Known first-class: google | opencode | openrouter, plus aliases. Allow string for future.
8
+ // Known first-class: google | openrouter | openai, plus aliases. Allow string for future.
9
9
  // ---------------------------------------------------------------------------
10
10
  /** Supported provider IDs plus arbitrary custom prefixes. */
11
- export type ProviderId = "google" | "opencode" | "opencode-go" | "openrouter" | "openai" | (string & {});
11
+ export type ProviderId = "google" | "openrouter" | "openai" | (string & {});
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
14
  // Raw models.dev shapes — battle-tested, 1:1 with https://models.dev/api.json
@@ -189,7 +189,7 @@ export interface ProviderGenerateResult {
189
189
  durationMs: number;
190
190
  }
191
191
 
192
- /** Provider contract implemented by the AI-SDK transport layer. */
192
+ /** Provider contract implemented by native REST transports. */
193
193
  export interface Provider {
194
194
  id: ProviderId;
195
195
  name: string;
@@ -205,8 +205,4 @@ export interface Provider {
205
205
  context: ProviderContext,
206
206
  options?: ProviderRequestOptions
207
207
  ): AssistantMessageEventStream;
208
- countTokens(
209
- model: string | ModelSpec,
210
- context: ProviderContext
211
- ): Promise<number>;
212
208
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Provider wire payload and schema types for complete backwards compatibility.
3
- * All providers use Vercel AI SDK under the hood, but these types allow existing
4
- * applications and tests to type-check requests and responses seamlessly.
3
+ * These types allow existing applications and tests to type-check requests
4
+ * and responses seamlessly.
5
5
  */
6
6
 
7
7
  // ============================================================================
@@ -105,88 +105,6 @@ export interface GoogleGenerateContentResponse {
105
105
  modelVersion?: string;
106
106
  }
107
107
 
108
- // ============================================================================
109
- // OpenCode Payload Types
110
- // ============================================================================
111
-
112
- export type OpenCodeReasoningEffort = "low" | "medium" | "high";
113
- export type OpenCodePromptCacheRetention = "none" | "1h" | "24h";
114
-
115
- export interface OpenCodeCacheControl {
116
- type: "ephemeral";
117
- ttl?: "1h";
118
- }
119
-
120
- export interface OpenCodeUsage {
121
- prompt_tokens?: number;
122
- completion_tokens?: number;
123
- total_tokens?: number;
124
- prompt_tokens_details?: {
125
- cached_tokens?: number;
126
- cache_read?: number;
127
- cache_creation?: number;
128
- };
129
- }
130
-
131
- export interface OpenCodeChatRequest {
132
- model: string;
133
- messages: any[];
134
- tools?: any[];
135
- tool_choice?: any;
136
- temperature?: number;
137
- top_p?: number;
138
- max_tokens?: number;
139
- stop?: string | string[];
140
- stream?: boolean;
141
- reasoning_effort?: OpenCodeReasoningEffort;
142
- prompt_cache_retention?: OpenCodePromptCacheRetention;
143
- session_id?: string;
144
- }
145
-
146
- export interface OpenCodeChatResponse {
147
- id?: string;
148
- object?: string;
149
- created?: number;
150
- model?: string;
151
- choices?: Array<{
152
- index?: number;
153
- message?: any;
154
- delta?: any;
155
- finish_reason?: string;
156
- }>;
157
- usage?: OpenCodeUsage;
158
- }
159
-
160
- export interface OpenCodeResponsesInputItem {
161
- role: string;
162
- content: any;
163
- cache_control?: OpenCodeCacheControl;
164
- }
165
-
166
- export interface OpenCodeResponsesOutputItem {
167
- id?: string;
168
- type?: string;
169
- status?: string;
170
- content?: any;
171
- name?: string;
172
- call_id?: string;
173
- arguments?: string;
174
- }
175
-
176
- export interface OpenCodeResponsesRequest {
177
- model: string;
178
- input: OpenCodeResponsesInputItem[];
179
- tools?: any[];
180
- parameters?: Record<string, unknown>;
181
- session_id?: string;
182
- }
183
-
184
- export interface OpenCodeResponsesResponse {
185
- id?: string;
186
- output?: OpenCodeResponsesOutputItem[];
187
- usage?: OpenCodeUsage;
188
- }
189
-
190
108
  // ============================================================================
191
109
  // OpenRouter Payload Types
192
110
  // ============================================================================
@@ -2,6 +2,19 @@ import type { TokenUsage } from "./core.ts";
2
2
  import type { ToolCallRecord, ToolResultRecord } from "./tool.ts";
3
3
  import type { ProviderRawData, ProviderId } from "./model.ts";
4
4
 
5
+ /** One logged step inside a sub-agent worker run (surfaced via `agent.track(id)`). */
6
+ export interface SubAgentStep {
7
+ turn: number;
8
+ type: "assistant" | "tool_call" | "tool_result";
9
+ name?: string;
10
+ text?: string;
11
+ thinking?: string;
12
+ isError?: boolean;
13
+ timestamp: number;
14
+ /** True while the worker is still streaming this step; finalized on turn end. */
15
+ partial?: boolean;
16
+ }
17
+
5
18
  /** Per-worker result and usage metadata attached to an AgentResponse. */
6
19
  export interface SubAgentExecutionMetadata {
7
20
  name: string;
@@ -20,6 +33,14 @@ export interface SubAgentExecutionMetadata {
20
33
  raw?: ProviderRawData;
21
34
  isError?: boolean;
22
35
  error?: string;
36
+ /** Stable display id: `SUBAGENT-NAME-{TrackingID}` (32-char hex, see `createTrackingId`). */
37
+ trackingId?: string;
38
+ /** Provider session id used by this worker (≤64 chars, affinity-safe). */
39
+ sessionId?: string;
40
+ /** Parent session this worker was spawned from. */
41
+ parentSessionId?: string;
42
+ /** Ordered step log for this worker (also persisted in the session file). */
43
+ steps?: SubAgentStep[];
23
44
  }
24
45
 
25
46
  /** JSON representation returned by AgentResponse.toJSON(). */
@@ -136,6 +157,7 @@ export type StreamEventType =
136
157
  | "tool_call_complete"
137
158
  | "tool_result"
138
159
  | "subagent_complete"
160
+ | "subagent_delta"
139
161
  | "usage"
140
162
  | "done"
141
163
  | "error";
@@ -148,6 +170,8 @@ export interface StreamEvent {
148
170
  toolCall?: ToolCallRecord;
149
171
  toolResult?: ToolResultRecord;
150
172
  subagent?: SubAgentExecutionMetadata;
173
+ /** Tracking id of the worker emitting a `subagent_delta` event. */
174
+ subagentTrackingId?: string;
151
175
  usage?: TokenUsage;
152
176
  responseId?: string;
153
177
  finishReason?: string;
package/src/types/tool.ts CHANGED
@@ -9,6 +9,8 @@ export interface ToolExecutionContext {
9
9
  signal?: AbortSignal;
10
10
  /** Session identifier associated with this invocation. */
11
11
  sessionId?: string;
12
+ /** Forwards live sub-agent deltas (used by `spawn_subagents` for realtime tracking). */
13
+ onSubagentEvent?: (event: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
12
14
  }
13
15
 
14
16
  export type ToolExecuteFn<TInput = any, TOutput = any> = (
@@ -50,6 +52,12 @@ export interface ToolCallRecord {
50
52
  arguments: Record<string, unknown>;
51
53
  rawArguments?: string;
52
54
  thoughtSignature?: string;
55
+ /**
56
+ * Provider pairing identifier, distinct from the item `id` when the
57
+ * provider uses both (e.g. Responses `call_id` alongside the `fc_…` item
58
+ * id). Tool results pair against this when present.
59
+ */
60
+ callId?: string;
53
61
  }
54
62
 
55
63
  export interface ToolResultRecord {
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Developer script to manually download and refresh the models.dev catalog.
4
+ *
5
+ * Usage:
6
+ * bun src/update-models.ts
7
+ * bun src/update-models.ts --force
8
+ * bun src/update-models.ts --ttl=24h
9
+ */
10
+
11
+ import { refreshModelCatalog, getCatalogStatus } from "./index.ts";
12
+
13
+ const args = process.argv.slice(2);
14
+ const force = args.includes("--force") || !args.some((a) => a.startsWith("--ttl"));
15
+
16
+ let customTtlMs: number | undefined;
17
+ for (const arg of args) {
18
+ if (arg.startsWith("--ttl=")) {
19
+ const val = arg.slice(6);
20
+ if (val.endsWith("h")) {
21
+ customTtlMs = parseFloat(val) * 60 * 60 * 1000;
22
+ } else if (val.endsWith("m")) {
23
+ customTtlMs = parseFloat(val) * 60 * 1000;
24
+ } else {
25
+ customTtlMs = parseFloat(val);
26
+ }
27
+ }
28
+ }
29
+
30
+ console.log("\x1b[36m[Agent Accelerator]\x1b[0m Checking models catalog from https://models.dev/api.json...");
31
+ const startTime = Date.now();
32
+
33
+ try {
34
+ const result = await refreshModelCatalog({ force, ttlMs: customTtlMs });
35
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
36
+ const ttlHours = (result.ttlMs / (1000 * 60 * 60)).toFixed(1);
37
+
38
+ if (result.fromCache) {
39
+ console.log(
40
+ `\x1b[32m✓\x1b[0m Models catalog is already fresh (within ${ttlHours}h TTL).`
41
+ );
42
+ } else {
43
+ console.log(
44
+ `\x1b[32m✓\x1b[0m Successfully downloaded and cached \x1b[1m${result.modelCount.toLocaleString()}\x1b[0m models ` +
45
+ `across \x1b[1m${result.providerCount}\x1b[0m providers in ${duration}s.`
46
+ );
47
+ }
48
+ console.log(` \x1b[90mCache location: ${result.cachePath}\x1b[0m`);
49
+ console.log(` \x1b[90mTTL: ${ttlHours} hours\x1b[0m`);
50
+ if (result.expiresAt) {
51
+ console.log(` \x1b[90mNext refresh after: ${new Date(result.expiresAt).toLocaleString()}\x1b[0m`);
52
+ }
53
+ } catch (error: any) {
54
+ console.error(`\x1b[31m✖ Failed to refresh model catalog:\x1b[0m`, error.message || error);
55
+ process.exit(1);
56
+ }
@@ -31,7 +31,7 @@ export function retentionToTtlSeconds(retention?: CacheRetention, ttlSeconds?: n
31
31
  return undefined;
32
32
  }
33
33
 
34
- /** Maps retention settings to OpenCode/OpenRouter prompt-cache TTL values. */
34
+ /** Maps retention settings to OpenRouter prompt-cache TTL values. */
35
35
  export function getPromptCacheRetention(retention?: CacheRetention, supportsLong = true): "24h" | "1h" | undefined {
36
36
  if (!retention || retention === "implicit") return undefined;
37
37
  if (retention === "long" && supportsLong) return "24h";