agent-accelerator 0.5.6 → 0.5.8
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 +10 -17
- package/package.json +1 -1
- package/src/agent/agent.ts +92 -57
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +7 -3
- package/src/agent/delegation.ts +30 -96
- package/src/agent/loop.ts +54 -56
- package/src/agent/subagent.ts +4 -4
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +13 -80
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +10 -7
- package/src/models/catalog.ts +20 -13
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +24 -4
- package/src/providers/openai-compat.ts +29 -7
- package/src/providers/openai.ts +22 -6
- package/src/providers/openrouter.ts +14 -2
- package/src/providers/registry.ts +1 -0
- package/src/providers/shared.ts +4 -4
- package/src/providers.ts +9 -53
- package/src/scheduler/architecture.md +22 -0
- package/src/scheduler/scheduler.ts +131 -56
- package/src/scheduler/store.ts +274 -54
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +37 -22
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +16 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +3 -2
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +14 -1
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/architecture.md +20 -0
- package/src/types/response.ts +8 -8
- package/src/types/scheduler.ts +2 -2
- package/src/types/tool.ts +5 -5
- package/src/update-models.ts +2 -2
- package/src/utils/architecture.md +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +6 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/errors.ts +4 -0
- package/src/utils/headers.ts +7 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/media.ts +1 -0
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +2 -1
- package/src/utils/safe-io.ts +4 -0
- package/src/utils/session.ts +10 -166
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;
|
|
@@ -164,7 +166,7 @@ function specFor(modelStr: string): ReturnType<typeof getModelFromCatalog> {
|
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/** Context window for telemetry display. Falls back to 1M like the chat CLI.
|
|
167
|
-
* @deprecated
|
|
169
|
+
* @deprecated Kept for compat; new code should use
|
|
168
170
|
* `getSessionContextWindowOrUndefined` and handle `undefined` explicitly.
|
|
169
171
|
*/
|
|
170
172
|
export function getSessionContextWindow(modelStr: string): number {
|
|
@@ -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,12 +569,13 @@ 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;
|
|
568
576
|
const text = (raw ?? "").trim();
|
|
569
577
|
if (!text) return null;
|
|
570
|
-
//
|
|
578
|
+
// Oversized payloads are rejected, but flagged as over-limit (NOT corrupt).
|
|
571
579
|
if (text.length > maxBytes) return OVER_LIMIT;
|
|
572
580
|
try {
|
|
573
581
|
// Legacy JSONL: lines shaped {type: session|mainModel|subagentModel|metrics|message|...}
|
|
@@ -618,7 +626,7 @@ function parseSession(raw: string, limits?: SessionLimits): PersistedAgentSessio
|
|
|
618
626
|
}
|
|
619
627
|
}
|
|
620
628
|
if (!sessionId && messages.length === 0) return null;
|
|
621
|
-
//
|
|
629
|
+
// Oversize guard — flagged as over-limit, not corrupt.
|
|
622
630
|
if (messages.length > maxMessages) return OVER_LIMIT;
|
|
623
631
|
return {
|
|
624
632
|
version: 1,
|
|
@@ -637,7 +645,7 @@ function parseSession(raw: string, limits?: SessionLimits): PersistedAgentSessio
|
|
|
637
645
|
}
|
|
638
646
|
const obj: any = JSON.parse(text);
|
|
639
647
|
if (!obj || typeof obj !== "object") return null;
|
|
640
|
-
//
|
|
648
|
+
// Version check — reject unknown versions, accept legacy shapes
|
|
641
649
|
// that predate versioning (no version field).
|
|
642
650
|
if ("version" in obj && (obj as { version?: unknown }).version !== 1) return null;
|
|
643
651
|
// Very old single-JSON shape without version: {sessionId, model, context:{messages}}
|
|
@@ -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;
|
|
@@ -966,7 +978,7 @@ export function saveSessionDir(
|
|
|
966
978
|
agent: SessionAgentLike,
|
|
967
979
|
telemetry?: SessionTelemetry | SessionTotals | null
|
|
968
980
|
): string {
|
|
969
|
-
//
|
|
981
|
+
// sessionDirFor already enforces assertValidSessionId.
|
|
970
982
|
const sessionDir = sessionDirFor(rootDir, agent.sessionId);
|
|
971
983
|
try {
|
|
972
984
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
@@ -1127,7 +1139,7 @@ export function findLatestSessionDir(rootDir: string): string | null {
|
|
|
1127
1139
|
}
|
|
1128
1140
|
|
|
1129
1141
|
// ---------------------------------------------------------------------------
|
|
1130
|
-
//
|
|
1142
|
+
// Session validation (append-only; deserializeSession above only gained
|
|
1131
1143
|
// size/version guards, no behavior change otherwise).
|
|
1132
1144
|
// ---------------------------------------------------------------------------
|
|
1133
1145
|
|
|
@@ -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";
|
|
@@ -1143,9 +1156,9 @@ function sessionLoadError(message: string): Error {
|
|
|
1143
1156
|
}
|
|
1144
1157
|
|
|
1145
1158
|
/**
|
|
1146
|
-
* Manual Zod-like validation for session snapshots (
|
|
1159
|
+
* Manual Zod-like validation for session snapshots (no new deps).
|
|
1147
1160
|
* Checks `version===1`, string `sessionId`, array `messages`, and oversize
|
|
1148
|
-
* (
|
|
1161
|
+
* (more than `MAX_SESSION_MESSAGES` = 50,000 messages or `MAX_SESSION_JSON_BYTES` = 100 MB of JSON). Throws a `SessionLoadError`-named Error
|
|
1149
1162
|
* on failure; returns the typed session on success.
|
|
1150
1163
|
*/
|
|
1151
1164
|
export function validateSessionData(data: unknown): PersistedAgentSession {
|
|
@@ -1178,12 +1191,12 @@ export function validateSessionData(data: unknown): PersistedAgentSession {
|
|
|
1178
1191
|
}
|
|
1179
1192
|
|
|
1180
1193
|
// ---------------------------------------------------------------------------
|
|
1181
|
-
//
|
|
1194
|
+
// Context-window lookup without the legacy 1M fallback.
|
|
1182
1195
|
// ---------------------------------------------------------------------------
|
|
1183
1196
|
|
|
1184
1197
|
/**
|
|
1185
|
-
* Returns the catalog context window for a model, or `undefined` when unknown
|
|
1186
|
-
*
|
|
1198
|
+
* Returns the catalog context window for a model, or `undefined` when unknown.
|
|
1199
|
+
* Prefer this over `getSessionContextWindow` for new code.
|
|
1187
1200
|
*/
|
|
1188
1201
|
export function getSessionContextWindowOrUndefined(modelStr: string): number | undefined {
|
|
1189
1202
|
try {
|
|
@@ -1197,7 +1210,7 @@ export function getSessionContextWindowOrUndefined(modelStr: string): number | u
|
|
|
1197
1210
|
}
|
|
1198
1211
|
|
|
1199
1212
|
// ---------------------------------------------------------------------------
|
|
1200
|
-
//
|
|
1213
|
+
// Telemetry bus lives in `../telemetry.ts` (dependency-free, so low-level
|
|
1201
1214
|
// modules such as the catalog cache and event stream can emit without import
|
|
1202
1215
|
// cycles). Re-exported here for backward compatibility.
|
|
1203
1216
|
// ---------------------------------------------------------------------------
|
|
@@ -1205,7 +1218,7 @@ import { telemetryBus } from "../telemetry.ts";
|
|
|
1205
1218
|
export { telemetryBus };
|
|
1206
1219
|
export type { TelemetryEvent, TelemetryListener } from "../telemetry.ts";
|
|
1207
1220
|
|
|
1208
|
-
/** Reports a usage/cost anomaly via the global hook + telemetry bus
|
|
1221
|
+
/** Reports a usage/cost anomaly via the global hook + telemetry bus. Never logs. */
|
|
1209
1222
|
export function reportUsageAnomaly(detail: { type: string; [k: string]: unknown }): void {
|
|
1210
1223
|
try {
|
|
1211
1224
|
(globalThis as unknown as { __agentAccelAnomaly?: (d: unknown) => void }).__agentAccelAnomaly?.(detail);
|
|
@@ -1220,10 +1233,10 @@ export function reportUsageAnomaly(detail: { type: string; [k: string]: unknown
|
|
|
1220
1233
|
}
|
|
1221
1234
|
|
|
1222
1235
|
// ---------------------------------------------------------------------------
|
|
1223
|
-
//
|
|
1236
|
+
// Canonical usage invariants.
|
|
1224
1237
|
// ---------------------------------------------------------------------------
|
|
1225
1238
|
/**
|
|
1226
|
-
* Canonical usage invariants (provider-agnostic
|
|
1239
|
+
* Canonical usage invariants (provider-agnostic):
|
|
1227
1240
|
* - all token counts are finite integers >= 0;
|
|
1228
1241
|
* - `cachedTokens` / `cacheReadTokens` are a SUBSET of `inputTokens`
|
|
1229
1242
|
* (cache hits cannot exceed input; aggregates clamp — see `accumulateUsage`
|
|
@@ -1274,13 +1287,13 @@ export function assertUsageInvariants(usage: TokenUsage): void {
|
|
|
1274
1287
|
}
|
|
1275
1288
|
|
|
1276
1289
|
// ---------------------------------------------------------------------------
|
|
1277
|
-
//
|
|
1290
|
+
// One cost model — integer micro-USD accumulation.
|
|
1278
1291
|
// ---------------------------------------------------------------------------
|
|
1279
1292
|
|
|
1280
|
-
/** Where a priced cost came from
|
|
1293
|
+
/** Where a priced cost came from. Unknown stays unknown, never 0. */
|
|
1281
1294
|
export type CostSource = "provider" | "catalog" | "unknown";
|
|
1282
1295
|
|
|
1283
|
-
/** Integer micro-USD breakdown behind a priced cost
|
|
1296
|
+
/** Integer micro-USD breakdown behind a priced cost. */
|
|
1284
1297
|
export interface CostBreakdown {
|
|
1285
1298
|
inputMicroUSD: number;
|
|
1286
1299
|
outputMicroUSD: number;
|
|
@@ -1302,7 +1315,7 @@ export type PricedCost = NonNullable<TokenUsage["cost"]> & {
|
|
|
1302
1315
|
};
|
|
1303
1316
|
|
|
1304
1317
|
/**
|
|
1305
|
-
* Service-tier price multipliers
|
|
1318
|
+
* Service-tier price multipliers. Aligned with pi
|
|
1306
1319
|
* (`api/openai-responses.ts:387`): `flex` trades latency for ~0.5x price,
|
|
1307
1320
|
* `priority`/`fast` reserve capacity at ~2x (`2.5x` for `gpt-5.5`).
|
|
1308
1321
|
* Standard/default is 1.0. Estimates because vendors do not publish exact
|
|
@@ -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,12 +1341,13 @@ 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
|
}
|
|
1333
1348
|
|
|
1334
1349
|
/**
|
|
1335
|
-
* Resolves effective per-1M pricing for a usage snapshot
|
|
1350
|
+
* Resolves effective per-1M pricing for a usage snapshot. When
|
|
1336
1351
|
* `usage.inputTokens > 200_000` and the catalog carries `cost.tiers` or
|
|
1337
1352
|
* `cost.context_over_200k`, the over-200k tier prices apply; otherwise base
|
|
1338
1353
|
* `pricing.*` (falling back to legacy `cost.*`) applies.
|
|
@@ -1389,7 +1404,7 @@ function resolveEffectivePricing(
|
|
|
1389
1404
|
}
|
|
1390
1405
|
|
|
1391
1406
|
/**
|
|
1392
|
-
* Prices one usage snapshot with integer micro-USD accumulation
|
|
1407
|
+
* Prices one usage snapshot with integer micro-USD accumulation.
|
|
1393
1408
|
*
|
|
1394
1409
|
* - A provider-reported `usage.cost.totalCost` (a number, 0 included: free or cached) wins (source `provider`).
|
|
1395
1410
|
* - Else catalog pricing via `resolveEffectivePricing` (source `catalog`).
|
|
@@ -1483,7 +1498,7 @@ export function priceUsage(
|
|
|
1483
1498
|
}
|
|
1484
1499
|
|
|
1485
1500
|
/**
|
|
1486
|
-
* Computes and attaches catalog pricing to a usage snapshot
|
|
1501
|
+
* Computes and attaches catalog pricing to a usage snapshot.
|
|
1487
1502
|
* Keeps provider-reported costs untouched; returns the cost (or undefined
|
|
1488
1503
|
* when unknown). Never fabricates a 0 object for unknown pricing.
|
|
1489
1504
|
*/
|
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()`.
|
|
@@ -4,7 +4,7 @@ import { telemetryBus } from "../telemetry.ts";
|
|
|
4
4
|
/** Listener invoked for a matching stream event type or `*`. */
|
|
5
5
|
export type StreamEventListener = (event: StreamEvent) => void;
|
|
6
6
|
|
|
7
|
-
/** Options for AssistantMessageEventStream
|
|
7
|
+
/** Options for AssistantMessageEventStream. */
|
|
8
8
|
export interface EventStreamOptions {
|
|
9
9
|
/**
|
|
10
10
|
* When false, `partialText`/`partialThinking` cumulative fields are stripped
|
|
@@ -25,11 +25,12 @@ type Resolver = {
|
|
|
25
25
|
reject: (err: Error) => void;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
/** Default max buffered events
|
|
28
|
+
/** Default max buffered events. */
|
|
29
29
|
export const MAX_BUFFERED_EVENTS = 1000;
|
|
30
|
-
/** Default max approx bytes per delta event
|
|
30
|
+
/** Default max approx bytes per delta event. */
|
|
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,9 +40,10 @@ 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
|
-
// per-drop would spam and break no-console policy
|
|
46
|
+
// per-drop would spam and break no-console policy.
|
|
45
47
|
telemetryBus.emitWarning(detail.type, `[Agent Accelerator] stream anomaly: ${detail.type}`, detail);
|
|
46
48
|
try {
|
|
47
49
|
(globalThis as unknown as { __agentAccelAnomaly?: (d: unknown) => void }).__agentAccelAnomaly?.(detail);
|
|
@@ -66,7 +68,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
66
68
|
private readonly buffered: boolean;
|
|
67
69
|
private readonly maxBufferedEvents: number;
|
|
68
70
|
private readonly maxEventBytes: number;
|
|
69
|
-
/** First listener exception captured for inspection
|
|
71
|
+
/** First listener exception captured for inspection. */
|
|
70
72
|
private listenerError: Error | null = null;
|
|
71
73
|
/** Count of events dropped by bounds (buffer cap / oversize / unbuffered). */
|
|
72
74
|
private droppedEvents = 0;
|
|
@@ -105,7 +107,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
105
107
|
return this.listenerError;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
/** Number of events dropped by
|
|
110
|
+
/** Number of events dropped by bounds. */
|
|
109
111
|
getDroppedCount(): number {
|
|
110
112
|
return this.droppedEvents;
|
|
111
113
|
}
|
|
@@ -114,7 +116,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
114
116
|
push(event: StreamEvent): void {
|
|
115
117
|
if (this.finished) return;
|
|
116
118
|
|
|
117
|
-
//
|
|
119
|
+
// 1MB oversize guard — drop oversized deltas (no console).
|
|
118
120
|
if (approxEventBytes(event) > this.maxEventBytes) {
|
|
119
121
|
this.droppedEvents++;
|
|
120
122
|
reportStreamAnomaly({ type: "stream_event_oversize", eventType: event.type });
|
|
@@ -130,7 +132,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
130
132
|
|
|
131
133
|
if (out.type === "error") {
|
|
132
134
|
this.error = (out.error instanceof Error ? out.error : new Error(String(out.error))) ?? new Error("Unknown error in stream");
|
|
133
|
-
// Single error semantic
|
|
135
|
+
// Single error semantic: deliver the error event to listeners,
|
|
134
136
|
// reject result(), mark finished, reject pending iterators, and queue
|
|
135
137
|
// the error event so a late iterator observes it before failing.
|
|
136
138
|
this.deliverToListeners(out);
|
|
@@ -154,13 +156,14 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
154
156
|
resolver.resolve({ value: out, done: false });
|
|
155
157
|
} else {
|
|
156
158
|
// No waiter right now (typical between `next()` polls of a burst):
|
|
157
|
-
// buffer for the iterator with a hard cap
|
|
159
|
+
// buffer for the iterator with a hard cap. Overflow drops the
|
|
158
160
|
// oldest with an anomaly report — see `enqueueBounded`. A stream built
|
|
159
161
|
// with `buffer: false` has no iterator to feed and keeps nothing.
|
|
160
162
|
if (this.buffered) this.enqueueBounded(out);
|
|
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,10 +190,11 @@ 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) {
|
|
193
|
-
// Drop oldest buffered event to bound memory
|
|
197
|
+
// Drop oldest buffered event to bound memory. Error events are
|
|
194
198
|
// never dropped: if the oldest is an error, drop the next-oldest
|
|
195
199
|
// non-error instead; if the queue is all errors, keep them.
|
|
196
200
|
if (this.queue[0]?.type === "error") {
|
|
@@ -209,7 +213,7 @@ export class AssistantMessageEventStream implements AsyncIterable<StreamEvent> {
|
|
|
209
213
|
end(finalResponse?: AgentResponse): void {
|
|
210
214
|
if (this.finished) return;
|
|
211
215
|
if (!finalResponse) {
|
|
212
|
-
//
|
|
216
|
+
// Never hang result() — ending without a response is a
|
|
213
217
|
// programming error, so fail loudly instead of leaving consumers
|
|
214
218
|
// waiting forever.
|
|
215
219
|
this.fail(new Error("end() requires response"));
|
|
@@ -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-file.ts
CHANGED
|
@@ -1,34 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Opt-in disk sink for the telemetry bus (`AgentConfig.telemetry.persist`).
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* Opt-in disk sink for the telemetry bus (`AgentConfig.telemetry.persist`). Appends one JSON line per event.
|
|
3
|
+
* An event that carries the id of a persisted session (`data.sessionId`) goes to that session's own file,
|
|
4
|
+
* `<session folder>/telemetry.ndjson`, so a session's trace sits next to its journal and tool logs; every other event
|
|
5
|
+
* goes to `<home>/telemetry/<YYYY-MM-DD>.ndjson`. Nothing is ever written outside a home folder: a session folder
|
|
6
|
+
* that is not inside a home that enabled telemetry (a `persist.dir` pointing elsewhere) falls back to the daily file.
|
|
7
|
+
* Each event is a single synchronous append, so a process crash keeps every event already emitted: a
|
|
8
|
+
* `tool.call_started` with no ending event was in flight, and the last `tool.progress` shows how far it got. Power loss
|
|
9
|
+
* is not covered (no fsync, by design: this is observability, not state). `turn.thinking` (full reasoning text) is never
|
|
10
|
+
* written. Several processes may enable the same home: each event is one append, so lines never mix.
|
|
9
11
|
*/
|
|
10
12
|
import * as fs from "node:fs";
|
|
11
13
|
import * as path from "node:path";
|
|
12
14
|
import { telemetryBus } from "./telemetry.ts";
|
|
15
|
+
import type { TelemetryEvent } from "./telemetry.ts";
|
|
13
16
|
import { safeStringify } from "./utils/serialization.ts";
|
|
14
|
-
import { lockFolder } from "./utils/durable.ts";
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
/** Homes (resolved paths) that asked for telemetry. */
|
|
19
|
+
const homes = new Set<string>();
|
|
20
|
+
/** Where each persisted session lives, by session id. */
|
|
21
|
+
const sessionFolders = new Map<string, string>();
|
|
22
|
+
let unsubscribe: (() => void) | undefined;
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
// True when `folder` is `home` itself or below it; in: the routing decision, out: a boolean.
|
|
25
|
+
function inside(home: string, folder: string): boolean {
|
|
26
|
+
const rel = path.relative(home, folder);
|
|
27
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Appends one line to a file, creating its folder; a full disk must never fail a run, and reporting it would re-enter this listener; in: the listener, out: the file.
|
|
31
|
+
function append(file: string, line: string): void {
|
|
32
|
+
try {
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
34
|
+
fs.appendFileSync(file, line, { mode: 0o600 });
|
|
35
|
+
} catch {
|
|
36
|
+
// why: observability must never fail a run.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The bus listener: skips reasoning text, writes a session's event to its own folder when that folder is inside an enabled home, else to the daily file of every enabled home; in: each bus event, out: NDJSON files.
|
|
41
|
+
function onEvent(event: TelemetryEvent): void {
|
|
42
|
+
if (event.code === "turn.thinking") return;
|
|
43
|
+
const line = safeStringify(event) + "\n";
|
|
44
|
+
const id = (event.data as { sessionId?: unknown } | undefined)?.sessionId;
|
|
45
|
+
const folder = typeof id === "string" ? sessionFolders.get(id) : undefined;
|
|
46
|
+
if (folder && [...homes].some((h) => inside(h, folder))) {
|
|
47
|
+
append(path.join(folder, "telemetry.ndjson"), line);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const day = new Date(event.timestamp).toISOString().slice(0, 10);
|
|
51
|
+
for (const home of homes) {
|
|
52
|
+
if (!fs.existsSync(home)) {
|
|
53
|
+
homes.delete(home); // a deleted home is not recreated by a stray event
|
|
54
|
+
continue;
|
|
32
55
|
}
|
|
33
|
-
|
|
56
|
+
append(path.join(home, "telemetry", `${day}.ndjson`), line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Starts appending bus events for `home` (idempotent per folder); one listener serves every enabled home. */
|
|
61
|
+
export function persistTelemetry(home: string): void {
|
|
62
|
+
const resolved = path.resolve(home);
|
|
63
|
+
unsubscribe = telemetryBus.onEvent(onEvent); // listeners are a Set, so this is idempotent, and it restores the sink after `telemetryBus.clear()`
|
|
64
|
+
if (homes.has(resolved)) return;
|
|
65
|
+
homes.add(resolved);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Remembers where a persisted session lives, so its events are written next to it; in: the Agent constructor and `importSession`, out: the routing map. */
|
|
69
|
+
export function registerSessionFolder(sessionId: string, folder: string): void {
|
|
70
|
+
sessionFolders.set(sessionId, path.resolve(folder));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Forgets a session's folder (its events fall back to the daily file); in: `disposeSession` and `importSession`, out: the routing map. */
|
|
74
|
+
export function unregisterSessionFolder(sessionId: string): void {
|
|
75
|
+
sessionFolders.delete(sessionId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Forgets every enabled home and session and removes the listener. Test hook (not part of the public entry). */
|
|
79
|
+
export function resetTelemetryFile(): void {
|
|
80
|
+
homes.clear();
|
|
81
|
+
sessionFolders.clear();
|
|
82
|
+
unsubscribe?.();
|
|
83
|
+
unsubscribe = undefined;
|
|
34
84
|
}
|
package/src/telemetry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-process telemetry bus
|
|
2
|
+
* In-process telemetry bus. Library code routes warnings, anomalies and
|
|
3
3
|
* per-turn traces here instead of `console.*`. The default has zero listeners,
|
|
4
4
|
* so emission is a no-op; CLIs, tests and observability layers attach with
|
|
5
5
|
* `telemetryBus.onEvent`. Intentionally dependency-free: every module in the
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* it without creating an import cycle.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
/** Telemetry event emitted via the in-process bus
|
|
10
|
+
/** Telemetry event emitted via the in-process bus. */
|
|
11
11
|
export interface TelemetryEvent {
|
|
12
12
|
code: string;
|
|
13
13
|
message: string;
|
|
@@ -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).
|