@axlsdk/axl 0.14.0 → 0.15.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
@@ -1965,6 +1965,21 @@ var TimeoutError = class extends AxlError {
1965
1965
  this.name = "TimeoutError";
1966
1966
  }
1967
1967
  };
1968
+ function formatBudgetCost(cost) {
1969
+ if (Number.isNaN(cost)) return "$NaN";
1970
+ if (cost === Infinity) return "$Infinity";
1971
+ if (cost === -Infinity) return "-$Infinity";
1972
+ if (cost === 0) return "$0.00";
1973
+ const sign = cost < 0 ? "-" : "";
1974
+ const abs = Math.abs(cost);
1975
+ if (abs < 1e-6) return `${sign}< $0.000001`;
1976
+ if (abs < 1e-4) {
1977
+ const [mantissa, exponent] = abs.toExponential(2).split("e");
1978
+ return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
1979
+ }
1980
+ if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
1981
+ return `${sign}$${abs.toFixed(2)}`;
1982
+ }
1968
1983
  var BudgetExceededError = class extends AxlError {
1969
1984
  limit;
1970
1985
  spent;
@@ -1972,7 +1987,9 @@ var BudgetExceededError = class extends AxlError {
1972
1987
  constructor(limit, spent, policy) {
1973
1988
  super(
1974
1989
  "BUDGET_EXCEEDED",
1975
- `Budget exceeded: spent $${spent.toFixed(4)} of $${limit.toFixed(4)} limit (policy: ${policy})`
1990
+ `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
1991
+ limit
1992
+ )} limit (policy: ${policy})`
1976
1993
  );
1977
1994
  this.name = "BudgetExceededError";
1978
1995
  this.limit = limit;
@@ -2160,6 +2177,14 @@ function extractBalanced(str, start, openChar, closeChar) {
2160
2177
  }
2161
2178
  return null;
2162
2179
  }
2180
+ function appendRetryMessages(messages, content, feedbackMessage, providerMetadata) {
2181
+ messages.push({
2182
+ role: "assistant",
2183
+ content,
2184
+ ...providerMetadata ? { providerMetadata } : {}
2185
+ });
2186
+ messages.push({ role: "system", content: feedbackMessage });
2187
+ }
2163
2188
  function estimateMessagesTokens(messages) {
2164
2189
  let total = 0;
2165
2190
  for (const msg of messages) {
@@ -2207,6 +2232,7 @@ var WorkflowContext = class _WorkflowContext {
2207
2232
  awaitHumanHandler;
2208
2233
  onAgentStart;
2209
2234
  onAgentCallComplete;
2235
+ parentToolCallId;
2210
2236
  constructor(init) {
2211
2237
  this.input = init.input;
2212
2238
  this.executionId = init.executionId;
@@ -2230,6 +2256,7 @@ var WorkflowContext = class _WorkflowContext {
2230
2256
  this.awaitHumanHandler = init.awaitHumanHandler;
2231
2257
  this.onAgentStart = init.onAgentStart;
2232
2258
  this.onAgentCallComplete = init.onAgentCallComplete;
2259
+ this.parentToolCallId = init.parentToolCallId;
2233
2260
  if (init.metadata?.summaryCache) {
2234
2261
  this.summaryCache = init.metadata.summaryCache;
2235
2262
  }
@@ -2240,8 +2267,13 @@ var WorkflowContext = class _WorkflowContext {
2240
2267
  * state store, span manager, memory manager, MCP manager, config,
2241
2268
  * awaitHuman handler, pending decisions, tool overrides.
2242
2269
  * Isolates: session history, step counter, streaming callbacks (onToken, onAgentStart, onToolCall).
2270
+ *
2271
+ * @param parentToolCallId - The `callId` of the outer `tool_call` that
2272
+ * spawned this child. When set, every trace event emitted by the child
2273
+ * will carry this id as `parentToolCallId`, so consumers can join nested
2274
+ * agent calls back to the outer tool call that invoked them.
2243
2275
  */
2244
- createChildContext() {
2276
+ createChildContext(parentToolCallId) {
2245
2277
  return new _WorkflowContext({
2246
2278
  input: this.input,
2247
2279
  executionId: this.executionId,
@@ -2260,7 +2292,11 @@ var WorkflowContext = class _WorkflowContext {
2260
2292
  pendingDecisions: this.pendingDecisions,
2261
2293
  toolOverrides: this.toolOverrides,
2262
2294
  signal: this.signal,
2263
- workflowName: this.workflowName
2295
+ workflowName: this.workflowName,
2296
+ // Join key for nested event correlation. Inherit the parent's
2297
+ // `parentToolCallId` when this child is itself nested inside another
2298
+ // child — so grand-children still point to the outermost tool call.
2299
+ parentToolCallId: parentToolCallId ?? this.parentToolCallId
2264
2300
  // Isolated: sessionHistory (empty), stepCounter (0),
2265
2301
  // onToken (null), onAgentStart (null), onToolCall (null)
2266
2302
  });
@@ -2430,11 +2466,22 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2430
2466
  this.emitTrace({
2431
2467
  type: "guardrail",
2432
2468
  agent: agent2._name,
2433
- data: { guardrailType: "input", blocked: inputResult.block, reason: inputResult.reason }
2469
+ data: {
2470
+ guardrailType: "input",
2471
+ blocked: inputResult.block,
2472
+ ...inputResult.reason ? { reason: inputResult.reason } : {},
2473
+ // Input guardrails can't retry (prompt is user-supplied), so attempt
2474
+ // and maxAttempts are always 1 — emit them for shape consistency with
2475
+ // output guardrails so consumers don't need two narrowers.
2476
+ attempt: 1,
2477
+ maxAttempts: 1
2478
+ }
2434
2479
  });
2435
2480
  this.spanManager?.addEventToActiveSpan("axl.guardrail.check", {
2436
2481
  "axl.guardrail.type": "input",
2437
2482
  "axl.guardrail.blocked": inputResult.block,
2483
+ "axl.guardrail.attempt": 1,
2484
+ "axl.guardrail.maxAttempts": 1,
2438
2485
  ...inputResult.reason ? { "axl.guardrail.reason": inputResult.reason } : {}
2439
2486
  });
2440
2487
  if (inputResult.block) {
@@ -2461,11 +2508,15 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2461
2508
  let guardrailOutputRetries = 0;
2462
2509
  let schemaRetries = 0;
2463
2510
  let validateRetries = 0;
2511
+ const maxGuardrailRetries = guardrails?.maxRetries ?? 2;
2512
+ let pendingRetryReason;
2513
+ const verboseTrace = this.config.trace?.level === "full";
2464
2514
  while (turns < maxTurns) {
2465
2515
  if (Date.now() - startTime > timeoutMs) {
2466
2516
  throw new TimeoutError("ctx.ask()", timeoutMs);
2467
2517
  }
2468
2518
  turns++;
2519
+ const turnStart = Date.now();
2469
2520
  const chatOptions = {
2470
2521
  model,
2471
2522
  temperature: options?.temperature ?? agent2._config.temperature,
@@ -2546,16 +2597,22 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2546
2597
  usageCapture.value = response.usage;
2547
2598
  }
2548
2599
  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
- }
2600
+ this._accumulateBudgetCost(response.cost);
2601
+ }
2602
+ let messagesSnapshot;
2603
+ if (verboseTrace) {
2604
+ try {
2605
+ messagesSnapshot = structuredClone(currentMessages);
2606
+ } catch (err) {
2607
+ console.warn(
2608
+ "[axl] verbose trace messages snapshot failed to clone; emitting shallow copy:",
2609
+ err instanceof Error ? err.message : String(err)
2610
+ );
2611
+ messagesSnapshot = [...currentMessages];
2557
2612
  }
2558
2613
  }
2614
+ const retryReason = pendingRetryReason;
2615
+ pendingRetryReason = void 0;
2559
2616
  this.emitTrace({
2560
2617
  type: "agent_call",
2561
2618
  agent: agent2._name,
@@ -2567,8 +2624,25 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2567
2624
  output: response.usage.completion_tokens,
2568
2625
  reasoning: response.usage.reasoning_tokens
2569
2626
  } : void 0,
2570
- duration: Date.now() - startTime,
2571
- data: { prompt, response: response.content }
2627
+ duration: Date.now() - turnStart,
2628
+ data: {
2629
+ prompt,
2630
+ response: response.content,
2631
+ ...systemPrompt ? { system: systemPrompt } : {},
2632
+ ...response.thinking_content ? { thinking: response.thinking_content } : {},
2633
+ params: {
2634
+ ...chatOptions.temperature !== void 0 ? { temperature: chatOptions.temperature } : {},
2635
+ maxTokens: chatOptions.maxTokens,
2636
+ ...chatOptions.effort !== void 0 ? { effort: chatOptions.effort } : {},
2637
+ ...chatOptions.thinkingBudget !== void 0 ? { thinkingBudget: chatOptions.thinkingBudget } : {},
2638
+ ...chatOptions.includeThoughts !== void 0 ? { includeThoughts: chatOptions.includeThoughts } : {},
2639
+ ...chatOptions.toolChoice !== void 0 ? { toolChoice: chatOptions.toolChoice } : {},
2640
+ ...chatOptions.stop !== void 0 ? { stop: chatOptions.stop } : {}
2641
+ },
2642
+ turn: turns,
2643
+ ...retryReason ? { retryReason } : {},
2644
+ ...messagesSnapshot ? { messages: messagesSnapshot } : {}
2645
+ }
2572
2646
  });
2573
2647
  if (response.tool_calls && response.tool_calls.length > 0) {
2574
2648
  currentMessages.push({
@@ -2633,7 +2707,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2633
2707
  this.emitTrace({
2634
2708
  type: "handoff",
2635
2709
  agent: agent2._name,
2636
- data: { target: targetName, mode, duration }
2710
+ data: {
2711
+ target: targetName,
2712
+ mode,
2713
+ duration,
2714
+ source: agent2._name,
2715
+ ...handoffPrompt !== prompt ? { message: handoffPrompt } : {}
2716
+ }
2637
2717
  });
2638
2718
  return result;
2639
2719
  }
@@ -2643,7 +2723,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2643
2723
  this.emitTrace({
2644
2724
  type: "handoff",
2645
2725
  agent: agent2._name,
2646
- data: { target: targetName, mode, duration: Date.now() - handoffStart }
2726
+ data: {
2727
+ target: targetName,
2728
+ mode,
2729
+ duration: Date.now() - handoffStart,
2730
+ source: agent2._name,
2731
+ ...handoffPrompt !== prompt ? { message: handoffPrompt } : {}
2732
+ }
2647
2733
  });
2648
2734
  }
2649
2735
  continue;
@@ -2663,7 +2749,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2663
2749
  this.emitTrace({
2664
2750
  type: "handoff",
2665
2751
  agent: agent2._name,
2666
- data: { target: targetName, mode, duration }
2752
+ data: { target: targetName, mode, duration, source: agent2._name }
2667
2753
  });
2668
2754
  return result;
2669
2755
  }
@@ -2673,7 +2759,12 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2673
2759
  this.emitTrace({
2674
2760
  type: "handoff",
2675
2761
  agent: agent2._name,
2676
- data: { target: targetName, mode, duration: Date.now() - handoffStart }
2762
+ data: {
2763
+ target: targetName,
2764
+ mode,
2765
+ duration: Date.now() - handoffStart,
2766
+ source: agent2._name
2767
+ }
2677
2768
  });
2678
2769
  return onewayResult;
2679
2770
  }
@@ -2766,24 +2857,18 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2766
2857
  });
2767
2858
  if (!decision.approved) {
2768
2859
  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
2860
  currentMessages.push({
2776
2861
  role: "tool",
2777
2862
  content: JSON.stringify({ error: `Tool denied by human: ${reason}` }),
2778
2863
  tool_call_id: toolCall.id
2779
2864
  });
2780
- return false;
2865
+ return { approved: false, reason };
2781
2866
  }
2782
- return true;
2867
+ return { approved: true };
2783
2868
  };
2784
- let approved;
2869
+ let approvalOutcome;
2785
2870
  if (this.spanManager) {
2786
- approved = await this.spanManager.withSpanAsync(
2871
+ approvalOutcome = await this.spanManager.withSpanAsync(
2787
2872
  "axl.tool.approval",
2788
2873
  {
2789
2874
  "axl.tool.name": toolName,
@@ -2791,20 +2876,24 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2791
2876
  },
2792
2877
  async (span) => {
2793
2878
  const result = await approvalFn();
2794
- span.setAttribute("axl.tool.approval.approved", result);
2879
+ span.setAttribute("axl.tool.approval.approved", result.approved);
2795
2880
  return result;
2796
2881
  }
2797
2882
  );
2798
2883
  } else {
2799
- approved = await approvalFn();
2884
+ approvalOutcome = await approvalFn();
2800
2885
  }
2801
- if (!approved) continue;
2802
2886
  this.emitTrace({
2803
- type: "tool_denied",
2887
+ type: "tool_approval",
2804
2888
  agent: agent2._name,
2805
2889
  tool: toolName,
2806
- data: { denied: false, args: toolArgs }
2890
+ data: {
2891
+ approved: approvalOutcome.approved,
2892
+ args: toolArgs,
2893
+ ...approvalOutcome.reason ? { reason: approvalOutcome.reason } : {}
2894
+ }
2807
2895
  });
2896
+ if (!approvalOutcome.approved) continue;
2808
2897
  }
2809
2898
  if (tool2 && tool2.hooks?.before) {
2810
2899
  try {
@@ -2837,7 +2926,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2837
2926
  resultContent2 = JSON.stringify(toolResult2);
2838
2927
  }
2839
2928
  } else if (tool2) {
2840
- const childCtx = this.createChildContext();
2929
+ const childCtx = this.createChildContext(toolCall.id);
2841
2930
  try {
2842
2931
  toolResult2 = await tool2._execute(toolArgs, childCtx);
2843
2932
  } catch (err) {
@@ -2897,37 +2986,43 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2897
2986
  const content = response.content;
2898
2987
  if (guardrails?.output) {
2899
2988
  const outputResult = await guardrails.output(content, { metadata: this.metadata });
2989
+ const attempt = guardrailOutputRetries + 1;
2990
+ const maxAttempts = maxGuardrailRetries + 1;
2991
+ const onBlock = guardrails.onBlock ?? "throw";
2992
+ let feedbackMessage;
2993
+ if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
2994
+ feedbackMessage = `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
2995
+ }
2900
2996
  this.emitTrace({
2901
2997
  type: "guardrail",
2902
2998
  agent: agent2._name,
2903
2999
  data: {
2904
3000
  guardrailType: "output",
2905
3001
  blocked: outputResult.block,
2906
- reason: outputResult.reason
3002
+ ...outputResult.reason ? { reason: outputResult.reason } : {},
3003
+ attempt,
3004
+ maxAttempts,
3005
+ ...feedbackMessage ? { feedbackMessage } : {}
2907
3006
  }
2908
3007
  });
2909
3008
  this.spanManager?.addEventToActiveSpan("axl.guardrail.check", {
2910
3009
  "axl.guardrail.type": "output",
2911
3010
  "axl.guardrail.blocked": outputResult.block,
3011
+ "axl.guardrail.attempt": attempt,
3012
+ "axl.guardrail.maxAttempts": maxAttempts,
2912
3013
  ...outputResult.reason ? { "axl.guardrail.reason": outputResult.reason } : {}
2913
3014
  });
2914
3015
  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
- }
3016
+ if (feedbackMessage) {
3017
+ guardrailOutputRetries++;
3018
+ appendRetryMessages(
3019
+ currentMessages,
3020
+ content,
3021
+ feedbackMessage,
3022
+ response.providerMetadata
3023
+ );
3024
+ pendingRetryReason = "guardrail";
3025
+ continue;
2931
3026
  }
2932
3027
  if (typeof onBlock === "function") {
2933
3028
  return onBlock(outputResult.reason ?? "Output blocked by guardrail", {
@@ -2939,30 +3034,58 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2939
3034
  }
2940
3035
  let validated = void 0;
2941
3036
  if (options?.schema) {
3037
+ const maxSchemaRetries = options.retries ?? 3;
3038
+ const schemaAttempt = schemaRetries + 1;
3039
+ const schemaMaxAttempts = maxSchemaRetries + 1;
3040
+ let schemaValid = false;
3041
+ let schemaReason;
3042
+ let schemaFeedback;
3043
+ let schemaErr;
2942
3044
  try {
2943
3045
  const parsed = JSON.parse(extractJson(content));
2944
3046
  validated = options.schema.parse(parsed);
3047
+ schemaValid = true;
2945
3048
  } catch (err) {
2946
- const maxSchemaRetries = options.retries ?? 3;
3049
+ schemaErr = err;
3050
+ schemaReason = err instanceof Error ? err.message : String(err);
2947
3051
  if (schemaRetries < maxSchemaRetries) {
3052
+ schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
3053
+ }
3054
+ }
3055
+ this.emitTrace({
3056
+ type: "schema_check",
3057
+ agent: agent2._name,
3058
+ data: {
3059
+ valid: schemaValid,
3060
+ ...schemaReason ? { reason: schemaReason } : {},
3061
+ attempt: schemaAttempt,
3062
+ maxAttempts: schemaMaxAttempts,
3063
+ ...schemaFeedback ? { feedbackMessage: schemaFeedback } : {}
3064
+ }
3065
+ });
3066
+ this.spanManager?.addEventToActiveSpan("axl.schema.check", {
3067
+ "axl.schema.valid": schemaValid,
3068
+ "axl.schema.attempt": schemaAttempt,
3069
+ "axl.schema.maxAttempts": schemaMaxAttempts,
3070
+ ...schemaReason ? { "axl.schema.reason": schemaReason } : {}
3071
+ });
3072
+ if (!schemaValid) {
3073
+ if (schemaFeedback) {
2948
3074
  schemaRetries++;
2949
- const errorMsg = err instanceof Error ? err.message : String(err);
2950
- currentMessages.push({
2951
- role: "assistant",
3075
+ appendRetryMessages(
3076
+ currentMessages,
2952
3077
  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
- });
3078
+ schemaFeedback,
3079
+ response.providerMetadata
3080
+ );
3081
+ pendingRetryReason = "schema";
2959
3082
  continue;
2960
3083
  }
2961
- const zodErr = err instanceof ZodError ? err : new ZodError([
3084
+ const zodErr = schemaErr instanceof ZodError ? schemaErr : new ZodError([
2962
3085
  {
2963
3086
  code: "custom",
2964
3087
  path: [],
2965
- message: err instanceof Error ? err.message : String(err)
3088
+ message: schemaReason ?? "Schema parse failed"
2966
3089
  }
2967
3090
  ]);
2968
3091
  throw new VerifyError(content, zodErr, maxSchemaRetries);
@@ -2978,31 +3101,40 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2978
3101
  const reason = err instanceof Error ? err.message : String(err);
2979
3102
  validateResult = { valid: false, reason: `Validator error: ${reason}` };
2980
3103
  }
3104
+ const maxValidateRetries = options.validateRetries ?? 2;
3105
+ const validateAttempt = validateRetries + 1;
3106
+ const validateMaxAttempts = maxValidateRetries + 1;
3107
+ let validateFeedback;
3108
+ if (!validateResult.valid && validateRetries < maxValidateRetries) {
3109
+ validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
3110
+ }
2981
3111
  this.emitTrace({
2982
3112
  type: "validate",
2983
3113
  agent: agent2._name,
2984
3114
  data: {
2985
3115
  valid: validateResult.valid,
2986
- ...validateResult.reason ? { reason: validateResult.reason } : {}
3116
+ ...validateResult.reason ? { reason: validateResult.reason } : {},
3117
+ attempt: validateAttempt,
3118
+ maxAttempts: validateMaxAttempts,
3119
+ ...validateFeedback ? { feedbackMessage: validateFeedback } : {}
2987
3120
  }
2988
3121
  });
2989
3122
  this.spanManager?.addEventToActiveSpan("axl.validate.check", {
2990
3123
  "axl.validate.valid": validateResult.valid,
3124
+ "axl.validate.attempt": validateAttempt,
3125
+ "axl.validate.maxAttempts": validateMaxAttempts,
2991
3126
  ...validateResult.reason ? { "axl.validate.reason": validateResult.reason } : {}
2992
3127
  });
2993
3128
  if (!validateResult.valid) {
2994
- const maxValidateRetries = options.validateRetries ?? 2;
2995
- if (validateRetries < maxValidateRetries) {
3129
+ if (validateFeedback) {
2996
3130
  validateRetries++;
2997
- currentMessages.push({
2998
- role: "assistant",
3131
+ appendRetryMessages(
3132
+ currentMessages,
2999
3133
  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
- });
3134
+ validateFeedback,
3135
+ response.providerMetadata
3136
+ );
3137
+ pendingRetryReason = "validate";
3006
3138
  continue;
3007
3139
  }
3008
3140
  throw new ValidationError(
@@ -3371,6 +3503,17 @@ ${summaryResponse.content}`
3371
3503
  async verify(fn, schema, options) {
3372
3504
  const maxRetries = options?.retries ?? 3;
3373
3505
  let lastRetry = void 0;
3506
+ const emitVerifyOutcome = (passed, attempts, lastError) => {
3507
+ this.emitTrace({
3508
+ type: "verify",
3509
+ agent: void 0,
3510
+ data: {
3511
+ passed,
3512
+ attempts,
3513
+ ...lastError ? { lastError } : {}
3514
+ }
3515
+ });
3516
+ };
3374
3517
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3375
3518
  let rawOutput;
3376
3519
  try {
@@ -3389,12 +3532,14 @@ ${summaryResponse.content}`
3389
3532
  const errorMsg = validateResult.reason ?? "Validation failed";
3390
3533
  lastRetry = { error: errorMsg, output: rawOutput, parsed };
3391
3534
  if (attempt === maxRetries) {
3535
+ emitVerifyOutcome(false, attempt + 1, errorMsg);
3392
3536
  if (options?.fallback !== void 0) return options.fallback;
3393
3537
  throw new ValidationError(parsed, errorMsg, maxRetries);
3394
3538
  }
3395
3539
  continue;
3396
3540
  }
3397
3541
  }
3542
+ emitVerifyOutcome(true, attempt + 1);
3398
3543
  return parsed;
3399
3544
  } catch (err) {
3400
3545
  if (err instanceof ValidationError) {
@@ -3404,6 +3549,7 @@ ${summaryResponse.content}`
3404
3549
  parsed: err.lastOutput
3405
3550
  };
3406
3551
  if (attempt === maxRetries) {
3552
+ emitVerifyOutcome(false, attempt + 1, err.reason);
3407
3553
  if (options?.fallback !== void 0) return options.fallback;
3408
3554
  throw err;
3409
3555
  }
@@ -3412,6 +3558,7 @@ ${summaryResponse.content}`
3412
3558
  if (err instanceof VerifyError) {
3413
3559
  lastRetry = { error: err.message, output: rawOutput ?? err.lastOutput };
3414
3560
  if (attempt === maxRetries) {
3561
+ emitVerifyOutcome(false, attempt + 1, err.message);
3415
3562
  if (options?.fallback !== void 0) return options.fallback;
3416
3563
  throw err;
3417
3564
  }
@@ -3420,12 +3567,14 @@ ${summaryResponse.content}`
3420
3567
  const errorMsg = err instanceof ZodError ? err.message : err instanceof Error ? err.message : String(err);
3421
3568
  lastRetry = { error: errorMsg, output: rawOutput };
3422
3569
  if (attempt === maxRetries) {
3570
+ emitVerifyOutcome(false, attempt + 1, errorMsg);
3423
3571
  if (options?.fallback !== void 0) return options.fallback;
3424
3572
  const zodErr = err instanceof ZodError ? err : new ZodError([{ code: "custom", path: [], message: errorMsg }]);
3425
3573
  throw new VerifyError(rawOutput, zodErr, maxRetries);
3426
3574
  }
3427
3575
  }
3428
3576
  }
3577
+ emitVerifyOutcome(false, maxRetries + 1, lastRetry?.error ?? "Verify failed");
3429
3578
  if (options?.fallback !== void 0) return options.fallback;
3430
3579
  throw new VerifyError(
3431
3580
  lastRetry?.output,
@@ -3731,6 +3880,76 @@ ${summaryResponse.content}`
3731
3880
  }
3732
3881
  return decision;
3733
3882
  }
3883
+ /**
3884
+ * Compose an `AbortSignal` for a memory operation's underlying embedder
3885
+ * fetch. Combines the parent context signal (user cancellation,
3886
+ * `runtime.execute({ signal })`) with the budget hard_stop abort so
3887
+ * that either cause correctly aborts an in-flight embed call.
3888
+ *
3889
+ * Returns `undefined` if there's nothing to cancel on — the embedder
3890
+ * runs without a signal in that case, matching its prior behavior.
3891
+ *
3892
+ * @internal
3893
+ */
3894
+ _composeMemorySignal() {
3895
+ const budgetSignal = this.budgetContext?.abortController?.signal;
3896
+ if (this.signal && budgetSignal) return AbortSignal.any([this.signal, budgetSignal]);
3897
+ return this.signal ?? budgetSignal;
3898
+ }
3899
+ /**
3900
+ * Accumulate a cost amount into the active `budgetContext` and trip the
3901
+ * `exceeded` flag if we've crossed the limit. On `hard_stop` policy,
3902
+ * also fires the abort controller so in-flight operations cancel.
3903
+ *
3904
+ * Called from every code path that spends money: the `agent_call` loop,
3905
+ * semantic memory operations (`ctx.remember({embed:true})`, `ctx.recall({query})`),
3906
+ * and any future cost-emitting primitive. Centralizing the logic here
3907
+ * means `ctx.budget({ limit, policy })` accurately enforces the limit
3908
+ * across ALL cost sources — not just agent calls.
3909
+ *
3910
+ * @internal
3911
+ */
3912
+ _accumulateBudgetCost(amount) {
3913
+ if (!this.budgetContext) return;
3914
+ if (!Number.isFinite(amount) || amount <= 0) return;
3915
+ this.budgetContext.totalCost += amount;
3916
+ if (this.budgetContext.totalCost >= this.budgetContext.limit) {
3917
+ this.budgetContext.exceeded = true;
3918
+ if (this.budgetContext.policy === "hard_stop" && this.budgetContext.abortController) {
3919
+ this.budgetContext.abortController.abort();
3920
+ }
3921
+ }
3922
+ }
3923
+ // ── workflow lifecycle trace emission ──────────────────────────────────
3924
+ //
3925
+ // These are called by the runtime at execution boundaries. They emit
3926
+ // first-class `workflow_start` / `workflow_end` trace events instead of
3927
+ // the previous `ctx.log('workflow_start', ...)` indirection — so consumers
3928
+ // that narrow via `event.type === 'workflow_start'` actually see them.
3929
+ // Internal: the runtime is the only caller, user workflows never call these.
3930
+ /** @internal */
3931
+ _emitWorkflowStart(input) {
3932
+ this.emitTrace({
3933
+ type: "workflow_start",
3934
+ workflow: this.workflowName,
3935
+ data: { input }
3936
+ });
3937
+ }
3938
+ /** @internal */
3939
+ _emitWorkflowEnd(info) {
3940
+ this.emitTrace({
3941
+ type: "workflow_end",
3942
+ workflow: this.workflowName,
3943
+ duration: info.duration,
3944
+ data: {
3945
+ status: info.status,
3946
+ duration: info.duration,
3947
+ ...info.result !== void 0 ? { result: info.result } : {},
3948
+ ...info.error !== void 0 ? { error: info.error } : {},
3949
+ ...info.aborted ? { aborted: true } : {}
3950
+ }
3951
+ });
3952
+ }
3734
3953
  // ── ctx.log() ─────────────────────────────────────────────────────────
3735
3954
  log(event, data) {
3736
3955
  this.emitTrace({
@@ -3766,8 +3985,59 @@ ${summaryResponse.content}`
3766
3985
  if (!this.stateStore) {
3767
3986
  throw new Error("A state store is required for memory operations.");
3768
3987
  }
3988
+ if (this.budgetContext?.exceeded) {
3989
+ const { limit, totalCost: spent, policy } = this.budgetContext;
3990
+ if (policy !== "warn") {
3991
+ throw new BudgetExceededError(limit, spent, policy);
3992
+ }
3993
+ }
3769
3994
  const sessionId = this.metadata?.sessionId;
3770
- await this.memoryManager.remember(key, value, this.stateStore, sessionId, options);
3995
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
3996
+ try {
3997
+ const memorySignal = this._composeMemorySignal();
3998
+ const { usage } = await this.memoryManager.remember(
3999
+ key,
4000
+ value,
4001
+ this.stateStore,
4002
+ sessionId,
4003
+ options,
4004
+ memorySignal
4005
+ );
4006
+ if (usage?.cost != null) {
4007
+ this._accumulateBudgetCost(usage.cost);
4008
+ }
4009
+ this.emitTrace({
4010
+ type: "log",
4011
+ ...usage?.cost != null ? { cost: usage.cost } : {},
4012
+ ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
4013
+ data: {
4014
+ event: "memory_remember",
4015
+ key,
4016
+ scope,
4017
+ embed: options?.embed === true,
4018
+ ...usage ? { usage } : {}
4019
+ }
4020
+ });
4021
+ } catch (err) {
4022
+ const partialUsage = err.axlEmbedUsage;
4023
+ if (partialUsage?.cost != null) {
4024
+ this._accumulateBudgetCost(partialUsage.cost);
4025
+ }
4026
+ this.emitTrace({
4027
+ type: "log",
4028
+ ...partialUsage?.cost != null ? { cost: partialUsage.cost } : {},
4029
+ ...partialUsage?.tokens != null ? { tokens: { input: partialUsage.tokens } } : {},
4030
+ data: {
4031
+ event: "memory_remember",
4032
+ key,
4033
+ scope,
4034
+ embed: options?.embed === true,
4035
+ ...partialUsage ? { usage: partialUsage } : {},
4036
+ error: err instanceof Error ? err.message : String(err)
4037
+ }
4038
+ });
4039
+ throw err;
4040
+ }
3771
4041
  }
3772
4042
  /**
3773
4043
  * Recall a value from memory by key, or perform semantic search if query option is provided.
@@ -3781,8 +4051,64 @@ ${summaryResponse.content}`
3781
4051
  if (!this.stateStore) {
3782
4052
  throw new Error("A state store is required for memory operations.");
3783
4053
  }
4054
+ if (this.budgetContext?.exceeded) {
4055
+ const { limit, totalCost: spent, policy } = this.budgetContext;
4056
+ if (policy !== "warn") {
4057
+ throw new BudgetExceededError(limit, spent, policy);
4058
+ }
4059
+ }
3784
4060
  const sessionId = this.metadata?.sessionId;
3785
- return this.memoryManager.recall(key, this.stateStore, sessionId, options);
4061
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
4062
+ const semantic = options?.query !== void 0;
4063
+ try {
4064
+ const memorySignal = this._composeMemorySignal();
4065
+ const { data, usage } = await this.memoryManager.recall(
4066
+ key,
4067
+ this.stateStore,
4068
+ sessionId,
4069
+ options,
4070
+ memorySignal
4071
+ );
4072
+ let hit;
4073
+ let resultCount;
4074
+ if (semantic) {
4075
+ resultCount = Array.isArray(data) ? data.length : 0;
4076
+ hit = resultCount > 0;
4077
+ } else {
4078
+ hit = data !== null && data !== void 0;
4079
+ }
4080
+ if (usage?.cost != null) {
4081
+ this._accumulateBudgetCost(usage.cost);
4082
+ }
4083
+ this.emitTrace({
4084
+ type: "log",
4085
+ ...usage?.cost != null ? { cost: usage.cost } : {},
4086
+ ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
4087
+ data: {
4088
+ event: "memory_recall",
4089
+ key,
4090
+ scope,
4091
+ semantic,
4092
+ hit,
4093
+ ...resultCount !== void 0 ? { resultCount } : {},
4094
+ ...usage ? { usage } : {}
4095
+ }
4096
+ });
4097
+ return data;
4098
+ } catch (err) {
4099
+ this.emitTrace({
4100
+ type: "log",
4101
+ data: {
4102
+ event: "memory_recall",
4103
+ key,
4104
+ scope,
4105
+ semantic,
4106
+ hit: false,
4107
+ error: err instanceof Error ? err.message : String(err)
4108
+ }
4109
+ });
4110
+ throw err;
4111
+ }
3786
4112
  }
3787
4113
  /** Delete a memory entry by key. */
3788
4114
  async forget(key, options) {
@@ -3795,7 +4121,25 @@ ${summaryResponse.content}`
3795
4121
  throw new Error("A state store is required for memory operations.");
3796
4122
  }
3797
4123
  const sessionId = this.metadata?.sessionId;
3798
- await this.memoryManager.forget(key, this.stateStore, sessionId, options);
4124
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
4125
+ try {
4126
+ await this.memoryManager.forget(key, this.stateStore, sessionId, options);
4127
+ this.emitTrace({
4128
+ type: "log",
4129
+ data: { event: "memory_forget", key, scope }
4130
+ });
4131
+ } catch (err) {
4132
+ this.emitTrace({
4133
+ type: "log",
4134
+ data: {
4135
+ event: "memory_forget",
4136
+ key,
4137
+ scope,
4138
+ error: err instanceof Error ? err.message : String(err)
4139
+ }
4140
+ });
4141
+ throw err;
4142
+ }
3799
4143
  }
3800
4144
  // ── ctx.delegate() ──────────────────────────────────────────────────
3801
4145
  /**
@@ -3823,6 +4167,15 @@ ${summaryResponse.content}`
3823
4167
  names.add(a._name);
3824
4168
  }
3825
4169
  if (agents.length === 1) {
4170
+ this.emitTrace({
4171
+ type: "delegate",
4172
+ agent: agents[0]._name,
4173
+ data: {
4174
+ candidates: [agents[0]._name],
4175
+ selected: agents[0]._name,
4176
+ reason: "single_candidate"
4177
+ }
4178
+ });
3826
4179
  return this.ask(agents[0], prompt, {
3827
4180
  schema: options?.schema,
3828
4181
  retries: options?.retries,
@@ -3863,7 +4216,8 @@ ${summaryResponse.content}`
3863
4216
  agent: "_delegate_router",
3864
4217
  data: {
3865
4218
  candidates: agents.map((a) => a._name),
3866
- routerModel: routerModelUri
4219
+ routerModel: routerModelUri,
4220
+ reason: "routed"
3867
4221
  }
3868
4222
  });
3869
4223
  return this.ask(routerAgent, prompt, {
@@ -3875,19 +4229,117 @@ ${summaryResponse.content}`
3875
4229
  });
3876
4230
  }
3877
4231
  // ── Private ───────────────────────────────────────────────────────────
4232
+ /**
4233
+ * Internal emitter input — intentionally loose so call sites don't need to
4234
+ * build a perfectly-narrowed discriminated-union member. The resulting
4235
+ * `TraceEvent` (exported type) remains strict, and TypeScript narrows it at
4236
+ * consumer sites via the `type` discriminator.
4237
+ */
3878
4238
  emitTrace(partial) {
3879
4239
  let data = partial.data;
3880
- if (this.config.trace?.redact && partial.type === "agent_call" && data) {
3881
- data = { ...data, prompt: "[redacted]", response: "[redacted]" };
4240
+ if (this.config.trace?.redact && data) {
4241
+ if (partial.type === "agent_call") {
4242
+ const d = data;
4243
+ const redacted = {
4244
+ ...d,
4245
+ prompt: "[redacted]",
4246
+ response: "[redacted]"
4247
+ };
4248
+ if (d.system !== void 0) redacted.system = "[redacted]";
4249
+ if (d.thinking !== void 0) redacted.thinking = "[redacted]";
4250
+ if (Array.isArray(d.messages)) {
4251
+ redacted.messages = [
4252
+ { role: "system", content: `[${d.messages.length} messages redacted]` }
4253
+ ];
4254
+ }
4255
+ data = redacted;
4256
+ } else if (partial.type === "guardrail" || partial.type === "schema_check" || partial.type === "validate") {
4257
+ const d = data;
4258
+ const needsRedact = d.feedbackMessage !== void 0 || d.reason !== void 0;
4259
+ if (needsRedact) {
4260
+ data = {
4261
+ ...d,
4262
+ ...d.reason !== void 0 ? { reason: "[redacted]" } : {},
4263
+ ...d.feedbackMessage !== void 0 ? { feedbackMessage: "[redacted]" } : {}
4264
+ };
4265
+ }
4266
+ } else if (partial.type === "tool_call") {
4267
+ const d = data;
4268
+ data = {
4269
+ ...d,
4270
+ args: "[redacted]",
4271
+ result: "[redacted]"
4272
+ };
4273
+ } else if (partial.type === "tool_approval") {
4274
+ const d = data;
4275
+ data = {
4276
+ ...d,
4277
+ args: "[redacted]",
4278
+ ...d.reason !== void 0 ? { reason: "[redacted]" } : {}
4279
+ };
4280
+ } else if (partial.type === "handoff") {
4281
+ const d = data;
4282
+ if (d.message !== void 0) {
4283
+ data = { ...d, message: "[redacted]" };
4284
+ }
4285
+ } else if (partial.type === "workflow_start") {
4286
+ const d = data;
4287
+ if (d.input !== void 0) {
4288
+ data = { ...d, input: "[redacted]" };
4289
+ }
4290
+ } else if (partial.type === "workflow_end") {
4291
+ const d = data;
4292
+ const redacted = { ...d };
4293
+ if (d.result !== void 0) redacted.result = "[redacted]";
4294
+ if (d.error !== void 0) redacted.error = "[redacted]";
4295
+ data = redacted;
4296
+ } else if (partial.type === "log") {
4297
+ const d = data;
4298
+ const redacted = {};
4299
+ for (const [k, v] of Object.entries(d)) {
4300
+ if (k === "event" || typeof v === "number" || typeof v === "boolean") {
4301
+ redacted[k] = v;
4302
+ } else if (typeof v === "string") {
4303
+ redacted[k] = "[redacted]";
4304
+ } else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
4305
+ const inner = v;
4306
+ const innerRedacted = {};
4307
+ for (const [ik, iv] of Object.entries(inner)) {
4308
+ if (typeof iv === "number" || typeof iv === "boolean") {
4309
+ innerRedacted[ik] = iv;
4310
+ } else if (typeof iv === "string") {
4311
+ innerRedacted[ik] = "[redacted]";
4312
+ } else {
4313
+ innerRedacted[ik] = "[redacted]";
4314
+ }
4315
+ }
4316
+ redacted[k] = innerRedacted;
4317
+ } else {
4318
+ redacted[k] = "[redacted]";
4319
+ }
4320
+ }
4321
+ data = redacted;
4322
+ }
3882
4323
  }
3883
4324
  const event = {
3884
4325
  executionId: this.executionId,
3885
4326
  step: this.stepCounter++,
3886
4327
  timestamp: Date.now(),
4328
+ ...this.workflowName ? { workflow: this.workflowName } : {},
4329
+ ...this.parentToolCallId ? { parentToolCallId: this.parentToolCallId } : {},
3887
4330
  ...partial,
3888
4331
  data
3889
4332
  };
3890
- this.onTrace?.(event);
4333
+ if (this.onTrace) {
4334
+ try {
4335
+ this.onTrace(event);
4336
+ } catch (err) {
4337
+ console.error(
4338
+ "[axl] onTrace handler threw; trace event dropped:",
4339
+ err instanceof Error ? err.message : String(err)
4340
+ );
4341
+ }
4342
+ }
3891
4343
  }
3892
4344
  };
3893
4345
 
@@ -5129,6 +5581,18 @@ var McpManager = class {
5129
5581
  };
5130
5582
 
5131
5583
  // src/memory/manager.ts
5584
+ function assertEmbedResult(result) {
5585
+ if (Array.isArray(result)) {
5586
+ throw new Error(
5587
+ "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() }`."
5588
+ );
5589
+ }
5590
+ if (!result || typeof result !== "object" || !Array.isArray(result.vectors)) {
5591
+ throw new Error(
5592
+ `Embedder.embed() must return { vectors: number[][]; usage?: EmbedUsage }. Got ${result === null ? "null" : typeof result}.`
5593
+ );
5594
+ }
5595
+ }
5132
5596
  var MemoryManager = class {
5133
5597
  vectorStore;
5134
5598
  embedder;
@@ -5143,8 +5607,11 @@ var MemoryManager = class {
5143
5607
  * Store a key-value pair in memory.
5144
5608
  * If a vector store and embedder are configured and embed is true,
5145
5609
  * also embeds the value for semantic search.
5610
+ *
5611
+ * Returns `{ usage }` when embedding happened, carrying the embedder's
5612
+ * cost/token reporting out to the caller for cost attribution.
5146
5613
  */
5147
- async remember(key, value, stateStore, sessionId, options) {
5614
+ async remember(key, value, stateStore, sessionId, options, signal) {
5148
5615
  const scope = options?.scope ?? "session";
5149
5616
  if (scope === "session" && !sessionId) {
5150
5617
  throw new Error(
@@ -5163,23 +5630,44 @@ var MemoryManager = class {
5163
5630
  }
5164
5631
  if (options?.embed === true && this.vectorStore && this.embedder) {
5165
5632
  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 }
5633
+ const raw = await this.embedder.embed([text], signal);
5634
+ assertEmbedResult(raw);
5635
+ const { vectors, usage } = raw;
5636
+ try {
5637
+ await this.vectorStore.upsert([
5638
+ {
5639
+ id: `${storeScope}:${key}`,
5640
+ content: text,
5641
+ embedding: vectors[0],
5642
+ metadata: { key, scope, ...options?.metadata }
5643
+ }
5644
+ ]);
5645
+ } catch (err) {
5646
+ if (usage && typeof err === "object" && err !== null) {
5647
+ Object.defineProperty(err, "axlEmbedUsage", {
5648
+ value: usage,
5649
+ enumerable: false,
5650
+ writable: true,
5651
+ configurable: true
5652
+ });
5173
5653
  }
5174
- ]);
5654
+ throw err;
5655
+ }
5656
+ return usage ? { usage } : {};
5175
5657
  }
5658
+ return {};
5176
5659
  }
5177
5660
  /**
5178
5661
  * Recall a value from memory by key, or perform semantic search if query is provided.
5662
+ *
5663
+ * Always returns `{ data }`; semantic recalls additionally include
5664
+ * `usage` from the embedder (when the embedder reported it).
5179
5665
  */
5180
- async recall(key, stateStore, sessionId, options) {
5666
+ async recall(key, stateStore, sessionId, options, signal) {
5181
5667
  if (options?.query && this.vectorStore && this.embedder) {
5182
- const [embedding] = await this.embedder.embed([options.query]);
5668
+ const raw = await this.embedder.embed([options.query], signal);
5669
+ assertEmbedResult(raw);
5670
+ const { vectors, usage } = raw;
5183
5671
  const topK = options?.topK ?? 5;
5184
5672
  const searchScope = options?.scope ?? "session";
5185
5673
  if (searchScope === "session" && !sessionId) {
@@ -5187,13 +5675,14 @@ var MemoryManager = class {
5187
5675
  'sessionId is required for session-scoped memory. Use { scope: "global" } or provide a sessionId in metadata.'
5188
5676
  );
5189
5677
  }
5190
- const rawResults = await this.vectorStore.search(embedding, topK * 3);
5678
+ const rawResults = await this.vectorStore.search(vectors[0], topK * 3);
5191
5679
  const filtered = rawResults.filter((r) => {
5192
5680
  if (r.id.startsWith("global:")) return true;
5193
5681
  if (searchScope === "session") return r.id.startsWith(`session:${sessionId}:`);
5194
5682
  return true;
5195
5683
  });
5196
- return filtered.slice(0, topK);
5684
+ const data = filtered.slice(0, topK);
5685
+ return usage ? { data, usage } : { data };
5197
5686
  }
5198
5687
  const scope = options?.scope ?? "session";
5199
5688
  if (scope === "session" && !sessionId) {
@@ -5203,10 +5692,10 @@ var MemoryManager = class {
5203
5692
  }
5204
5693
  const storeScope = scope === "session" ? `session:${sessionId}` : "global";
5205
5694
  if (stateStore.getMemory) {
5206
- return stateStore.getMemory(storeScope, key);
5695
+ return { data: await stateStore.getMemory(storeScope, key) };
5207
5696
  }
5208
5697
  const storeKey = `memory:${storeScope}:${key}`;
5209
- return stateStore.getSessionMeta(storeKey, "value");
5698
+ return { data: await stateStore.getSessionMeta(storeKey, "value") };
5210
5699
  }
5211
5700
  /** Delete a memory entry. If a vector store is configured, also removes the embedding. */
5212
5701
  async forget(key, stateStore, sessionId, options) {
@@ -5306,6 +5795,22 @@ var AxlRuntime = class extends EventEmitter2 {
5306
5795
  });
5307
5796
  }
5308
5797
  }
5798
+ /**
5799
+ * Whether `config.trace.redact` is enabled on this runtime. A narrow
5800
+ * boolean getter is preferred over exposing the full config because:
5801
+ * (a) `Readonly<AxlConfig>` is shallow so the config would be mutable
5802
+ * at runtime through sub-objects, subverting any compliance guarantee;
5803
+ * (b) observability consumers like Studio only need the boolean, not
5804
+ * the whole config tree; (c) if future observability config needs to
5805
+ * expand, each new flag gets its own narrow getter.
5806
+ *
5807
+ * Studio's server-side redaction helpers (executions, memory, sessions,
5808
+ * evals, decisions, tools, playground, workflows) all route through
5809
+ * this accessor to decide whether to scrub response payloads.
5810
+ */
5811
+ isRedactEnabled() {
5812
+ return this.config.trace?.redact === true;
5813
+ }
5309
5814
  /**
5310
5815
  * Initialize MCP servers configured in the config.
5311
5816
  * Call this before executing workflows that use MCP tools.
@@ -5455,10 +5960,19 @@ var AxlRuntime = class extends EventEmitter2 {
5455
5960
  metadata: result2.metadata ?? metadata
5456
5961
  };
5457
5962
  };
5458
- result = await runEvalFn(entry.config, wrappedExecuteFn, this);
5963
+ result = await runEvalFn(entry.config, wrappedExecuteFn, this, {
5964
+ onProgress: options?.onProgress,
5965
+ signal: options?.signal,
5966
+ captureTraces: options?.captureTraces
5967
+ });
5459
5968
  } else {
5460
5969
  result = await this.eval(
5461
- entry.config
5970
+ entry.config,
5971
+ {
5972
+ onProgress: options?.onProgress,
5973
+ signal: options?.signal,
5974
+ captureTraces: options?.captureTraces
5975
+ }
5462
5976
  );
5463
5977
  }
5464
5978
  const resultObj = result;
@@ -5596,6 +6110,7 @@ var AxlRuntime = class extends EventEmitter2 {
5596
6110
  }
5597
6111
  },
5598
6112
  pendingDecisions: this.pendingDecisionResolvers,
6113
+ awaitHumanHandler: options?.awaitHumanHandler,
5599
6114
  stateStore: this.stateStore,
5600
6115
  workflowName: name,
5601
6116
  mcpManager: this.mcpManager,
@@ -5609,7 +6124,6 @@ var AxlRuntime = class extends EventEmitter2 {
5609
6124
  policy: "finish_and_stop"
5610
6125
  }
5611
6126
  });
5612
- ctx.log("workflow_start", { workflow: name, executionId });
5613
6127
  return this.spanManager.withSpanAsync(
5614
6128
  "axl.workflow.execute",
5615
6129
  {
@@ -5619,17 +6133,17 @@ var AxlRuntime = class extends EventEmitter2 {
5619
6133
  },
5620
6134
  async (span) => {
5621
6135
  try {
6136
+ ctx._emitWorkflowStart(validated);
5622
6137
  const result = await workflow2.handler(ctx);
5623
6138
  const output = workflow2.outputSchema ? workflow2.outputSchema.parse(result) : result;
5624
6139
  execInfo.status = "completed";
5625
6140
  execInfo.completedAt = Date.now();
5626
6141
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5627
6142
  execInfo.result = output;
5628
- ctx.log("workflow_end", {
5629
- workflow: name,
6143
+ ctx._emitWorkflowEnd({
5630
6144
  status: "completed",
5631
6145
  duration: execInfo.duration,
5632
- cost: execInfo.totalCost
6146
+ result: output
5633
6147
  });
5634
6148
  if (this.stateStore.deleteCheckpoints) {
5635
6149
  await this.stateStore.deleteCheckpoints(executionId);
@@ -5639,11 +6153,17 @@ var AxlRuntime = class extends EventEmitter2 {
5639
6153
  this.persistExecution(execInfo);
5640
6154
  return output;
5641
6155
  } catch (err) {
6156
+ const aborted = typeof err === "object" && err !== null && err.name === "AbortError";
5642
6157
  execInfo.status = "failed";
5643
6158
  execInfo.completedAt = Date.now();
5644
6159
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5645
6160
  execInfo.error = err instanceof Error ? err.message : String(err);
5646
- ctx.log("workflow_end", { workflow: name, status: "failed", error: execInfo.error });
6161
+ ctx._emitWorkflowEnd({
6162
+ status: "failed",
6163
+ duration: execInfo.duration,
6164
+ error: execInfo.error,
6165
+ ...aborted ? { aborted: true } : {}
6166
+ });
5647
6167
  this.persistExecution(execInfo);
5648
6168
  throw err;
5649
6169
  } finally {
@@ -5708,24 +6228,16 @@ var AxlRuntime = class extends EventEmitter2 {
5708
6228
  duration: data?.duration ?? void 0
5709
6229
  });
5710
6230
  }
5711
- } else if (event.type === "tool_denied") {
6231
+ } else if (event.type === "tool_approval") {
5712
6232
  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
5719
- });
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
5727
- });
5728
- }
6233
+ axlStream._push({
6234
+ type: "tool_approval",
6235
+ name: event.tool ?? "",
6236
+ args: data?.args,
6237
+ approved: data?.approved === true,
6238
+ ...data?.reason ? { reason: data.reason } : {}
6239
+ });
6240
+ } else if (event.type === "tool_denied") {
5729
6241
  } else if (event.type === "agent_call") {
5730
6242
  axlStream._push({
5731
6243
  type: "agent_end",
@@ -5758,6 +6270,7 @@ var AxlRuntime = class extends EventEmitter2 {
5758
6270
  axlStream._push({ type: "agent_start", agent: info.agent, model: info.model });
5759
6271
  },
5760
6272
  pendingDecisions: this.pendingDecisionResolvers,
6273
+ awaitHumanHandler: options?.awaitHumanHandler,
5761
6274
  stateStore: this.stateStore,
5762
6275
  workflowName: name,
5763
6276
  mcpManager: this.mcpManager,
@@ -5781,17 +6294,17 @@ var AxlRuntime = class extends EventEmitter2 {
5781
6294
  },
5782
6295
  async (span) => {
5783
6296
  try {
6297
+ wfCtx._emitWorkflowStart(validated);
5784
6298
  const rawResult = await workflow2.handler(wfCtx);
5785
6299
  const result = workflow2.outputSchema ? workflow2.outputSchema.parse(rawResult) : rawResult;
5786
6300
  execInfo.status = "completed";
5787
6301
  execInfo.completedAt = Date.now();
5788
6302
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5789
6303
  execInfo.result = result;
5790
- wfCtx.log("workflow_end", {
5791
- workflow: name,
6304
+ wfCtx._emitWorkflowEnd({
5792
6305
  status: "completed",
5793
6306
  duration: execInfo.duration,
5794
- cost: execInfo.totalCost
6307
+ result
5795
6308
  });
5796
6309
  if (this.stateStore.deleteCheckpoints) {
5797
6310
  await this.stateStore.deleteCheckpoints(executionId);
@@ -5808,14 +6321,16 @@ var AxlRuntime = class extends EventEmitter2 {
5808
6321
  };
5809
6322
  run().then((result) => axlStream._done(result)).catch((err) => {
5810
6323
  if (execInfo) {
6324
+ const aborted = typeof err === "object" && err !== null && err.name === "AbortError";
5811
6325
  execInfo.status = "failed";
5812
6326
  execInfo.completedAt = Date.now();
5813
6327
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5814
6328
  execInfo.error = err instanceof Error ? err.message : String(err);
5815
- ctx?.log("workflow_end", {
5816
- workflow: name,
6329
+ ctx?._emitWorkflowEnd({
5817
6330
  status: "failed",
5818
- error: execInfo.error
6331
+ duration: execInfo.duration,
6332
+ error: execInfo.error,
6333
+ ...aborted ? { aborted: true } : {}
5819
6334
  });
5820
6335
  this.persistExecution(execInfo);
5821
6336
  }
@@ -5880,6 +6395,7 @@ var AxlRuntime = class extends EventEmitter2 {
5880
6395
  if (this.stateStore.saveEvalResult) {
5881
6396
  await this.stateStore.saveEvalResult(entry);
5882
6397
  }
6398
+ this.emit("eval_result", entry);
5883
6399
  }
5884
6400
  /**
5885
6401
  * Delete an eval history entry by id. Removes from in-memory cache and
@@ -6011,7 +6527,7 @@ var AxlRuntime = class extends EventEmitter2 {
6011
6527
  *
6012
6528
  * @see Spec Section 13.5
6013
6529
  */
6014
- async eval(config) {
6530
+ async eval(config, options) {
6015
6531
  let runEvalFn;
6016
6532
  try {
6017
6533
  ({ runEval: runEvalFn } = await import("@axlsdk/eval"));
@@ -6026,7 +6542,7 @@ var AxlRuntime = class extends EventEmitter2 {
6026
6542
  });
6027
6543
  return { output: result, cost, metadata };
6028
6544
  };
6029
- return runEvalFn(config, executeWorkflow, this);
6545
+ return runEvalFn(config, executeWorkflow, this, options);
6030
6546
  }
6031
6547
  /**
6032
6548
  * Compare two evaluation results to detect regressions and improvements.
@@ -6064,9 +6580,31 @@ var AxlRuntime = class extends EventEmitter2 {
6064
6580
  * Returns cost (same as `trackCost`) plus metadata extracted from trace events:
6065
6581
  * models (unique URIs), tokens (input/output/reasoning sums), and agent call count.
6066
6582
  *
6583
+ * ## Cost vs tokens semantics
6584
+ *
6585
+ * - `cost` is the full aggregate across EVERY event with a top-level
6586
+ * `event.cost` set: agent calls, tool calls, semantic memory ops, etc.
6587
+ * This is the number to reconcile against your provider bill.
6588
+ *
6589
+ * - `metadata.tokens` is narrowly scoped to **agent** prompt/completion/
6590
+ * reasoning tokens. Embedder tokens from semantic `ctx.remember({embed:true})`
6591
+ * / `ctx.recall({query})` are deliberately NOT summed here — they're a
6592
+ * different category (input-only, different pricing, different model).
6593
+ * Conflating them would make "prompt tokens" misleading in the UI. If you
6594
+ * need embedder token counts, subscribe to `runtime.on('trace', ...)` and
6595
+ * read `data.usage.tokens` on `memory_remember` / `memory_recall` events.
6596
+ *
6597
+ * Pass `{ captureTraces: true }` to also collect the raw `TraceEvent[]` observed
6598
+ * during `fn()`. This is opt-in because it keeps every event in memory for the
6599
+ * duration of the call — useful for eval per-item capture, debugging, and test
6600
+ * assertions, but overhead grows with trace volume. When enabled, verbose-mode
6601
+ * `agent_call.data.messages` snapshots are omitted from captured events (still
6602
+ * broadcast via onTrace) to keep memory bounded — callers who need the full
6603
+ * verbose snapshot should subscribe to `runtime.on('trace', ...)` directly.
6604
+ *
6067
6605
  * Works with both `createContext()` and `execute()` calls inside `fn`.
6068
6606
  */
6069
- async trackExecution(fn) {
6607
+ async trackExecution(fn, options) {
6070
6608
  const parentScope = costScopeStorage.getStore();
6071
6609
  const scope = {
6072
6610
  totalCost: 0,
@@ -6077,6 +6615,7 @@ var AxlRuntime = class extends EventEmitter2 {
6077
6615
  const workflowCalls = /* @__PURE__ */ new Map();
6078
6616
  const tokens = { input: 0, output: 0, reasoning: 0 };
6079
6617
  let agentCalls = 0;
6618
+ const capturedTraces = options?.captureTraces ? [] : void 0;
6080
6619
  const listener = (event) => {
6081
6620
  if (!scope.trackedIds.has(event.executionId)) return;
6082
6621
  if (event.cost) scope.totalCost += event.cost;
@@ -6089,17 +6628,22 @@ var AxlRuntime = class extends EventEmitter2 {
6089
6628
  tokens.reasoning += event.tokens.reasoning ?? 0;
6090
6629
  }
6091
6630
  }
6092
- let workflowName;
6093
6631
  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
- }
6632
+ workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
6100
6633
  }
6101
- if (workflowName) {
6102
- workflowCalls.set(workflowName, (workflowCalls.get(workflowName) ?? 0) + 1);
6634
+ if (capturedTraces) {
6635
+ if (event.type === "agent_call" && event.data) {
6636
+ const d = event.data;
6637
+ if ("messages" in d) {
6638
+ const rest = {};
6639
+ for (const k of Object.keys(d)) {
6640
+ if (k !== "messages") rest[k] = d[k];
6641
+ }
6642
+ capturedTraces.push({ ...event, data: rest });
6643
+ return;
6644
+ }
6645
+ }
6646
+ capturedTraces.push(event);
6103
6647
  }
6104
6648
  };
6105
6649
  this.setMaxListeners(this.getMaxListeners() + 1);
@@ -6109,6 +6653,7 @@ var AxlRuntime = class extends EventEmitter2 {
6109
6653
  return {
6110
6654
  result,
6111
6655
  cost: scope.totalCost,
6656
+ ...capturedTraces ? { traces: capturedTraces } : {},
6112
6657
  metadata: {
6113
6658
  models: [...modelCalls.keys()],
6114
6659
  modelCallCounts: modelCalls.size > 0 ? Object.fromEntries(modelCalls) : void 0,
@@ -6118,6 +6663,16 @@ var AxlRuntime = class extends EventEmitter2 {
6118
6663
  workflowCallCounts: workflowCalls.size > 0 ? Object.fromEntries(workflowCalls) : void 0
6119
6664
  }
6120
6665
  };
6666
+ } catch (err) {
6667
+ if (capturedTraces && typeof err === "object" && err !== null) {
6668
+ Object.defineProperty(err, "axlCapturedTraces", {
6669
+ value: capturedTraces,
6670
+ enumerable: false,
6671
+ writable: true,
6672
+ configurable: true
6673
+ });
6674
+ }
6675
+ throw err;
6121
6676
  } finally {
6122
6677
  this.off("trace", listener);
6123
6678
  this.setMaxListeners(this.getMaxListeners() - 1);
@@ -6172,11 +6727,13 @@ var AxlRuntime = class extends EventEmitter2 {
6172
6727
  const level = this.config.trace?.level ?? "steps";
6173
6728
  const workflowPrefix = event.workflow ? `workflow:${event.workflow} | ` : "";
6174
6729
  const parts = [`[axl] execution:${event.executionId}`];
6175
- const data = event.data;
6176
6730
  if (event.type === "workflow_start") {
6177
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | started`);
6731
+ parts.push(`${workflowPrefix}started`);
6178
6732
  } else if (event.type === "workflow_end") {
6179
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | completed`);
6733
+ const d = event.data;
6734
+ const status = d?.aborted ? "aborted" : d?.status ?? "completed";
6735
+ parts.push(`${workflowPrefix}${status}`);
6736
+ if (event.duration != null) parts.push(`${(event.duration / 1e3).toFixed(1)}s`);
6180
6737
  } else if (event.type === "agent_call") {
6181
6738
  parts.push(`${workflowPrefix}step:${event.step} agent_call`);
6182
6739
  if (event.agent) parts.push(`agent:${event.agent}`);
@@ -6412,6 +6969,16 @@ var RedisStore = class _RedisStore {
6412
6969
  };
6413
6970
 
6414
6971
  // src/memory/embedder-openai.ts
6972
+ var EMBEDDING_PRICE_PER_1M_TOKENS = {
6973
+ "text-embedding-3-small": 0.02,
6974
+ "text-embedding-3-large": 0.13,
6975
+ "text-embedding-ada-002": 0.1
6976
+ };
6977
+ function computeCost(model, tokens) {
6978
+ const pricePerMillion = EMBEDDING_PRICE_PER_1M_TOKENS[model];
6979
+ if (pricePerMillion == null) return void 0;
6980
+ return tokens / 1e6 * pricePerMillion;
6981
+ }
6415
6982
  var OpenAIEmbedder = class {
6416
6983
  apiKey;
6417
6984
  model;
@@ -6426,8 +6993,8 @@ var OpenAIEmbedder = class {
6426
6993
  this.baseUrl = options.baseUrl ?? "https://api.openai.com";
6427
6994
  this.dimensions = options.dimensions ?? 1536;
6428
6995
  }
6429
- async embed(texts) {
6430
- const response = await fetch(`${this.baseUrl}/v1/embeddings`, {
6996
+ async embed(texts, signal) {
6997
+ const response = await fetchWithRetry(`${this.baseUrl}/v1/embeddings`, {
6431
6998
  method: "POST",
6432
6999
  headers: {
6433
7000
  "Content-Type": "application/json",
@@ -6437,14 +7004,28 @@ var OpenAIEmbedder = class {
6437
7004
  model: this.model,
6438
7005
  input: texts,
6439
7006
  dimensions: this.dimensions
6440
- })
7007
+ }),
7008
+ signal
6441
7009
  });
6442
7010
  if (!response.ok) {
6443
7011
  const body = await response.text();
6444
7012
  throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
6445
7013
  }
6446
7014
  const json = await response.json();
6447
- return json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
7015
+ const vectors = json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
7016
+ const tokens = json.usage?.prompt_tokens ?? json.usage?.total_tokens;
7017
+ const cost = tokens != null ? computeCost(this.model, tokens) : void 0;
7018
+ if (tokens == null && cost == null) {
7019
+ return { vectors };
7020
+ }
7021
+ return {
7022
+ vectors,
7023
+ usage: {
7024
+ ...tokens != null ? { tokens } : {},
7025
+ ...cost != null ? { cost } : {},
7026
+ model: this.model
7027
+ }
7028
+ };
6448
7029
  }
6449
7030
  };
6450
7031