agent-accelerator 0.5.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- 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 +366 -161
- package/src/agent/architecture.md +13 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +8 -3
- package/src/agent/delegation.ts +82 -29
- package/src/agent/loop.ts +444 -365
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +45 -7
- package/src/agent/subagents.md +3 -0
- package/src/agent/turns.ts +3 -1
- package/src/agent/worker.ts +56 -27
- package/src/agent/workers.ts +113 -20
- package/src/architecture.md +16 -15
- package/src/index.ts +34 -10
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +147 -59
- package/src/providers/openai-compat.ts +57 -49
- package/src/providers/openai.ts +54 -37
- package/src/providers/openrouter.ts +85 -30
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +56 -115
- package/src/scheduler/architecture.md +7 -6
- package/src/scheduler/scheduled-actions.md +4 -0
- package/src/scheduler/scheduler.ts +186 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +33 -7
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +38 -12
- package/src/session/store.ts +122 -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 +8 -7
- package/src/tools/executor.ts +50 -48
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +2 -21
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +1 -9
- package/src/types/errors.ts +26 -4
- package/src/types/model.ts +0 -4
- package/src/types/tool.ts +12 -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 +6 -14
- 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,17 @@
|
|
|
2
2
|
|
|
3
3
|
Defining tools and running them.
|
|
4
4
|
|
|
5
|
-
| File
|
|
6
|
-
|
|
|
7
|
-
| `tool.ts`
|
|
8
|
-
| `schema.ts`
|
|
9
|
-
| `executor.ts`
|
|
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
|
+
| `subagent-result.ts` | The private-Symbol brand that marks a result as made by the SDK's delegation tools. |
|
|
11
11
|
|
|
12
12
|
## Invariants
|
|
13
|
+
|
|
13
14
|
- 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.
|
|
15
|
+
- 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
16
|
- 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
17
|
- A truncated call (`{ __truncated_raw }`) never executes.
|
|
17
18
|
- `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,11 +1,7 @@
|
|
|
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";
|
|
10
6
|
import { isTruncatedArguments } from "../types/tool.ts";
|
|
11
7
|
import { telemetryBus } from "../telemetry.ts";
|
|
@@ -23,7 +19,13 @@ export interface ExecuteToolsOptions {
|
|
|
23
19
|
/** 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
20
|
openToolLog?: (call: ToolCallRecord, sessionId: string | undefined) => ToolLogWriter | undefined;
|
|
25
21
|
/** Forwards live sub-agent deltas (realtime worker thinking/text) to the caller. */
|
|
26
|
-
onSubagentEvent?: (event: {
|
|
22
|
+
onSubagentEvent?: (event: {
|
|
23
|
+
trackingId: string;
|
|
24
|
+
delta?: string;
|
|
25
|
+
thinkingDelta?: string;
|
|
26
|
+
partialText?: string;
|
|
27
|
+
partialThinking?: string;
|
|
28
|
+
}) => void;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const DEFAULT_TOOL_CONCURRENCY = 8;
|
|
@@ -93,9 +95,7 @@ export function isTruncatedToolCall(call: ToolCallRecord): boolean {
|
|
|
93
95
|
* same name (case/format-insensitive via {@link normalizeToolName}), which
|
|
94
96
|
* would otherwise make model tool calls ambiguous.
|
|
95
97
|
*/
|
|
96
|
-
export function assertNoToolCollisions(
|
|
97
|
-
tools: Record<string, ToolDefinition>
|
|
98
|
-
): void {
|
|
98
|
+
export function assertNoToolCollisions(tools: Record<string, ToolDefinition>): void {
|
|
99
99
|
if (!tools) return;
|
|
100
100
|
const seen = new Map<string, string>();
|
|
101
101
|
for (const [key, definition] of Object.entries(tools)) {
|
|
@@ -296,9 +296,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
296
296
|
row[0] = i;
|
|
297
297
|
for (let j = 1; j <= b.length; j++) {
|
|
298
298
|
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);
|
|
299
|
+
row[j] = a[i - 1] === b[j - 1] ? previous : Math.min(previous + 1, row[j - 1]! + 1, current + 1);
|
|
302
300
|
previous = current;
|
|
303
301
|
}
|
|
304
302
|
}
|
|
@@ -339,10 +337,7 @@ function resolveTool(tools: Record<string, ToolDefinition>, requestedName: strin
|
|
|
339
337
|
}
|
|
340
338
|
|
|
341
339
|
/** 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 {
|
|
340
|
+
export function findToolDefinition(tools: Record<string, ToolDefinition>, name: string): ToolDefinition | undefined {
|
|
346
341
|
return resolveTool(tools, name).definition;
|
|
347
342
|
}
|
|
348
343
|
|
|
@@ -401,21 +396,6 @@ class Semaphore {
|
|
|
401
396
|
}
|
|
402
397
|
}
|
|
403
398
|
|
|
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
399
|
// Waits the retry backoff, rejecting `Tool execution aborted` as soon as the run aborts; in: runSingle's retry loop, out: nothing.
|
|
420
400
|
function waitForRetry(ms: number, signal?: AbortSignal): Promise<void> {
|
|
421
401
|
if (ms <= 0) return Promise.resolve();
|
|
@@ -505,10 +485,18 @@ async function executeAttempt(
|
|
|
505
485
|
*
|
|
506
486
|
* @example `const results = await executeToolCalls({ tools, toolCalls });`
|
|
507
487
|
*/
|
|
508
|
-
export async function executeToolCalls(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
488
|
+
export async function executeToolCalls(options: ExecuteToolsOptions): Promise<ToolResultRecord[]> {
|
|
489
|
+
const {
|
|
490
|
+
tools,
|
|
491
|
+
toolCalls,
|
|
492
|
+
agentName,
|
|
493
|
+
parallel = true,
|
|
494
|
+
signal,
|
|
495
|
+
sessionId,
|
|
496
|
+
onSubagentEvent,
|
|
497
|
+
onResult,
|
|
498
|
+
openToolLog,
|
|
499
|
+
} = options;
|
|
512
500
|
assertNoToolCollisions(tools);
|
|
513
501
|
const globalSemaphore = new Semaphore(DEFAULT_TOOL_CONCURRENCY);
|
|
514
502
|
const perToolSemaphores = new Map<string, Semaphore>();
|
|
@@ -516,9 +504,7 @@ export async function executeToolCalls(
|
|
|
516
504
|
const getToolSemaphore = (name: string, definition: ToolDefinition): Semaphore => {
|
|
517
505
|
const existing = perToolSemaphores.get(name);
|
|
518
506
|
if (existing) return existing;
|
|
519
|
-
const configured = integerOption(
|
|
520
|
-
definition.maxConcurrency
|
|
521
|
-
);
|
|
507
|
+
const configured = integerOption(definition.maxConcurrency);
|
|
522
508
|
const limit = configured && configured > 0 ? configured : DEFAULT_TOOL_CONCURRENCY;
|
|
523
509
|
const semaphore = new Semaphore(limit);
|
|
524
510
|
perToolSemaphores.set(name, semaphore);
|
|
@@ -578,7 +564,14 @@ export async function executeToolCalls(
|
|
|
578
564
|
let stopReason: string | undefined;
|
|
579
565
|
const stoppedResult = (): ToolResultRecord => {
|
|
580
566
|
trace("call_stopped", "warn", { reason: stopReason, attempts, durationMs: elapsedMs(executionStartTime) });
|
|
581
|
-
return {
|
|
567
|
+
return {
|
|
568
|
+
id: call.id,
|
|
569
|
+
name: resolved.name,
|
|
570
|
+
result: stopReason,
|
|
571
|
+
isError: false,
|
|
572
|
+
stopped: true,
|
|
573
|
+
durationMs: elapsedMs(executionStartTime),
|
|
574
|
+
};
|
|
582
575
|
};
|
|
583
576
|
|
|
584
577
|
try {
|
|
@@ -599,7 +592,8 @@ export async function executeToolCalls(
|
|
|
599
592
|
stopReason ??= String(reason);
|
|
600
593
|
throw new ToolStopSignal();
|
|
601
594
|
},
|
|
602
|
-
log: (message, data) =>
|
|
595
|
+
log: (message, data) =>
|
|
596
|
+
log?.write({ phase: "log", message, ...(data !== undefined ? { data: toJsonSafe(data) } : {}) }),
|
|
603
597
|
emit: (event) => {
|
|
604
598
|
if (event?.type === "progress") progress = event;
|
|
605
599
|
trace(event?.type === "progress" ? "progress" : "event", "info", { event: capJson(event) });
|
|
@@ -620,9 +614,7 @@ export async function executeToolCalls(
|
|
|
620
614
|
? JSON.stringify(toolDef.parameters).slice(0, 800)
|
|
621
615
|
: undefined;
|
|
622
616
|
const detail = parseRes.error.message;
|
|
623
|
-
const hint = expectedParams
|
|
624
|
-
? ` Expected parameters: ${expectedParams}.`
|
|
625
|
-
: "";
|
|
617
|
+
const hint = expectedParams ? ` Expected parameters: ${expectedParams}.` : "";
|
|
626
618
|
trace("call_blocked", "warn", { reason: "schema_validation", detail: parseRes.error.message });
|
|
627
619
|
return {
|
|
628
620
|
id: call.id,
|
|
@@ -648,7 +640,7 @@ export async function executeToolCalls(
|
|
|
648
640
|
// Attempts run serially in the while loop below — never concurrently —
|
|
649
641
|
// so a non-idempotent tool cannot execute twice at the same time.
|
|
650
642
|
const isIdempotent = (toolDef as any).idempotent === true;
|
|
651
|
-
const maxAttempts = configuredTries && configuredTries > 0 ? configuredTries :
|
|
643
|
+
const maxAttempts = configuredTries && configuredTries > 0 ? configuredTries : isIdempotent ? 3 : 1;
|
|
652
644
|
const timeoutMs = numericOption(toolDef.timeoutMs);
|
|
653
645
|
// Telemetry starts when the tool body is about to run, excluding queue
|
|
654
646
|
// wait and schema validation. Retries/backoff remain part of this call.
|
|
@@ -668,8 +660,16 @@ export async function executeToolCalls(
|
|
|
668
660
|
if (stopReason !== undefined) throw err; // ctx.stop: not a failure, never retried
|
|
669
661
|
timedOut = err?.toolTimeout === true;
|
|
670
662
|
const willRetry =
|
|
671
|
-
isTransientError(err) &&
|
|
672
|
-
|
|
663
|
+
isTransientError(err) &&
|
|
664
|
+
attempt < maxAttempts &&
|
|
665
|
+
!(err?.code === "ETIMEDOUT" && (!configuredTries || configuredTries <= 0));
|
|
666
|
+
trace("attempt_failed", "warn", {
|
|
667
|
+
attempt,
|
|
668
|
+
maxAttempts,
|
|
669
|
+
willRetry,
|
|
670
|
+
error: describeError(err),
|
|
671
|
+
progress: capJson(progress),
|
|
672
|
+
});
|
|
673
673
|
if (!isTransientError(err) || attempt >= maxAttempts) throw err;
|
|
674
674
|
// A timeout is a deliberate per-attempt deadline, not a parent
|
|
675
675
|
// cancellation (which rejects via the abort signal instead and is
|
|
@@ -765,7 +765,9 @@ export async function executeToolCalls(
|
|
|
765
765
|
error: {
|
|
766
766
|
name: err?.name,
|
|
767
767
|
...(err?.code !== undefined ? { code: err.code } : {}),
|
|
768
|
-
...(err?.status !== undefined || err?.statusCode !== undefined
|
|
768
|
+
...(err?.status !== undefined || err?.statusCode !== undefined
|
|
769
|
+
? { status: err.status ?? err.statusCode }
|
|
770
|
+
: {}),
|
|
769
771
|
},
|
|
770
772
|
}),
|
|
771
773
|
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",
|
package/src/tools/tool.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type {
|
|
3
|
-
ToolDefinition,
|
|
4
|
-
StandardToolDeclaration,
|
|
5
|
-
ToolExecuteFn,
|
|
6
|
-
} from "../types/tool.ts";
|
|
2
|
+
import type { ToolDefinition, StandardToolDeclaration, ToolExecuteFn } from "../types/tool.ts";
|
|
7
3
|
import type { ThinkingLevel } from "../types/core.ts";
|
|
8
4
|
import type { ThinkingOption } from "../types/agent.ts";
|
|
9
5
|
import { zodToJsonSchema } from "./schema.ts";
|
|
@@ -54,9 +50,10 @@ export function normalizeThinkingV2(
|
|
|
54
50
|
*
|
|
55
51
|
* @example `resolveWhenBusy({ whenBusy: "queue", mode: "steer" }); // "queue"`
|
|
56
52
|
*/
|
|
57
|
-
export function resolveWhenBusy(
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
export function resolveWhenBusy(config?: {
|
|
54
|
+
mode?: "steer" | "queue" | "auto";
|
|
55
|
+
whenBusy?: "steer" | "queue" | "auto";
|
|
56
|
+
}): "steer" | "queue" | "auto" {
|
|
60
57
|
const v = config?.whenBusy ?? config?.mode ?? "auto";
|
|
61
58
|
return v === "steer" || v === "queue" ? v : "auto";
|
|
62
59
|
}
|
|
@@ -67,9 +64,10 @@ export function resolveWhenBusy(
|
|
|
67
64
|
*
|
|
68
65
|
* @example `normalizeMaxAttempts({ maxAttempts: 3 }); // 3`
|
|
69
66
|
*/
|
|
70
|
-
export function normalizeMaxAttempts(
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
export function normalizeMaxAttempts(config?: {
|
|
68
|
+
maxTries?: number | string;
|
|
69
|
+
maxAttempts?: number | string;
|
|
70
|
+
}): number | string | undefined {
|
|
73
71
|
return config?.maxTries ?? config?.maxAttempts;
|
|
74
72
|
}
|
|
75
73
|
|
package/src/types/agent.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { ModelSpec } from "./model.ts";
|
|
2
2
|
import type { ToolDefinition } from "./tool.ts";
|
|
3
|
-
import type {
|
|
4
|
-
ThinkingLevel,
|
|
5
|
-
CacheOption,
|
|
6
|
-
ServiceTier,
|
|
7
|
-
} from "./core.ts";
|
|
3
|
+
import type { ThinkingLevel, CacheOption, ServiceTier } from "./core.ts";
|
|
8
4
|
import type { ModelProviderInstance } from "../providers/registry.ts";
|
|
9
5
|
import type { Agent } from "../agent/agent.ts";
|
|
10
6
|
import type { z } from "zod";
|
|
@@ -47,14 +43,7 @@ export interface AgentLogger {
|
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
/** Canonical tool-selection control. String form is the portable subset. */
|
|
50
|
-
export type ToolChoiceOption =
|
|
51
|
-
| "auto"
|
|
52
|
-
| "none"
|
|
53
|
-
| "required"
|
|
54
|
-
| { mode?: "auto" | "none" | "required"; tool?: string };
|
|
55
|
-
|
|
56
|
-
/** Canonical stop control: sequence(s) halting generation. */
|
|
57
|
-
export type StopOption = string | string[];
|
|
46
|
+
export type ToolChoiceOption = "auto" | "none" | "required" | { mode?: "auto" | "none" | "required"; tool?: string };
|
|
58
47
|
|
|
59
48
|
/** Canonical structured-output input. Zod, JSON Schema, or explicit form. */
|
|
60
49
|
export type StructuredOutputOption =
|
|
@@ -167,10 +156,6 @@ export interface AgentConfig {
|
|
|
167
156
|
googleTier?: "free" | "paid";
|
|
168
157
|
/** Maximum output tokens for one model call. */
|
|
169
158
|
maxOutputTokens?: number;
|
|
170
|
-
/** Stop sequence(s). */
|
|
171
|
-
stop?: StopOption;
|
|
172
|
-
/** Deterministic seed, when the provider supports it. */
|
|
173
|
-
seed?: number;
|
|
174
159
|
/**
|
|
175
160
|
* Optional output schema (Zod, JSON Schema, or `{ name, schema, strict }`,
|
|
176
161
|
* see `StructuredOutputOption`, stored as `unknown` so `zod`
|
|
@@ -383,10 +368,6 @@ export interface AgentRunOptions {
|
|
|
383
368
|
toolChoice?: ToolChoiceOption;
|
|
384
369
|
/** Per-run output-token cap. */
|
|
385
370
|
maxOutputTokens?: number;
|
|
386
|
-
/** Per-run stop sequence(s). */
|
|
387
|
-
stop?: StopOption;
|
|
388
|
-
/** Per-run seed. */
|
|
389
|
-
seed?: number;
|
|
390
371
|
/** Per-run retry override. */
|
|
391
372
|
retry?: RetryPolicyConfig;
|
|
392
373
|
/** Per-run fetch override. */
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Shared contracts. Types only, plus a few tiny helpers that belong to a type.
|
|
4
4
|
|
|
5
|
-
| File
|
|
6
|
-
|
|
|
7
|
-
| `agent.ts`
|
|
8
|
-
| `core.ts`
|
|
9
|
-
| `message.ts`
|
|
10
|
-
| `model.ts`
|
|
11
|
-
| `response.ts`
|
|
12
|
-
| `tool.ts`
|
|
13
|
-
| `scheduler.ts` | Scheduled-action input, record and delivery types.
|
|
14
|
-
| `errors.ts`
|
|
5
|
+
| File | What it defines |
|
|
6
|
+
| :------------- | :--------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `agent.ts` | `AgentConfig`, `AgentRunOptions`, `DynamicSubagentsConfig`, `Budget`, `EditableAgentConfig`, run/turn event shapes. |
|
|
8
|
+
| `core.ts` | `ThinkingLevel`, `CacheOption` (+ `CACHE_OPTIONS`), `ServiceTier`, `TokenUsage`. |
|
|
9
|
+
| `message.ts` | `Message` and the content parts (text, image, audio, video, file, tool call/result, thinking), `ProviderSteps`. |
|
|
10
|
+
| `model.ts` | `ModelSpec`, `Provider`, `ProviderRequestOptions`, `ProviderGenerateResult`, structured-output spec. |
|
|
11
|
+
| `response.ts` | `AgentResponse`, `FinishReason` + `normalizeFinishReason`, `StreamEvent`, worker metadata. |
|
|
12
|
+
| `tool.ts` | `ToolDefinition`, `ToolExecutionContext` (`log`, `stop`, `emit`), call/result records, the truncated-arguments marker. |
|
|
13
|
+
| `scheduler.ts` | Scheduled-action input, record and delivery types. |
|
|
14
|
+
| `errors.ts` | The typed error family (`AgentAccelError` and subclasses), `isAbortError`. |
|
|
15
15
|
|
|
16
16
|
## Invariants
|
|
17
|
+
|
|
17
18
|
- Adding a field to a public type is a public API change: update `README.md` and `docs/`.
|
|
18
19
|
- Abort means `signal.aborted` or `name === "AbortError"`, never a message match.
|
|
19
20
|
- Error messages are one line and ANSI-free.
|