@axlsdk/axl 0.15.0 → 0.16.1

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
@@ -2188,6 +2235,432 @@ var ToolDenied = class extends AxlError {
2188
2235
  }
2189
2236
  };
2190
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
+
2191
2664
  // src/config.ts
2192
2665
  function defineConfig(config) {
2193
2666
  return config;
@@ -2265,6 +2738,7 @@ function resolveConfig(config) {
2265
2738
 
2266
2739
  // src/context.ts
2267
2740
  var signalStorage = new import_node_async_hooks.AsyncLocalStorage();
2741
+ var askStorage = new import_node_async_hooks.AsyncLocalStorage();
2268
2742
  function zodToJsonSchema(schema) {
2269
2743
  const result = import_zod.z.toJSONSchema(schema, { unrepresentable: "any" });
2270
2744
  delete result.$schema;
@@ -2366,8 +2840,34 @@ var WorkflowContext = class _WorkflowContext {
2366
2840
  pendingDecisions;
2367
2841
  budgetContext;
2368
2842
  stateStore;
2369
- stepCounter = 0;
2370
- 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;
2371
2871
  signal;
2372
2872
  summaryCache;
2373
2873
  workflowName;
@@ -2379,7 +2879,6 @@ var WorkflowContext = class _WorkflowContext {
2379
2879
  awaitHumanHandler;
2380
2880
  onAgentStart;
2381
2881
  onAgentCallComplete;
2382
- parentToolCallId;
2383
2882
  constructor(init) {
2384
2883
  this.input = init.input;
2385
2884
  this.executionId = init.executionId;
@@ -2403,7 +2902,10 @@ var WorkflowContext = class _WorkflowContext {
2403
2902
  this.awaitHumanHandler = init.awaitHumanHandler;
2404
2903
  this.onAgentStart = init.onAgentStart;
2405
2904
  this.onAgentCallComplete = init.onAgentCallComplete;
2406
- this.parentToolCallId = init.parentToolCallId;
2905
+ this.autoCheckpointCounters = init.autoCheckpointCounters ?? {
2906
+ byAgent: /* @__PURE__ */ new Map(),
2907
+ root: 0
2908
+ };
2407
2909
  if (init.metadata?.summaryCache) {
2408
2910
  this.summaryCache = init.metadata.summaryCache;
2409
2911
  }
@@ -2412,15 +2914,23 @@ var WorkflowContext = class _WorkflowContext {
2412
2914
  * Create a child context for nested agent invocations (e.g., agent-as-tool).
2413
2915
  * Shares: budget tracking, abort signals, trace emission, provider registry,
2414
2916
  * state store, span manager, memory manager, MCP manager, config,
2415
- * awaitHuman handler, pending decisions, tool overrides.
2416
- * 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.
2417
2926
  *
2418
- * @param parentToolCallId - The `callId` of the outer `tool_call` that
2419
- * spawned this child. When set, every trace event emitted by the child
2420
- * will carry this id as `parentToolCallId`, so consumers can join nested
2421
- * agent calls back to the outer tool call that invoked them.
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.
2422
2932
  */
2423
- createChildContext(parentToolCallId) {
2933
+ createChildContext() {
2424
2934
  return new _WorkflowContext({
2425
2935
  input: this.input,
2426
2936
  executionId: this.executionId,
@@ -2434,18 +2944,20 @@ var WorkflowContext = class _WorkflowContext {
2434
2944
  spanManager: this.spanManager,
2435
2945
  memoryManager: this.memoryManager,
2436
2946
  onTrace: this.onTrace,
2947
+ onToken: this.onToken,
2948
+ onToolCall: this.onToolCall,
2949
+ onAgentStart: this.onAgentStart,
2437
2950
  onAgentCallComplete: this.onAgentCallComplete,
2438
2951
  awaitHumanHandler: this.awaitHumanHandler,
2439
2952
  pendingDecisions: this.pendingDecisions,
2440
2953
  toolOverrides: this.toolOverrides,
2441
2954
  signal: this.signal,
2442
2955
  workflowName: this.workflowName,
2443
- // Join key for nested event correlation. Inherit the parent's
2444
- // `parentToolCallId` when this child is itself nested inside another
2445
- // child — so grand-children still point to the outermost tool call.
2446
- parentToolCallId: parentToolCallId ?? this.parentToolCallId
2447
- // Isolated: sessionHistory (empty), stepCounter (0),
2448
- // onToken (null), onAgentStart (null), onToolCall (null)
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)
2449
2961
  });
2450
2962
  }
2451
2963
  /**
@@ -2456,72 +2968,150 @@ var WorkflowContext = class _WorkflowContext {
2456
2968
  get currentSignal() {
2457
2969
  return signalStorage.getStore() ?? this.signal;
2458
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
+ }
2459
2994
  // ── ctx.ask() ─────────────────────────────────────────────────────────
2460
2995
  async ask(agent2, prompt, options) {
2461
- return this._checkpoint(async () => {
2462
- const costBefore = this.budgetContext?.totalCost ?? 0;
2463
- const startTime = Date.now();
2464
- const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
2465
- const usageCapture = {};
2466
- const doCall = async () => {
2467
- const result2 = await this.executeAgentCall(
2468
- agent2,
2469
- prompt,
2470
- options,
2471
- void 0,
2472
- usageCapture
2473
- );
2474
- return result2;
2475
- };
2476
- const result = this.spanManager ? await this.spanManager.withSpanAsync(
2477
- "axl.agent.ask",
2478
- {
2479
- "axl.agent.name": agent2._name,
2480
- "axl.agent.model": agent2.resolveModel(resolveCtx)
2481
- },
2482
- async (span) => {
2483
- const r = await doCall();
2484
- const costAfter2 = this.budgetContext?.totalCost ?? 0;
2485
- span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
2486
- span.setAttribute("axl.agent.duration", Date.now() - startTime);
2487
- if (usageCapture.value) {
2488
- span.setAttribute("axl.agent.prompt_tokens", usageCapture.value.prompt_tokens);
2489
- span.setAttribute(
2490
- "axl.agent.completion_tokens",
2491
- 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
2492
3025
  );
2493
- if (usageCapture.value.cached_tokens)
2494
- 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
+ });
2495
3104
  }
2496
- return r;
2497
- }
2498
- ) : await doCall();
2499
- const costAfter = this.budgetContext?.totalCost ?? 0;
2500
- this.onAgentCallComplete?.({
2501
- agent: agent2._name,
2502
- prompt,
2503
- response: typeof result === "string" ? result : JSON.stringify(result),
2504
- model: agent2.resolveModel(resolveCtx),
2505
- cost: costAfter - costBefore,
2506
- duration: Date.now() - startTime,
2507
- promptVersion: agent2._config.version,
2508
- temperature: options?.temperature ?? agent2._config.temperature,
2509
- maxTokens: options?.maxTokens ?? agent2._config.maxTokens ?? 4096,
2510
- effort: options?.effort ?? agent2._config.effort,
2511
- thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
2512
- includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
2513
- toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
2514
- stop: options?.stop ?? agent2._config.stop,
2515
- providerOptions: options?.providerOptions ?? agent2._config.providerOptions
2516
- });
2517
- return result;
2518
- });
3105
+ });
3106
+ },
3107
+ { agent: agentName }
3108
+ );
2519
3109
  }
2520
3110
  async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
2521
3111
  if (this.budgetContext?.exceeded) {
2522
3112
  const { limit, totalCost: spent, policy } = this.budgetContext;
2523
3113
  if (policy === "warn") {
2524
- this.emitTrace({
3114
+ this.emitEvent({
2525
3115
  type: "log",
2526
3116
  data: { warning: "Budget exceeded", limit, spent, policy }
2527
3117
  });
@@ -2610,7 +3200,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2610
3200
  const guardrails = agent2._config.guardrails;
2611
3201
  if (guardrails?.input) {
2612
3202
  const inputResult = await guardrails.input(prompt, { metadata: this.metadata });
2613
- this.emitTrace({
3203
+ this.emitEvent({
2614
3204
  type: "guardrail",
2615
3205
  agent: agent2._name,
2616
3206
  data: {
@@ -2644,12 +3234,6 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2644
3234
  const maxTurns = agent2._config.maxTurns ?? 25;
2645
3235
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
2646
3236
  const startTime = Date.now();
2647
- if (this.onToken && options?.validate) {
2648
- throw new AxlError(
2649
- "INVALID_CONFIG",
2650
- "Cannot use validate with streaming. Validate requires schema (JSON output) which does not benefit from token streaming. Use a non-streaming call instead."
2651
- );
2652
- }
2653
3237
  const currentMessages = [...messages];
2654
3238
  let turns = 0;
2655
3239
  let guardrailOutputRetries = 0;
@@ -2658,11 +3242,42 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2658
3242
  const maxGuardrailRetries = guardrails?.maxRetries ?? 2;
2659
3243
  let pendingRetryReason;
2660
3244
  const verboseTrace = this.config.trace?.level === "full";
3245
+ let lastStartStage = "initial";
3246
+ let lastStartAttempt = 1;
3247
+ let lastStartMaxAttempts = 1;
2661
3248
  while (turns < maxTurns) {
2662
3249
  if (Date.now() - startTime > timeoutMs) {
2663
3250
  throw new TimeoutError("ctx.ask()", timeoutMs);
2664
3251
  }
2665
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
+ }
2666
3281
  const turnStart = Date.now();
2667
3282
  const chatOptions = {
2668
3283
  model,
@@ -2680,72 +3295,16 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2680
3295
  if (options?.schema && toolDefs.length === 0) {
2681
3296
  chatOptions.responseFormat = { type: "json_object" };
2682
3297
  }
2683
- this.onAgentStart?.({ agent: agent2._name, model: modelUri });
2684
- let response;
2685
- if (this.onToken) {
2686
- let content2 = "";
2687
- const toolCalls = [];
2688
- const toolCallBuffers = /* @__PURE__ */ new Map();
2689
- let streamProviderMetadata;
2690
- let thinkingContent = "";
2691
- for await (const chunk of provider.stream(currentMessages, chatOptions)) {
2692
- if (chunk.type === "text_delta") {
2693
- content2 += chunk.content;
2694
- this.onToken(chunk.content);
2695
- } else if (chunk.type === "thinking_delta") {
2696
- thinkingContent += chunk.content;
2697
- } else if (chunk.type === "tool_call_delta") {
2698
- let buffer = toolCallBuffers.get(chunk.id);
2699
- if (!buffer) {
2700
- buffer = { id: chunk.id, name: "", arguments: "" };
2701
- toolCallBuffers.set(chunk.id, buffer);
2702
- }
2703
- if (chunk.name) buffer.name = chunk.name;
2704
- if (chunk.arguments) buffer.arguments += chunk.arguments;
2705
- } else if (chunk.type === "done") {
2706
- streamProviderMetadata = chunk.providerMetadata;
2707
- if (chunk.usage) {
2708
- response = {
2709
- content: content2,
2710
- tool_calls: void 0,
2711
- usage: chunk.usage,
2712
- cost: chunk.cost
2713
- };
2714
- }
2715
- }
2716
- }
2717
- for (const buffer of toolCallBuffers.values()) {
2718
- toolCalls.push({
2719
- id: buffer.id,
2720
- type: "function",
2721
- function: {
2722
- name: buffer.name,
2723
- arguments: buffer.arguments
2724
- }
2725
- });
2726
- }
2727
- response ??= {
2728
- content: content2,
2729
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
2730
- };
2731
- if (toolCalls.length > 0) {
2732
- response.tool_calls = toolCalls;
2733
- }
2734
- if (streamProviderMetadata) {
2735
- response.providerMetadata = streamProviderMetadata;
2736
- }
2737
- if (thinkingContent) {
2738
- response.thinking_content = thinkingContent;
2739
- }
2740
- } else {
2741
- response = await provider.chat(currentMessages, chatOptions);
2742
- }
2743
- if (usageCapture && response.usage) {
2744
- usageCapture.value = response.usage;
2745
- }
2746
- if (response.cost) {
2747
- this._accumulateBudgetCost(response.cost);
2748
- }
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
+ };
2749
3308
  let messagesSnapshot;
2750
3309
  if (verboseTrace) {
2751
3310
  try {
@@ -2760,8 +3319,148 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2760
3319
  }
2761
3320
  const retryReason = pendingRetryReason;
2762
3321
  pendingRetryReason = void 0;
2763
- this.emitTrace({
2764
- type: "agent_call",
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);
3338
+ let response;
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
+ }
3412
+ }
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);
3439
+ }
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;
3455
+ }
3456
+ if (usageCapture && response.usage) {
3457
+ usageCapture.value = response.usage;
3458
+ }
3459
+ if (response.cost) {
3460
+ this._accumulateBudgetCost(response.cost);
3461
+ }
3462
+ this.emitEvent({
3463
+ type: "agent_call_end",
2765
3464
  agent: agent2._name,
2766
3465
  model: modelUri,
2767
3466
  promptVersion: agent2._config.version,
@@ -2773,22 +3472,10 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2773
3472
  } : void 0,
2774
3473
  duration: Date.now() - turnStart,
2775
3474
  data: {
2776
- prompt,
2777
3475
  response: response.content,
2778
- ...systemPrompt ? { system: systemPrompt } : {},
2779
3476
  ...response.thinking_content ? { thinking: response.thinking_content } : {},
2780
- params: {
2781
- ...chatOptions.temperature !== void 0 ? { temperature: chatOptions.temperature } : {},
2782
- maxTokens: chatOptions.maxTokens,
2783
- ...chatOptions.effort !== void 0 ? { effort: chatOptions.effort } : {},
2784
- ...chatOptions.thinkingBudget !== void 0 ? { thinkingBudget: chatOptions.thinkingBudget } : {},
2785
- ...chatOptions.includeThoughts !== void 0 ? { includeThoughts: chatOptions.includeThoughts } : {},
2786
- ...chatOptions.toolChoice !== void 0 ? { toolChoice: chatOptions.toolChoice } : {},
2787
- ...chatOptions.stop !== void 0 ? { stop: chatOptions.stop } : {}
2788
- },
2789
3477
  turn: turns,
2790
- ...retryReason ? { retryReason } : {},
2791
- ...messagesSnapshot ? { messages: messagesSnapshot } : {}
3478
+ ...retryReason ? { retryReason } : {}
2792
3479
  }
2793
3480
  });
2794
3481
  if (response.tool_calls && response.tool_calls.length > 0) {
@@ -2814,6 +3501,19 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2814
3501
  }
2815
3502
  }
2816
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
+ };
2817
3517
  const handoffOptions = options ? {
2818
3518
  schema: options.schema,
2819
3519
  retries: options.retries,
@@ -2821,13 +3521,51 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2821
3521
  validate: options.validate,
2822
3522
  validateRetries: options.validateRetries
2823
3523
  } : void 0;
2824
- const handoffFn = () => this.executeAgentCall(
2825
- descriptor.agent,
2826
- handoffPrompt,
2827
- handoffOptions,
2828
- currentMessages,
2829
- usageCapture
2830
- );
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
+ });
2831
3569
  if (mode === "roundtrip") {
2832
3570
  const executeRoundtrip = async () => {
2833
3571
  const result = await handoffFn();
@@ -2848,36 +3586,41 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2848
3586
  "axl.handoff.mode": mode
2849
3587
  },
2850
3588
  async (span) => {
2851
- const result = await executeRoundtrip();
2852
- const duration = Date.now() - handoffStart;
2853
- span.setAttribute("axl.handoff.duration", duration);
2854
- this.emitTrace({
2855
- type: "handoff",
2856
- agent: agent2._name,
2857
- data: {
2858
- target: targetName,
2859
- mode,
2860
- duration,
2861
- source: agent2._name,
2862
- ...handoffPrompt !== prompt ? { message: handoffPrompt } : {}
2863
- }
2864
- });
2865
- 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
+ }
2866
3604
  }
2867
3605
  );
2868
3606
  } else {
2869
- await executeRoundtrip();
2870
- this.emitTrace({
2871
- type: "handoff",
2872
- agent: agent2._name,
2873
- data: {
2874
- target: targetName,
2875
- mode,
2876
- duration: Date.now() - handoffStart,
2877
- source: agent2._name,
2878
- ...handoffPrompt !== prompt ? { message: handoffPrompt } : {}
2879
- }
2880
- });
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
+ }
2881
3624
  }
2882
3625
  continue;
2883
3626
  }
@@ -2891,29 +3634,12 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2891
3634
  },
2892
3635
  async (span) => {
2893
3636
  const result = await handoffFn();
2894
- const duration = Date.now() - handoffStart;
2895
- span.setAttribute("axl.handoff.duration", duration);
2896
- this.emitTrace({
2897
- type: "handoff",
2898
- agent: agent2._name,
2899
- data: { target: targetName, mode, duration, source: agent2._name }
2900
- });
3637
+ span.setAttribute("axl.handoff.duration", Date.now() - handoffStart);
2901
3638
  return result;
2902
3639
  }
2903
3640
  );
2904
3641
  }
2905
- const onewayResult = await handoffFn();
2906
- this.emitTrace({
2907
- type: "handoff",
2908
- agent: agent2._name,
2909
- data: {
2910
- target: targetName,
2911
- mode,
2912
- duration: Date.now() - handoffStart,
2913
- source: agent2._name
2914
- }
2915
- });
2916
- return onewayResult;
3642
+ return await handoffFn();
2917
3643
  }
2918
3644
  }
2919
3645
  const toolOverride = this.toolOverrides?.get(toolName);
@@ -2929,7 +3655,16 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2929
3655
  });
2930
3656
  continue;
2931
3657
  }
2932
- 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
+ );
2933
3668
  const toolStart2 = Date.now();
2934
3669
  const executeOverride = async () => {
2935
3670
  let toolResult3;
@@ -2957,8 +3692,8 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2957
3692
  }
2958
3693
  ) : await executeOverride();
2959
3694
  const resultContent2 = JSON.stringify(toolResult2);
2960
- this.emitTrace({
2961
- type: "tool_call",
3695
+ this.emitEvent({
3696
+ type: "tool_call_end",
2962
3697
  agent: agent2._name,
2963
3698
  tool: toolName,
2964
3699
  duration: Date.now() - toolStart2,
@@ -2974,7 +3709,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2974
3709
  const tool2 = agent2._config.tools?.find((t) => t.name === toolName);
2975
3710
  const isMcpTool = !tool2 && this.mcpManager?.isMcpTool(toolName);
2976
3711
  if (!tool2 && !isMcpTool) {
2977
- this.emitTrace({ type: "tool_denied", agent: agent2._name, tool: toolName });
3712
+ this.emitEvent({ type: "tool_denied", agent: agent2._name, tool: toolName });
2978
3713
  currentMessages.push({
2979
3714
  role: "tool",
2980
3715
  content: `Tool "${toolName}" is not available. Available tools: ${agent2._config.tools?.map((t) => t.name).join(", ") ?? "none"}`,
@@ -2993,7 +3728,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2993
3728
  });
2994
3729
  continue;
2995
3730
  }
2996
- 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);
2997
3738
  const toolStart = Date.now();
2998
3739
  if (tool2 && tool2.requireApproval) {
2999
3740
  const approvalFn = async () => {
@@ -3030,7 +3771,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3030
3771
  } else {
3031
3772
  approvalOutcome = await approvalFn();
3032
3773
  }
3033
- this.emitTrace({
3774
+ this.emitEvent({
3034
3775
  type: "tool_approval",
3035
3776
  agent: agent2._name,
3036
3777
  tool: toolName,
@@ -3073,7 +3814,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3073
3814
  resultContent2 = JSON.stringify(toolResult2);
3074
3815
  }
3075
3816
  } else if (tool2) {
3076
- const childCtx = this.createChildContext(toolCall.id);
3817
+ const childCtx = this.createChildContext();
3077
3818
  try {
3078
3819
  toolResult2 = await tool2._execute(toolArgs, childCtx);
3079
3820
  } catch (err) {
@@ -3115,8 +3856,8 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3115
3856
  return r;
3116
3857
  }
3117
3858
  ) : await executeTool();
3118
- this.emitTrace({
3119
- type: "tool_call",
3859
+ this.emitEvent({
3860
+ type: "tool_call_end",
3120
3861
  agent: agent2._name,
3121
3862
  tool: traceName,
3122
3863
  duration: Date.now() - toolStart,
@@ -3140,7 +3881,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3140
3881
  if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
3141
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.`;
3142
3883
  }
3143
- this.emitTrace({
3884
+ this.emitEvent({
3144
3885
  type: "guardrail",
3145
3886
  agent: agent2._name,
3146
3887
  data: {
@@ -3161,6 +3902,15 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3161
3902
  });
3162
3903
  if (outputResult.block) {
3163
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
+ });
3164
3914
  guardrailOutputRetries++;
3165
3915
  appendRetryMessages(
3166
3916
  currentMessages,
@@ -3199,7 +3949,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3199
3949
  schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
3200
3950
  }
3201
3951
  }
3202
- this.emitTrace({
3952
+ this.emitEvent({
3203
3953
  type: "schema_check",
3204
3954
  agent: agent2._name,
3205
3955
  data: {
@@ -3218,6 +3968,15 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3218
3968
  });
3219
3969
  if (!schemaValid) {
3220
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
+ });
3221
3980
  schemaRetries++;
3222
3981
  appendRetryMessages(
3223
3982
  currentMessages,
@@ -3255,7 +4014,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3255
4014
  if (!validateResult.valid && validateRetries < maxValidateRetries) {
3256
4015
  validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
3257
4016
  }
3258
- this.emitTrace({
4017
+ this.emitEvent({
3259
4018
  type: "validate",
3260
4019
  agent: agent2._name,
3261
4020
  data: {
@@ -3274,6 +4033,15 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3274
4033
  });
3275
4034
  if (!validateResult.valid) {
3276
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
+ });
3277
4045
  validateRetries++;
3278
4046
  appendRetryMessages(
3279
4047
  currentMessages,
@@ -3291,6 +4059,14 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3291
4059
  );
3292
4060
  }
3293
4061
  }
4062
+ this.emitEvent({
4063
+ type: "pipeline",
4064
+ agent: agent2._name,
4065
+ status: "committed",
4066
+ stage: lastStartStage,
4067
+ attempt: lastStartAttempt,
4068
+ maxAttempts: lastStartMaxAttempts
4069
+ });
3294
4070
  this.pushAssistantToSessionHistory(content, response.providerMetadata);
3295
4071
  return validated ?? content;
3296
4072
  }
@@ -3419,43 +4195,100 @@ ${summaryResponse.content}`
3419
4195
  /**
3420
4196
  * Execute a function with checkpoint-replay semantics.
3421
4197
  *
3422
- * On first execution, runs `fn()`, saves the result, and returns it.
3423
- * On replay (resume after restart), returns the saved result without re-executing.
3424
- * 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.
3425
4212
  */
3426
- async checkpoint(fn) {
3427
- 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);
3428
4228
  }
3429
4229
  /**
3430
- * Internal checkpoint implementation shared by both the public checkpoint()
3431
- * 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.
3432
4240
  */
3433
- async _checkpoint(fn) {
3434
- const step = this.checkpointCounter++;
4241
+ async _checkpoint(name, fn, options) {
3435
4242
  if (!this.stateStore) {
3436
4243
  return fn();
3437
4244
  }
3438
- 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);
3439
4247
  if (saved !== null) {
3440
- this.emitTrace({
3441
- type: "log",
3442
- data: { event: "checkpoint_replay", step }
4248
+ this.emitEvent({
4249
+ type: "checkpoint_replay",
4250
+ ...agentStamp,
4251
+ data: { name }
3443
4252
  });
3444
- this.spanManager?.addEventToActiveSpan("axl.checkpoint.hit", { "axl.checkpoint.step": step });
4253
+ this.spanManager?.addEventToActiveSpan("axl.checkpoint.hit", { "axl.checkpoint.name": name });
3445
4254
  return saved;
3446
4255
  }
3447
4256
  const result = await fn();
3448
- await this.stateStore.saveCheckpoint(this.executionId, step, result);
3449
- this.emitTrace({
3450
- type: "log",
3451
- 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 }
3452
4262
  });
3453
- this.spanManager?.addEventToActiveSpan("axl.checkpoint.miss", { "axl.checkpoint.step": step });
4263
+ this.spanManager?.addEventToActiveSpan("axl.checkpoint.miss", { "axl.checkpoint.name": name });
3454
4264
  return result;
3455
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
+ }
3456
4289
  // ── ctx.spawn() ───────────────────────────────────────────────────────
3457
4290
  async spawn(n, fn, options) {
3458
- return this._checkpoint(() => {
4291
+ return this._checkpoint(this._autoCheckpointName("spawn"), () => {
3459
4292
  if (this.spanManager) {
3460
4293
  return this.spanManager.withSpanAsync(
3461
4294
  "axl.ctx.spawn",
@@ -3651,7 +4484,7 @@ ${summaryResponse.content}`
3651
4484
  const maxRetries = options?.retries ?? 3;
3652
4485
  let lastRetry = void 0;
3653
4486
  const emitVerifyOutcome = (passed, attempts, lastError) => {
3654
- this.emitTrace({
4487
+ this.emitEvent({
3655
4488
  type: "verify",
3656
4489
  agent: void 0,
3657
4490
  data: {
@@ -3791,7 +4624,7 @@ ${summaryResponse.content}`
3791
4624
  }
3792
4625
  // ── ctx.race() ────────────────────────────────────────────────────────
3793
4626
  async race(fns, options) {
3794
- return this._checkpoint(() => {
4627
+ return this._checkpoint(this._autoCheckpointName("race"), () => {
3795
4628
  if (this.spanManager) {
3796
4629
  return this.spanManager.withSpanAsync(
3797
4630
  "axl.ctx.race",
@@ -3896,11 +4729,17 @@ ${summaryResponse.content}`
3896
4729
  }
3897
4730
  // ── ctx.parallel() ────────────────────────────────────────────────────
3898
4731
  async parallel(fns) {
3899
- 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
+ );
3900
4736
  }
3901
4737
  // ── ctx.map() ─────────────────────────────────────────────────────────
3902
4738
  async map(items, fn, options) {
3903
- return this._checkpoint(() => this._mapImpl(items, fn, options));
4739
+ return this._checkpoint(
4740
+ this._autoCheckpointName("map"),
4741
+ () => this._mapImpl(items, fn, options)
4742
+ );
3904
4743
  }
3905
4744
  async _mapImpl(items, fn, options) {
3906
4745
  const concurrency = options?.concurrency ?? 5;
@@ -3978,10 +4817,14 @@ ${summaryResponse.content}`
3978
4817
  }
3979
4818
  async _awaitHumanImpl(options) {
3980
4819
  if (this.awaitHumanHandler) {
4820
+ this.emitEvent({
4821
+ type: "await_human",
4822
+ data: { channel: options.channel, prompt: options.prompt }
4823
+ });
3981
4824
  const decision2 = await this.awaitHumanHandler(options);
3982
- this.emitTrace({
3983
- type: "log",
3984
- 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 }
3985
4828
  });
3986
4829
  return decision2;
3987
4830
  }
@@ -4001,7 +4844,7 @@ ${summaryResponse.content}`
4001
4844
  await this.stateStore.saveExecutionState(this.executionId, {
4002
4845
  workflow: this.workflowName ?? "unknown",
4003
4846
  input: this.input,
4004
- step: this.stepCounter,
4847
+ step: this.stepRefRoot.value,
4005
4848
  status: "waiting",
4006
4849
  metadata: {
4007
4850
  ...this.metadata,
@@ -4010,18 +4853,22 @@ ${summaryResponse.content}`
4010
4853
  }
4011
4854
  });
4012
4855
  }
4013
- this.emitTrace({
4014
- type: "log",
4015
- 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 }
4016
4859
  });
4017
4860
  const decision = await new Promise((resolve) => {
4018
4861
  this.pendingDecisions.set(this.executionId, resolve);
4019
4862
  });
4863
+ this.emitEvent({
4864
+ type: "await_human_resolved",
4865
+ data: { channel: options.channel, decision }
4866
+ });
4020
4867
  if (this.stateStore) {
4021
4868
  await this.stateStore.saveExecutionState(this.executionId, {
4022
4869
  workflow: this.workflowName ?? "unknown",
4023
4870
  input: this.input,
4024
- step: this.stepCounter,
4871
+ step: this.stepRefRoot.value,
4025
4872
  status: "running"
4026
4873
  });
4027
4874
  }
@@ -4074,17 +4921,28 @@ ${summaryResponse.content}`
4074
4921
  // the previous `ctx.log('workflow_start', ...)` indirection — so consumers
4075
4922
  // that narrow via `event.type === 'workflow_start'` actually see them.
4076
4923
  // Internal: the runtime is the only caller, user workflows never call these.
4077
- /** @internal */
4924
+ /** @internal — idempotent; no-ops on second+ call within one ctx. */
4078
4925
  _emitWorkflowStart(input) {
4079
- this.emitTrace({
4926
+ if (this._workflowStartEmitted) return;
4927
+ this._workflowStartEmitted = true;
4928
+ this.emitEvent({
4080
4929
  type: "workflow_start",
4081
4930
  workflow: this.workflowName,
4082
4931
  data: { input }
4083
4932
  });
4084
4933
  }
4085
- /** @internal */
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. */
4086
4942
  _emitWorkflowEnd(info) {
4087
- this.emitTrace({
4943
+ if (this._workflowEndEmitted) return;
4944
+ this._workflowEndEmitted = true;
4945
+ this.emitEvent({
4088
4946
  type: "workflow_end",
4089
4947
  workflow: this.workflowName,
4090
4948
  duration: info.duration,
@@ -4099,7 +4957,7 @@ ${summaryResponse.content}`
4099
4957
  }
4100
4958
  // ── ctx.log() ─────────────────────────────────────────────────────────
4101
4959
  log(event, data) {
4102
- this.emitTrace({
4960
+ this.emitEvent({
4103
4961
  type: "log",
4104
4962
  data: {
4105
4963
  event,
@@ -4153,12 +5011,11 @@ ${summaryResponse.content}`
4153
5011
  if (usage?.cost != null) {
4154
5012
  this._accumulateBudgetCost(usage.cost);
4155
5013
  }
4156
- this.emitTrace({
4157
- type: "log",
5014
+ this.emitEvent({
5015
+ type: "memory_remember",
4158
5016
  ...usage?.cost != null ? { cost: usage.cost } : {},
4159
5017
  ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
4160
5018
  data: {
4161
- event: "memory_remember",
4162
5019
  key,
4163
5020
  scope,
4164
5021
  embed: options?.embed === true,
@@ -4170,12 +5027,11 @@ ${summaryResponse.content}`
4170
5027
  if (partialUsage?.cost != null) {
4171
5028
  this._accumulateBudgetCost(partialUsage.cost);
4172
5029
  }
4173
- this.emitTrace({
4174
- type: "log",
5030
+ this.emitEvent({
5031
+ type: "memory_remember",
4175
5032
  ...partialUsage?.cost != null ? { cost: partialUsage.cost } : {},
4176
5033
  ...partialUsage?.tokens != null ? { tokens: { input: partialUsage.tokens } } : {},
4177
5034
  data: {
4178
- event: "memory_remember",
4179
5035
  key,
4180
5036
  scope,
4181
5037
  embed: options?.embed === true,
@@ -4227,26 +5083,24 @@ ${summaryResponse.content}`
4227
5083
  if (usage?.cost != null) {
4228
5084
  this._accumulateBudgetCost(usage.cost);
4229
5085
  }
4230
- this.emitTrace({
4231
- type: "log",
5086
+ this.emitEvent({
5087
+ type: "memory_recall",
4232
5088
  ...usage?.cost != null ? { cost: usage.cost } : {},
4233
5089
  ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
4234
5090
  data: {
4235
- event: "memory_recall",
4236
5091
  key,
4237
5092
  scope,
4238
5093
  semantic,
4239
5094
  hit,
4240
- ...resultCount !== void 0 ? { resultCount } : {},
5095
+ ...resultCount !== void 0 ? { resultCount, count: resultCount } : {},
4241
5096
  ...usage ? { usage } : {}
4242
5097
  }
4243
5098
  });
4244
5099
  return data;
4245
5100
  } catch (err) {
4246
- this.emitTrace({
4247
- type: "log",
5101
+ this.emitEvent({
5102
+ type: "memory_recall",
4248
5103
  data: {
4249
- event: "memory_recall",
4250
5104
  key,
4251
5105
  scope,
4252
5106
  semantic,
@@ -4271,15 +5125,14 @@ ${summaryResponse.content}`
4271
5125
  const scope = options?.scope ?? (sessionId ? "session" : "global");
4272
5126
  try {
4273
5127
  await this.memoryManager.forget(key, this.stateStore, sessionId, options);
4274
- this.emitTrace({
4275
- type: "log",
4276
- data: { event: "memory_forget", key, scope }
5128
+ this.emitEvent({
5129
+ type: "memory_forget",
5130
+ data: { key, scope }
4277
5131
  });
4278
5132
  } catch (err) {
4279
- this.emitTrace({
4280
- type: "log",
5133
+ this.emitEvent({
5134
+ type: "memory_forget",
4281
5135
  data: {
4282
- event: "memory_forget",
4283
5136
  key,
4284
5137
  scope,
4285
5138
  error: err instanceof Error ? err.message : String(err)
@@ -4314,7 +5167,7 @@ ${summaryResponse.content}`
4314
5167
  names.add(a._name);
4315
5168
  }
4316
5169
  if (agents.length === 1) {
4317
- this.emitTrace({
5170
+ this.emitEvent({
4318
5171
  type: "delegate",
4319
5172
  agent: agents[0]._name,
4320
5173
  data: {
@@ -4358,7 +5211,7 @@ ${summaryResponse.content}`
4358
5211
  resolveModel: () => routerModelUri,
4359
5212
  resolveSystem: () => routerSystem
4360
5213
  };
4361
- this.emitTrace({
5214
+ this.emitEvent({
4362
5215
  type: "delegate",
4363
5216
  agent: "_delegate_router",
4364
5217
  data: {
@@ -4379,107 +5232,42 @@ ${summaryResponse.content}`
4379
5232
  /**
4380
5233
  * Internal emitter input — intentionally loose so call sites don't need to
4381
5234
  * build a perfectly-narrowed discriminated-union member. The resulting
4382
- * `TraceEvent` (exported type) remains strict, and TypeScript narrows it at
5235
+ * `AxlEvent` (exported type) remains strict, and TypeScript narrows it at
4383
5236
  * consumer sites via the `type` discriminator.
4384
5237
  */
4385
- emitTrace(partial) {
4386
- let data = partial.data;
4387
- if (this.config.trace?.redact && data) {
4388
- if (partial.type === "agent_call") {
4389
- const d = data;
4390
- const redacted = {
4391
- ...d,
4392
- prompt: "[redacted]",
4393
- response: "[redacted]"
4394
- };
4395
- if (d.system !== void 0) redacted.system = "[redacted]";
4396
- if (d.thinking !== void 0) redacted.thinking = "[redacted]";
4397
- if (Array.isArray(d.messages)) {
4398
- redacted.messages = [
4399
- { role: "system", content: `[${d.messages.length} messages redacted]` }
4400
- ];
4401
- }
4402
- data = redacted;
4403
- } else if (partial.type === "guardrail" || partial.type === "schema_check" || partial.type === "validate") {
4404
- const d = data;
4405
- const needsRedact = d.feedbackMessage !== void 0 || d.reason !== void 0;
4406
- if (needsRedact) {
4407
- data = {
4408
- ...d,
4409
- ...d.reason !== void 0 ? { reason: "[redacted]" } : {},
4410
- ...d.feedbackMessage !== void 0 ? { feedbackMessage: "[redacted]" } : {}
4411
- };
4412
- }
4413
- } else if (partial.type === "tool_call") {
4414
- const d = data;
4415
- data = {
4416
- ...d,
4417
- args: "[redacted]",
4418
- result: "[redacted]"
4419
- };
4420
- } else if (partial.type === "tool_approval") {
4421
- const d = data;
4422
- data = {
4423
- ...d,
4424
- args: "[redacted]",
4425
- ...d.reason !== void 0 ? { reason: "[redacted]" } : {}
4426
- };
4427
- } else if (partial.type === "handoff") {
4428
- const d = data;
4429
- if (d.message !== void 0) {
4430
- data = { ...d, message: "[redacted]" };
4431
- }
4432
- } else if (partial.type === "workflow_start") {
4433
- const d = data;
4434
- if (d.input !== void 0) {
4435
- data = { ...d, input: "[redacted]" };
4436
- }
4437
- } else if (partial.type === "workflow_end") {
4438
- const d = data;
4439
- const redacted = { ...d };
4440
- if (d.result !== void 0) redacted.result = "[redacted]";
4441
- if (d.error !== void 0) redacted.error = "[redacted]";
4442
- data = redacted;
4443
- } else if (partial.type === "log") {
4444
- const d = data;
4445
- const redacted = {};
4446
- for (const [k, v] of Object.entries(d)) {
4447
- if (k === "event" || typeof v === "number" || typeof v === "boolean") {
4448
- redacted[k] = v;
4449
- } else if (typeof v === "string") {
4450
- redacted[k] = "[redacted]";
4451
- } else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
4452
- const inner = v;
4453
- const innerRedacted = {};
4454
- for (const [ik, iv] of Object.entries(inner)) {
4455
- if (typeof iv === "number" || typeof iv === "boolean") {
4456
- innerRedacted[ik] = iv;
4457
- } else if (typeof iv === "string") {
4458
- innerRedacted[ik] = "[redacted]";
4459
- } else {
4460
- innerRedacted[ik] = "[redacted]";
4461
- }
4462
- }
4463
- redacted[k] = innerRedacted;
4464
- } else {
4465
- redacted[k] = "[redacted]";
4466
- }
4467
- }
4468
- data = redacted;
4469
- }
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;
4470
5245
  }
4471
5246
  const event = {
4472
5247
  executionId: this.executionId,
4473
- step: this.stepCounter++,
5248
+ step,
4474
5249
  timestamp: Date.now(),
4475
5250
  ...this.workflowName ? { workflow: this.workflowName } : {},
4476
- ...this.parentToolCallId ? { parentToolCallId: this.parentToolCallId } : {},
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 },
4477
5264
  ...partial,
4478
5265
  data
4479
5266
  };
5267
+ const finalEvent = this.config.trace?.redact ? redactEvent(event) : event;
4480
5268
  if (this.onTrace) {
4481
5269
  try {
4482
- this.onTrace(event);
5270
+ this.onTrace(finalEvent);
4483
5271
  } catch (err) {
4484
5272
  console.error(
4485
5273
  "[axl] onTrace handler threw; trace event dropped:",
@@ -4491,7 +5279,7 @@ ${summaryResponse.content}`
4491
5279
  };
4492
5280
 
4493
5281
  // src/agent.ts
4494
- var import_node_crypto = require("crypto");
5282
+ var import_node_crypto2 = require("crypto");
4495
5283
  var agentCounter = 0;
4496
5284
  function agent(config) {
4497
5285
  agentCounter++;
@@ -4511,7 +5299,7 @@ function agent(config) {
4511
5299
  const registry = new ProviderRegistry();
4512
5300
  const ctx = new WorkflowContext({
4513
5301
  input: prompt,
4514
- executionId: (0, import_node_crypto.randomUUID)(),
5302
+ executionId: (0, import_node_crypto2.randomUUID)(),
4515
5303
  config: {},
4516
5304
  providerRegistry: registry
4517
5305
  });
@@ -4535,7 +5323,7 @@ function workflow(config) {
4535
5323
 
4536
5324
  // src/runtime.ts
4537
5325
  var import_node_events2 = require("events");
4538
- var import_node_crypto2 = require("crypto");
5326
+ var import_node_crypto3 = require("crypto");
4539
5327
  var import_node_async_hooks2 = require("async_hooks");
4540
5328
  var import_node_fs2 = require("fs");
4541
5329
  var import_node_path2 = require("path");
@@ -4558,29 +5346,20 @@ var MemoryStore = class {
4558
5346
  constructor() {
4559
5347
  this.loadPersistedState();
4560
5348
  }
4561
- async saveCheckpoint(executionId, step, data) {
4562
- let steps = this.checkpoints.get(executionId);
4563
- if (!steps) {
4564
- steps = /* @__PURE__ */ new Map();
4565
- 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);
4566
5354
  }
4567
- steps.set(step, structuredClone(data));
5355
+ entries.set(name, structuredClone(data));
4568
5356
  }
4569
- async getCheckpoint(executionId, step) {
4570
- const steps = this.checkpoints.get(executionId);
4571
- if (!steps) return null;
4572
- 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);
4573
5361
  return data !== void 0 ? structuredClone(data) : null;
4574
5362
  }
4575
- async getLatestCheckpoint(executionId) {
4576
- const steps = this.checkpoints.get(executionId);
4577
- if (!steps || steps.size === 0) return null;
4578
- let maxStep = -1;
4579
- for (const step of steps.keys()) {
4580
- if (step > maxStep) maxStep = step;
4581
- }
4582
- return { step: maxStep, data: structuredClone(steps.get(maxStep)) };
4583
- }
4584
5363
  async saveSession(sessionId, history) {
4585
5364
  this.sessions.set(sessionId, structuredClone(history));
4586
5365
  }
@@ -4753,7 +5532,7 @@ function rowToExecutionInfo(row) {
4753
5532
  completedAt: row.completed_at ?? void 0,
4754
5533
  duration: row.duration,
4755
5534
  error: row.error ?? void 0,
4756
- steps: safeJsonParse(row.steps) ?? []
5535
+ events: safeJsonParse(row.events) ?? []
4757
5536
  };
4758
5537
  }
4759
5538
  var SQLiteStore = class {
@@ -4769,15 +5548,80 @@ var SQLiteStore = class {
4769
5548
  }
4770
5549
  this.db = new BetterSqlite3(filePath);
4771
5550
  this.db.pragma("journal_mode = WAL");
5551
+ this.migrate();
4772
5552
  this.initTables();
4773
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
+ }
4774
5618
  initTables() {
4775
5619
  this.db.exec(`
4776
5620
  CREATE TABLE IF NOT EXISTS checkpoints (
4777
5621
  execution_id TEXT NOT NULL,
4778
- step INTEGER NOT NULL,
5622
+ name TEXT NOT NULL,
4779
5623
  data TEXT NOT NULL,
4780
- PRIMARY KEY (execution_id, step)
5624
+ PRIMARY KEY (execution_id, name)
4781
5625
  );
4782
5626
 
4783
5627
  CREATE TABLE IF NOT EXISTS sessions (
@@ -4826,7 +5670,7 @@ var SQLiteStore = class {
4826
5670
  completed_at INTEGER,
4827
5671
  duration INTEGER NOT NULL DEFAULT 0,
4828
5672
  error TEXT,
4829
- steps TEXT NOT NULL
5673
+ events TEXT NOT NULL
4830
5674
  );
4831
5675
 
4832
5676
  CREATE TABLE IF NOT EXISTS eval_history (
@@ -4841,26 +5685,19 @@ var SQLiteStore = class {
4841
5685
  `);
4842
5686
  }
4843
5687
  // ── Checkpoints ────────────────────────────────────────────────────────
4844
- async saveCheckpoint(executionId, step, data) {
5688
+ async saveCheckpoint(executionId, name, data) {
4845
5689
  const stmt = this.db.prepare(
4846
- "INSERT OR REPLACE INTO checkpoints (execution_id, step, data) VALUES (?, ?, ?)"
5690
+ "INSERT OR REPLACE INTO checkpoints (execution_id, name, data) VALUES (?, ?, ?)"
4847
5691
  );
4848
- stmt.run(executionId, step, JSON.stringify(data));
5692
+ stmt.run(executionId, name, JSON.stringify(data));
4849
5693
  }
4850
- async getCheckpoint(executionId, step) {
5694
+ async getCheckpoint(executionId, name) {
4851
5695
  const stmt = this.db.prepare(
4852
- "SELECT data FROM checkpoints WHERE execution_id = ? AND step = ?"
5696
+ "SELECT data FROM checkpoints WHERE execution_id = ? AND name = ?"
4853
5697
  );
4854
- const row = stmt.get(executionId, step);
5698
+ const row = stmt.get(executionId, name);
4855
5699
  return row ? safeJsonParse(row.data) : null;
4856
5700
  }
4857
- async getLatestCheckpoint(executionId) {
4858
- const stmt = this.db.prepare(
4859
- "SELECT step, data FROM checkpoints WHERE execution_id = ? ORDER BY step DESC LIMIT 1"
4860
- );
4861
- const row = stmt.get(executionId);
4862
- return row ? { step: row.step, data: safeJsonParse(row.data) } : null;
4863
- }
4864
5701
  // ── Sessions ────────────────────────────────────────────────────────────
4865
5702
  async saveSession(sessionId, history) {
4866
5703
  const stmt = this.db.prepare(
@@ -4954,7 +5791,7 @@ var SQLiteStore = class {
4954
5791
  // ── Execution History ────────────────────────────────────────────────────
4955
5792
  async saveExecution(execution) {
4956
5793
  this.db.prepare(
4957
- "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 (?, ?, ?, ?, ?, ?, ?, ?, ?)"
4958
5795
  ).run(
4959
5796
  execution.executionId,
4960
5797
  execution.workflow,
@@ -4964,7 +5801,7 @@ var SQLiteStore = class {
4964
5801
  execution.completedAt ?? null,
4965
5802
  execution.duration,
4966
5803
  execution.error ?? null,
4967
- JSON.stringify(execution.steps)
5804
+ JSON.stringify(execution.events)
4968
5805
  );
4969
5806
  }
4970
5807
  async getExecution(executionId) {
@@ -5138,7 +5975,8 @@ var Session = class _Session {
5138
5975
  }
5139
5976
  };
5140
5977
  axlStream.on("done", (event) => {
5141
- updateHistory(event.data).catch((err) => {
5978
+ const data = event.data;
5979
+ updateHistory(data.result).catch((err) => {
5142
5980
  this.runtime.emit("error", {
5143
5981
  type: "session_history_save_failed",
5144
5982
  sessionId: this.sessionId,
@@ -5180,9 +6018,88 @@ var Session = class _Session {
5180
6018
  // src/stream.ts
5181
6019
  var import_node_stream = require("stream");
5182
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
5183
6078
  var AxlStream = class _AxlStream extends import_node_stream.Readable {
5184
6079
  bus = new import_node_events.EventEmitter();
5185
- 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();
5186
6103
  result = void 0;
5187
6104
  finished = false;
5188
6105
  promise;
@@ -5204,18 +6121,10 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
5204
6121
  this.promise.catch(() => {
5205
6122
  });
5206
6123
  }
5207
- static STREAM_EVENTS = /* @__PURE__ */ new Set([
5208
- "token",
5209
- "tool_call",
5210
- "tool_result",
5211
- "tool_approval",
5212
- "agent_start",
5213
- "agent_end",
5214
- "handoff",
5215
- "step",
5216
- "done",
5217
- "error"
5218
- ]);
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);
5219
6128
  on(event, handler) {
5220
6129
  if (_AxlStream.STREAM_EVENTS.has(event)) {
5221
6130
  this.bus.on(event, handler);
@@ -5265,21 +6174,83 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
5265
6174
  while (true) {
5266
6175
  const { value, done } = await iter.next();
5267
6176
  if (done) return { value: void 0, done: true };
5268
- 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
+ }
5269
6218
  }
5270
6219
  }
5271
6220
  };
5272
6221
  }
5273
6222
  };
5274
6223
  }
5275
- get steps() {
5276
- const stepTypes = /* @__PURE__ */ new Set([
5277
- "agent_start",
5278
- "agent_end",
5279
- "tool_call",
5280
- "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",
5281
6239
  "tool_approval",
5282
- "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"
5283
6254
  ]);
5284
6255
  const self = this;
5285
6256
  return {
@@ -5290,7 +6261,7 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
5290
6261
  while (true) {
5291
6262
  const { value, done } = await iter.next();
5292
6263
  if (done) return { value: void 0, done: true };
5293
- if (stepTypes.has(value.type)) return { value, done: false };
6264
+ if (lifecycleTypes.has(value.type)) return { value, done: false };
5294
6265
  }
5295
6266
  }
5296
6267
  };
@@ -5310,10 +6281,34 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
5310
6281
  });
5311
6282
  return destination;
5312
6283
  }
5313
- /** Push a stream event. Called by the runtime. */
6284
+ /** Push an event onto the stream. Called by the runtime. */
5314
6285
  _push(event) {
5315
6286
  if (this.finished) return;
5316
- 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
+ }
5317
6312
  this.bus.emit(event.type, event);
5318
6313
  this.push(event);
5319
6314
  const waiter = this.waiters.shift();
@@ -5323,12 +6318,25 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
5323
6318
  this.eventQueue.push(event);
5324
6319
  }
5325
6320
  }
5326
- /** Signal successful completion. */
5327
- _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) {
5328
6330
  if (this.finished) return;
5329
6331
  this.finished = true;
5330
6332
  this.result = result;
5331
- 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
+ };
5332
6340
  this.bus.emit("done", doneEvent);
5333
6341
  this.push(doneEvent);
5334
6342
  this.push(null);
@@ -5344,11 +6352,19 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
5344
6352
  this.waiters.length = 0;
5345
6353
  this.bus.emit("__resolve", result);
5346
6354
  }
5347
- /** Signal an error. */
5348
- _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) {
5349
6359
  if (this.finished) return;
5350
6360
  this.finished = true;
5351
- 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
+ };
5352
6368
  this.bus.emit("error", errorEvent);
5353
6369
  this.push(errorEvent);
5354
6370
  this.push(null);
@@ -5364,8 +6380,31 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
5364
6380
  this.waiters.length = 0;
5365
6381
  this.bus.emit("__reject", error);
5366
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`. */
5367
6396
  get fullText() {
5368
- 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;
5369
6408
  }
5370
6409
  };
5371
6410
 
@@ -5912,6 +6951,32 @@ function hashInput(input) {
5912
6951
  }
5913
6952
  return (hash >>> 0).toString(16);
5914
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
+ }
5915
6980
  var costScopeStorage = new import_node_async_hooks2.AsyncLocalStorage();
5916
6981
  var AxlRuntime = class extends import_node_events2.EventEmitter {
5917
6982
  config;
@@ -5931,11 +6996,27 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5931
6996
  historicalExecutionsLoadPromise = null;
5932
6997
  evalHistory = [];
5933
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;
5934
7003
  constructor(config) {
5935
7004
  super();
5936
7005
  this.config = resolveConfig(config ?? {});
5937
7006
  this.providerRegistry = new ProviderRegistry();
5938
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
+ }
5939
7020
  if (this.config.memory) {
5940
7021
  this.memoryManager = new MemoryManager({
5941
7022
  vectorStore: this.config.memory.vectorStore,
@@ -5991,6 +7072,71 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5991
7072
  return new MemoryStore();
5992
7073
  }
5993
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
+ }
5994
7140
  /**
5995
7141
  * Persist a completed/failed execution to the state store (fire-and-forget)
5996
7142
  * and move it from the active executions map to the historical cache.
@@ -6006,7 +7152,6 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6006
7152
  this.executions.delete(id);
6007
7153
  }
6008
7154
  /** Register a workflow with the runtime. */
6009
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
6010
7155
  register(workflow2) {
6011
7156
  this.workflows.set(workflow2.name, workflow2);
6012
7157
  }
@@ -6132,7 +7277,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6132
7277
  }
6133
7278
  try {
6134
7279
  await this.saveEvalResult({
6135
- id: resultObj.id ?? (0, import_node_crypto2.randomUUID)(),
7280
+ id: resultObj.id ?? (0, import_node_crypto3.randomUUID)(),
6136
7281
  eval: name,
6137
7282
  timestamp: Date.now(),
6138
7283
  data: structuredClone(result)
@@ -6169,7 +7314,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6169
7314
  * and automatically emits trace events and tracks cost.
6170
7315
  */
6171
7316
  createContext(options) {
6172
- const executionId = (0, import_node_crypto2.randomUUID)();
7317
+ const executionId = (0, import_node_crypto3.randomUUID)();
6173
7318
  const budgetLimit = options?.budget ? parseCost(options.budget) : Infinity;
6174
7319
  this.registerWithCostScope(executionId);
6175
7320
  return new WorkflowContext({
@@ -6185,10 +7330,12 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6185
7330
  sessionHistory: options?.sessionHistory,
6186
7331
  signal: options?.signal,
6187
7332
  onToken: options?.onToken,
7333
+ onToolCall: options?.onToolCall,
7334
+ onAgentStart: options?.onAgentStart,
6188
7335
  awaitHumanHandler: options?.awaitHumanHandler,
6189
7336
  onTrace: (event) => {
6190
7337
  this.emit("trace", event);
6191
- this.outputTraceEvent(event);
7338
+ this.outputAxlEvent(event);
6192
7339
  },
6193
7340
  budgetContext: {
6194
7341
  totalCost: 0,
@@ -6215,7 +7362,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6215
7362
  );
6216
7363
  }
6217
7364
  const validated = workflow2.inputSchema.parse(input);
6218
- const executionId = (0, import_node_crypto2.randomUUID)();
7365
+ const executionId = (0, import_node_crypto3.randomUUID)();
6219
7366
  const controller = new AbortController();
6220
7367
  this.abortControllers.set(executionId, controller);
6221
7368
  this.registerWithCostScope(executionId);
@@ -6223,7 +7370,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6223
7370
  executionId,
6224
7371
  workflow: name,
6225
7372
  status: "running",
6226
- steps: [],
7373
+ events: [],
6227
7374
  totalCost: 0,
6228
7375
  startedAt: Date.now(),
6229
7376
  duration: 0
@@ -6239,20 +7386,35 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6239
7386
  sessionHistory,
6240
7387
  signal: controller.signal,
6241
7388
  onTrace: (event) => {
6242
- execInfo.steps.push(event);
6243
- if (event.cost) execInfo.totalCost += event.cost;
7389
+ pushEventBounded(execInfo, event, this.maxEventsPerExecution);
7390
+ execInfo.totalCost += eventCostContribution(event);
6244
7391
  this.emit("trace", event);
6245
- this.outputTraceEvent(event);
6246
- if (event.type === "handoff") {
7392
+ this.outputAxlEvent(event);
7393
+ if (event.type === "handoff_start") {
6247
7394
  const sessionId = options?.metadata?.sessionId;
6248
7395
  if (sessionId) {
6249
- const data = event.data;
6250
7396
  this.appendHandoffRecord(sessionId, {
6251
- source: event.agent ?? "",
6252
- target: data?.target ?? "",
6253
- mode: data?.mode ?? "oneway",
7397
+ source: event.data.source,
7398
+ target: event.data.target,
7399
+ mode: event.data.mode,
6254
7400
  timestamp: event.timestamp,
6255
- 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
6256
7418
  });
6257
7419
  }
6258
7420
  }
@@ -6279,45 +7441,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6279
7441
  "axl.execution.id": executionId,
6280
7442
  "axl.workflow.input_hash": hashInput(validated)
6281
7443
  },
6282
- async (span) => {
6283
- try {
6284
- ctx._emitWorkflowStart(validated);
6285
- const result = await workflow2.handler(ctx);
6286
- const output = workflow2.outputSchema ? workflow2.outputSchema.parse(result) : result;
6287
- execInfo.status = "completed";
6288
- execInfo.completedAt = Date.now();
6289
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
6290
- execInfo.result = output;
6291
- ctx._emitWorkflowEnd({
6292
- status: "completed",
6293
- duration: execInfo.duration,
6294
- result: output
6295
- });
6296
- if (this.stateStore.deleteCheckpoints) {
6297
- await this.stateStore.deleteCheckpoints(executionId);
6298
- }
6299
- span.setAttribute("axl.workflow.cost", execInfo.totalCost);
6300
- span.setAttribute("axl.workflow.duration", execInfo.duration);
6301
- this.persistExecution(execInfo);
6302
- return output;
6303
- } catch (err) {
6304
- const aborted = typeof err === "object" && err !== null && err.name === "AbortError";
6305
- execInfo.status = "failed";
6306
- execInfo.completedAt = Date.now();
6307
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
6308
- execInfo.error = err instanceof Error ? err.message : String(err);
6309
- ctx._emitWorkflowEnd({
6310
- status: "failed",
6311
- duration: execInfo.duration,
6312
- error: execInfo.error,
6313
- ...aborted ? { aborted: true } : {}
6314
- });
6315
- this.persistExecution(execInfo);
6316
- throw err;
6317
- } finally {
6318
- this.abortControllers.delete(executionId);
6319
- }
6320
- }
7444
+ (span) => this.runWorkflowBody({
7445
+ workflow: workflow2,
7446
+ ctx,
7447
+ execInfo,
7448
+ validated,
7449
+ span
7450
+ })
6321
7451
  );
6322
7452
  }
6323
7453
  /** Execute a workflow and return a stream. */
@@ -6325,21 +7455,20 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6325
7455
  const axlStream = new AxlStream();
6326
7456
  const controller = new AbortController();
6327
7457
  axlStream.on("close", () => controller.abort());
7458
+ const executionId = (0, import_node_crypto3.randomUUID)();
7459
+ this.abortControllers.set(executionId, controller);
6328
7460
  let execInfo;
6329
- let ctx;
6330
7461
  const run = async () => {
6331
7462
  const workflow2 = this.workflows.get(name);
6332
7463
  if (!workflow2) throw new Error(`Workflow "${name}" not registered`);
6333
7464
  const validated = workflow2.inputSchema.parse(input);
6334
- const executionId = (0, import_node_crypto2.randomUUID)();
6335
- this.abortControllers.set(executionId, controller);
6336
7465
  const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
6337
7466
  this.registerWithCostScope(executionId);
6338
7467
  execInfo = {
6339
7468
  executionId,
6340
7469
  workflow: name,
6341
7470
  status: "running",
6342
- steps: [],
7471
+ events: [],
6343
7472
  totalCost: 0,
6344
7473
  startedAt: Date.now(),
6345
7474
  duration: 0
@@ -6353,69 +7482,46 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6353
7482
  providerRegistry: this.providerRegistry,
6354
7483
  sessionHistory,
6355
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
+ },
6356
7491
  onTrace: (event) => {
6357
- execInfo.steps.push(event);
6358
- if (event.cost) execInfo.totalCost += event.cost;
7492
+ pushEventBounded(execInfo, event, this.maxEventsPerExecution);
7493
+ execInfo.totalCost += eventCostContribution(event);
6359
7494
  this.emit("trace", event);
6360
- this.outputTraceEvent(event);
6361
- if (event.type === "handoff") {
6362
- const data = event.data;
6363
- axlStream._push({
6364
- type: "handoff",
6365
- source: event.agent ?? "",
6366
- target: data?.target ?? "",
6367
- mode: data?.mode ?? void 0
6368
- });
7495
+ this.outputAxlEvent(event);
7496
+ axlStream._push(event);
7497
+ if (event.type === "handoff_start") {
6369
7498
  const sessionId = options?.metadata?.sessionId;
6370
7499
  if (sessionId) {
6371
7500
  this.appendHandoffRecord(sessionId, {
6372
- source: event.agent ?? "",
6373
- target: data?.target ?? "",
6374
- mode: data?.mode ?? "oneway",
7501
+ source: event.data.source,
7502
+ target: event.data.target,
7503
+ mode: event.data.mode,
6375
7504
  timestamp: event.timestamp,
6376
- duration: data?.duration ?? void 0
7505
+ toAskId: event.toAskId
7506
+ });
7507
+ }
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
7514
+ });
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
6377
7522
  });
6378
7523
  }
6379
- } else if (event.type === "tool_approval") {
6380
- const data = event.data;
6381
- axlStream._push({
6382
- type: "tool_approval",
6383
- name: event.tool ?? "",
6384
- args: data?.args,
6385
- approved: data?.approved === true,
6386
- ...data?.reason ? { reason: data.reason } : {}
6387
- });
6388
- } else if (event.type === "tool_denied") {
6389
- } else if (event.type === "agent_call") {
6390
- axlStream._push({
6391
- type: "agent_end",
6392
- agent: event.agent ?? "",
6393
- cost: event.cost,
6394
- duration: event.duration
6395
- });
6396
- } else if (event.type === "tool_call") {
6397
- axlStream._push({
6398
- type: "tool_result",
6399
- name: event.tool ?? "",
6400
- result: event.data?.result,
6401
- callId: event.data?.callId
6402
- });
6403
7524
  }
6404
- axlStream._push({ type: "step", step: event.step, data: event });
6405
- },
6406
- onToken: (token) => {
6407
- axlStream._push({ type: "token", data: token });
6408
- },
6409
- onToolCall: (call) => {
6410
- axlStream._push({
6411
- type: "tool_call",
6412
- name: call.name,
6413
- args: call.args,
6414
- callId: call.callId
6415
- });
6416
- },
6417
- onAgentStart: (info) => {
6418
- axlStream._push({ type: "agent_start", agent: info.agent, model: info.model });
6419
7525
  },
6420
7526
  pendingDecisions: this.pendingDecisionResolvers,
6421
7527
  awaitHumanHandler: options?.awaitHumanHandler,
@@ -6432,7 +7538,6 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6432
7538
  policy: "finish_and_stop"
6433
7539
  }
6434
7540
  });
6435
- ctx = wfCtx;
6436
7541
  return this.spanManager.withSpanAsync(
6437
7542
  "axl.workflow.execute",
6438
7543
  {
@@ -6440,49 +7545,25 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6440
7545
  "axl.execution.id": executionId,
6441
7546
  "axl.workflow.input_hash": hashInput(validated)
6442
7547
  },
6443
- async (span) => {
6444
- try {
6445
- wfCtx._emitWorkflowStart(validated);
6446
- const rawResult = await workflow2.handler(wfCtx);
6447
- const result = workflow2.outputSchema ? workflow2.outputSchema.parse(rawResult) : rawResult;
6448
- execInfo.status = "completed";
6449
- execInfo.completedAt = Date.now();
6450
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
6451
- execInfo.result = result;
6452
- wfCtx._emitWorkflowEnd({
6453
- status: "completed",
6454
- duration: execInfo.duration,
6455
- result
6456
- });
6457
- if (this.stateStore.deleteCheckpoints) {
6458
- await this.stateStore.deleteCheckpoints(executionId);
6459
- }
6460
- span.setAttribute("axl.workflow.cost", execInfo.totalCost);
6461
- span.setAttribute("axl.workflow.duration", execInfo.duration);
6462
- this.persistExecution(execInfo);
6463
- return result;
6464
- } finally {
6465
- this.abortControllers.delete(executionId);
6466
- }
6467
- }
7548
+ (span) => this.runWorkflowBody({
7549
+ workflow: workflow2,
7550
+ ctx: wfCtx,
7551
+ execInfo,
7552
+ validated,
7553
+ span
7554
+ })
6468
7555
  );
6469
7556
  };
6470
- run().then((result) => axlStream._done(result)).catch((err) => {
6471
- if (execInfo) {
6472
- const aborted = typeof err === "object" && err !== null && err.name === "AbortError";
7557
+ run().then((result) => axlStream._done(result, executionId)).catch((err) => {
7558
+ if (execInfo && execInfo.status === "running") {
6473
7559
  execInfo.status = "failed";
6474
7560
  execInfo.completedAt = Date.now();
6475
7561
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
6476
7562
  execInfo.error = err instanceof Error ? err.message : String(err);
6477
- ctx?._emitWorkflowEnd({
6478
- status: "failed",
6479
- duration: execInfo.duration,
6480
- error: execInfo.error,
6481
- ...aborted ? { aborted: true } : {}
6482
- });
6483
7563
  this.persistExecution(execInfo);
6484
7564
  }
6485
- 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);
6486
7567
  });
6487
7568
  return axlStream;
6488
7569
  }
@@ -6742,11 +7823,11 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6742
7823
  * need embedder token counts, subscribe to `runtime.on('trace', ...)` and
6743
7824
  * read `data.usage.tokens` on `memory_remember` / `memory_recall` events.
6744
7825
  *
6745
- * Pass `{ captureTraces: true }` to also collect the raw `TraceEvent[]` observed
7826
+ * Pass `{ captureTraces: true }` to also collect the raw `AxlEvent[]` observed
6746
7827
  * during `fn()`. This is opt-in because it keeps every event in memory for the
6747
7828
  * duration of the call — useful for eval per-item capture, debugging, and test
6748
7829
  * assertions, but overhead grows with trace volume. When enabled, verbose-mode
6749
- * `agent_call.data.messages` snapshots are omitted from captured events (still
7830
+ * `agent_call_start.data.messages` snapshots are omitted from captured events (still
6750
7831
  * broadcast via onTrace) to keep memory bounded — callers who need the full
6751
7832
  * verbose snapshot should subscribe to `runtime.on('trace', ...)` directly.
6752
7833
  *
@@ -6766,8 +7847,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6766
7847
  const capturedTraces = options?.captureTraces ? [] : void 0;
6767
7848
  const listener = (event) => {
6768
7849
  if (!scope.trackedIds.has(event.executionId)) return;
6769
- if (event.cost) scope.totalCost += event.cost;
6770
- if (event.type === "agent_call") {
7850
+ scope.totalCost += eventCostContribution(event);
7851
+ if (event.type === "agent_call_end") {
6771
7852
  if (event.model) modelCalls.set(event.model, (modelCalls.get(event.model) ?? 0) + 1);
6772
7853
  agentCalls++;
6773
7854
  if (event.tokens) {
@@ -6780,7 +7861,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6780
7861
  workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
6781
7862
  }
6782
7863
  if (capturedTraces) {
6783
- if (event.type === "agent_call" && event.data) {
7864
+ if (event.type === "token" || event.type === "partial_object") return;
7865
+ if (event.type === "agent_call_start" && event.data) {
6784
7866
  const d = event.data;
6785
7867
  if ("messages" in d) {
6786
7868
  const rest = {};
@@ -6845,7 +7927,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6845
7927
  * The emit('trace', event) call happens before this method is called, so
6846
7928
  * programmatic subscribers always receive events regardless of trace config.
6847
7929
  */
6848
- outputTraceEvent(event) {
7930
+ outputAxlEvent(event) {
6849
7931
  const traceConfig = this.config.trace;
6850
7932
  if (!traceConfig?.enabled) return;
6851
7933
  const level = traceConfig.level ?? "steps";
@@ -6869,9 +7951,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6869
7951
  }
6870
7952
  return;
6871
7953
  }
6872
- this.logTraceEvent(event);
7954
+ this.logAxlEvent(event);
6873
7955
  }
6874
- logTraceEvent(event) {
7956
+ logAxlEvent(event) {
6875
7957
  const level = this.config.trace?.level ?? "steps";
6876
7958
  const workflowPrefix = event.workflow ? `workflow:${event.workflow} | ` : "";
6877
7959
  const parts = [`[axl] execution:${event.executionId}`];
@@ -6882,7 +7964,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6882
7964
  const status = d?.aborted ? "aborted" : d?.status ?? "completed";
6883
7965
  parts.push(`${workflowPrefix}${status}`);
6884
7966
  if (event.duration != null) parts.push(`${(event.duration / 1e3).toFixed(1)}s`);
6885
- } else if (event.type === "agent_call") {
7967
+ } else if (event.type === "agent_call_end") {
6886
7968
  parts.push(`${workflowPrefix}step:${event.step} agent_call`);
6887
7969
  if (event.agent) parts.push(`agent:${event.agent}`);
6888
7970
  if (event.promptVersion) parts.push(`version:${event.promptVersion}`);
@@ -6892,7 +7974,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6892
7974
  if (level === "full" && event.data) {
6893
7975
  parts.push(`data:${JSON.stringify(event.data)}`);
6894
7976
  }
6895
- } else if (event.type === "tool_call") {
7977
+ } else if (event.type === "tool_call_end") {
6896
7978
  parts.push(`${workflowPrefix}step:${event.step} tool_call`);
6897
7979
  if (event.tool) parts.push(`tool:${event.tool}`);
6898
7980
  if (event.duration) parts.push(`${event.duration}ms`);
@@ -6909,8 +7991,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6909
7991
  parts.push(`${workflowPrefix}log: ${JSON.stringify(event.data)}`);
6910
7992
  } else {
6911
7993
  parts.push(`${workflowPrefix}${event.type}`);
6912
- if (level === "full" && event.data) {
6913
- 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)}`);
6914
7997
  }
6915
7998
  }
6916
7999
  console.log(parts.join(" | "));
@@ -6930,6 +8013,35 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6930
8013
  }).catch(() => {
6931
8014
  });
6932
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
+ }
6933
8045
  };
6934
8046
 
6935
8047
  // src/state/redis.ts
@@ -6992,27 +8104,13 @@ var RedisStore = class _RedisStore {
6992
8104
  return "axl:eval-history-ids";
6993
8105
  }
6994
8106
  // ── Checkpoints ──────────────────────────────────────────────────────
6995
- async saveCheckpoint(executionId, step, data) {
6996
- 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));
6997
8109
  }
6998
- async getCheckpoint(executionId, step) {
6999
- 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);
7000
8112
  return raw != null ? JSON.parse(raw) : null;
7001
8113
  }
7002
- async getLatestCheckpoint(executionId) {
7003
- const all = await this.client.hGetAll(this.checkpointKey(executionId));
7004
- if (!all || Object.keys(all).length === 0) return null;
7005
- let maxStep = -1;
7006
- let maxData = null;
7007
- for (const [stepStr, raw] of Object.entries(all)) {
7008
- const step = Number(stepStr);
7009
- if (step > maxStep) {
7010
- maxStep = step;
7011
- maxData = JSON.parse(raw);
7012
- }
7013
- }
7014
- return { step: maxStep, data: maxData };
7015
- }
7016
8114
  // ── Sessions ─────────────────────────────────────────────────────────
7017
8115
  async saveSession(sessionId, history) {
7018
8116
  await this.client.set(this.sessionKey(sessionId), JSON.stringify(history));
@@ -7298,11 +8396,13 @@ function cosineSimilarity2(a, b) {
7298
8396
  }
7299
8397
  // Annotate the CommonJS export names for ESM import in node:
7300
8398
  0 && (module.exports = {
8399
+ AXL_EVENT_TYPES,
7301
8400
  AnthropicProvider,
7302
8401
  AxlError,
7303
8402
  AxlRuntime,
7304
8403
  AxlStream,
7305
8404
  BudgetExceededError,
8405
+ COST_BEARING_LEAF_TYPES,
7306
8406
  GeminiProvider,
7307
8407
  GuardrailError,
7308
8408
  InMemoryVectorStore,
@@ -7317,6 +8417,8 @@ function cosineSimilarity2(a, b) {
7317
8417
  OpenAIResponsesProvider,
7318
8418
  ProviderRegistry,
7319
8419
  QuorumNotMet,
8420
+ REDACTED,
8421
+ REDACTION_RULES,
7320
8422
  RedisStore,
7321
8423
  SQLiteStore,
7322
8424
  Session,
@@ -7329,7 +8431,12 @@ function cosineSimilarity2(a, b) {
7329
8431
  agent,
7330
8432
  createSpanManager,
7331
8433
  defineConfig,
8434
+ eventCostContribution,
7332
8435
  extractJson,
8436
+ isCostBearingLeaf,
8437
+ isRootLevel,
8438
+ parsePartialJson,
8439
+ redactEvent,
7333
8440
  resolveThinkingOptions,
7334
8441
  tool,
7335
8442
  workflow,