agent-accelerator 0.5.9 → 0.6.5

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 (85) hide show
  1. package/README.md +64 -16
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +447 -172
  7. package/src/agent/architecture.md +16 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +9 -3
  10. package/src/agent/defaults.ts +88 -0
  11. package/src/agent/delegation.ts +86 -28
  12. package/src/agent/loop.ts +474 -374
  13. package/src/agent/subagent.ts +1 -3
  14. package/src/agent/subagents-tool.ts +50 -19
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/usage-limit.ts +92 -0
  17. package/src/agent/worker.ts +73 -30
  18. package/src/agent/workers.ts +126 -21
  19. package/src/architecture.md +17 -15
  20. package/src/index.ts +43 -19
  21. package/src/models/architecture.md +4 -3
  22. package/src/models/catalog-cache.ts +6 -5
  23. package/src/models/catalog.ts +39 -30
  24. package/src/prompts/additional-context.md +2 -0
  25. package/src/prompts/architecture.md +19 -0
  26. package/src/prompts/prompts.ts +81 -0
  27. package/src/prompts/reasoning-directive.md +5 -0
  28. package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
  29. package/src/prompts/structured-output.md +2 -0
  30. package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
  31. package/src/prompts/tool-bridge.md +1 -0
  32. package/src/prompts/tool-interrupted.md +1 -0
  33. package/src/prompts/tool-not-found.md +1 -0
  34. package/src/prompts/tool-repeat-blocked.md +1 -0
  35. package/src/prompts/tool-schema-error.md +1 -0
  36. package/src/prompts/tool-truncated-arguments.md +1 -0
  37. package/src/prompts/worker-failed.md +1 -0
  38. package/src/prompts/worker-replied.md +2 -0
  39. package/src/providers/architecture.md +10 -8
  40. package/src/providers/google.ts +228 -69
  41. package/src/providers/openai-compat.ts +74 -56
  42. package/src/providers/openai.ts +64 -41
  43. package/src/providers/openrouter.ts +270 -48
  44. package/src/providers/registry.ts +6 -21
  45. package/src/providers/shared.ts +87 -33
  46. package/src/providers.ts +115 -124
  47. package/src/scheduler/architecture.md +8 -7
  48. package/src/scheduler/scheduler.ts +226 -60
  49. package/src/scheduler/store.ts +23 -10
  50. package/src/scheduler/time.ts +17 -3
  51. package/src/scheduler/tool.ts +38 -16
  52. package/src/session/architecture.md +7 -6
  53. package/src/session/journal.ts +39 -12
  54. package/src/session/store.ts +126 -61
  55. package/src/session/usage.ts +58 -9
  56. package/src/streaming/architecture.md +4 -3
  57. package/src/streaming/event-stream.ts +26 -12
  58. package/src/telemetry.ts +6 -2
  59. package/src/tools/architecture.md +9 -7
  60. package/src/tools/executor.ts +109 -62
  61. package/src/tools/schema.ts +10 -6
  62. package/src/tools/subagent-result.ts +6 -1
  63. package/src/tools/tool-content.ts +32 -0
  64. package/src/tools/tool.ts +9 -11
  65. package/src/types/agent.ts +37 -22
  66. package/src/types/architecture.md +11 -10
  67. package/src/types/core.ts +5 -9
  68. package/src/types/errors.ts +26 -4
  69. package/src/types/message.ts +7 -0
  70. package/src/types/model.ts +4 -5
  71. package/src/types/scheduler.ts +4 -4
  72. package/src/types/tool.ts +14 -2
  73. package/src/update-models.ts +5 -5
  74. package/src/utils/architecture.md +16 -15
  75. package/src/utils/cache.ts +1 -3
  76. package/src/utils/durable.ts +17 -5
  77. package/src/utils/env.ts +9 -7
  78. package/src/utils/errors.ts +9 -17
  79. package/src/utils/headers.ts +3 -10
  80. package/src/utils/media.ts +23 -8
  81. package/src/utils/raw-capture.ts +9 -2
  82. package/src/utils/retry.ts +15 -3
  83. package/src/utils/safe-io.ts +29 -7
  84. package/src/utils/serialization.ts +1 -4
  85. package/src/utils/session.ts +1 -2
@@ -72,8 +72,17 @@ export function addTurn(totals: UsageTotals, usage: TokenUsage, priced: PricedCo
72
72
 
73
73
  /** Adds `other` into `into` (workers into a total). */
74
74
  export function mergeUsage(into: UsageTotals, other: UsageTotals): void {
75
- for (const k of ["turns", "inputTokens", "cacheReadTokens", "cacheWriteTokens", "outputTokens", "thinkingTokens"] as const) into[k] += other[k];
76
- for (const k of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) into.costMicroUSD[k] += other.costMicroUSD[k];
75
+ for (const k of [
76
+ "turns",
77
+ "inputTokens",
78
+ "cacheReadTokens",
79
+ "cacheWriteTokens",
80
+ "outputTokens",
81
+ "thinkingTokens",
82
+ ] as const)
83
+ into[k] += other[k];
84
+ for (const k of ["input", "output", "cacheRead", "cacheWrite", "total"] as const)
85
+ into.costMicroUSD[k] += other.costMicroUSD[k];
77
86
  for (const k of ["provider", "catalog", "unpriced"] as const) into.turnsBy[k] += other.turnsBy[k];
78
87
  }
79
88
 
@@ -82,8 +91,17 @@ export function readUsage(saved: unknown): UsageTotals {
82
91
  const out = emptyUsage();
83
92
  const s = saved as Partial<UsageTotals> | null | undefined;
84
93
  if (!s || typeof s !== "object") return out;
85
- for (const k of ["turns", "inputTokens", "cacheReadTokens", "cacheWriteTokens", "outputTokens", "thinkingTokens"] as const) out[k] = count(s[k]);
86
- for (const k of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) out.costMicroUSD[k] = count(s.costMicroUSD?.[k]);
94
+ for (const k of [
95
+ "turns",
96
+ "inputTokens",
97
+ "cacheReadTokens",
98
+ "cacheWriteTokens",
99
+ "outputTokens",
100
+ "thinkingTokens",
101
+ ] as const)
102
+ out[k] = count(s[k]);
103
+ for (const k of ["input", "output", "cacheRead", "cacheWrite", "total"] as const)
104
+ out.costMicroUSD[k] = count(s.costMicroUSD?.[k]);
87
105
  for (const k of ["provider", "catalog", "unpriced"] as const) out.turnsBy[k] = count(s.turnsBy?.[k]);
88
106
  return out;
89
107
  }
@@ -124,7 +142,14 @@ export interface UsageLine {
124
142
  /** Turns raw totals into a readable {@link UsageLine}. */
125
143
  export function usageLine(t: UsageTotals): UsageLine {
126
144
  const priced = t.turnsBy.provider + t.turnsBy.catalog;
127
- const source: CostSource = priced === 0 ? "unknown" : t.turnsBy.provider > 0 && t.turnsBy.catalog > 0 ? "mixed" : t.turnsBy.provider > 0 ? "provider" : "catalog";
145
+ const source: CostSource =
146
+ priced === 0
147
+ ? "unknown"
148
+ : t.turnsBy.provider > 0 && t.turnsBy.catalog > 0
149
+ ? "mixed"
150
+ : t.turnsBy.provider > 0
151
+ ? "provider"
152
+ : "catalog";
128
153
  const usd = (micro: number): number => micro / 1_000_000;
129
154
  const c = t.costMicroUSD;
130
155
  return {
@@ -143,7 +168,16 @@ export function usageLine(t: UsageTotals): UsageLine {
143
168
  source,
144
169
  unpricedTurns: t.turnsBy.unpriced,
145
170
  totalUsd: priced === 0 ? null : usd(c.total),
146
- ...(priced === 0 ? {} : { parts: { inputUsd: usd(c.input), outputUsd: usd(c.output), cacheReadUsd: usd(c.cacheRead), cacheWriteUsd: usd(c.cacheWrite) } }),
171
+ ...(priced === 0
172
+ ? {}
173
+ : {
174
+ parts: {
175
+ inputUsd: usd(c.input),
176
+ outputUsd: usd(c.output),
177
+ cacheReadUsd: usd(c.cacheRead),
178
+ cacheWriteUsd: usd(c.cacheWrite),
179
+ },
180
+ }),
147
181
  },
148
182
  };
149
183
  }
@@ -190,13 +224,28 @@ function sumTree(into: UsageTotals, nodes: UsageNode[]): void {
190
224
 
191
225
  // One node as a report line, children included; in: a usage node, out: UsageReport.workers.
192
226
  function nodeLine(n: UsageNode): WorkerUsage {
193
- return { id: n.id, name: n.name, ...(n.role ? { role: n.role } : {}), model: n.model, status: n.status, ...usageLine(n.totals), workers: n.workers.map(nodeLine) };
227
+ return {
228
+ id: n.id,
229
+ name: n.name,
230
+ ...(n.role ? { role: n.role } : {}),
231
+ model: n.model,
232
+ status: n.status,
233
+ ...usageLine(n.totals),
234
+ workers: n.workers.map(nodeLine),
235
+ };
194
236
  }
195
237
 
196
238
  /** Builds the report from the agent's own totals and its worker tree. */
197
- export function buildUsageReport(own: { id: string; name: string; model: string; totals: UsageTotals }, workers: UsageNode[]): UsageReport {
239
+ export function buildUsageReport(
240
+ own: { id: string; name: string; model: string; totals: UsageTotals },
241
+ workers: UsageNode[]
242
+ ): UsageReport {
198
243
  const total = emptyUsage();
199
244
  mergeUsage(total, own.totals);
200
245
  sumTree(total, workers);
201
- return { agent: { id: own.id, name: own.name, model: own.model, ...usageLine(own.totals) }, workers: workers.map(nodeLine), total: usageLine(total) };
246
+ return {
247
+ agent: { id: own.id, name: own.name, model: own.model, ...usageLine(own.totals) },
248
+ workers: workers.map(nodeLine),
249
+ total: usageLine(total),
250
+ };
202
251
  }
@@ -1,11 +1,12 @@
1
1
  # src/streaming/
2
2
 
3
- | File | What it is |
4
- | :-- | :-- |
3
+ | File | What it is |
4
+ | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5
5
  | `event-stream.ts` | `AssistantMessageEventStream`: an async-iterable event stream with listeners (`on`/`off`), a `result()` promise, `cancel()` and bounded buffering. Providers produce it; the loop and `Agent` consume it. |
6
- | `sse-parser.ts` | `SSEParser`: incremental Server-Sent Events framing that survives events split across network chunks. Internal (not exported from the barrel). |
6
+ | `sse-parser.ts` | `SSEParser`: incremental Server-Sent Events framing that survives events split across network chunks. Internal (not exported from the barrel). |
7
7
 
8
8
  ## Invariants
9
+
9
10
  - The buffer holds at most 1000 events (oldest dropped, reported on the bus); an event over 1 MB is dropped. `{ buffer: false }` keeps none (for a caller that only wants `result()`).
10
11
  - `end()` without a response fails the stream instead of leaving `result()` hanging.
11
12
  - A cancel is recognised by the `AbortError` name only, never by message text.
@@ -1,5 +1,6 @@
1
1
  import type { StreamEvent, AgentResponse } from "../types/response.ts";
2
2
  import { telemetryBus } from "../telemetry.ts";
3
+ import { ValidationError } from "../types/errors.ts";
3
4
 
4
5
  /** Listener invoked for a matching stream event type or `*`. */
5
6
  export type StreamEventListener = (event: StreamEvent) => void;
@@ -87,7 +88,9 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
87
88
  this.includePartial = opts?.includePartial !== false;
88
89
  this.buffered = opts?.buffer !== false;
89
90
  this.maxBufferedEvents =
90
- typeof opts?.maxBufferedEvents === "number" && Number.isFinite(opts.maxBufferedEvents) && opts.maxBufferedEvents > 0
91
+ typeof opts?.maxBufferedEvents === "number" &&
92
+ Number.isFinite(opts.maxBufferedEvents) &&
93
+ opts.maxBufferedEvents > 0
91
94
  ? Math.floor(opts.maxBufferedEvents)
92
95
  : MAX_BUFFERED_EVENTS;
93
96
  this.maxEventBytes =
@@ -131,12 +134,15 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
131
134
  }
132
135
 
133
136
  if (out.type === "error") {
134
- this.error = (out.error instanceof Error ? out.error : new Error(String(out.error))) ?? new Error("Unknown error in stream");
137
+ this.error =
138
+ (out.error instanceof Error ? out.error : new Error(String(out.error))) ?? new Error("Unknown error in stream");
135
139
  // Single error semantic: deliver the error event to listeners,
136
140
  // reject result(), mark finished, reject pending iterators, and queue
137
141
  // the error event so a late iterator observes it before failing.
138
142
  this.deliverToListeners(out);
139
- try { this.rejectFinalResult(this.error); } catch {
143
+ try {
144
+ this.rejectFinalResult(this.error);
145
+ } catch {
140
146
  // why: rejecting an already-settled promise helper must not throw.
141
147
  }
142
148
  this.finished = true;
@@ -168,7 +174,9 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
168
174
  const specificListeners = this.listeners.get(event.type);
169
175
  if (specificListeners) {
170
176
  for (const listener of specificListeners) {
171
- try { listener(event); } catch (e) {
177
+ try {
178
+ listener(event);
179
+ } catch (e) {
172
180
  // why: one bad listener must not break the stream or other
173
181
  // listeners; capture the first exception for inspection instead.
174
182
  if (!this.listenerError) {
@@ -180,7 +188,9 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
180
188
  const allListeners = this.listeners.get("*");
181
189
  if (allListeners) {
182
190
  for (const listener of allListeners) {
183
- try { listener(event); } catch (e) {
191
+ try {
192
+ listener(event);
193
+ } catch (e) {
184
194
  // why: same as above — isolate wildcard-listener failures.
185
195
  if (!this.listenerError) {
186
196
  this.listenerError = e instanceof Error ? e : new Error(String(e));
@@ -216,7 +226,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
216
226
  // Never hang result() — ending without a response is a
217
227
  // programming error, so fail loudly instead of leaving consumers
218
228
  // waiting forever.
219
- this.fail(new Error("end() requires response"));
229
+ this.fail(new ValidationError("end() requires response"));
220
230
  return;
221
231
  }
222
232
  this.finished = true;
@@ -245,13 +255,17 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
245
255
  /** Registers a callback invoked once on cancel(). Returns an unsubscribe fn. */
246
256
  onCancel(handler: () => void): () => void {
247
257
  if (this.cancelled) {
248
- try { handler(); } catch {
258
+ try {
259
+ handler();
260
+ } catch {
249
261
  // why: a throwing cancel handler must not break the canceller.
250
262
  }
251
263
  return () => {};
252
264
  }
253
265
  this.cancelHandlers.add(handler);
254
- return () => { this.cancelHandlers.delete(handler); };
266
+ return () => {
267
+ this.cancelHandlers.delete(handler);
268
+ };
255
269
  }
256
270
 
257
271
  /** Cancels the stream: stops background work and rejects result() with AbortError.
@@ -260,16 +274,16 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
260
274
  if (this.finished || this.cancelled) return;
261
275
  this.cancelled = true;
262
276
  for (const h of [...this.cancelHandlers]) {
263
- try { h(); } catch {
277
+ try {
278
+ h();
279
+ } catch {
264
280
  // why: isolate faulty cancel handlers so every handler still runs.
265
281
  }
266
282
  }
267
283
  this.cancelHandlers.clear();
268
284
  const err = reason instanceof Error ? reason : null;
269
285
  const abortErr: Error =
270
- err && err.name === "AbortError"
271
- ? err
272
- : Object.assign(new Error("Stream aborted"), { name: "AbortError" });
286
+ err && err.name === "AbortError" ? err : Object.assign(new Error("Stream aborted"), { name: "AbortError" });
273
287
  this.fail(abortErr);
274
288
  }
275
289
 
package/src/telemetry.ts CHANGED
@@ -35,7 +35,9 @@ export const telemetryBus: TelemetryBus = (() => {
35
35
  const listeners = new Set<TelemetryListener>();
36
36
  const deliver = (event: TelemetryEvent): void => {
37
37
  for (const l of [...listeners]) {
38
- try { l(event); } catch {
38
+ try {
39
+ l(event);
40
+ } catch {
39
41
  // why: a faulty telemetry listener must not break the emitting run.
40
42
  }
41
43
  }
@@ -43,7 +45,9 @@ export const telemetryBus: TelemetryBus = (() => {
43
45
  return {
44
46
  onEvent(listener: TelemetryListener): () => void {
45
47
  listeners.add(listener);
46
- return () => { listeners.delete(listener); };
48
+ return () => {
49
+ listeners.delete(listener);
50
+ };
47
51
  },
48
52
  emitWarning(code: string, message: string, data?: unknown): void {
49
53
  try {
@@ -2,16 +2,18 @@
2
2
 
3
3
  Defining tools and running them.
4
4
 
5
- | File | What it is |
6
- | :-- | :-- |
7
- | `tool.ts` | `tool()` factory, name validation, `toStandardToolDeclarations`, option normalizers. |
8
- | `schema.ts` | Zod to JSON Schema, `$ref` resolution, `stripSchemaForGoogle`. |
9
- | `executor.ts` | `executeToolCalls`: validation, concurrency (8 global + per-tool), attempts, timeout, result cap, error scrubbing, `ctx.stop`, `tool.*` events, per-call logs. |
10
- | `subagent-result.ts` | The private-Symbol brand that marks a result as made by the SDK's delegation tools. |
5
+ | File | What it is |
6
+ | :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `tool.ts` | `tool()` factory, name validation, `toStandardToolDeclarations`, option normalizers. |
8
+ | `schema.ts` | Zod to JSON Schema, `$ref` resolution, `stripSchemaForGoogle`. |
9
+ | `executor.ts` | `executeToolCalls`: validation, concurrency (8 global + per-tool), attempts, timeout, result cap, error scrubbing, `ctx.stop`, `tool.*` events, per-call logs. |
10
+ | `tool-content.ts` | `toolContent([...])`: a branded (private Symbol) tool result of text and media. The executor turns it into `result` (text) plus `parts` (inline data URLs), so only a tool's own code can produce media. |
11
+ | `subagent-result.ts` | The private-Symbol brand that marks a result as made by the SDK's delegation tools. |
11
12
 
12
13
  ## Invariants
14
+
13
15
  - Tool names match `^[a-zA-Z0-9_-]{1,64}$` and must not collide after normalization (checked in the `Agent` constructor).
14
- - One attempt by default; only idempotent tools retry transient failures; the timeout is best effort (`ctx.signal` is cooperative), and an uncooperative tool no longer holds an aborted run.
16
+ - One attempt by default; only idempotent tools retry transient failures (what counts as transient is the one `isTransientError` in `utils/errors.ts`, shared with the provider retries); the timeout is best effort (`ctx.signal` is cooperative), and an uncooperative tool no longer holds an aborted run.
15
17
  - Results are capped by serialized size (default 20000 chars); nested error stacks and absolute paths are scrubbed. The telemetry bus gets full detail, the model gets the scrubbed text.
16
18
  - A truncated call (`{ __truncated_raw }`) never executes.
17
19
  - `ctx.stop(reason)` throws a private signal; the batch finishes, then the loop ends the run as `stopped`.
@@ -1,12 +1,12 @@
1
- import type {
2
- ToolDefinition,
3
- ToolCallRecord,
4
- ToolResultRecord,
5
- ToolExecutionContext,
6
- } from "../types/tool.ts";
1
+ import type { ToolDefinition, ToolCallRecord, ToolResultRecord, ToolExecutionContext } from "../types/tool.ts";
7
2
  import { toJsonSafe } from "../utils/serialization.ts";
8
3
  import { ConfigError } from "../types/errors.ts";
4
+ import { isTransientError } from "../utils/errors.ts";
9
5
  import { isSubagentResult } from "./subagent-result.ts";
6
+ import { isToolContent, type ToolContent } from "./tool-content.ts";
7
+ import { normalizeMediaInput } from "../utils/media.ts";
8
+ import type { ContentPart } from "../types/message.ts";
9
+ import { DEFAULT_PROMPTS, fillPrompt, type Prompts } from "../prompts/prompts.ts";
10
10
  import { isTruncatedArguments } from "../types/tool.ts";
11
11
  import { telemetryBus } from "../telemetry.ts";
12
12
  import type { ToolLogWriter } from "../session/tool-log.ts";
@@ -18,12 +18,20 @@ export interface ExecuteToolsOptions {
18
18
  parallel?: boolean;
19
19
  signal?: AbortSignal;
20
20
  sessionId?: string;
21
+ /** The refusal texts shown to the model (`AgentConfig.prompts`); the shipped ones when unset. */
22
+ prompts?: Pick<Prompts, "toolTruncatedArguments" | "toolNotFound" | "toolSchemaError">;
21
23
  /** Called the moment EACH call finishes (before the whole batch does), so a caller can save a result before the next tool is done. Never throws into the run. */
22
24
  onResult?: (result: ToolResultRecord) => void;
23
25
  /** Opens the log file of one call (only when the Agent persists to a folder); the executor writes its steps into it and `ctx.log` writes through it. */
24
26
  openToolLog?: (call: ToolCallRecord, sessionId: string | undefined) => ToolLogWriter | undefined;
25
27
  /** Forwards live sub-agent deltas (realtime worker thinking/text) to the caller. */
26
- onSubagentEvent?: (event: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
28
+ onSubagentEvent?: (event: {
29
+ trackingId: string;
30
+ delta?: string;
31
+ thinkingDelta?: string;
32
+ partialText?: string;
33
+ partialThinking?: string;
34
+ }) => void;
27
35
  }
28
36
 
29
37
  const DEFAULT_TOOL_CONCURRENCY = 8;
@@ -93,9 +101,7 @@ export function isTruncatedToolCall(call: ToolCallRecord): boolean {
93
101
  * same name (case/format-insensitive via {@link normalizeToolName}), which
94
102
  * would otherwise make model tool calls ambiguous.
95
103
  */
96
- export function assertNoToolCollisions(
97
- tools: Record<string, ToolDefinition>
98
- ): void {
104
+ export function assertNoToolCollisions(tools: Record<string, ToolDefinition>): void {
99
105
  if (!tools) return;
100
106
  const seen = new Map<string, string>();
101
107
  for (const [key, definition] of Object.entries(tools)) {
@@ -232,10 +238,11 @@ export function shouldBlockRepeat(
232
238
  * machine-readable code lives at `(result as any).errorCode` while the human
233
239
  * prefix keeps plain-string consumers working.
234
240
  */
235
- export function makeRepeatBlockedResult(call: ToolCallRecord): ToolResultRecord {
236
- const message =
237
- `repeat_blocked: Tool '${call.name}' was called again with identical arguments. ` +
238
- "The previous result is already available; reuse it or call the tool with different arguments.";
241
+ export function makeRepeatBlockedResult(
242
+ call: ToolCallRecord,
243
+ template = DEFAULT_PROMPTS.toolRepeatBlocked
244
+ ): ToolResultRecord {
245
+ const message = fillPrompt(template, { tool: call.name });
239
246
  return {
240
247
  id: call.id,
241
248
  name: call.name,
@@ -296,9 +303,7 @@ function levenshtein(a: string, b: string): number {
296
303
  row[0] = i;
297
304
  for (let j = 1; j <= b.length; j++) {
298
305
  const current = row[j]!;
299
- row[j] = a[i - 1] === b[j - 1]
300
- ? previous
301
- : Math.min(previous + 1, row[j - 1]! + 1, current + 1);
306
+ row[j] = a[i - 1] === b[j - 1] ? previous : Math.min(previous + 1, row[j - 1]! + 1, current + 1);
302
307
  previous = current;
303
308
  }
304
309
  }
@@ -339,10 +344,7 @@ function resolveTool(tools: Record<string, ToolDefinition>, requestedName: strin
339
344
  }
340
345
 
341
346
  /** Finds a tool definition by exact or case/format-insensitive name (no suggestion logic). */
342
- export function findToolDefinition(
343
- tools: Record<string, ToolDefinition>,
344
- name: string
345
- ): ToolDefinition | undefined {
347
+ export function findToolDefinition(tools: Record<string, ToolDefinition>, name: string): ToolDefinition | undefined {
346
348
  return resolveTool(tools, name).definition;
347
349
  }
348
350
 
@@ -401,21 +403,6 @@ class Semaphore {
401
403
  }
402
404
  }
403
405
 
404
- /**
405
- * True for retry-worthy failures: explicit opt-in flags (`transient` /
406
- * `retryable`), retryable HTTP statuses, or retryable network codes. Never
407
- * matches on message text — message content is untrusted and locale-fragile.
408
- */
409
- function isTransientError(error: any): boolean {
410
- if (!error) return false;
411
- if (error.transient === true || error.retryable === true) return true;
412
- const status = Number(error.status ?? error.statusCode ?? error.response?.status);
413
- if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
414
- const code = String(error.code || "").toUpperCase();
415
- if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "ETIMEDOUT", "EPIPE", "UND_ERR_CONNECT_TIMEOUT"].includes(code)) return true;
416
- return false;
417
- }
418
-
419
406
  // Waits the retry backoff, rejecting `Tool execution aborted` as soon as the run aborts; in: runSingle's retry loop, out: nothing.
420
407
  function waitForRetry(ms: number, signal?: AbortSignal): Promise<void> {
421
408
  if (ms <= 0) return Promise.resolve();
@@ -497,6 +484,36 @@ async function executeAttempt(
497
484
  }
498
485
  }
499
486
 
487
+ // Turns a `toolContent` result into its text (what every provider sees) and its media as inline data URLs (what Google also sends); in: runSingle for a branded tool result, out: the result record's `result` and `parts`.
488
+ async function resolveToolContent(
489
+ content: ToolContent,
490
+ charLimit: number
491
+ ): Promise<{ text: string; parts: ContentPart[] }> {
492
+ const texts: string[] = [];
493
+ const parts: ContentPart[] = [];
494
+ const kinds: string[] = [];
495
+ for (const raw of content.parts) {
496
+ const part = typeof raw === "string" ? ({ type: "text", text: raw } as const) : raw;
497
+ if (part.type === "text") {
498
+ texts.push(part.text);
499
+ continue;
500
+ }
501
+ const value = (part as unknown as Record<string, string | Uint8Array | ArrayBuffer>)[part.type]!;
502
+ const media = await normalizeMediaInput(value, (part as { mimeType?: string }).mimeType);
503
+ const filename = (part as { filename?: string }).filename;
504
+ parts.push({
505
+ type: part.type,
506
+ [part.type]: media.dataUrl,
507
+ mimeType: media.mimeType,
508
+ ...(filename ? { filename } : {}),
509
+ ...((part.type === "image" || part.type === "video") && part.resolution ? { resolution: part.resolution } : {}),
510
+ } as unknown as ContentPart);
511
+ kinds.push(media.mimeType);
512
+ }
513
+ const text = texts.join("\n") || (kinds.length > 0 ? `[media result: ${kinds.join(", ")}]` : "null");
514
+ return { text: truncateResultString(text, charLimit), parts };
515
+ }
516
+
500
517
  /**
501
518
  * Executes one or more tool calls in parallel or sequence, handling errors safely
502
519
  */
@@ -505,10 +522,19 @@ async function executeAttempt(
505
522
  *
506
523
  * @example `const results = await executeToolCalls({ tools, toolCalls });`
507
524
  */
508
- export async function executeToolCalls(
509
- options: ExecuteToolsOptions
510
- ): Promise<ToolResultRecord[]> {
511
- const { tools, toolCalls, agentName, parallel = true, signal, sessionId, onSubagentEvent, onResult, openToolLog } = options;
525
+ export async function executeToolCalls(options: ExecuteToolsOptions): Promise<ToolResultRecord[]> {
526
+ const {
527
+ tools,
528
+ toolCalls,
529
+ agentName,
530
+ parallel = true,
531
+ signal,
532
+ sessionId,
533
+ onSubagentEvent,
534
+ onResult,
535
+ openToolLog,
536
+ prompts = DEFAULT_PROMPTS,
537
+ } = options;
512
538
  assertNoToolCollisions(tools);
513
539
  const globalSemaphore = new Semaphore(DEFAULT_TOOL_CONCURRENCY);
514
540
  const perToolSemaphores = new Map<string, Semaphore>();
@@ -516,9 +542,7 @@ export async function executeToolCalls(
516
542
  const getToolSemaphore = (name: string, definition: ToolDefinition): Semaphore => {
517
543
  const existing = perToolSemaphores.get(name);
518
544
  if (existing) return existing;
519
- const configured = integerOption(
520
- definition.maxConcurrency
521
- );
545
+ const configured = integerOption(definition.maxConcurrency);
522
546
  const limit = configured && configured > 0 ? configured : DEFAULT_TOOL_CONCURRENCY;
523
547
  const semaphore = new Semaphore(limit);
524
548
  perToolSemaphores.set(name, semaphore);
@@ -548,9 +572,7 @@ export async function executeToolCalls(
548
572
  return {
549
573
  id: call.id,
550
574
  name: call.name,
551
- result:
552
- `Error: Tool '${call.name}' arguments are truncated or incomplete and could not be parsed. ` +
553
- "Call the tool again with the complete JSON arguments. Do not explain the error, just retry the call.",
575
+ result: fillPrompt(prompts.toolTruncatedArguments, { tool: call.name }),
554
576
  isError: true,
555
577
  durationMs: elapsedMs(startTime),
556
578
  };
@@ -561,11 +583,11 @@ export async function executeToolCalls(
561
583
 
562
584
  if (!toolDef) {
563
585
  trace("call_blocked", "warn", { reason: "tool_not_found", suggestion: resolved.suggestion });
564
- const suffix = resolved.suggestion ? ` Did you mean '${resolved.suggestion}'?` : "";
586
+ const suggestion = resolved.suggestion ? ` Did you mean '${resolved.suggestion}'?` : "";
565
587
  return {
566
588
  id: call.id,
567
589
  name: call.name,
568
- result: `Error: Tool '${call.name}' not found.${suffix}`,
590
+ result: fillPrompt(prompts.toolNotFound, { tool: call.name, suggestion }),
569
591
  isError: true,
570
592
  durationMs: elapsedMs(startTime),
571
593
  };
@@ -578,7 +600,14 @@ export async function executeToolCalls(
578
600
  let stopReason: string | undefined;
579
601
  const stoppedResult = (): ToolResultRecord => {
580
602
  trace("call_stopped", "warn", { reason: stopReason, attempts, durationMs: elapsedMs(executionStartTime) });
581
- return { id: call.id, name: resolved.name, result: stopReason, isError: false, stopped: true, durationMs: elapsedMs(executionStartTime) };
603
+ return {
604
+ id: call.id,
605
+ name: resolved.name,
606
+ result: stopReason,
607
+ isError: false,
608
+ stopped: true,
609
+ durationMs: elapsedMs(executionStartTime),
610
+ };
582
611
  };
583
612
 
584
613
  try {
@@ -599,7 +628,8 @@ export async function executeToolCalls(
599
628
  stopReason ??= String(reason);
600
629
  throw new ToolStopSignal();
601
630
  },
602
- log: (message, data) => log?.write({ phase: "log", message, ...(data !== undefined ? { data: toJsonSafe(data) } : {}) }),
631
+ log: (message, data) =>
632
+ log?.write({ phase: "log", message, ...(data !== undefined ? { data: toJsonSafe(data) } : {}) }),
603
633
  emit: (event) => {
604
634
  if (event?.type === "progress") progress = event;
605
635
  trace(event?.type === "progress" ? "progress" : "event", "info", { event: capJson(event) });
@@ -620,18 +650,12 @@ export async function executeToolCalls(
620
650
  ? JSON.stringify(toolDef.parameters).slice(0, 800)
621
651
  : undefined;
622
652
  const detail = parseRes.error.message;
623
- const hint = expectedParams
624
- ? ` Expected parameters: ${expectedParams}.`
625
- : "";
653
+ const expected = expectedParams ? ` Expected parameters: ${expectedParams}.` : "";
626
654
  trace("call_blocked", "warn", { reason: "schema_validation", detail: parseRes.error.message });
627
655
  return {
628
656
  id: call.id,
629
657
  name: call.name,
630
- result:
631
- `Schema validation error for tool '${call.name}': ${detail}` +
632
- ` Received argument keys: [${receivedKeys}].` +
633
- hint +
634
- ` Fix the arguments and call the tool again with corrected JSON. Do not explain the error, just retry the call.`,
658
+ result: fillPrompt(prompts.toolSchemaError, { tool: call.name, detail, keys: receivedKeys, expected }),
635
659
  isError: true,
636
660
  durationMs: elapsedMs(startTime),
637
661
  };
@@ -648,7 +672,7 @@ export async function executeToolCalls(
648
672
  // Attempts run serially in the while loop below — never concurrently —
649
673
  // so a non-idempotent tool cannot execute twice at the same time.
650
674
  const isIdempotent = (toolDef as any).idempotent === true;
651
- const maxAttempts = configuredTries && configuredTries > 0 ? configuredTries : (isIdempotent ? 3 : 1);
675
+ const maxAttempts = configuredTries && configuredTries > 0 ? configuredTries : isIdempotent ? 3 : 1;
652
676
  const timeoutMs = numericOption(toolDef.timeoutMs);
653
677
  // Telemetry starts when the tool body is about to run, excluding queue
654
678
  // wait and schema validation. Retries/backoff remain part of this call.
@@ -668,8 +692,16 @@ export async function executeToolCalls(
668
692
  if (stopReason !== undefined) throw err; // ctx.stop: not a failure, never retried
669
693
  timedOut = err?.toolTimeout === true;
670
694
  const willRetry =
671
- isTransientError(err) && attempt < maxAttempts && !(err?.code === "ETIMEDOUT" && (!configuredTries || configuredTries <= 0));
672
- trace("attempt_failed", "warn", { attempt, maxAttempts, willRetry, error: describeError(err), progress: capJson(progress) });
695
+ isTransientError(err) &&
696
+ attempt < maxAttempts &&
697
+ !(err?.code === "ETIMEDOUT" && (!configuredTries || configuredTries <= 0));
698
+ trace("attempt_failed", "warn", {
699
+ attempt,
700
+ maxAttempts,
701
+ willRetry,
702
+ error: describeError(err),
703
+ progress: capJson(progress),
704
+ });
673
705
  if (!isTransientError(err) || attempt >= maxAttempts) throw err;
674
706
  // A timeout is a deliberate per-attempt deadline, not a parent
675
707
  // cancellation (which rejects via the abort signal instead and is
@@ -692,6 +724,19 @@ export async function executeToolCalls(
692
724
  log?.write({ phase: "result", result: isSubagentResult(rawResult) ? rawResult.xml : toJsonSafe(rawResult) });
693
725
  const finishOk = (resultChars: number): void =>
694
726
  trace("call_succeeded", "info", { attempts: attempt, durationMs: elapsedMs(executionStartTime), resultChars });
727
+ // Text and media from `toolContent`: the text is the result, the media rides in `parts` (inline data, set only here).
728
+ if (isToolContent(rawResult)) {
729
+ const { text, parts } = await resolveToolContent(rawResult, resultCharLimit(toolDef));
730
+ finishOk(text.length);
731
+ return {
732
+ id: call.id,
733
+ name: resolved.name,
734
+ result: text,
735
+ ...(parts.length > 0 ? { parts } : {}),
736
+ isError: false,
737
+ durationMs: elapsedMs(executionStartTime),
738
+ };
739
+ }
695
740
  if (rawResult === undefined) {
696
741
  finishOk(4);
697
742
  return {
@@ -765,7 +810,9 @@ export async function executeToolCalls(
765
810
  error: {
766
811
  name: err?.name,
767
812
  ...(err?.code !== undefined ? { code: err.code } : {}),
768
- ...(err?.status !== undefined || err?.statusCode !== undefined ? { status: err.status ?? err.statusCode } : {}),
813
+ ...(err?.status !== undefined || err?.statusCode !== undefined
814
+ ? { status: err.status ?? err.statusCode }
815
+ : {}),
769
816
  },
770
817
  }),
771
818
  attempts,
@@ -65,7 +65,11 @@ 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>, seenRefs: Set<string> = new Set()): Record<string, unknown> {
68
+ export function cleanJsonSchema(
69
+ schema: any,
70
+ rootDefs?: Record<string, any>,
71
+ seenRefs: Set<string> = new Set()
72
+ ): Record<string, unknown> {
69
73
  if (typeof schema !== "object" || schema === null) {
70
74
  return schema;
71
75
  }
@@ -115,7 +119,8 @@ export function cleanJsonSchema(schema: any, rootDefs?: Record<string, any>, see
115
119
  if (rest.anyOf) rest.anyOf = (rest.anyOf as any[]).map((v: any) => cleanJsonSchema(v, defs, seenRefs));
116
120
  if (rest.oneOf) rest.oneOf = (rest.oneOf as any[]).map((v: any) => cleanJsonSchema(v, defs, seenRefs));
117
121
  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));
122
+ if (rest.prefixItems)
123
+ rest.prefixItems = (rest.prefixItems as any[]).map((v: any) => cleanJsonSchema(v, defs, seenRefs));
119
124
  // Recursively clean nested $ref inside properties that were not top-level
120
125
  for (const k of Object.keys(rest)) {
121
126
  if (rest[k] && typeof rest[k] === "object" && !Array.isArray(rest[k]) && (rest[k] as any).$ref) {
@@ -142,9 +147,7 @@ function extractZodObjectSchema(zodSchema: any): Record<string, unknown> {
142
147
 
143
148
  for (const [key, propSchema] of Object.entries(shape as Record<string, any>)) {
144
149
  const isOptional =
145
- propSchema.isOptional?.() ||
146
- propSchema._def?.typeName === "ZodOptional" ||
147
- propSchema._def?.type === "optional";
150
+ propSchema.isOptional?.() || propSchema._def?.typeName === "ZodOptional" || propSchema._def?.type === "optional";
148
151
 
149
152
  if (!isOptional) {
150
153
  required.push(key);
@@ -169,7 +172,8 @@ function inferZodPropertyType(prop: any): Record<string, unknown> {
169
172
  const tn = String(typeName).toLowerCase();
170
173
 
171
174
  if (tn.includes("number") || tn === "zodnumber") return { type: "number", ...(description ? { description } : {}) };
172
- if (tn.includes("boolean") || tn === "zodboolean") return { type: "boolean", ...(description ? { description } : {}) };
175
+ if (tn.includes("boolean") || tn === "zodboolean")
176
+ return { type: "boolean", ...(description ? { description } : {}) };
173
177
  if (tn.includes("integer")) return { type: "integer", ...(description ? { description } : {}) };
174
178
  if (tn.includes("enum") || tn === "zodenum") {
175
179
  const vals = unwrapped._def?.values || unwrapped._def?.entries || [];
@@ -49,7 +49,12 @@ export function normalizeSubagentMetadata(metas: unknown): SubAgentExecutionMeta
49
49
  if (!raw || typeof raw !== "object") continue;
50
50
  const m = raw as Record<string, unknown>;
51
51
  const u = (m.usage && typeof m.usage === "object" ? m.usage : {}) as Record<string, unknown>;
52
- const usage = { ...u, inputTokens: num(u.inputTokens), outputTokens: num(u.outputTokens), totalTokens: num(u.totalTokens) };
52
+ const usage = {
53
+ ...u,
54
+ inputTokens: num(u.inputTokens),
55
+ outputTokens: num(u.outputTokens),
56
+ totalTokens: num(u.totalTokens),
57
+ };
53
58
  out.push({
54
59
  ...(m as unknown as SubAgentExecutionMetadata),
55
60
  model: typeof m.model === "string" ? m.model : "unknown",