@axlsdk/axl 0.14.0 → 0.16.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/dist/index.cjs CHANGED
@@ -110,11 +110,13 @@ var init_span_manager = __esm({
110
110
  // src/index.ts
111
111
  var index_exports = {};
112
112
  __export(index_exports, {
113
+ AXL_EVENT_TYPES: () => AXL_EVENT_TYPES,
113
114
  AnthropicProvider: () => AnthropicProvider,
114
115
  AxlError: () => AxlError,
115
116
  AxlRuntime: () => AxlRuntime,
116
117
  AxlStream: () => AxlStream,
117
118
  BudgetExceededError: () => BudgetExceededError,
119
+ COST_BEARING_LEAF_TYPES: () => COST_BEARING_LEAF_TYPES,
118
120
  GeminiProvider: () => GeminiProvider,
119
121
  GuardrailError: () => GuardrailError,
120
122
  InMemoryVectorStore: () => InMemoryVectorStore,
@@ -129,6 +131,8 @@ __export(index_exports, {
129
131
  OpenAIResponsesProvider: () => OpenAIResponsesProvider,
130
132
  ProviderRegistry: () => ProviderRegistry,
131
133
  QuorumNotMet: () => QuorumNotMet,
134
+ REDACTED: () => REDACTED,
135
+ REDACTION_RULES: () => REDACTION_RULES,
132
136
  RedisStore: () => RedisStore,
133
137
  SQLiteStore: () => SQLiteStore,
134
138
  Session: () => Session,
@@ -141,7 +145,12 @@ __export(index_exports, {
141
145
  agent: () => agent,
142
146
  createSpanManager: () => createSpanManager,
143
147
  defineConfig: () => defineConfig,
148
+ eventCostContribution: () => eventCostContribution,
144
149
  extractJson: () => extractJson,
150
+ isCostBearingLeaf: () => isCostBearingLeaf,
151
+ isRootLevel: () => isRootLevel,
152
+ parsePartialJson: () => parsePartialJson,
153
+ redactEvent: () => redactEvent,
145
154
  resolveThinkingOptions: () => resolveThinkingOptions,
146
155
  tool: () => tool,
147
156
  workflow: () => workflow,
@@ -1463,6 +1472,41 @@ ${jsonInstruction}` : jsonInstruction;
1463
1472
  };
1464
1473
 
1465
1474
  // src/providers/gemini.ts
1475
+ var GEMINI_DISALLOWED_SCHEMA_KEYS = /* @__PURE__ */ new Set([
1476
+ "additionalProperties",
1477
+ "$schema",
1478
+ "$ref",
1479
+ "$defs",
1480
+ "definitions",
1481
+ "not",
1482
+ "allOf",
1483
+ "patternProperties",
1484
+ "unevaluatedProperties",
1485
+ "unevaluatedItems"
1486
+ ]);
1487
+ function sanitizeSchemaForGemini(schema) {
1488
+ if (Array.isArray(schema)) {
1489
+ return schema.map((item) => sanitizeSchemaForGemini(item));
1490
+ }
1491
+ if (schema === null || typeof schema !== "object") {
1492
+ return schema;
1493
+ }
1494
+ const src = schema;
1495
+ const out = {};
1496
+ for (const [key, value] of Object.entries(src)) {
1497
+ if (GEMINI_DISALLOWED_SCHEMA_KEYS.has(key)) continue;
1498
+ if (key === "oneOf") {
1499
+ out.anyOf = sanitizeSchemaForGemini(value);
1500
+ continue;
1501
+ }
1502
+ if (key === "const") {
1503
+ if (!("enum" in src)) out.enum = [value];
1504
+ continue;
1505
+ }
1506
+ out[key] = sanitizeSchemaForGemini(value);
1507
+ }
1508
+ return out;
1509
+ }
1466
1510
  var GEMINI_PRICING = {
1467
1511
  "gemini-2.5-pro": [125e-8, 1e-5],
1468
1512
  "gemini-2.5-flash": [3e-7, 25e-7],
@@ -1633,7 +1677,9 @@ var GeminiProvider = class {
1633
1677
  if (options.responseFormat && options.responseFormat.type !== "text") {
1634
1678
  generationConfig.responseMimeType = "application/json";
1635
1679
  if (options.responseFormat.type === "json_schema" && options.responseFormat.json_schema?.schema) {
1636
- generationConfig.responseSchema = options.responseFormat.json_schema.schema;
1680
+ generationConfig.responseSchema = sanitizeSchemaForGemini(
1681
+ options.responseFormat.json_schema.schema
1682
+ );
1637
1683
  }
1638
1684
  }
1639
1685
  if (Object.keys(generationConfig).length > 0) {
@@ -1810,7 +1856,7 @@ var GeminiProvider = class {
1810
1856
  return {
1811
1857
  name: tool2.function.name,
1812
1858
  description: tool2.function.description,
1813
- parameters: tool2.function.parameters
1859
+ parameters: sanitizeSchemaForGemini(tool2.function.parameters)
1814
1860
  };
1815
1861
  }
1816
1862
  // ---------------------------------------------------------------------------
@@ -2069,6 +2115,7 @@ var defaultRegistry = new ProviderRegistry();
2069
2115
 
2070
2116
  // src/context.ts
2071
2117
  var import_node_async_hooks = require("async_hooks");
2118
+ var import_node_crypto = require("crypto");
2072
2119
  var import_zod = require("zod");
2073
2120
 
2074
2121
  // src/errors.ts
@@ -2112,6 +2159,21 @@ var TimeoutError = class extends AxlError {
2112
2159
  this.name = "TimeoutError";
2113
2160
  }
2114
2161
  };
2162
+ function formatBudgetCost(cost) {
2163
+ if (Number.isNaN(cost)) return "$NaN";
2164
+ if (cost === Infinity) return "$Infinity";
2165
+ if (cost === -Infinity) return "-$Infinity";
2166
+ if (cost === 0) return "$0.00";
2167
+ const sign = cost < 0 ? "-" : "";
2168
+ const abs = Math.abs(cost);
2169
+ if (abs < 1e-6) return `${sign}< $0.000001`;
2170
+ if (abs < 1e-4) {
2171
+ const [mantissa, exponent] = abs.toExponential(2).split("e");
2172
+ return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
2173
+ }
2174
+ if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
2175
+ return `${sign}$${abs.toFixed(2)}`;
2176
+ }
2115
2177
  var BudgetExceededError = class extends AxlError {
2116
2178
  limit;
2117
2179
  spent;
@@ -2119,7 +2181,9 @@ var BudgetExceededError = class extends AxlError {
2119
2181
  constructor(limit, spent, policy) {
2120
2182
  super(
2121
2183
  "BUDGET_EXCEEDED",
2122
- `Budget exceeded: spent $${spent.toFixed(4)} of $${limit.toFixed(4)} limit (policy: ${policy})`
2184
+ `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
2185
+ limit
2186
+ )} limit (policy: ${policy})`
2123
2187
  );
2124
2188
  this.name = "BudgetExceededError";
2125
2189
  this.limit = limit;
@@ -2171,6 +2235,432 @@ var ToolDenied = class extends AxlError {
2171
2235
  }
2172
2236
  };
2173
2237
 
2238
+ // src/partial-json.ts
2239
+ var MAX_DEPTH = 256;
2240
+ function parsePartialJson(input) {
2241
+ const parser = new PartialParser(input);
2242
+ parser.skipWhitespace();
2243
+ if (parser.eof()) return void 0;
2244
+ const result = parser.parseValue();
2245
+ return result.value;
2246
+ }
2247
+ var PartialParser = class {
2248
+ constructor(src) {
2249
+ this.src = src;
2250
+ }
2251
+ pos = 0;
2252
+ depth = 0;
2253
+ eof() {
2254
+ return this.pos >= this.src.length;
2255
+ }
2256
+ peek() {
2257
+ return this.src[this.pos];
2258
+ }
2259
+ advance() {
2260
+ return this.src[this.pos++];
2261
+ }
2262
+ skipWhitespace() {
2263
+ while (!this.eof()) {
2264
+ const c = this.peek();
2265
+ if (c === " " || c === "\n" || c === " " || c === "\r") {
2266
+ this.pos++;
2267
+ } else {
2268
+ break;
2269
+ }
2270
+ }
2271
+ }
2272
+ parseValue() {
2273
+ this.skipWhitespace();
2274
+ if (this.eof()) return { value: void 0, complete: false };
2275
+ const c = this.peek();
2276
+ if (c === "{" || c === "[") {
2277
+ if (++this.depth > MAX_DEPTH) {
2278
+ throw new SyntaxError(
2279
+ `Maximum nesting depth exceeded (${MAX_DEPTH}) at position ${this.pos}`
2280
+ );
2281
+ }
2282
+ try {
2283
+ return c === "{" ? this.parseObject() : this.parseArray();
2284
+ } finally {
2285
+ this.depth--;
2286
+ }
2287
+ }
2288
+ if (c === '"') return this.parseString();
2289
+ if (c === "t" || c === "f") return this.parseBool();
2290
+ if (c === "n") return this.parseNull();
2291
+ if (c === "-" || c >= "0" && c <= "9") return this.parseNumber();
2292
+ throw new SyntaxError(`Unexpected character '${c}' at position ${this.pos}`);
2293
+ }
2294
+ parseObject() {
2295
+ this.advance();
2296
+ const obj = {};
2297
+ this.skipWhitespace();
2298
+ if (this.eof()) return { value: obj, complete: false };
2299
+ if (this.peek() === "}") {
2300
+ this.advance();
2301
+ return { value: obj, complete: true };
2302
+ }
2303
+ while (!this.eof()) {
2304
+ this.skipWhitespace();
2305
+ if (this.eof()) return { value: obj, complete: false };
2306
+ if (this.peek() === "}") {
2307
+ this.advance();
2308
+ return { value: obj, complete: true };
2309
+ }
2310
+ if (this.peek() !== '"') {
2311
+ throw new SyntaxError(`Expected '"' at position ${this.pos}, got '${this.peek()}'`);
2312
+ }
2313
+ const keyResult = this.parseString();
2314
+ if (!keyResult.complete) {
2315
+ return { value: obj, complete: false };
2316
+ }
2317
+ const key = keyResult.value;
2318
+ this.skipWhitespace();
2319
+ if (this.eof()) return { value: obj, complete: false };
2320
+ if (this.peek() !== ":") {
2321
+ return { value: obj, complete: false };
2322
+ }
2323
+ this.advance();
2324
+ this.skipWhitespace();
2325
+ if (this.eof()) return { value: obj, complete: false };
2326
+ const valueResult = this.parseValue();
2327
+ if (valueResult.value !== void 0 || valueResult.complete) {
2328
+ obj[key] = valueResult.value;
2329
+ }
2330
+ if (!valueResult.complete) {
2331
+ return { value: obj, complete: false };
2332
+ }
2333
+ this.skipWhitespace();
2334
+ if (this.eof()) return { value: obj, complete: false };
2335
+ const next = this.peek();
2336
+ if (next === ",") {
2337
+ this.advance();
2338
+ continue;
2339
+ }
2340
+ if (next === "}") {
2341
+ this.advance();
2342
+ return { value: obj, complete: true };
2343
+ }
2344
+ throw new SyntaxError(`Expected ',' or '}' at position ${this.pos}, got '${next}'`);
2345
+ }
2346
+ return { value: obj, complete: false };
2347
+ }
2348
+ parseArray() {
2349
+ this.advance();
2350
+ const arr = [];
2351
+ this.skipWhitespace();
2352
+ if (this.eof()) return { value: arr, complete: false };
2353
+ if (this.peek() === "]") {
2354
+ this.advance();
2355
+ return { value: arr, complete: true };
2356
+ }
2357
+ while (!this.eof()) {
2358
+ this.skipWhitespace();
2359
+ if (this.eof()) return { value: arr, complete: false };
2360
+ if (this.peek() === "]") {
2361
+ this.advance();
2362
+ return { value: arr, complete: true };
2363
+ }
2364
+ const elemResult = this.parseValue();
2365
+ if (elemResult.value !== void 0 || elemResult.complete) {
2366
+ arr.push(elemResult.value);
2367
+ }
2368
+ if (!elemResult.complete) {
2369
+ return { value: arr, complete: false };
2370
+ }
2371
+ this.skipWhitespace();
2372
+ if (this.eof()) return { value: arr, complete: false };
2373
+ const next = this.peek();
2374
+ if (next === ",") {
2375
+ this.advance();
2376
+ continue;
2377
+ }
2378
+ if (next === "]") {
2379
+ this.advance();
2380
+ return { value: arr, complete: true };
2381
+ }
2382
+ throw new SyntaxError(`Expected ',' or ']' at position ${this.pos}, got '${next}'`);
2383
+ }
2384
+ return { value: arr, complete: false };
2385
+ }
2386
+ parseString() {
2387
+ this.advance();
2388
+ let result = "";
2389
+ while (!this.eof()) {
2390
+ const c = this.advance();
2391
+ if (c === '"') return { value: result, complete: true };
2392
+ if (c === "\\") {
2393
+ if (this.eof()) {
2394
+ return { value: result, complete: false };
2395
+ }
2396
+ const esc = this.advance();
2397
+ switch (esc) {
2398
+ case '"':
2399
+ result += '"';
2400
+ break;
2401
+ case "\\":
2402
+ result += "\\";
2403
+ break;
2404
+ case "/":
2405
+ result += "/";
2406
+ break;
2407
+ case "b":
2408
+ result += "\b";
2409
+ break;
2410
+ case "f":
2411
+ result += "\f";
2412
+ break;
2413
+ case "n":
2414
+ result += "\n";
2415
+ break;
2416
+ case "r":
2417
+ result += "\r";
2418
+ break;
2419
+ case "t":
2420
+ result += " ";
2421
+ break;
2422
+ case "u": {
2423
+ if (this.pos + 4 > this.src.length) {
2424
+ return { value: result, complete: false };
2425
+ }
2426
+ const hex = this.src.slice(this.pos, this.pos + 4);
2427
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
2428
+ throw new SyntaxError(`Invalid \\u escape '${hex}' at position ${this.pos}`);
2429
+ }
2430
+ this.pos += 4;
2431
+ result += String.fromCharCode(parseInt(hex, 16));
2432
+ break;
2433
+ }
2434
+ default:
2435
+ throw new SyntaxError(`Invalid escape '\\${esc}' at position ${this.pos - 1}`);
2436
+ }
2437
+ } else {
2438
+ result += c;
2439
+ }
2440
+ }
2441
+ return { value: result, complete: false };
2442
+ }
2443
+ parseNumber() {
2444
+ const start = this.pos;
2445
+ if (this.peek() === "-") this.advance();
2446
+ while (!this.eof() && this.peek() >= "0" && this.peek() <= "9") this.advance();
2447
+ if (!this.eof() && this.peek() === ".") {
2448
+ this.advance();
2449
+ while (!this.eof() && this.peek() >= "0" && this.peek() <= "9") this.advance();
2450
+ }
2451
+ if (!this.eof() && (this.peek() === "e" || this.peek() === "E")) {
2452
+ this.advance();
2453
+ if (!this.eof() && (this.peek() === "+" || this.peek() === "-")) this.advance();
2454
+ while (!this.eof() && this.peek() >= "0" && this.peek() <= "9") this.advance();
2455
+ }
2456
+ const text = this.src.slice(start, this.pos);
2457
+ if (text === "" || text === "-" || /\.$/.test(text) || /[eE][+-]?$/.test(text)) {
2458
+ return { value: void 0, complete: false };
2459
+ }
2460
+ const n = Number(text);
2461
+ if (Number.isNaN(n)) {
2462
+ return { value: void 0, complete: false };
2463
+ }
2464
+ return { value: n, complete: true };
2465
+ }
2466
+ parseBool() {
2467
+ const remaining = this.src.slice(this.pos);
2468
+ if (remaining.startsWith("true")) {
2469
+ this.pos += 4;
2470
+ return { value: true, complete: true };
2471
+ }
2472
+ if (remaining.startsWith("false")) {
2473
+ this.pos += 5;
2474
+ return { value: false, complete: true };
2475
+ }
2476
+ if ("true".startsWith(remaining) || "false".startsWith(remaining)) {
2477
+ this.pos = this.src.length;
2478
+ return { value: void 0, complete: false };
2479
+ }
2480
+ throw new SyntaxError(`Expected 'true' or 'false' at position ${this.pos}`);
2481
+ }
2482
+ parseNull() {
2483
+ const remaining = this.src.slice(this.pos);
2484
+ if (remaining.startsWith("null")) {
2485
+ this.pos += 4;
2486
+ return { value: null, complete: true };
2487
+ }
2488
+ if ("null".startsWith(remaining)) {
2489
+ this.pos = this.src.length;
2490
+ return { value: void 0, complete: false };
2491
+ }
2492
+ throw new SyntaxError(`Expected 'null' at position ${this.pos}`);
2493
+ }
2494
+ };
2495
+
2496
+ // src/event-utils.ts
2497
+ var COST_BEARING_LEAF_TYPES = [
2498
+ "agent_call_end",
2499
+ "tool_call_end",
2500
+ "memory_remember",
2501
+ "memory_recall"
2502
+ ];
2503
+ var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
2504
+ function eventCostContribution(event) {
2505
+ if (event.type === "ask_end") return 0;
2506
+ const c = event.cost;
2507
+ return typeof c === "number" && Number.isFinite(c) && c >= 0 ? c : 0;
2508
+ }
2509
+ function isCostBearingLeaf(event) {
2510
+ return COST_LEAF_SET.has(event.type);
2511
+ }
2512
+ function isRootLevel(event) {
2513
+ const d = event.depth;
2514
+ return (d ?? 0) === 0;
2515
+ }
2516
+
2517
+ // src/redaction.ts
2518
+ var REDACTED = "[redacted]";
2519
+ function walkObjectOneLevel(obj, preserveKeys) {
2520
+ const out = {};
2521
+ for (const [k, v] of Object.entries(obj)) {
2522
+ if (preserveKeys.has(k) || typeof v === "number" || typeof v === "boolean") {
2523
+ out[k] = v;
2524
+ } else if (typeof v === "string") {
2525
+ out[k] = REDACTED;
2526
+ } else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
2527
+ const inner = v;
2528
+ const innerOut = {};
2529
+ for (const [ik, iv] of Object.entries(inner)) {
2530
+ if (typeof iv === "number" || typeof iv === "boolean") {
2531
+ innerOut[ik] = iv;
2532
+ } else {
2533
+ innerOut[ik] = REDACTED;
2534
+ }
2535
+ }
2536
+ out[k] = innerOut;
2537
+ } else {
2538
+ out[k] = REDACTED;
2539
+ }
2540
+ }
2541
+ return out;
2542
+ }
2543
+ var LOG_PRESERVE_KEYS = /* @__PURE__ */ new Set(["event"]);
2544
+ var MEMORY_PRESERVE_KEYS = /* @__PURE__ */ new Set(["scope"]);
2545
+ function redactGate(event) {
2546
+ const d = event.data;
2547
+ if (!d) return event;
2548
+ if (d.reason === void 0 && d.feedbackMessage === void 0) return event;
2549
+ return {
2550
+ ...event,
2551
+ data: {
2552
+ ...d,
2553
+ ...d.reason !== void 0 ? { reason: REDACTED } : {},
2554
+ ...d.feedbackMessage !== void 0 ? { feedbackMessage: REDACTED } : {}
2555
+ }
2556
+ };
2557
+ }
2558
+ function redactMemory(event) {
2559
+ const d = event.data;
2560
+ if (!d || typeof d !== "object") return event;
2561
+ return {
2562
+ ...event,
2563
+ data: walkObjectOneLevel(d, MEMORY_PRESERVE_KEYS)
2564
+ };
2565
+ }
2566
+ var passthrough = (e) => e;
2567
+ var REDACTION_RULES = {
2568
+ workflow_start: (e) => e.data.input !== void 0 ? { ...e, data: { ...e.data, input: REDACTED } } : e,
2569
+ workflow_end: (e) => {
2570
+ const d = e.data;
2571
+ if (d.result === void 0 && d.error === void 0) return e;
2572
+ return {
2573
+ ...e,
2574
+ data: {
2575
+ ...d,
2576
+ ...d.result !== void 0 ? { result: REDACTED } : {},
2577
+ ...d.error !== void 0 ? { error: REDACTED } : {}
2578
+ }
2579
+ };
2580
+ },
2581
+ ask_start: (e) => ({ ...e, prompt: REDACTED }),
2582
+ ask_end: (e) => ({
2583
+ ...e,
2584
+ outcome: e.outcome.ok ? { ok: true, result: REDACTED } : { ok: false, error: REDACTED }
2585
+ }),
2586
+ agent_call_start: (e) => {
2587
+ const d = e.data;
2588
+ const out = { ...d, prompt: REDACTED };
2589
+ if (d.system !== void 0) out.system = REDACTED;
2590
+ if (Array.isArray(d.messages)) {
2591
+ out.messages = [{ role: "system", content: `[${d.messages.length} messages redacted]` }];
2592
+ }
2593
+ return { ...e, data: out };
2594
+ },
2595
+ agent_call_end: (e) => {
2596
+ const d = e.data;
2597
+ const out = { ...d, response: REDACTED };
2598
+ if (d.thinking !== void 0) out.thinking = REDACTED;
2599
+ if (d.error !== void 0) out.error = REDACTED;
2600
+ return { ...e, data: out };
2601
+ },
2602
+ token: (e) => ({ ...e, data: REDACTED }),
2603
+ tool_call_start: (e) => ({ ...e, data: { ...e.data, args: REDACTED } }),
2604
+ tool_call_end: (e) => ({ ...e, data: { ...e.data, args: REDACTED, result: REDACTED } }),
2605
+ tool_approval: (e) => ({
2606
+ ...e,
2607
+ data: {
2608
+ ...e.data,
2609
+ args: REDACTED,
2610
+ ...e.data.reason !== void 0 ? { reason: REDACTED } : {}
2611
+ }
2612
+ }),
2613
+ tool_denied: (e) => {
2614
+ if (!e.data) return e;
2615
+ const d = e.data;
2616
+ if (d.args === void 0 && d.reason === void 0) return e;
2617
+ return {
2618
+ ...e,
2619
+ data: {
2620
+ ...d,
2621
+ ...d.args !== void 0 ? { args: REDACTED } : {},
2622
+ ...d.reason !== void 0 ? { reason: REDACTED } : {}
2623
+ }
2624
+ };
2625
+ },
2626
+ delegate: passthrough,
2627
+ handoff_start: (e) => e.data.message !== void 0 ? { ...e, data: { ...e.data, message: REDACTED } } : e,
2628
+ handoff_return: passthrough,
2629
+ pipeline: (e) => e.status === "failed" ? { ...e, reason: REDACTED } : e,
2630
+ partial_object: (e) => ({ ...e, data: { ...e.data, object: REDACTED } }),
2631
+ verify: (e) => e.data.lastError !== void 0 ? { ...e, data: { ...e.data, lastError: REDACTED } } : e,
2632
+ log: (e) => {
2633
+ if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
2634
+ return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
2635
+ },
2636
+ memory_remember: redactMemory,
2637
+ memory_recall: redactMemory,
2638
+ memory_forget: redactMemory,
2639
+ checkpoint_save: passthrough,
2640
+ checkpoint_replay: passthrough,
2641
+ await_human: (e) => e.data.prompt !== void 0 ? { ...e, data: { ...e.data, prompt: REDACTED } } : e,
2642
+ await_human_resolved: (e) => {
2643
+ const dec = e.data.decision;
2644
+ if (!dec) return e;
2645
+ const hasData = "data" in dec && dec.data !== void 0;
2646
+ const hasReason = "reason" in dec && dec.reason !== void 0;
2647
+ if (!hasData && !hasReason) return e;
2648
+ const scrubbed = { ...dec };
2649
+ if (hasData) scrubbed.data = REDACTED;
2650
+ if (hasReason) scrubbed.reason = REDACTED;
2651
+ return { ...e, data: { ...e.data, decision: scrubbed } };
2652
+ },
2653
+ guardrail: redactGate,
2654
+ schema_check: redactGate,
2655
+ validate: redactGate,
2656
+ done: (e) => ({ ...e, data: { result: REDACTED } }),
2657
+ error: (e) => ({ ...e, data: { ...e.data, message: REDACTED } })
2658
+ };
2659
+ function redactEvent(event) {
2660
+ const rule = REDACTION_RULES[event.type];
2661
+ return rule(event);
2662
+ }
2663
+
2174
2664
  // src/config.ts
2175
2665
  function defineConfig(config) {
2176
2666
  return config;
@@ -2248,6 +2738,7 @@ function resolveConfig(config) {
2248
2738
 
2249
2739
  // src/context.ts
2250
2740
  var signalStorage = new import_node_async_hooks.AsyncLocalStorage();
2741
+ var askStorage = new import_node_async_hooks.AsyncLocalStorage();
2251
2742
  function zodToJsonSchema(schema) {
2252
2743
  const result = import_zod.z.toJSONSchema(schema, { unrepresentable: "any" });
2253
2744
  delete result.$schema;
@@ -2307,6 +2798,14 @@ function extractBalanced(str, start, openChar, closeChar) {
2307
2798
  }
2308
2799
  return null;
2309
2800
  }
2801
+ function appendRetryMessages(messages, content, feedbackMessage, providerMetadata) {
2802
+ messages.push({
2803
+ role: "assistant",
2804
+ content,
2805
+ ...providerMetadata ? { providerMetadata } : {}
2806
+ });
2807
+ messages.push({ role: "system", content: feedbackMessage });
2808
+ }
2310
2809
  function estimateMessagesTokens(messages) {
2311
2810
  let total = 0;
2312
2811
  for (const msg of messages) {
@@ -2341,8 +2840,34 @@ var WorkflowContext = class _WorkflowContext {
2341
2840
  pendingDecisions;
2342
2841
  budgetContext;
2343
2842
  stateStore;
2344
- stepCounter = 0;
2345
- checkpointCounter = 0;
2843
+ /** Root step counter for this execution. Inherited by every ctx.ask()
2844
+ * frame so all events from this WorkflowContext (root + nested) share
2845
+ * a single monotonic counter, even when concurrent branch primitives
2846
+ * fire asks before any single parent ask exists. Per-instance so
2847
+ * separate executions don't cross-talk. */
2848
+ stepRefRoot = { value: 0 };
2849
+ /**
2850
+ * Auto-checkpoint counters for internal `_checkpoint` callers. Held by
2851
+ * reference so child contexts created via `createChildContext` share
2852
+ * the same number space — otherwise a tool handler's nested `ctx.ask()`
2853
+ * (which auto-checkpoints) would collide with the parent's
2854
+ * auto-checkpoints in the state store. See `WorkflowContextInit
2855
+ * .autoCheckpointCounters` for the per-agent vs root split.
2856
+ * User-facing `ctx.checkpoint(name, fn)` requires an explicit name and
2857
+ * does NOT touch these counters — it goes straight to the store under
2858
+ * the caller-supplied key.
2859
+ */
2860
+ autoCheckpointCounters;
2861
+ /** Idempotency guards for `workflow_start` / `workflow_end` emission.
2862
+ * Both `runtime.execute()` and `runtime.stream()` have paths where a
2863
+ * post-emit side-effect (checkpoint deletion, state-store persistence)
2864
+ * can throw AFTER `_emitWorkflowEnd({status: 'completed'})` has already
2865
+ * fired — the outer catch would then fire `_emitWorkflowEnd({status:
2866
+ * 'failed'})` for a second time. These flags make both emitters
2867
+ * single-fire so consumers never see paired-then-conflicting terminal
2868
+ * events for one execution. */
2869
+ _workflowStartEmitted = false;
2870
+ _workflowEndEmitted = false;
2346
2871
  signal;
2347
2872
  summaryCache;
2348
2873
  workflowName;
@@ -2377,6 +2902,10 @@ var WorkflowContext = class _WorkflowContext {
2377
2902
  this.awaitHumanHandler = init.awaitHumanHandler;
2378
2903
  this.onAgentStart = init.onAgentStart;
2379
2904
  this.onAgentCallComplete = init.onAgentCallComplete;
2905
+ this.autoCheckpointCounters = init.autoCheckpointCounters ?? {
2906
+ byAgent: /* @__PURE__ */ new Map(),
2907
+ root: 0
2908
+ };
2380
2909
  if (init.metadata?.summaryCache) {
2381
2910
  this.summaryCache = init.metadata.summaryCache;
2382
2911
  }
@@ -2385,8 +2914,21 @@ var WorkflowContext = class _WorkflowContext {
2385
2914
  * Create a child context for nested agent invocations (e.g., agent-as-tool).
2386
2915
  * Shares: budget tracking, abort signals, trace emission, provider registry,
2387
2916
  * state store, span manager, memory manager, MCP manager, config,
2388
- * awaitHuman handler, pending decisions, tool overrides.
2389
- * Isolates: session history, step counter, streaming callbacks (onToken, onAgentStart, onToolCall).
2917
+ * awaitHuman handler, pending decisions, tool overrides, AND the
2918
+ * streaming callbacks (onToken / onAgentStart / onToolCall).
2919
+ * Isolates: session history.
2920
+ *
2921
+ * Streaming callbacks now propagate into nested asks because every callback
2922
+ * invocation carries `meta.askId`/`meta.parentAskId`/`meta.depth` so
2923
+ * consumers that want root-only behavior can filter on `meta.depth === 0`
2924
+ * instead of relying on the runtime to drop nested events. This is the
2925
+ * "nested ask visibility" fix from spec §3.2.
2926
+ *
2927
+ * The shared step counter lives in `askStorage` (ALS), so there is no
2928
+ * per-context counter to pass through anymore. Nested events are
2929
+ * correlated to their parent via `parentAskId` (set on every nested ask
2930
+ * by ALS frame allocation) — the deprecated `parentToolCallId` field
2931
+ * was removed in 0.16.0.
2390
2932
  */
2391
2933
  createChildContext() {
2392
2934
  return new _WorkflowContext({
@@ -2402,14 +2944,20 @@ var WorkflowContext = class _WorkflowContext {
2402
2944
  spanManager: this.spanManager,
2403
2945
  memoryManager: this.memoryManager,
2404
2946
  onTrace: this.onTrace,
2947
+ onToken: this.onToken,
2948
+ onToolCall: this.onToolCall,
2949
+ onAgentStart: this.onAgentStart,
2405
2950
  onAgentCallComplete: this.onAgentCallComplete,
2406
2951
  awaitHumanHandler: this.awaitHumanHandler,
2407
2952
  pendingDecisions: this.pendingDecisions,
2408
2953
  toolOverrides: this.toolOverrides,
2409
2954
  signal: this.signal,
2410
- workflowName: this.workflowName
2411
- // Isolated: sessionHistory (empty), stepCounter (0),
2412
- // onToken (null), onAgentStart (null), onToolCall (null)
2955
+ workflowName: this.workflowName,
2956
+ // Share the parent's auto-checkpoint counters so internal checkpoints
2957
+ // emitted from `ctx.ask` / spawn / race / parallel / map in this
2958
+ // child context don't collide with the parent's in the state store.
2959
+ autoCheckpointCounters: this.autoCheckpointCounters
2960
+ // Isolated: sessionHistory (empty)
2413
2961
  });
2414
2962
  }
2415
2963
  /**
@@ -2420,72 +2968,150 @@ var WorkflowContext = class _WorkflowContext {
2420
2968
  get currentSignal() {
2421
2969
  return signalStorage.getStore() ?? this.signal;
2422
2970
  }
2971
+ /**
2972
+ * Build a `CallbackMeta` for the current ask frame. Used at every
2973
+ * `onToken`/`onToolCall`/`onAgentStart` call site so consumers can
2974
+ * route or filter by ask correlation (`meta.depth === 0` for root
2975
+ * chat UIs, etc.).
2976
+ *
2977
+ * If the call is somehow outside an ask frame (e.g., a programmatic
2978
+ * `tool.run()` test harness), falls back to a synthetic meta keyed off
2979
+ * `executionId` so the type contract holds — consumers see `depth: 0`
2980
+ * and can ignore those events.
2981
+ */
2982
+ currentCallbackMeta(agentName) {
2983
+ const frame = askStorage.getStore();
2984
+ if (frame) {
2985
+ return {
2986
+ askId: frame.askId,
2987
+ ...frame.parentAskId ? { parentAskId: frame.parentAskId } : {},
2988
+ depth: frame.depth,
2989
+ agent: agentName
2990
+ };
2991
+ }
2992
+ return { askId: this.executionId, depth: 0, agent: agentName };
2993
+ }
2423
2994
  // ── ctx.ask() ─────────────────────────────────────────────────────────
2424
2995
  async ask(agent2, prompt, options) {
2425
- return this._checkpoint(async () => {
2426
- const costBefore = this.budgetContext?.totalCost ?? 0;
2427
- const startTime = Date.now();
2428
- const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
2429
- const usageCapture = {};
2430
- const doCall = async () => {
2431
- const result2 = await this.executeAgentCall(
2432
- agent2,
2433
- prompt,
2434
- options,
2435
- void 0,
2436
- usageCapture
2437
- );
2438
- return result2;
2439
- };
2440
- const result = this.spanManager ? await this.spanManager.withSpanAsync(
2441
- "axl.agent.ask",
2442
- {
2443
- "axl.agent.name": agent2._name,
2444
- "axl.agent.model": agent2.resolveModel(resolveCtx)
2445
- },
2446
- async (span) => {
2447
- const r = await doCall();
2448
- const costAfter2 = this.budgetContext?.totalCost ?? 0;
2449
- span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
2450
- span.setAttribute("axl.agent.duration", Date.now() - startTime);
2451
- if (usageCapture.value) {
2452
- span.setAttribute("axl.agent.prompt_tokens", usageCapture.value.prompt_tokens);
2453
- span.setAttribute(
2454
- "axl.agent.completion_tokens",
2455
- usageCapture.value.completion_tokens
2996
+ const agentName = agent2._name;
2997
+ return this._checkpoint(
2998
+ this._autoCheckpointName("ask", agentName),
2999
+ async () => {
3000
+ const parentFrame = askStorage.getStore();
3001
+ const askId = (0, import_node_crypto.randomUUID)();
3002
+ const depth = (parentFrame?.depth ?? -1) + 1;
3003
+ const stepRef = parentFrame?.stepRef ?? this.stepRefRoot;
3004
+ const frame = {
3005
+ askId,
3006
+ parentAskId: parentFrame?.askId,
3007
+ depth,
3008
+ agent: agent2._name,
3009
+ stepRef,
3010
+ askCost: { value: 0 }
3011
+ };
3012
+ return askStorage.run(frame, async () => {
3013
+ const askStart = Date.now();
3014
+ this.emitEvent({ type: "ask_start", prompt });
3015
+ const costBefore = this.budgetContext?.totalCost ?? 0;
3016
+ const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
3017
+ const usageCapture = {};
3018
+ const doCall = async () => {
3019
+ const result = await this.executeAgentCall(
3020
+ agent2,
3021
+ prompt,
3022
+ options,
3023
+ void 0,
3024
+ usageCapture
2456
3025
  );
2457
- if (usageCapture.value.cached_tokens)
2458
- span.setAttribute("axl.agent.cached_tokens", usageCapture.value.cached_tokens);
3026
+ return result;
3027
+ };
3028
+ let outcome;
3029
+ try {
3030
+ const result = this.spanManager ? await this.spanManager.withSpanAsync(
3031
+ "axl.agent.ask",
3032
+ {
3033
+ "axl.agent.name": agent2._name,
3034
+ "axl.agent.model": agent2.resolveModel(resolveCtx)
3035
+ },
3036
+ async (span) => {
3037
+ const r = await doCall();
3038
+ const costAfter2 = this.budgetContext?.totalCost ?? 0;
3039
+ span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
3040
+ span.setAttribute("axl.agent.duration", Date.now() - askStart);
3041
+ if (usageCapture.value) {
3042
+ span.setAttribute(
3043
+ "axl.agent.prompt_tokens",
3044
+ usageCapture.value.prompt_tokens
3045
+ );
3046
+ span.setAttribute(
3047
+ "axl.agent.completion_tokens",
3048
+ usageCapture.value.completion_tokens
3049
+ );
3050
+ if (usageCapture.value.cached_tokens)
3051
+ span.setAttribute(
3052
+ "axl.agent.cached_tokens",
3053
+ usageCapture.value.cached_tokens
3054
+ );
3055
+ }
3056
+ return r;
3057
+ }
3058
+ ) : await doCall();
3059
+ outcome = { ok: true, result };
3060
+ const costAfter = this.budgetContext?.totalCost ?? 0;
3061
+ if (this.onAgentCallComplete) {
3062
+ try {
3063
+ this.onAgentCallComplete({
3064
+ agent: agent2._name,
3065
+ prompt,
3066
+ response: typeof result === "string" ? result : JSON.stringify(result),
3067
+ model: agent2.resolveModel(resolveCtx),
3068
+ cost: costAfter - costBefore,
3069
+ duration: Date.now() - askStart,
3070
+ promptVersion: agent2._config.version,
3071
+ temperature: options?.temperature ?? agent2._config.temperature,
3072
+ maxTokens: options?.maxTokens ?? agent2._config.maxTokens ?? 4096,
3073
+ effort: options?.effort ?? agent2._config.effort,
3074
+ thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
3075
+ includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
3076
+ toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
3077
+ stop: options?.stop ?? agent2._config.stop,
3078
+ providerOptions: options?.providerOptions ?? agent2._config.providerOptions
3079
+ });
3080
+ } catch (hookErr) {
3081
+ console.error(
3082
+ "[axl] onAgentCallComplete hook threw; ask outcome unchanged:",
3083
+ hookErr instanceof Error ? hookErr.message : String(hookErr)
3084
+ );
3085
+ }
3086
+ }
3087
+ return result;
3088
+ } catch (err) {
3089
+ outcome = {
3090
+ ok: false,
3091
+ error: err instanceof Error ? err.message : String(err)
3092
+ };
3093
+ throw err;
3094
+ } finally {
3095
+ this.emitEvent({
3096
+ type: "ask_end",
3097
+ outcome: outcome ?? {
3098
+ ok: false,
3099
+ error: "ask_end emitted without outcome \u2014 internal bug"
3100
+ },
3101
+ cost: frame.askCost.value,
3102
+ duration: Date.now() - askStart
3103
+ });
2459
3104
  }
2460
- return r;
2461
- }
2462
- ) : await doCall();
2463
- const costAfter = this.budgetContext?.totalCost ?? 0;
2464
- this.onAgentCallComplete?.({
2465
- agent: agent2._name,
2466
- prompt,
2467
- response: typeof result === "string" ? result : JSON.stringify(result),
2468
- model: agent2.resolveModel(resolveCtx),
2469
- cost: costAfter - costBefore,
2470
- duration: Date.now() - startTime,
2471
- promptVersion: agent2._config.version,
2472
- temperature: options?.temperature ?? agent2._config.temperature,
2473
- maxTokens: options?.maxTokens ?? agent2._config.maxTokens ?? 4096,
2474
- effort: options?.effort ?? agent2._config.effort,
2475
- thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
2476
- includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
2477
- toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
2478
- stop: options?.stop ?? agent2._config.stop,
2479
- providerOptions: options?.providerOptions ?? agent2._config.providerOptions
2480
- });
2481
- return result;
2482
- });
3105
+ });
3106
+ },
3107
+ { agent: agentName }
3108
+ );
2483
3109
  }
2484
3110
  async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
2485
3111
  if (this.budgetContext?.exceeded) {
2486
3112
  const { limit, totalCost: spent, policy } = this.budgetContext;
2487
3113
  if (policy === "warn") {
2488
- this.emitTrace({
3114
+ this.emitEvent({
2489
3115
  type: "log",
2490
3116
  data: { warning: "Budget exceeded", limit, spent, policy }
2491
3117
  });
@@ -2574,14 +3200,25 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2574
3200
  const guardrails = agent2._config.guardrails;
2575
3201
  if (guardrails?.input) {
2576
3202
  const inputResult = await guardrails.input(prompt, { metadata: this.metadata });
2577
- this.emitTrace({
3203
+ this.emitEvent({
2578
3204
  type: "guardrail",
2579
3205
  agent: agent2._name,
2580
- data: { guardrailType: "input", blocked: inputResult.block, reason: inputResult.reason }
3206
+ data: {
3207
+ guardrailType: "input",
3208
+ blocked: inputResult.block,
3209
+ ...inputResult.reason ? { reason: inputResult.reason } : {},
3210
+ // Input guardrails can't retry (prompt is user-supplied), so attempt
3211
+ // and maxAttempts are always 1 — emit them for shape consistency with
3212
+ // output guardrails so consumers don't need two narrowers.
3213
+ attempt: 1,
3214
+ maxAttempts: 1
3215
+ }
2581
3216
  });
2582
3217
  this.spanManager?.addEventToActiveSpan("axl.guardrail.check", {
2583
3218
  "axl.guardrail.type": "input",
2584
3219
  "axl.guardrail.blocked": inputResult.block,
3220
+ "axl.guardrail.attempt": 1,
3221
+ "axl.guardrail.maxAttempts": 1,
2585
3222
  ...inputResult.reason ? { "axl.guardrail.reason": inputResult.reason } : {}
2586
3223
  });
2587
3224
  if (inputResult.block) {
@@ -2597,22 +3234,51 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2597
3234
  const maxTurns = agent2._config.maxTurns ?? 25;
2598
3235
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
2599
3236
  const startTime = Date.now();
2600
- if (this.onToken && options?.validate) {
2601
- throw new AxlError(
2602
- "INVALID_CONFIG",
2603
- "Cannot use validate with streaming. Validate requires schema (JSON output) which does not benefit from token streaming. Use a non-streaming call instead."
2604
- );
2605
- }
2606
3237
  const currentMessages = [...messages];
2607
3238
  let turns = 0;
2608
3239
  let guardrailOutputRetries = 0;
2609
3240
  let schemaRetries = 0;
2610
3241
  let validateRetries = 0;
3242
+ const maxGuardrailRetries = guardrails?.maxRetries ?? 2;
3243
+ let pendingRetryReason;
3244
+ const verboseTrace = this.config.trace?.level === "full";
3245
+ let lastStartStage = "initial";
3246
+ let lastStartAttempt = 1;
3247
+ let lastStartMaxAttempts = 1;
2611
3248
  while (turns < maxTurns) {
2612
3249
  if (Date.now() - startTime > timeoutMs) {
2613
3250
  throw new TimeoutError("ctx.ask()", timeoutMs);
2614
3251
  }
2615
3252
  turns++;
3253
+ const isFirstTurn = turns === 1;
3254
+ const isRetryTurn = pendingRetryReason !== void 0;
3255
+ if (isFirstTurn || isRetryTurn) {
3256
+ const stage = pendingRetryReason ?? "initial";
3257
+ let pipelineAttempt = 1;
3258
+ let pipelineMaxAttempts = 1;
3259
+ if (stage === "guardrail") {
3260
+ pipelineAttempt = guardrailOutputRetries + 1;
3261
+ pipelineMaxAttempts = maxGuardrailRetries + 1;
3262
+ } else if (stage === "schema") {
3263
+ pipelineAttempt = schemaRetries + 1;
3264
+ pipelineMaxAttempts = (options?.retries ?? 3) + 1;
3265
+ } else if (stage === "validate") {
3266
+ pipelineAttempt = validateRetries + 1;
3267
+ pipelineMaxAttempts = (options?.validateRetries ?? 2) + 1;
3268
+ }
3269
+ lastStartStage = stage;
3270
+ lastStartAttempt = pipelineAttempt;
3271
+ lastStartMaxAttempts = pipelineMaxAttempts;
3272
+ this.emitEvent({
3273
+ type: "pipeline",
3274
+ agent: agent2._name,
3275
+ status: "start",
3276
+ stage,
3277
+ attempt: pipelineAttempt,
3278
+ maxAttempts: pipelineMaxAttempts
3279
+ });
3280
+ }
3281
+ const turnStart = Date.now();
2616
3282
  const chatOptions = {
2617
3283
  model,
2618
3284
  temperature: options?.temperature ?? agent2._config.temperature,
@@ -2629,82 +3295,172 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2629
3295
  if (options?.schema && toolDefs.length === 0) {
2630
3296
  chatOptions.responseFormat = { type: "json_object" };
2631
3297
  }
2632
- this.onAgentStart?.({ agent: agent2._name, model: modelUri });
3298
+ const callbackMeta = this.currentCallbackMeta(agent2._name);
3299
+ const callParams = {
3300
+ ...chatOptions.temperature !== void 0 ? { temperature: chatOptions.temperature } : {},
3301
+ maxTokens: chatOptions.maxTokens,
3302
+ ...chatOptions.effort !== void 0 ? { effort: chatOptions.effort } : {},
3303
+ ...chatOptions.thinkingBudget !== void 0 ? { thinkingBudget: chatOptions.thinkingBudget } : {},
3304
+ ...chatOptions.includeThoughts !== void 0 ? { includeThoughts: chatOptions.includeThoughts } : {},
3305
+ ...chatOptions.toolChoice !== void 0 ? { toolChoice: chatOptions.toolChoice } : {},
3306
+ ...chatOptions.stop !== void 0 ? { stop: chatOptions.stop } : {}
3307
+ };
3308
+ let messagesSnapshot;
3309
+ if (verboseTrace) {
3310
+ try {
3311
+ messagesSnapshot = structuredClone(currentMessages);
3312
+ } catch (err) {
3313
+ console.warn(
3314
+ "[axl] verbose trace messages snapshot failed to clone; emitting shallow copy:",
3315
+ err instanceof Error ? err.message : String(err)
3316
+ );
3317
+ messagesSnapshot = [...currentMessages];
3318
+ }
3319
+ }
3320
+ const retryReason = pendingRetryReason;
3321
+ pendingRetryReason = void 0;
3322
+ this.emitEvent({
3323
+ type: "agent_call_start",
3324
+ agent: agent2._name,
3325
+ model: modelUri,
3326
+ turn: turns,
3327
+ data: {
3328
+ prompt,
3329
+ ...systemPrompt ? { system: systemPrompt } : {},
3330
+ params: callParams,
3331
+ turn: turns,
3332
+ ...retryReason ? { retryReason } : {},
3333
+ ...toolDefs.length > 0 ? { toolNames: toolDefs.map((t) => t.function.name) } : {},
3334
+ ...messagesSnapshot ? { messages: messagesSnapshot } : {}
3335
+ }
3336
+ });
3337
+ this.onAgentStart?.({ agent: agent2._name, model: modelUri }, callbackMeta);
2633
3338
  let response;
2634
- if (this.onToken) {
2635
- let content2 = "";
2636
- const toolCalls = [];
2637
- const toolCallBuffers = /* @__PURE__ */ new Map();
2638
- let streamProviderMetadata;
2639
- let thinkingContent = "";
2640
- for await (const chunk of provider.stream(currentMessages, chatOptions)) {
2641
- if (chunk.type === "text_delta") {
2642
- content2 += chunk.content;
2643
- this.onToken(chunk.content);
2644
- } else if (chunk.type === "thinking_delta") {
2645
- thinkingContent += chunk.content;
2646
- } else if (chunk.type === "tool_call_delta") {
2647
- let buffer = toolCallBuffers.get(chunk.id);
2648
- if (!buffer) {
2649
- buffer = { id: chunk.id, name: "", arguments: "" };
2650
- toolCallBuffers.set(chunk.id, buffer);
2651
- }
2652
- if (chunk.name) buffer.name = chunk.name;
2653
- if (chunk.arguments) buffer.arguments += chunk.arguments;
2654
- } else if (chunk.type === "done") {
2655
- streamProviderMetadata = chunk.providerMetadata;
2656
- if (chunk.usage) {
2657
- response = {
2658
- content: content2,
2659
- tool_calls: void 0,
2660
- usage: chunk.usage,
2661
- cost: chunk.cost
2662
- };
3339
+ try {
3340
+ if (this.onToken) {
3341
+ let content2 = "";
3342
+ const toolCalls = [];
3343
+ const toolCallBuffers = /* @__PURE__ */ new Map();
3344
+ let streamProviderMetadata;
3345
+ let thinkingContent = "";
3346
+ const partialObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof import_zod.z.ZodObject;
3347
+ const currentAttempt = schemaRetries + 1;
3348
+ let inString = false;
3349
+ let escaped = false;
3350
+ let boundaryPending = false;
3351
+ for await (const chunk of provider.stream(currentMessages, chatOptions)) {
3352
+ if (chunk.type === "text_delta") {
3353
+ content2 += chunk.content;
3354
+ this.emitEvent({ type: "token", data: chunk.content });
3355
+ this.onToken(chunk.content, callbackMeta);
3356
+ if (partialObjectEnabled) {
3357
+ for (const ch of chunk.content) {
3358
+ if (escaped) {
3359
+ escaped = false;
3360
+ continue;
3361
+ }
3362
+ if (ch === "\\") {
3363
+ escaped = true;
3364
+ continue;
3365
+ }
3366
+ if (ch === '"') {
3367
+ inString = !inString;
3368
+ continue;
3369
+ }
3370
+ if (!inString && (ch === "," || ch === "}" || ch === "]")) {
3371
+ boundaryPending = true;
3372
+ }
3373
+ }
3374
+ if (boundaryPending) {
3375
+ boundaryPending = false;
3376
+ let parsed;
3377
+ try {
3378
+ parsed = parsePartialJson(extractJson(content2));
3379
+ } catch {
3380
+ parsed = void 0;
3381
+ }
3382
+ if (parsed !== void 0) {
3383
+ this.emitEvent({
3384
+ type: "partial_object",
3385
+ agent: agent2._name,
3386
+ attempt: currentAttempt,
3387
+ data: { object: parsed }
3388
+ });
3389
+ }
3390
+ }
3391
+ }
3392
+ } else if (chunk.type === "thinking_delta") {
3393
+ thinkingContent += chunk.content;
3394
+ } else if (chunk.type === "tool_call_delta") {
3395
+ let buffer = toolCallBuffers.get(chunk.id);
3396
+ if (!buffer) {
3397
+ buffer = { id: chunk.id, name: "", arguments: "" };
3398
+ toolCallBuffers.set(chunk.id, buffer);
3399
+ }
3400
+ if (chunk.name) buffer.name = chunk.name;
3401
+ if (chunk.arguments) buffer.arguments += chunk.arguments;
3402
+ } else if (chunk.type === "done") {
3403
+ streamProviderMetadata = chunk.providerMetadata;
3404
+ if (chunk.usage) {
3405
+ response = {
3406
+ content: content2,
3407
+ tool_calls: void 0,
3408
+ usage: chunk.usage,
3409
+ cost: chunk.cost
3410
+ };
3411
+ }
2663
3412
  }
2664
3413
  }
3414
+ for (const buffer of toolCallBuffers.values()) {
3415
+ toolCalls.push({
3416
+ id: buffer.id,
3417
+ type: "function",
3418
+ function: {
3419
+ name: buffer.name,
3420
+ arguments: buffer.arguments
3421
+ }
3422
+ });
3423
+ }
3424
+ response ??= {
3425
+ content: content2,
3426
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
3427
+ };
3428
+ if (toolCalls.length > 0) {
3429
+ response.tool_calls = toolCalls;
3430
+ }
3431
+ if (streamProviderMetadata) {
3432
+ response.providerMetadata = streamProviderMetadata;
3433
+ }
3434
+ if (thinkingContent) {
3435
+ response.thinking_content = thinkingContent;
3436
+ }
3437
+ } else {
3438
+ response = await provider.chat(currentMessages, chatOptions);
2665
3439
  }
2666
- for (const buffer of toolCallBuffers.values()) {
2667
- toolCalls.push({
2668
- id: buffer.id,
2669
- type: "function",
2670
- function: {
2671
- name: buffer.name,
2672
- arguments: buffer.arguments
2673
- }
2674
- });
2675
- }
2676
- response ??= {
2677
- content: content2,
2678
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
2679
- };
2680
- if (toolCalls.length > 0) {
2681
- response.tool_calls = toolCalls;
2682
- }
2683
- if (streamProviderMetadata) {
2684
- response.providerMetadata = streamProviderMetadata;
2685
- }
2686
- if (thinkingContent) {
2687
- response.thinking_content = thinkingContent;
2688
- }
2689
- } else {
2690
- response = await provider.chat(currentMessages, chatOptions);
3440
+ } catch (err) {
3441
+ this.emitEvent({
3442
+ type: "agent_call_end",
3443
+ agent: agent2._name,
3444
+ model: modelUri,
3445
+ promptVersion: agent2._config.version,
3446
+ duration: Date.now() - turnStart,
3447
+ data: {
3448
+ response: "",
3449
+ turn: turns,
3450
+ ...retryReason ? { retryReason } : {},
3451
+ error: err instanceof Error ? err.message : String(err)
3452
+ }
3453
+ });
3454
+ throw err;
2691
3455
  }
2692
3456
  if (usageCapture && response.usage) {
2693
3457
  usageCapture.value = response.usage;
2694
3458
  }
2695
3459
  if (response.cost) {
2696
- if (this.budgetContext) {
2697
- this.budgetContext.totalCost += response.cost;
2698
- if (this.budgetContext.totalCost >= this.budgetContext.limit) {
2699
- this.budgetContext.exceeded = true;
2700
- if (this.budgetContext.policy === "hard_stop" && this.budgetContext.abortController) {
2701
- this.budgetContext.abortController.abort();
2702
- }
2703
- }
2704
- }
3460
+ this._accumulateBudgetCost(response.cost);
2705
3461
  }
2706
- this.emitTrace({
2707
- type: "agent_call",
3462
+ this.emitEvent({
3463
+ type: "agent_call_end",
2708
3464
  agent: agent2._name,
2709
3465
  model: modelUri,
2710
3466
  promptVersion: agent2._config.version,
@@ -2714,8 +3470,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2714
3470
  output: response.usage.completion_tokens,
2715
3471
  reasoning: response.usage.reasoning_tokens
2716
3472
  } : void 0,
2717
- duration: Date.now() - startTime,
2718
- data: { prompt, response: response.content }
3473
+ duration: Date.now() - turnStart,
3474
+ data: {
3475
+ response: response.content,
3476
+ ...response.thinking_content ? { thinking: response.thinking_content } : {},
3477
+ turn: turns,
3478
+ ...retryReason ? { retryReason } : {}
3479
+ }
2719
3480
  });
2720
3481
  if (response.tool_calls && response.tool_calls.length > 0) {
2721
3482
  currentMessages.push({
@@ -2740,6 +3501,19 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2740
3501
  }
2741
3502
  }
2742
3503
  const handoffStart = Date.now();
3504
+ const sourceFrame = askStorage.getStore();
3505
+ const handoffFromAskId = sourceFrame?.askId ?? this.executionId;
3506
+ const handoffSourceDepth = sourceFrame?.depth ?? 0;
3507
+ const handoffToAskId = (0, import_node_crypto.randomUUID)();
3508
+ const handoffTargetDepth = handoffSourceDepth + 1;
3509
+ const targetFrame = {
3510
+ askId: handoffToAskId,
3511
+ parentAskId: handoffFromAskId,
3512
+ depth: handoffTargetDepth,
3513
+ agent: descriptor.agent._name,
3514
+ stepRef: sourceFrame?.stepRef ?? this.stepRefRoot,
3515
+ askCost: { value: 0 }
3516
+ };
2743
3517
  const handoffOptions = options ? {
2744
3518
  schema: options.schema,
2745
3519
  retries: options.retries,
@@ -2747,13 +3521,51 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2747
3521
  validate: options.validate,
2748
3522
  validateRetries: options.validateRetries
2749
3523
  } : void 0;
2750
- const handoffFn = () => this.executeAgentCall(
2751
- descriptor.agent,
2752
- handoffPrompt,
2753
- handoffOptions,
2754
- currentMessages,
2755
- usageCapture
2756
- );
3524
+ const handoffFn = () => askStorage.run(targetFrame, async () => {
3525
+ this.emitEvent({ type: "ask_start", prompt: handoffPrompt });
3526
+ const targetAskStart = Date.now();
3527
+ try {
3528
+ const result = await this.executeAgentCall(
3529
+ descriptor.agent,
3530
+ handoffPrompt,
3531
+ handoffOptions,
3532
+ currentMessages,
3533
+ usageCapture
3534
+ );
3535
+ this.emitEvent({
3536
+ type: "ask_end",
3537
+ outcome: { ok: true, result },
3538
+ cost: targetFrame.askCost.value,
3539
+ duration: Date.now() - targetAskStart
3540
+ });
3541
+ return result;
3542
+ } catch (err) {
3543
+ this.emitEvent({
3544
+ type: "ask_end",
3545
+ outcome: {
3546
+ ok: false,
3547
+ error: err instanceof Error ? err.message : String(err)
3548
+ },
3549
+ cost: targetFrame.askCost.value,
3550
+ duration: Date.now() - targetAskStart
3551
+ });
3552
+ throw err;
3553
+ }
3554
+ });
3555
+ this.emitEvent({
3556
+ type: "handoff_start",
3557
+ agent: agent2._name,
3558
+ fromAskId: handoffFromAskId,
3559
+ toAskId: handoffToAskId,
3560
+ sourceDepth: handoffSourceDepth,
3561
+ targetDepth: handoffTargetDepth,
3562
+ data: {
3563
+ source: agent2._name,
3564
+ target: targetName,
3565
+ mode,
3566
+ ...mode === "roundtrip" && handoffPrompt !== prompt ? { message: handoffPrompt } : {}
3567
+ }
3568
+ });
2757
3569
  if (mode === "roundtrip") {
2758
3570
  const executeRoundtrip = async () => {
2759
3571
  const result = await handoffFn();
@@ -2774,24 +3586,41 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2774
3586
  "axl.handoff.mode": mode
2775
3587
  },
2776
3588
  async (span) => {
2777
- const result = await executeRoundtrip();
2778
- const duration = Date.now() - handoffStart;
2779
- span.setAttribute("axl.handoff.duration", duration);
2780
- this.emitTrace({
2781
- type: "handoff",
2782
- agent: agent2._name,
2783
- data: { target: targetName, mode, duration }
2784
- });
2785
- return result;
3589
+ try {
3590
+ return await executeRoundtrip();
3591
+ } finally {
3592
+ const duration = Date.now() - handoffStart;
3593
+ span.setAttribute("axl.handoff.duration", duration);
3594
+ this.emitEvent({
3595
+ type: "handoff_return",
3596
+ agent: agent2._name,
3597
+ fromAskId: handoffFromAskId,
3598
+ toAskId: handoffToAskId,
3599
+ sourceDepth: handoffSourceDepth,
3600
+ targetDepth: handoffTargetDepth,
3601
+ data: { source: agent2._name, target: targetName, duration }
3602
+ });
3603
+ }
2786
3604
  }
2787
3605
  );
2788
3606
  } else {
2789
- await executeRoundtrip();
2790
- this.emitTrace({
2791
- type: "handoff",
2792
- agent: agent2._name,
2793
- data: { target: targetName, mode, duration: Date.now() - handoffStart }
2794
- });
3607
+ try {
3608
+ await executeRoundtrip();
3609
+ } finally {
3610
+ this.emitEvent({
3611
+ type: "handoff_return",
3612
+ agent: agent2._name,
3613
+ fromAskId: handoffFromAskId,
3614
+ toAskId: handoffToAskId,
3615
+ sourceDepth: handoffSourceDepth,
3616
+ targetDepth: handoffTargetDepth,
3617
+ data: {
3618
+ source: agent2._name,
3619
+ target: targetName,
3620
+ duration: Date.now() - handoffStart
3621
+ }
3622
+ });
3623
+ }
2795
3624
  }
2796
3625
  continue;
2797
3626
  }
@@ -2805,24 +3634,12 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2805
3634
  },
2806
3635
  async (span) => {
2807
3636
  const result = await handoffFn();
2808
- const duration = Date.now() - handoffStart;
2809
- span.setAttribute("axl.handoff.duration", duration);
2810
- this.emitTrace({
2811
- type: "handoff",
2812
- agent: agent2._name,
2813
- data: { target: targetName, mode, duration }
2814
- });
3637
+ span.setAttribute("axl.handoff.duration", Date.now() - handoffStart);
2815
3638
  return result;
2816
3639
  }
2817
3640
  );
2818
3641
  }
2819
- const onewayResult = await handoffFn();
2820
- this.emitTrace({
2821
- type: "handoff",
2822
- agent: agent2._name,
2823
- data: { target: targetName, mode, duration: Date.now() - handoffStart }
2824
- });
2825
- return onewayResult;
3642
+ return await handoffFn();
2826
3643
  }
2827
3644
  }
2828
3645
  const toolOverride = this.toolOverrides?.get(toolName);
@@ -2838,7 +3655,16 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2838
3655
  });
2839
3656
  continue;
2840
3657
  }
2841
- this.onToolCall?.({ name: toolName, args: toolArgs2, callId: toolCall.id });
3658
+ this.emitEvent({
3659
+ type: "tool_call_start",
3660
+ tool: toolName,
3661
+ callId: toolCall.id,
3662
+ data: { args: toolArgs2 }
3663
+ });
3664
+ this.onToolCall?.(
3665
+ { name: toolName, args: toolArgs2, callId: toolCall.id },
3666
+ callbackMeta
3667
+ );
2842
3668
  const toolStart2 = Date.now();
2843
3669
  const executeOverride = async () => {
2844
3670
  let toolResult3;
@@ -2866,8 +3692,8 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2866
3692
  }
2867
3693
  ) : await executeOverride();
2868
3694
  const resultContent2 = JSON.stringify(toolResult2);
2869
- this.emitTrace({
2870
- type: "tool_call",
3695
+ this.emitEvent({
3696
+ type: "tool_call_end",
2871
3697
  agent: agent2._name,
2872
3698
  tool: toolName,
2873
3699
  duration: Date.now() - toolStart2,
@@ -2883,7 +3709,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2883
3709
  const tool2 = agent2._config.tools?.find((t) => t.name === toolName);
2884
3710
  const isMcpTool = !tool2 && this.mcpManager?.isMcpTool(toolName);
2885
3711
  if (!tool2 && !isMcpTool) {
2886
- this.emitTrace({ type: "tool_denied", agent: agent2._name, tool: toolName });
3712
+ this.emitEvent({ type: "tool_denied", agent: agent2._name, tool: toolName });
2887
3713
  currentMessages.push({
2888
3714
  role: "tool",
2889
3715
  content: `Tool "${toolName}" is not available. Available tools: ${agent2._config.tools?.map((t) => t.name).join(", ") ?? "none"}`,
@@ -2902,7 +3728,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2902
3728
  });
2903
3729
  continue;
2904
3730
  }
2905
- this.onToolCall?.({ name: toolName, args: toolArgs, callId: toolCall.id });
3731
+ this.emitEvent({
3732
+ type: "tool_call_start",
3733
+ tool: toolName,
3734
+ callId: toolCall.id,
3735
+ data: { args: toolArgs }
3736
+ });
3737
+ this.onToolCall?.({ name: toolName, args: toolArgs, callId: toolCall.id }, callbackMeta);
2906
3738
  const toolStart = Date.now();
2907
3739
  if (tool2 && tool2.requireApproval) {
2908
3740
  const approvalFn = async () => {
@@ -2913,24 +3745,18 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2913
3745
  });
2914
3746
  if (!decision.approved) {
2915
3747
  const reason = decision.reason ?? "Denied by human";
2916
- this.emitTrace({
2917
- type: "tool_denied",
2918
- agent: agent2._name,
2919
- tool: toolName,
2920
- data: { denied: true, reason, args: toolArgs }
2921
- });
2922
3748
  currentMessages.push({
2923
3749
  role: "tool",
2924
3750
  content: JSON.stringify({ error: `Tool denied by human: ${reason}` }),
2925
3751
  tool_call_id: toolCall.id
2926
3752
  });
2927
- return false;
3753
+ return { approved: false, reason };
2928
3754
  }
2929
- return true;
3755
+ return { approved: true };
2930
3756
  };
2931
- let approved;
3757
+ let approvalOutcome;
2932
3758
  if (this.spanManager) {
2933
- approved = await this.spanManager.withSpanAsync(
3759
+ approvalOutcome = await this.spanManager.withSpanAsync(
2934
3760
  "axl.tool.approval",
2935
3761
  {
2936
3762
  "axl.tool.name": toolName,
@@ -2938,20 +3764,24 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2938
3764
  },
2939
3765
  async (span) => {
2940
3766
  const result = await approvalFn();
2941
- span.setAttribute("axl.tool.approval.approved", result);
3767
+ span.setAttribute("axl.tool.approval.approved", result.approved);
2942
3768
  return result;
2943
3769
  }
2944
3770
  );
2945
3771
  } else {
2946
- approved = await approvalFn();
3772
+ approvalOutcome = await approvalFn();
2947
3773
  }
2948
- if (!approved) continue;
2949
- this.emitTrace({
2950
- type: "tool_denied",
3774
+ this.emitEvent({
3775
+ type: "tool_approval",
2951
3776
  agent: agent2._name,
2952
3777
  tool: toolName,
2953
- data: { denied: false, args: toolArgs }
3778
+ data: {
3779
+ approved: approvalOutcome.approved,
3780
+ args: toolArgs,
3781
+ ...approvalOutcome.reason ? { reason: approvalOutcome.reason } : {}
3782
+ }
2954
3783
  });
3784
+ if (!approvalOutcome.approved) continue;
2955
3785
  }
2956
3786
  if (tool2 && tool2.hooks?.before) {
2957
3787
  try {
@@ -3026,8 +3856,8 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3026
3856
  return r;
3027
3857
  }
3028
3858
  ) : await executeTool();
3029
- this.emitTrace({
3030
- type: "tool_call",
3859
+ this.emitEvent({
3860
+ type: "tool_call_end",
3031
3861
  agent: agent2._name,
3032
3862
  tool: traceName,
3033
3863
  duration: Date.now() - toolStart,
@@ -3044,37 +3874,52 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3044
3874
  const content = response.content;
3045
3875
  if (guardrails?.output) {
3046
3876
  const outputResult = await guardrails.output(content, { metadata: this.metadata });
3047
- this.emitTrace({
3877
+ const attempt = guardrailOutputRetries + 1;
3878
+ const maxAttempts = maxGuardrailRetries + 1;
3879
+ const onBlock = guardrails.onBlock ?? "throw";
3880
+ let feedbackMessage;
3881
+ if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
3882
+ feedbackMessage = `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
3883
+ }
3884
+ this.emitEvent({
3048
3885
  type: "guardrail",
3049
3886
  agent: agent2._name,
3050
3887
  data: {
3051
3888
  guardrailType: "output",
3052
3889
  blocked: outputResult.block,
3053
- reason: outputResult.reason
3890
+ ...outputResult.reason ? { reason: outputResult.reason } : {},
3891
+ attempt,
3892
+ maxAttempts,
3893
+ ...feedbackMessage ? { feedbackMessage } : {}
3054
3894
  }
3055
3895
  });
3056
3896
  this.spanManager?.addEventToActiveSpan("axl.guardrail.check", {
3057
3897
  "axl.guardrail.type": "output",
3058
3898
  "axl.guardrail.blocked": outputResult.block,
3899
+ "axl.guardrail.attempt": attempt,
3900
+ "axl.guardrail.maxAttempts": maxAttempts,
3059
3901
  ...outputResult.reason ? { "axl.guardrail.reason": outputResult.reason } : {}
3060
3902
  });
3061
3903
  if (outputResult.block) {
3062
- const onBlock = guardrails.onBlock ?? "throw";
3063
- if (onBlock === "retry") {
3064
- const maxGuardrailRetries = guardrails.maxRetries ?? 2;
3065
- if (guardrailOutputRetries < maxGuardrailRetries) {
3066
- guardrailOutputRetries++;
3067
- currentMessages.push({
3068
- role: "assistant",
3069
- content,
3070
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3071
- });
3072
- currentMessages.push({
3073
- role: "system",
3074
- content: `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`
3075
- });
3076
- continue;
3077
- }
3904
+ if (feedbackMessage) {
3905
+ this.emitEvent({
3906
+ type: "pipeline",
3907
+ agent: agent2._name,
3908
+ status: "failed",
3909
+ stage: "guardrail",
3910
+ attempt: guardrailOutputRetries + 1,
3911
+ maxAttempts: maxGuardrailRetries + 1,
3912
+ reason: feedbackMessage
3913
+ });
3914
+ guardrailOutputRetries++;
3915
+ appendRetryMessages(
3916
+ currentMessages,
3917
+ content,
3918
+ feedbackMessage,
3919
+ response.providerMetadata
3920
+ );
3921
+ pendingRetryReason = "guardrail";
3922
+ continue;
3078
3923
  }
3079
3924
  if (typeof onBlock === "function") {
3080
3925
  return onBlock(outputResult.reason ?? "Output blocked by guardrail", {
@@ -3086,30 +3931,67 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3086
3931
  }
3087
3932
  let validated = void 0;
3088
3933
  if (options?.schema) {
3934
+ const maxSchemaRetries = options.retries ?? 3;
3935
+ const schemaAttempt = schemaRetries + 1;
3936
+ const schemaMaxAttempts = maxSchemaRetries + 1;
3937
+ let schemaValid = false;
3938
+ let schemaReason;
3939
+ let schemaFeedback;
3940
+ let schemaErr;
3089
3941
  try {
3090
3942
  const parsed = JSON.parse(extractJson(content));
3091
3943
  validated = options.schema.parse(parsed);
3944
+ schemaValid = true;
3092
3945
  } catch (err) {
3093
- const maxSchemaRetries = options.retries ?? 3;
3946
+ schemaErr = err;
3947
+ schemaReason = err instanceof Error ? err.message : String(err);
3094
3948
  if (schemaRetries < maxSchemaRetries) {
3949
+ schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
3950
+ }
3951
+ }
3952
+ this.emitEvent({
3953
+ type: "schema_check",
3954
+ agent: agent2._name,
3955
+ data: {
3956
+ valid: schemaValid,
3957
+ ...schemaReason ? { reason: schemaReason } : {},
3958
+ attempt: schemaAttempt,
3959
+ maxAttempts: schemaMaxAttempts,
3960
+ ...schemaFeedback ? { feedbackMessage: schemaFeedback } : {}
3961
+ }
3962
+ });
3963
+ this.spanManager?.addEventToActiveSpan("axl.schema.check", {
3964
+ "axl.schema.valid": schemaValid,
3965
+ "axl.schema.attempt": schemaAttempt,
3966
+ "axl.schema.maxAttempts": schemaMaxAttempts,
3967
+ ...schemaReason ? { "axl.schema.reason": schemaReason } : {}
3968
+ });
3969
+ if (!schemaValid) {
3970
+ if (schemaFeedback) {
3971
+ this.emitEvent({
3972
+ type: "pipeline",
3973
+ agent: agent2._name,
3974
+ status: "failed",
3975
+ stage: "schema",
3976
+ attempt: schemaRetries + 1,
3977
+ maxAttempts: (options.retries ?? 3) + 1,
3978
+ reason: schemaFeedback
3979
+ });
3095
3980
  schemaRetries++;
3096
- const errorMsg = err instanceof Error ? err.message : String(err);
3097
- currentMessages.push({
3098
- role: "assistant",
3981
+ appendRetryMessages(
3982
+ currentMessages,
3099
3983
  content,
3100
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3101
- });
3102
- currentMessages.push({
3103
- role: "system",
3104
- content: `Your response was not valid JSON or did not match the required schema: ${errorMsg}. Please fix and try again.`
3105
- });
3984
+ schemaFeedback,
3985
+ response.providerMetadata
3986
+ );
3987
+ pendingRetryReason = "schema";
3106
3988
  continue;
3107
3989
  }
3108
- const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([
3990
+ const zodErr = schemaErr instanceof import_zod.ZodError ? schemaErr : new import_zod.ZodError([
3109
3991
  {
3110
3992
  code: "custom",
3111
3993
  path: [],
3112
- message: err instanceof Error ? err.message : String(err)
3994
+ message: schemaReason ?? "Schema parse failed"
3113
3995
  }
3114
3996
  ]);
3115
3997
  throw new VerifyError(content, zodErr, maxSchemaRetries);
@@ -3125,31 +4007,49 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3125
4007
  const reason = err instanceof Error ? err.message : String(err);
3126
4008
  validateResult = { valid: false, reason: `Validator error: ${reason}` };
3127
4009
  }
3128
- this.emitTrace({
4010
+ const maxValidateRetries = options.validateRetries ?? 2;
4011
+ const validateAttempt = validateRetries + 1;
4012
+ const validateMaxAttempts = maxValidateRetries + 1;
4013
+ let validateFeedback;
4014
+ if (!validateResult.valid && validateRetries < maxValidateRetries) {
4015
+ validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
4016
+ }
4017
+ this.emitEvent({
3129
4018
  type: "validate",
3130
4019
  agent: agent2._name,
3131
4020
  data: {
3132
4021
  valid: validateResult.valid,
3133
- ...validateResult.reason ? { reason: validateResult.reason } : {}
4022
+ ...validateResult.reason ? { reason: validateResult.reason } : {},
4023
+ attempt: validateAttempt,
4024
+ maxAttempts: validateMaxAttempts,
4025
+ ...validateFeedback ? { feedbackMessage: validateFeedback } : {}
3134
4026
  }
3135
4027
  });
3136
4028
  this.spanManager?.addEventToActiveSpan("axl.validate.check", {
3137
4029
  "axl.validate.valid": validateResult.valid,
4030
+ "axl.validate.attempt": validateAttempt,
4031
+ "axl.validate.maxAttempts": validateMaxAttempts,
3138
4032
  ...validateResult.reason ? { "axl.validate.reason": validateResult.reason } : {}
3139
4033
  });
3140
4034
  if (!validateResult.valid) {
3141
- const maxValidateRetries = options.validateRetries ?? 2;
3142
- if (validateRetries < maxValidateRetries) {
4035
+ if (validateFeedback) {
4036
+ this.emitEvent({
4037
+ type: "pipeline",
4038
+ agent: agent2._name,
4039
+ status: "failed",
4040
+ stage: "validate",
4041
+ attempt: validateRetries + 1,
4042
+ maxAttempts: (options.validateRetries ?? 2) + 1,
4043
+ reason: validateFeedback
4044
+ });
3143
4045
  validateRetries++;
3144
- currentMessages.push({
3145
- role: "assistant",
4046
+ appendRetryMessages(
4047
+ currentMessages,
3146
4048
  content,
3147
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3148
- });
3149
- currentMessages.push({
3150
- role: "system",
3151
- content: `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`
3152
- });
4049
+ validateFeedback,
4050
+ response.providerMetadata
4051
+ );
4052
+ pendingRetryReason = "validate";
3153
4053
  continue;
3154
4054
  }
3155
4055
  throw new ValidationError(
@@ -3159,6 +4059,14 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3159
4059
  );
3160
4060
  }
3161
4061
  }
4062
+ this.emitEvent({
4063
+ type: "pipeline",
4064
+ agent: agent2._name,
4065
+ status: "committed",
4066
+ stage: lastStartStage,
4067
+ attempt: lastStartAttempt,
4068
+ maxAttempts: lastStartMaxAttempts
4069
+ });
3162
4070
  this.pushAssistantToSessionHistory(content, response.providerMetadata);
3163
4071
  return validated ?? content;
3164
4072
  }
@@ -3287,43 +4195,100 @@ ${summaryResponse.content}`
3287
4195
  /**
3288
4196
  * Execute a function with checkpoint-replay semantics.
3289
4197
  *
3290
- * On first execution, runs `fn()`, saves the result, and returns it.
3291
- * On replay (resume after restart), returns the saved result without re-executing.
3292
- * This prevents duplicate side effects (double API calls, double refunds, etc.).
4198
+ * On first execution, runs `fn()`, saves the result under `name`, and
4199
+ * returns it. On replay (resume after restart), returns the saved result
4200
+ * without re-executing. This prevents duplicate side effects (double API
4201
+ * calls, double refunds, etc.).
4202
+ *
4203
+ * `name` must be a stable, caller-supplied identifier — the same call
4204
+ * site MUST pass the same name across runs of the same execution for
4205
+ * replay to work. Names are scoped to a single execution and must be
4206
+ * unique within it; reusing a name (e.g. inside a loop) gives
4207
+ * last-write-wins behavior. For loops, compose names from the iterator:
4208
+ * `ctx.checkpoint(`process-${id}`, fn)`.
4209
+ *
4210
+ * Names starting with `__auto/` are reserved for runtime-internal
4211
+ * auto-checkpointing of `ctx.ask` / spawn / race / parallel / map.
3293
4212
  */
3294
- async checkpoint(fn) {
3295
- return this._checkpoint(fn);
4213
+ async checkpoint(name, fn) {
4214
+ if (typeof name !== "string" || name.length === 0) {
4215
+ throw new Error("ctx.checkpoint name must be a non-empty string.");
4216
+ }
4217
+ if (name !== name.trim()) {
4218
+ throw new Error(
4219
+ `ctx.checkpoint name "${name}" has leading/trailing whitespace \u2014 trim before passing.`
4220
+ );
4221
+ }
4222
+ if (name.toLowerCase().startsWith("__auto/")) {
4223
+ throw new Error(
4224
+ `ctx.checkpoint name "${name}" is reserved \u2014 names starting with "__auto/" (case-insensitive) are used by the runtime for ask/spawn/race/parallel/map auto-checkpointing.`
4225
+ );
4226
+ }
4227
+ return this._checkpoint(name, fn);
3296
4228
  }
3297
4229
  /**
3298
- * Internal checkpoint implementation shared by both the public checkpoint()
3299
- * and the automatic checkpointing in ask/spawn/race/parallel/map.
4230
+ * Internal checkpoint implementation shared by both the public
4231
+ * `checkpoint(name, fn)` and the automatic checkpointing in
4232
+ * ask/spawn/race/parallel/map. Internal callers pass an `__auto/...`
4233
+ * name composed by `_autoCheckpointName`.
4234
+ *
4235
+ * `agent` is the originating agent for ask auto-checkpoints — passing
4236
+ * it explicitly stamps `event.agent` on the trace events even though
4237
+ * the emit fires before/after `fn()` (where the ask's ALS frame
4238
+ * either doesn't exist yet or has already torn down). Without this,
4239
+ * post-fn `checkpoint_save` rows show no agent in the trace UI.
3300
4240
  */
3301
- async _checkpoint(fn) {
3302
- const step = this.checkpointCounter++;
4241
+ async _checkpoint(name, fn, options) {
3303
4242
  if (!this.stateStore) {
3304
4243
  return fn();
3305
4244
  }
3306
- const saved = await this.stateStore.getCheckpoint(this.executionId, step);
4245
+ const agentStamp = options?.agent ? { agent: options.agent } : {};
4246
+ const saved = await this.stateStore.getCheckpoint(this.executionId, name);
3307
4247
  if (saved !== null) {
3308
- this.emitTrace({
3309
- type: "log",
3310
- data: { event: "checkpoint_replay", step }
4248
+ this.emitEvent({
4249
+ type: "checkpoint_replay",
4250
+ ...agentStamp,
4251
+ data: { name }
3311
4252
  });
3312
- this.spanManager?.addEventToActiveSpan("axl.checkpoint.hit", { "axl.checkpoint.step": step });
4253
+ this.spanManager?.addEventToActiveSpan("axl.checkpoint.hit", { "axl.checkpoint.name": name });
3313
4254
  return saved;
3314
4255
  }
3315
4256
  const result = await fn();
3316
- await this.stateStore.saveCheckpoint(this.executionId, step, result);
3317
- this.emitTrace({
3318
- type: "log",
3319
- data: { event: "checkpoint_save", step }
4257
+ await this.stateStore.saveCheckpoint(this.executionId, name, result);
4258
+ this.emitEvent({
4259
+ type: "checkpoint_save",
4260
+ ...agentStamp,
4261
+ data: { name }
3320
4262
  });
3321
- this.spanManager?.addEventToActiveSpan("axl.checkpoint.miss", { "axl.checkpoint.step": step });
4263
+ this.spanManager?.addEventToActiveSpan("axl.checkpoint.miss", { "axl.checkpoint.name": name });
3322
4264
  return result;
3323
4265
  }
4266
+ /** Generate a stable auto-checkpoint name for an internal primitive.
4267
+ *
4268
+ * For `ctx.ask`, the originating agent is known and gets a per-agent
4269
+ * counter — the resulting name reads as "orchestrator's first ask"
4270
+ * (`__auto/orchestrator-agent/ask/0`) instead of a global ordinal.
4271
+ * For `spawn` / `race` / `parallel` / `map`, no agent context is
4272
+ * available (those wrap arbitrary fn callbacks), so the name uses
4273
+ * the root counter: `__auto/spawn/0`, `__auto/race/3`, etc.
4274
+ *
4275
+ * All counters live on a shared ref passed through `createChildContext`,
4276
+ * so a tool handler's nested `ctx.ask()` (which runs in a child
4277
+ * WorkflowContext) can never collide with the parent's auto-names in
4278
+ * the state store. */
4279
+ _autoCheckpointName(primitive, agent2) {
4280
+ const counters = this.autoCheckpointCounters;
4281
+ if (primitive === "ask" && agent2) {
4282
+ const n2 = counters.byAgent.get(agent2) ?? 0;
4283
+ counters.byAgent.set(agent2, n2 + 1);
4284
+ return `__auto/${agent2}/ask/${n2}`;
4285
+ }
4286
+ const n = counters.root++;
4287
+ return `__auto/${primitive}/${n}`;
4288
+ }
3324
4289
  // ── ctx.spawn() ───────────────────────────────────────────────────────
3325
4290
  async spawn(n, fn, options) {
3326
- return this._checkpoint(() => {
4291
+ return this._checkpoint(this._autoCheckpointName("spawn"), () => {
3327
4292
  if (this.spanManager) {
3328
4293
  return this.spanManager.withSpanAsync(
3329
4294
  "axl.ctx.spawn",
@@ -3518,6 +4483,17 @@ ${summaryResponse.content}`
3518
4483
  async verify(fn, schema, options) {
3519
4484
  const maxRetries = options?.retries ?? 3;
3520
4485
  let lastRetry = void 0;
4486
+ const emitVerifyOutcome = (passed, attempts, lastError) => {
4487
+ this.emitEvent({
4488
+ type: "verify",
4489
+ agent: void 0,
4490
+ data: {
4491
+ passed,
4492
+ attempts,
4493
+ ...lastError ? { lastError } : {}
4494
+ }
4495
+ });
4496
+ };
3521
4497
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3522
4498
  let rawOutput;
3523
4499
  try {
@@ -3536,12 +4512,14 @@ ${summaryResponse.content}`
3536
4512
  const errorMsg = validateResult.reason ?? "Validation failed";
3537
4513
  lastRetry = { error: errorMsg, output: rawOutput, parsed };
3538
4514
  if (attempt === maxRetries) {
4515
+ emitVerifyOutcome(false, attempt + 1, errorMsg);
3539
4516
  if (options?.fallback !== void 0) return options.fallback;
3540
4517
  throw new ValidationError(parsed, errorMsg, maxRetries);
3541
4518
  }
3542
4519
  continue;
3543
4520
  }
3544
4521
  }
4522
+ emitVerifyOutcome(true, attempt + 1);
3545
4523
  return parsed;
3546
4524
  } catch (err) {
3547
4525
  if (err instanceof ValidationError) {
@@ -3551,6 +4529,7 @@ ${summaryResponse.content}`
3551
4529
  parsed: err.lastOutput
3552
4530
  };
3553
4531
  if (attempt === maxRetries) {
4532
+ emitVerifyOutcome(false, attempt + 1, err.reason);
3554
4533
  if (options?.fallback !== void 0) return options.fallback;
3555
4534
  throw err;
3556
4535
  }
@@ -3559,6 +4538,7 @@ ${summaryResponse.content}`
3559
4538
  if (err instanceof VerifyError) {
3560
4539
  lastRetry = { error: err.message, output: rawOutput ?? err.lastOutput };
3561
4540
  if (attempt === maxRetries) {
4541
+ emitVerifyOutcome(false, attempt + 1, err.message);
3562
4542
  if (options?.fallback !== void 0) return options.fallback;
3563
4543
  throw err;
3564
4544
  }
@@ -3567,12 +4547,14 @@ ${summaryResponse.content}`
3567
4547
  const errorMsg = err instanceof import_zod.ZodError ? err.message : err instanceof Error ? err.message : String(err);
3568
4548
  lastRetry = { error: errorMsg, output: rawOutput };
3569
4549
  if (attempt === maxRetries) {
4550
+ emitVerifyOutcome(false, attempt + 1, errorMsg);
3570
4551
  if (options?.fallback !== void 0) return options.fallback;
3571
4552
  const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([{ code: "custom", path: [], message: errorMsg }]);
3572
4553
  throw new VerifyError(rawOutput, zodErr, maxRetries);
3573
4554
  }
3574
4555
  }
3575
4556
  }
4557
+ emitVerifyOutcome(false, maxRetries + 1, lastRetry?.error ?? "Verify failed");
3576
4558
  if (options?.fallback !== void 0) return options.fallback;
3577
4559
  throw new VerifyError(
3578
4560
  lastRetry?.output,
@@ -3642,7 +4624,7 @@ ${summaryResponse.content}`
3642
4624
  }
3643
4625
  // ── ctx.race() ────────────────────────────────────────────────────────
3644
4626
  async race(fns, options) {
3645
- return this._checkpoint(() => {
4627
+ return this._checkpoint(this._autoCheckpointName("race"), () => {
3646
4628
  if (this.spanManager) {
3647
4629
  return this.spanManager.withSpanAsync(
3648
4630
  "axl.ctx.race",
@@ -3747,11 +4729,17 @@ ${summaryResponse.content}`
3747
4729
  }
3748
4730
  // ── ctx.parallel() ────────────────────────────────────────────────────
3749
4731
  async parallel(fns) {
3750
- return this._checkpoint(() => Promise.all(fns.map((fn) => fn())));
4732
+ return this._checkpoint(
4733
+ this._autoCheckpointName("parallel"),
4734
+ () => Promise.all(fns.map((fn) => fn()))
4735
+ );
3751
4736
  }
3752
4737
  // ── ctx.map() ─────────────────────────────────────────────────────────
3753
4738
  async map(items, fn, options) {
3754
- return this._checkpoint(() => this._mapImpl(items, fn, options));
4739
+ return this._checkpoint(
4740
+ this._autoCheckpointName("map"),
4741
+ () => this._mapImpl(items, fn, options)
4742
+ );
3755
4743
  }
3756
4744
  async _mapImpl(items, fn, options) {
3757
4745
  const concurrency = options?.concurrency ?? 5;
@@ -3829,10 +4817,14 @@ ${summaryResponse.content}`
3829
4817
  }
3830
4818
  async _awaitHumanImpl(options) {
3831
4819
  if (this.awaitHumanHandler) {
4820
+ this.emitEvent({
4821
+ type: "await_human",
4822
+ data: { channel: options.channel, prompt: options.prompt }
4823
+ });
3832
4824
  const decision2 = await this.awaitHumanHandler(options);
3833
- this.emitTrace({
3834
- type: "log",
3835
- data: { event: "await_human_resolved", channel: options.channel, decision: decision2 }
4825
+ this.emitEvent({
4826
+ type: "await_human_resolved",
4827
+ data: { channel: options.channel, decision: decision2 }
3836
4828
  });
3837
4829
  return decision2;
3838
4830
  }
@@ -3852,7 +4844,7 @@ ${summaryResponse.content}`
3852
4844
  await this.stateStore.saveExecutionState(this.executionId, {
3853
4845
  workflow: this.workflowName ?? "unknown",
3854
4846
  input: this.input,
3855
- step: this.stepCounter,
4847
+ step: this.stepRefRoot.value,
3856
4848
  status: "waiting",
3857
4849
  metadata: {
3858
4850
  ...this.metadata,
@@ -3861,26 +4853,111 @@ ${summaryResponse.content}`
3861
4853
  }
3862
4854
  });
3863
4855
  }
3864
- this.emitTrace({
3865
- type: "log",
3866
- data: { event: "await_human", channel: options.channel, prompt: options.prompt }
4856
+ this.emitEvent({
4857
+ type: "await_human",
4858
+ data: { channel: options.channel, prompt: options.prompt }
3867
4859
  });
3868
4860
  const decision = await new Promise((resolve) => {
3869
4861
  this.pendingDecisions.set(this.executionId, resolve);
3870
4862
  });
4863
+ this.emitEvent({
4864
+ type: "await_human_resolved",
4865
+ data: { channel: options.channel, decision }
4866
+ });
3871
4867
  if (this.stateStore) {
3872
4868
  await this.stateStore.saveExecutionState(this.executionId, {
3873
4869
  workflow: this.workflowName ?? "unknown",
3874
4870
  input: this.input,
3875
- step: this.stepCounter,
4871
+ step: this.stepRefRoot.value,
3876
4872
  status: "running"
3877
4873
  });
3878
4874
  }
3879
4875
  return decision;
3880
4876
  }
4877
+ /**
4878
+ * Compose an `AbortSignal` for a memory operation's underlying embedder
4879
+ * fetch. Combines the parent context signal (user cancellation,
4880
+ * `runtime.execute({ signal })`) with the budget hard_stop abort so
4881
+ * that either cause correctly aborts an in-flight embed call.
4882
+ *
4883
+ * Returns `undefined` if there's nothing to cancel on — the embedder
4884
+ * runs without a signal in that case, matching its prior behavior.
4885
+ *
4886
+ * @internal
4887
+ */
4888
+ _composeMemorySignal() {
4889
+ const budgetSignal = this.budgetContext?.abortController?.signal;
4890
+ if (this.signal && budgetSignal) return AbortSignal.any([this.signal, budgetSignal]);
4891
+ return this.signal ?? budgetSignal;
4892
+ }
4893
+ /**
4894
+ * Accumulate a cost amount into the active `budgetContext` and trip the
4895
+ * `exceeded` flag if we've crossed the limit. On `hard_stop` policy,
4896
+ * also fires the abort controller so in-flight operations cancel.
4897
+ *
4898
+ * Called from every code path that spends money: the `agent_call` loop,
4899
+ * semantic memory operations (`ctx.remember({embed:true})`, `ctx.recall({query})`),
4900
+ * and any future cost-emitting primitive. Centralizing the logic here
4901
+ * means `ctx.budget({ limit, policy })` accurately enforces the limit
4902
+ * across ALL cost sources — not just agent calls.
4903
+ *
4904
+ * @internal
4905
+ */
4906
+ _accumulateBudgetCost(amount) {
4907
+ if (!this.budgetContext) return;
4908
+ if (!Number.isFinite(amount) || amount <= 0) return;
4909
+ this.budgetContext.totalCost += amount;
4910
+ if (this.budgetContext.totalCost >= this.budgetContext.limit) {
4911
+ this.budgetContext.exceeded = true;
4912
+ if (this.budgetContext.policy === "hard_stop" && this.budgetContext.abortController) {
4913
+ this.budgetContext.abortController.abort();
4914
+ }
4915
+ }
4916
+ }
4917
+ // ── workflow lifecycle trace emission ──────────────────────────────────
4918
+ //
4919
+ // These are called by the runtime at execution boundaries. They emit
4920
+ // first-class `workflow_start` / `workflow_end` trace events instead of
4921
+ // the previous `ctx.log('workflow_start', ...)` indirection — so consumers
4922
+ // that narrow via `event.type === 'workflow_start'` actually see them.
4923
+ // Internal: the runtime is the only caller, user workflows never call these.
4924
+ /** @internal — idempotent; no-ops on second+ call within one ctx. */
4925
+ _emitWorkflowStart(input) {
4926
+ if (this._workflowStartEmitted) return;
4927
+ this._workflowStartEmitted = true;
4928
+ this.emitEvent({
4929
+ type: "workflow_start",
4930
+ workflow: this.workflowName,
4931
+ data: { input }
4932
+ });
4933
+ }
4934
+ /** @internal — idempotent; no-ops on second+ call within one ctx.
4935
+ * Protects against the post-emit-side-effect double-fire (reviewer
4936
+ * bug B1): `runtime.execute()` / `runtime.stream()` emit
4937
+ * `workflow_end(completed)`, then call `deleteCheckpoints` /
4938
+ * `persistExecution`. If either throws, the outer catch would
4939
+ * otherwise fire a second `workflow_end(failed)` with conflicting
4940
+ * status. First-wins semantics: the completed event stands, the
4941
+ * inner side-effect failure propagates as a normal thrown error. */
4942
+ _emitWorkflowEnd(info) {
4943
+ if (this._workflowEndEmitted) return;
4944
+ this._workflowEndEmitted = true;
4945
+ this.emitEvent({
4946
+ type: "workflow_end",
4947
+ workflow: this.workflowName,
4948
+ duration: info.duration,
4949
+ data: {
4950
+ status: info.status,
4951
+ duration: info.duration,
4952
+ ...info.result !== void 0 ? { result: info.result } : {},
4953
+ ...info.error !== void 0 ? { error: info.error } : {},
4954
+ ...info.aborted ? { aborted: true } : {}
4955
+ }
4956
+ });
4957
+ }
3881
4958
  // ── ctx.log() ─────────────────────────────────────────────────────────
3882
4959
  log(event, data) {
3883
- this.emitTrace({
4960
+ this.emitEvent({
3884
4961
  type: "log",
3885
4962
  data: {
3886
4963
  event,
@@ -3913,8 +4990,57 @@ ${summaryResponse.content}`
3913
4990
  if (!this.stateStore) {
3914
4991
  throw new Error("A state store is required for memory operations.");
3915
4992
  }
4993
+ if (this.budgetContext?.exceeded) {
4994
+ const { limit, totalCost: spent, policy } = this.budgetContext;
4995
+ if (policy !== "warn") {
4996
+ throw new BudgetExceededError(limit, spent, policy);
4997
+ }
4998
+ }
3916
4999
  const sessionId = this.metadata?.sessionId;
3917
- await this.memoryManager.remember(key, value, this.stateStore, sessionId, options);
5000
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
5001
+ try {
5002
+ const memorySignal = this._composeMemorySignal();
5003
+ const { usage } = await this.memoryManager.remember(
5004
+ key,
5005
+ value,
5006
+ this.stateStore,
5007
+ sessionId,
5008
+ options,
5009
+ memorySignal
5010
+ );
5011
+ if (usage?.cost != null) {
5012
+ this._accumulateBudgetCost(usage.cost);
5013
+ }
5014
+ this.emitEvent({
5015
+ type: "memory_remember",
5016
+ ...usage?.cost != null ? { cost: usage.cost } : {},
5017
+ ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
5018
+ data: {
5019
+ key,
5020
+ scope,
5021
+ embed: options?.embed === true,
5022
+ ...usage ? { usage } : {}
5023
+ }
5024
+ });
5025
+ } catch (err) {
5026
+ const partialUsage = err.axlEmbedUsage;
5027
+ if (partialUsage?.cost != null) {
5028
+ this._accumulateBudgetCost(partialUsage.cost);
5029
+ }
5030
+ this.emitEvent({
5031
+ type: "memory_remember",
5032
+ ...partialUsage?.cost != null ? { cost: partialUsage.cost } : {},
5033
+ ...partialUsage?.tokens != null ? { tokens: { input: partialUsage.tokens } } : {},
5034
+ data: {
5035
+ key,
5036
+ scope,
5037
+ embed: options?.embed === true,
5038
+ ...partialUsage ? { usage: partialUsage } : {},
5039
+ error: err instanceof Error ? err.message : String(err)
5040
+ }
5041
+ });
5042
+ throw err;
5043
+ }
3918
5044
  }
3919
5045
  /**
3920
5046
  * Recall a value from memory by key, or perform semantic search if query option is provided.
@@ -3928,8 +5054,62 @@ ${summaryResponse.content}`
3928
5054
  if (!this.stateStore) {
3929
5055
  throw new Error("A state store is required for memory operations.");
3930
5056
  }
5057
+ if (this.budgetContext?.exceeded) {
5058
+ const { limit, totalCost: spent, policy } = this.budgetContext;
5059
+ if (policy !== "warn") {
5060
+ throw new BudgetExceededError(limit, spent, policy);
5061
+ }
5062
+ }
3931
5063
  const sessionId = this.metadata?.sessionId;
3932
- return this.memoryManager.recall(key, this.stateStore, sessionId, options);
5064
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
5065
+ const semantic = options?.query !== void 0;
5066
+ try {
5067
+ const memorySignal = this._composeMemorySignal();
5068
+ const { data, usage } = await this.memoryManager.recall(
5069
+ key,
5070
+ this.stateStore,
5071
+ sessionId,
5072
+ options,
5073
+ memorySignal
5074
+ );
5075
+ let hit;
5076
+ let resultCount;
5077
+ if (semantic) {
5078
+ resultCount = Array.isArray(data) ? data.length : 0;
5079
+ hit = resultCount > 0;
5080
+ } else {
5081
+ hit = data !== null && data !== void 0;
5082
+ }
5083
+ if (usage?.cost != null) {
5084
+ this._accumulateBudgetCost(usage.cost);
5085
+ }
5086
+ this.emitEvent({
5087
+ type: "memory_recall",
5088
+ ...usage?.cost != null ? { cost: usage.cost } : {},
5089
+ ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
5090
+ data: {
5091
+ key,
5092
+ scope,
5093
+ semantic,
5094
+ hit,
5095
+ ...resultCount !== void 0 ? { resultCount, count: resultCount } : {},
5096
+ ...usage ? { usage } : {}
5097
+ }
5098
+ });
5099
+ return data;
5100
+ } catch (err) {
5101
+ this.emitEvent({
5102
+ type: "memory_recall",
5103
+ data: {
5104
+ key,
5105
+ scope,
5106
+ semantic,
5107
+ hit: false,
5108
+ error: err instanceof Error ? err.message : String(err)
5109
+ }
5110
+ });
5111
+ throw err;
5112
+ }
3933
5113
  }
3934
5114
  /** Delete a memory entry by key. */
3935
5115
  async forget(key, options) {
@@ -3942,7 +5122,24 @@ ${summaryResponse.content}`
3942
5122
  throw new Error("A state store is required for memory operations.");
3943
5123
  }
3944
5124
  const sessionId = this.metadata?.sessionId;
3945
- await this.memoryManager.forget(key, this.stateStore, sessionId, options);
5125
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
5126
+ try {
5127
+ await this.memoryManager.forget(key, this.stateStore, sessionId, options);
5128
+ this.emitEvent({
5129
+ type: "memory_forget",
5130
+ data: { key, scope }
5131
+ });
5132
+ } catch (err) {
5133
+ this.emitEvent({
5134
+ type: "memory_forget",
5135
+ data: {
5136
+ key,
5137
+ scope,
5138
+ error: err instanceof Error ? err.message : String(err)
5139
+ }
5140
+ });
5141
+ throw err;
5142
+ }
3946
5143
  }
3947
5144
  // ── ctx.delegate() ──────────────────────────────────────────────────
3948
5145
  /**
@@ -3970,6 +5167,15 @@ ${summaryResponse.content}`
3970
5167
  names.add(a._name);
3971
5168
  }
3972
5169
  if (agents.length === 1) {
5170
+ this.emitEvent({
5171
+ type: "delegate",
5172
+ agent: agents[0]._name,
5173
+ data: {
5174
+ candidates: [agents[0]._name],
5175
+ selected: agents[0]._name,
5176
+ reason: "single_candidate"
5177
+ }
5178
+ });
3973
5179
  return this.ask(agents[0], prompt, {
3974
5180
  schema: options?.schema,
3975
5181
  retries: options?.retries,
@@ -4005,12 +5211,13 @@ ${summaryResponse.content}`
4005
5211
  resolveModel: () => routerModelUri,
4006
5212
  resolveSystem: () => routerSystem
4007
5213
  };
4008
- this.emitTrace({
5214
+ this.emitEvent({
4009
5215
  type: "delegate",
4010
5216
  agent: "_delegate_router",
4011
5217
  data: {
4012
5218
  candidates: agents.map((a) => a._name),
4013
- routerModel: routerModelUri
5219
+ routerModel: routerModelUri,
5220
+ reason: "routed"
4014
5221
  }
4015
5222
  });
4016
5223
  return this.ask(routerAgent, prompt, {
@@ -4022,24 +5229,57 @@ ${summaryResponse.content}`
4022
5229
  });
4023
5230
  }
4024
5231
  // ── Private ───────────────────────────────────────────────────────────
4025
- emitTrace(partial) {
4026
- let data = partial.data;
4027
- if (this.config.trace?.redact && partial.type === "agent_call" && data) {
4028
- data = { ...data, prompt: "[redacted]", response: "[redacted]" };
5232
+ /**
5233
+ * Internal emitter input — intentionally loose so call sites don't need to
5234
+ * build a perfectly-narrowed discriminated-union member. The resulting
5235
+ * `AxlEvent` (exported type) remains strict, and TypeScript narrows it at
5236
+ * consumer sites via the `type` discriminator.
5237
+ */
5238
+ emitEvent(partial) {
5239
+ const data = partial.data;
5240
+ const frame = askStorage.getStore();
5241
+ const step = (frame?.stepRef ?? this.stepRefRoot).value++;
5242
+ const cost = partial.cost;
5243
+ if (frame && typeof cost === "number" && Number.isFinite(cost) && cost >= 0 && COST_BEARING_LEAF_TYPES.includes(partial.type)) {
5244
+ frame.askCost.value += cost;
4029
5245
  }
4030
5246
  const event = {
4031
5247
  executionId: this.executionId,
4032
- step: this.stepCounter++,
5248
+ step,
4033
5249
  timestamp: Date.now(),
5250
+ ...this.workflowName ? { workflow: this.workflowName } : {},
5251
+ // Stamp ask correlation from ALS. Variants like `workflow_start` /
5252
+ // `workflow_end` and out-of-ask events (log/memory/checkpoint/await_human
5253
+ // when emitted at workflow scope) may legitimately have no frame —
5254
+ // they still get `depth: 0` so consumers can render them at root
5255
+ // level without having to guess. `askId`/`parentAskId`/`agent` stay
5256
+ // unset for non-ask-scoped events, matching the `Partial<AskScoped>`
5257
+ // shape on those union members.
5258
+ ...frame ? {
5259
+ askId: frame.askId,
5260
+ ...frame.parentAskId ? { parentAskId: frame.parentAskId } : {},
5261
+ depth: frame.depth,
5262
+ ...frame.agent ? { agent: frame.agent } : {}
5263
+ } : { depth: 0 },
4034
5264
  ...partial,
4035
5265
  data
4036
5266
  };
4037
- this.onTrace?.(event);
5267
+ const finalEvent = this.config.trace?.redact ? redactEvent(event) : event;
5268
+ if (this.onTrace) {
5269
+ try {
5270
+ this.onTrace(finalEvent);
5271
+ } catch (err) {
5272
+ console.error(
5273
+ "[axl] onTrace handler threw; trace event dropped:",
5274
+ err instanceof Error ? err.message : String(err)
5275
+ );
5276
+ }
5277
+ }
4038
5278
  }
4039
5279
  };
4040
5280
 
4041
5281
  // src/agent.ts
4042
- var import_node_crypto = require("crypto");
5282
+ var import_node_crypto2 = require("crypto");
4043
5283
  var agentCounter = 0;
4044
5284
  function agent(config) {
4045
5285
  agentCounter++;
@@ -4059,7 +5299,7 @@ function agent(config) {
4059
5299
  const registry = new ProviderRegistry();
4060
5300
  const ctx = new WorkflowContext({
4061
5301
  input: prompt,
4062
- executionId: (0, import_node_crypto.randomUUID)(),
5302
+ executionId: (0, import_node_crypto2.randomUUID)(),
4063
5303
  config: {},
4064
5304
  providerRegistry: registry
4065
5305
  });
@@ -4083,7 +5323,7 @@ function workflow(config) {
4083
5323
 
4084
5324
  // src/runtime.ts
4085
5325
  var import_node_events2 = require("events");
4086
- var import_node_crypto2 = require("crypto");
5326
+ var import_node_crypto3 = require("crypto");
4087
5327
  var import_node_async_hooks2 = require("async_hooks");
4088
5328
  var import_node_fs2 = require("fs");
4089
5329
  var import_node_path2 = require("path");
@@ -4106,29 +5346,20 @@ var MemoryStore = class {
4106
5346
  constructor() {
4107
5347
  this.loadPersistedState();
4108
5348
  }
4109
- async saveCheckpoint(executionId, step, data) {
4110
- let steps = this.checkpoints.get(executionId);
4111
- if (!steps) {
4112
- steps = /* @__PURE__ */ new Map();
4113
- this.checkpoints.set(executionId, steps);
5349
+ async saveCheckpoint(executionId, name, data) {
5350
+ let entries = this.checkpoints.get(executionId);
5351
+ if (!entries) {
5352
+ entries = /* @__PURE__ */ new Map();
5353
+ this.checkpoints.set(executionId, entries);
4114
5354
  }
4115
- steps.set(step, structuredClone(data));
5355
+ entries.set(name, structuredClone(data));
4116
5356
  }
4117
- async getCheckpoint(executionId, step) {
4118
- const steps = this.checkpoints.get(executionId);
4119
- if (!steps) return null;
4120
- const data = steps.get(step);
5357
+ async getCheckpoint(executionId, name) {
5358
+ const entries = this.checkpoints.get(executionId);
5359
+ if (!entries) return null;
5360
+ const data = entries.get(name);
4121
5361
  return data !== void 0 ? structuredClone(data) : null;
4122
5362
  }
4123
- async getLatestCheckpoint(executionId) {
4124
- const steps = this.checkpoints.get(executionId);
4125
- if (!steps || steps.size === 0) return null;
4126
- let maxStep = -1;
4127
- for (const step of steps.keys()) {
4128
- if (step > maxStep) maxStep = step;
4129
- }
4130
- return { step: maxStep, data: structuredClone(steps.get(maxStep)) };
4131
- }
4132
5363
  async saveSession(sessionId, history) {
4133
5364
  this.sessions.set(sessionId, structuredClone(history));
4134
5365
  }
@@ -4301,7 +5532,7 @@ function rowToExecutionInfo(row) {
4301
5532
  completedAt: row.completed_at ?? void 0,
4302
5533
  duration: row.duration,
4303
5534
  error: row.error ?? void 0,
4304
- steps: safeJsonParse(row.steps) ?? []
5535
+ events: safeJsonParse(row.events) ?? []
4305
5536
  };
4306
5537
  }
4307
5538
  var SQLiteStore = class {
@@ -4317,15 +5548,80 @@ var SQLiteStore = class {
4317
5548
  }
4318
5549
  this.db = new BetterSqlite3(filePath);
4319
5550
  this.db.pragma("journal_mode = WAL");
5551
+ this.migrate();
4320
5552
  this.initTables();
4321
5553
  }
5554
+ /**
5555
+ * Idempotent schema migration. Tracks version via `PRAGMA user_version`
5556
+ * so reopens skip applied steps. Wrapped in `BEGIN IMMEDIATE` so
5557
+ * concurrent `SQLiteStore` constructors serialize at the file level
5558
+ * and rollback on failure leaves the DB in a clean pre-migration
5559
+ * state.
5560
+ *
5561
+ * Migration history:
5562
+ * - v1 (spec/16): rename `execution_history.steps` → `events`. The
5563
+ * JS-level `ExecutionInfo.steps` was renamed to `.events` in PR 1
5564
+ * commit 1; this brings the on-disk schema in line.
5565
+ * - v2 (named checkpoints): rename `checkpoints.step` (INTEGER) →
5566
+ * `checkpoints.name` (TEXT). The runtime now requires
5567
+ * `ctx.checkpoint(name, fn)` so the per-context numeric counter is
5568
+ * gone — names are caller-supplied stable identifiers. Existing
5569
+ * numeric step values become stringified names ("0", "1", …) to
5570
+ * preserve replay continuity for in-flight executions across the
5571
+ * upgrade boundary.
5572
+ *
5573
+ * Applied BEFORE `initTables()` so the subsequent `CREATE TABLE
5574
+ * IF NOT EXISTS` runs against the post-migration column name. Fresh
5575
+ * installs just create the table with `events` directly and bump
5576
+ * `user_version` to the current version.
5577
+ *
5578
+ * TOCTOU guard (review B-3): the initial `user_version` read is
5579
+ * informational only — a concurrent process can complete the
5580
+ * migration between that read and our `BEGIN IMMEDIATE`. We
5581
+ * re-read `user_version` inside the transaction and short-circuit
5582
+ * if it's already at target, so the non-idempotent half (writes
5583
+ * that went beyond a simple ALTER) never double-applies under a
5584
+ * race. The current v0→v1 step IS idempotent via the column-
5585
+ * presence check, but future migrations may not be, and the
5586
+ * transactional re-read costs nothing.
5587
+ */
5588
+ migrate() {
5589
+ const TARGET_VERSION = 2;
5590
+ const current = this.db.pragma("user_version", { simple: true });
5591
+ if (current >= TARGET_VERSION) return;
5592
+ this.db.exec("BEGIN IMMEDIATE");
5593
+ try {
5594
+ const committed = this.db.pragma("user_version", { simple: true });
5595
+ if (committed >= TARGET_VERSION) {
5596
+ this.db.exec("COMMIT");
5597
+ return;
5598
+ }
5599
+ if (committed < 1) {
5600
+ const cols = this.db.pragma("table_info(execution_history)");
5601
+ if (cols.some((c) => c.name === "steps") && !cols.some((c) => c.name === "events")) {
5602
+ this.db.exec("ALTER TABLE execution_history RENAME COLUMN steps TO events");
5603
+ }
5604
+ }
5605
+ if (committed < 2) {
5606
+ const cols = this.db.pragma("table_info(checkpoints)");
5607
+ if (cols.some((c) => c.name === "step") && !cols.some((c) => c.name === "name")) {
5608
+ this.db.exec("ALTER TABLE checkpoints RENAME COLUMN step TO name");
5609
+ }
5610
+ }
5611
+ this.db.pragma(`user_version = ${TARGET_VERSION}`);
5612
+ this.db.exec("COMMIT");
5613
+ } catch (err) {
5614
+ this.db.exec("ROLLBACK");
5615
+ throw err;
5616
+ }
5617
+ }
4322
5618
  initTables() {
4323
5619
  this.db.exec(`
4324
5620
  CREATE TABLE IF NOT EXISTS checkpoints (
4325
5621
  execution_id TEXT NOT NULL,
4326
- step INTEGER NOT NULL,
5622
+ name TEXT NOT NULL,
4327
5623
  data TEXT NOT NULL,
4328
- PRIMARY KEY (execution_id, step)
5624
+ PRIMARY KEY (execution_id, name)
4329
5625
  );
4330
5626
 
4331
5627
  CREATE TABLE IF NOT EXISTS sessions (
@@ -4374,7 +5670,7 @@ var SQLiteStore = class {
4374
5670
  completed_at INTEGER,
4375
5671
  duration INTEGER NOT NULL DEFAULT 0,
4376
5672
  error TEXT,
4377
- steps TEXT NOT NULL
5673
+ events TEXT NOT NULL
4378
5674
  );
4379
5675
 
4380
5676
  CREATE TABLE IF NOT EXISTS eval_history (
@@ -4389,26 +5685,19 @@ var SQLiteStore = class {
4389
5685
  `);
4390
5686
  }
4391
5687
  // ── Checkpoints ────────────────────────────────────────────────────────
4392
- async saveCheckpoint(executionId, step, data) {
5688
+ async saveCheckpoint(executionId, name, data) {
4393
5689
  const stmt = this.db.prepare(
4394
- "INSERT OR REPLACE INTO checkpoints (execution_id, step, data) VALUES (?, ?, ?)"
5690
+ "INSERT OR REPLACE INTO checkpoints (execution_id, name, data) VALUES (?, ?, ?)"
4395
5691
  );
4396
- stmt.run(executionId, step, JSON.stringify(data));
5692
+ stmt.run(executionId, name, JSON.stringify(data));
4397
5693
  }
4398
- async getCheckpoint(executionId, step) {
5694
+ async getCheckpoint(executionId, name) {
4399
5695
  const stmt = this.db.prepare(
4400
- "SELECT data FROM checkpoints WHERE execution_id = ? AND step = ?"
5696
+ "SELECT data FROM checkpoints WHERE execution_id = ? AND name = ?"
4401
5697
  );
4402
- const row = stmt.get(executionId, step);
5698
+ const row = stmt.get(executionId, name);
4403
5699
  return row ? safeJsonParse(row.data) : null;
4404
5700
  }
4405
- async getLatestCheckpoint(executionId) {
4406
- const stmt = this.db.prepare(
4407
- "SELECT step, data FROM checkpoints WHERE execution_id = ? ORDER BY step DESC LIMIT 1"
4408
- );
4409
- const row = stmt.get(executionId);
4410
- return row ? { step: row.step, data: safeJsonParse(row.data) } : null;
4411
- }
4412
5701
  // ── Sessions ────────────────────────────────────────────────────────────
4413
5702
  async saveSession(sessionId, history) {
4414
5703
  const stmt = this.db.prepare(
@@ -4502,7 +5791,7 @@ var SQLiteStore = class {
4502
5791
  // ── Execution History ────────────────────────────────────────────────────
4503
5792
  async saveExecution(execution) {
4504
5793
  this.db.prepare(
4505
- "INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, steps) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
5794
+ "INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, events) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
4506
5795
  ).run(
4507
5796
  execution.executionId,
4508
5797
  execution.workflow,
@@ -4512,7 +5801,7 @@ var SQLiteStore = class {
4512
5801
  execution.completedAt ?? null,
4513
5802
  execution.duration,
4514
5803
  execution.error ?? null,
4515
- JSON.stringify(execution.steps)
5804
+ JSON.stringify(execution.events)
4516
5805
  );
4517
5806
  }
4518
5807
  async getExecution(executionId) {
@@ -4686,7 +5975,8 @@ var Session = class _Session {
4686
5975
  }
4687
5976
  };
4688
5977
  axlStream.on("done", (event) => {
4689
- updateHistory(event.data).catch((err) => {
5978
+ const data = event.data;
5979
+ updateHistory(data.result).catch((err) => {
4690
5980
  this.runtime.emit("error", {
4691
5981
  type: "session_history_save_failed",
4692
5982
  sessionId: this.sessionId,
@@ -4728,9 +6018,88 @@ var Session = class _Session {
4728
6018
  // src/stream.ts
4729
6019
  var import_node_stream = require("stream");
4730
6020
  var import_node_events = require("events");
6021
+
6022
+ // src/types.ts
6023
+ var AXL_EVENT_TYPES = [
6024
+ // Workflow lifecycle
6025
+ "workflow_start",
6026
+ "workflow_end",
6027
+ // Ask boundary
6028
+ "ask_start",
6029
+ "ask_end",
6030
+ // Agent turn lifecycle
6031
+ "agent_call_start",
6032
+ "agent_call_end",
6033
+ // Content delivery (stream-only)
6034
+ "token",
6035
+ // Tool invocation lifecycle
6036
+ "tool_call_start",
6037
+ "tool_call_end",
6038
+ // Single-point tool events
6039
+ "tool_approval",
6040
+ "tool_denied",
6041
+ // Delegation
6042
+ "delegate",
6043
+ // Handoff (spans two asks; not AskScoped).
6044
+ // `handoff_start` emits before the target ask begins; always fired.
6045
+ // `handoff_return` emits when control returns to source; roundtrip only
6046
+ // (oneway handoffs have no return trip — the target's `ask_end` IS the
6047
+ // end of the chain).
6048
+ "handoff_start",
6049
+ "handoff_return",
6050
+ // Pipeline (retry/validation lifecycle) — added in PR 2; reserved here.
6051
+ "pipeline",
6052
+ // Progressive structured output — added in PR 2; reserved here.
6053
+ "partial_object",
6054
+ // Verification
6055
+ "verify",
6056
+ // Observability
6057
+ "log",
6058
+ "memory_remember",
6059
+ "memory_recall",
6060
+ "memory_forget",
6061
+ // Durable execution checkpoints (`ctx.checkpoint`)
6062
+ "checkpoint_save",
6063
+ "checkpoint_replay",
6064
+ // Human-in-the-loop (`ctx.awaitHuman`)
6065
+ "await_human",
6066
+ "await_human_resolved",
6067
+ // Legacy gate events — emitted by current code; collapsed into `pipeline`
6068
+ // in PR 2 (spec/16-streaming-wire-reliability §4.2).
6069
+ "guardrail",
6070
+ "schema_check",
6071
+ "validate",
6072
+ // Terminal workflow markers
6073
+ "done",
6074
+ "error"
6075
+ ];
6076
+
6077
+ // src/stream.ts
4731
6078
  var AxlStream = class _AxlStream extends import_node_stream.Readable {
4732
6079
  bus = new import_node_events.EventEmitter();
4733
- tokens = [];
6080
+ /**
6081
+ * Per-ask token buffers split into "in-progress" and "committed"
6082
+ * halves so `fullText` only includes tokens from attempts that
6083
+ * actually won. Scoping is per-`askId` so concurrent root-level asks
6084
+ * (`ctx.parallel`, `ctx.spawn`, `ctx.race`, `ctx.map`) don't
6085
+ * interleave each other's tokens — each branch's chunks stay
6086
+ * contiguous, and a `pipeline(failed)` on one branch only discards
6087
+ * THAT branch's in-progress buffer (previously: shared buffer caused
6088
+ * a failure on one branch to discard a peer's in-flight successful
6089
+ * tokens). Insertion order in the Maps reflects which ask emitted
6090
+ * its first token first, which is what `fullText` joins on.
6091
+ *
6092
+ * On `pipeline(status: 'committed')` for an ask, that ask's
6093
+ * in-progress entry flushes to its committed entry. On
6094
+ * `pipeline(status: 'failed')` or `ask_end({ok:false})` for an ask,
6095
+ * that ask's in-progress entry is discarded. Spec/16 §4.3.
6096
+ *
6097
+ * Tokens emitted outside any ask (synthesized test fixtures with no
6098
+ * `askId`) fall back to an empty-string sentinel key — preserves the
6099
+ * single-buffer behavior for that legacy case.
6100
+ */
6101
+ attemptByAsk = /* @__PURE__ */ new Map();
6102
+ committedByAsk = /* @__PURE__ */ new Map();
4734
6103
  result = void 0;
4735
6104
  finished = false;
4736
6105
  promise;
@@ -4752,18 +6121,10 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
4752
6121
  this.promise.catch(() => {
4753
6122
  });
4754
6123
  }
4755
- static STREAM_EVENTS = /* @__PURE__ */ new Set([
4756
- "token",
4757
- "tool_call",
4758
- "tool_result",
4759
- "tool_approval",
4760
- "agent_start",
4761
- "agent_end",
4762
- "handoff",
4763
- "step",
4764
- "done",
4765
- "error"
4766
- ]);
6124
+ /** Wire-format event names callers can subscribe to via `.on(name, fn)`.
6125
+ * Derived from the canonical `AXL_EVENT_TYPES` tuple — adding a new
6126
+ * variant in `types.ts` automatically extends the subscribable set. */
6127
+ static STREAM_EVENTS = new Set(AXL_EVENT_TYPES);
4767
6128
  on(event, handler) {
4768
6129
  if (_AxlStream.STREAM_EVENTS.has(event)) {
4769
6130
  this.bus.on(event, handler);
@@ -4813,21 +6174,83 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
4813
6174
  while (true) {
4814
6175
  const { value, done } = await iter.next();
4815
6176
  if (done) return { value: void 0, done: true };
4816
- if (value.type === "token") return { value: value.data, done: false };
6177
+ if (value.type === "token" && isRootLevel(value)) {
6178
+ return { value: value.data, done: false };
6179
+ }
6180
+ }
6181
+ }
6182
+ };
6183
+ }
6184
+ };
6185
+ }
6186
+ /**
6187
+ * Iterator over `{ askId, text }` pairs — one emission per token chunk,
6188
+ * tagged with the ask frame that produced it. Complements `.text`
6189
+ * (root-only stream): consumers building a split-pane UI that shows
6190
+ * each sub-agent's output in its own lane can group by `askId`
6191
+ * without iterating the raw stream and hand-filtering on `event.type`.
6192
+ *
6193
+ * Nested and root tokens both flow through. The `agent` field names
6194
+ * the producing agent for UI labelling; it's undefined when the token
6195
+ * was emitted outside any ask (rare — synthesized test fixtures).
6196
+ */
6197
+ get textByAsk() {
6198
+ const self = this;
6199
+ return {
6200
+ [Symbol.asyncIterator]() {
6201
+ const iter = self[Symbol.asyncIterator]();
6202
+ return {
6203
+ async next() {
6204
+ while (true) {
6205
+ const { value, done } = await iter.next();
6206
+ if (done) {
6207
+ return {
6208
+ value: void 0,
6209
+ done: true
6210
+ };
6211
+ }
6212
+ if (value.type === "token") {
6213
+ return {
6214
+ value: { askId: value.askId, agent: value.agent, text: value.data },
6215
+ done: false
6216
+ };
6217
+ }
4817
6218
  }
4818
6219
  }
4819
6220
  };
4820
6221
  }
4821
6222
  };
4822
6223
  }
4823
- get steps() {
4824
- const stepTypes = /* @__PURE__ */ new Set([
4825
- "agent_start",
4826
- "agent_end",
4827
- "tool_call",
4828
- "tool_result",
6224
+ /**
6225
+ * Iterator over structural lifecycle events only — skips per-token chatter
6226
+ * and progressive partial_object emissions. Useful for waterfall UIs and
6227
+ * any consumer that wants the "what happened" timeline without per-chunk
6228
+ * noise. Renamed from `.steps` in the unified-event-model migration
6229
+ * because these are events, not pipeline steps.
6230
+ */
6231
+ get lifecycle() {
6232
+ const lifecycleTypes = /* @__PURE__ */ new Set([
6233
+ "ask_start",
6234
+ "ask_end",
6235
+ "agent_call_start",
6236
+ "agent_call_end",
6237
+ "tool_call_start",
6238
+ "tool_call_end",
4829
6239
  "tool_approval",
4830
- "handoff"
6240
+ "tool_denied",
6241
+ "handoff_start",
6242
+ "handoff_return",
6243
+ "delegate",
6244
+ "pipeline",
6245
+ "verify",
6246
+ "workflow_start",
6247
+ "workflow_end",
6248
+ // Durable-execution checkpoints — structural points in the timeline.
6249
+ "checkpoint_save",
6250
+ "checkpoint_replay",
6251
+ // Human-in-the-loop — pause/resume are major timeline landmarks.
6252
+ "await_human",
6253
+ "await_human_resolved"
4831
6254
  ]);
4832
6255
  const self = this;
4833
6256
  return {
@@ -4838,7 +6261,7 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
4838
6261
  while (true) {
4839
6262
  const { value, done } = await iter.next();
4840
6263
  if (done) return { value: void 0, done: true };
4841
- if (stepTypes.has(value.type)) return { value, done: false };
6264
+ if (lifecycleTypes.has(value.type)) return { value, done: false };
4842
6265
  }
4843
6266
  }
4844
6267
  };
@@ -4858,10 +6281,34 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
4858
6281
  });
4859
6282
  return destination;
4860
6283
  }
4861
- /** Push a stream event. Called by the runtime. */
6284
+ /** Push an event onto the stream. Called by the runtime. */
4862
6285
  _push(event) {
4863
6286
  if (this.finished) return;
4864
- if (event.type === "token") this.tokens.push(event.data);
6287
+ if (event.type === "token" && isRootLevel(event)) {
6288
+ const key = event.askId ?? "";
6289
+ const attempt = this.attemptByAsk.get(key);
6290
+ if (attempt) {
6291
+ attempt.push(event.data);
6292
+ } else {
6293
+ this.attemptByAsk.set(key, [event.data]);
6294
+ }
6295
+ }
6296
+ if (event.type === "pipeline" && isRootLevel(event)) {
6297
+ const key = event.askId ?? "";
6298
+ if (event.status === "committed") {
6299
+ const attempt = this.attemptByAsk.get(key);
6300
+ if (attempt && attempt.length > 0) {
6301
+ const prev = this.committedByAsk.get(key) ?? "";
6302
+ this.committedByAsk.set(key, prev + attempt.join(""));
6303
+ this.attemptByAsk.set(key, []);
6304
+ }
6305
+ } else if (event.status === "failed") {
6306
+ this.attemptByAsk.set(key, []);
6307
+ }
6308
+ }
6309
+ if (event.type === "ask_end" && isRootLevel(event) && !event.outcome.ok) {
6310
+ this.attemptByAsk.set(event.askId ?? "", []);
6311
+ }
4865
6312
  this.bus.emit(event.type, event);
4866
6313
  this.push(event);
4867
6314
  const waiter = this.waiters.shift();
@@ -4871,12 +6318,25 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
4871
6318
  this.eventQueue.push(event);
4872
6319
  }
4873
6320
  }
4874
- /** Signal successful completion. */
4875
- _done(result) {
6321
+ /**
6322
+ * Signal successful completion.
6323
+ *
6324
+ * `executionId` is required — the runtime must allocate it before
6325
+ * calling `stream()` so terminal events always carry a real id.
6326
+ * Previously the default-empty parameter surfaced blank executionIds
6327
+ * on error paths that threw before `execInfo` was assigned (review S4).
6328
+ */
6329
+ _done(result, executionId) {
4876
6330
  if (this.finished) return;
4877
6331
  this.finished = true;
4878
6332
  this.result = result;
4879
- const doneEvent = { type: "done", data: result };
6333
+ const doneEvent = {
6334
+ type: "done",
6335
+ executionId,
6336
+ step: Number.MAX_SAFE_INTEGER,
6337
+ timestamp: Date.now(),
6338
+ data: { result }
6339
+ };
4880
6340
  this.bus.emit("done", doneEvent);
4881
6341
  this.push(doneEvent);
4882
6342
  this.push(null);
@@ -4892,11 +6352,19 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
4892
6352
  this.waiters.length = 0;
4893
6353
  this.bus.emit("__resolve", result);
4894
6354
  }
4895
- /** Signal an error. */
4896
- _error(error) {
6355
+ /** Signal an error. `executionId` is required for the same reason as
6356
+ * `_done`: terminal events must carry a real id even when the
6357
+ * failure happens before any real trace event fires (review S4). */
6358
+ _error(error, executionId) {
4897
6359
  if (this.finished) return;
4898
6360
  this.finished = true;
4899
- const errorEvent = { type: "error", message: error.message };
6361
+ const errorEvent = {
6362
+ type: "error",
6363
+ executionId,
6364
+ step: Number.MAX_SAFE_INTEGER,
6365
+ timestamp: Date.now(),
6366
+ data: { message: error.message, name: error.name }
6367
+ };
4900
6368
  this.bus.emit("error", errorEvent);
4901
6369
  this.push(errorEvent);
4902
6370
  this.push(null);
@@ -4912,8 +6380,31 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
4912
6380
  this.waiters.length = 0;
4913
6381
  this.bus.emit("__reject", error);
4914
6382
  }
6383
+ /** Concatenated root-only text from committed attempts plus the
6384
+ * current in-flight attempt(s). Retried (gate-rejected) attempts
6385
+ * are excluded — see spec/16 §4.3. Reading mid-attempt returns the
6386
+ * in-progress text; reading after `pipeline(committed)` (which
6387
+ * fires before `done`) returns the canonical winning text.
6388
+ *
6389
+ * With concurrent root-level asks (`ctx.parallel`, `ctx.spawn`,
6390
+ * `ctx.race`, `ctx.map`), each branch's tokens are scoped per-`askId`
6391
+ * and emitted contiguously in the order each ask first started
6392
+ * emitting tokens. A `pipeline(failed)` or `ask_end({ok:false})` on
6393
+ * one branch only discards THAT branch's in-progress buffer —
6394
+ * sibling branches are unaffected. For UIs that want each sub-agent
6395
+ * in its own lane, prefer `textByAsk`. */
4915
6396
  get fullText() {
4916
- return this.tokens.join("");
6397
+ let out = "";
6398
+ for (const [key, committed] of this.committedByAsk) {
6399
+ out += committed;
6400
+ const attempt = this.attemptByAsk.get(key);
6401
+ if (attempt && attempt.length > 0) out += attempt.join("");
6402
+ }
6403
+ for (const [key, attempt] of this.attemptByAsk) {
6404
+ if (this.committedByAsk.has(key)) continue;
6405
+ if (attempt.length > 0) out += attempt.join("");
6406
+ }
6407
+ return out;
4917
6408
  }
4918
6409
  };
4919
6410
 
@@ -5276,6 +6767,18 @@ var McpManager = class {
5276
6767
  };
5277
6768
 
5278
6769
  // src/memory/manager.ts
6770
+ function assertEmbedResult(result) {
6771
+ if (Array.isArray(result)) {
6772
+ throw new Error(
6773
+ "Embedder.embed() returned a bare number[][] (legacy shape). As of @axlsdk/axl 0.15, Embedder.embed() must return { vectors: number[][]; usage?: { tokens?, cost?, model? } }. Wrap your existing return value: `return { vectors: await old() }`."
6774
+ );
6775
+ }
6776
+ if (!result || typeof result !== "object" || !Array.isArray(result.vectors)) {
6777
+ throw new Error(
6778
+ `Embedder.embed() must return { vectors: number[][]; usage?: EmbedUsage }. Got ${result === null ? "null" : typeof result}.`
6779
+ );
6780
+ }
6781
+ }
5279
6782
  var MemoryManager = class {
5280
6783
  vectorStore;
5281
6784
  embedder;
@@ -5290,8 +6793,11 @@ var MemoryManager = class {
5290
6793
  * Store a key-value pair in memory.
5291
6794
  * If a vector store and embedder are configured and embed is true,
5292
6795
  * also embeds the value for semantic search.
6796
+ *
6797
+ * Returns `{ usage }` when embedding happened, carrying the embedder's
6798
+ * cost/token reporting out to the caller for cost attribution.
5293
6799
  */
5294
- async remember(key, value, stateStore, sessionId, options) {
6800
+ async remember(key, value, stateStore, sessionId, options, signal) {
5295
6801
  const scope = options?.scope ?? "session";
5296
6802
  if (scope === "session" && !sessionId) {
5297
6803
  throw new Error(
@@ -5310,23 +6816,44 @@ var MemoryManager = class {
5310
6816
  }
5311
6817
  if (options?.embed === true && this.vectorStore && this.embedder) {
5312
6818
  const text = typeof value === "string" ? value : JSON.stringify(value);
5313
- const [embedding] = await this.embedder.embed([text]);
5314
- await this.vectorStore.upsert([
5315
- {
5316
- id: `${storeScope}:${key}`,
5317
- content: text,
5318
- embedding,
5319
- metadata: { key, scope, ...options?.metadata }
6819
+ const raw = await this.embedder.embed([text], signal);
6820
+ assertEmbedResult(raw);
6821
+ const { vectors, usage } = raw;
6822
+ try {
6823
+ await this.vectorStore.upsert([
6824
+ {
6825
+ id: `${storeScope}:${key}`,
6826
+ content: text,
6827
+ embedding: vectors[0],
6828
+ metadata: { key, scope, ...options?.metadata }
6829
+ }
6830
+ ]);
6831
+ } catch (err) {
6832
+ if (usage && typeof err === "object" && err !== null) {
6833
+ Object.defineProperty(err, "axlEmbedUsage", {
6834
+ value: usage,
6835
+ enumerable: false,
6836
+ writable: true,
6837
+ configurable: true
6838
+ });
5320
6839
  }
5321
- ]);
6840
+ throw err;
6841
+ }
6842
+ return usage ? { usage } : {};
5322
6843
  }
6844
+ return {};
5323
6845
  }
5324
6846
  /**
5325
6847
  * Recall a value from memory by key, or perform semantic search if query is provided.
6848
+ *
6849
+ * Always returns `{ data }`; semantic recalls additionally include
6850
+ * `usage` from the embedder (when the embedder reported it).
5326
6851
  */
5327
- async recall(key, stateStore, sessionId, options) {
6852
+ async recall(key, stateStore, sessionId, options, signal) {
5328
6853
  if (options?.query && this.vectorStore && this.embedder) {
5329
- const [embedding] = await this.embedder.embed([options.query]);
6854
+ const raw = await this.embedder.embed([options.query], signal);
6855
+ assertEmbedResult(raw);
6856
+ const { vectors, usage } = raw;
5330
6857
  const topK = options?.topK ?? 5;
5331
6858
  const searchScope = options?.scope ?? "session";
5332
6859
  if (searchScope === "session" && !sessionId) {
@@ -5334,13 +6861,14 @@ var MemoryManager = class {
5334
6861
  'sessionId is required for session-scoped memory. Use { scope: "global" } or provide a sessionId in metadata.'
5335
6862
  );
5336
6863
  }
5337
- const rawResults = await this.vectorStore.search(embedding, topK * 3);
6864
+ const rawResults = await this.vectorStore.search(vectors[0], topK * 3);
5338
6865
  const filtered = rawResults.filter((r) => {
5339
6866
  if (r.id.startsWith("global:")) return true;
5340
6867
  if (searchScope === "session") return r.id.startsWith(`session:${sessionId}:`);
5341
6868
  return true;
5342
6869
  });
5343
- return filtered.slice(0, topK);
6870
+ const data = filtered.slice(0, topK);
6871
+ return usage ? { data, usage } : { data };
5344
6872
  }
5345
6873
  const scope = options?.scope ?? "session";
5346
6874
  if (scope === "session" && !sessionId) {
@@ -5350,10 +6878,10 @@ var MemoryManager = class {
5350
6878
  }
5351
6879
  const storeScope = scope === "session" ? `session:${sessionId}` : "global";
5352
6880
  if (stateStore.getMemory) {
5353
- return stateStore.getMemory(storeScope, key);
6881
+ return { data: await stateStore.getMemory(storeScope, key) };
5354
6882
  }
5355
6883
  const storeKey = `memory:${storeScope}:${key}`;
5356
- return stateStore.getSessionMeta(storeKey, "value");
6884
+ return { data: await stateStore.getSessionMeta(storeKey, "value") };
5357
6885
  }
5358
6886
  /** Delete a memory entry. If a vector store is configured, also removes the embedding. */
5359
6887
  async forget(key, stateStore, sessionId, options) {
@@ -5423,6 +6951,32 @@ function hashInput(input) {
5423
6951
  }
5424
6952
  return (hash >>> 0).toString(16);
5425
6953
  }
6954
+ var DEFAULT_MAX_EVENTS_PER_EXECUTION = 5e4;
6955
+ function isAbortError(err) {
6956
+ return typeof err === "object" && err !== null && err.name === "AbortError";
6957
+ }
6958
+ function pushEventBounded(execInfo, event, cap) {
6959
+ if (event.type === "token" || event.type === "partial_object") return;
6960
+ if (cap === Infinity || execInfo.events.length < cap) {
6961
+ execInfo.events.push(event);
6962
+ return;
6963
+ }
6964
+ const last = execInfo.events[execInfo.events.length - 1];
6965
+ const alreadyTruncated = last?.type === "log" && typeof last.data === "object" && last.data !== null && last.data.event === "events_truncated";
6966
+ if (!alreadyTruncated) {
6967
+ execInfo.events[execInfo.events.length - 1] = {
6968
+ type: "log",
6969
+ executionId: event.executionId,
6970
+ step: event.step,
6971
+ timestamp: Date.now(),
6972
+ data: {
6973
+ event: "events_truncated",
6974
+ cap,
6975
+ message: `ExecutionInfo.events truncated at ${cap} entries. Subsequent events are still delivered to runtime.on('trace') and the WS broadcast \u2014 only the in-memory array is bounded. Raise via config.state.maxEventsPerExecution.`
6976
+ }
6977
+ };
6978
+ }
6979
+ }
5426
6980
  var costScopeStorage = new import_node_async_hooks2.AsyncLocalStorage();
5427
6981
  var AxlRuntime = class extends import_node_events2.EventEmitter {
5428
6982
  config;
@@ -5442,11 +6996,27 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5442
6996
  historicalExecutionsLoadPromise = null;
5443
6997
  evalHistory = [];
5444
6998
  evalHistoryLoadPromise = null;
6999
+ /** Resolved cap on `ExecutionInfo.events` per execution. Cached on
7000
+ * the instance so per-event onTrace handlers don't reach into
7001
+ * `this.config.state` on every emission. */
7002
+ maxEventsPerExecution;
5445
7003
  constructor(config) {
5446
7004
  super();
5447
7005
  this.config = resolveConfig(config ?? {});
5448
7006
  this.providerRegistry = new ProviderRegistry();
5449
7007
  this.stateStore = this.createStateStore();
7008
+ const requested = this.config.state?.maxEventsPerExecution;
7009
+ if (requested === void 0) {
7010
+ this.maxEventsPerExecution = DEFAULT_MAX_EVENTS_PER_EXECUTION;
7011
+ } else if (requested === Infinity) {
7012
+ this.maxEventsPerExecution = Infinity;
7013
+ } else if (typeof requested !== "number" || !Number.isFinite(requested) || !Number.isInteger(requested) || requested < 1) {
7014
+ throw new RangeError(
7015
+ `config.state.maxEventsPerExecution must be a positive integer or Infinity; got ${requested}`
7016
+ );
7017
+ } else {
7018
+ this.maxEventsPerExecution = requested;
7019
+ }
5450
7020
  if (this.config.memory) {
5451
7021
  this.memoryManager = new MemoryManager({
5452
7022
  vectorStore: this.config.memory.vectorStore,
@@ -5454,6 +7024,22 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5454
7024
  });
5455
7025
  }
5456
7026
  }
7027
+ /**
7028
+ * Whether `config.trace.redact` is enabled on this runtime. A narrow
7029
+ * boolean getter is preferred over exposing the full config because:
7030
+ * (a) `Readonly<AxlConfig>` is shallow so the config would be mutable
7031
+ * at runtime through sub-objects, subverting any compliance guarantee;
7032
+ * (b) observability consumers like Studio only need the boolean, not
7033
+ * the whole config tree; (c) if future observability config needs to
7034
+ * expand, each new flag gets its own narrow getter.
7035
+ *
7036
+ * Studio's server-side redaction helpers (executions, memory, sessions,
7037
+ * evals, decisions, tools, playground, workflows) all route through
7038
+ * this accessor to decide whether to scrub response payloads.
7039
+ */
7040
+ isRedactEnabled() {
7041
+ return this.config.trace?.redact === true;
7042
+ }
5457
7043
  /**
5458
7044
  * Initialize MCP servers configured in the config.
5459
7045
  * Call this before executing workflows that use MCP tools.
@@ -5486,6 +7072,71 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5486
7072
  return new MemoryStore();
5487
7073
  }
5488
7074
  }
7075
+ /**
7076
+ * Run a workflow body inside an ALREADY-CONSTRUCTED `WorkflowContext`
7077
+ * with full lifecycle invariants: emit `workflow_start` → run handler
7078
+ * → parse output → emit `workflow_end` → delete checkpoints → persist.
7079
+ *
7080
+ * Shared between `execute()` and `stream()`. Centralizes the
7081
+ * "start iff end" pairing invariant so a future emit path
7082
+ * (e.g., `resumeExecution`) can inherit correct behavior by routing
7083
+ * through this helper. Also factors out the AbortError detection.
7084
+ *
7085
+ * Side effects on `execInfo`: sets `status` / `completedAt` /
7086
+ * `duration` / `result` (or `error`) before emitting `workflow_end`.
7087
+ * On throw: emits `workflow_end({status: 'failed', aborted?})` and
7088
+ * persists before rethrowing. `abortControllers.delete` runs in
7089
+ * `finally` regardless of outcome.
7090
+ *
7091
+ * Caller responsibilities:
7092
+ * - Allocate `executionId` and register it in `abortControllers` map
7093
+ * BEFORE calling this helper (so early-throw paths still have a
7094
+ * correlation id available for terminal events).
7095
+ * - Construct `execInfo` and `ctx` before calling.
7096
+ * - Pass `span` if the call site is inside a tracer span — this
7097
+ * helper sets `axl.workflow.cost` / `.duration` attributes on
7098
+ * success.
7099
+ */
7100
+ async runWorkflowBody(args) {
7101
+ const { workflow: workflow2, ctx, execInfo, validated, span } = args;
7102
+ try {
7103
+ ctx._emitWorkflowStart(validated);
7104
+ const raw = await workflow2.handler(ctx);
7105
+ const result = workflow2.outputSchema ? workflow2.outputSchema.parse(raw) : raw;
7106
+ execInfo.status = "completed";
7107
+ execInfo.completedAt = Date.now();
7108
+ execInfo.duration = execInfo.completedAt - execInfo.startedAt;
7109
+ execInfo.result = result;
7110
+ ctx._emitWorkflowEnd({
7111
+ status: "completed",
7112
+ duration: execInfo.duration,
7113
+ result
7114
+ });
7115
+ if (this.stateStore.deleteCheckpoints) {
7116
+ await this.stateStore.deleteCheckpoints(execInfo.executionId);
7117
+ }
7118
+ span?.setAttribute("axl.workflow.cost", execInfo.totalCost);
7119
+ span?.setAttribute("axl.workflow.duration", execInfo.duration);
7120
+ this.persistExecution(execInfo);
7121
+ return result;
7122
+ } catch (err) {
7123
+ const aborted = isAbortError(err);
7124
+ execInfo.status = "failed";
7125
+ execInfo.completedAt = Date.now();
7126
+ execInfo.duration = execInfo.completedAt - execInfo.startedAt;
7127
+ execInfo.error = err instanceof Error ? err.message : String(err);
7128
+ ctx._emitWorkflowEnd({
7129
+ status: "failed",
7130
+ duration: execInfo.duration,
7131
+ error: execInfo.error,
7132
+ ...aborted ? { aborted: true } : {}
7133
+ });
7134
+ this.persistExecution(execInfo);
7135
+ throw err;
7136
+ } finally {
7137
+ this.abortControllers.delete(execInfo.executionId);
7138
+ }
7139
+ }
5489
7140
  /**
5490
7141
  * Persist a completed/failed execution to the state store (fire-and-forget)
5491
7142
  * and move it from the active executions map to the historical cache.
@@ -5501,7 +7152,6 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5501
7152
  this.executions.delete(id);
5502
7153
  }
5503
7154
  /** Register a workflow with the runtime. */
5504
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5505
7155
  register(workflow2) {
5506
7156
  this.workflows.set(workflow2.name, workflow2);
5507
7157
  }
@@ -5603,10 +7253,19 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5603
7253
  metadata: result2.metadata ?? metadata
5604
7254
  };
5605
7255
  };
5606
- result = await runEvalFn(entry.config, wrappedExecuteFn, this);
7256
+ result = await runEvalFn(entry.config, wrappedExecuteFn, this, {
7257
+ onProgress: options?.onProgress,
7258
+ signal: options?.signal,
7259
+ captureTraces: options?.captureTraces
7260
+ });
5607
7261
  } else {
5608
7262
  result = await this.eval(
5609
- entry.config
7263
+ entry.config,
7264
+ {
7265
+ onProgress: options?.onProgress,
7266
+ signal: options?.signal,
7267
+ captureTraces: options?.captureTraces
7268
+ }
5610
7269
  );
5611
7270
  }
5612
7271
  const resultObj = result;
@@ -5618,7 +7277,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5618
7277
  }
5619
7278
  try {
5620
7279
  await this.saveEvalResult({
5621
- id: resultObj.id ?? (0, import_node_crypto2.randomUUID)(),
7280
+ id: resultObj.id ?? (0, import_node_crypto3.randomUUID)(),
5622
7281
  eval: name,
5623
7282
  timestamp: Date.now(),
5624
7283
  data: structuredClone(result)
@@ -5655,7 +7314,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5655
7314
  * and automatically emits trace events and tracks cost.
5656
7315
  */
5657
7316
  createContext(options) {
5658
- const executionId = (0, import_node_crypto2.randomUUID)();
7317
+ const executionId = (0, import_node_crypto3.randomUUID)();
5659
7318
  const budgetLimit = options?.budget ? parseCost(options.budget) : Infinity;
5660
7319
  this.registerWithCostScope(executionId);
5661
7320
  return new WorkflowContext({
@@ -5671,10 +7330,12 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5671
7330
  sessionHistory: options?.sessionHistory,
5672
7331
  signal: options?.signal,
5673
7332
  onToken: options?.onToken,
7333
+ onToolCall: options?.onToolCall,
7334
+ onAgentStart: options?.onAgentStart,
5674
7335
  awaitHumanHandler: options?.awaitHumanHandler,
5675
7336
  onTrace: (event) => {
5676
7337
  this.emit("trace", event);
5677
- this.outputTraceEvent(event);
7338
+ this.outputAxlEvent(event);
5678
7339
  },
5679
7340
  budgetContext: {
5680
7341
  totalCost: 0,
@@ -5701,7 +7362,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5701
7362
  );
5702
7363
  }
5703
7364
  const validated = workflow2.inputSchema.parse(input);
5704
- const executionId = (0, import_node_crypto2.randomUUID)();
7365
+ const executionId = (0, import_node_crypto3.randomUUID)();
5705
7366
  const controller = new AbortController();
5706
7367
  this.abortControllers.set(executionId, controller);
5707
7368
  this.registerWithCostScope(executionId);
@@ -5709,7 +7370,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5709
7370
  executionId,
5710
7371
  workflow: name,
5711
7372
  status: "running",
5712
- steps: [],
7373
+ events: [],
5713
7374
  totalCost: 0,
5714
7375
  startedAt: Date.now(),
5715
7376
  duration: 0
@@ -5725,25 +7386,41 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5725
7386
  sessionHistory,
5726
7387
  signal: controller.signal,
5727
7388
  onTrace: (event) => {
5728
- execInfo.steps.push(event);
5729
- if (event.cost) execInfo.totalCost += event.cost;
7389
+ pushEventBounded(execInfo, event, this.maxEventsPerExecution);
7390
+ execInfo.totalCost += eventCostContribution(event);
5730
7391
  this.emit("trace", event);
5731
- this.outputTraceEvent(event);
5732
- if (event.type === "handoff") {
7392
+ this.outputAxlEvent(event);
7393
+ if (event.type === "handoff_start") {
5733
7394
  const sessionId = options?.metadata?.sessionId;
5734
7395
  if (sessionId) {
5735
- const data = event.data;
5736
7396
  this.appendHandoffRecord(sessionId, {
5737
- source: event.agent ?? "",
5738
- target: data?.target ?? "",
5739
- mode: data?.mode ?? "oneway",
7397
+ source: event.data.source,
7398
+ target: event.data.target,
7399
+ mode: event.data.mode,
5740
7400
  timestamp: event.timestamp,
5741
- duration: data?.duration ?? void 0
7401
+ toAskId: event.toAskId
7402
+ });
7403
+ }
7404
+ } else if (event.type === "handoff_return") {
7405
+ const sessionId = options?.metadata?.sessionId;
7406
+ if (sessionId) {
7407
+ this.updateHandoffDuration(sessionId, {
7408
+ toAskId: event.toAskId,
7409
+ duration: event.data.duration
7410
+ });
7411
+ }
7412
+ } else if (event.type === "ask_end") {
7413
+ const sessionId = options?.metadata?.sessionId;
7414
+ if (sessionId && event.askId && typeof event.duration === "number") {
7415
+ this.updateHandoffDuration(sessionId, {
7416
+ toAskId: event.askId,
7417
+ duration: event.duration
5742
7418
  });
5743
7419
  }
5744
7420
  }
5745
7421
  },
5746
7422
  pendingDecisions: this.pendingDecisionResolvers,
7423
+ awaitHumanHandler: options?.awaitHumanHandler,
5747
7424
  stateStore: this.stateStore,
5748
7425
  workflowName: name,
5749
7426
  mcpManager: this.mcpManager,
@@ -5757,7 +7434,6 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5757
7434
  policy: "finish_and_stop"
5758
7435
  }
5759
7436
  });
5760
- ctx.log("workflow_start", { workflow: name, executionId });
5761
7437
  return this.spanManager.withSpanAsync(
5762
7438
  "axl.workflow.execute",
5763
7439
  {
@@ -5765,39 +7441,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5765
7441
  "axl.execution.id": executionId,
5766
7442
  "axl.workflow.input_hash": hashInput(validated)
5767
7443
  },
5768
- async (span) => {
5769
- try {
5770
- const result = await workflow2.handler(ctx);
5771
- const output = workflow2.outputSchema ? workflow2.outputSchema.parse(result) : result;
5772
- execInfo.status = "completed";
5773
- execInfo.completedAt = Date.now();
5774
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5775
- execInfo.result = output;
5776
- ctx.log("workflow_end", {
5777
- workflow: name,
5778
- status: "completed",
5779
- duration: execInfo.duration,
5780
- cost: execInfo.totalCost
5781
- });
5782
- if (this.stateStore.deleteCheckpoints) {
5783
- await this.stateStore.deleteCheckpoints(executionId);
5784
- }
5785
- span.setAttribute("axl.workflow.cost", execInfo.totalCost);
5786
- span.setAttribute("axl.workflow.duration", execInfo.duration);
5787
- this.persistExecution(execInfo);
5788
- return output;
5789
- } catch (err) {
5790
- execInfo.status = "failed";
5791
- execInfo.completedAt = Date.now();
5792
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5793
- execInfo.error = err instanceof Error ? err.message : String(err);
5794
- ctx.log("workflow_end", { workflow: name, status: "failed", error: execInfo.error });
5795
- this.persistExecution(execInfo);
5796
- throw err;
5797
- } finally {
5798
- this.abortControllers.delete(executionId);
5799
- }
5800
- }
7444
+ (span) => this.runWorkflowBody({
7445
+ workflow: workflow2,
7446
+ ctx,
7447
+ execInfo,
7448
+ validated,
7449
+ span
7450
+ })
5801
7451
  );
5802
7452
  }
5803
7453
  /** Execute a workflow and return a stream. */
@@ -5805,21 +7455,20 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5805
7455
  const axlStream = new AxlStream();
5806
7456
  const controller = new AbortController();
5807
7457
  axlStream.on("close", () => controller.abort());
7458
+ const executionId = (0, import_node_crypto3.randomUUID)();
7459
+ this.abortControllers.set(executionId, controller);
5808
7460
  let execInfo;
5809
- let ctx;
5810
7461
  const run = async () => {
5811
7462
  const workflow2 = this.workflows.get(name);
5812
7463
  if (!workflow2) throw new Error(`Workflow "${name}" not registered`);
5813
7464
  const validated = workflow2.inputSchema.parse(input);
5814
- const executionId = (0, import_node_crypto2.randomUUID)();
5815
- this.abortControllers.set(executionId, controller);
5816
7465
  const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
5817
7466
  this.registerWithCostScope(executionId);
5818
7467
  execInfo = {
5819
7468
  executionId,
5820
7469
  workflow: name,
5821
7470
  status: "running",
5822
- steps: [],
7471
+ events: [],
5823
7472
  totalCost: 0,
5824
7473
  startedAt: Date.now(),
5825
7474
  duration: 0
@@ -5833,79 +7482,49 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5833
7482
  providerRegistry: this.providerRegistry,
5834
7483
  sessionHistory,
5835
7484
  signal: controller.signal,
7485
+ // Sentinel `onToken` so the streaming code path in WorkflowContext
7486
+ // is entered (it's gated on `this.onToken` being defined). The
7487
+ // actual `token` AxlEvents are emitted via `emitEvent` and reach
7488
+ // the wire through `onTrace` — this callback is just the gate.
7489
+ onToken: () => {
7490
+ },
5836
7491
  onTrace: (event) => {
5837
- execInfo.steps.push(event);
5838
- if (event.cost) execInfo.totalCost += event.cost;
7492
+ pushEventBounded(execInfo, event, this.maxEventsPerExecution);
7493
+ execInfo.totalCost += eventCostContribution(event);
5839
7494
  this.emit("trace", event);
5840
- this.outputTraceEvent(event);
5841
- if (event.type === "handoff") {
5842
- const data = event.data;
5843
- axlStream._push({
5844
- type: "handoff",
5845
- source: event.agent ?? "",
5846
- target: data?.target ?? "",
5847
- mode: data?.mode ?? void 0
5848
- });
7495
+ this.outputAxlEvent(event);
7496
+ axlStream._push(event);
7497
+ if (event.type === "handoff_start") {
5849
7498
  const sessionId = options?.metadata?.sessionId;
5850
7499
  if (sessionId) {
5851
7500
  this.appendHandoffRecord(sessionId, {
5852
- source: event.agent ?? "",
5853
- target: data?.target ?? "",
5854
- mode: data?.mode ?? "oneway",
7501
+ source: event.data.source,
7502
+ target: event.data.target,
7503
+ mode: event.data.mode,
5855
7504
  timestamp: event.timestamp,
5856
- duration: data?.duration ?? void 0
7505
+ toAskId: event.toAskId
5857
7506
  });
5858
7507
  }
5859
- } else if (event.type === "tool_denied") {
5860
- const data = event.data;
5861
- if (data?.denied === false) {
5862
- axlStream._push({
5863
- type: "tool_approval",
5864
- name: event.tool ?? "",
5865
- args: data?.args,
5866
- approved: true
7508
+ } else if (event.type === "handoff_return") {
7509
+ const sessionId = options?.metadata?.sessionId;
7510
+ if (sessionId) {
7511
+ this.updateHandoffDuration(sessionId, {
7512
+ toAskId: event.toAskId,
7513
+ duration: event.data.duration
5867
7514
  });
5868
- } else {
5869
- axlStream._push({
5870
- type: "tool_approval",
5871
- name: event.tool ?? "",
5872
- args: data?.args,
5873
- approved: false,
5874
- reason: data?.reason ?? void 0
7515
+ }
7516
+ } else if (event.type === "ask_end") {
7517
+ const sessionId = options?.metadata?.sessionId;
7518
+ if (sessionId && event.askId && typeof event.duration === "number") {
7519
+ this.updateHandoffDuration(sessionId, {
7520
+ toAskId: event.askId,
7521
+ duration: event.duration
5875
7522
  });
5876
7523
  }
5877
- } else if (event.type === "agent_call") {
5878
- axlStream._push({
5879
- type: "agent_end",
5880
- agent: event.agent ?? "",
5881
- cost: event.cost,
5882
- duration: event.duration
5883
- });
5884
- } else if (event.type === "tool_call") {
5885
- axlStream._push({
5886
- type: "tool_result",
5887
- name: event.tool ?? "",
5888
- result: event.data?.result,
5889
- callId: event.data?.callId
5890
- });
5891
7524
  }
5892
- axlStream._push({ type: "step", step: event.step, data: event });
5893
- },
5894
- onToken: (token) => {
5895
- axlStream._push({ type: "token", data: token });
5896
- },
5897
- onToolCall: (call) => {
5898
- axlStream._push({
5899
- type: "tool_call",
5900
- name: call.name,
5901
- args: call.args,
5902
- callId: call.callId
5903
- });
5904
- },
5905
- onAgentStart: (info) => {
5906
- axlStream._push({ type: "agent_start", agent: info.agent, model: info.model });
5907
7525
  },
5908
7526
  pendingDecisions: this.pendingDecisionResolvers,
7527
+ awaitHumanHandler: options?.awaitHumanHandler,
5909
7528
  stateStore: this.stateStore,
5910
7529
  workflowName: name,
5911
7530
  mcpManager: this.mcpManager,
@@ -5919,7 +7538,6 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5919
7538
  policy: "finish_and_stop"
5920
7539
  }
5921
7540
  });
5922
- ctx = wfCtx;
5923
7541
  return this.spanManager.withSpanAsync(
5924
7542
  "axl.workflow.execute",
5925
7543
  {
@@ -5927,47 +7545,25 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5927
7545
  "axl.execution.id": executionId,
5928
7546
  "axl.workflow.input_hash": hashInput(validated)
5929
7547
  },
5930
- async (span) => {
5931
- try {
5932
- const rawResult = await workflow2.handler(wfCtx);
5933
- const result = workflow2.outputSchema ? workflow2.outputSchema.parse(rawResult) : rawResult;
5934
- execInfo.status = "completed";
5935
- execInfo.completedAt = Date.now();
5936
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5937
- execInfo.result = result;
5938
- wfCtx.log("workflow_end", {
5939
- workflow: name,
5940
- status: "completed",
5941
- duration: execInfo.duration,
5942
- cost: execInfo.totalCost
5943
- });
5944
- if (this.stateStore.deleteCheckpoints) {
5945
- await this.stateStore.deleteCheckpoints(executionId);
5946
- }
5947
- span.setAttribute("axl.workflow.cost", execInfo.totalCost);
5948
- span.setAttribute("axl.workflow.duration", execInfo.duration);
5949
- this.persistExecution(execInfo);
5950
- return result;
5951
- } finally {
5952
- this.abortControllers.delete(executionId);
5953
- }
5954
- }
7548
+ (span) => this.runWorkflowBody({
7549
+ workflow: workflow2,
7550
+ ctx: wfCtx,
7551
+ execInfo,
7552
+ validated,
7553
+ span
7554
+ })
5955
7555
  );
5956
7556
  };
5957
- run().then((result) => axlStream._done(result)).catch((err) => {
5958
- if (execInfo) {
7557
+ run().then((result) => axlStream._done(result, executionId)).catch((err) => {
7558
+ if (execInfo && execInfo.status === "running") {
5959
7559
  execInfo.status = "failed";
5960
7560
  execInfo.completedAt = Date.now();
5961
7561
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5962
7562
  execInfo.error = err instanceof Error ? err.message : String(err);
5963
- ctx?.log("workflow_end", {
5964
- workflow: name,
5965
- status: "failed",
5966
- error: execInfo.error
5967
- });
5968
7563
  this.persistExecution(execInfo);
5969
7564
  }
5970
- axlStream._error(err instanceof Error ? err : new Error(String(err)));
7565
+ this.abortControllers.delete(executionId);
7566
+ axlStream._error(err instanceof Error ? err : new Error(String(err)), executionId);
5971
7567
  });
5972
7568
  return axlStream;
5973
7569
  }
@@ -6028,6 +7624,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6028
7624
  if (this.stateStore.saveEvalResult) {
6029
7625
  await this.stateStore.saveEvalResult(entry);
6030
7626
  }
7627
+ this.emit("eval_result", entry);
6031
7628
  }
6032
7629
  /**
6033
7630
  * Delete an eval history entry by id. Removes from in-memory cache and
@@ -6159,7 +7756,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6159
7756
  *
6160
7757
  * @see Spec Section 13.5
6161
7758
  */
6162
- async eval(config) {
7759
+ async eval(config, options) {
6163
7760
  let runEvalFn;
6164
7761
  try {
6165
7762
  ({ runEval: runEvalFn } = await import("@axlsdk/eval"));
@@ -6174,7 +7771,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6174
7771
  });
6175
7772
  return { output: result, cost, metadata };
6176
7773
  };
6177
- return runEvalFn(config, executeWorkflow, this);
7774
+ return runEvalFn(config, executeWorkflow, this, options);
6178
7775
  }
6179
7776
  /**
6180
7777
  * Compare two evaluation results to detect regressions and improvements.
@@ -6212,9 +7809,31 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6212
7809
  * Returns cost (same as `trackCost`) plus metadata extracted from trace events:
6213
7810
  * models (unique URIs), tokens (input/output/reasoning sums), and agent call count.
6214
7811
  *
7812
+ * ## Cost vs tokens semantics
7813
+ *
7814
+ * - `cost` is the full aggregate across EVERY event with a top-level
7815
+ * `event.cost` set: agent calls, tool calls, semantic memory ops, etc.
7816
+ * This is the number to reconcile against your provider bill.
7817
+ *
7818
+ * - `metadata.tokens` is narrowly scoped to **agent** prompt/completion/
7819
+ * reasoning tokens. Embedder tokens from semantic `ctx.remember({embed:true})`
7820
+ * / `ctx.recall({query})` are deliberately NOT summed here — they're a
7821
+ * different category (input-only, different pricing, different model).
7822
+ * Conflating them would make "prompt tokens" misleading in the UI. If you
7823
+ * need embedder token counts, subscribe to `runtime.on('trace', ...)` and
7824
+ * read `data.usage.tokens` on `memory_remember` / `memory_recall` events.
7825
+ *
7826
+ * Pass `{ captureTraces: true }` to also collect the raw `AxlEvent[]` observed
7827
+ * during `fn()`. This is opt-in because it keeps every event in memory for the
7828
+ * duration of the call — useful for eval per-item capture, debugging, and test
7829
+ * assertions, but overhead grows with trace volume. When enabled, verbose-mode
7830
+ * `agent_call_start.data.messages` snapshots are omitted from captured events (still
7831
+ * broadcast via onTrace) to keep memory bounded — callers who need the full
7832
+ * verbose snapshot should subscribe to `runtime.on('trace', ...)` directly.
7833
+ *
6215
7834
  * Works with both `createContext()` and `execute()` calls inside `fn`.
6216
7835
  */
6217
- async trackExecution(fn) {
7836
+ async trackExecution(fn, options) {
6218
7837
  const parentScope = costScopeStorage.getStore();
6219
7838
  const scope = {
6220
7839
  totalCost: 0,
@@ -6225,10 +7844,11 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6225
7844
  const workflowCalls = /* @__PURE__ */ new Map();
6226
7845
  const tokens = { input: 0, output: 0, reasoning: 0 };
6227
7846
  let agentCalls = 0;
7847
+ const capturedTraces = options?.captureTraces ? [] : void 0;
6228
7848
  const listener = (event) => {
6229
7849
  if (!scope.trackedIds.has(event.executionId)) return;
6230
- if (event.cost) scope.totalCost += event.cost;
6231
- if (event.type === "agent_call") {
7850
+ scope.totalCost += eventCostContribution(event);
7851
+ if (event.type === "agent_call_end") {
6232
7852
  if (event.model) modelCalls.set(event.model, (modelCalls.get(event.model) ?? 0) + 1);
6233
7853
  agentCalls++;
6234
7854
  if (event.tokens) {
@@ -6237,17 +7857,23 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6237
7857
  tokens.reasoning += event.tokens.reasoning ?? 0;
6238
7858
  }
6239
7859
  }
6240
- let workflowName;
6241
7860
  if (event.type === "workflow_start" && event.workflow) {
6242
- workflowName = event.workflow;
6243
- } else if (event.type === "log" && event.data && typeof event.data === "object") {
6244
- const d = event.data;
6245
- if (d.event === "workflow_start" && typeof d.workflow === "string") {
6246
- workflowName = d.workflow;
6247
- }
7861
+ workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
6248
7862
  }
6249
- if (workflowName) {
6250
- workflowCalls.set(workflowName, (workflowCalls.get(workflowName) ?? 0) + 1);
7863
+ if (capturedTraces) {
7864
+ if (event.type === "token" || event.type === "partial_object") return;
7865
+ if (event.type === "agent_call_start" && event.data) {
7866
+ const d = event.data;
7867
+ if ("messages" in d) {
7868
+ const rest = {};
7869
+ for (const k of Object.keys(d)) {
7870
+ if (k !== "messages") rest[k] = d[k];
7871
+ }
7872
+ capturedTraces.push({ ...event, data: rest });
7873
+ return;
7874
+ }
7875
+ }
7876
+ capturedTraces.push(event);
6251
7877
  }
6252
7878
  };
6253
7879
  this.setMaxListeners(this.getMaxListeners() + 1);
@@ -6257,6 +7883,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6257
7883
  return {
6258
7884
  result,
6259
7885
  cost: scope.totalCost,
7886
+ ...capturedTraces ? { traces: capturedTraces } : {},
6260
7887
  metadata: {
6261
7888
  models: [...modelCalls.keys()],
6262
7889
  modelCallCounts: modelCalls.size > 0 ? Object.fromEntries(modelCalls) : void 0,
@@ -6266,6 +7893,16 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6266
7893
  workflowCallCounts: workflowCalls.size > 0 ? Object.fromEntries(workflowCalls) : void 0
6267
7894
  }
6268
7895
  };
7896
+ } catch (err) {
7897
+ if (capturedTraces && typeof err === "object" && err !== null) {
7898
+ Object.defineProperty(err, "axlCapturedTraces", {
7899
+ value: capturedTraces,
7900
+ enumerable: false,
7901
+ writable: true,
7902
+ configurable: true
7903
+ });
7904
+ }
7905
+ throw err;
6269
7906
  } finally {
6270
7907
  this.off("trace", listener);
6271
7908
  this.setMaxListeners(this.getMaxListeners() - 1);
@@ -6290,7 +7927,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6290
7927
  * The emit('trace', event) call happens before this method is called, so
6291
7928
  * programmatic subscribers always receive events regardless of trace config.
6292
7929
  */
6293
- outputTraceEvent(event) {
7930
+ outputAxlEvent(event) {
6294
7931
  const traceConfig = this.config.trace;
6295
7932
  if (!traceConfig?.enabled) return;
6296
7933
  const level = traceConfig.level ?? "steps";
@@ -6314,18 +7951,20 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6314
7951
  }
6315
7952
  return;
6316
7953
  }
6317
- this.logTraceEvent(event);
7954
+ this.logAxlEvent(event);
6318
7955
  }
6319
- logTraceEvent(event) {
7956
+ logAxlEvent(event) {
6320
7957
  const level = this.config.trace?.level ?? "steps";
6321
7958
  const workflowPrefix = event.workflow ? `workflow:${event.workflow} | ` : "";
6322
7959
  const parts = [`[axl] execution:${event.executionId}`];
6323
- const data = event.data;
6324
7960
  if (event.type === "workflow_start") {
6325
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | started`);
7961
+ parts.push(`${workflowPrefix}started`);
6326
7962
  } else if (event.type === "workflow_end") {
6327
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | completed`);
6328
- } else if (event.type === "agent_call") {
7963
+ const d = event.data;
7964
+ const status = d?.aborted ? "aborted" : d?.status ?? "completed";
7965
+ parts.push(`${workflowPrefix}${status}`);
7966
+ if (event.duration != null) parts.push(`${(event.duration / 1e3).toFixed(1)}s`);
7967
+ } else if (event.type === "agent_call_end") {
6329
7968
  parts.push(`${workflowPrefix}step:${event.step} agent_call`);
6330
7969
  if (event.agent) parts.push(`agent:${event.agent}`);
6331
7970
  if (event.promptVersion) parts.push(`version:${event.promptVersion}`);
@@ -6335,7 +7974,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6335
7974
  if (level === "full" && event.data) {
6336
7975
  parts.push(`data:${JSON.stringify(event.data)}`);
6337
7976
  }
6338
- } else if (event.type === "tool_call") {
7977
+ } else if (event.type === "tool_call_end") {
6339
7978
  parts.push(`${workflowPrefix}step:${event.step} tool_call`);
6340
7979
  if (event.tool) parts.push(`tool:${event.tool}`);
6341
7980
  if (event.duration) parts.push(`${event.duration}ms`);
@@ -6352,8 +7991,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6352
7991
  parts.push(`${workflowPrefix}log: ${JSON.stringify(event.data)}`);
6353
7992
  } else {
6354
7993
  parts.push(`${workflowPrefix}${event.type}`);
6355
- if (level === "full" && event.data) {
6356
- parts.push(`data:${JSON.stringify(event.data)}`);
7994
+ const data = event.data;
7995
+ if (level === "full" && data !== void 0) {
7996
+ parts.push(`data:${JSON.stringify(data)}`);
6357
7997
  }
6358
7998
  }
6359
7999
  console.log(parts.join(" | "));
@@ -6373,6 +8013,35 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6373
8013
  }).catch(() => {
6374
8014
  });
6375
8015
  }
8016
+ /**
8017
+ * Patch `duration` on the handoff record matching `toAskId` (the target
8018
+ * frame's askId, which the runtime stamps on `handoff_start` and which
8019
+ * appears as `askId` on the target's `ask_end`).
8020
+ *
8021
+ * Called from two places per handoff:
8022
+ * - target's `ask_end` — first-write source for ONEWAY (no return event).
8023
+ * - `handoff_return` — overwrites for ROUNDTRIP with the round-trip
8024
+ * wall-clock figure (intentional: round-trip
8025
+ * duration is the more useful metric for the
8026
+ * source-paused-for-N-ms semantic).
8027
+ *
8028
+ * No-op if no record matches `toAskId` (e.g., stream subscribed mid-
8029
+ * handoff, or session metadata got wiped) — `duration` stays undefined,
8030
+ * which is the documented default for `HandoffRecord`.
8031
+ */
8032
+ updateHandoffDuration(sessionId, match) {
8033
+ this.stateStore.getSessionMeta(sessionId, "handoffHistory").then((existing) => {
8034
+ const history = existing ?? [];
8035
+ for (const r of history) {
8036
+ if (r.toAskId === match.toAskId) {
8037
+ r.duration = match.duration;
8038
+ return this.stateStore.saveSessionMeta(sessionId, "handoffHistory", history);
8039
+ }
8040
+ }
8041
+ return void 0;
8042
+ }).catch(() => {
8043
+ });
8044
+ }
6376
8045
  };
6377
8046
 
6378
8047
  // src/state/redis.ts
@@ -6435,27 +8104,13 @@ var RedisStore = class _RedisStore {
6435
8104
  return "axl:eval-history-ids";
6436
8105
  }
6437
8106
  // ── Checkpoints ──────────────────────────────────────────────────────
6438
- async saveCheckpoint(executionId, step, data) {
6439
- await this.client.hSet(this.checkpointKey(executionId), String(step), JSON.stringify(data));
8107
+ async saveCheckpoint(executionId, name, data) {
8108
+ await this.client.hSet(this.checkpointKey(executionId), name, JSON.stringify(data));
6440
8109
  }
6441
- async getCheckpoint(executionId, step) {
6442
- const raw = await this.client.hGet(this.checkpointKey(executionId), String(step));
8110
+ async getCheckpoint(executionId, name) {
8111
+ const raw = await this.client.hGet(this.checkpointKey(executionId), name);
6443
8112
  return raw != null ? JSON.parse(raw) : null;
6444
8113
  }
6445
- async getLatestCheckpoint(executionId) {
6446
- const all = await this.client.hGetAll(this.checkpointKey(executionId));
6447
- if (!all || Object.keys(all).length === 0) return null;
6448
- let maxStep = -1;
6449
- let maxData = null;
6450
- for (const [stepStr, raw] of Object.entries(all)) {
6451
- const step = Number(stepStr);
6452
- if (step > maxStep) {
6453
- maxStep = step;
6454
- maxData = JSON.parse(raw);
6455
- }
6456
- }
6457
- return { step: maxStep, data: maxData };
6458
- }
6459
8114
  // ── Sessions ─────────────────────────────────────────────────────────
6460
8115
  async saveSession(sessionId, history) {
6461
8116
  await this.client.set(this.sessionKey(sessionId), JSON.stringify(history));
@@ -6560,6 +8215,16 @@ var RedisStore = class _RedisStore {
6560
8215
  };
6561
8216
 
6562
8217
  // src/memory/embedder-openai.ts
8218
+ var EMBEDDING_PRICE_PER_1M_TOKENS = {
8219
+ "text-embedding-3-small": 0.02,
8220
+ "text-embedding-3-large": 0.13,
8221
+ "text-embedding-ada-002": 0.1
8222
+ };
8223
+ function computeCost(model, tokens) {
8224
+ const pricePerMillion = EMBEDDING_PRICE_PER_1M_TOKENS[model];
8225
+ if (pricePerMillion == null) return void 0;
8226
+ return tokens / 1e6 * pricePerMillion;
8227
+ }
6563
8228
  var OpenAIEmbedder = class {
6564
8229
  apiKey;
6565
8230
  model;
@@ -6574,8 +8239,8 @@ var OpenAIEmbedder = class {
6574
8239
  this.baseUrl = options.baseUrl ?? "https://api.openai.com";
6575
8240
  this.dimensions = options.dimensions ?? 1536;
6576
8241
  }
6577
- async embed(texts) {
6578
- const response = await fetch(`${this.baseUrl}/v1/embeddings`, {
8242
+ async embed(texts, signal) {
8243
+ const response = await fetchWithRetry(`${this.baseUrl}/v1/embeddings`, {
6579
8244
  method: "POST",
6580
8245
  headers: {
6581
8246
  "Content-Type": "application/json",
@@ -6585,14 +8250,28 @@ var OpenAIEmbedder = class {
6585
8250
  model: this.model,
6586
8251
  input: texts,
6587
8252
  dimensions: this.dimensions
6588
- })
8253
+ }),
8254
+ signal
6589
8255
  });
6590
8256
  if (!response.ok) {
6591
8257
  const body = await response.text();
6592
8258
  throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
6593
8259
  }
6594
8260
  const json = await response.json();
6595
- return json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
8261
+ const vectors = json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
8262
+ const tokens = json.usage?.prompt_tokens ?? json.usage?.total_tokens;
8263
+ const cost = tokens != null ? computeCost(this.model, tokens) : void 0;
8264
+ if (tokens == null && cost == null) {
8265
+ return { vectors };
8266
+ }
8267
+ return {
8268
+ vectors,
8269
+ usage: {
8270
+ ...tokens != null ? { tokens } : {},
8271
+ ...cost != null ? { cost } : {},
8272
+ model: this.model
8273
+ }
8274
+ };
6596
8275
  }
6597
8276
  };
6598
8277
 
@@ -6717,11 +8396,13 @@ function cosineSimilarity2(a, b) {
6717
8396
  }
6718
8397
  // Annotate the CommonJS export names for ESM import in node:
6719
8398
  0 && (module.exports = {
8399
+ AXL_EVENT_TYPES,
6720
8400
  AnthropicProvider,
6721
8401
  AxlError,
6722
8402
  AxlRuntime,
6723
8403
  AxlStream,
6724
8404
  BudgetExceededError,
8405
+ COST_BEARING_LEAF_TYPES,
6725
8406
  GeminiProvider,
6726
8407
  GuardrailError,
6727
8408
  InMemoryVectorStore,
@@ -6736,6 +8417,8 @@ function cosineSimilarity2(a, b) {
6736
8417
  OpenAIResponsesProvider,
6737
8418
  ProviderRegistry,
6738
8419
  QuorumNotMet,
8420
+ REDACTED,
8421
+ REDACTION_RULES,
6739
8422
  RedisStore,
6740
8423
  SQLiteStore,
6741
8424
  Session,
@@ -6748,7 +8431,12 @@ function cosineSimilarity2(a, b) {
6748
8431
  agent,
6749
8432
  createSpanManager,
6750
8433
  defineConfig,
8434
+ eventCostContribution,
6751
8435
  extractJson,
8436
+ isCostBearingLeaf,
8437
+ isRootLevel,
8438
+ parsePartialJson,
8439
+ redactEvent,
6752
8440
  resolveThinkingOptions,
6753
8441
  tool,
6754
8442
  workflow,