@axlsdk/axl 0.13.8 → 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
 
@@ -4082,6 +4534,9 @@ var MemoryStore = class {
4082
4534
  const result = limit ? sorted.slice(0, limit) : sorted;
4083
4535
  return result.map((e) => structuredClone(e));
4084
4536
  }
4537
+ async deleteEvalResult(id) {
4538
+ return this.evalHistory.delete(id);
4539
+ }
4085
4540
  // ── Sessions (Studio introspection) ─────────────────────────────────
4086
4541
  async listSessions() {
4087
4542
  return [...this.sessions.keys()];
@@ -4390,6 +4845,10 @@ var SQLiteStore = class {
4390
4845
  data: safeJsonParse(r.data)
4391
4846
  }));
4392
4847
  }
4848
+ async deleteEvalResult(id) {
4849
+ const result = this.db.prepare("DELETE FROM eval_history WHERE id = ?").run(id);
4850
+ return result.changes > 0;
4851
+ }
4393
4852
  // ── Sessions (Studio introspection) ────────────────────────────────────
4394
4853
  async listSessions() {
4395
4854
  const rows = this.db.prepare("SELECT session_id FROM sessions").all();
@@ -5122,6 +5581,18 @@ var McpManager = class {
5122
5581
  };
5123
5582
 
5124
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
+ }
5125
5596
  var MemoryManager = class {
5126
5597
  vectorStore;
5127
5598
  embedder;
@@ -5136,8 +5607,11 @@ var MemoryManager = class {
5136
5607
  * Store a key-value pair in memory.
5137
5608
  * If a vector store and embedder are configured and embed is true,
5138
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.
5139
5613
  */
5140
- async remember(key, value, stateStore, sessionId, options) {
5614
+ async remember(key, value, stateStore, sessionId, options, signal) {
5141
5615
  const scope = options?.scope ?? "session";
5142
5616
  if (scope === "session" && !sessionId) {
5143
5617
  throw new Error(
@@ -5156,23 +5630,44 @@ var MemoryManager = class {
5156
5630
  }
5157
5631
  if (options?.embed === true && this.vectorStore && this.embedder) {
5158
5632
  const text = typeof value === "string" ? value : JSON.stringify(value);
5159
- const [embedding] = await this.embedder.embed([text]);
5160
- await this.vectorStore.upsert([
5161
- {
5162
- id: `${storeScope}:${key}`,
5163
- content: text,
5164
- embedding,
5165
- 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
+ });
5166
5653
  }
5167
- ]);
5654
+ throw err;
5655
+ }
5656
+ return usage ? { usage } : {};
5168
5657
  }
5658
+ return {};
5169
5659
  }
5170
5660
  /**
5171
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).
5172
5665
  */
5173
- async recall(key, stateStore, sessionId, options) {
5666
+ async recall(key, stateStore, sessionId, options, signal) {
5174
5667
  if (options?.query && this.vectorStore && this.embedder) {
5175
- 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;
5176
5671
  const topK = options?.topK ?? 5;
5177
5672
  const searchScope = options?.scope ?? "session";
5178
5673
  if (searchScope === "session" && !sessionId) {
@@ -5180,13 +5675,14 @@ var MemoryManager = class {
5180
5675
  'sessionId is required for session-scoped memory. Use { scope: "global" } or provide a sessionId in metadata.'
5181
5676
  );
5182
5677
  }
5183
- const rawResults = await this.vectorStore.search(embedding, topK * 3);
5678
+ const rawResults = await this.vectorStore.search(vectors[0], topK * 3);
5184
5679
  const filtered = rawResults.filter((r) => {
5185
5680
  if (r.id.startsWith("global:")) return true;
5186
5681
  if (searchScope === "session") return r.id.startsWith(`session:${sessionId}:`);
5187
5682
  return true;
5188
5683
  });
5189
- return filtered.slice(0, topK);
5684
+ const data = filtered.slice(0, topK);
5685
+ return usage ? { data, usage } : { data };
5190
5686
  }
5191
5687
  const scope = options?.scope ?? "session";
5192
5688
  if (scope === "session" && !sessionId) {
@@ -5196,10 +5692,10 @@ var MemoryManager = class {
5196
5692
  }
5197
5693
  const storeScope = scope === "session" ? `session:${sessionId}` : "global";
5198
5694
  if (stateStore.getMemory) {
5199
- return stateStore.getMemory(storeScope, key);
5695
+ return { data: await stateStore.getMemory(storeScope, key) };
5200
5696
  }
5201
5697
  const storeKey = `memory:${storeScope}:${key}`;
5202
- return stateStore.getSessionMeta(storeKey, "value");
5698
+ return { data: await stateStore.getSessionMeta(storeKey, "value") };
5203
5699
  }
5204
5700
  /** Delete a memory entry. If a vector store is configured, also removes the embedding. */
5205
5701
  async forget(key, stateStore, sessionId, options) {
@@ -5299,6 +5795,22 @@ var AxlRuntime = class extends EventEmitter2 {
5299
5795
  });
5300
5796
  }
5301
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
+ }
5302
5814
  /**
5303
5815
  * Initialize MCP servers configured in the config.
5304
5816
  * Call this before executing workflows that use MCP tools.
@@ -5448,10 +5960,19 @@ var AxlRuntime = class extends EventEmitter2 {
5448
5960
  metadata: result2.metadata ?? metadata
5449
5961
  };
5450
5962
  };
5451
- 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
+ });
5452
5968
  } else {
5453
5969
  result = await this.eval(
5454
- entry.config
5970
+ entry.config,
5971
+ {
5972
+ onProgress: options?.onProgress,
5973
+ signal: options?.signal,
5974
+ captureTraces: options?.captureTraces
5975
+ }
5455
5976
  );
5456
5977
  }
5457
5978
  const resultObj = result;
@@ -5589,6 +6110,7 @@ var AxlRuntime = class extends EventEmitter2 {
5589
6110
  }
5590
6111
  },
5591
6112
  pendingDecisions: this.pendingDecisionResolvers,
6113
+ awaitHumanHandler: options?.awaitHumanHandler,
5592
6114
  stateStore: this.stateStore,
5593
6115
  workflowName: name,
5594
6116
  mcpManager: this.mcpManager,
@@ -5602,7 +6124,6 @@ var AxlRuntime = class extends EventEmitter2 {
5602
6124
  policy: "finish_and_stop"
5603
6125
  }
5604
6126
  });
5605
- ctx.log("workflow_start", { workflow: name, executionId });
5606
6127
  return this.spanManager.withSpanAsync(
5607
6128
  "axl.workflow.execute",
5608
6129
  {
@@ -5612,17 +6133,17 @@ var AxlRuntime = class extends EventEmitter2 {
5612
6133
  },
5613
6134
  async (span) => {
5614
6135
  try {
6136
+ ctx._emitWorkflowStart(validated);
5615
6137
  const result = await workflow2.handler(ctx);
5616
6138
  const output = workflow2.outputSchema ? workflow2.outputSchema.parse(result) : result;
5617
6139
  execInfo.status = "completed";
5618
6140
  execInfo.completedAt = Date.now();
5619
6141
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5620
6142
  execInfo.result = output;
5621
- ctx.log("workflow_end", {
5622
- workflow: name,
6143
+ ctx._emitWorkflowEnd({
5623
6144
  status: "completed",
5624
6145
  duration: execInfo.duration,
5625
- cost: execInfo.totalCost
6146
+ result: output
5626
6147
  });
5627
6148
  if (this.stateStore.deleteCheckpoints) {
5628
6149
  await this.stateStore.deleteCheckpoints(executionId);
@@ -5632,11 +6153,17 @@ var AxlRuntime = class extends EventEmitter2 {
5632
6153
  this.persistExecution(execInfo);
5633
6154
  return output;
5634
6155
  } catch (err) {
6156
+ const aborted = typeof err === "object" && err !== null && err.name === "AbortError";
5635
6157
  execInfo.status = "failed";
5636
6158
  execInfo.completedAt = Date.now();
5637
6159
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5638
6160
  execInfo.error = err instanceof Error ? err.message : String(err);
5639
- 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
+ });
5640
6167
  this.persistExecution(execInfo);
5641
6168
  throw err;
5642
6169
  } finally {
@@ -5701,24 +6228,16 @@ var AxlRuntime = class extends EventEmitter2 {
5701
6228
  duration: data?.duration ?? void 0
5702
6229
  });
5703
6230
  }
5704
- } else if (event.type === "tool_denied") {
6231
+ } else if (event.type === "tool_approval") {
5705
6232
  const data = event.data;
5706
- if (data?.denied === false) {
5707
- axlStream._push({
5708
- type: "tool_approval",
5709
- name: event.tool ?? "",
5710
- args: data?.args,
5711
- approved: true
5712
- });
5713
- } else {
5714
- axlStream._push({
5715
- type: "tool_approval",
5716
- name: event.tool ?? "",
5717
- args: data?.args,
5718
- approved: false,
5719
- reason: data?.reason ?? void 0
5720
- });
5721
- }
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") {
5722
6241
  } else if (event.type === "agent_call") {
5723
6242
  axlStream._push({
5724
6243
  type: "agent_end",
@@ -5751,6 +6270,7 @@ var AxlRuntime = class extends EventEmitter2 {
5751
6270
  axlStream._push({ type: "agent_start", agent: info.agent, model: info.model });
5752
6271
  },
5753
6272
  pendingDecisions: this.pendingDecisionResolvers,
6273
+ awaitHumanHandler: options?.awaitHumanHandler,
5754
6274
  stateStore: this.stateStore,
5755
6275
  workflowName: name,
5756
6276
  mcpManager: this.mcpManager,
@@ -5774,17 +6294,17 @@ var AxlRuntime = class extends EventEmitter2 {
5774
6294
  },
5775
6295
  async (span) => {
5776
6296
  try {
6297
+ wfCtx._emitWorkflowStart(validated);
5777
6298
  const rawResult = await workflow2.handler(wfCtx);
5778
6299
  const result = workflow2.outputSchema ? workflow2.outputSchema.parse(rawResult) : rawResult;
5779
6300
  execInfo.status = "completed";
5780
6301
  execInfo.completedAt = Date.now();
5781
6302
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5782
6303
  execInfo.result = result;
5783
- wfCtx.log("workflow_end", {
5784
- workflow: name,
6304
+ wfCtx._emitWorkflowEnd({
5785
6305
  status: "completed",
5786
6306
  duration: execInfo.duration,
5787
- cost: execInfo.totalCost
6307
+ result
5788
6308
  });
5789
6309
  if (this.stateStore.deleteCheckpoints) {
5790
6310
  await this.stateStore.deleteCheckpoints(executionId);
@@ -5801,14 +6321,16 @@ var AxlRuntime = class extends EventEmitter2 {
5801
6321
  };
5802
6322
  run().then((result) => axlStream._done(result)).catch((err) => {
5803
6323
  if (execInfo) {
6324
+ const aborted = typeof err === "object" && err !== null && err.name === "AbortError";
5804
6325
  execInfo.status = "failed";
5805
6326
  execInfo.completedAt = Date.now();
5806
6327
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5807
6328
  execInfo.error = err instanceof Error ? err.message : String(err);
5808
- ctx?.log("workflow_end", {
5809
- workflow: name,
6329
+ ctx?._emitWorkflowEnd({
5810
6330
  status: "failed",
5811
- error: execInfo.error
6331
+ duration: execInfo.duration,
6332
+ error: execInfo.error,
6333
+ ...aborted ? { aborted: true } : {}
5812
6334
  });
5813
6335
  this.persistExecution(execInfo);
5814
6336
  }
@@ -5873,6 +6395,25 @@ var AxlRuntime = class extends EventEmitter2 {
5873
6395
  if (this.stateStore.saveEvalResult) {
5874
6396
  await this.stateStore.saveEvalResult(entry);
5875
6397
  }
6398
+ this.emit("eval_result", entry);
6399
+ }
6400
+ /**
6401
+ * Delete an eval history entry by id. Removes from in-memory cache and
6402
+ * the configured StateStore. Returns true if an entry was actually removed.
6403
+ *
6404
+ * Ensures lazy-loaded history is loaded first so the in-memory cache and
6405
+ * the store can't drift apart on the deletion path.
6406
+ */
6407
+ async deleteEvalResult(id) {
6408
+ await this.getEvalHistory();
6409
+ const beforeLength = this.evalHistory.length;
6410
+ this.evalHistory = this.evalHistory.filter((e) => e.id !== id);
6411
+ const removedFromMemory = this.evalHistory.length < beforeLength;
6412
+ let removedFromStore = false;
6413
+ if (this.stateStore.deleteEvalResult) {
6414
+ removedFromStore = await this.stateStore.deleteEvalResult(id);
6415
+ }
6416
+ return removedFromMemory || removedFromStore;
5876
6417
  }
5877
6418
  /** Get eval result history (most recent first). */
5878
6419
  async getEvalHistory() {
@@ -5986,7 +6527,7 @@ var AxlRuntime = class extends EventEmitter2 {
5986
6527
  *
5987
6528
  * @see Spec Section 13.5
5988
6529
  */
5989
- async eval(config) {
6530
+ async eval(config, options) {
5990
6531
  let runEvalFn;
5991
6532
  try {
5992
6533
  ({ runEval: runEvalFn } = await import("@axlsdk/eval"));
@@ -6001,7 +6542,7 @@ var AxlRuntime = class extends EventEmitter2 {
6001
6542
  });
6002
6543
  return { output: result, cost, metadata };
6003
6544
  };
6004
- return runEvalFn(config, executeWorkflow, this);
6545
+ return runEvalFn(config, executeWorkflow, this, options);
6005
6546
  }
6006
6547
  /**
6007
6548
  * Compare two evaluation results to detect regressions and improvements.
@@ -6039,9 +6580,31 @@ var AxlRuntime = class extends EventEmitter2 {
6039
6580
  * Returns cost (same as `trackCost`) plus metadata extracted from trace events:
6040
6581
  * models (unique URIs), tokens (input/output/reasoning sums), and agent call count.
6041
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
+ *
6042
6605
  * Works with both `createContext()` and `execute()` calls inside `fn`.
6043
6606
  */
6044
- async trackExecution(fn) {
6607
+ async trackExecution(fn, options) {
6045
6608
  const parentScope = costScopeStorage.getStore();
6046
6609
  const scope = {
6047
6610
  totalCost: 0,
@@ -6049,8 +6612,10 @@ var AxlRuntime = class extends EventEmitter2 {
6049
6612
  parent: parentScope
6050
6613
  };
6051
6614
  const modelCalls = /* @__PURE__ */ new Map();
6615
+ const workflowCalls = /* @__PURE__ */ new Map();
6052
6616
  const tokens = { input: 0, output: 0, reasoning: 0 };
6053
6617
  let agentCalls = 0;
6618
+ const capturedTraces = options?.captureTraces ? [] : void 0;
6054
6619
  const listener = (event) => {
6055
6620
  if (!scope.trackedIds.has(event.executionId)) return;
6056
6621
  if (event.cost) scope.totalCost += event.cost;
@@ -6063,6 +6628,23 @@ var AxlRuntime = class extends EventEmitter2 {
6063
6628
  tokens.reasoning += event.tokens.reasoning ?? 0;
6064
6629
  }
6065
6630
  }
6631
+ if (event.type === "workflow_start" && event.workflow) {
6632
+ workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
6633
+ }
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);
6647
+ }
6066
6648
  };
6067
6649
  this.setMaxListeners(this.getMaxListeners() + 1);
6068
6650
  this.on("trace", listener);
@@ -6071,13 +6653,26 @@ var AxlRuntime = class extends EventEmitter2 {
6071
6653
  return {
6072
6654
  result,
6073
6655
  cost: scope.totalCost,
6656
+ ...capturedTraces ? { traces: capturedTraces } : {},
6074
6657
  metadata: {
6075
6658
  models: [...modelCalls.keys()],
6076
6659
  modelCallCounts: modelCalls.size > 0 ? Object.fromEntries(modelCalls) : void 0,
6077
6660
  tokens,
6078
- agentCalls
6661
+ agentCalls,
6662
+ workflows: [...workflowCalls.keys()],
6663
+ workflowCallCounts: workflowCalls.size > 0 ? Object.fromEntries(workflowCalls) : void 0
6079
6664
  }
6080
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;
6081
6676
  } finally {
6082
6677
  this.off("trace", listener);
6083
6678
  this.setMaxListeners(this.getMaxListeners() - 1);
@@ -6132,11 +6727,13 @@ var AxlRuntime = class extends EventEmitter2 {
6132
6727
  const level = this.config.trace?.level ?? "steps";
6133
6728
  const workflowPrefix = event.workflow ? `workflow:${event.workflow} | ` : "";
6134
6729
  const parts = [`[axl] execution:${event.executionId}`];
6135
- const data = event.data;
6136
6730
  if (event.type === "workflow_start") {
6137
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | started`);
6731
+ parts.push(`${workflowPrefix}started`);
6138
6732
  } else if (event.type === "workflow_end") {
6139
- 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`);
6140
6737
  } else if (event.type === "agent_call") {
6141
6738
  parts.push(`${workflowPrefix}step:${event.step} agent_call`);
6142
6739
  if (event.agent) parts.push(`agent:${event.agent}`);
@@ -6353,6 +6950,11 @@ var RedisStore = class _RedisStore {
6353
6950
  entries.sort((a, b) => b.timestamp - a.timestamp);
6354
6951
  return limit ? entries.slice(0, limit) : entries;
6355
6952
  }
6953
+ async deleteEvalResult(id) {
6954
+ await this.client.sRem(this.evalHistorySetKey(), id);
6955
+ const deleted = await this.client.del(this.evalHistoryKey(id));
6956
+ return deleted > 0;
6957
+ }
6356
6958
  // ── Sessions (Studio introspection) ────────────────────────────────────
6357
6959
  async listSessions() {
6358
6960
  return this.client.sMembers("axl:session-ids");
@@ -6367,6 +6969,16 @@ var RedisStore = class _RedisStore {
6367
6969
  };
6368
6970
 
6369
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
+ }
6370
6982
  var OpenAIEmbedder = class {
6371
6983
  apiKey;
6372
6984
  model;
@@ -6381,8 +6993,8 @@ var OpenAIEmbedder = class {
6381
6993
  this.baseUrl = options.baseUrl ?? "https://api.openai.com";
6382
6994
  this.dimensions = options.dimensions ?? 1536;
6383
6995
  }
6384
- async embed(texts) {
6385
- const response = await fetch(`${this.baseUrl}/v1/embeddings`, {
6996
+ async embed(texts, signal) {
6997
+ const response = await fetchWithRetry(`${this.baseUrl}/v1/embeddings`, {
6386
6998
  method: "POST",
6387
6999
  headers: {
6388
7000
  "Content-Type": "application/json",
@@ -6392,14 +7004,28 @@ var OpenAIEmbedder = class {
6392
7004
  model: this.model,
6393
7005
  input: texts,
6394
7006
  dimensions: this.dimensions
6395
- })
7007
+ }),
7008
+ signal
6396
7009
  });
6397
7010
  if (!response.ok) {
6398
7011
  const body = await response.text();
6399
7012
  throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
6400
7013
  }
6401
7014
  const json = await response.json();
6402
- 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
+ };
6403
7029
  }
6404
7030
  };
6405
7031