agent-accelerator 0.5.6 → 0.5.7
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 +6 -13
- package/package.json +1 -1
- package/src/agent/agent.ts +16 -18
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +4 -0
- package/src/agent/delegation.ts +18 -84
- package/src/agent/loop.ts +13 -15
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +6 -73
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +3 -0
- package/src/models/catalog.ts +7 -0
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +20 -0
- package/src/providers/openai-compat.ts +22 -0
- package/src/providers/openai.ts +17 -1
- package/src/providers/openrouter.ts +12 -0
- package/src/providers/registry.ts +1 -0
- package/src/providers.ts +9 -53
- package/src/scheduler/architecture.md +19 -0
- package/src/scheduler/scheduler.ts +21 -0
- package/src/scheduler/store.ts +6 -0
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +15 -0
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +5 -0
- package/src/telemetry.ts +1 -0
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +13 -0
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/types/architecture.md +20 -0
- package/src/utils/architecture.md +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +2 -0
- package/src/utils/errors.ts +4 -0
- package/src/utils/headers.ts +4 -1
- package/src/utils/media.ts +1 -0
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +1 -0
- package/src/utils/safe-io.ts +4 -0
- package/src/utils/session.ts +5 -161
package/src/session/store.ts
CHANGED
|
@@ -127,10 +127,12 @@ export interface PersistedAgentSession {
|
|
|
127
127
|
tools?: SessionToolRecord[];
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** Zeroed cumulative session totals; in: SessionTelemetry, out: a SessionTotals. */
|
|
130
131
|
export function emptyTotals(): SessionTotals {
|
|
131
132
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: 0 };
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// A finite non-negative number, else 0; in: telemetry and formatting, out: a safe number.
|
|
134
136
|
function toNumber(n: unknown): number {
|
|
135
137
|
const v = typeof n === "number" ? n : Number(n);
|
|
136
138
|
return Number.isFinite(v) && v >= 0 ? v : 0;
|
|
@@ -191,6 +193,7 @@ export function computeSessionTurnCost(usage: TokenUsage, modelStr: string): num
|
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
|
|
196
|
+
/** Compact token count (`1.2k`, `3.4M`); in: the status bar, out: a string. */
|
|
194
197
|
export function formatSessionTokens(n: number): string {
|
|
195
198
|
const v = toNumber(n);
|
|
196
199
|
if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
|
|
@@ -198,6 +201,7 @@ export function formatSessionTokens(n: number): string {
|
|
|
198
201
|
return `${Math.floor(v)}`;
|
|
199
202
|
}
|
|
200
203
|
|
|
204
|
+
/** Cost with precision that fits its size (`$0.0042`); in: the status bar and banner, out: a string. */
|
|
201
205
|
export function formatSessionCost(c?: number): string {
|
|
202
206
|
if (!c || c <= 0) return "$0.00";
|
|
203
207
|
if (c < 0.0001) return `$${c.toFixed(6)}`;
|
|
@@ -246,10 +250,12 @@ export class SessionTelemetry {
|
|
|
246
250
|
return turnCost;
|
|
247
251
|
}
|
|
248
252
|
|
|
253
|
+
// Zeroes the totals; in: callers starting a fresh session, out: this.totals.
|
|
249
254
|
reset(): void {
|
|
250
255
|
this.totals = emptyTotals();
|
|
251
256
|
}
|
|
252
257
|
|
|
258
|
+
// A copy of the totals for saving; in: buildSessionData/JSON.stringify, out: a SessionTotals.
|
|
253
259
|
toJSON(): SessionTotals {
|
|
254
260
|
return { ...this.totals };
|
|
255
261
|
}
|
|
@@ -343,6 +349,7 @@ export interface SessionAgentLike {
|
|
|
343
349
|
context: { systemPrompt?: string; messages: Message[] };
|
|
344
350
|
}
|
|
345
351
|
|
|
352
|
+
// Slims worker traces for the snapshot (one key per tracking id, repeated final text dropped, reasoning kept); in: buildSessionData, out: the `subagents` field.
|
|
346
353
|
function snapshotSubagents(traces: Record<string, PersistedSubAgentTrace> | undefined): Record<string, PersistedSubAgentTrace> | undefined {
|
|
347
354
|
if (!traces) return undefined;
|
|
348
355
|
const keys = Object.keys(traces);
|
|
@@ -562,6 +569,7 @@ function toCacheOption(raw: unknown): CacheOption | undefined {
|
|
|
562
569
|
return typeof retention === "string" ? legacy[retention] : undefined;
|
|
563
570
|
}
|
|
564
571
|
|
|
572
|
+
// Parses any supported snapshot shape (current, legacy pretty JSON, JSONL, ancient) or flags it over-limit; in: raw file text, out: a session, null or OVER_LIMIT.
|
|
565
573
|
function parseSession(raw: string, limits?: SessionLimits): PersistedAgentSession | null | typeof OVER_LIMIT {
|
|
566
574
|
const maxMessages = limits?.maxMessages ?? MAX_SESSION_MESSAGES;
|
|
567
575
|
const maxBytes = limits?.maxBytes ?? MAX_SESSION_JSON_BYTES;
|
|
@@ -780,10 +788,12 @@ export function saveSessionFile(
|
|
|
780
788
|
|
|
781
789
|
const MEDIA_PART_KEYS = ["image", "audio", "video", "file"] as const;
|
|
782
790
|
|
|
791
|
+
// True for an http(s) link, which stays a reference instead of being saved as media; in: embeddedMedia, out: a boolean.
|
|
783
792
|
function isHttpRef(value: string): boolean {
|
|
784
793
|
return value.startsWith("http://") || value.startsWith("https://");
|
|
785
794
|
}
|
|
786
795
|
|
|
796
|
+
// Splits a `data:<mime>;base64,<payload>` string; in: embeddedMedia, out: its mime type and payload or undefined.
|
|
787
797
|
function parseDataUrl(value: string): { mimeType: string; base64: string } | undefined {
|
|
788
798
|
const m = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,(.+)$/s);
|
|
789
799
|
if (m && m[2]) return { mimeType: m[1] || "application/octet-stream", base64: m[2] };
|
|
@@ -797,6 +807,7 @@ function looksLikeEmbeddedBase64(value: string): boolean {
|
|
|
797
807
|
return /^[A-Za-z0-9+/=\n\r]+$/.test(value.slice(0, 500));
|
|
798
808
|
}
|
|
799
809
|
|
|
810
|
+
// File extension for saved media from its mime type, else its filename, else `bin`; in: embeddedMedia, out: the extension.
|
|
800
811
|
function extForMedia(mimeType?: string, filename?: string): string {
|
|
801
812
|
const mime = (mimeType || "").toLowerCase().split(";")[0]!.trim();
|
|
802
813
|
const known: Record<string, string> = {
|
|
@@ -850,6 +861,7 @@ export function embeddedMedia(part: ContentPart): { key: (typeof MEDIA_PART_KEYS
|
|
|
850
861
|
return { key, bytes, ...(mimeType ? { mimeType } : {}), ext: extForMedia(mimeType, typeof p["filename"] === "string" ? (p["filename"] as string) : undefined) };
|
|
851
862
|
}
|
|
852
863
|
|
|
864
|
+
// Copy of the messages that keeps media bytes (structuredClone, else JSON); in: extractMediaToDir/resolveMediaPaths, out: a new array.
|
|
853
865
|
function cloneMessages(messages: Message[]): Message[] {
|
|
854
866
|
try {
|
|
855
867
|
const g: any = globalThis as any;
|
|
@@ -1136,6 +1148,7 @@ export const MAX_SESSION_MESSAGES = 50_000;
|
|
|
1136
1148
|
/** Default max raw JSON size accepted when loading a session snapshot (override via `SessionLimits`). */
|
|
1137
1149
|
export const MAX_SESSION_JSON_BYTES = 100 * 1024 * 1024;
|
|
1138
1150
|
|
|
1151
|
+
// A SessionLoadError-named Error with the SDK prefix; in: validateSessionData, out: the error.
|
|
1139
1152
|
function sessionLoadError(message: string): Error {
|
|
1140
1153
|
const e = new Error(`[Agent Accelerator] Invalid session: ${message}`);
|
|
1141
1154
|
e.name = "SessionLoadError";
|
|
@@ -1314,6 +1327,7 @@ const SERVICE_TIER_MULTIPLIERS: Record<string, number> = {
|
|
|
1314
1327
|
fast: 2,
|
|
1315
1328
|
};
|
|
1316
1329
|
|
|
1330
|
+
// Price multiplier for a service tier (flex 0.5, priority 2, 2.5 for gpt-5.5; estimates); in: priceUsage, out: the multiplier and tier name.
|
|
1317
1331
|
function serviceTierMultiplier(serviceTier?: string, modelId?: string): { multiplier: number; tier?: string } {
|
|
1318
1332
|
if (!serviceTier) return { multiplier: 1 };
|
|
1319
1333
|
const lower = serviceTier.toLowerCase();
|
|
@@ -1327,6 +1341,7 @@ function serviceTierMultiplier(serviceTier?: string, modelId?: string): { multip
|
|
|
1327
1341
|
return m !== undefined ? { multiplier: m, tier: serviceTier } : { multiplier: 1, tier: serviceTier };
|
|
1328
1342
|
}
|
|
1329
1343
|
|
|
1344
|
+
// Integer micro-USD to dollars; in: priceUsage, out: a number.
|
|
1330
1345
|
function toDollars(microUSD: number): number {
|
|
1331
1346
|
return microUSD / 1_000_000;
|
|
1332
1347
|
}
|
package/src/session/usage.ts
CHANGED
|
@@ -39,6 +39,7 @@ export function emptyUsage(): UsageTotals {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// A finite whole number >= 0, else 0; in: the usage maths and readUsage, out: a safe count.
|
|
42
43
|
const count = (n: unknown): number => (typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0);
|
|
43
44
|
|
|
44
45
|
/** A model reference (string or catalog spec) as a plain string for a report line. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# src/streaming/
|
|
2
|
+
|
|
3
|
+
| File | What it is |
|
|
4
|
+
| :-- | :-- |
|
|
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). |
|
|
7
|
+
|
|
8
|
+
## Invariants
|
|
9
|
+
- 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
|
+
- `end()` without a response fails the stream instead of leaving `result()` hanging.
|
|
11
|
+
- A cancel is recognised by the `AbortError` name only, never by message text.
|
|
12
|
+
- Breaking out of `for await` cancels the stream, which tears down the HTTP request.
|
|
13
|
+
- A throwing listener never breaks the stream; the first error is kept in `getListenerError()`.
|
|
@@ -30,6 +30,7 @@ export const MAX_BUFFERED_EVENTS = 1000;
|
|
|
30
30
|
/** Default max approx bytes per delta event (Q-29). */
|
|
31
31
|
export const MAX_EVENT_BYTES = 1_048_576;
|
|
32
32
|
|
|
33
|
+
// Approximate size of an event's text fields; in: push's oversize guard, out: a character count.
|
|
33
34
|
function approxEventBytes(event: StreamEvent): number {
|
|
34
35
|
let n = 0;
|
|
35
36
|
if (typeof event.delta === "string") n += event.delta.length;
|
|
@@ -39,6 +40,7 @@ function approxEventBytes(event: StreamEvent): number {
|
|
|
39
40
|
return n;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// Reports a dropped or overflowing event on the bus and the legacy hook, never throwing; in: push/enqueueBounded, out: telemetryBus.
|
|
42
44
|
function reportStreamAnomaly(detail: { type: string; [k: string]: unknown }): void {
|
|
43
45
|
// why: bus + hook only, never console — streams are hot paths and logging
|
|
44
46
|
// per-drop would spam and break no-console policy (Q-29/Q-41).
|
|
@@ -161,6 +163,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
// Calls the listeners of the event's type and `*`, remembering the first listener error instead of throwing; in: push, out: those callbacks.
|
|
164
167
|
private deliverToListeners(event: StreamEvent): void {
|
|
165
168
|
const specificListeners = this.listeners.get(event.type);
|
|
166
169
|
if (specificListeners) {
|
|
@@ -187,6 +190,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
|
|
193
|
+
// Buffers an event for the iterator, dropping the oldest non-error events beyond the cap; in: push, out: this.queue.
|
|
190
194
|
private enqueueBounded(event: StreamEvent): void {
|
|
191
195
|
this.queue.push(event);
|
|
192
196
|
while (this.queue.length > this.maxBufferedEvents) {
|
|
@@ -289,6 +293,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
289
293
|
return this.finalResultPromise;
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
// Iterator over the stream: buffered events first, then waits for new ones; breaking out cancels the stream and a failed stream rejects after delivering its error event; in: for-await consumers, out: StreamEvents.
|
|
292
297
|
[Symbol.asyncIterator](): AsyncIterator<StreamEvent> {
|
|
293
298
|
let iteratorDone = false;
|
|
294
299
|
return {
|
package/src/telemetry.ts
CHANGED
|
@@ -30,6 +30,7 @@ interface TelemetryBus {
|
|
|
30
30
|
clear(): void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** The in-process telemetry bus: listeners subscribe with `onEvent`; emission never throws and does nothing without listeners. */
|
|
33
34
|
export const telemetryBus: TelemetryBus = (() => {
|
|
34
35
|
const listeners = new Set<TelemetryListener>();
|
|
35
36
|
const deliver = (event: TelemetryEvent): void => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# src/tools/
|
|
2
|
+
|
|
3
|
+
Defining tools and running them.
|
|
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. |
|
|
11
|
+
|
|
12
|
+
## Invariants
|
|
13
|
+
- 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
|
+
- 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
|
+
- A truncated call (`{ __truncated_raw }`) never executes.
|
|
17
|
+
- `ctx.stop(reason)` throws a private signal; the batch finishes, then the loop ends the run as `stopped`.
|
|
18
|
+
- Only branded envelopes get their `_subagentMetadata` trusted; the executor passes them through untouched.
|
|
19
|
+
- Failures return `Error executing <name>: <msg>` plus a JSON block (attempts, timedOut, last progress, hint).
|
package/src/tools/executor.ts
CHANGED
|
@@ -28,20 +28,24 @@ export interface ExecuteToolsOptions {
|
|
|
28
28
|
|
|
29
29
|
const DEFAULT_TOOL_CONCURRENCY = 8;
|
|
30
30
|
|
|
31
|
+
// Wall-clock milliseconds; in: the executor's timing, out: a number.
|
|
31
32
|
function nowMs(): number {
|
|
32
33
|
return Date.now();
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// Milliseconds since `start`, at least 1 so a result never reports 0; in: result records, out: durationMs.
|
|
35
37
|
function elapsedMs(start: number): number {
|
|
36
38
|
return Math.max(1, nowMs() - start);
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
// A finite number >= 0 from a number or numeric string option, else undefined; in: tool options, out: timeout and limits.
|
|
39
42
|
function numericOption(value: number | string | undefined): number | undefined {
|
|
40
43
|
if (value === undefined || value === "") return undefined;
|
|
41
44
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
42
45
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
// numericOption rounded down to a whole number; in: tool options, out: attempts, concurrency and char caps.
|
|
45
49
|
function integerOption(value: number | string | undefined): number | undefined {
|
|
46
50
|
const parsed = numericOption(value);
|
|
47
51
|
return parsed === undefined ? undefined : Math.floor(parsed);
|
|
@@ -128,11 +132,13 @@ export function sanitizeErrorMessage(message: string): string {
|
|
|
128
132
|
return out;
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
// The tool's `maxResultChars` or the 20000 default; in: runSingle, out: the cap on a result kept for the model.
|
|
131
136
|
function resultCharLimit(toolDef: ToolDefinition): number {
|
|
132
137
|
const configured = integerOption((toolDef as any).maxResultChars);
|
|
133
138
|
return configured && configured > 0 ? configured : DEFAULT_MAX_RESULT_CHARS;
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
// Cuts a string to the limit and appends a `[Truncated: ...]` note; in: runSingle, out: the capped result.
|
|
136
142
|
function truncateResultString(value: string, limit: number): string {
|
|
137
143
|
if (value.length <= limit) return value;
|
|
138
144
|
return value.slice(0, limit) + `\n[Truncated: showing ${limit} of ${value.length} chars]`;
|
|
@@ -275,12 +281,14 @@ export function traceToolEvent(
|
|
|
275
281
|
});
|
|
276
282
|
}
|
|
277
283
|
|
|
284
|
+
// Scrubbed, JSON-safe value capped to `max` serialized characters for logs and events; in: progress and event tracing, out: a value or a truncated string.
|
|
278
285
|
function capJson(value: unknown, max = 2000): unknown {
|
|
279
286
|
const safe = scrubErrors(toJsonSafe(value));
|
|
280
287
|
const json = JSON.stringify(safe);
|
|
281
288
|
return typeof json === "string" && json.length > max ? `${json.slice(0, max)}…[truncated]` : safe;
|
|
282
289
|
}
|
|
283
290
|
|
|
291
|
+
// Edit distance between two strings; in: resolveTool's did-you-mean, out: a number.
|
|
284
292
|
function levenshtein(a: string, b: string): number {
|
|
285
293
|
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
286
294
|
for (let i = 1; i <= a.length; i++) {
|
|
@@ -303,6 +311,7 @@ interface ResolvedTool {
|
|
|
303
311
|
suggestion?: string;
|
|
304
312
|
}
|
|
305
313
|
|
|
314
|
+
// Finds the tool a call names (exact, then case/format-insensitive), else suggests the closest name; in: runSingle and findToolDefinition, out: ResolvedTool.
|
|
306
315
|
function resolveTool(tools: Record<string, ToolDefinition>, requestedName: string): ResolvedTool {
|
|
307
316
|
const direct = tools[requestedName];
|
|
308
317
|
if (direct) return { definition: direct, name: direct.name || requestedName };
|
|
@@ -348,6 +357,7 @@ class Semaphore {
|
|
|
348
357
|
|
|
349
358
|
constructor(private readonly limit: number) {}
|
|
350
359
|
|
|
360
|
+
// Takes a slot (immediately if free, else waits in line), rejecting if the run aborts first; in: runSingle, out: a release function.
|
|
351
361
|
acquire(signal?: AbortSignal): Promise<() => void> {
|
|
352
362
|
if (signal?.aborted) return Promise.reject(new Error("Tool execution aborted"));
|
|
353
363
|
if (this.limit === Number.POSITIVE_INFINITY || this.active < this.limit) {
|
|
@@ -372,6 +382,7 @@ class Semaphore {
|
|
|
372
382
|
});
|
|
373
383
|
}
|
|
374
384
|
|
|
385
|
+
// Frees a slot and hands it to the next live waiter, dropping aborted ones; in: the function acquire returned, out: the next waiter's grant.
|
|
375
386
|
private release(): void {
|
|
376
387
|
this.active = Math.max(0, this.active - 1);
|
|
377
388
|
while (this.waiters.length > 0) {
|
|
@@ -405,6 +416,7 @@ function isTransientError(error: any): boolean {
|
|
|
405
416
|
return false;
|
|
406
417
|
}
|
|
407
418
|
|
|
419
|
+
// Waits the retry backoff, rejecting `Tool execution aborted` as soon as the run aborts; in: runSingle's retry loop, out: nothing.
|
|
408
420
|
function waitForRetry(ms: number, signal?: AbortSignal): Promise<void> {
|
|
409
421
|
if (ms <= 0) return Promise.resolve();
|
|
410
422
|
return new Promise((resolve, reject) => {
|
|
@@ -437,6 +449,7 @@ function raceAbort<T>(work: Promise<T>, signal: AbortSignal | undefined): Promis
|
|
|
437
449
|
});
|
|
438
450
|
}
|
|
439
451
|
|
|
452
|
+
// One attempt of a tool: runs it, raced against the run's abort and the per-attempt timeout (which also aborts the tool's signal); in: runSingle, out: the tool's result or a thrown timeout/abort.
|
|
440
453
|
async function executeAttempt(
|
|
441
454
|
toolDef: ToolDefinition,
|
|
442
455
|
parsedInput: unknown,
|
package/src/tools/schema.ts
CHANGED
|
@@ -160,6 +160,7 @@ function extractZodObjectSchema(zodSchema: any): Record<string, unknown> {
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// Best-effort JSON Schema for one Zod property when toJSONSchema is unavailable; in: extractZodObjectSchema, out: a schema fragment.
|
|
163
164
|
function inferZodPropertyType(prop: any): Record<string, unknown> {
|
|
164
165
|
const unwrapped = prop.unwrap?.() || prop._def?.innerType || prop;
|
|
165
166
|
const description = prop.description || unwrapped.description || prop._def?.description;
|
|
@@ -34,6 +34,7 @@ export function isSubagentResult(value: unknown): value is SubagentResult {
|
|
|
34
34
|
return !!value && typeof value === "object" && (value as Record<symbol, unknown>)[SUBAGENT_RESULT_BRAND] === true;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// A finite number >= 0, else 0; in: normalizeSubagentMetadata, out: a safe usage count.
|
|
37
38
|
const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0);
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# src/types/
|
|
2
|
+
|
|
3
|
+
Shared contracts. Types only, plus a few tiny helpers that belong to a type.
|
|
4
|
+
|
|
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
|
+
|
|
16
|
+
## Invariants
|
|
17
|
+
- Adding a field to a public type is a public API change: update `README.md` and `docs/`.
|
|
18
|
+
- Abort means `signal.aborted` or `name === "AbortError"`, never a message match.
|
|
19
|
+
- Error messages are one line and ANSI-free.
|
|
20
|
+
- No logic here beyond `normalizeFinishReason`, `isTruncatedArguments` and the error classes.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# src/utils/
|
|
2
|
+
|
|
3
|
+
Small shared helpers. Each file has one job.
|
|
4
|
+
|
|
5
|
+
| File | What it does |
|
|
6
|
+
| :-- | :-- |
|
|
7
|
+
| `durable.ts` | The ONE disk writer (`writeFileDurable`: temp, fsync, `.bak`, rename, folder fsync), `readWithBackup`, and `lockFolder` (one process per folder, stale-lock takeover, temp sweep). |
|
|
8
|
+
| `home.ts` | `resolveHome` / `setHome`: the single folder everything is stored in. |
|
|
9
|
+
| `env.ts` | Process-env lookups and API key resolution. No `globalThis` fallback. |
|
|
10
|
+
| `headers.ts` | Session-affinity headers, opt-in attribution, browser stripping, package version fallback. |
|
|
11
|
+
| `media.ts` | Normalizes a path, URL, data URL, base64 or bytes to base64 and a MIME type; magic-byte sniffing. |
|
|
12
|
+
| `safe-io.ts` | `fileAccess.roots` containment and the guarded remote fetch (private hosts refused, redirects re-checked, size cap). |
|
|
13
|
+
| `raw-capture.ts` | The `captureRaw` policy for `AgentResponse.raw`. |
|
|
14
|
+
| `cache.ts` | Cache-key clamp (64 chars) and the central secret redaction. |
|
|
15
|
+
| `errors.ts` | `toConciseProviderError` and pre-network modality guards. |
|
|
16
|
+
| `retry.ts` | `withRetries`, `isTransientError`, `isRetryableRun`, `sleep`. Structured data only (status, code, flags), never message text. |
|
|
17
|
+
| `session.ts` | Id helpers (`createSessionId`, `hashSessionPart`, `createTrackingId`, run/turn ids), `parseModelRef`, `previewPrompt`. |
|
|
18
|
+
| `serialization.ts` | `toJsonSafe`, `safeStringify`, `escapeXml`. |
|
|
19
|
+
| `base64.ts`, `thought-signature.ts` | Runtime-agnostic base64; Gemini thought-signature helpers. |
|
|
20
|
+
|
|
21
|
+
## Invariants
|
|
22
|
+
- Never write state with a bare `writeFileSync`; use `writeFileDurable`.
|
|
23
|
+
- `media.ts` and `safe-io.ts` import `node:*` lazily so browser bundles keep working.
|
|
24
|
+
- Redaction is central (`cache.ts`); adapters must not keep their own deny-lists.
|
|
25
|
+
- `AGENT_ACCEL_HOME` is read at import time; `setHome()` after import relies on the catalog reload hook.
|
package/src/utils/base64.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Runtime-agnostic base64 helpers — no Node Buffer, safe in browsers. */
|
|
2
2
|
|
|
3
|
+
/** Decodes base64 (whitespace ignored) to bytes, with Buffer when present, else atob; in: media and session code, out: a Uint8Array. */
|
|
3
4
|
export function base64ToBytes(b64: string): Uint8Array {
|
|
4
5
|
const clean = b64.replace(/\s/g, "");
|
|
5
6
|
const nodeBuffer = (globalThis as any)?.Buffer;
|
|
@@ -13,6 +14,7 @@ export function base64ToBytes(b64: string): Uint8Array {
|
|
|
13
14
|
return out;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/** Encodes bytes as base64, with Buffer when present, else btoa in chunks; in: media and session code, out: a string. */
|
|
16
18
|
export function bytesToBase64(bytes: Uint8Array): string {
|
|
17
19
|
const nodeBuffer = (globalThis as any)?.Buffer;
|
|
18
20
|
if (nodeBuffer && typeof nodeBuffer.from === "function") {
|
package/src/utils/cache.ts
CHANGED
|
@@ -28,11 +28,13 @@ const SENSITIVE_VALUE_PATTERNS: RegExp[] = [
|
|
|
28
28
|
/(^|[^A-Za-z0-9])AIza[A-Za-z0-9_-]{10,}/,
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
+
// True when a header name contains key, token, secret, cookie or authorization; in: redactHeadersCentral, out: a boolean.
|
|
31
32
|
function headerNameIsSensitive(name: string): boolean {
|
|
32
33
|
const lower = String(name ?? "").toLowerCase();
|
|
33
34
|
return REDACT_HEADER_SUBSTRINGS.some((s) => lower.includes(s));
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// True when a string looks like a credential (sk-, gsk_, xox, AIza, long Bearer token); in: the redactors, out: a boolean.
|
|
36
38
|
function valueLooksSensitive(value: unknown): boolean {
|
|
37
39
|
if (typeof value !== "string") return false;
|
|
38
40
|
const v = value.trim();
|
package/src/utils/errors.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { getModelFromCatalog } from "../models/catalog.ts";
|
|
|
3
3
|
|
|
4
4
|
const CONCISE = "__agentAccelConcise";
|
|
5
5
|
|
|
6
|
+
// HTTP status from the shapes provider errors carry, else undefined; in: toConciseProviderError, out: a status.
|
|
6
7
|
function readStatus(error: any): number | undefined {
|
|
7
8
|
const s =
|
|
8
9
|
error?.statusCode ??
|
|
@@ -14,6 +15,7 @@ function readStatus(error: any): number | undefined {
|
|
|
14
15
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// The request URL of an error without its query string; in: toConciseProviderError, out: a string.
|
|
17
19
|
function readUrl(error: any): string {
|
|
18
20
|
const raw =
|
|
19
21
|
(typeof error?.url === "string" && error.url) ||
|
|
@@ -22,6 +24,7 @@ function readUrl(error: any): string {
|
|
|
22
24
|
return raw.split("?")[0];
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
// The provider's own message from the error's data, body or message; in: toConciseProviderError, out: one trimmed line.
|
|
25
28
|
function readProviderMessage(error: any): string {
|
|
26
29
|
const data = error?.data;
|
|
27
30
|
if (typeof data?.error?.message === "string" && data.error.message.trim()) return data.error.message.trim();
|
|
@@ -39,6 +42,7 @@ function readProviderMessage(error: any): string {
|
|
|
39
42
|
return String(error?.message ?? error).slice(0, 300);
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
// `provider/model` without doubling an existing prefix; in: the error messages, out: a label.
|
|
42
46
|
function displayModel(providerId: string, modelId: string): string {
|
|
43
47
|
if (modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`)) return modelId;
|
|
44
48
|
return `${providerId}/${modelId}`;
|
package/src/utils/headers.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ProviderId } from "../types/model.ts";
|
|
2
2
|
import { clampCacheKey } from "./cache.ts";
|
|
3
3
|
|
|
4
|
+
/** True in a browser, worker or React Native scope, where custom headers cause CORS preflights; in: header building, out: a boolean. */
|
|
4
5
|
export function isBrowserRuntime(): boolean {
|
|
5
6
|
try {
|
|
6
7
|
const g = globalThis as any;
|
|
@@ -23,9 +24,10 @@ export function getPackageVersion(): string {
|
|
|
23
24
|
const v = (globalThis as any).process?.env?.npm_package_version;
|
|
24
25
|
if (typeof v === "string" && v) return v;
|
|
25
26
|
} catch {}
|
|
26
|
-
return "0.5.
|
|
27
|
+
return "0.5.7";
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// User-Agent string with version and platform; in: AGENT_ACCEL_USER_AGENT, out: a string.
|
|
29
31
|
function getAgentAccelUserAgent(): string {
|
|
30
32
|
const version = getPackageVersion();
|
|
31
33
|
try {
|
|
@@ -125,6 +127,7 @@ const BROWSER_DROPPED = new Set([
|
|
|
125
127
|
"x-goog-api-client",
|
|
126
128
|
]);
|
|
127
129
|
|
|
130
|
+
// Drops the headers a browser may not or should not send; in: buildSessionHeaders, out: the safe headers.
|
|
128
131
|
function stripForBrowser(headers: Record<string, string>): Record<string, string> {
|
|
129
132
|
const out: Record<string, string> = {};
|
|
130
133
|
for (const [k, v] of Object.entries(headers)) {
|
package/src/utils/media.ts
CHANGED
|
@@ -38,6 +38,7 @@ export function inferMimeType(input: string, fallback = "application/octet-strea
|
|
|
38
38
|
return fallback;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// True when the bytes spell `text` at `start`; in: sniffMimeType, out: a boolean.
|
|
41
42
|
const ascii = (bytes: Uint8Array, start: number, text: string): boolean =>
|
|
42
43
|
bytes.length >= start + text.length && [...text].every((ch, i) => bytes[start + i] === ch.charCodeAt(0));
|
|
43
44
|
|
package/src/utils/raw-capture.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ProviderRawData } from "../types/model.ts";
|
|
|
3
3
|
/** How much of the wire exchange `AgentResponse.raw` keeps (see `AgentConfig.captureRaw`). */
|
|
4
4
|
export type CaptureRawMode = false | "redacted" | true;
|
|
5
5
|
|
|
6
|
+
// Serialized length of a value, 0 if it cannot be serialized; in: the body summaries, out: a number.
|
|
6
7
|
function byteLength(value: unknown): number {
|
|
7
8
|
try {
|
|
8
9
|
return JSON.stringify(value)?.length ?? 0;
|
|
@@ -11,6 +12,7 @@ function byteLength(value: unknown): number {
|
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// Tool names from a request body's `tools`, if any; in: summarizeRequestBody, out: names or undefined.
|
|
14
16
|
function toolNames(tools: unknown): string[] | undefined {
|
|
15
17
|
if (!Array.isArray(tools)) return undefined;
|
|
16
18
|
const names = tools
|
|
@@ -34,6 +36,7 @@ function summarizeRequestBody(body: unknown): unknown {
|
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
// Content-free summary of a response body (size, id, status); in: applyCaptureRaw, out: the summary.
|
|
37
40
|
function summarizeResponseBody(body: unknown): unknown {
|
|
38
41
|
if (body === null || body === undefined) return body;
|
|
39
42
|
const b = (typeof body === "object" ? body : {}) as Record<string, unknown>;
|
package/src/utils/retry.ts
CHANGED
|
@@ -43,6 +43,7 @@ export function isRetryableRun(error: unknown): boolean {
|
|
|
43
43
|
return isTransientError(error);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** The server's wait hint in ms from `retryAfterMs` or a Retry-After header, capped at 60 s; in: withRetries, out: ms or undefined. */
|
|
46
47
|
export function readRetryAfterMs(error: unknown): number | undefined {
|
|
47
48
|
const err = error as Record<string, unknown>;
|
|
48
49
|
// Preferred: milliseconds attached by the adapter from the Retry-After header.
|
package/src/utils/safe-io.ts
CHANGED
|
@@ -98,6 +98,7 @@ export async function enforceFileAccess(messages: Message[], roots: readonly str
|
|
|
98
98
|
// Remote fetch guard (SSRF + size cap)
|
|
99
99
|
// ---------------------------------------------------------------------------
|
|
100
100
|
|
|
101
|
+
// True for the private, loopback, link-local and CGNAT IPv4 ranges; in: isPrivateAddress, out: a boolean.
|
|
101
102
|
function ipv4Private(a: number, b: number): boolean {
|
|
102
103
|
return (
|
|
103
104
|
a === 0 ||
|
|
@@ -124,6 +125,7 @@ export function isPrivateAddress(address: string): boolean {
|
|
|
124
125
|
return false;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
// True for localhost, .local/.internal names and private addresses; in: assertRemoteUrlAllowed, out: a boolean.
|
|
127
129
|
function isPrivateHostname(host: string): boolean {
|
|
128
130
|
return (
|
|
129
131
|
host === "localhost" ||
|
|
@@ -134,6 +136,7 @@ function isPrivateHostname(host: string): boolean {
|
|
|
134
136
|
);
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
// Throws RemoteFetchError for a non-http(s) URL or, unless private hosts are allowed, a private host (also by DNS result); in: fetchRemoteBytes on every hop, out: nothing or a thrown error.
|
|
137
140
|
async function assertRemoteUrlAllowed(url: URL, allowPrivateNetwork: boolean): Promise<void> {
|
|
138
141
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
139
142
|
throw new RemoteFetchError("blocked", `[Agent Accelerator] Only http(s) URLs are allowed (got ${url.protocol}).`, url.href);
|
|
@@ -156,6 +159,7 @@ async function assertRemoteUrlAllowed(url: URL, allowPrivateNetwork: boolean): P
|
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
// Reads a response body up to `maxBytes`, throwing too_large as soon as the declared or streamed size passes it; in: fetchRemoteBytes, out: the bytes.
|
|
159
163
|
async function readCapped(res: Response, maxBytes: number, url: string): Promise<Uint8Array> {
|
|
160
164
|
const tooLarge = () =>
|
|
161
165
|
new RemoteFetchError(
|