@axlsdk/axl 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1316,6 +1316,41 @@ ${jsonInstruction}` : jsonInstruction;
1316
1316
  };
1317
1317
 
1318
1318
  // src/providers/gemini.ts
1319
+ var GEMINI_DISALLOWED_SCHEMA_KEYS = /* @__PURE__ */ new Set([
1320
+ "additionalProperties",
1321
+ "$schema",
1322
+ "$ref",
1323
+ "$defs",
1324
+ "definitions",
1325
+ "not",
1326
+ "allOf",
1327
+ "patternProperties",
1328
+ "unevaluatedProperties",
1329
+ "unevaluatedItems"
1330
+ ]);
1331
+ function sanitizeSchemaForGemini(schema) {
1332
+ if (Array.isArray(schema)) {
1333
+ return schema.map((item) => sanitizeSchemaForGemini(item));
1334
+ }
1335
+ if (schema === null || typeof schema !== "object") {
1336
+ return schema;
1337
+ }
1338
+ const src = schema;
1339
+ const out = {};
1340
+ for (const [key, value] of Object.entries(src)) {
1341
+ if (GEMINI_DISALLOWED_SCHEMA_KEYS.has(key)) continue;
1342
+ if (key === "oneOf") {
1343
+ out.anyOf = sanitizeSchemaForGemini(value);
1344
+ continue;
1345
+ }
1346
+ if (key === "const") {
1347
+ if (!("enum" in src)) out.enum = [value];
1348
+ continue;
1349
+ }
1350
+ out[key] = sanitizeSchemaForGemini(value);
1351
+ }
1352
+ return out;
1353
+ }
1319
1354
  var GEMINI_PRICING = {
1320
1355
  "gemini-2.5-pro": [125e-8, 1e-5],
1321
1356
  "gemini-2.5-flash": [3e-7, 25e-7],
@@ -1486,7 +1521,9 @@ var GeminiProvider = class {
1486
1521
  if (options.responseFormat && options.responseFormat.type !== "text") {
1487
1522
  generationConfig.responseMimeType = "application/json";
1488
1523
  if (options.responseFormat.type === "json_schema" && options.responseFormat.json_schema?.schema) {
1489
- generationConfig.responseSchema = options.responseFormat.json_schema.schema;
1524
+ generationConfig.responseSchema = sanitizeSchemaForGemini(
1525
+ options.responseFormat.json_schema.schema
1526
+ );
1490
1527
  }
1491
1528
  }
1492
1529
  if (Object.keys(generationConfig).length > 0) {
@@ -1663,7 +1700,7 @@ var GeminiProvider = class {
1663
1700
  return {
1664
1701
  name: tool2.function.name,
1665
1702
  description: tool2.function.description,
1666
- parameters: tool2.function.parameters
1703
+ parameters: sanitizeSchemaForGemini(tool2.function.parameters)
1667
1704
  };
1668
1705
  }
1669
1706
  // ---------------------------------------------------------------------------
@@ -1922,6 +1959,7 @@ var defaultRegistry = new ProviderRegistry();
1922
1959
 
1923
1960
  // src/context.ts
1924
1961
  import { AsyncLocalStorage } from "async_hooks";
1962
+ import { randomUUID } from "crypto";
1925
1963
  import { z, ZodError } from "zod";
1926
1964
 
1927
1965
  // src/errors.ts
@@ -1965,6 +2003,21 @@ var TimeoutError = class extends AxlError {
1965
2003
  this.name = "TimeoutError";
1966
2004
  }
1967
2005
  };
2006
+ function formatBudgetCost(cost) {
2007
+ if (Number.isNaN(cost)) return "$NaN";
2008
+ if (cost === Infinity) return "$Infinity";
2009
+ if (cost === -Infinity) return "-$Infinity";
2010
+ if (cost === 0) return "$0.00";
2011
+ const sign = cost < 0 ? "-" : "";
2012
+ const abs = Math.abs(cost);
2013
+ if (abs < 1e-6) return `${sign}< $0.000001`;
2014
+ if (abs < 1e-4) {
2015
+ const [mantissa, exponent] = abs.toExponential(2).split("e");
2016
+ return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
2017
+ }
2018
+ if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
2019
+ return `${sign}$${abs.toFixed(2)}`;
2020
+ }
1968
2021
  var BudgetExceededError = class extends AxlError {
1969
2022
  limit;
1970
2023
  spent;
@@ -1972,7 +2025,9 @@ var BudgetExceededError = class extends AxlError {
1972
2025
  constructor(limit, spent, policy) {
1973
2026
  super(
1974
2027
  "BUDGET_EXCEEDED",
1975
- `Budget exceeded: spent $${spent.toFixed(4)} of $${limit.toFixed(4)} limit (policy: ${policy})`
2028
+ `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
2029
+ limit
2030
+ )} limit (policy: ${policy})`
1976
2031
  );
1977
2032
  this.name = "BudgetExceededError";
1978
2033
  this.limit = limit;
@@ -2024,6 +2079,432 @@ var ToolDenied = class extends AxlError {
2024
2079
  }
2025
2080
  };
2026
2081
 
2082
+ // src/partial-json.ts
2083
+ var MAX_DEPTH = 256;
2084
+ function parsePartialJson(input) {
2085
+ const parser = new PartialParser(input);
2086
+ parser.skipWhitespace();
2087
+ if (parser.eof()) return void 0;
2088
+ const result = parser.parseValue();
2089
+ return result.value;
2090
+ }
2091
+ var PartialParser = class {
2092
+ constructor(src) {
2093
+ this.src = src;
2094
+ }
2095
+ pos = 0;
2096
+ depth = 0;
2097
+ eof() {
2098
+ return this.pos >= this.src.length;
2099
+ }
2100
+ peek() {
2101
+ return this.src[this.pos];
2102
+ }
2103
+ advance() {
2104
+ return this.src[this.pos++];
2105
+ }
2106
+ skipWhitespace() {
2107
+ while (!this.eof()) {
2108
+ const c = this.peek();
2109
+ if (c === " " || c === "\n" || c === " " || c === "\r") {
2110
+ this.pos++;
2111
+ } else {
2112
+ break;
2113
+ }
2114
+ }
2115
+ }
2116
+ parseValue() {
2117
+ this.skipWhitespace();
2118
+ if (this.eof()) return { value: void 0, complete: false };
2119
+ const c = this.peek();
2120
+ if (c === "{" || c === "[") {
2121
+ if (++this.depth > MAX_DEPTH) {
2122
+ throw new SyntaxError(
2123
+ `Maximum nesting depth exceeded (${MAX_DEPTH}) at position ${this.pos}`
2124
+ );
2125
+ }
2126
+ try {
2127
+ return c === "{" ? this.parseObject() : this.parseArray();
2128
+ } finally {
2129
+ this.depth--;
2130
+ }
2131
+ }
2132
+ if (c === '"') return this.parseString();
2133
+ if (c === "t" || c === "f") return this.parseBool();
2134
+ if (c === "n") return this.parseNull();
2135
+ if (c === "-" || c >= "0" && c <= "9") return this.parseNumber();
2136
+ throw new SyntaxError(`Unexpected character '${c}' at position ${this.pos}`);
2137
+ }
2138
+ parseObject() {
2139
+ this.advance();
2140
+ const obj = {};
2141
+ this.skipWhitespace();
2142
+ if (this.eof()) return { value: obj, complete: false };
2143
+ if (this.peek() === "}") {
2144
+ this.advance();
2145
+ return { value: obj, complete: true };
2146
+ }
2147
+ while (!this.eof()) {
2148
+ this.skipWhitespace();
2149
+ if (this.eof()) return { value: obj, complete: false };
2150
+ if (this.peek() === "}") {
2151
+ this.advance();
2152
+ return { value: obj, complete: true };
2153
+ }
2154
+ if (this.peek() !== '"') {
2155
+ throw new SyntaxError(`Expected '"' at position ${this.pos}, got '${this.peek()}'`);
2156
+ }
2157
+ const keyResult = this.parseString();
2158
+ if (!keyResult.complete) {
2159
+ return { value: obj, complete: false };
2160
+ }
2161
+ const key = keyResult.value;
2162
+ this.skipWhitespace();
2163
+ if (this.eof()) return { value: obj, complete: false };
2164
+ if (this.peek() !== ":") {
2165
+ return { value: obj, complete: false };
2166
+ }
2167
+ this.advance();
2168
+ this.skipWhitespace();
2169
+ if (this.eof()) return { value: obj, complete: false };
2170
+ const valueResult = this.parseValue();
2171
+ if (valueResult.value !== void 0 || valueResult.complete) {
2172
+ obj[key] = valueResult.value;
2173
+ }
2174
+ if (!valueResult.complete) {
2175
+ return { value: obj, complete: false };
2176
+ }
2177
+ this.skipWhitespace();
2178
+ if (this.eof()) return { value: obj, complete: false };
2179
+ const next = this.peek();
2180
+ if (next === ",") {
2181
+ this.advance();
2182
+ continue;
2183
+ }
2184
+ if (next === "}") {
2185
+ this.advance();
2186
+ return { value: obj, complete: true };
2187
+ }
2188
+ throw new SyntaxError(`Expected ',' or '}' at position ${this.pos}, got '${next}'`);
2189
+ }
2190
+ return { value: obj, complete: false };
2191
+ }
2192
+ parseArray() {
2193
+ this.advance();
2194
+ const arr = [];
2195
+ this.skipWhitespace();
2196
+ if (this.eof()) return { value: arr, complete: false };
2197
+ if (this.peek() === "]") {
2198
+ this.advance();
2199
+ return { value: arr, complete: true };
2200
+ }
2201
+ while (!this.eof()) {
2202
+ this.skipWhitespace();
2203
+ if (this.eof()) return { value: arr, complete: false };
2204
+ if (this.peek() === "]") {
2205
+ this.advance();
2206
+ return { value: arr, complete: true };
2207
+ }
2208
+ const elemResult = this.parseValue();
2209
+ if (elemResult.value !== void 0 || elemResult.complete) {
2210
+ arr.push(elemResult.value);
2211
+ }
2212
+ if (!elemResult.complete) {
2213
+ return { value: arr, complete: false };
2214
+ }
2215
+ this.skipWhitespace();
2216
+ if (this.eof()) return { value: arr, complete: false };
2217
+ const next = this.peek();
2218
+ if (next === ",") {
2219
+ this.advance();
2220
+ continue;
2221
+ }
2222
+ if (next === "]") {
2223
+ this.advance();
2224
+ return { value: arr, complete: true };
2225
+ }
2226
+ throw new SyntaxError(`Expected ',' or ']' at position ${this.pos}, got '${next}'`);
2227
+ }
2228
+ return { value: arr, complete: false };
2229
+ }
2230
+ parseString() {
2231
+ this.advance();
2232
+ let result = "";
2233
+ while (!this.eof()) {
2234
+ const c = this.advance();
2235
+ if (c === '"') return { value: result, complete: true };
2236
+ if (c === "\\") {
2237
+ if (this.eof()) {
2238
+ return { value: result, complete: false };
2239
+ }
2240
+ const esc = this.advance();
2241
+ switch (esc) {
2242
+ case '"':
2243
+ result += '"';
2244
+ break;
2245
+ case "\\":
2246
+ result += "\\";
2247
+ break;
2248
+ case "/":
2249
+ result += "/";
2250
+ break;
2251
+ case "b":
2252
+ result += "\b";
2253
+ break;
2254
+ case "f":
2255
+ result += "\f";
2256
+ break;
2257
+ case "n":
2258
+ result += "\n";
2259
+ break;
2260
+ case "r":
2261
+ result += "\r";
2262
+ break;
2263
+ case "t":
2264
+ result += " ";
2265
+ break;
2266
+ case "u": {
2267
+ if (this.pos + 4 > this.src.length) {
2268
+ return { value: result, complete: false };
2269
+ }
2270
+ const hex = this.src.slice(this.pos, this.pos + 4);
2271
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
2272
+ throw new SyntaxError(`Invalid \\u escape '${hex}' at position ${this.pos}`);
2273
+ }
2274
+ this.pos += 4;
2275
+ result += String.fromCharCode(parseInt(hex, 16));
2276
+ break;
2277
+ }
2278
+ default:
2279
+ throw new SyntaxError(`Invalid escape '\\${esc}' at position ${this.pos - 1}`);
2280
+ }
2281
+ } else {
2282
+ result += c;
2283
+ }
2284
+ }
2285
+ return { value: result, complete: false };
2286
+ }
2287
+ parseNumber() {
2288
+ const start = this.pos;
2289
+ if (this.peek() === "-") this.advance();
2290
+ while (!this.eof() && this.peek() >= "0" && this.peek() <= "9") this.advance();
2291
+ if (!this.eof() && this.peek() === ".") {
2292
+ this.advance();
2293
+ while (!this.eof() && this.peek() >= "0" && this.peek() <= "9") this.advance();
2294
+ }
2295
+ if (!this.eof() && (this.peek() === "e" || this.peek() === "E")) {
2296
+ this.advance();
2297
+ if (!this.eof() && (this.peek() === "+" || this.peek() === "-")) this.advance();
2298
+ while (!this.eof() && this.peek() >= "0" && this.peek() <= "9") this.advance();
2299
+ }
2300
+ const text = this.src.slice(start, this.pos);
2301
+ if (text === "" || text === "-" || /\.$/.test(text) || /[eE][+-]?$/.test(text)) {
2302
+ return { value: void 0, complete: false };
2303
+ }
2304
+ const n = Number(text);
2305
+ if (Number.isNaN(n)) {
2306
+ return { value: void 0, complete: false };
2307
+ }
2308
+ return { value: n, complete: true };
2309
+ }
2310
+ parseBool() {
2311
+ const remaining = this.src.slice(this.pos);
2312
+ if (remaining.startsWith("true")) {
2313
+ this.pos += 4;
2314
+ return { value: true, complete: true };
2315
+ }
2316
+ if (remaining.startsWith("false")) {
2317
+ this.pos += 5;
2318
+ return { value: false, complete: true };
2319
+ }
2320
+ if ("true".startsWith(remaining) || "false".startsWith(remaining)) {
2321
+ this.pos = this.src.length;
2322
+ return { value: void 0, complete: false };
2323
+ }
2324
+ throw new SyntaxError(`Expected 'true' or 'false' at position ${this.pos}`);
2325
+ }
2326
+ parseNull() {
2327
+ const remaining = this.src.slice(this.pos);
2328
+ if (remaining.startsWith("null")) {
2329
+ this.pos += 4;
2330
+ return { value: null, complete: true };
2331
+ }
2332
+ if ("null".startsWith(remaining)) {
2333
+ this.pos = this.src.length;
2334
+ return { value: void 0, complete: false };
2335
+ }
2336
+ throw new SyntaxError(`Expected 'null' at position ${this.pos}`);
2337
+ }
2338
+ };
2339
+
2340
+ // src/event-utils.ts
2341
+ var COST_BEARING_LEAF_TYPES = [
2342
+ "agent_call_end",
2343
+ "tool_call_end",
2344
+ "memory_remember",
2345
+ "memory_recall"
2346
+ ];
2347
+ var COST_LEAF_SET = new Set(COST_BEARING_LEAF_TYPES);
2348
+ function eventCostContribution(event) {
2349
+ if (event.type === "ask_end") return 0;
2350
+ const c = event.cost;
2351
+ return typeof c === "number" && Number.isFinite(c) && c >= 0 ? c : 0;
2352
+ }
2353
+ function isCostBearingLeaf(event) {
2354
+ return COST_LEAF_SET.has(event.type);
2355
+ }
2356
+ function isRootLevel(event) {
2357
+ const d = event.depth;
2358
+ return (d ?? 0) === 0;
2359
+ }
2360
+
2361
+ // src/redaction.ts
2362
+ var REDACTED = "[redacted]";
2363
+ function walkObjectOneLevel(obj, preserveKeys) {
2364
+ const out = {};
2365
+ for (const [k, v] of Object.entries(obj)) {
2366
+ if (preserveKeys.has(k) || typeof v === "number" || typeof v === "boolean") {
2367
+ out[k] = v;
2368
+ } else if (typeof v === "string") {
2369
+ out[k] = REDACTED;
2370
+ } else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
2371
+ const inner = v;
2372
+ const innerOut = {};
2373
+ for (const [ik, iv] of Object.entries(inner)) {
2374
+ if (typeof iv === "number" || typeof iv === "boolean") {
2375
+ innerOut[ik] = iv;
2376
+ } else {
2377
+ innerOut[ik] = REDACTED;
2378
+ }
2379
+ }
2380
+ out[k] = innerOut;
2381
+ } else {
2382
+ out[k] = REDACTED;
2383
+ }
2384
+ }
2385
+ return out;
2386
+ }
2387
+ var LOG_PRESERVE_KEYS = /* @__PURE__ */ new Set(["event"]);
2388
+ var MEMORY_PRESERVE_KEYS = /* @__PURE__ */ new Set(["scope"]);
2389
+ function redactGate(event) {
2390
+ const d = event.data;
2391
+ if (!d) return event;
2392
+ if (d.reason === void 0 && d.feedbackMessage === void 0) return event;
2393
+ return {
2394
+ ...event,
2395
+ data: {
2396
+ ...d,
2397
+ ...d.reason !== void 0 ? { reason: REDACTED } : {},
2398
+ ...d.feedbackMessage !== void 0 ? { feedbackMessage: REDACTED } : {}
2399
+ }
2400
+ };
2401
+ }
2402
+ function redactMemory(event) {
2403
+ const d = event.data;
2404
+ if (!d || typeof d !== "object") return event;
2405
+ return {
2406
+ ...event,
2407
+ data: walkObjectOneLevel(d, MEMORY_PRESERVE_KEYS)
2408
+ };
2409
+ }
2410
+ var passthrough = (e) => e;
2411
+ var REDACTION_RULES = {
2412
+ workflow_start: (e) => e.data.input !== void 0 ? { ...e, data: { ...e.data, input: REDACTED } } : e,
2413
+ workflow_end: (e) => {
2414
+ const d = e.data;
2415
+ if (d.result === void 0 && d.error === void 0) return e;
2416
+ return {
2417
+ ...e,
2418
+ data: {
2419
+ ...d,
2420
+ ...d.result !== void 0 ? { result: REDACTED } : {},
2421
+ ...d.error !== void 0 ? { error: REDACTED } : {}
2422
+ }
2423
+ };
2424
+ },
2425
+ ask_start: (e) => ({ ...e, prompt: REDACTED }),
2426
+ ask_end: (e) => ({
2427
+ ...e,
2428
+ outcome: e.outcome.ok ? { ok: true, result: REDACTED } : { ok: false, error: REDACTED }
2429
+ }),
2430
+ agent_call_start: (e) => {
2431
+ const d = e.data;
2432
+ const out = { ...d, prompt: REDACTED };
2433
+ if (d.system !== void 0) out.system = REDACTED;
2434
+ if (Array.isArray(d.messages)) {
2435
+ out.messages = [{ role: "system", content: `[${d.messages.length} messages redacted]` }];
2436
+ }
2437
+ return { ...e, data: out };
2438
+ },
2439
+ agent_call_end: (e) => {
2440
+ const d = e.data;
2441
+ const out = { ...d, response: REDACTED };
2442
+ if (d.thinking !== void 0) out.thinking = REDACTED;
2443
+ if (d.error !== void 0) out.error = REDACTED;
2444
+ return { ...e, data: out };
2445
+ },
2446
+ token: (e) => ({ ...e, data: REDACTED }),
2447
+ tool_call_start: (e) => ({ ...e, data: { ...e.data, args: REDACTED } }),
2448
+ tool_call_end: (e) => ({ ...e, data: { ...e.data, args: REDACTED, result: REDACTED } }),
2449
+ tool_approval: (e) => ({
2450
+ ...e,
2451
+ data: {
2452
+ ...e.data,
2453
+ args: REDACTED,
2454
+ ...e.data.reason !== void 0 ? { reason: REDACTED } : {}
2455
+ }
2456
+ }),
2457
+ tool_denied: (e) => {
2458
+ if (!e.data) return e;
2459
+ const d = e.data;
2460
+ if (d.args === void 0 && d.reason === void 0) return e;
2461
+ return {
2462
+ ...e,
2463
+ data: {
2464
+ ...d,
2465
+ ...d.args !== void 0 ? { args: REDACTED } : {},
2466
+ ...d.reason !== void 0 ? { reason: REDACTED } : {}
2467
+ }
2468
+ };
2469
+ },
2470
+ delegate: passthrough,
2471
+ handoff_start: (e) => e.data.message !== void 0 ? { ...e, data: { ...e.data, message: REDACTED } } : e,
2472
+ handoff_return: passthrough,
2473
+ pipeline: (e) => e.status === "failed" ? { ...e, reason: REDACTED } : e,
2474
+ partial_object: (e) => ({ ...e, data: { ...e.data, object: REDACTED } }),
2475
+ verify: (e) => e.data.lastError !== void 0 ? { ...e, data: { ...e.data, lastError: REDACTED } } : e,
2476
+ log: (e) => {
2477
+ if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
2478
+ return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
2479
+ },
2480
+ memory_remember: redactMemory,
2481
+ memory_recall: redactMemory,
2482
+ memory_forget: redactMemory,
2483
+ checkpoint_save: passthrough,
2484
+ checkpoint_replay: passthrough,
2485
+ await_human: (e) => e.data.prompt !== void 0 ? { ...e, data: { ...e.data, prompt: REDACTED } } : e,
2486
+ await_human_resolved: (e) => {
2487
+ const dec = e.data.decision;
2488
+ if (!dec) return e;
2489
+ const hasData = "data" in dec && dec.data !== void 0;
2490
+ const hasReason = "reason" in dec && dec.reason !== void 0;
2491
+ if (!hasData && !hasReason) return e;
2492
+ const scrubbed = { ...dec };
2493
+ if (hasData) scrubbed.data = REDACTED;
2494
+ if (hasReason) scrubbed.reason = REDACTED;
2495
+ return { ...e, data: { ...e.data, decision: scrubbed } };
2496
+ },
2497
+ guardrail: redactGate,
2498
+ schema_check: redactGate,
2499
+ validate: redactGate,
2500
+ done: (e) => ({ ...e, data: { result: REDACTED } }),
2501
+ error: (e) => ({ ...e, data: { ...e.data, message: REDACTED } })
2502
+ };
2503
+ function redactEvent(event) {
2504
+ const rule = REDACTION_RULES[event.type];
2505
+ return rule(event);
2506
+ }
2507
+
2027
2508
  // src/config.ts
2028
2509
  function defineConfig(config) {
2029
2510
  return config;
@@ -2101,6 +2582,7 @@ function resolveConfig(config) {
2101
2582
 
2102
2583
  // src/context.ts
2103
2584
  var signalStorage = new AsyncLocalStorage();
2585
+ var askStorage = new AsyncLocalStorage();
2104
2586
  function zodToJsonSchema(schema) {
2105
2587
  const result = z.toJSONSchema(schema, { unrepresentable: "any" });
2106
2588
  delete result.$schema;
@@ -2160,6 +2642,14 @@ function extractBalanced(str, start, openChar, closeChar) {
2160
2642
  }
2161
2643
  return null;
2162
2644
  }
2645
+ function appendRetryMessages(messages, content, feedbackMessage, providerMetadata) {
2646
+ messages.push({
2647
+ role: "assistant",
2648
+ content,
2649
+ ...providerMetadata ? { providerMetadata } : {}
2650
+ });
2651
+ messages.push({ role: "system", content: feedbackMessage });
2652
+ }
2163
2653
  function estimateMessagesTokens(messages) {
2164
2654
  let total = 0;
2165
2655
  for (const msg of messages) {
@@ -2194,8 +2684,34 @@ var WorkflowContext = class _WorkflowContext {
2194
2684
  pendingDecisions;
2195
2685
  budgetContext;
2196
2686
  stateStore;
2197
- stepCounter = 0;
2198
- checkpointCounter = 0;
2687
+ /** Root step counter for this execution. Inherited by every ctx.ask()
2688
+ * frame so all events from this WorkflowContext (root + nested) share
2689
+ * a single monotonic counter, even when concurrent branch primitives
2690
+ * fire asks before any single parent ask exists. Per-instance so
2691
+ * separate executions don't cross-talk. */
2692
+ stepRefRoot = { value: 0 };
2693
+ /**
2694
+ * Auto-checkpoint counters for internal `_checkpoint` callers. Held by
2695
+ * reference so child contexts created via `createChildContext` share
2696
+ * the same number space — otherwise a tool handler's nested `ctx.ask()`
2697
+ * (which auto-checkpoints) would collide with the parent's
2698
+ * auto-checkpoints in the state store. See `WorkflowContextInit
2699
+ * .autoCheckpointCounters` for the per-agent vs root split.
2700
+ * User-facing `ctx.checkpoint(name, fn)` requires an explicit name and
2701
+ * does NOT touch these counters — it goes straight to the store under
2702
+ * the caller-supplied key.
2703
+ */
2704
+ autoCheckpointCounters;
2705
+ /** Idempotency guards for `workflow_start` / `workflow_end` emission.
2706
+ * Both `runtime.execute()` and `runtime.stream()` have paths where a
2707
+ * post-emit side-effect (checkpoint deletion, state-store persistence)
2708
+ * can throw AFTER `_emitWorkflowEnd({status: 'completed'})` has already
2709
+ * fired — the outer catch would then fire `_emitWorkflowEnd({status:
2710
+ * 'failed'})` for a second time. These flags make both emitters
2711
+ * single-fire so consumers never see paired-then-conflicting terminal
2712
+ * events for one execution. */
2713
+ _workflowStartEmitted = false;
2714
+ _workflowEndEmitted = false;
2199
2715
  signal;
2200
2716
  summaryCache;
2201
2717
  workflowName;
@@ -2230,6 +2746,10 @@ var WorkflowContext = class _WorkflowContext {
2230
2746
  this.awaitHumanHandler = init.awaitHumanHandler;
2231
2747
  this.onAgentStart = init.onAgentStart;
2232
2748
  this.onAgentCallComplete = init.onAgentCallComplete;
2749
+ this.autoCheckpointCounters = init.autoCheckpointCounters ?? {
2750
+ byAgent: /* @__PURE__ */ new Map(),
2751
+ root: 0
2752
+ };
2233
2753
  if (init.metadata?.summaryCache) {
2234
2754
  this.summaryCache = init.metadata.summaryCache;
2235
2755
  }
@@ -2238,8 +2758,21 @@ var WorkflowContext = class _WorkflowContext {
2238
2758
  * Create a child context for nested agent invocations (e.g., agent-as-tool).
2239
2759
  * Shares: budget tracking, abort signals, trace emission, provider registry,
2240
2760
  * state store, span manager, memory manager, MCP manager, config,
2241
- * awaitHuman handler, pending decisions, tool overrides.
2242
- * Isolates: session history, step counter, streaming callbacks (onToken, onAgentStart, onToolCall).
2761
+ * awaitHuman handler, pending decisions, tool overrides, AND the
2762
+ * streaming callbacks (onToken / onAgentStart / onToolCall).
2763
+ * Isolates: session history.
2764
+ *
2765
+ * Streaming callbacks now propagate into nested asks because every callback
2766
+ * invocation carries `meta.askId`/`meta.parentAskId`/`meta.depth` so
2767
+ * consumers that want root-only behavior can filter on `meta.depth === 0`
2768
+ * instead of relying on the runtime to drop nested events. This is the
2769
+ * "nested ask visibility" fix from spec §3.2.
2770
+ *
2771
+ * The shared step counter lives in `askStorage` (ALS), so there is no
2772
+ * per-context counter to pass through anymore. Nested events are
2773
+ * correlated to their parent via `parentAskId` (set on every nested ask
2774
+ * by ALS frame allocation) — the deprecated `parentToolCallId` field
2775
+ * was removed in 0.16.0.
2243
2776
  */
2244
2777
  createChildContext() {
2245
2778
  return new _WorkflowContext({
@@ -2255,14 +2788,20 @@ var WorkflowContext = class _WorkflowContext {
2255
2788
  spanManager: this.spanManager,
2256
2789
  memoryManager: this.memoryManager,
2257
2790
  onTrace: this.onTrace,
2791
+ onToken: this.onToken,
2792
+ onToolCall: this.onToolCall,
2793
+ onAgentStart: this.onAgentStart,
2258
2794
  onAgentCallComplete: this.onAgentCallComplete,
2259
2795
  awaitHumanHandler: this.awaitHumanHandler,
2260
2796
  pendingDecisions: this.pendingDecisions,
2261
2797
  toolOverrides: this.toolOverrides,
2262
2798
  signal: this.signal,
2263
- workflowName: this.workflowName
2264
- // Isolated: sessionHistory (empty), stepCounter (0),
2265
- // onToken (null), onAgentStart (null), onToolCall (null)
2799
+ workflowName: this.workflowName,
2800
+ // Share the parent's auto-checkpoint counters so internal checkpoints
2801
+ // emitted from `ctx.ask` / spawn / race / parallel / map in this
2802
+ // child context don't collide with the parent's in the state store.
2803
+ autoCheckpointCounters: this.autoCheckpointCounters
2804
+ // Isolated: sessionHistory (empty)
2266
2805
  });
2267
2806
  }
2268
2807
  /**
@@ -2273,72 +2812,150 @@ var WorkflowContext = class _WorkflowContext {
2273
2812
  get currentSignal() {
2274
2813
  return signalStorage.getStore() ?? this.signal;
2275
2814
  }
2815
+ /**
2816
+ * Build a `CallbackMeta` for the current ask frame. Used at every
2817
+ * `onToken`/`onToolCall`/`onAgentStart` call site so consumers can
2818
+ * route or filter by ask correlation (`meta.depth === 0` for root
2819
+ * chat UIs, etc.).
2820
+ *
2821
+ * If the call is somehow outside an ask frame (e.g., a programmatic
2822
+ * `tool.run()` test harness), falls back to a synthetic meta keyed off
2823
+ * `executionId` so the type contract holds — consumers see `depth: 0`
2824
+ * and can ignore those events.
2825
+ */
2826
+ currentCallbackMeta(agentName) {
2827
+ const frame = askStorage.getStore();
2828
+ if (frame) {
2829
+ return {
2830
+ askId: frame.askId,
2831
+ ...frame.parentAskId ? { parentAskId: frame.parentAskId } : {},
2832
+ depth: frame.depth,
2833
+ agent: agentName
2834
+ };
2835
+ }
2836
+ return { askId: this.executionId, depth: 0, agent: agentName };
2837
+ }
2276
2838
  // ── ctx.ask() ─────────────────────────────────────────────────────────
2277
2839
  async ask(agent2, prompt, options) {
2278
- return this._checkpoint(async () => {
2279
- const costBefore = this.budgetContext?.totalCost ?? 0;
2280
- const startTime = Date.now();
2281
- const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
2282
- const usageCapture = {};
2283
- const doCall = async () => {
2284
- const result2 = await this.executeAgentCall(
2285
- agent2,
2286
- prompt,
2287
- options,
2288
- void 0,
2289
- usageCapture
2290
- );
2291
- return result2;
2292
- };
2293
- const result = this.spanManager ? await this.spanManager.withSpanAsync(
2294
- "axl.agent.ask",
2295
- {
2296
- "axl.agent.name": agent2._name,
2297
- "axl.agent.model": agent2.resolveModel(resolveCtx)
2298
- },
2299
- async (span) => {
2300
- const r = await doCall();
2301
- const costAfter2 = this.budgetContext?.totalCost ?? 0;
2302
- span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
2303
- span.setAttribute("axl.agent.duration", Date.now() - startTime);
2304
- if (usageCapture.value) {
2305
- span.setAttribute("axl.agent.prompt_tokens", usageCapture.value.prompt_tokens);
2306
- span.setAttribute(
2307
- "axl.agent.completion_tokens",
2308
- usageCapture.value.completion_tokens
2840
+ const agentName = agent2._name;
2841
+ return this._checkpoint(
2842
+ this._autoCheckpointName("ask", agentName),
2843
+ async () => {
2844
+ const parentFrame = askStorage.getStore();
2845
+ const askId = randomUUID();
2846
+ const depth = (parentFrame?.depth ?? -1) + 1;
2847
+ const stepRef = parentFrame?.stepRef ?? this.stepRefRoot;
2848
+ const frame = {
2849
+ askId,
2850
+ parentAskId: parentFrame?.askId,
2851
+ depth,
2852
+ agent: agent2._name,
2853
+ stepRef,
2854
+ askCost: { value: 0 }
2855
+ };
2856
+ return askStorage.run(frame, async () => {
2857
+ const askStart = Date.now();
2858
+ this.emitEvent({ type: "ask_start", prompt });
2859
+ const costBefore = this.budgetContext?.totalCost ?? 0;
2860
+ const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
2861
+ const usageCapture = {};
2862
+ const doCall = async () => {
2863
+ const result = await this.executeAgentCall(
2864
+ agent2,
2865
+ prompt,
2866
+ options,
2867
+ void 0,
2868
+ usageCapture
2309
2869
  );
2310
- if (usageCapture.value.cached_tokens)
2311
- span.setAttribute("axl.agent.cached_tokens", usageCapture.value.cached_tokens);
2870
+ return result;
2871
+ };
2872
+ let outcome;
2873
+ try {
2874
+ const result = this.spanManager ? await this.spanManager.withSpanAsync(
2875
+ "axl.agent.ask",
2876
+ {
2877
+ "axl.agent.name": agent2._name,
2878
+ "axl.agent.model": agent2.resolveModel(resolveCtx)
2879
+ },
2880
+ async (span) => {
2881
+ const r = await doCall();
2882
+ const costAfter2 = this.budgetContext?.totalCost ?? 0;
2883
+ span.setAttribute("axl.agent.cost", costAfter2 - costBefore);
2884
+ span.setAttribute("axl.agent.duration", Date.now() - askStart);
2885
+ if (usageCapture.value) {
2886
+ span.setAttribute(
2887
+ "axl.agent.prompt_tokens",
2888
+ usageCapture.value.prompt_tokens
2889
+ );
2890
+ span.setAttribute(
2891
+ "axl.agent.completion_tokens",
2892
+ usageCapture.value.completion_tokens
2893
+ );
2894
+ if (usageCapture.value.cached_tokens)
2895
+ span.setAttribute(
2896
+ "axl.agent.cached_tokens",
2897
+ usageCapture.value.cached_tokens
2898
+ );
2899
+ }
2900
+ return r;
2901
+ }
2902
+ ) : await doCall();
2903
+ outcome = { ok: true, result };
2904
+ const costAfter = this.budgetContext?.totalCost ?? 0;
2905
+ if (this.onAgentCallComplete) {
2906
+ try {
2907
+ this.onAgentCallComplete({
2908
+ agent: agent2._name,
2909
+ prompt,
2910
+ response: typeof result === "string" ? result : JSON.stringify(result),
2911
+ model: agent2.resolveModel(resolveCtx),
2912
+ cost: costAfter - costBefore,
2913
+ duration: Date.now() - askStart,
2914
+ promptVersion: agent2._config.version,
2915
+ temperature: options?.temperature ?? agent2._config.temperature,
2916
+ maxTokens: options?.maxTokens ?? agent2._config.maxTokens ?? 4096,
2917
+ effort: options?.effort ?? agent2._config.effort,
2918
+ thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
2919
+ includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
2920
+ toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
2921
+ stop: options?.stop ?? agent2._config.stop,
2922
+ providerOptions: options?.providerOptions ?? agent2._config.providerOptions
2923
+ });
2924
+ } catch (hookErr) {
2925
+ console.error(
2926
+ "[axl] onAgentCallComplete hook threw; ask outcome unchanged:",
2927
+ hookErr instanceof Error ? hookErr.message : String(hookErr)
2928
+ );
2929
+ }
2930
+ }
2931
+ return result;
2932
+ } catch (err) {
2933
+ outcome = {
2934
+ ok: false,
2935
+ error: err instanceof Error ? err.message : String(err)
2936
+ };
2937
+ throw err;
2938
+ } finally {
2939
+ this.emitEvent({
2940
+ type: "ask_end",
2941
+ outcome: outcome ?? {
2942
+ ok: false,
2943
+ error: "ask_end emitted without outcome \u2014 internal bug"
2944
+ },
2945
+ cost: frame.askCost.value,
2946
+ duration: Date.now() - askStart
2947
+ });
2312
2948
  }
2313
- return r;
2314
- }
2315
- ) : await doCall();
2316
- const costAfter = this.budgetContext?.totalCost ?? 0;
2317
- this.onAgentCallComplete?.({
2318
- agent: agent2._name,
2319
- prompt,
2320
- response: typeof result === "string" ? result : JSON.stringify(result),
2321
- model: agent2.resolveModel(resolveCtx),
2322
- cost: costAfter - costBefore,
2323
- duration: Date.now() - startTime,
2324
- promptVersion: agent2._config.version,
2325
- temperature: options?.temperature ?? agent2._config.temperature,
2326
- maxTokens: options?.maxTokens ?? agent2._config.maxTokens ?? 4096,
2327
- effort: options?.effort ?? agent2._config.effort,
2328
- thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
2329
- includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
2330
- toolChoice: options?.toolChoice ?? agent2._config.toolChoice,
2331
- stop: options?.stop ?? agent2._config.stop,
2332
- providerOptions: options?.providerOptions ?? agent2._config.providerOptions
2333
- });
2334
- return result;
2335
- });
2949
+ });
2950
+ },
2951
+ { agent: agentName }
2952
+ );
2336
2953
  }
2337
2954
  async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
2338
2955
  if (this.budgetContext?.exceeded) {
2339
2956
  const { limit, totalCost: spent, policy } = this.budgetContext;
2340
2957
  if (policy === "warn") {
2341
- this.emitTrace({
2958
+ this.emitEvent({
2342
2959
  type: "log",
2343
2960
  data: { warning: "Budget exceeded", limit, spent, policy }
2344
2961
  });
@@ -2427,14 +3044,25 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2427
3044
  const guardrails = agent2._config.guardrails;
2428
3045
  if (guardrails?.input) {
2429
3046
  const inputResult = await guardrails.input(prompt, { metadata: this.metadata });
2430
- this.emitTrace({
3047
+ this.emitEvent({
2431
3048
  type: "guardrail",
2432
3049
  agent: agent2._name,
2433
- data: { guardrailType: "input", blocked: inputResult.block, reason: inputResult.reason }
3050
+ data: {
3051
+ guardrailType: "input",
3052
+ blocked: inputResult.block,
3053
+ ...inputResult.reason ? { reason: inputResult.reason } : {},
3054
+ // Input guardrails can't retry (prompt is user-supplied), so attempt
3055
+ // and maxAttempts are always 1 — emit them for shape consistency with
3056
+ // output guardrails so consumers don't need two narrowers.
3057
+ attempt: 1,
3058
+ maxAttempts: 1
3059
+ }
2434
3060
  });
2435
3061
  this.spanManager?.addEventToActiveSpan("axl.guardrail.check", {
2436
3062
  "axl.guardrail.type": "input",
2437
3063
  "axl.guardrail.blocked": inputResult.block,
3064
+ "axl.guardrail.attempt": 1,
3065
+ "axl.guardrail.maxAttempts": 1,
2438
3066
  ...inputResult.reason ? { "axl.guardrail.reason": inputResult.reason } : {}
2439
3067
  });
2440
3068
  if (inputResult.block) {
@@ -2450,22 +3078,51 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2450
3078
  const maxTurns = agent2._config.maxTurns ?? 25;
2451
3079
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
2452
3080
  const startTime = Date.now();
2453
- if (this.onToken && options?.validate) {
2454
- throw new AxlError(
2455
- "INVALID_CONFIG",
2456
- "Cannot use validate with streaming. Validate requires schema (JSON output) which does not benefit from token streaming. Use a non-streaming call instead."
2457
- );
2458
- }
2459
3081
  const currentMessages = [...messages];
2460
3082
  let turns = 0;
2461
3083
  let guardrailOutputRetries = 0;
2462
3084
  let schemaRetries = 0;
2463
3085
  let validateRetries = 0;
3086
+ const maxGuardrailRetries = guardrails?.maxRetries ?? 2;
3087
+ let pendingRetryReason;
3088
+ const verboseTrace = this.config.trace?.level === "full";
3089
+ let lastStartStage = "initial";
3090
+ let lastStartAttempt = 1;
3091
+ let lastStartMaxAttempts = 1;
2464
3092
  while (turns < maxTurns) {
2465
3093
  if (Date.now() - startTime > timeoutMs) {
2466
3094
  throw new TimeoutError("ctx.ask()", timeoutMs);
2467
3095
  }
2468
3096
  turns++;
3097
+ const isFirstTurn = turns === 1;
3098
+ const isRetryTurn = pendingRetryReason !== void 0;
3099
+ if (isFirstTurn || isRetryTurn) {
3100
+ const stage = pendingRetryReason ?? "initial";
3101
+ let pipelineAttempt = 1;
3102
+ let pipelineMaxAttempts = 1;
3103
+ if (stage === "guardrail") {
3104
+ pipelineAttempt = guardrailOutputRetries + 1;
3105
+ pipelineMaxAttempts = maxGuardrailRetries + 1;
3106
+ } else if (stage === "schema") {
3107
+ pipelineAttempt = schemaRetries + 1;
3108
+ pipelineMaxAttempts = (options?.retries ?? 3) + 1;
3109
+ } else if (stage === "validate") {
3110
+ pipelineAttempt = validateRetries + 1;
3111
+ pipelineMaxAttempts = (options?.validateRetries ?? 2) + 1;
3112
+ }
3113
+ lastStartStage = stage;
3114
+ lastStartAttempt = pipelineAttempt;
3115
+ lastStartMaxAttempts = pipelineMaxAttempts;
3116
+ this.emitEvent({
3117
+ type: "pipeline",
3118
+ agent: agent2._name,
3119
+ status: "start",
3120
+ stage,
3121
+ attempt: pipelineAttempt,
3122
+ maxAttempts: pipelineMaxAttempts
3123
+ });
3124
+ }
3125
+ const turnStart = Date.now();
2469
3126
  const chatOptions = {
2470
3127
  model,
2471
3128
  temperature: options?.temperature ?? agent2._config.temperature,
@@ -2482,82 +3139,172 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2482
3139
  if (options?.schema && toolDefs.length === 0) {
2483
3140
  chatOptions.responseFormat = { type: "json_object" };
2484
3141
  }
2485
- this.onAgentStart?.({ agent: agent2._name, model: modelUri });
3142
+ const callbackMeta = this.currentCallbackMeta(agent2._name);
3143
+ const callParams = {
3144
+ ...chatOptions.temperature !== void 0 ? { temperature: chatOptions.temperature } : {},
3145
+ maxTokens: chatOptions.maxTokens,
3146
+ ...chatOptions.effort !== void 0 ? { effort: chatOptions.effort } : {},
3147
+ ...chatOptions.thinkingBudget !== void 0 ? { thinkingBudget: chatOptions.thinkingBudget } : {},
3148
+ ...chatOptions.includeThoughts !== void 0 ? { includeThoughts: chatOptions.includeThoughts } : {},
3149
+ ...chatOptions.toolChoice !== void 0 ? { toolChoice: chatOptions.toolChoice } : {},
3150
+ ...chatOptions.stop !== void 0 ? { stop: chatOptions.stop } : {}
3151
+ };
3152
+ let messagesSnapshot;
3153
+ if (verboseTrace) {
3154
+ try {
3155
+ messagesSnapshot = structuredClone(currentMessages);
3156
+ } catch (err) {
3157
+ console.warn(
3158
+ "[axl] verbose trace messages snapshot failed to clone; emitting shallow copy:",
3159
+ err instanceof Error ? err.message : String(err)
3160
+ );
3161
+ messagesSnapshot = [...currentMessages];
3162
+ }
3163
+ }
3164
+ const retryReason = pendingRetryReason;
3165
+ pendingRetryReason = void 0;
3166
+ this.emitEvent({
3167
+ type: "agent_call_start",
3168
+ agent: agent2._name,
3169
+ model: modelUri,
3170
+ turn: turns,
3171
+ data: {
3172
+ prompt,
3173
+ ...systemPrompt ? { system: systemPrompt } : {},
3174
+ params: callParams,
3175
+ turn: turns,
3176
+ ...retryReason ? { retryReason } : {},
3177
+ ...toolDefs.length > 0 ? { toolNames: toolDefs.map((t) => t.function.name) } : {},
3178
+ ...messagesSnapshot ? { messages: messagesSnapshot } : {}
3179
+ }
3180
+ });
3181
+ this.onAgentStart?.({ agent: agent2._name, model: modelUri }, callbackMeta);
2486
3182
  let response;
2487
- if (this.onToken) {
2488
- let content2 = "";
2489
- const toolCalls = [];
2490
- const toolCallBuffers = /* @__PURE__ */ new Map();
2491
- let streamProviderMetadata;
2492
- let thinkingContent = "";
2493
- for await (const chunk of provider.stream(currentMessages, chatOptions)) {
2494
- if (chunk.type === "text_delta") {
2495
- content2 += chunk.content;
2496
- this.onToken(chunk.content);
2497
- } else if (chunk.type === "thinking_delta") {
2498
- thinkingContent += chunk.content;
2499
- } else if (chunk.type === "tool_call_delta") {
2500
- let buffer = toolCallBuffers.get(chunk.id);
2501
- if (!buffer) {
2502
- buffer = { id: chunk.id, name: "", arguments: "" };
2503
- toolCallBuffers.set(chunk.id, buffer);
2504
- }
2505
- if (chunk.name) buffer.name = chunk.name;
2506
- if (chunk.arguments) buffer.arguments += chunk.arguments;
2507
- } else if (chunk.type === "done") {
2508
- streamProviderMetadata = chunk.providerMetadata;
2509
- if (chunk.usage) {
2510
- response = {
2511
- content: content2,
2512
- tool_calls: void 0,
2513
- usage: chunk.usage,
2514
- cost: chunk.cost
2515
- };
3183
+ try {
3184
+ if (this.onToken) {
3185
+ let content2 = "";
3186
+ const toolCalls = [];
3187
+ const toolCallBuffers = /* @__PURE__ */ new Map();
3188
+ let streamProviderMetadata;
3189
+ let thinkingContent = "";
3190
+ const partialObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof z.ZodObject;
3191
+ const currentAttempt = schemaRetries + 1;
3192
+ let inString = false;
3193
+ let escaped = false;
3194
+ let boundaryPending = false;
3195
+ for await (const chunk of provider.stream(currentMessages, chatOptions)) {
3196
+ if (chunk.type === "text_delta") {
3197
+ content2 += chunk.content;
3198
+ this.emitEvent({ type: "token", data: chunk.content });
3199
+ this.onToken(chunk.content, callbackMeta);
3200
+ if (partialObjectEnabled) {
3201
+ for (const ch of chunk.content) {
3202
+ if (escaped) {
3203
+ escaped = false;
3204
+ continue;
3205
+ }
3206
+ if (ch === "\\") {
3207
+ escaped = true;
3208
+ continue;
3209
+ }
3210
+ if (ch === '"') {
3211
+ inString = !inString;
3212
+ continue;
3213
+ }
3214
+ if (!inString && (ch === "," || ch === "}" || ch === "]")) {
3215
+ boundaryPending = true;
3216
+ }
3217
+ }
3218
+ if (boundaryPending) {
3219
+ boundaryPending = false;
3220
+ let parsed;
3221
+ try {
3222
+ parsed = parsePartialJson(extractJson(content2));
3223
+ } catch {
3224
+ parsed = void 0;
3225
+ }
3226
+ if (parsed !== void 0) {
3227
+ this.emitEvent({
3228
+ type: "partial_object",
3229
+ agent: agent2._name,
3230
+ attempt: currentAttempt,
3231
+ data: { object: parsed }
3232
+ });
3233
+ }
3234
+ }
3235
+ }
3236
+ } else if (chunk.type === "thinking_delta") {
3237
+ thinkingContent += chunk.content;
3238
+ } else if (chunk.type === "tool_call_delta") {
3239
+ let buffer = toolCallBuffers.get(chunk.id);
3240
+ if (!buffer) {
3241
+ buffer = { id: chunk.id, name: "", arguments: "" };
3242
+ toolCallBuffers.set(chunk.id, buffer);
3243
+ }
3244
+ if (chunk.name) buffer.name = chunk.name;
3245
+ if (chunk.arguments) buffer.arguments += chunk.arguments;
3246
+ } else if (chunk.type === "done") {
3247
+ streamProviderMetadata = chunk.providerMetadata;
3248
+ if (chunk.usage) {
3249
+ response = {
3250
+ content: content2,
3251
+ tool_calls: void 0,
3252
+ usage: chunk.usage,
3253
+ cost: chunk.cost
3254
+ };
3255
+ }
2516
3256
  }
2517
3257
  }
3258
+ for (const buffer of toolCallBuffers.values()) {
3259
+ toolCalls.push({
3260
+ id: buffer.id,
3261
+ type: "function",
3262
+ function: {
3263
+ name: buffer.name,
3264
+ arguments: buffer.arguments
3265
+ }
3266
+ });
3267
+ }
3268
+ response ??= {
3269
+ content: content2,
3270
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
3271
+ };
3272
+ if (toolCalls.length > 0) {
3273
+ response.tool_calls = toolCalls;
3274
+ }
3275
+ if (streamProviderMetadata) {
3276
+ response.providerMetadata = streamProviderMetadata;
3277
+ }
3278
+ if (thinkingContent) {
3279
+ response.thinking_content = thinkingContent;
3280
+ }
3281
+ } else {
3282
+ response = await provider.chat(currentMessages, chatOptions);
2518
3283
  }
2519
- for (const buffer of toolCallBuffers.values()) {
2520
- toolCalls.push({
2521
- id: buffer.id,
2522
- type: "function",
2523
- function: {
2524
- name: buffer.name,
2525
- arguments: buffer.arguments
2526
- }
2527
- });
2528
- }
2529
- response ??= {
2530
- content: content2,
2531
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
2532
- };
2533
- if (toolCalls.length > 0) {
2534
- response.tool_calls = toolCalls;
2535
- }
2536
- if (streamProviderMetadata) {
2537
- response.providerMetadata = streamProviderMetadata;
2538
- }
2539
- if (thinkingContent) {
2540
- response.thinking_content = thinkingContent;
2541
- }
2542
- } else {
2543
- response = await provider.chat(currentMessages, chatOptions);
3284
+ } catch (err) {
3285
+ this.emitEvent({
3286
+ type: "agent_call_end",
3287
+ agent: agent2._name,
3288
+ model: modelUri,
3289
+ promptVersion: agent2._config.version,
3290
+ duration: Date.now() - turnStart,
3291
+ data: {
3292
+ response: "",
3293
+ turn: turns,
3294
+ ...retryReason ? { retryReason } : {},
3295
+ error: err instanceof Error ? err.message : String(err)
3296
+ }
3297
+ });
3298
+ throw err;
2544
3299
  }
2545
3300
  if (usageCapture && response.usage) {
2546
3301
  usageCapture.value = response.usage;
2547
3302
  }
2548
3303
  if (response.cost) {
2549
- if (this.budgetContext) {
2550
- this.budgetContext.totalCost += response.cost;
2551
- if (this.budgetContext.totalCost >= this.budgetContext.limit) {
2552
- this.budgetContext.exceeded = true;
2553
- if (this.budgetContext.policy === "hard_stop" && this.budgetContext.abortController) {
2554
- this.budgetContext.abortController.abort();
2555
- }
2556
- }
2557
- }
3304
+ this._accumulateBudgetCost(response.cost);
2558
3305
  }
2559
- this.emitTrace({
2560
- type: "agent_call",
3306
+ this.emitEvent({
3307
+ type: "agent_call_end",
2561
3308
  agent: agent2._name,
2562
3309
  model: modelUri,
2563
3310
  promptVersion: agent2._config.version,
@@ -2567,8 +3314,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2567
3314
  output: response.usage.completion_tokens,
2568
3315
  reasoning: response.usage.reasoning_tokens
2569
3316
  } : void 0,
2570
- duration: Date.now() - startTime,
2571
- data: { prompt, response: response.content }
3317
+ duration: Date.now() - turnStart,
3318
+ data: {
3319
+ response: response.content,
3320
+ ...response.thinking_content ? { thinking: response.thinking_content } : {},
3321
+ turn: turns,
3322
+ ...retryReason ? { retryReason } : {}
3323
+ }
2572
3324
  });
2573
3325
  if (response.tool_calls && response.tool_calls.length > 0) {
2574
3326
  currentMessages.push({
@@ -2593,6 +3345,19 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2593
3345
  }
2594
3346
  }
2595
3347
  const handoffStart = Date.now();
3348
+ const sourceFrame = askStorage.getStore();
3349
+ const handoffFromAskId = sourceFrame?.askId ?? this.executionId;
3350
+ const handoffSourceDepth = sourceFrame?.depth ?? 0;
3351
+ const handoffToAskId = randomUUID();
3352
+ const handoffTargetDepth = handoffSourceDepth + 1;
3353
+ const targetFrame = {
3354
+ askId: handoffToAskId,
3355
+ parentAskId: handoffFromAskId,
3356
+ depth: handoffTargetDepth,
3357
+ agent: descriptor.agent._name,
3358
+ stepRef: sourceFrame?.stepRef ?? this.stepRefRoot,
3359
+ askCost: { value: 0 }
3360
+ };
2596
3361
  const handoffOptions = options ? {
2597
3362
  schema: options.schema,
2598
3363
  retries: options.retries,
@@ -2600,13 +3365,51 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2600
3365
  validate: options.validate,
2601
3366
  validateRetries: options.validateRetries
2602
3367
  } : void 0;
2603
- const handoffFn = () => this.executeAgentCall(
2604
- descriptor.agent,
2605
- handoffPrompt,
2606
- handoffOptions,
2607
- currentMessages,
2608
- usageCapture
2609
- );
3368
+ const handoffFn = () => askStorage.run(targetFrame, async () => {
3369
+ this.emitEvent({ type: "ask_start", prompt: handoffPrompt });
3370
+ const targetAskStart = Date.now();
3371
+ try {
3372
+ const result = await this.executeAgentCall(
3373
+ descriptor.agent,
3374
+ handoffPrompt,
3375
+ handoffOptions,
3376
+ currentMessages,
3377
+ usageCapture
3378
+ );
3379
+ this.emitEvent({
3380
+ type: "ask_end",
3381
+ outcome: { ok: true, result },
3382
+ cost: targetFrame.askCost.value,
3383
+ duration: Date.now() - targetAskStart
3384
+ });
3385
+ return result;
3386
+ } catch (err) {
3387
+ this.emitEvent({
3388
+ type: "ask_end",
3389
+ outcome: {
3390
+ ok: false,
3391
+ error: err instanceof Error ? err.message : String(err)
3392
+ },
3393
+ cost: targetFrame.askCost.value,
3394
+ duration: Date.now() - targetAskStart
3395
+ });
3396
+ throw err;
3397
+ }
3398
+ });
3399
+ this.emitEvent({
3400
+ type: "handoff_start",
3401
+ agent: agent2._name,
3402
+ fromAskId: handoffFromAskId,
3403
+ toAskId: handoffToAskId,
3404
+ sourceDepth: handoffSourceDepth,
3405
+ targetDepth: handoffTargetDepth,
3406
+ data: {
3407
+ source: agent2._name,
3408
+ target: targetName,
3409
+ mode,
3410
+ ...mode === "roundtrip" && handoffPrompt !== prompt ? { message: handoffPrompt } : {}
3411
+ }
3412
+ });
2610
3413
  if (mode === "roundtrip") {
2611
3414
  const executeRoundtrip = async () => {
2612
3415
  const result = await handoffFn();
@@ -2627,24 +3430,41 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2627
3430
  "axl.handoff.mode": mode
2628
3431
  },
2629
3432
  async (span) => {
2630
- const result = await executeRoundtrip();
2631
- const duration = Date.now() - handoffStart;
2632
- span.setAttribute("axl.handoff.duration", duration);
2633
- this.emitTrace({
2634
- type: "handoff",
2635
- agent: agent2._name,
2636
- data: { target: targetName, mode, duration }
2637
- });
2638
- return result;
3433
+ try {
3434
+ return await executeRoundtrip();
3435
+ } finally {
3436
+ const duration = Date.now() - handoffStart;
3437
+ span.setAttribute("axl.handoff.duration", duration);
3438
+ this.emitEvent({
3439
+ type: "handoff_return",
3440
+ agent: agent2._name,
3441
+ fromAskId: handoffFromAskId,
3442
+ toAskId: handoffToAskId,
3443
+ sourceDepth: handoffSourceDepth,
3444
+ targetDepth: handoffTargetDepth,
3445
+ data: { source: agent2._name, target: targetName, duration }
3446
+ });
3447
+ }
2639
3448
  }
2640
3449
  );
2641
3450
  } else {
2642
- await executeRoundtrip();
2643
- this.emitTrace({
2644
- type: "handoff",
2645
- agent: agent2._name,
2646
- data: { target: targetName, mode, duration: Date.now() - handoffStart }
2647
- });
3451
+ try {
3452
+ await executeRoundtrip();
3453
+ } finally {
3454
+ this.emitEvent({
3455
+ type: "handoff_return",
3456
+ agent: agent2._name,
3457
+ fromAskId: handoffFromAskId,
3458
+ toAskId: handoffToAskId,
3459
+ sourceDepth: handoffSourceDepth,
3460
+ targetDepth: handoffTargetDepth,
3461
+ data: {
3462
+ source: agent2._name,
3463
+ target: targetName,
3464
+ duration: Date.now() - handoffStart
3465
+ }
3466
+ });
3467
+ }
2648
3468
  }
2649
3469
  continue;
2650
3470
  }
@@ -2658,24 +3478,12 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2658
3478
  },
2659
3479
  async (span) => {
2660
3480
  const result = await handoffFn();
2661
- const duration = Date.now() - handoffStart;
2662
- span.setAttribute("axl.handoff.duration", duration);
2663
- this.emitTrace({
2664
- type: "handoff",
2665
- agent: agent2._name,
2666
- data: { target: targetName, mode, duration }
2667
- });
3481
+ span.setAttribute("axl.handoff.duration", Date.now() - handoffStart);
2668
3482
  return result;
2669
3483
  }
2670
3484
  );
2671
3485
  }
2672
- const onewayResult = await handoffFn();
2673
- this.emitTrace({
2674
- type: "handoff",
2675
- agent: agent2._name,
2676
- data: { target: targetName, mode, duration: Date.now() - handoffStart }
2677
- });
2678
- return onewayResult;
3486
+ return await handoffFn();
2679
3487
  }
2680
3488
  }
2681
3489
  const toolOverride = this.toolOverrides?.get(toolName);
@@ -2691,7 +3499,16 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2691
3499
  });
2692
3500
  continue;
2693
3501
  }
2694
- this.onToolCall?.({ name: toolName, args: toolArgs2, callId: toolCall.id });
3502
+ this.emitEvent({
3503
+ type: "tool_call_start",
3504
+ tool: toolName,
3505
+ callId: toolCall.id,
3506
+ data: { args: toolArgs2 }
3507
+ });
3508
+ this.onToolCall?.(
3509
+ { name: toolName, args: toolArgs2, callId: toolCall.id },
3510
+ callbackMeta
3511
+ );
2695
3512
  const toolStart2 = Date.now();
2696
3513
  const executeOverride = async () => {
2697
3514
  let toolResult3;
@@ -2719,8 +3536,8 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2719
3536
  }
2720
3537
  ) : await executeOverride();
2721
3538
  const resultContent2 = JSON.stringify(toolResult2);
2722
- this.emitTrace({
2723
- type: "tool_call",
3539
+ this.emitEvent({
3540
+ type: "tool_call_end",
2724
3541
  agent: agent2._name,
2725
3542
  tool: toolName,
2726
3543
  duration: Date.now() - toolStart2,
@@ -2736,7 +3553,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2736
3553
  const tool2 = agent2._config.tools?.find((t) => t.name === toolName);
2737
3554
  const isMcpTool = !tool2 && this.mcpManager?.isMcpTool(toolName);
2738
3555
  if (!tool2 && !isMcpTool) {
2739
- this.emitTrace({ type: "tool_denied", agent: agent2._name, tool: toolName });
3556
+ this.emitEvent({ type: "tool_denied", agent: agent2._name, tool: toolName });
2740
3557
  currentMessages.push({
2741
3558
  role: "tool",
2742
3559
  content: `Tool "${toolName}" is not available. Available tools: ${agent2._config.tools?.map((t) => t.name).join(", ") ?? "none"}`,
@@ -2755,7 +3572,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2755
3572
  });
2756
3573
  continue;
2757
3574
  }
2758
- this.onToolCall?.({ name: toolName, args: toolArgs, callId: toolCall.id });
3575
+ this.emitEvent({
3576
+ type: "tool_call_start",
3577
+ tool: toolName,
3578
+ callId: toolCall.id,
3579
+ data: { args: toolArgs }
3580
+ });
3581
+ this.onToolCall?.({ name: toolName, args: toolArgs, callId: toolCall.id }, callbackMeta);
2759
3582
  const toolStart = Date.now();
2760
3583
  if (tool2 && tool2.requireApproval) {
2761
3584
  const approvalFn = async () => {
@@ -2766,24 +3589,18 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2766
3589
  });
2767
3590
  if (!decision.approved) {
2768
3591
  const reason = decision.reason ?? "Denied by human";
2769
- this.emitTrace({
2770
- type: "tool_denied",
2771
- agent: agent2._name,
2772
- tool: toolName,
2773
- data: { denied: true, reason, args: toolArgs }
2774
- });
2775
3592
  currentMessages.push({
2776
3593
  role: "tool",
2777
3594
  content: JSON.stringify({ error: `Tool denied by human: ${reason}` }),
2778
3595
  tool_call_id: toolCall.id
2779
3596
  });
2780
- return false;
3597
+ return { approved: false, reason };
2781
3598
  }
2782
- return true;
3599
+ return { approved: true };
2783
3600
  };
2784
- let approved;
3601
+ let approvalOutcome;
2785
3602
  if (this.spanManager) {
2786
- approved = await this.spanManager.withSpanAsync(
3603
+ approvalOutcome = await this.spanManager.withSpanAsync(
2787
3604
  "axl.tool.approval",
2788
3605
  {
2789
3606
  "axl.tool.name": toolName,
@@ -2791,20 +3608,24 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2791
3608
  },
2792
3609
  async (span) => {
2793
3610
  const result = await approvalFn();
2794
- span.setAttribute("axl.tool.approval.approved", result);
3611
+ span.setAttribute("axl.tool.approval.approved", result.approved);
2795
3612
  return result;
2796
3613
  }
2797
3614
  );
2798
3615
  } else {
2799
- approved = await approvalFn();
3616
+ approvalOutcome = await approvalFn();
2800
3617
  }
2801
- if (!approved) continue;
2802
- this.emitTrace({
2803
- type: "tool_denied",
3618
+ this.emitEvent({
3619
+ type: "tool_approval",
2804
3620
  agent: agent2._name,
2805
3621
  tool: toolName,
2806
- data: { denied: false, args: toolArgs }
3622
+ data: {
3623
+ approved: approvalOutcome.approved,
3624
+ args: toolArgs,
3625
+ ...approvalOutcome.reason ? { reason: approvalOutcome.reason } : {}
3626
+ }
2807
3627
  });
3628
+ if (!approvalOutcome.approved) continue;
2808
3629
  }
2809
3630
  if (tool2 && tool2.hooks?.before) {
2810
3631
  try {
@@ -2879,8 +3700,8 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2879
3700
  return r;
2880
3701
  }
2881
3702
  ) : await executeTool();
2882
- this.emitTrace({
2883
- type: "tool_call",
3703
+ this.emitEvent({
3704
+ type: "tool_call_end",
2884
3705
  agent: agent2._name,
2885
3706
  tool: traceName,
2886
3707
  duration: Date.now() - toolStart,
@@ -2897,37 +3718,52 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2897
3718
  const content = response.content;
2898
3719
  if (guardrails?.output) {
2899
3720
  const outputResult = await guardrails.output(content, { metadata: this.metadata });
2900
- this.emitTrace({
3721
+ const attempt = guardrailOutputRetries + 1;
3722
+ const maxAttempts = maxGuardrailRetries + 1;
3723
+ const onBlock = guardrails.onBlock ?? "throw";
3724
+ let feedbackMessage;
3725
+ if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
3726
+ feedbackMessage = `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
3727
+ }
3728
+ this.emitEvent({
2901
3729
  type: "guardrail",
2902
3730
  agent: agent2._name,
2903
3731
  data: {
2904
3732
  guardrailType: "output",
2905
3733
  blocked: outputResult.block,
2906
- reason: outputResult.reason
3734
+ ...outputResult.reason ? { reason: outputResult.reason } : {},
3735
+ attempt,
3736
+ maxAttempts,
3737
+ ...feedbackMessage ? { feedbackMessage } : {}
2907
3738
  }
2908
3739
  });
2909
3740
  this.spanManager?.addEventToActiveSpan("axl.guardrail.check", {
2910
3741
  "axl.guardrail.type": "output",
2911
3742
  "axl.guardrail.blocked": outputResult.block,
3743
+ "axl.guardrail.attempt": attempt,
3744
+ "axl.guardrail.maxAttempts": maxAttempts,
2912
3745
  ...outputResult.reason ? { "axl.guardrail.reason": outputResult.reason } : {}
2913
3746
  });
2914
3747
  if (outputResult.block) {
2915
- const onBlock = guardrails.onBlock ?? "throw";
2916
- if (onBlock === "retry") {
2917
- const maxGuardrailRetries = guardrails.maxRetries ?? 2;
2918
- if (guardrailOutputRetries < maxGuardrailRetries) {
2919
- guardrailOutputRetries++;
2920
- currentMessages.push({
2921
- role: "assistant",
2922
- content,
2923
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
2924
- });
2925
- currentMessages.push({
2926
- role: "system",
2927
- content: `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`
2928
- });
2929
- continue;
2930
- }
3748
+ if (feedbackMessage) {
3749
+ this.emitEvent({
3750
+ type: "pipeline",
3751
+ agent: agent2._name,
3752
+ status: "failed",
3753
+ stage: "guardrail",
3754
+ attempt: guardrailOutputRetries + 1,
3755
+ maxAttempts: maxGuardrailRetries + 1,
3756
+ reason: feedbackMessage
3757
+ });
3758
+ guardrailOutputRetries++;
3759
+ appendRetryMessages(
3760
+ currentMessages,
3761
+ content,
3762
+ feedbackMessage,
3763
+ response.providerMetadata
3764
+ );
3765
+ pendingRetryReason = "guardrail";
3766
+ continue;
2931
3767
  }
2932
3768
  if (typeof onBlock === "function") {
2933
3769
  return onBlock(outputResult.reason ?? "Output blocked by guardrail", {
@@ -2939,30 +3775,67 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2939
3775
  }
2940
3776
  let validated = void 0;
2941
3777
  if (options?.schema) {
3778
+ const maxSchemaRetries = options.retries ?? 3;
3779
+ const schemaAttempt = schemaRetries + 1;
3780
+ const schemaMaxAttempts = maxSchemaRetries + 1;
3781
+ let schemaValid = false;
3782
+ let schemaReason;
3783
+ let schemaFeedback;
3784
+ let schemaErr;
2942
3785
  try {
2943
3786
  const parsed = JSON.parse(extractJson(content));
2944
3787
  validated = options.schema.parse(parsed);
3788
+ schemaValid = true;
2945
3789
  } catch (err) {
2946
- const maxSchemaRetries = options.retries ?? 3;
3790
+ schemaErr = err;
3791
+ schemaReason = err instanceof Error ? err.message : String(err);
2947
3792
  if (schemaRetries < maxSchemaRetries) {
3793
+ schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
3794
+ }
3795
+ }
3796
+ this.emitEvent({
3797
+ type: "schema_check",
3798
+ agent: agent2._name,
3799
+ data: {
3800
+ valid: schemaValid,
3801
+ ...schemaReason ? { reason: schemaReason } : {},
3802
+ attempt: schemaAttempt,
3803
+ maxAttempts: schemaMaxAttempts,
3804
+ ...schemaFeedback ? { feedbackMessage: schemaFeedback } : {}
3805
+ }
3806
+ });
3807
+ this.spanManager?.addEventToActiveSpan("axl.schema.check", {
3808
+ "axl.schema.valid": schemaValid,
3809
+ "axl.schema.attempt": schemaAttempt,
3810
+ "axl.schema.maxAttempts": schemaMaxAttempts,
3811
+ ...schemaReason ? { "axl.schema.reason": schemaReason } : {}
3812
+ });
3813
+ if (!schemaValid) {
3814
+ if (schemaFeedback) {
3815
+ this.emitEvent({
3816
+ type: "pipeline",
3817
+ agent: agent2._name,
3818
+ status: "failed",
3819
+ stage: "schema",
3820
+ attempt: schemaRetries + 1,
3821
+ maxAttempts: (options.retries ?? 3) + 1,
3822
+ reason: schemaFeedback
3823
+ });
2948
3824
  schemaRetries++;
2949
- const errorMsg = err instanceof Error ? err.message : String(err);
2950
- currentMessages.push({
2951
- role: "assistant",
3825
+ appendRetryMessages(
3826
+ currentMessages,
2952
3827
  content,
2953
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
2954
- });
2955
- currentMessages.push({
2956
- role: "system",
2957
- content: `Your response was not valid JSON or did not match the required schema: ${errorMsg}. Please fix and try again.`
2958
- });
3828
+ schemaFeedback,
3829
+ response.providerMetadata
3830
+ );
3831
+ pendingRetryReason = "schema";
2959
3832
  continue;
2960
3833
  }
2961
- const zodErr = err instanceof ZodError ? err : new ZodError([
3834
+ const zodErr = schemaErr instanceof ZodError ? schemaErr : new ZodError([
2962
3835
  {
2963
3836
  code: "custom",
2964
3837
  path: [],
2965
- message: err instanceof Error ? err.message : String(err)
3838
+ message: schemaReason ?? "Schema parse failed"
2966
3839
  }
2967
3840
  ]);
2968
3841
  throw new VerifyError(content, zodErr, maxSchemaRetries);
@@ -2978,31 +3851,49 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2978
3851
  const reason = err instanceof Error ? err.message : String(err);
2979
3852
  validateResult = { valid: false, reason: `Validator error: ${reason}` };
2980
3853
  }
2981
- this.emitTrace({
3854
+ const maxValidateRetries = options.validateRetries ?? 2;
3855
+ const validateAttempt = validateRetries + 1;
3856
+ const validateMaxAttempts = maxValidateRetries + 1;
3857
+ let validateFeedback;
3858
+ if (!validateResult.valid && validateRetries < maxValidateRetries) {
3859
+ validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
3860
+ }
3861
+ this.emitEvent({
2982
3862
  type: "validate",
2983
3863
  agent: agent2._name,
2984
3864
  data: {
2985
3865
  valid: validateResult.valid,
2986
- ...validateResult.reason ? { reason: validateResult.reason } : {}
3866
+ ...validateResult.reason ? { reason: validateResult.reason } : {},
3867
+ attempt: validateAttempt,
3868
+ maxAttempts: validateMaxAttempts,
3869
+ ...validateFeedback ? { feedbackMessage: validateFeedback } : {}
2987
3870
  }
2988
3871
  });
2989
3872
  this.spanManager?.addEventToActiveSpan("axl.validate.check", {
2990
3873
  "axl.validate.valid": validateResult.valid,
3874
+ "axl.validate.attempt": validateAttempt,
3875
+ "axl.validate.maxAttempts": validateMaxAttempts,
2991
3876
  ...validateResult.reason ? { "axl.validate.reason": validateResult.reason } : {}
2992
3877
  });
2993
3878
  if (!validateResult.valid) {
2994
- const maxValidateRetries = options.validateRetries ?? 2;
2995
- if (validateRetries < maxValidateRetries) {
3879
+ if (validateFeedback) {
3880
+ this.emitEvent({
3881
+ type: "pipeline",
3882
+ agent: agent2._name,
3883
+ status: "failed",
3884
+ stage: "validate",
3885
+ attempt: validateRetries + 1,
3886
+ maxAttempts: (options.validateRetries ?? 2) + 1,
3887
+ reason: validateFeedback
3888
+ });
2996
3889
  validateRetries++;
2997
- currentMessages.push({
2998
- role: "assistant",
3890
+ appendRetryMessages(
3891
+ currentMessages,
2999
3892
  content,
3000
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3001
- });
3002
- currentMessages.push({
3003
- role: "system",
3004
- content: `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`
3005
- });
3893
+ validateFeedback,
3894
+ response.providerMetadata
3895
+ );
3896
+ pendingRetryReason = "validate";
3006
3897
  continue;
3007
3898
  }
3008
3899
  throw new ValidationError(
@@ -3012,6 +3903,14 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3012
3903
  );
3013
3904
  }
3014
3905
  }
3906
+ this.emitEvent({
3907
+ type: "pipeline",
3908
+ agent: agent2._name,
3909
+ status: "committed",
3910
+ stage: lastStartStage,
3911
+ attempt: lastStartAttempt,
3912
+ maxAttempts: lastStartMaxAttempts
3913
+ });
3015
3914
  this.pushAssistantToSessionHistory(content, response.providerMetadata);
3016
3915
  return validated ?? content;
3017
3916
  }
@@ -3140,43 +4039,100 @@ ${summaryResponse.content}`
3140
4039
  /**
3141
4040
  * Execute a function with checkpoint-replay semantics.
3142
4041
  *
3143
- * On first execution, runs `fn()`, saves the result, and returns it.
3144
- * On replay (resume after restart), returns the saved result without re-executing.
3145
- * This prevents duplicate side effects (double API calls, double refunds, etc.).
4042
+ * On first execution, runs `fn()`, saves the result under `name`, and
4043
+ * returns it. On replay (resume after restart), returns the saved result
4044
+ * without re-executing. This prevents duplicate side effects (double API
4045
+ * calls, double refunds, etc.).
4046
+ *
4047
+ * `name` must be a stable, caller-supplied identifier — the same call
4048
+ * site MUST pass the same name across runs of the same execution for
4049
+ * replay to work. Names are scoped to a single execution and must be
4050
+ * unique within it; reusing a name (e.g. inside a loop) gives
4051
+ * last-write-wins behavior. For loops, compose names from the iterator:
4052
+ * `ctx.checkpoint(`process-${id}`, fn)`.
4053
+ *
4054
+ * Names starting with `__auto/` are reserved for runtime-internal
4055
+ * auto-checkpointing of `ctx.ask` / spawn / race / parallel / map.
3146
4056
  */
3147
- async checkpoint(fn) {
3148
- return this._checkpoint(fn);
4057
+ async checkpoint(name, fn) {
4058
+ if (typeof name !== "string" || name.length === 0) {
4059
+ throw new Error("ctx.checkpoint name must be a non-empty string.");
4060
+ }
4061
+ if (name !== name.trim()) {
4062
+ throw new Error(
4063
+ `ctx.checkpoint name "${name}" has leading/trailing whitespace \u2014 trim before passing.`
4064
+ );
4065
+ }
4066
+ if (name.toLowerCase().startsWith("__auto/")) {
4067
+ throw new Error(
4068
+ `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.`
4069
+ );
4070
+ }
4071
+ return this._checkpoint(name, fn);
3149
4072
  }
3150
4073
  /**
3151
- * Internal checkpoint implementation shared by both the public checkpoint()
3152
- * and the automatic checkpointing in ask/spawn/race/parallel/map.
4074
+ * Internal checkpoint implementation shared by both the public
4075
+ * `checkpoint(name, fn)` and the automatic checkpointing in
4076
+ * ask/spawn/race/parallel/map. Internal callers pass an `__auto/...`
4077
+ * name composed by `_autoCheckpointName`.
4078
+ *
4079
+ * `agent` is the originating agent for ask auto-checkpoints — passing
4080
+ * it explicitly stamps `event.agent` on the trace events even though
4081
+ * the emit fires before/after `fn()` (where the ask's ALS frame
4082
+ * either doesn't exist yet or has already torn down). Without this,
4083
+ * post-fn `checkpoint_save` rows show no agent in the trace UI.
3153
4084
  */
3154
- async _checkpoint(fn) {
3155
- const step = this.checkpointCounter++;
4085
+ async _checkpoint(name, fn, options) {
3156
4086
  if (!this.stateStore) {
3157
4087
  return fn();
3158
4088
  }
3159
- const saved = await this.stateStore.getCheckpoint(this.executionId, step);
4089
+ const agentStamp = options?.agent ? { agent: options.agent } : {};
4090
+ const saved = await this.stateStore.getCheckpoint(this.executionId, name);
3160
4091
  if (saved !== null) {
3161
- this.emitTrace({
3162
- type: "log",
3163
- data: { event: "checkpoint_replay", step }
4092
+ this.emitEvent({
4093
+ type: "checkpoint_replay",
4094
+ ...agentStamp,
4095
+ data: { name }
3164
4096
  });
3165
- this.spanManager?.addEventToActiveSpan("axl.checkpoint.hit", { "axl.checkpoint.step": step });
4097
+ this.spanManager?.addEventToActiveSpan("axl.checkpoint.hit", { "axl.checkpoint.name": name });
3166
4098
  return saved;
3167
4099
  }
3168
4100
  const result = await fn();
3169
- await this.stateStore.saveCheckpoint(this.executionId, step, result);
3170
- this.emitTrace({
3171
- type: "log",
3172
- data: { event: "checkpoint_save", step }
4101
+ await this.stateStore.saveCheckpoint(this.executionId, name, result);
4102
+ this.emitEvent({
4103
+ type: "checkpoint_save",
4104
+ ...agentStamp,
4105
+ data: { name }
3173
4106
  });
3174
- this.spanManager?.addEventToActiveSpan("axl.checkpoint.miss", { "axl.checkpoint.step": step });
4107
+ this.spanManager?.addEventToActiveSpan("axl.checkpoint.miss", { "axl.checkpoint.name": name });
3175
4108
  return result;
3176
4109
  }
4110
+ /** Generate a stable auto-checkpoint name for an internal primitive.
4111
+ *
4112
+ * For `ctx.ask`, the originating agent is known and gets a per-agent
4113
+ * counter — the resulting name reads as "orchestrator's first ask"
4114
+ * (`__auto/orchestrator-agent/ask/0`) instead of a global ordinal.
4115
+ * For `spawn` / `race` / `parallel` / `map`, no agent context is
4116
+ * available (those wrap arbitrary fn callbacks), so the name uses
4117
+ * the root counter: `__auto/spawn/0`, `__auto/race/3`, etc.
4118
+ *
4119
+ * All counters live on a shared ref passed through `createChildContext`,
4120
+ * so a tool handler's nested `ctx.ask()` (which runs in a child
4121
+ * WorkflowContext) can never collide with the parent's auto-names in
4122
+ * the state store. */
4123
+ _autoCheckpointName(primitive, agent2) {
4124
+ const counters = this.autoCheckpointCounters;
4125
+ if (primitive === "ask" && agent2) {
4126
+ const n2 = counters.byAgent.get(agent2) ?? 0;
4127
+ counters.byAgent.set(agent2, n2 + 1);
4128
+ return `__auto/${agent2}/ask/${n2}`;
4129
+ }
4130
+ const n = counters.root++;
4131
+ return `__auto/${primitive}/${n}`;
4132
+ }
3177
4133
  // ── ctx.spawn() ───────────────────────────────────────────────────────
3178
4134
  async spawn(n, fn, options) {
3179
- return this._checkpoint(() => {
4135
+ return this._checkpoint(this._autoCheckpointName("spawn"), () => {
3180
4136
  if (this.spanManager) {
3181
4137
  return this.spanManager.withSpanAsync(
3182
4138
  "axl.ctx.spawn",
@@ -3371,6 +4327,17 @@ ${summaryResponse.content}`
3371
4327
  async verify(fn, schema, options) {
3372
4328
  const maxRetries = options?.retries ?? 3;
3373
4329
  let lastRetry = void 0;
4330
+ const emitVerifyOutcome = (passed, attempts, lastError) => {
4331
+ this.emitEvent({
4332
+ type: "verify",
4333
+ agent: void 0,
4334
+ data: {
4335
+ passed,
4336
+ attempts,
4337
+ ...lastError ? { lastError } : {}
4338
+ }
4339
+ });
4340
+ };
3374
4341
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3375
4342
  let rawOutput;
3376
4343
  try {
@@ -3389,12 +4356,14 @@ ${summaryResponse.content}`
3389
4356
  const errorMsg = validateResult.reason ?? "Validation failed";
3390
4357
  lastRetry = { error: errorMsg, output: rawOutput, parsed };
3391
4358
  if (attempt === maxRetries) {
4359
+ emitVerifyOutcome(false, attempt + 1, errorMsg);
3392
4360
  if (options?.fallback !== void 0) return options.fallback;
3393
4361
  throw new ValidationError(parsed, errorMsg, maxRetries);
3394
4362
  }
3395
4363
  continue;
3396
4364
  }
3397
4365
  }
4366
+ emitVerifyOutcome(true, attempt + 1);
3398
4367
  return parsed;
3399
4368
  } catch (err) {
3400
4369
  if (err instanceof ValidationError) {
@@ -3404,6 +4373,7 @@ ${summaryResponse.content}`
3404
4373
  parsed: err.lastOutput
3405
4374
  };
3406
4375
  if (attempt === maxRetries) {
4376
+ emitVerifyOutcome(false, attempt + 1, err.reason);
3407
4377
  if (options?.fallback !== void 0) return options.fallback;
3408
4378
  throw err;
3409
4379
  }
@@ -3412,6 +4382,7 @@ ${summaryResponse.content}`
3412
4382
  if (err instanceof VerifyError) {
3413
4383
  lastRetry = { error: err.message, output: rawOutput ?? err.lastOutput };
3414
4384
  if (attempt === maxRetries) {
4385
+ emitVerifyOutcome(false, attempt + 1, err.message);
3415
4386
  if (options?.fallback !== void 0) return options.fallback;
3416
4387
  throw err;
3417
4388
  }
@@ -3420,12 +4391,14 @@ ${summaryResponse.content}`
3420
4391
  const errorMsg = err instanceof ZodError ? err.message : err instanceof Error ? err.message : String(err);
3421
4392
  lastRetry = { error: errorMsg, output: rawOutput };
3422
4393
  if (attempt === maxRetries) {
4394
+ emitVerifyOutcome(false, attempt + 1, errorMsg);
3423
4395
  if (options?.fallback !== void 0) return options.fallback;
3424
4396
  const zodErr = err instanceof ZodError ? err : new ZodError([{ code: "custom", path: [], message: errorMsg }]);
3425
4397
  throw new VerifyError(rawOutput, zodErr, maxRetries);
3426
4398
  }
3427
4399
  }
3428
4400
  }
4401
+ emitVerifyOutcome(false, maxRetries + 1, lastRetry?.error ?? "Verify failed");
3429
4402
  if (options?.fallback !== void 0) return options.fallback;
3430
4403
  throw new VerifyError(
3431
4404
  lastRetry?.output,
@@ -3495,7 +4468,7 @@ ${summaryResponse.content}`
3495
4468
  }
3496
4469
  // ── ctx.race() ────────────────────────────────────────────────────────
3497
4470
  async race(fns, options) {
3498
- return this._checkpoint(() => {
4471
+ return this._checkpoint(this._autoCheckpointName("race"), () => {
3499
4472
  if (this.spanManager) {
3500
4473
  return this.spanManager.withSpanAsync(
3501
4474
  "axl.ctx.race",
@@ -3600,11 +4573,17 @@ ${summaryResponse.content}`
3600
4573
  }
3601
4574
  // ── ctx.parallel() ────────────────────────────────────────────────────
3602
4575
  async parallel(fns) {
3603
- return this._checkpoint(() => Promise.all(fns.map((fn) => fn())));
4576
+ return this._checkpoint(
4577
+ this._autoCheckpointName("parallel"),
4578
+ () => Promise.all(fns.map((fn) => fn()))
4579
+ );
3604
4580
  }
3605
4581
  // ── ctx.map() ─────────────────────────────────────────────────────────
3606
4582
  async map(items, fn, options) {
3607
- return this._checkpoint(() => this._mapImpl(items, fn, options));
4583
+ return this._checkpoint(
4584
+ this._autoCheckpointName("map"),
4585
+ () => this._mapImpl(items, fn, options)
4586
+ );
3608
4587
  }
3609
4588
  async _mapImpl(items, fn, options) {
3610
4589
  const concurrency = options?.concurrency ?? 5;
@@ -3682,10 +4661,14 @@ ${summaryResponse.content}`
3682
4661
  }
3683
4662
  async _awaitHumanImpl(options) {
3684
4663
  if (this.awaitHumanHandler) {
4664
+ this.emitEvent({
4665
+ type: "await_human",
4666
+ data: { channel: options.channel, prompt: options.prompt }
4667
+ });
3685
4668
  const decision2 = await this.awaitHumanHandler(options);
3686
- this.emitTrace({
3687
- type: "log",
3688
- data: { event: "await_human_resolved", channel: options.channel, decision: decision2 }
4669
+ this.emitEvent({
4670
+ type: "await_human_resolved",
4671
+ data: { channel: options.channel, decision: decision2 }
3689
4672
  });
3690
4673
  return decision2;
3691
4674
  }
@@ -3705,7 +4688,7 @@ ${summaryResponse.content}`
3705
4688
  await this.stateStore.saveExecutionState(this.executionId, {
3706
4689
  workflow: this.workflowName ?? "unknown",
3707
4690
  input: this.input,
3708
- step: this.stepCounter,
4691
+ step: this.stepRefRoot.value,
3709
4692
  status: "waiting",
3710
4693
  metadata: {
3711
4694
  ...this.metadata,
@@ -3714,26 +4697,111 @@ ${summaryResponse.content}`
3714
4697
  }
3715
4698
  });
3716
4699
  }
3717
- this.emitTrace({
3718
- type: "log",
3719
- data: { event: "await_human", channel: options.channel, prompt: options.prompt }
4700
+ this.emitEvent({
4701
+ type: "await_human",
4702
+ data: { channel: options.channel, prompt: options.prompt }
3720
4703
  });
3721
4704
  const decision = await new Promise((resolve) => {
3722
4705
  this.pendingDecisions.set(this.executionId, resolve);
3723
4706
  });
4707
+ this.emitEvent({
4708
+ type: "await_human_resolved",
4709
+ data: { channel: options.channel, decision }
4710
+ });
3724
4711
  if (this.stateStore) {
3725
4712
  await this.stateStore.saveExecutionState(this.executionId, {
3726
4713
  workflow: this.workflowName ?? "unknown",
3727
4714
  input: this.input,
3728
- step: this.stepCounter,
4715
+ step: this.stepRefRoot.value,
3729
4716
  status: "running"
3730
4717
  });
3731
4718
  }
3732
4719
  return decision;
3733
4720
  }
4721
+ /**
4722
+ * Compose an `AbortSignal` for a memory operation's underlying embedder
4723
+ * fetch. Combines the parent context signal (user cancellation,
4724
+ * `runtime.execute({ signal })`) with the budget hard_stop abort so
4725
+ * that either cause correctly aborts an in-flight embed call.
4726
+ *
4727
+ * Returns `undefined` if there's nothing to cancel on — the embedder
4728
+ * runs without a signal in that case, matching its prior behavior.
4729
+ *
4730
+ * @internal
4731
+ */
4732
+ _composeMemorySignal() {
4733
+ const budgetSignal = this.budgetContext?.abortController?.signal;
4734
+ if (this.signal && budgetSignal) return AbortSignal.any([this.signal, budgetSignal]);
4735
+ return this.signal ?? budgetSignal;
4736
+ }
4737
+ /**
4738
+ * Accumulate a cost amount into the active `budgetContext` and trip the
4739
+ * `exceeded` flag if we've crossed the limit. On `hard_stop` policy,
4740
+ * also fires the abort controller so in-flight operations cancel.
4741
+ *
4742
+ * Called from every code path that spends money: the `agent_call` loop,
4743
+ * semantic memory operations (`ctx.remember({embed:true})`, `ctx.recall({query})`),
4744
+ * and any future cost-emitting primitive. Centralizing the logic here
4745
+ * means `ctx.budget({ limit, policy })` accurately enforces the limit
4746
+ * across ALL cost sources — not just agent calls.
4747
+ *
4748
+ * @internal
4749
+ */
4750
+ _accumulateBudgetCost(amount) {
4751
+ if (!this.budgetContext) return;
4752
+ if (!Number.isFinite(amount) || amount <= 0) return;
4753
+ this.budgetContext.totalCost += amount;
4754
+ if (this.budgetContext.totalCost >= this.budgetContext.limit) {
4755
+ this.budgetContext.exceeded = true;
4756
+ if (this.budgetContext.policy === "hard_stop" && this.budgetContext.abortController) {
4757
+ this.budgetContext.abortController.abort();
4758
+ }
4759
+ }
4760
+ }
4761
+ // ── workflow lifecycle trace emission ──────────────────────────────────
4762
+ //
4763
+ // These are called by the runtime at execution boundaries. They emit
4764
+ // first-class `workflow_start` / `workflow_end` trace events instead of
4765
+ // the previous `ctx.log('workflow_start', ...)` indirection — so consumers
4766
+ // that narrow via `event.type === 'workflow_start'` actually see them.
4767
+ // Internal: the runtime is the only caller, user workflows never call these.
4768
+ /** @internal — idempotent; no-ops on second+ call within one ctx. */
4769
+ _emitWorkflowStart(input) {
4770
+ if (this._workflowStartEmitted) return;
4771
+ this._workflowStartEmitted = true;
4772
+ this.emitEvent({
4773
+ type: "workflow_start",
4774
+ workflow: this.workflowName,
4775
+ data: { input }
4776
+ });
4777
+ }
4778
+ /** @internal — idempotent; no-ops on second+ call within one ctx.
4779
+ * Protects against the post-emit-side-effect double-fire (reviewer
4780
+ * bug B1): `runtime.execute()` / `runtime.stream()` emit
4781
+ * `workflow_end(completed)`, then call `deleteCheckpoints` /
4782
+ * `persistExecution`. If either throws, the outer catch would
4783
+ * otherwise fire a second `workflow_end(failed)` with conflicting
4784
+ * status. First-wins semantics: the completed event stands, the
4785
+ * inner side-effect failure propagates as a normal thrown error. */
4786
+ _emitWorkflowEnd(info) {
4787
+ if (this._workflowEndEmitted) return;
4788
+ this._workflowEndEmitted = true;
4789
+ this.emitEvent({
4790
+ type: "workflow_end",
4791
+ workflow: this.workflowName,
4792
+ duration: info.duration,
4793
+ data: {
4794
+ status: info.status,
4795
+ duration: info.duration,
4796
+ ...info.result !== void 0 ? { result: info.result } : {},
4797
+ ...info.error !== void 0 ? { error: info.error } : {},
4798
+ ...info.aborted ? { aborted: true } : {}
4799
+ }
4800
+ });
4801
+ }
3734
4802
  // ── ctx.log() ─────────────────────────────────────────────────────────
3735
4803
  log(event, data) {
3736
- this.emitTrace({
4804
+ this.emitEvent({
3737
4805
  type: "log",
3738
4806
  data: {
3739
4807
  event,
@@ -3766,8 +4834,57 @@ ${summaryResponse.content}`
3766
4834
  if (!this.stateStore) {
3767
4835
  throw new Error("A state store is required for memory operations.");
3768
4836
  }
4837
+ if (this.budgetContext?.exceeded) {
4838
+ const { limit, totalCost: spent, policy } = this.budgetContext;
4839
+ if (policy !== "warn") {
4840
+ throw new BudgetExceededError(limit, spent, policy);
4841
+ }
4842
+ }
3769
4843
  const sessionId = this.metadata?.sessionId;
3770
- await this.memoryManager.remember(key, value, this.stateStore, sessionId, options);
4844
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
4845
+ try {
4846
+ const memorySignal = this._composeMemorySignal();
4847
+ const { usage } = await this.memoryManager.remember(
4848
+ key,
4849
+ value,
4850
+ this.stateStore,
4851
+ sessionId,
4852
+ options,
4853
+ memorySignal
4854
+ );
4855
+ if (usage?.cost != null) {
4856
+ this._accumulateBudgetCost(usage.cost);
4857
+ }
4858
+ this.emitEvent({
4859
+ type: "memory_remember",
4860
+ ...usage?.cost != null ? { cost: usage.cost } : {},
4861
+ ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
4862
+ data: {
4863
+ key,
4864
+ scope,
4865
+ embed: options?.embed === true,
4866
+ ...usage ? { usage } : {}
4867
+ }
4868
+ });
4869
+ } catch (err) {
4870
+ const partialUsage = err.axlEmbedUsage;
4871
+ if (partialUsage?.cost != null) {
4872
+ this._accumulateBudgetCost(partialUsage.cost);
4873
+ }
4874
+ this.emitEvent({
4875
+ type: "memory_remember",
4876
+ ...partialUsage?.cost != null ? { cost: partialUsage.cost } : {},
4877
+ ...partialUsage?.tokens != null ? { tokens: { input: partialUsage.tokens } } : {},
4878
+ data: {
4879
+ key,
4880
+ scope,
4881
+ embed: options?.embed === true,
4882
+ ...partialUsage ? { usage: partialUsage } : {},
4883
+ error: err instanceof Error ? err.message : String(err)
4884
+ }
4885
+ });
4886
+ throw err;
4887
+ }
3771
4888
  }
3772
4889
  /**
3773
4890
  * Recall a value from memory by key, or perform semantic search if query option is provided.
@@ -3781,8 +4898,62 @@ ${summaryResponse.content}`
3781
4898
  if (!this.stateStore) {
3782
4899
  throw new Error("A state store is required for memory operations.");
3783
4900
  }
4901
+ if (this.budgetContext?.exceeded) {
4902
+ const { limit, totalCost: spent, policy } = this.budgetContext;
4903
+ if (policy !== "warn") {
4904
+ throw new BudgetExceededError(limit, spent, policy);
4905
+ }
4906
+ }
3784
4907
  const sessionId = this.metadata?.sessionId;
3785
- return this.memoryManager.recall(key, this.stateStore, sessionId, options);
4908
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
4909
+ const semantic = options?.query !== void 0;
4910
+ try {
4911
+ const memorySignal = this._composeMemorySignal();
4912
+ const { data, usage } = await this.memoryManager.recall(
4913
+ key,
4914
+ this.stateStore,
4915
+ sessionId,
4916
+ options,
4917
+ memorySignal
4918
+ );
4919
+ let hit;
4920
+ let resultCount;
4921
+ if (semantic) {
4922
+ resultCount = Array.isArray(data) ? data.length : 0;
4923
+ hit = resultCount > 0;
4924
+ } else {
4925
+ hit = data !== null && data !== void 0;
4926
+ }
4927
+ if (usage?.cost != null) {
4928
+ this._accumulateBudgetCost(usage.cost);
4929
+ }
4930
+ this.emitEvent({
4931
+ type: "memory_recall",
4932
+ ...usage?.cost != null ? { cost: usage.cost } : {},
4933
+ ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
4934
+ data: {
4935
+ key,
4936
+ scope,
4937
+ semantic,
4938
+ hit,
4939
+ ...resultCount !== void 0 ? { resultCount, count: resultCount } : {},
4940
+ ...usage ? { usage } : {}
4941
+ }
4942
+ });
4943
+ return data;
4944
+ } catch (err) {
4945
+ this.emitEvent({
4946
+ type: "memory_recall",
4947
+ data: {
4948
+ key,
4949
+ scope,
4950
+ semantic,
4951
+ hit: false,
4952
+ error: err instanceof Error ? err.message : String(err)
4953
+ }
4954
+ });
4955
+ throw err;
4956
+ }
3786
4957
  }
3787
4958
  /** Delete a memory entry by key. */
3788
4959
  async forget(key, options) {
@@ -3795,7 +4966,24 @@ ${summaryResponse.content}`
3795
4966
  throw new Error("A state store is required for memory operations.");
3796
4967
  }
3797
4968
  const sessionId = this.metadata?.sessionId;
3798
- await this.memoryManager.forget(key, this.stateStore, sessionId, options);
4969
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
4970
+ try {
4971
+ await this.memoryManager.forget(key, this.stateStore, sessionId, options);
4972
+ this.emitEvent({
4973
+ type: "memory_forget",
4974
+ data: { key, scope }
4975
+ });
4976
+ } catch (err) {
4977
+ this.emitEvent({
4978
+ type: "memory_forget",
4979
+ data: {
4980
+ key,
4981
+ scope,
4982
+ error: err instanceof Error ? err.message : String(err)
4983
+ }
4984
+ });
4985
+ throw err;
4986
+ }
3799
4987
  }
3800
4988
  // ── ctx.delegate() ──────────────────────────────────────────────────
3801
4989
  /**
@@ -3823,6 +5011,15 @@ ${summaryResponse.content}`
3823
5011
  names.add(a._name);
3824
5012
  }
3825
5013
  if (agents.length === 1) {
5014
+ this.emitEvent({
5015
+ type: "delegate",
5016
+ agent: agents[0]._name,
5017
+ data: {
5018
+ candidates: [agents[0]._name],
5019
+ selected: agents[0]._name,
5020
+ reason: "single_candidate"
5021
+ }
5022
+ });
3826
5023
  return this.ask(agents[0], prompt, {
3827
5024
  schema: options?.schema,
3828
5025
  retries: options?.retries,
@@ -3858,12 +5055,13 @@ ${summaryResponse.content}`
3858
5055
  resolveModel: () => routerModelUri,
3859
5056
  resolveSystem: () => routerSystem
3860
5057
  };
3861
- this.emitTrace({
5058
+ this.emitEvent({
3862
5059
  type: "delegate",
3863
5060
  agent: "_delegate_router",
3864
5061
  data: {
3865
5062
  candidates: agents.map((a) => a._name),
3866
- routerModel: routerModelUri
5063
+ routerModel: routerModelUri,
5064
+ reason: "routed"
3867
5065
  }
3868
5066
  });
3869
5067
  return this.ask(routerAgent, prompt, {
@@ -3875,24 +5073,57 @@ ${summaryResponse.content}`
3875
5073
  });
3876
5074
  }
3877
5075
  // ── Private ───────────────────────────────────────────────────────────
3878
- emitTrace(partial) {
3879
- let data = partial.data;
3880
- if (this.config.trace?.redact && partial.type === "agent_call" && data) {
3881
- data = { ...data, prompt: "[redacted]", response: "[redacted]" };
5076
+ /**
5077
+ * Internal emitter input — intentionally loose so call sites don't need to
5078
+ * build a perfectly-narrowed discriminated-union member. The resulting
5079
+ * `AxlEvent` (exported type) remains strict, and TypeScript narrows it at
5080
+ * consumer sites via the `type` discriminator.
5081
+ */
5082
+ emitEvent(partial) {
5083
+ const data = partial.data;
5084
+ const frame = askStorage.getStore();
5085
+ const step = (frame?.stepRef ?? this.stepRefRoot).value++;
5086
+ const cost = partial.cost;
5087
+ if (frame && typeof cost === "number" && Number.isFinite(cost) && cost >= 0 && COST_BEARING_LEAF_TYPES.includes(partial.type)) {
5088
+ frame.askCost.value += cost;
3882
5089
  }
3883
5090
  const event = {
3884
5091
  executionId: this.executionId,
3885
- step: this.stepCounter++,
5092
+ step,
3886
5093
  timestamp: Date.now(),
5094
+ ...this.workflowName ? { workflow: this.workflowName } : {},
5095
+ // Stamp ask correlation from ALS. Variants like `workflow_start` /
5096
+ // `workflow_end` and out-of-ask events (log/memory/checkpoint/await_human
5097
+ // when emitted at workflow scope) may legitimately have no frame —
5098
+ // they still get `depth: 0` so consumers can render them at root
5099
+ // level without having to guess. `askId`/`parentAskId`/`agent` stay
5100
+ // unset for non-ask-scoped events, matching the `Partial<AskScoped>`
5101
+ // shape on those union members.
5102
+ ...frame ? {
5103
+ askId: frame.askId,
5104
+ ...frame.parentAskId ? { parentAskId: frame.parentAskId } : {},
5105
+ depth: frame.depth,
5106
+ ...frame.agent ? { agent: frame.agent } : {}
5107
+ } : { depth: 0 },
3887
5108
  ...partial,
3888
5109
  data
3889
5110
  };
3890
- this.onTrace?.(event);
5111
+ const finalEvent = this.config.trace?.redact ? redactEvent(event) : event;
5112
+ if (this.onTrace) {
5113
+ try {
5114
+ this.onTrace(finalEvent);
5115
+ } catch (err) {
5116
+ console.error(
5117
+ "[axl] onTrace handler threw; trace event dropped:",
5118
+ err instanceof Error ? err.message : String(err)
5119
+ );
5120
+ }
5121
+ }
3891
5122
  }
3892
5123
  };
3893
5124
 
3894
5125
  // src/agent.ts
3895
- import { randomUUID } from "crypto";
5126
+ import { randomUUID as randomUUID2 } from "crypto";
3896
5127
  var agentCounter = 0;
3897
5128
  function agent(config) {
3898
5129
  agentCounter++;
@@ -3912,7 +5143,7 @@ function agent(config) {
3912
5143
  const registry = new ProviderRegistry();
3913
5144
  const ctx = new WorkflowContext({
3914
5145
  input: prompt,
3915
- executionId: randomUUID(),
5146
+ executionId: randomUUID2(),
3916
5147
  config: {},
3917
5148
  providerRegistry: registry
3918
5149
  });
@@ -3936,7 +5167,7 @@ function workflow(config) {
3936
5167
 
3937
5168
  // src/runtime.ts
3938
5169
  import { EventEmitter as EventEmitter2 } from "events";
3939
- import { randomUUID as randomUUID2 } from "crypto";
5170
+ import { randomUUID as randomUUID3 } from "crypto";
3940
5171
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3941
5172
  import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
3942
5173
  import { dirname } from "path";
@@ -3959,29 +5190,20 @@ var MemoryStore = class {
3959
5190
  constructor() {
3960
5191
  this.loadPersistedState();
3961
5192
  }
3962
- async saveCheckpoint(executionId, step, data) {
3963
- let steps = this.checkpoints.get(executionId);
3964
- if (!steps) {
3965
- steps = /* @__PURE__ */ new Map();
3966
- this.checkpoints.set(executionId, steps);
5193
+ async saveCheckpoint(executionId, name, data) {
5194
+ let entries = this.checkpoints.get(executionId);
5195
+ if (!entries) {
5196
+ entries = /* @__PURE__ */ new Map();
5197
+ this.checkpoints.set(executionId, entries);
3967
5198
  }
3968
- steps.set(step, structuredClone(data));
5199
+ entries.set(name, structuredClone(data));
3969
5200
  }
3970
- async getCheckpoint(executionId, step) {
3971
- const steps = this.checkpoints.get(executionId);
3972
- if (!steps) return null;
3973
- const data = steps.get(step);
5201
+ async getCheckpoint(executionId, name) {
5202
+ const entries = this.checkpoints.get(executionId);
5203
+ if (!entries) return null;
5204
+ const data = entries.get(name);
3974
5205
  return data !== void 0 ? structuredClone(data) : null;
3975
5206
  }
3976
- async getLatestCheckpoint(executionId) {
3977
- const steps = this.checkpoints.get(executionId);
3978
- if (!steps || steps.size === 0) return null;
3979
- let maxStep = -1;
3980
- for (const step of steps.keys()) {
3981
- if (step > maxStep) maxStep = step;
3982
- }
3983
- return { step: maxStep, data: structuredClone(steps.get(maxStep)) };
3984
- }
3985
5207
  async saveSession(sessionId, history) {
3986
5208
  this.sessions.set(sessionId, structuredClone(history));
3987
5209
  }
@@ -4154,7 +5376,7 @@ function rowToExecutionInfo(row) {
4154
5376
  completedAt: row.completed_at ?? void 0,
4155
5377
  duration: row.duration,
4156
5378
  error: row.error ?? void 0,
4157
- steps: safeJsonParse(row.steps) ?? []
5379
+ events: safeJsonParse(row.events) ?? []
4158
5380
  };
4159
5381
  }
4160
5382
  var SQLiteStore = class {
@@ -4170,15 +5392,80 @@ var SQLiteStore = class {
4170
5392
  }
4171
5393
  this.db = new BetterSqlite3(filePath);
4172
5394
  this.db.pragma("journal_mode = WAL");
5395
+ this.migrate();
4173
5396
  this.initTables();
4174
5397
  }
5398
+ /**
5399
+ * Idempotent schema migration. Tracks version via `PRAGMA user_version`
5400
+ * so reopens skip applied steps. Wrapped in `BEGIN IMMEDIATE` so
5401
+ * concurrent `SQLiteStore` constructors serialize at the file level
5402
+ * and rollback on failure leaves the DB in a clean pre-migration
5403
+ * state.
5404
+ *
5405
+ * Migration history:
5406
+ * - v1 (spec/16): rename `execution_history.steps` → `events`. The
5407
+ * JS-level `ExecutionInfo.steps` was renamed to `.events` in PR 1
5408
+ * commit 1; this brings the on-disk schema in line.
5409
+ * - v2 (named checkpoints): rename `checkpoints.step` (INTEGER) →
5410
+ * `checkpoints.name` (TEXT). The runtime now requires
5411
+ * `ctx.checkpoint(name, fn)` so the per-context numeric counter is
5412
+ * gone — names are caller-supplied stable identifiers. Existing
5413
+ * numeric step values become stringified names ("0", "1", …) to
5414
+ * preserve replay continuity for in-flight executions across the
5415
+ * upgrade boundary.
5416
+ *
5417
+ * Applied BEFORE `initTables()` so the subsequent `CREATE TABLE
5418
+ * IF NOT EXISTS` runs against the post-migration column name. Fresh
5419
+ * installs just create the table with `events` directly and bump
5420
+ * `user_version` to the current version.
5421
+ *
5422
+ * TOCTOU guard (review B-3): the initial `user_version` read is
5423
+ * informational only — a concurrent process can complete the
5424
+ * migration between that read and our `BEGIN IMMEDIATE`. We
5425
+ * re-read `user_version` inside the transaction and short-circuit
5426
+ * if it's already at target, so the non-idempotent half (writes
5427
+ * that went beyond a simple ALTER) never double-applies under a
5428
+ * race. The current v0→v1 step IS idempotent via the column-
5429
+ * presence check, but future migrations may not be, and the
5430
+ * transactional re-read costs nothing.
5431
+ */
5432
+ migrate() {
5433
+ const TARGET_VERSION = 2;
5434
+ const current = this.db.pragma("user_version", { simple: true });
5435
+ if (current >= TARGET_VERSION) return;
5436
+ this.db.exec("BEGIN IMMEDIATE");
5437
+ try {
5438
+ const committed = this.db.pragma("user_version", { simple: true });
5439
+ if (committed >= TARGET_VERSION) {
5440
+ this.db.exec("COMMIT");
5441
+ return;
5442
+ }
5443
+ if (committed < 1) {
5444
+ const cols = this.db.pragma("table_info(execution_history)");
5445
+ if (cols.some((c) => c.name === "steps") && !cols.some((c) => c.name === "events")) {
5446
+ this.db.exec("ALTER TABLE execution_history RENAME COLUMN steps TO events");
5447
+ }
5448
+ }
5449
+ if (committed < 2) {
5450
+ const cols = this.db.pragma("table_info(checkpoints)");
5451
+ if (cols.some((c) => c.name === "step") && !cols.some((c) => c.name === "name")) {
5452
+ this.db.exec("ALTER TABLE checkpoints RENAME COLUMN step TO name");
5453
+ }
5454
+ }
5455
+ this.db.pragma(`user_version = ${TARGET_VERSION}`);
5456
+ this.db.exec("COMMIT");
5457
+ } catch (err) {
5458
+ this.db.exec("ROLLBACK");
5459
+ throw err;
5460
+ }
5461
+ }
4175
5462
  initTables() {
4176
5463
  this.db.exec(`
4177
5464
  CREATE TABLE IF NOT EXISTS checkpoints (
4178
5465
  execution_id TEXT NOT NULL,
4179
- step INTEGER NOT NULL,
5466
+ name TEXT NOT NULL,
4180
5467
  data TEXT NOT NULL,
4181
- PRIMARY KEY (execution_id, step)
5468
+ PRIMARY KEY (execution_id, name)
4182
5469
  );
4183
5470
 
4184
5471
  CREATE TABLE IF NOT EXISTS sessions (
@@ -4227,7 +5514,7 @@ var SQLiteStore = class {
4227
5514
  completed_at INTEGER,
4228
5515
  duration INTEGER NOT NULL DEFAULT 0,
4229
5516
  error TEXT,
4230
- steps TEXT NOT NULL
5517
+ events TEXT NOT NULL
4231
5518
  );
4232
5519
 
4233
5520
  CREATE TABLE IF NOT EXISTS eval_history (
@@ -4242,26 +5529,19 @@ var SQLiteStore = class {
4242
5529
  `);
4243
5530
  }
4244
5531
  // ── Checkpoints ────────────────────────────────────────────────────────
4245
- async saveCheckpoint(executionId, step, data) {
5532
+ async saveCheckpoint(executionId, name, data) {
4246
5533
  const stmt = this.db.prepare(
4247
- "INSERT OR REPLACE INTO checkpoints (execution_id, step, data) VALUES (?, ?, ?)"
5534
+ "INSERT OR REPLACE INTO checkpoints (execution_id, name, data) VALUES (?, ?, ?)"
4248
5535
  );
4249
- stmt.run(executionId, step, JSON.stringify(data));
5536
+ stmt.run(executionId, name, JSON.stringify(data));
4250
5537
  }
4251
- async getCheckpoint(executionId, step) {
5538
+ async getCheckpoint(executionId, name) {
4252
5539
  const stmt = this.db.prepare(
4253
- "SELECT data FROM checkpoints WHERE execution_id = ? AND step = ?"
5540
+ "SELECT data FROM checkpoints WHERE execution_id = ? AND name = ?"
4254
5541
  );
4255
- const row = stmt.get(executionId, step);
5542
+ const row = stmt.get(executionId, name);
4256
5543
  return row ? safeJsonParse(row.data) : null;
4257
5544
  }
4258
- async getLatestCheckpoint(executionId) {
4259
- const stmt = this.db.prepare(
4260
- "SELECT step, data FROM checkpoints WHERE execution_id = ? ORDER BY step DESC LIMIT 1"
4261
- );
4262
- const row = stmt.get(executionId);
4263
- return row ? { step: row.step, data: safeJsonParse(row.data) } : null;
4264
- }
4265
5545
  // ── Sessions ────────────────────────────────────────────────────────────
4266
5546
  async saveSession(sessionId, history) {
4267
5547
  const stmt = this.db.prepare(
@@ -4355,7 +5635,7 @@ var SQLiteStore = class {
4355
5635
  // ── Execution History ────────────────────────────────────────────────────
4356
5636
  async saveExecution(execution) {
4357
5637
  this.db.prepare(
4358
- "INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, steps) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
5638
+ "INSERT OR REPLACE INTO execution_history (execution_id, workflow, status, total_cost, started_at, completed_at, duration, error, events) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
4359
5639
  ).run(
4360
5640
  execution.executionId,
4361
5641
  execution.workflow,
@@ -4365,7 +5645,7 @@ var SQLiteStore = class {
4365
5645
  execution.completedAt ?? null,
4366
5646
  execution.duration,
4367
5647
  execution.error ?? null,
4368
- JSON.stringify(execution.steps)
5648
+ JSON.stringify(execution.events)
4369
5649
  );
4370
5650
  }
4371
5651
  async getExecution(executionId) {
@@ -4539,7 +5819,8 @@ var Session = class _Session {
4539
5819
  }
4540
5820
  };
4541
5821
  axlStream.on("done", (event) => {
4542
- updateHistory(event.data).catch((err) => {
5822
+ const data = event.data;
5823
+ updateHistory(data.result).catch((err) => {
4543
5824
  this.runtime.emit("error", {
4544
5825
  type: "session_history_save_failed",
4545
5826
  sessionId: this.sessionId,
@@ -4581,9 +5862,88 @@ var Session = class _Session {
4581
5862
  // src/stream.ts
4582
5863
  import { Readable } from "stream";
4583
5864
  import { EventEmitter } from "events";
5865
+
5866
+ // src/types.ts
5867
+ var AXL_EVENT_TYPES = [
5868
+ // Workflow lifecycle
5869
+ "workflow_start",
5870
+ "workflow_end",
5871
+ // Ask boundary
5872
+ "ask_start",
5873
+ "ask_end",
5874
+ // Agent turn lifecycle
5875
+ "agent_call_start",
5876
+ "agent_call_end",
5877
+ // Content delivery (stream-only)
5878
+ "token",
5879
+ // Tool invocation lifecycle
5880
+ "tool_call_start",
5881
+ "tool_call_end",
5882
+ // Single-point tool events
5883
+ "tool_approval",
5884
+ "tool_denied",
5885
+ // Delegation
5886
+ "delegate",
5887
+ // Handoff (spans two asks; not AskScoped).
5888
+ // `handoff_start` emits before the target ask begins; always fired.
5889
+ // `handoff_return` emits when control returns to source; roundtrip only
5890
+ // (oneway handoffs have no return trip — the target's `ask_end` IS the
5891
+ // end of the chain).
5892
+ "handoff_start",
5893
+ "handoff_return",
5894
+ // Pipeline (retry/validation lifecycle) — added in PR 2; reserved here.
5895
+ "pipeline",
5896
+ // Progressive structured output — added in PR 2; reserved here.
5897
+ "partial_object",
5898
+ // Verification
5899
+ "verify",
5900
+ // Observability
5901
+ "log",
5902
+ "memory_remember",
5903
+ "memory_recall",
5904
+ "memory_forget",
5905
+ // Durable execution checkpoints (`ctx.checkpoint`)
5906
+ "checkpoint_save",
5907
+ "checkpoint_replay",
5908
+ // Human-in-the-loop (`ctx.awaitHuman`)
5909
+ "await_human",
5910
+ "await_human_resolved",
5911
+ // Legacy gate events — emitted by current code; collapsed into `pipeline`
5912
+ // in PR 2 (spec/16-streaming-wire-reliability §4.2).
5913
+ "guardrail",
5914
+ "schema_check",
5915
+ "validate",
5916
+ // Terminal workflow markers
5917
+ "done",
5918
+ "error"
5919
+ ];
5920
+
5921
+ // src/stream.ts
4584
5922
  var AxlStream = class _AxlStream extends Readable {
4585
5923
  bus = new EventEmitter();
4586
- tokens = [];
5924
+ /**
5925
+ * Per-ask token buffers split into "in-progress" and "committed"
5926
+ * halves so `fullText` only includes tokens from attempts that
5927
+ * actually won. Scoping is per-`askId` so concurrent root-level asks
5928
+ * (`ctx.parallel`, `ctx.spawn`, `ctx.race`, `ctx.map`) don't
5929
+ * interleave each other's tokens — each branch's chunks stay
5930
+ * contiguous, and a `pipeline(failed)` on one branch only discards
5931
+ * THAT branch's in-progress buffer (previously: shared buffer caused
5932
+ * a failure on one branch to discard a peer's in-flight successful
5933
+ * tokens). Insertion order in the Maps reflects which ask emitted
5934
+ * its first token first, which is what `fullText` joins on.
5935
+ *
5936
+ * On `pipeline(status: 'committed')` for an ask, that ask's
5937
+ * in-progress entry flushes to its committed entry. On
5938
+ * `pipeline(status: 'failed')` or `ask_end({ok:false})` for an ask,
5939
+ * that ask's in-progress entry is discarded. Spec/16 §4.3.
5940
+ *
5941
+ * Tokens emitted outside any ask (synthesized test fixtures with no
5942
+ * `askId`) fall back to an empty-string sentinel key — preserves the
5943
+ * single-buffer behavior for that legacy case.
5944
+ */
5945
+ attemptByAsk = /* @__PURE__ */ new Map();
5946
+ committedByAsk = /* @__PURE__ */ new Map();
4587
5947
  result = void 0;
4588
5948
  finished = false;
4589
5949
  promise;
@@ -4605,18 +5965,10 @@ var AxlStream = class _AxlStream extends Readable {
4605
5965
  this.promise.catch(() => {
4606
5966
  });
4607
5967
  }
4608
- static STREAM_EVENTS = /* @__PURE__ */ new Set([
4609
- "token",
4610
- "tool_call",
4611
- "tool_result",
4612
- "tool_approval",
4613
- "agent_start",
4614
- "agent_end",
4615
- "handoff",
4616
- "step",
4617
- "done",
4618
- "error"
4619
- ]);
5968
+ /** Wire-format event names callers can subscribe to via `.on(name, fn)`.
5969
+ * Derived from the canonical `AXL_EVENT_TYPES` tuple — adding a new
5970
+ * variant in `types.ts` automatically extends the subscribable set. */
5971
+ static STREAM_EVENTS = new Set(AXL_EVENT_TYPES);
4620
5972
  on(event, handler) {
4621
5973
  if (_AxlStream.STREAM_EVENTS.has(event)) {
4622
5974
  this.bus.on(event, handler);
@@ -4666,21 +6018,83 @@ var AxlStream = class _AxlStream extends Readable {
4666
6018
  while (true) {
4667
6019
  const { value, done } = await iter.next();
4668
6020
  if (done) return { value: void 0, done: true };
4669
- if (value.type === "token") return { value: value.data, done: false };
6021
+ if (value.type === "token" && isRootLevel(value)) {
6022
+ return { value: value.data, done: false };
6023
+ }
6024
+ }
6025
+ }
6026
+ };
6027
+ }
6028
+ };
6029
+ }
6030
+ /**
6031
+ * Iterator over `{ askId, text }` pairs — one emission per token chunk,
6032
+ * tagged with the ask frame that produced it. Complements `.text`
6033
+ * (root-only stream): consumers building a split-pane UI that shows
6034
+ * each sub-agent's output in its own lane can group by `askId`
6035
+ * without iterating the raw stream and hand-filtering on `event.type`.
6036
+ *
6037
+ * Nested and root tokens both flow through. The `agent` field names
6038
+ * the producing agent for UI labelling; it's undefined when the token
6039
+ * was emitted outside any ask (rare — synthesized test fixtures).
6040
+ */
6041
+ get textByAsk() {
6042
+ const self = this;
6043
+ return {
6044
+ [Symbol.asyncIterator]() {
6045
+ const iter = self[Symbol.asyncIterator]();
6046
+ return {
6047
+ async next() {
6048
+ while (true) {
6049
+ const { value, done } = await iter.next();
6050
+ if (done) {
6051
+ return {
6052
+ value: void 0,
6053
+ done: true
6054
+ };
6055
+ }
6056
+ if (value.type === "token") {
6057
+ return {
6058
+ value: { askId: value.askId, agent: value.agent, text: value.data },
6059
+ done: false
6060
+ };
6061
+ }
4670
6062
  }
4671
6063
  }
4672
6064
  };
4673
6065
  }
4674
6066
  };
4675
6067
  }
4676
- get steps() {
4677
- const stepTypes = /* @__PURE__ */ new Set([
4678
- "agent_start",
4679
- "agent_end",
4680
- "tool_call",
4681
- "tool_result",
6068
+ /**
6069
+ * Iterator over structural lifecycle events only — skips per-token chatter
6070
+ * and progressive partial_object emissions. Useful for waterfall UIs and
6071
+ * any consumer that wants the "what happened" timeline without per-chunk
6072
+ * noise. Renamed from `.steps` in the unified-event-model migration
6073
+ * because these are events, not pipeline steps.
6074
+ */
6075
+ get lifecycle() {
6076
+ const lifecycleTypes = /* @__PURE__ */ new Set([
6077
+ "ask_start",
6078
+ "ask_end",
6079
+ "agent_call_start",
6080
+ "agent_call_end",
6081
+ "tool_call_start",
6082
+ "tool_call_end",
4682
6083
  "tool_approval",
4683
- "handoff"
6084
+ "tool_denied",
6085
+ "handoff_start",
6086
+ "handoff_return",
6087
+ "delegate",
6088
+ "pipeline",
6089
+ "verify",
6090
+ "workflow_start",
6091
+ "workflow_end",
6092
+ // Durable-execution checkpoints — structural points in the timeline.
6093
+ "checkpoint_save",
6094
+ "checkpoint_replay",
6095
+ // Human-in-the-loop — pause/resume are major timeline landmarks.
6096
+ "await_human",
6097
+ "await_human_resolved"
4684
6098
  ]);
4685
6099
  const self = this;
4686
6100
  return {
@@ -4691,7 +6105,7 @@ var AxlStream = class _AxlStream extends Readable {
4691
6105
  while (true) {
4692
6106
  const { value, done } = await iter.next();
4693
6107
  if (done) return { value: void 0, done: true };
4694
- if (stepTypes.has(value.type)) return { value, done: false };
6108
+ if (lifecycleTypes.has(value.type)) return { value, done: false };
4695
6109
  }
4696
6110
  }
4697
6111
  };
@@ -4711,10 +6125,34 @@ var AxlStream = class _AxlStream extends Readable {
4711
6125
  });
4712
6126
  return destination;
4713
6127
  }
4714
- /** Push a stream event. Called by the runtime. */
6128
+ /** Push an event onto the stream. Called by the runtime. */
4715
6129
  _push(event) {
4716
6130
  if (this.finished) return;
4717
- if (event.type === "token") this.tokens.push(event.data);
6131
+ if (event.type === "token" && isRootLevel(event)) {
6132
+ const key = event.askId ?? "";
6133
+ const attempt = this.attemptByAsk.get(key);
6134
+ if (attempt) {
6135
+ attempt.push(event.data);
6136
+ } else {
6137
+ this.attemptByAsk.set(key, [event.data]);
6138
+ }
6139
+ }
6140
+ if (event.type === "pipeline" && isRootLevel(event)) {
6141
+ const key = event.askId ?? "";
6142
+ if (event.status === "committed") {
6143
+ const attempt = this.attemptByAsk.get(key);
6144
+ if (attempt && attempt.length > 0) {
6145
+ const prev = this.committedByAsk.get(key) ?? "";
6146
+ this.committedByAsk.set(key, prev + attempt.join(""));
6147
+ this.attemptByAsk.set(key, []);
6148
+ }
6149
+ } else if (event.status === "failed") {
6150
+ this.attemptByAsk.set(key, []);
6151
+ }
6152
+ }
6153
+ if (event.type === "ask_end" && isRootLevel(event) && !event.outcome.ok) {
6154
+ this.attemptByAsk.set(event.askId ?? "", []);
6155
+ }
4718
6156
  this.bus.emit(event.type, event);
4719
6157
  this.push(event);
4720
6158
  const waiter = this.waiters.shift();
@@ -4724,12 +6162,25 @@ var AxlStream = class _AxlStream extends Readable {
4724
6162
  this.eventQueue.push(event);
4725
6163
  }
4726
6164
  }
4727
- /** Signal successful completion. */
4728
- _done(result) {
6165
+ /**
6166
+ * Signal successful completion.
6167
+ *
6168
+ * `executionId` is required — the runtime must allocate it before
6169
+ * calling `stream()` so terminal events always carry a real id.
6170
+ * Previously the default-empty parameter surfaced blank executionIds
6171
+ * on error paths that threw before `execInfo` was assigned (review S4).
6172
+ */
6173
+ _done(result, executionId) {
4729
6174
  if (this.finished) return;
4730
6175
  this.finished = true;
4731
6176
  this.result = result;
4732
- const doneEvent = { type: "done", data: result };
6177
+ const doneEvent = {
6178
+ type: "done",
6179
+ executionId,
6180
+ step: Number.MAX_SAFE_INTEGER,
6181
+ timestamp: Date.now(),
6182
+ data: { result }
6183
+ };
4733
6184
  this.bus.emit("done", doneEvent);
4734
6185
  this.push(doneEvent);
4735
6186
  this.push(null);
@@ -4745,11 +6196,19 @@ var AxlStream = class _AxlStream extends Readable {
4745
6196
  this.waiters.length = 0;
4746
6197
  this.bus.emit("__resolve", result);
4747
6198
  }
4748
- /** Signal an error. */
4749
- _error(error) {
6199
+ /** Signal an error. `executionId` is required for the same reason as
6200
+ * `_done`: terminal events must carry a real id even when the
6201
+ * failure happens before any real trace event fires (review S4). */
6202
+ _error(error, executionId) {
4750
6203
  if (this.finished) return;
4751
6204
  this.finished = true;
4752
- const errorEvent = { type: "error", message: error.message };
6205
+ const errorEvent = {
6206
+ type: "error",
6207
+ executionId,
6208
+ step: Number.MAX_SAFE_INTEGER,
6209
+ timestamp: Date.now(),
6210
+ data: { message: error.message, name: error.name }
6211
+ };
4753
6212
  this.bus.emit("error", errorEvent);
4754
6213
  this.push(errorEvent);
4755
6214
  this.push(null);
@@ -4765,8 +6224,31 @@ var AxlStream = class _AxlStream extends Readable {
4765
6224
  this.waiters.length = 0;
4766
6225
  this.bus.emit("__reject", error);
4767
6226
  }
6227
+ /** Concatenated root-only text from committed attempts plus the
6228
+ * current in-flight attempt(s). Retried (gate-rejected) attempts
6229
+ * are excluded — see spec/16 §4.3. Reading mid-attempt returns the
6230
+ * in-progress text; reading after `pipeline(committed)` (which
6231
+ * fires before `done`) returns the canonical winning text.
6232
+ *
6233
+ * With concurrent root-level asks (`ctx.parallel`, `ctx.spawn`,
6234
+ * `ctx.race`, `ctx.map`), each branch's tokens are scoped per-`askId`
6235
+ * and emitted contiguously in the order each ask first started
6236
+ * emitting tokens. A `pipeline(failed)` or `ask_end({ok:false})` on
6237
+ * one branch only discards THAT branch's in-progress buffer —
6238
+ * sibling branches are unaffected. For UIs that want each sub-agent
6239
+ * in its own lane, prefer `textByAsk`. */
4768
6240
  get fullText() {
4769
- return this.tokens.join("");
6241
+ let out = "";
6242
+ for (const [key, committed] of this.committedByAsk) {
6243
+ out += committed;
6244
+ const attempt = this.attemptByAsk.get(key);
6245
+ if (attempt && attempt.length > 0) out += attempt.join("");
6246
+ }
6247
+ for (const [key, attempt] of this.attemptByAsk) {
6248
+ if (this.committedByAsk.has(key)) continue;
6249
+ if (attempt.length > 0) out += attempt.join("");
6250
+ }
6251
+ return out;
4770
6252
  }
4771
6253
  };
4772
6254
 
@@ -5129,6 +6611,18 @@ var McpManager = class {
5129
6611
  };
5130
6612
 
5131
6613
  // src/memory/manager.ts
6614
+ function assertEmbedResult(result) {
6615
+ if (Array.isArray(result)) {
6616
+ throw new Error(
6617
+ "Embedder.embed() returned a bare number[][] (legacy shape). As of @axlsdk/axl 0.15, Embedder.embed() must return { vectors: number[][]; usage?: { tokens?, cost?, model? } }. Wrap your existing return value: `return { vectors: await old() }`."
6618
+ );
6619
+ }
6620
+ if (!result || typeof result !== "object" || !Array.isArray(result.vectors)) {
6621
+ throw new Error(
6622
+ `Embedder.embed() must return { vectors: number[][]; usage?: EmbedUsage }. Got ${result === null ? "null" : typeof result}.`
6623
+ );
6624
+ }
6625
+ }
5132
6626
  var MemoryManager = class {
5133
6627
  vectorStore;
5134
6628
  embedder;
@@ -5143,8 +6637,11 @@ var MemoryManager = class {
5143
6637
  * Store a key-value pair in memory.
5144
6638
  * If a vector store and embedder are configured and embed is true,
5145
6639
  * also embeds the value for semantic search.
6640
+ *
6641
+ * Returns `{ usage }` when embedding happened, carrying the embedder's
6642
+ * cost/token reporting out to the caller for cost attribution.
5146
6643
  */
5147
- async remember(key, value, stateStore, sessionId, options) {
6644
+ async remember(key, value, stateStore, sessionId, options, signal) {
5148
6645
  const scope = options?.scope ?? "session";
5149
6646
  if (scope === "session" && !sessionId) {
5150
6647
  throw new Error(
@@ -5163,23 +6660,44 @@ var MemoryManager = class {
5163
6660
  }
5164
6661
  if (options?.embed === true && this.vectorStore && this.embedder) {
5165
6662
  const text = typeof value === "string" ? value : JSON.stringify(value);
5166
- const [embedding] = await this.embedder.embed([text]);
5167
- await this.vectorStore.upsert([
5168
- {
5169
- id: `${storeScope}:${key}`,
5170
- content: text,
5171
- embedding,
5172
- metadata: { key, scope, ...options?.metadata }
6663
+ const raw = await this.embedder.embed([text], signal);
6664
+ assertEmbedResult(raw);
6665
+ const { vectors, usage } = raw;
6666
+ try {
6667
+ await this.vectorStore.upsert([
6668
+ {
6669
+ id: `${storeScope}:${key}`,
6670
+ content: text,
6671
+ embedding: vectors[0],
6672
+ metadata: { key, scope, ...options?.metadata }
6673
+ }
6674
+ ]);
6675
+ } catch (err) {
6676
+ if (usage && typeof err === "object" && err !== null) {
6677
+ Object.defineProperty(err, "axlEmbedUsage", {
6678
+ value: usage,
6679
+ enumerable: false,
6680
+ writable: true,
6681
+ configurable: true
6682
+ });
5173
6683
  }
5174
- ]);
6684
+ throw err;
6685
+ }
6686
+ return usage ? { usage } : {};
5175
6687
  }
6688
+ return {};
5176
6689
  }
5177
6690
  /**
5178
6691
  * Recall a value from memory by key, or perform semantic search if query is provided.
6692
+ *
6693
+ * Always returns `{ data }`; semantic recalls additionally include
6694
+ * `usage` from the embedder (when the embedder reported it).
5179
6695
  */
5180
- async recall(key, stateStore, sessionId, options) {
6696
+ async recall(key, stateStore, sessionId, options, signal) {
5181
6697
  if (options?.query && this.vectorStore && this.embedder) {
5182
- const [embedding] = await this.embedder.embed([options.query]);
6698
+ const raw = await this.embedder.embed([options.query], signal);
6699
+ assertEmbedResult(raw);
6700
+ const { vectors, usage } = raw;
5183
6701
  const topK = options?.topK ?? 5;
5184
6702
  const searchScope = options?.scope ?? "session";
5185
6703
  if (searchScope === "session" && !sessionId) {
@@ -5187,13 +6705,14 @@ var MemoryManager = class {
5187
6705
  'sessionId is required for session-scoped memory. Use { scope: "global" } or provide a sessionId in metadata.'
5188
6706
  );
5189
6707
  }
5190
- const rawResults = await this.vectorStore.search(embedding, topK * 3);
6708
+ const rawResults = await this.vectorStore.search(vectors[0], topK * 3);
5191
6709
  const filtered = rawResults.filter((r) => {
5192
6710
  if (r.id.startsWith("global:")) return true;
5193
6711
  if (searchScope === "session") return r.id.startsWith(`session:${sessionId}:`);
5194
6712
  return true;
5195
6713
  });
5196
- return filtered.slice(0, topK);
6714
+ const data = filtered.slice(0, topK);
6715
+ return usage ? { data, usage } : { data };
5197
6716
  }
5198
6717
  const scope = options?.scope ?? "session";
5199
6718
  if (scope === "session" && !sessionId) {
@@ -5203,10 +6722,10 @@ var MemoryManager = class {
5203
6722
  }
5204
6723
  const storeScope = scope === "session" ? `session:${sessionId}` : "global";
5205
6724
  if (stateStore.getMemory) {
5206
- return stateStore.getMemory(storeScope, key);
6725
+ return { data: await stateStore.getMemory(storeScope, key) };
5207
6726
  }
5208
6727
  const storeKey = `memory:${storeScope}:${key}`;
5209
- return stateStore.getSessionMeta(storeKey, "value");
6728
+ return { data: await stateStore.getSessionMeta(storeKey, "value") };
5210
6729
  }
5211
6730
  /** Delete a memory entry. If a vector store is configured, also removes the embedding. */
5212
6731
  async forget(key, stateStore, sessionId, options) {
@@ -5275,6 +6794,32 @@ function hashInput(input) {
5275
6794
  }
5276
6795
  return (hash >>> 0).toString(16);
5277
6796
  }
6797
+ var DEFAULT_MAX_EVENTS_PER_EXECUTION = 5e4;
6798
+ function isAbortError(err) {
6799
+ return typeof err === "object" && err !== null && err.name === "AbortError";
6800
+ }
6801
+ function pushEventBounded(execInfo, event, cap) {
6802
+ if (event.type === "token" || event.type === "partial_object") return;
6803
+ if (cap === Infinity || execInfo.events.length < cap) {
6804
+ execInfo.events.push(event);
6805
+ return;
6806
+ }
6807
+ const last = execInfo.events[execInfo.events.length - 1];
6808
+ const alreadyTruncated = last?.type === "log" && typeof last.data === "object" && last.data !== null && last.data.event === "events_truncated";
6809
+ if (!alreadyTruncated) {
6810
+ execInfo.events[execInfo.events.length - 1] = {
6811
+ type: "log",
6812
+ executionId: event.executionId,
6813
+ step: event.step,
6814
+ timestamp: Date.now(),
6815
+ data: {
6816
+ event: "events_truncated",
6817
+ cap,
6818
+ 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.`
6819
+ }
6820
+ };
6821
+ }
6822
+ }
5278
6823
  var costScopeStorage = new AsyncLocalStorage2();
5279
6824
  var AxlRuntime = class extends EventEmitter2 {
5280
6825
  config;
@@ -5294,11 +6839,27 @@ var AxlRuntime = class extends EventEmitter2 {
5294
6839
  historicalExecutionsLoadPromise = null;
5295
6840
  evalHistory = [];
5296
6841
  evalHistoryLoadPromise = null;
6842
+ /** Resolved cap on `ExecutionInfo.events` per execution. Cached on
6843
+ * the instance so per-event onTrace handlers don't reach into
6844
+ * `this.config.state` on every emission. */
6845
+ maxEventsPerExecution;
5297
6846
  constructor(config) {
5298
6847
  super();
5299
6848
  this.config = resolveConfig(config ?? {});
5300
6849
  this.providerRegistry = new ProviderRegistry();
5301
6850
  this.stateStore = this.createStateStore();
6851
+ const requested = this.config.state?.maxEventsPerExecution;
6852
+ if (requested === void 0) {
6853
+ this.maxEventsPerExecution = DEFAULT_MAX_EVENTS_PER_EXECUTION;
6854
+ } else if (requested === Infinity) {
6855
+ this.maxEventsPerExecution = Infinity;
6856
+ } else if (typeof requested !== "number" || !Number.isFinite(requested) || !Number.isInteger(requested) || requested < 1) {
6857
+ throw new RangeError(
6858
+ `config.state.maxEventsPerExecution must be a positive integer or Infinity; got ${requested}`
6859
+ );
6860
+ } else {
6861
+ this.maxEventsPerExecution = requested;
6862
+ }
5302
6863
  if (this.config.memory) {
5303
6864
  this.memoryManager = new MemoryManager({
5304
6865
  vectorStore: this.config.memory.vectorStore,
@@ -5306,6 +6867,22 @@ var AxlRuntime = class extends EventEmitter2 {
5306
6867
  });
5307
6868
  }
5308
6869
  }
6870
+ /**
6871
+ * Whether `config.trace.redact` is enabled on this runtime. A narrow
6872
+ * boolean getter is preferred over exposing the full config because:
6873
+ * (a) `Readonly<AxlConfig>` is shallow so the config would be mutable
6874
+ * at runtime through sub-objects, subverting any compliance guarantee;
6875
+ * (b) observability consumers like Studio only need the boolean, not
6876
+ * the whole config tree; (c) if future observability config needs to
6877
+ * expand, each new flag gets its own narrow getter.
6878
+ *
6879
+ * Studio's server-side redaction helpers (executions, memory, sessions,
6880
+ * evals, decisions, tools, playground, workflows) all route through
6881
+ * this accessor to decide whether to scrub response payloads.
6882
+ */
6883
+ isRedactEnabled() {
6884
+ return this.config.trace?.redact === true;
6885
+ }
5309
6886
  /**
5310
6887
  * Initialize MCP servers configured in the config.
5311
6888
  * Call this before executing workflows that use MCP tools.
@@ -5338,6 +6915,71 @@ var AxlRuntime = class extends EventEmitter2 {
5338
6915
  return new MemoryStore();
5339
6916
  }
5340
6917
  }
6918
+ /**
6919
+ * Run a workflow body inside an ALREADY-CONSTRUCTED `WorkflowContext`
6920
+ * with full lifecycle invariants: emit `workflow_start` → run handler
6921
+ * → parse output → emit `workflow_end` → delete checkpoints → persist.
6922
+ *
6923
+ * Shared between `execute()` and `stream()`. Centralizes the
6924
+ * "start iff end" pairing invariant so a future emit path
6925
+ * (e.g., `resumeExecution`) can inherit correct behavior by routing
6926
+ * through this helper. Also factors out the AbortError detection.
6927
+ *
6928
+ * Side effects on `execInfo`: sets `status` / `completedAt` /
6929
+ * `duration` / `result` (or `error`) before emitting `workflow_end`.
6930
+ * On throw: emits `workflow_end({status: 'failed', aborted?})` and
6931
+ * persists before rethrowing. `abortControllers.delete` runs in
6932
+ * `finally` regardless of outcome.
6933
+ *
6934
+ * Caller responsibilities:
6935
+ * - Allocate `executionId` and register it in `abortControllers` map
6936
+ * BEFORE calling this helper (so early-throw paths still have a
6937
+ * correlation id available for terminal events).
6938
+ * - Construct `execInfo` and `ctx` before calling.
6939
+ * - Pass `span` if the call site is inside a tracer span — this
6940
+ * helper sets `axl.workflow.cost` / `.duration` attributes on
6941
+ * success.
6942
+ */
6943
+ async runWorkflowBody(args) {
6944
+ const { workflow: workflow2, ctx, execInfo, validated, span } = args;
6945
+ try {
6946
+ ctx._emitWorkflowStart(validated);
6947
+ const raw = await workflow2.handler(ctx);
6948
+ const result = workflow2.outputSchema ? workflow2.outputSchema.parse(raw) : raw;
6949
+ execInfo.status = "completed";
6950
+ execInfo.completedAt = Date.now();
6951
+ execInfo.duration = execInfo.completedAt - execInfo.startedAt;
6952
+ execInfo.result = result;
6953
+ ctx._emitWorkflowEnd({
6954
+ status: "completed",
6955
+ duration: execInfo.duration,
6956
+ result
6957
+ });
6958
+ if (this.stateStore.deleteCheckpoints) {
6959
+ await this.stateStore.deleteCheckpoints(execInfo.executionId);
6960
+ }
6961
+ span?.setAttribute("axl.workflow.cost", execInfo.totalCost);
6962
+ span?.setAttribute("axl.workflow.duration", execInfo.duration);
6963
+ this.persistExecution(execInfo);
6964
+ return result;
6965
+ } catch (err) {
6966
+ const aborted = isAbortError(err);
6967
+ execInfo.status = "failed";
6968
+ execInfo.completedAt = Date.now();
6969
+ execInfo.duration = execInfo.completedAt - execInfo.startedAt;
6970
+ execInfo.error = err instanceof Error ? err.message : String(err);
6971
+ ctx._emitWorkflowEnd({
6972
+ status: "failed",
6973
+ duration: execInfo.duration,
6974
+ error: execInfo.error,
6975
+ ...aborted ? { aborted: true } : {}
6976
+ });
6977
+ this.persistExecution(execInfo);
6978
+ throw err;
6979
+ } finally {
6980
+ this.abortControllers.delete(execInfo.executionId);
6981
+ }
6982
+ }
5341
6983
  /**
5342
6984
  * Persist a completed/failed execution to the state store (fire-and-forget)
5343
6985
  * and move it from the active executions map to the historical cache.
@@ -5353,7 +6995,6 @@ var AxlRuntime = class extends EventEmitter2 {
5353
6995
  this.executions.delete(id);
5354
6996
  }
5355
6997
  /** Register a workflow with the runtime. */
5356
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5357
6998
  register(workflow2) {
5358
6999
  this.workflows.set(workflow2.name, workflow2);
5359
7000
  }
@@ -5455,10 +7096,19 @@ var AxlRuntime = class extends EventEmitter2 {
5455
7096
  metadata: result2.metadata ?? metadata
5456
7097
  };
5457
7098
  };
5458
- result = await runEvalFn(entry.config, wrappedExecuteFn, this);
7099
+ result = await runEvalFn(entry.config, wrappedExecuteFn, this, {
7100
+ onProgress: options?.onProgress,
7101
+ signal: options?.signal,
7102
+ captureTraces: options?.captureTraces
7103
+ });
5459
7104
  } else {
5460
7105
  result = await this.eval(
5461
- entry.config
7106
+ entry.config,
7107
+ {
7108
+ onProgress: options?.onProgress,
7109
+ signal: options?.signal,
7110
+ captureTraces: options?.captureTraces
7111
+ }
5462
7112
  );
5463
7113
  }
5464
7114
  const resultObj = result;
@@ -5470,7 +7120,7 @@ var AxlRuntime = class extends EventEmitter2 {
5470
7120
  }
5471
7121
  try {
5472
7122
  await this.saveEvalResult({
5473
- id: resultObj.id ?? randomUUID2(),
7123
+ id: resultObj.id ?? randomUUID3(),
5474
7124
  eval: name,
5475
7125
  timestamp: Date.now(),
5476
7126
  data: structuredClone(result)
@@ -5507,7 +7157,7 @@ var AxlRuntime = class extends EventEmitter2 {
5507
7157
  * and automatically emits trace events and tracks cost.
5508
7158
  */
5509
7159
  createContext(options) {
5510
- const executionId = randomUUID2();
7160
+ const executionId = randomUUID3();
5511
7161
  const budgetLimit = options?.budget ? parseCost(options.budget) : Infinity;
5512
7162
  this.registerWithCostScope(executionId);
5513
7163
  return new WorkflowContext({
@@ -5523,10 +7173,12 @@ var AxlRuntime = class extends EventEmitter2 {
5523
7173
  sessionHistory: options?.sessionHistory,
5524
7174
  signal: options?.signal,
5525
7175
  onToken: options?.onToken,
7176
+ onToolCall: options?.onToolCall,
7177
+ onAgentStart: options?.onAgentStart,
5526
7178
  awaitHumanHandler: options?.awaitHumanHandler,
5527
7179
  onTrace: (event) => {
5528
7180
  this.emit("trace", event);
5529
- this.outputTraceEvent(event);
7181
+ this.outputAxlEvent(event);
5530
7182
  },
5531
7183
  budgetContext: {
5532
7184
  totalCost: 0,
@@ -5553,7 +7205,7 @@ var AxlRuntime = class extends EventEmitter2 {
5553
7205
  );
5554
7206
  }
5555
7207
  const validated = workflow2.inputSchema.parse(input);
5556
- const executionId = randomUUID2();
7208
+ const executionId = randomUUID3();
5557
7209
  const controller = new AbortController();
5558
7210
  this.abortControllers.set(executionId, controller);
5559
7211
  this.registerWithCostScope(executionId);
@@ -5561,7 +7213,7 @@ var AxlRuntime = class extends EventEmitter2 {
5561
7213
  executionId,
5562
7214
  workflow: name,
5563
7215
  status: "running",
5564
- steps: [],
7216
+ events: [],
5565
7217
  totalCost: 0,
5566
7218
  startedAt: Date.now(),
5567
7219
  duration: 0
@@ -5577,25 +7229,41 @@ var AxlRuntime = class extends EventEmitter2 {
5577
7229
  sessionHistory,
5578
7230
  signal: controller.signal,
5579
7231
  onTrace: (event) => {
5580
- execInfo.steps.push(event);
5581
- if (event.cost) execInfo.totalCost += event.cost;
7232
+ pushEventBounded(execInfo, event, this.maxEventsPerExecution);
7233
+ execInfo.totalCost += eventCostContribution(event);
5582
7234
  this.emit("trace", event);
5583
- this.outputTraceEvent(event);
5584
- if (event.type === "handoff") {
7235
+ this.outputAxlEvent(event);
7236
+ if (event.type === "handoff_start") {
5585
7237
  const sessionId = options?.metadata?.sessionId;
5586
7238
  if (sessionId) {
5587
- const data = event.data;
5588
7239
  this.appendHandoffRecord(sessionId, {
5589
- source: event.agent ?? "",
5590
- target: data?.target ?? "",
5591
- mode: data?.mode ?? "oneway",
7240
+ source: event.data.source,
7241
+ target: event.data.target,
7242
+ mode: event.data.mode,
5592
7243
  timestamp: event.timestamp,
5593
- duration: data?.duration ?? void 0
7244
+ toAskId: event.toAskId
7245
+ });
7246
+ }
7247
+ } else if (event.type === "handoff_return") {
7248
+ const sessionId = options?.metadata?.sessionId;
7249
+ if (sessionId) {
7250
+ this.updateHandoffDuration(sessionId, {
7251
+ toAskId: event.toAskId,
7252
+ duration: event.data.duration
7253
+ });
7254
+ }
7255
+ } else if (event.type === "ask_end") {
7256
+ const sessionId = options?.metadata?.sessionId;
7257
+ if (sessionId && event.askId && typeof event.duration === "number") {
7258
+ this.updateHandoffDuration(sessionId, {
7259
+ toAskId: event.askId,
7260
+ duration: event.duration
5594
7261
  });
5595
7262
  }
5596
7263
  }
5597
7264
  },
5598
7265
  pendingDecisions: this.pendingDecisionResolvers,
7266
+ awaitHumanHandler: options?.awaitHumanHandler,
5599
7267
  stateStore: this.stateStore,
5600
7268
  workflowName: name,
5601
7269
  mcpManager: this.mcpManager,
@@ -5609,7 +7277,6 @@ var AxlRuntime = class extends EventEmitter2 {
5609
7277
  policy: "finish_and_stop"
5610
7278
  }
5611
7279
  });
5612
- ctx.log("workflow_start", { workflow: name, executionId });
5613
7280
  return this.spanManager.withSpanAsync(
5614
7281
  "axl.workflow.execute",
5615
7282
  {
@@ -5617,39 +7284,13 @@ var AxlRuntime = class extends EventEmitter2 {
5617
7284
  "axl.execution.id": executionId,
5618
7285
  "axl.workflow.input_hash": hashInput(validated)
5619
7286
  },
5620
- async (span) => {
5621
- try {
5622
- const result = await workflow2.handler(ctx);
5623
- const output = workflow2.outputSchema ? workflow2.outputSchema.parse(result) : result;
5624
- execInfo.status = "completed";
5625
- execInfo.completedAt = Date.now();
5626
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5627
- execInfo.result = output;
5628
- ctx.log("workflow_end", {
5629
- workflow: name,
5630
- status: "completed",
5631
- duration: execInfo.duration,
5632
- cost: execInfo.totalCost
5633
- });
5634
- if (this.stateStore.deleteCheckpoints) {
5635
- await this.stateStore.deleteCheckpoints(executionId);
5636
- }
5637
- span.setAttribute("axl.workflow.cost", execInfo.totalCost);
5638
- span.setAttribute("axl.workflow.duration", execInfo.duration);
5639
- this.persistExecution(execInfo);
5640
- return output;
5641
- } catch (err) {
5642
- execInfo.status = "failed";
5643
- execInfo.completedAt = Date.now();
5644
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5645
- execInfo.error = err instanceof Error ? err.message : String(err);
5646
- ctx.log("workflow_end", { workflow: name, status: "failed", error: execInfo.error });
5647
- this.persistExecution(execInfo);
5648
- throw err;
5649
- } finally {
5650
- this.abortControllers.delete(executionId);
5651
- }
5652
- }
7287
+ (span) => this.runWorkflowBody({
7288
+ workflow: workflow2,
7289
+ ctx,
7290
+ execInfo,
7291
+ validated,
7292
+ span
7293
+ })
5653
7294
  );
5654
7295
  }
5655
7296
  /** Execute a workflow and return a stream. */
@@ -5657,21 +7298,20 @@ var AxlRuntime = class extends EventEmitter2 {
5657
7298
  const axlStream = new AxlStream();
5658
7299
  const controller = new AbortController();
5659
7300
  axlStream.on("close", () => controller.abort());
7301
+ const executionId = randomUUID3();
7302
+ this.abortControllers.set(executionId, controller);
5660
7303
  let execInfo;
5661
- let ctx;
5662
7304
  const run = async () => {
5663
7305
  const workflow2 = this.workflows.get(name);
5664
7306
  if (!workflow2) throw new Error(`Workflow "${name}" not registered`);
5665
7307
  const validated = workflow2.inputSchema.parse(input);
5666
- const executionId = randomUUID2();
5667
- this.abortControllers.set(executionId, controller);
5668
7308
  const sessionHistory = options?.metadata?.sessionHistory ?? void 0;
5669
7309
  this.registerWithCostScope(executionId);
5670
7310
  execInfo = {
5671
7311
  executionId,
5672
7312
  workflow: name,
5673
7313
  status: "running",
5674
- steps: [],
7314
+ events: [],
5675
7315
  totalCost: 0,
5676
7316
  startedAt: Date.now(),
5677
7317
  duration: 0
@@ -5685,79 +7325,49 @@ var AxlRuntime = class extends EventEmitter2 {
5685
7325
  providerRegistry: this.providerRegistry,
5686
7326
  sessionHistory,
5687
7327
  signal: controller.signal,
7328
+ // Sentinel `onToken` so the streaming code path in WorkflowContext
7329
+ // is entered (it's gated on `this.onToken` being defined). The
7330
+ // actual `token` AxlEvents are emitted via `emitEvent` and reach
7331
+ // the wire through `onTrace` — this callback is just the gate.
7332
+ onToken: () => {
7333
+ },
5688
7334
  onTrace: (event) => {
5689
- execInfo.steps.push(event);
5690
- if (event.cost) execInfo.totalCost += event.cost;
7335
+ pushEventBounded(execInfo, event, this.maxEventsPerExecution);
7336
+ execInfo.totalCost += eventCostContribution(event);
5691
7337
  this.emit("trace", event);
5692
- this.outputTraceEvent(event);
5693
- if (event.type === "handoff") {
5694
- const data = event.data;
5695
- axlStream._push({
5696
- type: "handoff",
5697
- source: event.agent ?? "",
5698
- target: data?.target ?? "",
5699
- mode: data?.mode ?? void 0
5700
- });
7338
+ this.outputAxlEvent(event);
7339
+ axlStream._push(event);
7340
+ if (event.type === "handoff_start") {
5701
7341
  const sessionId = options?.metadata?.sessionId;
5702
7342
  if (sessionId) {
5703
7343
  this.appendHandoffRecord(sessionId, {
5704
- source: event.agent ?? "",
5705
- target: data?.target ?? "",
5706
- mode: data?.mode ?? "oneway",
7344
+ source: event.data.source,
7345
+ target: event.data.target,
7346
+ mode: event.data.mode,
5707
7347
  timestamp: event.timestamp,
5708
- duration: data?.duration ?? void 0
7348
+ toAskId: event.toAskId
5709
7349
  });
5710
7350
  }
5711
- } else if (event.type === "tool_denied") {
5712
- const data = event.data;
5713
- if (data?.denied === false) {
5714
- axlStream._push({
5715
- type: "tool_approval",
5716
- name: event.tool ?? "",
5717
- args: data?.args,
5718
- approved: true
7351
+ } else if (event.type === "handoff_return") {
7352
+ const sessionId = options?.metadata?.sessionId;
7353
+ if (sessionId) {
7354
+ this.updateHandoffDuration(sessionId, {
7355
+ toAskId: event.toAskId,
7356
+ duration: event.data.duration
5719
7357
  });
5720
- } else {
5721
- axlStream._push({
5722
- type: "tool_approval",
5723
- name: event.tool ?? "",
5724
- args: data?.args,
5725
- approved: false,
5726
- reason: data?.reason ?? void 0
7358
+ }
7359
+ } else if (event.type === "ask_end") {
7360
+ const sessionId = options?.metadata?.sessionId;
7361
+ if (sessionId && event.askId && typeof event.duration === "number") {
7362
+ this.updateHandoffDuration(sessionId, {
7363
+ toAskId: event.askId,
7364
+ duration: event.duration
5727
7365
  });
5728
7366
  }
5729
- } else if (event.type === "agent_call") {
5730
- axlStream._push({
5731
- type: "agent_end",
5732
- agent: event.agent ?? "",
5733
- cost: event.cost,
5734
- duration: event.duration
5735
- });
5736
- } else if (event.type === "tool_call") {
5737
- axlStream._push({
5738
- type: "tool_result",
5739
- name: event.tool ?? "",
5740
- result: event.data?.result,
5741
- callId: event.data?.callId
5742
- });
5743
7367
  }
5744
- axlStream._push({ type: "step", step: event.step, data: event });
5745
- },
5746
- onToken: (token) => {
5747
- axlStream._push({ type: "token", data: token });
5748
- },
5749
- onToolCall: (call) => {
5750
- axlStream._push({
5751
- type: "tool_call",
5752
- name: call.name,
5753
- args: call.args,
5754
- callId: call.callId
5755
- });
5756
- },
5757
- onAgentStart: (info) => {
5758
- axlStream._push({ type: "agent_start", agent: info.agent, model: info.model });
5759
7368
  },
5760
7369
  pendingDecisions: this.pendingDecisionResolvers,
7370
+ awaitHumanHandler: options?.awaitHumanHandler,
5761
7371
  stateStore: this.stateStore,
5762
7372
  workflowName: name,
5763
7373
  mcpManager: this.mcpManager,
@@ -5771,7 +7381,6 @@ var AxlRuntime = class extends EventEmitter2 {
5771
7381
  policy: "finish_and_stop"
5772
7382
  }
5773
7383
  });
5774
- ctx = wfCtx;
5775
7384
  return this.spanManager.withSpanAsync(
5776
7385
  "axl.workflow.execute",
5777
7386
  {
@@ -5779,47 +7388,25 @@ var AxlRuntime = class extends EventEmitter2 {
5779
7388
  "axl.execution.id": executionId,
5780
7389
  "axl.workflow.input_hash": hashInput(validated)
5781
7390
  },
5782
- async (span) => {
5783
- try {
5784
- const rawResult = await workflow2.handler(wfCtx);
5785
- const result = workflow2.outputSchema ? workflow2.outputSchema.parse(rawResult) : rawResult;
5786
- execInfo.status = "completed";
5787
- execInfo.completedAt = Date.now();
5788
- execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5789
- execInfo.result = result;
5790
- wfCtx.log("workflow_end", {
5791
- workflow: name,
5792
- status: "completed",
5793
- duration: execInfo.duration,
5794
- cost: execInfo.totalCost
5795
- });
5796
- if (this.stateStore.deleteCheckpoints) {
5797
- await this.stateStore.deleteCheckpoints(executionId);
5798
- }
5799
- span.setAttribute("axl.workflow.cost", execInfo.totalCost);
5800
- span.setAttribute("axl.workflow.duration", execInfo.duration);
5801
- this.persistExecution(execInfo);
5802
- return result;
5803
- } finally {
5804
- this.abortControllers.delete(executionId);
5805
- }
5806
- }
7391
+ (span) => this.runWorkflowBody({
7392
+ workflow: workflow2,
7393
+ ctx: wfCtx,
7394
+ execInfo,
7395
+ validated,
7396
+ span
7397
+ })
5807
7398
  );
5808
7399
  };
5809
- run().then((result) => axlStream._done(result)).catch((err) => {
5810
- if (execInfo) {
7400
+ run().then((result) => axlStream._done(result, executionId)).catch((err) => {
7401
+ if (execInfo && execInfo.status === "running") {
5811
7402
  execInfo.status = "failed";
5812
7403
  execInfo.completedAt = Date.now();
5813
7404
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5814
7405
  execInfo.error = err instanceof Error ? err.message : String(err);
5815
- ctx?.log("workflow_end", {
5816
- workflow: name,
5817
- status: "failed",
5818
- error: execInfo.error
5819
- });
5820
7406
  this.persistExecution(execInfo);
5821
7407
  }
5822
- axlStream._error(err instanceof Error ? err : new Error(String(err)));
7408
+ this.abortControllers.delete(executionId);
7409
+ axlStream._error(err instanceof Error ? err : new Error(String(err)), executionId);
5823
7410
  });
5824
7411
  return axlStream;
5825
7412
  }
@@ -5880,6 +7467,7 @@ var AxlRuntime = class extends EventEmitter2 {
5880
7467
  if (this.stateStore.saveEvalResult) {
5881
7468
  await this.stateStore.saveEvalResult(entry);
5882
7469
  }
7470
+ this.emit("eval_result", entry);
5883
7471
  }
5884
7472
  /**
5885
7473
  * Delete an eval history entry by id. Removes from in-memory cache and
@@ -6011,7 +7599,7 @@ var AxlRuntime = class extends EventEmitter2 {
6011
7599
  *
6012
7600
  * @see Spec Section 13.5
6013
7601
  */
6014
- async eval(config) {
7602
+ async eval(config, options) {
6015
7603
  let runEvalFn;
6016
7604
  try {
6017
7605
  ({ runEval: runEvalFn } = await import("@axlsdk/eval"));
@@ -6026,7 +7614,7 @@ var AxlRuntime = class extends EventEmitter2 {
6026
7614
  });
6027
7615
  return { output: result, cost, metadata };
6028
7616
  };
6029
- return runEvalFn(config, executeWorkflow, this);
7617
+ return runEvalFn(config, executeWorkflow, this, options);
6030
7618
  }
6031
7619
  /**
6032
7620
  * Compare two evaluation results to detect regressions and improvements.
@@ -6064,9 +7652,31 @@ var AxlRuntime = class extends EventEmitter2 {
6064
7652
  * Returns cost (same as `trackCost`) plus metadata extracted from trace events:
6065
7653
  * models (unique URIs), tokens (input/output/reasoning sums), and agent call count.
6066
7654
  *
7655
+ * ## Cost vs tokens semantics
7656
+ *
7657
+ * - `cost` is the full aggregate across EVERY event with a top-level
7658
+ * `event.cost` set: agent calls, tool calls, semantic memory ops, etc.
7659
+ * This is the number to reconcile against your provider bill.
7660
+ *
7661
+ * - `metadata.tokens` is narrowly scoped to **agent** prompt/completion/
7662
+ * reasoning tokens. Embedder tokens from semantic `ctx.remember({embed:true})`
7663
+ * / `ctx.recall({query})` are deliberately NOT summed here — they're a
7664
+ * different category (input-only, different pricing, different model).
7665
+ * Conflating them would make "prompt tokens" misleading in the UI. If you
7666
+ * need embedder token counts, subscribe to `runtime.on('trace', ...)` and
7667
+ * read `data.usage.tokens` on `memory_remember` / `memory_recall` events.
7668
+ *
7669
+ * Pass `{ captureTraces: true }` to also collect the raw `AxlEvent[]` observed
7670
+ * during `fn()`. This is opt-in because it keeps every event in memory for the
7671
+ * duration of the call — useful for eval per-item capture, debugging, and test
7672
+ * assertions, but overhead grows with trace volume. When enabled, verbose-mode
7673
+ * `agent_call_start.data.messages` snapshots are omitted from captured events (still
7674
+ * broadcast via onTrace) to keep memory bounded — callers who need the full
7675
+ * verbose snapshot should subscribe to `runtime.on('trace', ...)` directly.
7676
+ *
6067
7677
  * Works with both `createContext()` and `execute()` calls inside `fn`.
6068
7678
  */
6069
- async trackExecution(fn) {
7679
+ async trackExecution(fn, options) {
6070
7680
  const parentScope = costScopeStorage.getStore();
6071
7681
  const scope = {
6072
7682
  totalCost: 0,
@@ -6077,10 +7687,11 @@ var AxlRuntime = class extends EventEmitter2 {
6077
7687
  const workflowCalls = /* @__PURE__ */ new Map();
6078
7688
  const tokens = { input: 0, output: 0, reasoning: 0 };
6079
7689
  let agentCalls = 0;
7690
+ const capturedTraces = options?.captureTraces ? [] : void 0;
6080
7691
  const listener = (event) => {
6081
7692
  if (!scope.trackedIds.has(event.executionId)) return;
6082
- if (event.cost) scope.totalCost += event.cost;
6083
- if (event.type === "agent_call") {
7693
+ scope.totalCost += eventCostContribution(event);
7694
+ if (event.type === "agent_call_end") {
6084
7695
  if (event.model) modelCalls.set(event.model, (modelCalls.get(event.model) ?? 0) + 1);
6085
7696
  agentCalls++;
6086
7697
  if (event.tokens) {
@@ -6089,17 +7700,23 @@ var AxlRuntime = class extends EventEmitter2 {
6089
7700
  tokens.reasoning += event.tokens.reasoning ?? 0;
6090
7701
  }
6091
7702
  }
6092
- let workflowName;
6093
7703
  if (event.type === "workflow_start" && event.workflow) {
6094
- workflowName = event.workflow;
6095
- } else if (event.type === "log" && event.data && typeof event.data === "object") {
6096
- const d = event.data;
6097
- if (d.event === "workflow_start" && typeof d.workflow === "string") {
6098
- workflowName = d.workflow;
6099
- }
7704
+ workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
6100
7705
  }
6101
- if (workflowName) {
6102
- workflowCalls.set(workflowName, (workflowCalls.get(workflowName) ?? 0) + 1);
7706
+ if (capturedTraces) {
7707
+ if (event.type === "token" || event.type === "partial_object") return;
7708
+ if (event.type === "agent_call_start" && event.data) {
7709
+ const d = event.data;
7710
+ if ("messages" in d) {
7711
+ const rest = {};
7712
+ for (const k of Object.keys(d)) {
7713
+ if (k !== "messages") rest[k] = d[k];
7714
+ }
7715
+ capturedTraces.push({ ...event, data: rest });
7716
+ return;
7717
+ }
7718
+ }
7719
+ capturedTraces.push(event);
6103
7720
  }
6104
7721
  };
6105
7722
  this.setMaxListeners(this.getMaxListeners() + 1);
@@ -6109,6 +7726,7 @@ var AxlRuntime = class extends EventEmitter2 {
6109
7726
  return {
6110
7727
  result,
6111
7728
  cost: scope.totalCost,
7729
+ ...capturedTraces ? { traces: capturedTraces } : {},
6112
7730
  metadata: {
6113
7731
  models: [...modelCalls.keys()],
6114
7732
  modelCallCounts: modelCalls.size > 0 ? Object.fromEntries(modelCalls) : void 0,
@@ -6118,6 +7736,16 @@ var AxlRuntime = class extends EventEmitter2 {
6118
7736
  workflowCallCounts: workflowCalls.size > 0 ? Object.fromEntries(workflowCalls) : void 0
6119
7737
  }
6120
7738
  };
7739
+ } catch (err) {
7740
+ if (capturedTraces && typeof err === "object" && err !== null) {
7741
+ Object.defineProperty(err, "axlCapturedTraces", {
7742
+ value: capturedTraces,
7743
+ enumerable: false,
7744
+ writable: true,
7745
+ configurable: true
7746
+ });
7747
+ }
7748
+ throw err;
6121
7749
  } finally {
6122
7750
  this.off("trace", listener);
6123
7751
  this.setMaxListeners(this.getMaxListeners() - 1);
@@ -6142,7 +7770,7 @@ var AxlRuntime = class extends EventEmitter2 {
6142
7770
  * The emit('trace', event) call happens before this method is called, so
6143
7771
  * programmatic subscribers always receive events regardless of trace config.
6144
7772
  */
6145
- outputTraceEvent(event) {
7773
+ outputAxlEvent(event) {
6146
7774
  const traceConfig = this.config.trace;
6147
7775
  if (!traceConfig?.enabled) return;
6148
7776
  const level = traceConfig.level ?? "steps";
@@ -6166,18 +7794,20 @@ var AxlRuntime = class extends EventEmitter2 {
6166
7794
  }
6167
7795
  return;
6168
7796
  }
6169
- this.logTraceEvent(event);
7797
+ this.logAxlEvent(event);
6170
7798
  }
6171
- logTraceEvent(event) {
7799
+ logAxlEvent(event) {
6172
7800
  const level = this.config.trace?.level ?? "steps";
6173
7801
  const workflowPrefix = event.workflow ? `workflow:${event.workflow} | ` : "";
6174
7802
  const parts = [`[axl] execution:${event.executionId}`];
6175
- const data = event.data;
6176
7803
  if (event.type === "workflow_start") {
6177
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | started`);
7804
+ parts.push(`${workflowPrefix}started`);
6178
7805
  } else if (event.type === "workflow_end") {
6179
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | completed`);
6180
- } else if (event.type === "agent_call") {
7806
+ const d = event.data;
7807
+ const status = d?.aborted ? "aborted" : d?.status ?? "completed";
7808
+ parts.push(`${workflowPrefix}${status}`);
7809
+ if (event.duration != null) parts.push(`${(event.duration / 1e3).toFixed(1)}s`);
7810
+ } else if (event.type === "agent_call_end") {
6181
7811
  parts.push(`${workflowPrefix}step:${event.step} agent_call`);
6182
7812
  if (event.agent) parts.push(`agent:${event.agent}`);
6183
7813
  if (event.promptVersion) parts.push(`version:${event.promptVersion}`);
@@ -6187,7 +7817,7 @@ var AxlRuntime = class extends EventEmitter2 {
6187
7817
  if (level === "full" && event.data) {
6188
7818
  parts.push(`data:${JSON.stringify(event.data)}`);
6189
7819
  }
6190
- } else if (event.type === "tool_call") {
7820
+ } else if (event.type === "tool_call_end") {
6191
7821
  parts.push(`${workflowPrefix}step:${event.step} tool_call`);
6192
7822
  if (event.tool) parts.push(`tool:${event.tool}`);
6193
7823
  if (event.duration) parts.push(`${event.duration}ms`);
@@ -6204,8 +7834,9 @@ var AxlRuntime = class extends EventEmitter2 {
6204
7834
  parts.push(`${workflowPrefix}log: ${JSON.stringify(event.data)}`);
6205
7835
  } else {
6206
7836
  parts.push(`${workflowPrefix}${event.type}`);
6207
- if (level === "full" && event.data) {
6208
- parts.push(`data:${JSON.stringify(event.data)}`);
7837
+ const data = event.data;
7838
+ if (level === "full" && data !== void 0) {
7839
+ parts.push(`data:${JSON.stringify(data)}`);
6209
7840
  }
6210
7841
  }
6211
7842
  console.log(parts.join(" | "));
@@ -6225,6 +7856,35 @@ var AxlRuntime = class extends EventEmitter2 {
6225
7856
  }).catch(() => {
6226
7857
  });
6227
7858
  }
7859
+ /**
7860
+ * Patch `duration` on the handoff record matching `toAskId` (the target
7861
+ * frame's askId, which the runtime stamps on `handoff_start` and which
7862
+ * appears as `askId` on the target's `ask_end`).
7863
+ *
7864
+ * Called from two places per handoff:
7865
+ * - target's `ask_end` — first-write source for ONEWAY (no return event).
7866
+ * - `handoff_return` — overwrites for ROUNDTRIP with the round-trip
7867
+ * wall-clock figure (intentional: round-trip
7868
+ * duration is the more useful metric for the
7869
+ * source-paused-for-N-ms semantic).
7870
+ *
7871
+ * No-op if no record matches `toAskId` (e.g., stream subscribed mid-
7872
+ * handoff, or session metadata got wiped) — `duration` stays undefined,
7873
+ * which is the documented default for `HandoffRecord`.
7874
+ */
7875
+ updateHandoffDuration(sessionId, match) {
7876
+ this.stateStore.getSessionMeta(sessionId, "handoffHistory").then((existing) => {
7877
+ const history = existing ?? [];
7878
+ for (const r of history) {
7879
+ if (r.toAskId === match.toAskId) {
7880
+ r.duration = match.duration;
7881
+ return this.stateStore.saveSessionMeta(sessionId, "handoffHistory", history);
7882
+ }
7883
+ }
7884
+ return void 0;
7885
+ }).catch(() => {
7886
+ });
7887
+ }
6228
7888
  };
6229
7889
 
6230
7890
  // src/state/redis.ts
@@ -6287,27 +7947,13 @@ var RedisStore = class _RedisStore {
6287
7947
  return "axl:eval-history-ids";
6288
7948
  }
6289
7949
  // ── Checkpoints ──────────────────────────────────────────────────────
6290
- async saveCheckpoint(executionId, step, data) {
6291
- await this.client.hSet(this.checkpointKey(executionId), String(step), JSON.stringify(data));
7950
+ async saveCheckpoint(executionId, name, data) {
7951
+ await this.client.hSet(this.checkpointKey(executionId), name, JSON.stringify(data));
6292
7952
  }
6293
- async getCheckpoint(executionId, step) {
6294
- const raw = await this.client.hGet(this.checkpointKey(executionId), String(step));
7953
+ async getCheckpoint(executionId, name) {
7954
+ const raw = await this.client.hGet(this.checkpointKey(executionId), name);
6295
7955
  return raw != null ? JSON.parse(raw) : null;
6296
7956
  }
6297
- async getLatestCheckpoint(executionId) {
6298
- const all = await this.client.hGetAll(this.checkpointKey(executionId));
6299
- if (!all || Object.keys(all).length === 0) return null;
6300
- let maxStep = -1;
6301
- let maxData = null;
6302
- for (const [stepStr, raw] of Object.entries(all)) {
6303
- const step = Number(stepStr);
6304
- if (step > maxStep) {
6305
- maxStep = step;
6306
- maxData = JSON.parse(raw);
6307
- }
6308
- }
6309
- return { step: maxStep, data: maxData };
6310
- }
6311
7957
  // ── Sessions ─────────────────────────────────────────────────────────
6312
7958
  async saveSession(sessionId, history) {
6313
7959
  await this.client.set(this.sessionKey(sessionId), JSON.stringify(history));
@@ -6412,6 +8058,16 @@ var RedisStore = class _RedisStore {
6412
8058
  };
6413
8059
 
6414
8060
  // src/memory/embedder-openai.ts
8061
+ var EMBEDDING_PRICE_PER_1M_TOKENS = {
8062
+ "text-embedding-3-small": 0.02,
8063
+ "text-embedding-3-large": 0.13,
8064
+ "text-embedding-ada-002": 0.1
8065
+ };
8066
+ function computeCost(model, tokens) {
8067
+ const pricePerMillion = EMBEDDING_PRICE_PER_1M_TOKENS[model];
8068
+ if (pricePerMillion == null) return void 0;
8069
+ return tokens / 1e6 * pricePerMillion;
8070
+ }
6415
8071
  var OpenAIEmbedder = class {
6416
8072
  apiKey;
6417
8073
  model;
@@ -6426,8 +8082,8 @@ var OpenAIEmbedder = class {
6426
8082
  this.baseUrl = options.baseUrl ?? "https://api.openai.com";
6427
8083
  this.dimensions = options.dimensions ?? 1536;
6428
8084
  }
6429
- async embed(texts) {
6430
- const response = await fetch(`${this.baseUrl}/v1/embeddings`, {
8085
+ async embed(texts, signal) {
8086
+ const response = await fetchWithRetry(`${this.baseUrl}/v1/embeddings`, {
6431
8087
  method: "POST",
6432
8088
  headers: {
6433
8089
  "Content-Type": "application/json",
@@ -6437,14 +8093,28 @@ var OpenAIEmbedder = class {
6437
8093
  model: this.model,
6438
8094
  input: texts,
6439
8095
  dimensions: this.dimensions
6440
- })
8096
+ }),
8097
+ signal
6441
8098
  });
6442
8099
  if (!response.ok) {
6443
8100
  const body = await response.text();
6444
8101
  throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
6445
8102
  }
6446
8103
  const json = await response.json();
6447
- return json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
8104
+ const vectors = json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
8105
+ const tokens = json.usage?.prompt_tokens ?? json.usage?.total_tokens;
8106
+ const cost = tokens != null ? computeCost(this.model, tokens) : void 0;
8107
+ if (tokens == null && cost == null) {
8108
+ return { vectors };
8109
+ }
8110
+ return {
8111
+ vectors,
8112
+ usage: {
8113
+ ...tokens != null ? { tokens } : {},
8114
+ ...cost != null ? { cost } : {},
8115
+ model: this.model
8116
+ }
8117
+ };
6448
8118
  }
6449
8119
  };
6450
8120
 
@@ -6568,11 +8238,13 @@ function cosineSimilarity2(a, b) {
6568
8238
  return denom === 0 ? 0 : dot / denom;
6569
8239
  }
6570
8240
  export {
8241
+ AXL_EVENT_TYPES,
6571
8242
  AnthropicProvider,
6572
8243
  AxlError,
6573
8244
  AxlRuntime,
6574
8245
  AxlStream,
6575
8246
  BudgetExceededError,
8247
+ COST_BEARING_LEAF_TYPES,
6576
8248
  GeminiProvider,
6577
8249
  GuardrailError,
6578
8250
  InMemoryVectorStore,
@@ -6587,6 +8259,8 @@ export {
6587
8259
  OpenAIResponsesProvider,
6588
8260
  ProviderRegistry,
6589
8261
  QuorumNotMet,
8262
+ REDACTED,
8263
+ REDACTION_RULES,
6590
8264
  RedisStore,
6591
8265
  SQLiteStore,
6592
8266
  Session,
@@ -6599,7 +8273,12 @@ export {
6599
8273
  agent,
6600
8274
  createSpanManager,
6601
8275
  defineConfig,
8276
+ eventCostContribution,
6602
8277
  extractJson,
8278
+ isCostBearingLeaf,
8279
+ isRootLevel,
8280
+ parsePartialJson,
8281
+ redactEvent,
6603
8282
  resolveThinkingOptions,
6604
8283
  tool,
6605
8284
  workflow,