@axlsdk/axl 0.7.5 → 0.8.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/README.md CHANGED
@@ -320,7 +320,7 @@ for await (const event of sessionStream) {
320
320
  All available on `ctx` inside workflow handlers. See the [API Reference](../../docs/api-reference.md) for complete option types, valid values, and defaults.
321
321
 
322
322
  ```typescript
323
- // Invoke an agent (schema retries rebuild the call with the failed output + error in the prompt)
323
+ // Invoke an agent (schema/validate retries accumulate LLM sees all previous failed attempts)
324
324
  const answer = await ctx.ask(agent, 'prompt', { schema, retries });
325
325
 
326
326
  // Run 3 agents in parallel — each gets the same question independently
@@ -329,11 +329,11 @@ const results = await ctx.spawn(3, async (i) => ctx.ask(agent, prompts[i]));
329
329
  // Pick the answer that appeared most often — also supports LLM-as-judge via scorer
330
330
  const winner = await ctx.vote(results, { strategy: 'majority', key: 'answer' });
331
331
 
332
- // Generic retry-until-valid loop (not conversation-aware you decide how to use the error)
332
+ // Retry-until-valid loop for APIs, pipelines, or as a repair fallback for ctx.ask()
333
333
  const valid = await ctx.verify(
334
- async (lastOutput, error) => ctx.ask(agent, error ? `Fix: ${error}` : prompt),
335
- schema,
336
- { retries: 3, fallback: defaultValue },
334
+ async () => fetchRouteFromAPI(origin, destination),
335
+ RouteSchema,
336
+ { retries: 3, fallback: defaultRoute },
337
337
  );
338
338
 
339
339
  // Cost control — returns { value, budgetExceeded, totalCost }
@@ -477,7 +477,29 @@ const safe = agent({
477
477
  });
478
478
  ```
479
479
 
480
- When `onBlock` is `'retry'`, the LLM's blocked output is appended to the conversation (as an assistant message) along with a system message containing the block reason, then the LLM is re-called so it can self-correct. These messages **accumulate** across retries — if the guardrail blocks multiple times, the LLM sees all prior failed attempts and corrections before its next try. All retry messages are ephemeral — they are **not** persisted to session history, so subsequent session turns never see the blocked attempts. Note: `ctx.ask()` schema retries work differently each retry rebuilds the call from scratch and only includes the most recent failed output and error (previous failures do not accumulate). Input guardrails always throw since the prompt is user-supplied. Throws `GuardrailError` if retries are exhausted or `onBlock` is `'throw'`.
480
+ When `onBlock` is `'retry'`, the LLM's blocked output is appended to the conversation (as an assistant message) along with a system message containing the block reason, then the LLM is re-called so it can self-correct. These messages **accumulate** across retries — if the guardrail blocks multiple times, the LLM sees all prior failed attempts and corrections before its next try. All retry messages are ephemeral — they are **not** persisted to session history, so subsequent session turns never see the blocked attempts. Schema retries and validate retries use the same accumulating pattern. Input guardrails always throw since the prompt is user-supplied. Throws `GuardrailError` if retries are exhausted or `onBlock` is `'throw'`.
481
+
482
+ For **business rule validation** on the parsed typed object (not raw text), use `validate` on `ctx.ask()`:
483
+
484
+ ```typescript
485
+ const UserSchema = z.object({
486
+ name: z.string(),
487
+ email: z.string(),
488
+ role: z.enum(['admin', 'editor', 'viewer']),
489
+ });
490
+
491
+ const result = await ctx.ask(extractAgent, 'Extract user from this text', {
492
+ schema: UserSchema,
493
+ validate: (user) => {
494
+ if (user.role === 'admin' && !user.email.endsWith('@company.com')) {
495
+ return { valid: false, reason: 'Admin users must have a company email' };
496
+ }
497
+ return { valid: true };
498
+ },
499
+ });
500
+ ```
501
+
502
+ `validate` is per-call, co-located with the `schema` it validates. It runs **after** schema parsing succeeds, receiving the fully typed object. On failure, the LLM sees all previous attempts (accumulating context) and the validation reason. Requires `schema` — without it, validate is skipped (use guardrails for raw text). Throws `ValidationError` after retries are exhausted. Also supported on `ctx.delegate()`, `ctx.race()`, and `ctx.verify()`.
481
503
 
482
504
  ### State Stores
483
505
 
@@ -556,6 +578,7 @@ import {
556
578
  MaxTurnsError, // Agent exceeded max tool-calling turns
557
579
  BudgetExceededError, // Budget limit exceeded
558
580
  GuardrailError, // Guardrail blocked input or output
581
+ ValidationError, // Post-schema business rule validation failed after retries
559
582
  ToolDenied, // Agent tried to call unauthorized tool
560
583
  } from '@axlsdk/axl';
561
584
  ```
package/dist/index.cjs CHANGED
@@ -135,6 +135,7 @@ __export(index_exports, {
135
135
  SqliteVectorStore: () => SqliteVectorStore,
136
136
  TimeoutError: () => TimeoutError,
137
137
  ToolDenied: () => ToolDenied,
138
+ ValidationError: () => ValidationError,
138
139
  VerifyError: () => VerifyError,
139
140
  WorkflowContext: () => WorkflowContext,
140
141
  agent: () => agent,
@@ -310,29 +311,32 @@ async function fetchWithRetry(input, init, maxRetries = MAX_RETRIES) {
310
311
 
311
312
  // src/providers/openai.ts
312
313
  var OPENAI_PRICING = {
313
- "gpt-4o": [25e-7, 1e-5],
314
- "gpt-4o-mini": [15e-8, 6e-7],
315
- "gpt-4-turbo": [1e-5, 3e-5],
316
- "gpt-4": [3e-5, 6e-5],
317
- "gpt-3.5-turbo": [5e-7, 15e-7],
318
- "gpt-5": [125e-8, 1e-5],
319
- "gpt-5-mini": [25e-8, 2e-6],
320
- "gpt-5-nano": [5e-8, 4e-7],
321
- "gpt-5.1": [125e-8, 1e-5],
322
- "gpt-5.2": [175e-8, 14e-6],
323
- "gpt-5.3": [175e-8, 14e-6],
324
- "gpt-5.4": [25e-7, 15e-6],
325
- "gpt-5.4-pro": [3e-5, 18e-5],
326
- o1: [15e-6, 6e-5],
327
- "o1-mini": [3e-6, 12e-6],
328
- "o1-pro": [15e-5, 6e-4],
329
- o3: [1e-5, 4e-5],
330
- "o3-mini": [11e-7, 44e-7],
331
- "o3-pro": [2e-5, 8e-5],
332
- "o4-mini": [11e-7, 44e-7],
333
- "gpt-4.1": [2e-6, 8e-6],
334
- "gpt-4.1-mini": [4e-7, 16e-7],
335
- "gpt-4.1-nano": [1e-7, 4e-7]
314
+ // gpt-4o era — cache reads at 50% of input rate
315
+ "gpt-4o": [25e-7, 1e-5, 0.5],
316
+ "gpt-4o-mini": [15e-8, 6e-7, 0.5],
317
+ "gpt-4-turbo": [1e-5, 3e-5, 0.5],
318
+ "gpt-4": [3e-5, 6e-5, 0.5],
319
+ "gpt-3.5-turbo": [5e-7, 15e-7, 0.5],
320
+ o1: [15e-6, 6e-5, 0.5],
321
+ "o1-mini": [3e-6, 12e-6, 0.5],
322
+ "o1-pro": [15e-5, 6e-4, 0.5],
323
+ // gpt-4.1 / o3 / o4 era — cache reads at 25% of input rate
324
+ "gpt-4.1": [2e-6, 8e-6, 0.25],
325
+ "gpt-4.1-mini": [4e-7, 16e-7, 0.25],
326
+ "gpt-4.1-nano": [1e-7, 4e-7, 0.25],
327
+ o3: [1e-5, 4e-5, 0.25],
328
+ "o3-mini": [11e-7, 44e-7, 0.25],
329
+ "o3-pro": [2e-5, 8e-5, 0.25],
330
+ "o4-mini": [11e-7, 44e-7, 0.25],
331
+ // gpt-5 era — cache reads at 10% of input rate
332
+ "gpt-5": [125e-8, 1e-5, 0.1],
333
+ "gpt-5-mini": [25e-8, 2e-6, 0.1],
334
+ "gpt-5-nano": [5e-8, 4e-7, 0.1],
335
+ "gpt-5.1": [125e-8, 1e-5, 0.1],
336
+ "gpt-5.2": [175e-8, 14e-6, 0.1],
337
+ "gpt-5.3": [175e-8, 14e-6, 0.1],
338
+ "gpt-5.4": [25e-7, 15e-6, 0.1],
339
+ "gpt-5.4-pro": [3e-5, 18e-5, 0.1]
336
340
  };
337
341
  var PRICING_KEYS_BY_LENGTH = Object.keys(OPENAI_PRICING).sort((a, b) => b.length - a.length);
338
342
  function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens) {
@@ -346,9 +350,9 @@ function estimateOpenAICost(model, promptTokens, completionTokens, cachedTokens)
346
350
  }
347
351
  }
348
352
  if (!pricing) return 0;
349
- const [inputRate, outputRate] = pricing;
353
+ const [inputRate, outputRate, cacheMultiplier] = pricing;
350
354
  const cached = cachedTokens ?? 0;
351
- const inputCost = (promptTokens - cached) * inputRate + cached * inputRate * 0.5;
355
+ const inputCost = (promptTokens - cached) * inputRate + cached * inputRate * cacheMultiplier;
352
356
  return inputCost + completionTokens * outputRate;
353
357
  }
354
358
  function isOSeriesModel(model) {
@@ -458,7 +462,7 @@ var OpenAIProvider = class {
458
462
  if (!res.body) {
459
463
  throw new Error("OpenAI stream response has no body");
460
464
  }
461
- yield* this.parseSSEStream(res.body);
465
+ yield* this.parseSSEStream(res.body, options.model);
462
466
  }
463
467
  // ---------------------------------------------------------------------------
464
468
  // Internal helpers
@@ -532,7 +536,7 @@ var OpenAIProvider = class {
532
536
  if (msg.tool_call_id) out.tool_call_id = msg.tool_call_id;
533
537
  return out;
534
538
  }
535
- async *parseSSEStream(body) {
539
+ async *parseSSEStream(body, model) {
536
540
  const reader = body.getReader();
537
541
  const decoder = new TextDecoder();
538
542
  let buffer = "";
@@ -549,7 +553,16 @@ var OpenAIProvider = class {
549
553
  const trimmed = line.trim();
550
554
  if (!trimmed || trimmed.startsWith(":")) continue;
551
555
  if (trimmed === "data: [DONE]") {
552
- yield { type: "done", usage: usageData };
556
+ yield {
557
+ type: "done",
558
+ usage: usageData,
559
+ cost: usageData ? estimateOpenAICost(
560
+ model,
561
+ usageData.prompt_tokens,
562
+ usageData.completion_tokens,
563
+ usageData.cached_tokens
564
+ ) : void 0
565
+ };
553
566
  return;
554
567
  }
555
568
  if (trimmed.startsWith("data: ")) {
@@ -592,7 +605,16 @@ var OpenAIProvider = class {
592
605
  }
593
606
  }
594
607
  }
595
- yield { type: "done", usage: usageData };
608
+ yield {
609
+ type: "done",
610
+ usage: usageData,
611
+ cost: usageData ? estimateOpenAICost(
612
+ model,
613
+ usageData.prompt_tokens,
614
+ usageData.completion_tokens,
615
+ usageData.cached_tokens
616
+ ) : void 0
617
+ };
596
618
  } finally {
597
619
  reader.releaseLock();
598
620
  }
@@ -850,6 +872,7 @@ var OpenAIResponsesProvider = class {
850
872
  const decoder = new TextDecoder();
851
873
  let buffer = "";
852
874
  const callIdMap = /* @__PURE__ */ new Map();
875
+ let eventType = "";
853
876
  try {
854
877
  while (true) {
855
878
  const { done, value } = await reader.read();
@@ -857,7 +880,6 @@ var OpenAIResponsesProvider = class {
857
880
  buffer += decoder.decode(value, { stream: true });
858
881
  const lines = buffer.split("\n");
859
882
  buffer = lines.pop() ?? "";
860
- let eventType = "";
861
883
  for (const line of lines) {
862
884
  const trimmed = line.trim();
863
885
  if (!trimmed || trimmed.startsWith(":")) continue;
@@ -925,7 +947,17 @@ var OpenAIResponsesProvider = class {
925
947
  } : void 0;
926
948
  const reasoningItems = response?.output?.filter((item) => item.type === "reasoning") ?? [];
927
949
  const providerMetadata = reasoningItems.length > 0 ? { openaiReasoningItems: reasoningItems } : void 0;
928
- return { type: "done", usage, providerMetadata };
950
+ return {
951
+ type: "done",
952
+ usage,
953
+ cost: usage ? estimateOpenAICost(
954
+ model,
955
+ usage.prompt_tokens,
956
+ usage.completion_tokens,
957
+ usage.cached_tokens
958
+ ) : void 0,
959
+ providerMetadata
960
+ };
929
961
  }
930
962
  case "response.failed": {
931
963
  const errorMsg = data.response?.error?.message ?? data.response?.status_details?.error?.message ?? "Unknown error";
@@ -1058,7 +1090,7 @@ var AnthropicProvider = class {
1058
1090
  if (!res.body) {
1059
1091
  throw new Error("Anthropic stream response has no body");
1060
1092
  }
1061
- yield* this.parseSSEStream(res.body);
1093
+ yield* this.parseSSEStream(res.body, options.model);
1062
1094
  }
1063
1095
  // ---------------------------------------------------------------------------
1064
1096
  // Internal: request building
@@ -1301,13 +1333,14 @@ ${jsonInstruction}` : jsonInstruction;
1301
1333
  // ---------------------------------------------------------------------------
1302
1334
  // Internal: SSE stream parsing
1303
1335
  // ---------------------------------------------------------------------------
1304
- async *parseSSEStream(body) {
1336
+ async *parseSSEStream(body, model) {
1305
1337
  const reader = body.getReader();
1306
1338
  const decoder = new TextDecoder();
1307
1339
  let buffer = "";
1308
1340
  let currentToolId = "";
1309
1341
  let currentToolName = "";
1310
1342
  let usage;
1343
+ let cacheWrite = 0;
1311
1344
  try {
1312
1345
  while (true) {
1313
1346
  const { done, value } = await reader.read();
@@ -1364,7 +1397,7 @@ ${jsonInstruction}` : jsonInstruction;
1364
1397
  case "message_start": {
1365
1398
  if (event.message?.usage) {
1366
1399
  const cacheRead = event.message.usage.cache_read_input_tokens ?? 0;
1367
- const cacheWrite = event.message.usage.cache_creation_input_tokens ?? 0;
1400
+ cacheWrite = event.message.usage.cache_creation_input_tokens ?? 0;
1368
1401
  const inputTokens = (event.message.usage.input_tokens ?? 0) + cacheRead + cacheWrite;
1369
1402
  usage = {
1370
1403
  prompt_tokens: inputTokens,
@@ -1395,13 +1428,33 @@ ${jsonInstruction}` : jsonInstruction;
1395
1428
  if (usage) {
1396
1429
  usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
1397
1430
  }
1398
- yield { type: "done", usage };
1431
+ yield {
1432
+ type: "done",
1433
+ usage,
1434
+ cost: usage ? estimateAnthropicCost(
1435
+ model,
1436
+ usage.prompt_tokens,
1437
+ usage.completion_tokens,
1438
+ usage.cached_tokens,
1439
+ cacheWrite
1440
+ ) : void 0
1441
+ };
1399
1442
  return;
1400
1443
  }
1401
1444
  }
1402
1445
  }
1403
1446
  }
1404
- yield { type: "done", usage };
1447
+ yield {
1448
+ type: "done",
1449
+ usage,
1450
+ cost: usage ? estimateAnthropicCost(
1451
+ model,
1452
+ usage.prompt_tokens,
1453
+ usage.completion_tokens,
1454
+ usage.cached_tokens,
1455
+ cacheWrite
1456
+ ) : void 0
1457
+ };
1405
1458
  } finally {
1406
1459
  reader.releaseLock();
1407
1460
  }
@@ -1528,7 +1581,7 @@ var GeminiProvider = class {
1528
1581
  if (!res.body) {
1529
1582
  throw new Error("Gemini stream response has no body");
1530
1583
  }
1531
- yield* this.parseSSEStream(res.body);
1584
+ yield* this.parseSSEStream(res.body, options.model);
1532
1585
  }
1533
1586
  // ---------------------------------------------------------------------------
1534
1587
  // Internal: request building
@@ -1809,7 +1862,7 @@ var GeminiProvider = class {
1809
1862
  // ---------------------------------------------------------------------------
1810
1863
  // Internal: SSE stream parsing
1811
1864
  // ---------------------------------------------------------------------------
1812
- async *parseSSEStream(body) {
1865
+ async *parseSSEStream(body, model) {
1813
1866
  const reader = body.getReader();
1814
1867
  const decoder = new TextDecoder();
1815
1868
  let buffer = "";
@@ -1865,7 +1918,17 @@ var GeminiProvider = class {
1865
1918
  }
1866
1919
  }
1867
1920
  const providerMetadata = accumulatedParts.length > 0 ? { geminiParts: accumulatedParts } : void 0;
1868
- yield { type: "done", usage, providerMetadata };
1921
+ yield {
1922
+ type: "done",
1923
+ usage,
1924
+ cost: usage ? estimateGeminiCost(
1925
+ model,
1926
+ usage.prompt_tokens,
1927
+ usage.completion_tokens,
1928
+ usage.cached_tokens
1929
+ ) : void 0,
1930
+ providerMetadata
1931
+ };
1869
1932
  } finally {
1870
1933
  reader.releaseLock();
1871
1934
  }
@@ -2081,6 +2144,18 @@ var GuardrailError = class extends AxlError {
2081
2144
  this.reason = reason;
2082
2145
  }
2083
2146
  };
2147
+ var ValidationError = class extends AxlError {
2148
+ lastOutput;
2149
+ reason;
2150
+ retries;
2151
+ constructor(lastOutput, reason, retries) {
2152
+ super("VALIDATION_ERROR", `Validation failed after ${retries} retries: ${reason}`);
2153
+ this.name = "ValidationError";
2154
+ this.lastOutput = lastOutput;
2155
+ this.reason = reason;
2156
+ this.retries = retries;
2157
+ }
2158
+ };
2084
2159
  var ToolDenied = class extends AxlError {
2085
2160
  toolName;
2086
2161
  agentName;
@@ -2345,9 +2420,6 @@ var WorkflowContext = class _WorkflowContext {
2345
2420
  agent2,
2346
2421
  prompt,
2347
2422
  options,
2348
- 0,
2349
- void 0,
2350
- void 0,
2351
2423
  void 0,
2352
2424
  usageCapture
2353
2425
  );
@@ -2397,7 +2469,7 @@ var WorkflowContext = class _WorkflowContext {
2397
2469
  return result;
2398
2470
  });
2399
2471
  }
2400
- async executeAgentCall(agent2, prompt, options, retryCount = 0, previousOutput, previousError, handoffMessages, usageCapture) {
2472
+ async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
2401
2473
  if (this.budgetContext?.exceeded) {
2402
2474
  const { limit, totalCost: spent, policy } = this.budgetContext;
2403
2475
  if (policy === "warn") {
@@ -2469,16 +2541,6 @@ var WorkflowContext = class _WorkflowContext {
2469
2541
 
2470
2542
  Respond with valid JSON matching this schema:
2471
2543
  ${JSON.stringify(jsonSchema, null, 2)}`;
2472
- }
2473
- if (previousOutput && previousError) {
2474
- userContent += `
2475
-
2476
- Your previous response was invalid:
2477
- ${previousOutput}
2478
-
2479
- Error: ${previousError}
2480
-
2481
- Please fix and try again.`;
2482
2544
  }
2483
2545
  messages.push({ role: "user", content: userContent });
2484
2546
  if (handoffMessages && handoffMessages.length > 0) {
@@ -2523,9 +2585,17 @@ Please fix and try again.`;
2523
2585
  const maxTurns = agent2._config.maxTurns ?? 25;
2524
2586
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
2525
2587
  const startTime = Date.now();
2588
+ if (this.onToken && options?.validate) {
2589
+ throw new AxlError(
2590
+ "INVALID_CONFIG",
2591
+ "Cannot use validate with streaming. Validate requires schema (JSON output) which does not benefit from token streaming. Use a non-streaming call instead."
2592
+ );
2593
+ }
2526
2594
  const currentMessages = [...messages];
2527
2595
  let turns = 0;
2528
2596
  let guardrailOutputRetries = 0;
2597
+ let schemaRetries = 0;
2598
+ let validateRetries = 0;
2529
2599
  while (turns < maxTurns) {
2530
2600
  if (Date.now() - startTime > timeoutMs) {
2531
2601
  throw new TimeoutError("ctx.ask()", timeoutMs);
@@ -2575,7 +2645,8 @@ Please fix and try again.`;
2575
2645
  response = {
2576
2646
  content: content2,
2577
2647
  tool_calls: void 0,
2578
- usage: chunk.usage
2648
+ usage: chunk.usage,
2649
+ cost: chunk.cost
2579
2650
  };
2580
2651
  }
2581
2652
  }
@@ -2652,14 +2723,17 @@ Please fix and try again.`;
2652
2723
  }
2653
2724
  }
2654
2725
  const handoffStart = Date.now();
2655
- const handoffOptions = options ? { schema: options.schema, retries: options.retries, metadata: options.metadata } : void 0;
2726
+ const handoffOptions = options ? {
2727
+ schema: options.schema,
2728
+ retries: options.retries,
2729
+ metadata: options.metadata,
2730
+ validate: options.validate,
2731
+ validateRetries: options.validateRetries
2732
+ } : void 0;
2656
2733
  const handoffFn = () => this.executeAgentCall(
2657
2734
  descriptor.agent,
2658
2735
  handoffPrompt,
2659
2736
  handoffOptions,
2660
- 0,
2661
- void 0,
2662
- void 0,
2663
2737
  currentMessages,
2664
2738
  usageCapture
2665
2739
  );
@@ -2993,26 +3067,26 @@ Please fix and try again.`;
2993
3067
  throw new GuardrailError("output", outputResult.reason ?? "Output blocked by guardrail");
2994
3068
  }
2995
3069
  }
3070
+ let validated = void 0;
2996
3071
  if (options?.schema) {
2997
3072
  try {
2998
3073
  const parsed = JSON.parse(stripMarkdownFences(content));
2999
- const validated = options.schema.parse(parsed);
3000
- this.pushAssistantToSessionHistory(content, response.providerMetadata);
3001
- return validated;
3074
+ validated = options.schema.parse(parsed);
3002
3075
  } catch (err) {
3003
- const maxRetries = options.retries ?? 3;
3004
- if (retryCount < maxRetries) {
3076
+ const maxSchemaRetries = options.retries ?? 3;
3077
+ if (schemaRetries < maxSchemaRetries) {
3078
+ schemaRetries++;
3005
3079
  const errorMsg = err instanceof Error ? err.message : String(err);
3006
- return this.executeAgentCall(
3007
- agent2,
3008
- prompt,
3009
- options,
3010
- retryCount + 1,
3080
+ currentMessages.push({
3081
+ role: "assistant",
3011
3082
  content,
3012
- errorMsg,
3013
- void 0,
3014
- usageCapture
3015
- );
3083
+ ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3084
+ });
3085
+ currentMessages.push({
3086
+ role: "system",
3087
+ content: `Your response was not valid JSON or did not match the required schema: ${errorMsg}. Please fix and try again.`
3088
+ });
3089
+ continue;
3016
3090
  }
3017
3091
  const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([
3018
3092
  {
@@ -3021,11 +3095,55 @@ Please fix and try again.`;
3021
3095
  message: err instanceof Error ? err.message : String(err)
3022
3096
  }
3023
3097
  ]);
3024
- throw new VerifyError(content, zodErr, maxRetries);
3098
+ throw new VerifyError(content, zodErr, maxSchemaRetries);
3099
+ }
3100
+ }
3101
+ if (options?.schema && options.validate) {
3102
+ let validateResult;
3103
+ try {
3104
+ validateResult = await options.validate(validated, {
3105
+ metadata: this.metadata
3106
+ });
3107
+ } catch (err) {
3108
+ const reason = err instanceof Error ? err.message : String(err);
3109
+ validateResult = { valid: false, reason: `Validator error: ${reason}` };
3110
+ }
3111
+ this.emitTrace({
3112
+ type: "validate",
3113
+ agent: agent2._name,
3114
+ data: {
3115
+ valid: validateResult.valid,
3116
+ ...validateResult.reason ? { reason: validateResult.reason } : {}
3117
+ }
3118
+ });
3119
+ this.spanManager?.addEventToActiveSpan("axl.validate.check", {
3120
+ "axl.validate.valid": validateResult.valid,
3121
+ ...validateResult.reason ? { "axl.validate.reason": validateResult.reason } : {}
3122
+ });
3123
+ if (!validateResult.valid) {
3124
+ const maxValidateRetries = options.validateRetries ?? 2;
3125
+ if (validateRetries < maxValidateRetries) {
3126
+ validateRetries++;
3127
+ currentMessages.push({
3128
+ role: "assistant",
3129
+ content,
3130
+ ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3131
+ });
3132
+ currentMessages.push({
3133
+ role: "system",
3134
+ content: `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`
3135
+ });
3136
+ continue;
3137
+ }
3138
+ throw new ValidationError(
3139
+ validated,
3140
+ validateResult.reason ?? "Validation failed",
3141
+ maxValidateRetries
3142
+ );
3025
3143
  }
3026
3144
  }
3027
3145
  this.pushAssistantToSessionHistory(content, response.providerMetadata);
3028
- return content;
3146
+ return validated ?? content;
3029
3147
  }
3030
3148
  throw new MaxTurnsError("ctx.ask()", maxTurns);
3031
3149
  }
@@ -3382,32 +3500,57 @@ ${summaryResponse.content}`
3382
3500
  // ── ctx.verify() ──────────────────────────────────────────────────────
3383
3501
  async verify(fn, schema, options) {
3384
3502
  const maxRetries = options?.retries ?? 3;
3385
- let lastOutput = void 0;
3386
- let lastErrorMessage = void 0;
3503
+ let lastRetry = void 0;
3387
3504
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3388
- let result;
3505
+ let rawOutput;
3389
3506
  try {
3390
- result = await fn(lastOutput, lastErrorMessage);
3391
- lastOutput = result;
3392
- return schema.parse(result);
3507
+ const result = await fn(lastRetry);
3508
+ rawOutput = result;
3509
+ const parsed = schema.parse(result);
3510
+ if (options?.validate) {
3511
+ let validateResult;
3512
+ try {
3513
+ validateResult = await options.validate(parsed, { metadata: this.metadata });
3514
+ } catch (err) {
3515
+ const reason = err instanceof Error ? err.message : String(err);
3516
+ validateResult = { valid: false, reason: `Validator error: ${reason}` };
3517
+ }
3518
+ if (!validateResult.valid) {
3519
+ const errorMsg = validateResult.reason ?? "Validation failed";
3520
+ lastRetry = { error: errorMsg, output: rawOutput, parsed };
3521
+ if (attempt === maxRetries) {
3522
+ if (options?.fallback !== void 0) return options.fallback;
3523
+ throw new ValidationError(parsed, errorMsg, maxRetries);
3524
+ }
3525
+ continue;
3526
+ }
3527
+ }
3528
+ return parsed;
3393
3529
  } catch (err) {
3394
- if (err instanceof import_zod.ZodError) {
3395
- lastErrorMessage = err.message;
3396
- } else if (err instanceof Error) {
3397
- lastErrorMessage = err.message;
3398
- } else {
3399
- lastErrorMessage = String(err);
3530
+ if (err instanceof ValidationError) {
3531
+ lastRetry = {
3532
+ error: err.reason,
3533
+ output: rawOutput,
3534
+ parsed: err.lastOutput
3535
+ };
3536
+ if (attempt === maxRetries) {
3537
+ if (options?.fallback !== void 0) return options.fallback;
3538
+ throw err;
3539
+ }
3540
+ continue;
3400
3541
  }
3542
+ const errorMsg = err instanceof import_zod.ZodError ? err.message : err instanceof Error ? err.message : String(err);
3543
+ lastRetry = { error: errorMsg, output: rawOutput };
3401
3544
  if (attempt === maxRetries) {
3402
3545
  if (options?.fallback !== void 0) return options.fallback;
3403
- const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([{ code: "custom", path: [], message: lastErrorMessage }]);
3404
- throw new VerifyError(lastOutput, zodErr, maxRetries);
3546
+ const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([{ code: "custom", path: [], message: errorMsg }]);
3547
+ throw new VerifyError(rawOutput, zodErr, maxRetries);
3405
3548
  }
3406
3549
  }
3407
3550
  }
3408
3551
  if (options?.fallback !== void 0) return options.fallback;
3409
3552
  throw new VerifyError(
3410
- lastOutput,
3553
+ lastRetry?.output,
3411
3554
  new import_zod.ZodError([{ code: "custom", path: [], message: "Verify failed" }]),
3412
3555
  maxRetries
3413
3556
  );
@@ -3509,7 +3652,7 @@ ${summaryResponse.content}`
3509
3652
  let remaining = fns.length;
3510
3653
  for (const fn of fns) {
3511
3654
  const p = signalStorage.run(composedSignal, fn);
3512
- p.then((value) => {
3655
+ p.then(async (value) => {
3513
3656
  if (settled) return;
3514
3657
  if (schema) {
3515
3658
  const parsed = schema.safeParse(value);
@@ -3522,6 +3665,33 @@ ${summaryResponse.content}`
3522
3665
  }
3523
3666
  return;
3524
3667
  }
3668
+ if (options?.validate) {
3669
+ try {
3670
+ const validateResult = await options.validate(parsed.data, {
3671
+ metadata: this.metadata
3672
+ });
3673
+ if (!validateResult.valid) {
3674
+ remaining--;
3675
+ lastError = new Error(
3676
+ `Validation failed: ${validateResult.reason ?? "Validation failed"}`
3677
+ );
3678
+ if (remaining === 0 && !settled) {
3679
+ settled = true;
3680
+ reject(lastError);
3681
+ }
3682
+ return;
3683
+ }
3684
+ } catch (err) {
3685
+ remaining--;
3686
+ lastError = err instanceof Error ? err : new Error(`Validator error: ${String(err)}`);
3687
+ if (remaining === 0 && !settled) {
3688
+ settled = true;
3689
+ reject(lastError);
3690
+ }
3691
+ return;
3692
+ }
3693
+ }
3694
+ if (settled) return;
3525
3695
  settled = true;
3526
3696
  controller.abort();
3527
3697
  resolve(parsed.data);
@@ -3773,7 +3943,9 @@ ${summaryResponse.content}`
3773
3943
  return this.ask(agents[0], prompt, {
3774
3944
  schema: options?.schema,
3775
3945
  retries: options?.retries,
3776
- metadata: options?.metadata
3946
+ metadata: options?.metadata,
3947
+ validate: options?.validate,
3948
+ validateRetries: options?.validateRetries
3777
3949
  });
3778
3950
  }
3779
3951
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -3814,7 +3986,9 @@ ${summaryResponse.content}`
3814
3986
  return this.ask(routerAgent, prompt, {
3815
3987
  schema: options?.schema,
3816
3988
  retries: options?.retries,
3817
- metadata: options?.metadata
3989
+ metadata: options?.metadata,
3990
+ validate: options?.validate,
3991
+ validateRetries: options?.validateRetries
3818
3992
  });
3819
3993
  }
3820
3994
  // ── Private ───────────────────────────────────────────────────────────
@@ -6096,6 +6270,7 @@ function cosineSimilarity2(a, b) {
6096
6270
  SqliteVectorStore,
6097
6271
  TimeoutError,
6098
6272
  ToolDenied,
6273
+ ValidationError,
6099
6274
  VerifyError,
6100
6275
  WorkflowContext,
6101
6276
  agent,