@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.cjs CHANGED
@@ -2112,6 +2112,21 @@ var TimeoutError = class extends AxlError {
2112
2112
  this.name = "TimeoutError";
2113
2113
  }
2114
2114
  };
2115
+ function formatBudgetCost(cost) {
2116
+ if (Number.isNaN(cost)) return "$NaN";
2117
+ if (cost === Infinity) return "$Infinity";
2118
+ if (cost === -Infinity) return "-$Infinity";
2119
+ if (cost === 0) return "$0.00";
2120
+ const sign = cost < 0 ? "-" : "";
2121
+ const abs = Math.abs(cost);
2122
+ if (abs < 1e-6) return `${sign}< $0.000001`;
2123
+ if (abs < 1e-4) {
2124
+ const [mantissa, exponent] = abs.toExponential(2).split("e");
2125
+ return `${sign}$${mantissa}e${parseInt(exponent, 10)}`;
2126
+ }
2127
+ if (abs < 0.01) return `${sign}$${abs.toFixed(6)}`;
2128
+ return `${sign}$${abs.toFixed(2)}`;
2129
+ }
2115
2130
  var BudgetExceededError = class extends AxlError {
2116
2131
  limit;
2117
2132
  spent;
@@ -2119,7 +2134,9 @@ var BudgetExceededError = class extends AxlError {
2119
2134
  constructor(limit, spent, policy) {
2120
2135
  super(
2121
2136
  "BUDGET_EXCEEDED",
2122
- `Budget exceeded: spent $${spent.toFixed(4)} of $${limit.toFixed(4)} limit (policy: ${policy})`
2137
+ `Budget exceeded: spent ${formatBudgetCost(spent)} of ${formatBudgetCost(
2138
+ limit
2139
+ )} limit (policy: ${policy})`
2123
2140
  );
2124
2141
  this.name = "BudgetExceededError";
2125
2142
  this.limit = limit;
@@ -2307,6 +2324,14 @@ function extractBalanced(str, start, openChar, closeChar) {
2307
2324
  }
2308
2325
  return null;
2309
2326
  }
2327
+ function appendRetryMessages(messages, content, feedbackMessage, providerMetadata) {
2328
+ messages.push({
2329
+ role: "assistant",
2330
+ content,
2331
+ ...providerMetadata ? { providerMetadata } : {}
2332
+ });
2333
+ messages.push({ role: "system", content: feedbackMessage });
2334
+ }
2310
2335
  function estimateMessagesTokens(messages) {
2311
2336
  let total = 0;
2312
2337
  for (const msg of messages) {
@@ -2354,6 +2379,7 @@ var WorkflowContext = class _WorkflowContext {
2354
2379
  awaitHumanHandler;
2355
2380
  onAgentStart;
2356
2381
  onAgentCallComplete;
2382
+ parentToolCallId;
2357
2383
  constructor(init) {
2358
2384
  this.input = init.input;
2359
2385
  this.executionId = init.executionId;
@@ -2377,6 +2403,7 @@ var WorkflowContext = class _WorkflowContext {
2377
2403
  this.awaitHumanHandler = init.awaitHumanHandler;
2378
2404
  this.onAgentStart = init.onAgentStart;
2379
2405
  this.onAgentCallComplete = init.onAgentCallComplete;
2406
+ this.parentToolCallId = init.parentToolCallId;
2380
2407
  if (init.metadata?.summaryCache) {
2381
2408
  this.summaryCache = init.metadata.summaryCache;
2382
2409
  }
@@ -2387,8 +2414,13 @@ var WorkflowContext = class _WorkflowContext {
2387
2414
  * state store, span manager, memory manager, MCP manager, config,
2388
2415
  * awaitHuman handler, pending decisions, tool overrides.
2389
2416
  * Isolates: session history, step counter, streaming callbacks (onToken, onAgentStart, onToolCall).
2417
+ *
2418
+ * @param parentToolCallId - The `callId` of the outer `tool_call` that
2419
+ * spawned this child. When set, every trace event emitted by the child
2420
+ * will carry this id as `parentToolCallId`, so consumers can join nested
2421
+ * agent calls back to the outer tool call that invoked them.
2390
2422
  */
2391
- createChildContext() {
2423
+ createChildContext(parentToolCallId) {
2392
2424
  return new _WorkflowContext({
2393
2425
  input: this.input,
2394
2426
  executionId: this.executionId,
@@ -2407,7 +2439,11 @@ var WorkflowContext = class _WorkflowContext {
2407
2439
  pendingDecisions: this.pendingDecisions,
2408
2440
  toolOverrides: this.toolOverrides,
2409
2441
  signal: this.signal,
2410
- workflowName: this.workflowName
2442
+ workflowName: this.workflowName,
2443
+ // Join key for nested event correlation. Inherit the parent's
2444
+ // `parentToolCallId` when this child is itself nested inside another
2445
+ // child — so grand-children still point to the outermost tool call.
2446
+ parentToolCallId: parentToolCallId ?? this.parentToolCallId
2411
2447
  // Isolated: sessionHistory (empty), stepCounter (0),
2412
2448
  // onToken (null), onAgentStart (null), onToolCall (null)
2413
2449
  });
@@ -2577,11 +2613,22 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2577
2613
  this.emitTrace({
2578
2614
  type: "guardrail",
2579
2615
  agent: agent2._name,
2580
- data: { guardrailType: "input", blocked: inputResult.block, reason: inputResult.reason }
2616
+ data: {
2617
+ guardrailType: "input",
2618
+ blocked: inputResult.block,
2619
+ ...inputResult.reason ? { reason: inputResult.reason } : {},
2620
+ // Input guardrails can't retry (prompt is user-supplied), so attempt
2621
+ // and maxAttempts are always 1 — emit them for shape consistency with
2622
+ // output guardrails so consumers don't need two narrowers.
2623
+ attempt: 1,
2624
+ maxAttempts: 1
2625
+ }
2581
2626
  });
2582
2627
  this.spanManager?.addEventToActiveSpan("axl.guardrail.check", {
2583
2628
  "axl.guardrail.type": "input",
2584
2629
  "axl.guardrail.blocked": inputResult.block,
2630
+ "axl.guardrail.attempt": 1,
2631
+ "axl.guardrail.maxAttempts": 1,
2585
2632
  ...inputResult.reason ? { "axl.guardrail.reason": inputResult.reason } : {}
2586
2633
  });
2587
2634
  if (inputResult.block) {
@@ -2608,11 +2655,15 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2608
2655
  let guardrailOutputRetries = 0;
2609
2656
  let schemaRetries = 0;
2610
2657
  let validateRetries = 0;
2658
+ const maxGuardrailRetries = guardrails?.maxRetries ?? 2;
2659
+ let pendingRetryReason;
2660
+ const verboseTrace = this.config.trace?.level === "full";
2611
2661
  while (turns < maxTurns) {
2612
2662
  if (Date.now() - startTime > timeoutMs) {
2613
2663
  throw new TimeoutError("ctx.ask()", timeoutMs);
2614
2664
  }
2615
2665
  turns++;
2666
+ const turnStart = Date.now();
2616
2667
  const chatOptions = {
2617
2668
  model,
2618
2669
  temperature: options?.temperature ?? agent2._config.temperature,
@@ -2693,16 +2744,22 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2693
2744
  usageCapture.value = response.usage;
2694
2745
  }
2695
2746
  if (response.cost) {
2696
- if (this.budgetContext) {
2697
- this.budgetContext.totalCost += response.cost;
2698
- if (this.budgetContext.totalCost >= this.budgetContext.limit) {
2699
- this.budgetContext.exceeded = true;
2700
- if (this.budgetContext.policy === "hard_stop" && this.budgetContext.abortController) {
2701
- this.budgetContext.abortController.abort();
2702
- }
2703
- }
2747
+ this._accumulateBudgetCost(response.cost);
2748
+ }
2749
+ let messagesSnapshot;
2750
+ if (verboseTrace) {
2751
+ try {
2752
+ messagesSnapshot = structuredClone(currentMessages);
2753
+ } catch (err) {
2754
+ console.warn(
2755
+ "[axl] verbose trace messages snapshot failed to clone; emitting shallow copy:",
2756
+ err instanceof Error ? err.message : String(err)
2757
+ );
2758
+ messagesSnapshot = [...currentMessages];
2704
2759
  }
2705
2760
  }
2761
+ const retryReason = pendingRetryReason;
2762
+ pendingRetryReason = void 0;
2706
2763
  this.emitTrace({
2707
2764
  type: "agent_call",
2708
2765
  agent: agent2._name,
@@ -2714,8 +2771,25 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2714
2771
  output: response.usage.completion_tokens,
2715
2772
  reasoning: response.usage.reasoning_tokens
2716
2773
  } : void 0,
2717
- duration: Date.now() - startTime,
2718
- data: { prompt, response: response.content }
2774
+ duration: Date.now() - turnStart,
2775
+ data: {
2776
+ prompt,
2777
+ response: response.content,
2778
+ ...systemPrompt ? { system: systemPrompt } : {},
2779
+ ...response.thinking_content ? { thinking: response.thinking_content } : {},
2780
+ params: {
2781
+ ...chatOptions.temperature !== void 0 ? { temperature: chatOptions.temperature } : {},
2782
+ maxTokens: chatOptions.maxTokens,
2783
+ ...chatOptions.effort !== void 0 ? { effort: chatOptions.effort } : {},
2784
+ ...chatOptions.thinkingBudget !== void 0 ? { thinkingBudget: chatOptions.thinkingBudget } : {},
2785
+ ...chatOptions.includeThoughts !== void 0 ? { includeThoughts: chatOptions.includeThoughts } : {},
2786
+ ...chatOptions.toolChoice !== void 0 ? { toolChoice: chatOptions.toolChoice } : {},
2787
+ ...chatOptions.stop !== void 0 ? { stop: chatOptions.stop } : {}
2788
+ },
2789
+ turn: turns,
2790
+ ...retryReason ? { retryReason } : {},
2791
+ ...messagesSnapshot ? { messages: messagesSnapshot } : {}
2792
+ }
2719
2793
  });
2720
2794
  if (response.tool_calls && response.tool_calls.length > 0) {
2721
2795
  currentMessages.push({
@@ -2780,7 +2854,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2780
2854
  this.emitTrace({
2781
2855
  type: "handoff",
2782
2856
  agent: agent2._name,
2783
- data: { target: targetName, mode, duration }
2857
+ data: {
2858
+ target: targetName,
2859
+ mode,
2860
+ duration,
2861
+ source: agent2._name,
2862
+ ...handoffPrompt !== prompt ? { message: handoffPrompt } : {}
2863
+ }
2784
2864
  });
2785
2865
  return result;
2786
2866
  }
@@ -2790,7 +2870,13 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2790
2870
  this.emitTrace({
2791
2871
  type: "handoff",
2792
2872
  agent: agent2._name,
2793
- data: { target: targetName, mode, duration: Date.now() - handoffStart }
2873
+ data: {
2874
+ target: targetName,
2875
+ mode,
2876
+ duration: Date.now() - handoffStart,
2877
+ source: agent2._name,
2878
+ ...handoffPrompt !== prompt ? { message: handoffPrompt } : {}
2879
+ }
2794
2880
  });
2795
2881
  }
2796
2882
  continue;
@@ -2810,7 +2896,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2810
2896
  this.emitTrace({
2811
2897
  type: "handoff",
2812
2898
  agent: agent2._name,
2813
- data: { target: targetName, mode, duration }
2899
+ data: { target: targetName, mode, duration, source: agent2._name }
2814
2900
  });
2815
2901
  return result;
2816
2902
  }
@@ -2820,7 +2906,12 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2820
2906
  this.emitTrace({
2821
2907
  type: "handoff",
2822
2908
  agent: agent2._name,
2823
- data: { target: targetName, mode, duration: Date.now() - handoffStart }
2909
+ data: {
2910
+ target: targetName,
2911
+ mode,
2912
+ duration: Date.now() - handoffStart,
2913
+ source: agent2._name
2914
+ }
2824
2915
  });
2825
2916
  return onewayResult;
2826
2917
  }
@@ -2913,24 +3004,18 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2913
3004
  });
2914
3005
  if (!decision.approved) {
2915
3006
  const reason = decision.reason ?? "Denied by human";
2916
- this.emitTrace({
2917
- type: "tool_denied",
2918
- agent: agent2._name,
2919
- tool: toolName,
2920
- data: { denied: true, reason, args: toolArgs }
2921
- });
2922
3007
  currentMessages.push({
2923
3008
  role: "tool",
2924
3009
  content: JSON.stringify({ error: `Tool denied by human: ${reason}` }),
2925
3010
  tool_call_id: toolCall.id
2926
3011
  });
2927
- return false;
3012
+ return { approved: false, reason };
2928
3013
  }
2929
- return true;
3014
+ return { approved: true };
2930
3015
  };
2931
- let approved;
3016
+ let approvalOutcome;
2932
3017
  if (this.spanManager) {
2933
- approved = await this.spanManager.withSpanAsync(
3018
+ approvalOutcome = await this.spanManager.withSpanAsync(
2934
3019
  "axl.tool.approval",
2935
3020
  {
2936
3021
  "axl.tool.name": toolName,
@@ -2938,20 +3023,24 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2938
3023
  },
2939
3024
  async (span) => {
2940
3025
  const result = await approvalFn();
2941
- span.setAttribute("axl.tool.approval.approved", result);
3026
+ span.setAttribute("axl.tool.approval.approved", result.approved);
2942
3027
  return result;
2943
3028
  }
2944
3029
  );
2945
3030
  } else {
2946
- approved = await approvalFn();
3031
+ approvalOutcome = await approvalFn();
2947
3032
  }
2948
- if (!approved) continue;
2949
3033
  this.emitTrace({
2950
- type: "tool_denied",
3034
+ type: "tool_approval",
2951
3035
  agent: agent2._name,
2952
3036
  tool: toolName,
2953
- data: { denied: false, args: toolArgs }
3037
+ data: {
3038
+ approved: approvalOutcome.approved,
3039
+ args: toolArgs,
3040
+ ...approvalOutcome.reason ? { reason: approvalOutcome.reason } : {}
3041
+ }
2954
3042
  });
3043
+ if (!approvalOutcome.approved) continue;
2955
3044
  }
2956
3045
  if (tool2 && tool2.hooks?.before) {
2957
3046
  try {
@@ -2984,7 +3073,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
2984
3073
  resultContent2 = JSON.stringify(toolResult2);
2985
3074
  }
2986
3075
  } else if (tool2) {
2987
- const childCtx = this.createChildContext();
3076
+ const childCtx = this.createChildContext(toolCall.id);
2988
3077
  try {
2989
3078
  toolResult2 = await tool2._execute(toolArgs, childCtx);
2990
3079
  } catch (err) {
@@ -3044,37 +3133,43 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3044
3133
  const content = response.content;
3045
3134
  if (guardrails?.output) {
3046
3135
  const outputResult = await guardrails.output(content, { metadata: this.metadata });
3136
+ const attempt = guardrailOutputRetries + 1;
3137
+ const maxAttempts = maxGuardrailRetries + 1;
3138
+ const onBlock = guardrails.onBlock ?? "throw";
3139
+ let feedbackMessage;
3140
+ if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
3141
+ feedbackMessage = `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
3142
+ }
3047
3143
  this.emitTrace({
3048
3144
  type: "guardrail",
3049
3145
  agent: agent2._name,
3050
3146
  data: {
3051
3147
  guardrailType: "output",
3052
3148
  blocked: outputResult.block,
3053
- reason: outputResult.reason
3149
+ ...outputResult.reason ? { reason: outputResult.reason } : {},
3150
+ attempt,
3151
+ maxAttempts,
3152
+ ...feedbackMessage ? { feedbackMessage } : {}
3054
3153
  }
3055
3154
  });
3056
3155
  this.spanManager?.addEventToActiveSpan("axl.guardrail.check", {
3057
3156
  "axl.guardrail.type": "output",
3058
3157
  "axl.guardrail.blocked": outputResult.block,
3158
+ "axl.guardrail.attempt": attempt,
3159
+ "axl.guardrail.maxAttempts": maxAttempts,
3059
3160
  ...outputResult.reason ? { "axl.guardrail.reason": outputResult.reason } : {}
3060
3161
  });
3061
3162
  if (outputResult.block) {
3062
- const onBlock = guardrails.onBlock ?? "throw";
3063
- if (onBlock === "retry") {
3064
- const maxGuardrailRetries = guardrails.maxRetries ?? 2;
3065
- if (guardrailOutputRetries < maxGuardrailRetries) {
3066
- guardrailOutputRetries++;
3067
- currentMessages.push({
3068
- role: "assistant",
3069
- content,
3070
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3071
- });
3072
- currentMessages.push({
3073
- role: "system",
3074
- content: `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`
3075
- });
3076
- continue;
3077
- }
3163
+ if (feedbackMessage) {
3164
+ guardrailOutputRetries++;
3165
+ appendRetryMessages(
3166
+ currentMessages,
3167
+ content,
3168
+ feedbackMessage,
3169
+ response.providerMetadata
3170
+ );
3171
+ pendingRetryReason = "guardrail";
3172
+ continue;
3078
3173
  }
3079
3174
  if (typeof onBlock === "function") {
3080
3175
  return onBlock(outputResult.reason ?? "Output blocked by guardrail", {
@@ -3086,30 +3181,58 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3086
3181
  }
3087
3182
  let validated = void 0;
3088
3183
  if (options?.schema) {
3184
+ const maxSchemaRetries = options.retries ?? 3;
3185
+ const schemaAttempt = schemaRetries + 1;
3186
+ const schemaMaxAttempts = maxSchemaRetries + 1;
3187
+ let schemaValid = false;
3188
+ let schemaReason;
3189
+ let schemaFeedback;
3190
+ let schemaErr;
3089
3191
  try {
3090
3192
  const parsed = JSON.parse(extractJson(content));
3091
3193
  validated = options.schema.parse(parsed);
3194
+ schemaValid = true;
3092
3195
  } catch (err) {
3093
- const maxSchemaRetries = options.retries ?? 3;
3196
+ schemaErr = err;
3197
+ schemaReason = err instanceof Error ? err.message : String(err);
3094
3198
  if (schemaRetries < maxSchemaRetries) {
3199
+ schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
3200
+ }
3201
+ }
3202
+ this.emitTrace({
3203
+ type: "schema_check",
3204
+ agent: agent2._name,
3205
+ data: {
3206
+ valid: schemaValid,
3207
+ ...schemaReason ? { reason: schemaReason } : {},
3208
+ attempt: schemaAttempt,
3209
+ maxAttempts: schemaMaxAttempts,
3210
+ ...schemaFeedback ? { feedbackMessage: schemaFeedback } : {}
3211
+ }
3212
+ });
3213
+ this.spanManager?.addEventToActiveSpan("axl.schema.check", {
3214
+ "axl.schema.valid": schemaValid,
3215
+ "axl.schema.attempt": schemaAttempt,
3216
+ "axl.schema.maxAttempts": schemaMaxAttempts,
3217
+ ...schemaReason ? { "axl.schema.reason": schemaReason } : {}
3218
+ });
3219
+ if (!schemaValid) {
3220
+ if (schemaFeedback) {
3095
3221
  schemaRetries++;
3096
- const errorMsg = err instanceof Error ? err.message : String(err);
3097
- currentMessages.push({
3098
- role: "assistant",
3222
+ appendRetryMessages(
3223
+ currentMessages,
3099
3224
  content,
3100
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3101
- });
3102
- currentMessages.push({
3103
- role: "system",
3104
- content: `Your response was not valid JSON or did not match the required schema: ${errorMsg}. Please fix and try again.`
3105
- });
3225
+ schemaFeedback,
3226
+ response.providerMetadata
3227
+ );
3228
+ pendingRetryReason = "schema";
3106
3229
  continue;
3107
3230
  }
3108
- const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([
3231
+ const zodErr = schemaErr instanceof import_zod.ZodError ? schemaErr : new import_zod.ZodError([
3109
3232
  {
3110
3233
  code: "custom",
3111
3234
  path: [],
3112
- message: err instanceof Error ? err.message : String(err)
3235
+ message: schemaReason ?? "Schema parse failed"
3113
3236
  }
3114
3237
  ]);
3115
3238
  throw new VerifyError(content, zodErr, maxSchemaRetries);
@@ -3125,31 +3248,40 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3125
3248
  const reason = err instanceof Error ? err.message : String(err);
3126
3249
  validateResult = { valid: false, reason: `Validator error: ${reason}` };
3127
3250
  }
3251
+ const maxValidateRetries = options.validateRetries ?? 2;
3252
+ const validateAttempt = validateRetries + 1;
3253
+ const validateMaxAttempts = maxValidateRetries + 1;
3254
+ let validateFeedback;
3255
+ if (!validateResult.valid && validateRetries < maxValidateRetries) {
3256
+ validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
3257
+ }
3128
3258
  this.emitTrace({
3129
3259
  type: "validate",
3130
3260
  agent: agent2._name,
3131
3261
  data: {
3132
3262
  valid: validateResult.valid,
3133
- ...validateResult.reason ? { reason: validateResult.reason } : {}
3263
+ ...validateResult.reason ? { reason: validateResult.reason } : {},
3264
+ attempt: validateAttempt,
3265
+ maxAttempts: validateMaxAttempts,
3266
+ ...validateFeedback ? { feedbackMessage: validateFeedback } : {}
3134
3267
  }
3135
3268
  });
3136
3269
  this.spanManager?.addEventToActiveSpan("axl.validate.check", {
3137
3270
  "axl.validate.valid": validateResult.valid,
3271
+ "axl.validate.attempt": validateAttempt,
3272
+ "axl.validate.maxAttempts": validateMaxAttempts,
3138
3273
  ...validateResult.reason ? { "axl.validate.reason": validateResult.reason } : {}
3139
3274
  });
3140
3275
  if (!validateResult.valid) {
3141
- const maxValidateRetries = options.validateRetries ?? 2;
3142
- if (validateRetries < maxValidateRetries) {
3276
+ if (validateFeedback) {
3143
3277
  validateRetries++;
3144
- currentMessages.push({
3145
- role: "assistant",
3278
+ appendRetryMessages(
3279
+ currentMessages,
3146
3280
  content,
3147
- ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3148
- });
3149
- currentMessages.push({
3150
- role: "system",
3151
- content: `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`
3152
- });
3281
+ validateFeedback,
3282
+ response.providerMetadata
3283
+ );
3284
+ pendingRetryReason = "validate";
3153
3285
  continue;
3154
3286
  }
3155
3287
  throw new ValidationError(
@@ -3518,6 +3650,17 @@ ${summaryResponse.content}`
3518
3650
  async verify(fn, schema, options) {
3519
3651
  const maxRetries = options?.retries ?? 3;
3520
3652
  let lastRetry = void 0;
3653
+ const emitVerifyOutcome = (passed, attempts, lastError) => {
3654
+ this.emitTrace({
3655
+ type: "verify",
3656
+ agent: void 0,
3657
+ data: {
3658
+ passed,
3659
+ attempts,
3660
+ ...lastError ? { lastError } : {}
3661
+ }
3662
+ });
3663
+ };
3521
3664
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3522
3665
  let rawOutput;
3523
3666
  try {
@@ -3536,12 +3679,14 @@ ${summaryResponse.content}`
3536
3679
  const errorMsg = validateResult.reason ?? "Validation failed";
3537
3680
  lastRetry = { error: errorMsg, output: rawOutput, parsed };
3538
3681
  if (attempt === maxRetries) {
3682
+ emitVerifyOutcome(false, attempt + 1, errorMsg);
3539
3683
  if (options?.fallback !== void 0) return options.fallback;
3540
3684
  throw new ValidationError(parsed, errorMsg, maxRetries);
3541
3685
  }
3542
3686
  continue;
3543
3687
  }
3544
3688
  }
3689
+ emitVerifyOutcome(true, attempt + 1);
3545
3690
  return parsed;
3546
3691
  } catch (err) {
3547
3692
  if (err instanceof ValidationError) {
@@ -3551,6 +3696,7 @@ ${summaryResponse.content}`
3551
3696
  parsed: err.lastOutput
3552
3697
  };
3553
3698
  if (attempt === maxRetries) {
3699
+ emitVerifyOutcome(false, attempt + 1, err.reason);
3554
3700
  if (options?.fallback !== void 0) return options.fallback;
3555
3701
  throw err;
3556
3702
  }
@@ -3559,6 +3705,7 @@ ${summaryResponse.content}`
3559
3705
  if (err instanceof VerifyError) {
3560
3706
  lastRetry = { error: err.message, output: rawOutput ?? err.lastOutput };
3561
3707
  if (attempt === maxRetries) {
3708
+ emitVerifyOutcome(false, attempt + 1, err.message);
3562
3709
  if (options?.fallback !== void 0) return options.fallback;
3563
3710
  throw err;
3564
3711
  }
@@ -3567,12 +3714,14 @@ ${summaryResponse.content}`
3567
3714
  const errorMsg = err instanceof import_zod.ZodError ? err.message : err instanceof Error ? err.message : String(err);
3568
3715
  lastRetry = { error: errorMsg, output: rawOutput };
3569
3716
  if (attempt === maxRetries) {
3717
+ emitVerifyOutcome(false, attempt + 1, errorMsg);
3570
3718
  if (options?.fallback !== void 0) return options.fallback;
3571
3719
  const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([{ code: "custom", path: [], message: errorMsg }]);
3572
3720
  throw new VerifyError(rawOutput, zodErr, maxRetries);
3573
3721
  }
3574
3722
  }
3575
3723
  }
3724
+ emitVerifyOutcome(false, maxRetries + 1, lastRetry?.error ?? "Verify failed");
3576
3725
  if (options?.fallback !== void 0) return options.fallback;
3577
3726
  throw new VerifyError(
3578
3727
  lastRetry?.output,
@@ -3878,6 +4027,76 @@ ${summaryResponse.content}`
3878
4027
  }
3879
4028
  return decision;
3880
4029
  }
4030
+ /**
4031
+ * Compose an `AbortSignal` for a memory operation's underlying embedder
4032
+ * fetch. Combines the parent context signal (user cancellation,
4033
+ * `runtime.execute({ signal })`) with the budget hard_stop abort so
4034
+ * that either cause correctly aborts an in-flight embed call.
4035
+ *
4036
+ * Returns `undefined` if there's nothing to cancel on — the embedder
4037
+ * runs without a signal in that case, matching its prior behavior.
4038
+ *
4039
+ * @internal
4040
+ */
4041
+ _composeMemorySignal() {
4042
+ const budgetSignal = this.budgetContext?.abortController?.signal;
4043
+ if (this.signal && budgetSignal) return AbortSignal.any([this.signal, budgetSignal]);
4044
+ return this.signal ?? budgetSignal;
4045
+ }
4046
+ /**
4047
+ * Accumulate a cost amount into the active `budgetContext` and trip the
4048
+ * `exceeded` flag if we've crossed the limit. On `hard_stop` policy,
4049
+ * also fires the abort controller so in-flight operations cancel.
4050
+ *
4051
+ * Called from every code path that spends money: the `agent_call` loop,
4052
+ * semantic memory operations (`ctx.remember({embed:true})`, `ctx.recall({query})`),
4053
+ * and any future cost-emitting primitive. Centralizing the logic here
4054
+ * means `ctx.budget({ limit, policy })` accurately enforces the limit
4055
+ * across ALL cost sources — not just agent calls.
4056
+ *
4057
+ * @internal
4058
+ */
4059
+ _accumulateBudgetCost(amount) {
4060
+ if (!this.budgetContext) return;
4061
+ if (!Number.isFinite(amount) || amount <= 0) return;
4062
+ this.budgetContext.totalCost += amount;
4063
+ if (this.budgetContext.totalCost >= this.budgetContext.limit) {
4064
+ this.budgetContext.exceeded = true;
4065
+ if (this.budgetContext.policy === "hard_stop" && this.budgetContext.abortController) {
4066
+ this.budgetContext.abortController.abort();
4067
+ }
4068
+ }
4069
+ }
4070
+ // ── workflow lifecycle trace emission ──────────────────────────────────
4071
+ //
4072
+ // These are called by the runtime at execution boundaries. They emit
4073
+ // first-class `workflow_start` / `workflow_end` trace events instead of
4074
+ // the previous `ctx.log('workflow_start', ...)` indirection — so consumers
4075
+ // that narrow via `event.type === 'workflow_start'` actually see them.
4076
+ // Internal: the runtime is the only caller, user workflows never call these.
4077
+ /** @internal */
4078
+ _emitWorkflowStart(input) {
4079
+ this.emitTrace({
4080
+ type: "workflow_start",
4081
+ workflow: this.workflowName,
4082
+ data: { input }
4083
+ });
4084
+ }
4085
+ /** @internal */
4086
+ _emitWorkflowEnd(info) {
4087
+ this.emitTrace({
4088
+ type: "workflow_end",
4089
+ workflow: this.workflowName,
4090
+ duration: info.duration,
4091
+ data: {
4092
+ status: info.status,
4093
+ duration: info.duration,
4094
+ ...info.result !== void 0 ? { result: info.result } : {},
4095
+ ...info.error !== void 0 ? { error: info.error } : {},
4096
+ ...info.aborted ? { aborted: true } : {}
4097
+ }
4098
+ });
4099
+ }
3881
4100
  // ── ctx.log() ─────────────────────────────────────────────────────────
3882
4101
  log(event, data) {
3883
4102
  this.emitTrace({
@@ -3913,8 +4132,59 @@ ${summaryResponse.content}`
3913
4132
  if (!this.stateStore) {
3914
4133
  throw new Error("A state store is required for memory operations.");
3915
4134
  }
4135
+ if (this.budgetContext?.exceeded) {
4136
+ const { limit, totalCost: spent, policy } = this.budgetContext;
4137
+ if (policy !== "warn") {
4138
+ throw new BudgetExceededError(limit, spent, policy);
4139
+ }
4140
+ }
3916
4141
  const sessionId = this.metadata?.sessionId;
3917
- await this.memoryManager.remember(key, value, this.stateStore, sessionId, options);
4142
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
4143
+ try {
4144
+ const memorySignal = this._composeMemorySignal();
4145
+ const { usage } = await this.memoryManager.remember(
4146
+ key,
4147
+ value,
4148
+ this.stateStore,
4149
+ sessionId,
4150
+ options,
4151
+ memorySignal
4152
+ );
4153
+ if (usage?.cost != null) {
4154
+ this._accumulateBudgetCost(usage.cost);
4155
+ }
4156
+ this.emitTrace({
4157
+ type: "log",
4158
+ ...usage?.cost != null ? { cost: usage.cost } : {},
4159
+ ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
4160
+ data: {
4161
+ event: "memory_remember",
4162
+ key,
4163
+ scope,
4164
+ embed: options?.embed === true,
4165
+ ...usage ? { usage } : {}
4166
+ }
4167
+ });
4168
+ } catch (err) {
4169
+ const partialUsage = err.axlEmbedUsage;
4170
+ if (partialUsage?.cost != null) {
4171
+ this._accumulateBudgetCost(partialUsage.cost);
4172
+ }
4173
+ this.emitTrace({
4174
+ type: "log",
4175
+ ...partialUsage?.cost != null ? { cost: partialUsage.cost } : {},
4176
+ ...partialUsage?.tokens != null ? { tokens: { input: partialUsage.tokens } } : {},
4177
+ data: {
4178
+ event: "memory_remember",
4179
+ key,
4180
+ scope,
4181
+ embed: options?.embed === true,
4182
+ ...partialUsage ? { usage: partialUsage } : {},
4183
+ error: err instanceof Error ? err.message : String(err)
4184
+ }
4185
+ });
4186
+ throw err;
4187
+ }
3918
4188
  }
3919
4189
  /**
3920
4190
  * Recall a value from memory by key, or perform semantic search if query option is provided.
@@ -3928,8 +4198,64 @@ ${summaryResponse.content}`
3928
4198
  if (!this.stateStore) {
3929
4199
  throw new Error("A state store is required for memory operations.");
3930
4200
  }
4201
+ if (this.budgetContext?.exceeded) {
4202
+ const { limit, totalCost: spent, policy } = this.budgetContext;
4203
+ if (policy !== "warn") {
4204
+ throw new BudgetExceededError(limit, spent, policy);
4205
+ }
4206
+ }
3931
4207
  const sessionId = this.metadata?.sessionId;
3932
- return this.memoryManager.recall(key, this.stateStore, sessionId, options);
4208
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
4209
+ const semantic = options?.query !== void 0;
4210
+ try {
4211
+ const memorySignal = this._composeMemorySignal();
4212
+ const { data, usage } = await this.memoryManager.recall(
4213
+ key,
4214
+ this.stateStore,
4215
+ sessionId,
4216
+ options,
4217
+ memorySignal
4218
+ );
4219
+ let hit;
4220
+ let resultCount;
4221
+ if (semantic) {
4222
+ resultCount = Array.isArray(data) ? data.length : 0;
4223
+ hit = resultCount > 0;
4224
+ } else {
4225
+ hit = data !== null && data !== void 0;
4226
+ }
4227
+ if (usage?.cost != null) {
4228
+ this._accumulateBudgetCost(usage.cost);
4229
+ }
4230
+ this.emitTrace({
4231
+ type: "log",
4232
+ ...usage?.cost != null ? { cost: usage.cost } : {},
4233
+ ...usage?.tokens != null ? { tokens: { input: usage.tokens } } : {},
4234
+ data: {
4235
+ event: "memory_recall",
4236
+ key,
4237
+ scope,
4238
+ semantic,
4239
+ hit,
4240
+ ...resultCount !== void 0 ? { resultCount } : {},
4241
+ ...usage ? { usage } : {}
4242
+ }
4243
+ });
4244
+ return data;
4245
+ } catch (err) {
4246
+ this.emitTrace({
4247
+ type: "log",
4248
+ data: {
4249
+ event: "memory_recall",
4250
+ key,
4251
+ scope,
4252
+ semantic,
4253
+ hit: false,
4254
+ error: err instanceof Error ? err.message : String(err)
4255
+ }
4256
+ });
4257
+ throw err;
4258
+ }
3933
4259
  }
3934
4260
  /** Delete a memory entry by key. */
3935
4261
  async forget(key, options) {
@@ -3942,7 +4268,25 @@ ${summaryResponse.content}`
3942
4268
  throw new Error("A state store is required for memory operations.");
3943
4269
  }
3944
4270
  const sessionId = this.metadata?.sessionId;
3945
- await this.memoryManager.forget(key, this.stateStore, sessionId, options);
4271
+ const scope = options?.scope ?? (sessionId ? "session" : "global");
4272
+ try {
4273
+ await this.memoryManager.forget(key, this.stateStore, sessionId, options);
4274
+ this.emitTrace({
4275
+ type: "log",
4276
+ data: { event: "memory_forget", key, scope }
4277
+ });
4278
+ } catch (err) {
4279
+ this.emitTrace({
4280
+ type: "log",
4281
+ data: {
4282
+ event: "memory_forget",
4283
+ key,
4284
+ scope,
4285
+ error: err instanceof Error ? err.message : String(err)
4286
+ }
4287
+ });
4288
+ throw err;
4289
+ }
3946
4290
  }
3947
4291
  // ── ctx.delegate() ──────────────────────────────────────────────────
3948
4292
  /**
@@ -3970,6 +4314,15 @@ ${summaryResponse.content}`
3970
4314
  names.add(a._name);
3971
4315
  }
3972
4316
  if (agents.length === 1) {
4317
+ this.emitTrace({
4318
+ type: "delegate",
4319
+ agent: agents[0]._name,
4320
+ data: {
4321
+ candidates: [agents[0]._name],
4322
+ selected: agents[0]._name,
4323
+ reason: "single_candidate"
4324
+ }
4325
+ });
3973
4326
  return this.ask(agents[0], prompt, {
3974
4327
  schema: options?.schema,
3975
4328
  retries: options?.retries,
@@ -4010,7 +4363,8 @@ ${summaryResponse.content}`
4010
4363
  agent: "_delegate_router",
4011
4364
  data: {
4012
4365
  candidates: agents.map((a) => a._name),
4013
- routerModel: routerModelUri
4366
+ routerModel: routerModelUri,
4367
+ reason: "routed"
4014
4368
  }
4015
4369
  });
4016
4370
  return this.ask(routerAgent, prompt, {
@@ -4022,19 +4376,117 @@ ${summaryResponse.content}`
4022
4376
  });
4023
4377
  }
4024
4378
  // ── Private ───────────────────────────────────────────────────────────
4379
+ /**
4380
+ * Internal emitter input — intentionally loose so call sites don't need to
4381
+ * build a perfectly-narrowed discriminated-union member. The resulting
4382
+ * `TraceEvent` (exported type) remains strict, and TypeScript narrows it at
4383
+ * consumer sites via the `type` discriminator.
4384
+ */
4025
4385
  emitTrace(partial) {
4026
4386
  let data = partial.data;
4027
- if (this.config.trace?.redact && partial.type === "agent_call" && data) {
4028
- data = { ...data, prompt: "[redacted]", response: "[redacted]" };
4387
+ if (this.config.trace?.redact && data) {
4388
+ if (partial.type === "agent_call") {
4389
+ const d = data;
4390
+ const redacted = {
4391
+ ...d,
4392
+ prompt: "[redacted]",
4393
+ response: "[redacted]"
4394
+ };
4395
+ if (d.system !== void 0) redacted.system = "[redacted]";
4396
+ if (d.thinking !== void 0) redacted.thinking = "[redacted]";
4397
+ if (Array.isArray(d.messages)) {
4398
+ redacted.messages = [
4399
+ { role: "system", content: `[${d.messages.length} messages redacted]` }
4400
+ ];
4401
+ }
4402
+ data = redacted;
4403
+ } else if (partial.type === "guardrail" || partial.type === "schema_check" || partial.type === "validate") {
4404
+ const d = data;
4405
+ const needsRedact = d.feedbackMessage !== void 0 || d.reason !== void 0;
4406
+ if (needsRedact) {
4407
+ data = {
4408
+ ...d,
4409
+ ...d.reason !== void 0 ? { reason: "[redacted]" } : {},
4410
+ ...d.feedbackMessage !== void 0 ? { feedbackMessage: "[redacted]" } : {}
4411
+ };
4412
+ }
4413
+ } else if (partial.type === "tool_call") {
4414
+ const d = data;
4415
+ data = {
4416
+ ...d,
4417
+ args: "[redacted]",
4418
+ result: "[redacted]"
4419
+ };
4420
+ } else if (partial.type === "tool_approval") {
4421
+ const d = data;
4422
+ data = {
4423
+ ...d,
4424
+ args: "[redacted]",
4425
+ ...d.reason !== void 0 ? { reason: "[redacted]" } : {}
4426
+ };
4427
+ } else if (partial.type === "handoff") {
4428
+ const d = data;
4429
+ if (d.message !== void 0) {
4430
+ data = { ...d, message: "[redacted]" };
4431
+ }
4432
+ } else if (partial.type === "workflow_start") {
4433
+ const d = data;
4434
+ if (d.input !== void 0) {
4435
+ data = { ...d, input: "[redacted]" };
4436
+ }
4437
+ } else if (partial.type === "workflow_end") {
4438
+ const d = data;
4439
+ const redacted = { ...d };
4440
+ if (d.result !== void 0) redacted.result = "[redacted]";
4441
+ if (d.error !== void 0) redacted.error = "[redacted]";
4442
+ data = redacted;
4443
+ } else if (partial.type === "log") {
4444
+ const d = data;
4445
+ const redacted = {};
4446
+ for (const [k, v] of Object.entries(d)) {
4447
+ if (k === "event" || typeof v === "number" || typeof v === "boolean") {
4448
+ redacted[k] = v;
4449
+ } else if (typeof v === "string") {
4450
+ redacted[k] = "[redacted]";
4451
+ } else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
4452
+ const inner = v;
4453
+ const innerRedacted = {};
4454
+ for (const [ik, iv] of Object.entries(inner)) {
4455
+ if (typeof iv === "number" || typeof iv === "boolean") {
4456
+ innerRedacted[ik] = iv;
4457
+ } else if (typeof iv === "string") {
4458
+ innerRedacted[ik] = "[redacted]";
4459
+ } else {
4460
+ innerRedacted[ik] = "[redacted]";
4461
+ }
4462
+ }
4463
+ redacted[k] = innerRedacted;
4464
+ } else {
4465
+ redacted[k] = "[redacted]";
4466
+ }
4467
+ }
4468
+ data = redacted;
4469
+ }
4029
4470
  }
4030
4471
  const event = {
4031
4472
  executionId: this.executionId,
4032
4473
  step: this.stepCounter++,
4033
4474
  timestamp: Date.now(),
4475
+ ...this.workflowName ? { workflow: this.workflowName } : {},
4476
+ ...this.parentToolCallId ? { parentToolCallId: this.parentToolCallId } : {},
4034
4477
  ...partial,
4035
4478
  data
4036
4479
  };
4037
- this.onTrace?.(event);
4480
+ if (this.onTrace) {
4481
+ try {
4482
+ this.onTrace(event);
4483
+ } catch (err) {
4484
+ console.error(
4485
+ "[axl] onTrace handler threw; trace event dropped:",
4486
+ err instanceof Error ? err.message : String(err)
4487
+ );
4488
+ }
4489
+ }
4038
4490
  }
4039
4491
  };
4040
4492
 
@@ -4229,6 +4681,9 @@ var MemoryStore = class {
4229
4681
  const result = limit ? sorted.slice(0, limit) : sorted;
4230
4682
  return result.map((e) => structuredClone(e));
4231
4683
  }
4684
+ async deleteEvalResult(id) {
4685
+ return this.evalHistory.delete(id);
4686
+ }
4232
4687
  // ── Sessions (Studio introspection) ─────────────────────────────────
4233
4688
  async listSessions() {
4234
4689
  return [...this.sessions.keys()];
@@ -4537,6 +4992,10 @@ var SQLiteStore = class {
4537
4992
  data: safeJsonParse(r.data)
4538
4993
  }));
4539
4994
  }
4995
+ async deleteEvalResult(id) {
4996
+ const result = this.db.prepare("DELETE FROM eval_history WHERE id = ?").run(id);
4997
+ return result.changes > 0;
4998
+ }
4540
4999
  // ── Sessions (Studio introspection) ────────────────────────────────────
4541
5000
  async listSessions() {
4542
5001
  const rows = this.db.prepare("SELECT session_id FROM sessions").all();
@@ -5269,6 +5728,18 @@ var McpManager = class {
5269
5728
  };
5270
5729
 
5271
5730
  // src/memory/manager.ts
5731
+ function assertEmbedResult(result) {
5732
+ if (Array.isArray(result)) {
5733
+ throw new Error(
5734
+ "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() }`."
5735
+ );
5736
+ }
5737
+ if (!result || typeof result !== "object" || !Array.isArray(result.vectors)) {
5738
+ throw new Error(
5739
+ `Embedder.embed() must return { vectors: number[][]; usage?: EmbedUsage }. Got ${result === null ? "null" : typeof result}.`
5740
+ );
5741
+ }
5742
+ }
5272
5743
  var MemoryManager = class {
5273
5744
  vectorStore;
5274
5745
  embedder;
@@ -5283,8 +5754,11 @@ var MemoryManager = class {
5283
5754
  * Store a key-value pair in memory.
5284
5755
  * If a vector store and embedder are configured and embed is true,
5285
5756
  * also embeds the value for semantic search.
5757
+ *
5758
+ * Returns `{ usage }` when embedding happened, carrying the embedder's
5759
+ * cost/token reporting out to the caller for cost attribution.
5286
5760
  */
5287
- async remember(key, value, stateStore, sessionId, options) {
5761
+ async remember(key, value, stateStore, sessionId, options, signal) {
5288
5762
  const scope = options?.scope ?? "session";
5289
5763
  if (scope === "session" && !sessionId) {
5290
5764
  throw new Error(
@@ -5303,23 +5777,44 @@ var MemoryManager = class {
5303
5777
  }
5304
5778
  if (options?.embed === true && this.vectorStore && this.embedder) {
5305
5779
  const text = typeof value === "string" ? value : JSON.stringify(value);
5306
- const [embedding] = await this.embedder.embed([text]);
5307
- await this.vectorStore.upsert([
5308
- {
5309
- id: `${storeScope}:${key}`,
5310
- content: text,
5311
- embedding,
5312
- metadata: { key, scope, ...options?.metadata }
5780
+ const raw = await this.embedder.embed([text], signal);
5781
+ assertEmbedResult(raw);
5782
+ const { vectors, usage } = raw;
5783
+ try {
5784
+ await this.vectorStore.upsert([
5785
+ {
5786
+ id: `${storeScope}:${key}`,
5787
+ content: text,
5788
+ embedding: vectors[0],
5789
+ metadata: { key, scope, ...options?.metadata }
5790
+ }
5791
+ ]);
5792
+ } catch (err) {
5793
+ if (usage && typeof err === "object" && err !== null) {
5794
+ Object.defineProperty(err, "axlEmbedUsage", {
5795
+ value: usage,
5796
+ enumerable: false,
5797
+ writable: true,
5798
+ configurable: true
5799
+ });
5313
5800
  }
5314
- ]);
5801
+ throw err;
5802
+ }
5803
+ return usage ? { usage } : {};
5315
5804
  }
5805
+ return {};
5316
5806
  }
5317
5807
  /**
5318
5808
  * Recall a value from memory by key, or perform semantic search if query is provided.
5809
+ *
5810
+ * Always returns `{ data }`; semantic recalls additionally include
5811
+ * `usage` from the embedder (when the embedder reported it).
5319
5812
  */
5320
- async recall(key, stateStore, sessionId, options) {
5813
+ async recall(key, stateStore, sessionId, options, signal) {
5321
5814
  if (options?.query && this.vectorStore && this.embedder) {
5322
- const [embedding] = await this.embedder.embed([options.query]);
5815
+ const raw = await this.embedder.embed([options.query], signal);
5816
+ assertEmbedResult(raw);
5817
+ const { vectors, usage } = raw;
5323
5818
  const topK = options?.topK ?? 5;
5324
5819
  const searchScope = options?.scope ?? "session";
5325
5820
  if (searchScope === "session" && !sessionId) {
@@ -5327,13 +5822,14 @@ var MemoryManager = class {
5327
5822
  'sessionId is required for session-scoped memory. Use { scope: "global" } or provide a sessionId in metadata.'
5328
5823
  );
5329
5824
  }
5330
- const rawResults = await this.vectorStore.search(embedding, topK * 3);
5825
+ const rawResults = await this.vectorStore.search(vectors[0], topK * 3);
5331
5826
  const filtered = rawResults.filter((r) => {
5332
5827
  if (r.id.startsWith("global:")) return true;
5333
5828
  if (searchScope === "session") return r.id.startsWith(`session:${sessionId}:`);
5334
5829
  return true;
5335
5830
  });
5336
- return filtered.slice(0, topK);
5831
+ const data = filtered.slice(0, topK);
5832
+ return usage ? { data, usage } : { data };
5337
5833
  }
5338
5834
  const scope = options?.scope ?? "session";
5339
5835
  if (scope === "session" && !sessionId) {
@@ -5343,10 +5839,10 @@ var MemoryManager = class {
5343
5839
  }
5344
5840
  const storeScope = scope === "session" ? `session:${sessionId}` : "global";
5345
5841
  if (stateStore.getMemory) {
5346
- return stateStore.getMemory(storeScope, key);
5842
+ return { data: await stateStore.getMemory(storeScope, key) };
5347
5843
  }
5348
5844
  const storeKey = `memory:${storeScope}:${key}`;
5349
- return stateStore.getSessionMeta(storeKey, "value");
5845
+ return { data: await stateStore.getSessionMeta(storeKey, "value") };
5350
5846
  }
5351
5847
  /** Delete a memory entry. If a vector store is configured, also removes the embedding. */
5352
5848
  async forget(key, stateStore, sessionId, options) {
@@ -5447,6 +5943,22 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5447
5943
  });
5448
5944
  }
5449
5945
  }
5946
+ /**
5947
+ * Whether `config.trace.redact` is enabled on this runtime. A narrow
5948
+ * boolean getter is preferred over exposing the full config because:
5949
+ * (a) `Readonly<AxlConfig>` is shallow so the config would be mutable
5950
+ * at runtime through sub-objects, subverting any compliance guarantee;
5951
+ * (b) observability consumers like Studio only need the boolean, not
5952
+ * the whole config tree; (c) if future observability config needs to
5953
+ * expand, each new flag gets its own narrow getter.
5954
+ *
5955
+ * Studio's server-side redaction helpers (executions, memory, sessions,
5956
+ * evals, decisions, tools, playground, workflows) all route through
5957
+ * this accessor to decide whether to scrub response payloads.
5958
+ */
5959
+ isRedactEnabled() {
5960
+ return this.config.trace?.redact === true;
5961
+ }
5450
5962
  /**
5451
5963
  * Initialize MCP servers configured in the config.
5452
5964
  * Call this before executing workflows that use MCP tools.
@@ -5596,10 +6108,19 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5596
6108
  metadata: result2.metadata ?? metadata
5597
6109
  };
5598
6110
  };
5599
- result = await runEvalFn(entry.config, wrappedExecuteFn, this);
6111
+ result = await runEvalFn(entry.config, wrappedExecuteFn, this, {
6112
+ onProgress: options?.onProgress,
6113
+ signal: options?.signal,
6114
+ captureTraces: options?.captureTraces
6115
+ });
5600
6116
  } else {
5601
6117
  result = await this.eval(
5602
- entry.config
6118
+ entry.config,
6119
+ {
6120
+ onProgress: options?.onProgress,
6121
+ signal: options?.signal,
6122
+ captureTraces: options?.captureTraces
6123
+ }
5603
6124
  );
5604
6125
  }
5605
6126
  const resultObj = result;
@@ -5737,6 +6258,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5737
6258
  }
5738
6259
  },
5739
6260
  pendingDecisions: this.pendingDecisionResolvers,
6261
+ awaitHumanHandler: options?.awaitHumanHandler,
5740
6262
  stateStore: this.stateStore,
5741
6263
  workflowName: name,
5742
6264
  mcpManager: this.mcpManager,
@@ -5750,7 +6272,6 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5750
6272
  policy: "finish_and_stop"
5751
6273
  }
5752
6274
  });
5753
- ctx.log("workflow_start", { workflow: name, executionId });
5754
6275
  return this.spanManager.withSpanAsync(
5755
6276
  "axl.workflow.execute",
5756
6277
  {
@@ -5760,17 +6281,17 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5760
6281
  },
5761
6282
  async (span) => {
5762
6283
  try {
6284
+ ctx._emitWorkflowStart(validated);
5763
6285
  const result = await workflow2.handler(ctx);
5764
6286
  const output = workflow2.outputSchema ? workflow2.outputSchema.parse(result) : result;
5765
6287
  execInfo.status = "completed";
5766
6288
  execInfo.completedAt = Date.now();
5767
6289
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5768
6290
  execInfo.result = output;
5769
- ctx.log("workflow_end", {
5770
- workflow: name,
6291
+ ctx._emitWorkflowEnd({
5771
6292
  status: "completed",
5772
6293
  duration: execInfo.duration,
5773
- cost: execInfo.totalCost
6294
+ result: output
5774
6295
  });
5775
6296
  if (this.stateStore.deleteCheckpoints) {
5776
6297
  await this.stateStore.deleteCheckpoints(executionId);
@@ -5780,11 +6301,17 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5780
6301
  this.persistExecution(execInfo);
5781
6302
  return output;
5782
6303
  } catch (err) {
6304
+ const aborted = typeof err === "object" && err !== null && err.name === "AbortError";
5783
6305
  execInfo.status = "failed";
5784
6306
  execInfo.completedAt = Date.now();
5785
6307
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5786
6308
  execInfo.error = err instanceof Error ? err.message : String(err);
5787
- ctx.log("workflow_end", { workflow: name, status: "failed", error: execInfo.error });
6309
+ ctx._emitWorkflowEnd({
6310
+ status: "failed",
6311
+ duration: execInfo.duration,
6312
+ error: execInfo.error,
6313
+ ...aborted ? { aborted: true } : {}
6314
+ });
5788
6315
  this.persistExecution(execInfo);
5789
6316
  throw err;
5790
6317
  } finally {
@@ -5849,24 +6376,16 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5849
6376
  duration: data?.duration ?? void 0
5850
6377
  });
5851
6378
  }
5852
- } else if (event.type === "tool_denied") {
6379
+ } else if (event.type === "tool_approval") {
5853
6380
  const data = event.data;
5854
- if (data?.denied === false) {
5855
- axlStream._push({
5856
- type: "tool_approval",
5857
- name: event.tool ?? "",
5858
- args: data?.args,
5859
- approved: true
5860
- });
5861
- } else {
5862
- axlStream._push({
5863
- type: "tool_approval",
5864
- name: event.tool ?? "",
5865
- args: data?.args,
5866
- approved: false,
5867
- reason: data?.reason ?? void 0
5868
- });
5869
- }
6381
+ axlStream._push({
6382
+ type: "tool_approval",
6383
+ name: event.tool ?? "",
6384
+ args: data?.args,
6385
+ approved: data?.approved === true,
6386
+ ...data?.reason ? { reason: data.reason } : {}
6387
+ });
6388
+ } else if (event.type === "tool_denied") {
5870
6389
  } else if (event.type === "agent_call") {
5871
6390
  axlStream._push({
5872
6391
  type: "agent_end",
@@ -5899,6 +6418,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5899
6418
  axlStream._push({ type: "agent_start", agent: info.agent, model: info.model });
5900
6419
  },
5901
6420
  pendingDecisions: this.pendingDecisionResolvers,
6421
+ awaitHumanHandler: options?.awaitHumanHandler,
5902
6422
  stateStore: this.stateStore,
5903
6423
  workflowName: name,
5904
6424
  mcpManager: this.mcpManager,
@@ -5922,17 +6442,17 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5922
6442
  },
5923
6443
  async (span) => {
5924
6444
  try {
6445
+ wfCtx._emitWorkflowStart(validated);
5925
6446
  const rawResult = await workflow2.handler(wfCtx);
5926
6447
  const result = workflow2.outputSchema ? workflow2.outputSchema.parse(rawResult) : rawResult;
5927
6448
  execInfo.status = "completed";
5928
6449
  execInfo.completedAt = Date.now();
5929
6450
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5930
6451
  execInfo.result = result;
5931
- wfCtx.log("workflow_end", {
5932
- workflow: name,
6452
+ wfCtx._emitWorkflowEnd({
5933
6453
  status: "completed",
5934
6454
  duration: execInfo.duration,
5935
- cost: execInfo.totalCost
6455
+ result
5936
6456
  });
5937
6457
  if (this.stateStore.deleteCheckpoints) {
5938
6458
  await this.stateStore.deleteCheckpoints(executionId);
@@ -5949,14 +6469,16 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
5949
6469
  };
5950
6470
  run().then((result) => axlStream._done(result)).catch((err) => {
5951
6471
  if (execInfo) {
6472
+ const aborted = typeof err === "object" && err !== null && err.name === "AbortError";
5952
6473
  execInfo.status = "failed";
5953
6474
  execInfo.completedAt = Date.now();
5954
6475
  execInfo.duration = execInfo.completedAt - execInfo.startedAt;
5955
6476
  execInfo.error = err instanceof Error ? err.message : String(err);
5956
- ctx?.log("workflow_end", {
5957
- workflow: name,
6477
+ ctx?._emitWorkflowEnd({
5958
6478
  status: "failed",
5959
- error: execInfo.error
6479
+ duration: execInfo.duration,
6480
+ error: execInfo.error,
6481
+ ...aborted ? { aborted: true } : {}
5960
6482
  });
5961
6483
  this.persistExecution(execInfo);
5962
6484
  }
@@ -6021,6 +6543,25 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6021
6543
  if (this.stateStore.saveEvalResult) {
6022
6544
  await this.stateStore.saveEvalResult(entry);
6023
6545
  }
6546
+ this.emit("eval_result", entry);
6547
+ }
6548
+ /**
6549
+ * Delete an eval history entry by id. Removes from in-memory cache and
6550
+ * the configured StateStore. Returns true if an entry was actually removed.
6551
+ *
6552
+ * Ensures lazy-loaded history is loaded first so the in-memory cache and
6553
+ * the store can't drift apart on the deletion path.
6554
+ */
6555
+ async deleteEvalResult(id) {
6556
+ await this.getEvalHistory();
6557
+ const beforeLength = this.evalHistory.length;
6558
+ this.evalHistory = this.evalHistory.filter((e) => e.id !== id);
6559
+ const removedFromMemory = this.evalHistory.length < beforeLength;
6560
+ let removedFromStore = false;
6561
+ if (this.stateStore.deleteEvalResult) {
6562
+ removedFromStore = await this.stateStore.deleteEvalResult(id);
6563
+ }
6564
+ return removedFromMemory || removedFromStore;
6024
6565
  }
6025
6566
  /** Get eval result history (most recent first). */
6026
6567
  async getEvalHistory() {
@@ -6134,7 +6675,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6134
6675
  *
6135
6676
  * @see Spec Section 13.5
6136
6677
  */
6137
- async eval(config) {
6678
+ async eval(config, options) {
6138
6679
  let runEvalFn;
6139
6680
  try {
6140
6681
  ({ runEval: runEvalFn } = await import("@axlsdk/eval"));
@@ -6149,7 +6690,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6149
6690
  });
6150
6691
  return { output: result, cost, metadata };
6151
6692
  };
6152
- return runEvalFn(config, executeWorkflow, this);
6693
+ return runEvalFn(config, executeWorkflow, this, options);
6153
6694
  }
6154
6695
  /**
6155
6696
  * Compare two evaluation results to detect regressions and improvements.
@@ -6187,9 +6728,31 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6187
6728
  * Returns cost (same as `trackCost`) plus metadata extracted from trace events:
6188
6729
  * models (unique URIs), tokens (input/output/reasoning sums), and agent call count.
6189
6730
  *
6731
+ * ## Cost vs tokens semantics
6732
+ *
6733
+ * - `cost` is the full aggregate across EVERY event with a top-level
6734
+ * `event.cost` set: agent calls, tool calls, semantic memory ops, etc.
6735
+ * This is the number to reconcile against your provider bill.
6736
+ *
6737
+ * - `metadata.tokens` is narrowly scoped to **agent** prompt/completion/
6738
+ * reasoning tokens. Embedder tokens from semantic `ctx.remember({embed:true})`
6739
+ * / `ctx.recall({query})` are deliberately NOT summed here — they're a
6740
+ * different category (input-only, different pricing, different model).
6741
+ * Conflating them would make "prompt tokens" misleading in the UI. If you
6742
+ * need embedder token counts, subscribe to `runtime.on('trace', ...)` and
6743
+ * read `data.usage.tokens` on `memory_remember` / `memory_recall` events.
6744
+ *
6745
+ * Pass `{ captureTraces: true }` to also collect the raw `TraceEvent[]` observed
6746
+ * during `fn()`. This is opt-in because it keeps every event in memory for the
6747
+ * duration of the call — useful for eval per-item capture, debugging, and test
6748
+ * assertions, but overhead grows with trace volume. When enabled, verbose-mode
6749
+ * `agent_call.data.messages` snapshots are omitted from captured events (still
6750
+ * broadcast via onTrace) to keep memory bounded — callers who need the full
6751
+ * verbose snapshot should subscribe to `runtime.on('trace', ...)` directly.
6752
+ *
6190
6753
  * Works with both `createContext()` and `execute()` calls inside `fn`.
6191
6754
  */
6192
- async trackExecution(fn) {
6755
+ async trackExecution(fn, options) {
6193
6756
  const parentScope = costScopeStorage.getStore();
6194
6757
  const scope = {
6195
6758
  totalCost: 0,
@@ -6197,8 +6760,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6197
6760
  parent: parentScope
6198
6761
  };
6199
6762
  const modelCalls = /* @__PURE__ */ new Map();
6763
+ const workflowCalls = /* @__PURE__ */ new Map();
6200
6764
  const tokens = { input: 0, output: 0, reasoning: 0 };
6201
6765
  let agentCalls = 0;
6766
+ const capturedTraces = options?.captureTraces ? [] : void 0;
6202
6767
  const listener = (event) => {
6203
6768
  if (!scope.trackedIds.has(event.executionId)) return;
6204
6769
  if (event.cost) scope.totalCost += event.cost;
@@ -6211,6 +6776,23 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6211
6776
  tokens.reasoning += event.tokens.reasoning ?? 0;
6212
6777
  }
6213
6778
  }
6779
+ if (event.type === "workflow_start" && event.workflow) {
6780
+ workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
6781
+ }
6782
+ if (capturedTraces) {
6783
+ if (event.type === "agent_call" && event.data) {
6784
+ const d = event.data;
6785
+ if ("messages" in d) {
6786
+ const rest = {};
6787
+ for (const k of Object.keys(d)) {
6788
+ if (k !== "messages") rest[k] = d[k];
6789
+ }
6790
+ capturedTraces.push({ ...event, data: rest });
6791
+ return;
6792
+ }
6793
+ }
6794
+ capturedTraces.push(event);
6795
+ }
6214
6796
  };
6215
6797
  this.setMaxListeners(this.getMaxListeners() + 1);
6216
6798
  this.on("trace", listener);
@@ -6219,13 +6801,26 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6219
6801
  return {
6220
6802
  result,
6221
6803
  cost: scope.totalCost,
6804
+ ...capturedTraces ? { traces: capturedTraces } : {},
6222
6805
  metadata: {
6223
6806
  models: [...modelCalls.keys()],
6224
6807
  modelCallCounts: modelCalls.size > 0 ? Object.fromEntries(modelCalls) : void 0,
6225
6808
  tokens,
6226
- agentCalls
6809
+ agentCalls,
6810
+ workflows: [...workflowCalls.keys()],
6811
+ workflowCallCounts: workflowCalls.size > 0 ? Object.fromEntries(workflowCalls) : void 0
6227
6812
  }
6228
6813
  };
6814
+ } catch (err) {
6815
+ if (capturedTraces && typeof err === "object" && err !== null) {
6816
+ Object.defineProperty(err, "axlCapturedTraces", {
6817
+ value: capturedTraces,
6818
+ enumerable: false,
6819
+ writable: true,
6820
+ configurable: true
6821
+ });
6822
+ }
6823
+ throw err;
6229
6824
  } finally {
6230
6825
  this.off("trace", listener);
6231
6826
  this.setMaxListeners(this.getMaxListeners() - 1);
@@ -6280,11 +6875,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
6280
6875
  const level = this.config.trace?.level ?? "steps";
6281
6876
  const workflowPrefix = event.workflow ? `workflow:${event.workflow} | ` : "";
6282
6877
  const parts = [`[axl] execution:${event.executionId}`];
6283
- const data = event.data;
6284
6878
  if (event.type === "workflow_start") {
6285
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | started`);
6879
+ parts.push(`${workflowPrefix}started`);
6286
6880
  } else if (event.type === "workflow_end") {
6287
- parts.push(`${workflowPrefix}workflow:${data?.workflow ?? "unknown"} | completed`);
6881
+ const d = event.data;
6882
+ const status = d?.aborted ? "aborted" : d?.status ?? "completed";
6883
+ parts.push(`${workflowPrefix}${status}`);
6884
+ if (event.duration != null) parts.push(`${(event.duration / 1e3).toFixed(1)}s`);
6288
6885
  } else if (event.type === "agent_call") {
6289
6886
  parts.push(`${workflowPrefix}step:${event.step} agent_call`);
6290
6887
  if (event.agent) parts.push(`agent:${event.agent}`);
@@ -6501,6 +7098,11 @@ var RedisStore = class _RedisStore {
6501
7098
  entries.sort((a, b) => b.timestamp - a.timestamp);
6502
7099
  return limit ? entries.slice(0, limit) : entries;
6503
7100
  }
7101
+ async deleteEvalResult(id) {
7102
+ await this.client.sRem(this.evalHistorySetKey(), id);
7103
+ const deleted = await this.client.del(this.evalHistoryKey(id));
7104
+ return deleted > 0;
7105
+ }
6504
7106
  // ── Sessions (Studio introspection) ────────────────────────────────────
6505
7107
  async listSessions() {
6506
7108
  return this.client.sMembers("axl:session-ids");
@@ -6515,6 +7117,16 @@ var RedisStore = class _RedisStore {
6515
7117
  };
6516
7118
 
6517
7119
  // src/memory/embedder-openai.ts
7120
+ var EMBEDDING_PRICE_PER_1M_TOKENS = {
7121
+ "text-embedding-3-small": 0.02,
7122
+ "text-embedding-3-large": 0.13,
7123
+ "text-embedding-ada-002": 0.1
7124
+ };
7125
+ function computeCost(model, tokens) {
7126
+ const pricePerMillion = EMBEDDING_PRICE_PER_1M_TOKENS[model];
7127
+ if (pricePerMillion == null) return void 0;
7128
+ return tokens / 1e6 * pricePerMillion;
7129
+ }
6518
7130
  var OpenAIEmbedder = class {
6519
7131
  apiKey;
6520
7132
  model;
@@ -6529,8 +7141,8 @@ var OpenAIEmbedder = class {
6529
7141
  this.baseUrl = options.baseUrl ?? "https://api.openai.com";
6530
7142
  this.dimensions = options.dimensions ?? 1536;
6531
7143
  }
6532
- async embed(texts) {
6533
- const response = await fetch(`${this.baseUrl}/v1/embeddings`, {
7144
+ async embed(texts, signal) {
7145
+ const response = await fetchWithRetry(`${this.baseUrl}/v1/embeddings`, {
6534
7146
  method: "POST",
6535
7147
  headers: {
6536
7148
  "Content-Type": "application/json",
@@ -6540,14 +7152,28 @@ var OpenAIEmbedder = class {
6540
7152
  model: this.model,
6541
7153
  input: texts,
6542
7154
  dimensions: this.dimensions
6543
- })
7155
+ }),
7156
+ signal
6544
7157
  });
6545
7158
  if (!response.ok) {
6546
7159
  const body = await response.text();
6547
7160
  throw new Error(`OpenAI embeddings API error (${response.status}): ${body}`);
6548
7161
  }
6549
7162
  const json = await response.json();
6550
- return json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
7163
+ const vectors = json.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
7164
+ const tokens = json.usage?.prompt_tokens ?? json.usage?.total_tokens;
7165
+ const cost = tokens != null ? computeCost(this.model, tokens) : void 0;
7166
+ if (tokens == null && cost == null) {
7167
+ return { vectors };
7168
+ }
7169
+ return {
7170
+ vectors,
7171
+ usage: {
7172
+ ...tokens != null ? { tokens } : {},
7173
+ ...cost != null ? { cost } : {},
7174
+ model: this.model
7175
+ }
7176
+ };
6551
7177
  }
6552
7178
  };
6553
7179