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.
- package/README.md +64 -16
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +447 -172
- package/src/agent/architecture.md +16 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +9 -3
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +86 -28
- package/src/agent/loop.ts +474 -374
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +50 -19
- package/src/agent/turns.ts +3 -1
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +73 -30
- package/src/agent/workers.ts +126 -21
- package/src/architecture.md +17 -15
- package/src/index.ts +43 -19
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/prompts/additional-context.md +2 -0
- package/src/prompts/architecture.md +19 -0
- package/src/prompts/prompts.ts +81 -0
- package/src/prompts/reasoning-directive.md +5 -0
- package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
- package/src/prompts/tool-bridge.md +1 -0
- package/src/prompts/tool-interrupted.md +1 -0
- package/src/prompts/tool-not-found.md +1 -0
- package/src/prompts/tool-repeat-blocked.md +1 -0
- package/src/prompts/tool-schema-error.md +1 -0
- package/src/prompts/tool-truncated-arguments.md +1 -0
- package/src/prompts/worker-failed.md +1 -0
- package/src/prompts/worker-replied.md +2 -0
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +228 -69
- package/src/providers/openai-compat.ts +74 -56
- package/src/providers/openai.ts +64 -41
- package/src/providers/openrouter.ts +270 -48
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +115 -124
- package/src/scheduler/architecture.md +8 -7
- package/src/scheduler/scheduler.ts +226 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +38 -16
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +39 -12
- package/src/session/store.ts +126 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +9 -7
- package/src/tools/executor.ts +109 -62
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool-content.ts +32 -0
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +37 -22
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +5 -9
- package/src/types/errors.ts +26 -4
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -5
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +14 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +9 -17
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +15 -3
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
package/src/session/usage.ts
CHANGED
|
@@ -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 [
|
|
76
|
-
|
|
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 [
|
|
86
|
-
|
|
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 =
|
|
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
|
|
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 {
|
|
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(
|
|
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 {
|
|
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
|
|
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`
|
|
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" &&
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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 () => {
|
|
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 {
|
|
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 {
|
|
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 () => {
|
|
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
|
|
6
|
-
|
|
|
7
|
-
| `tool.ts`
|
|
8
|
-
| `schema.ts`
|
|
9
|
-
| `executor.ts`
|
|
10
|
-
| `
|
|
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`.
|
package/src/tools/executor.ts
CHANGED
|
@@ -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: {
|
|
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(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
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:
|
|
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 {
|
|
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) =>
|
|
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
|
|
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 :
|
|
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) &&
|
|
672
|
-
|
|
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
|
|
813
|
+
...(err?.status !== undefined || err?.statusCode !== undefined
|
|
814
|
+
? { status: err.status ?? err.statusCode }
|
|
815
|
+
: {}),
|
|
769
816
|
},
|
|
770
817
|
}),
|
|
771
818
|
attempts,
|
package/src/tools/schema.ts
CHANGED
|
@@ -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(
|
|
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)
|
|
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")
|
|
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 = {
|
|
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",
|