@axlsdk/axl 0.7.6 → 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,
@@ -2143,6 +2144,18 @@ var GuardrailError = class extends AxlError {
2143
2144
  this.reason = reason;
2144
2145
  }
2145
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
+ };
2146
2159
  var ToolDenied = class extends AxlError {
2147
2160
  toolName;
2148
2161
  agentName;
@@ -2407,9 +2420,6 @@ var WorkflowContext = class _WorkflowContext {
2407
2420
  agent2,
2408
2421
  prompt,
2409
2422
  options,
2410
- 0,
2411
- void 0,
2412
- void 0,
2413
2423
  void 0,
2414
2424
  usageCapture
2415
2425
  );
@@ -2459,7 +2469,7 @@ var WorkflowContext = class _WorkflowContext {
2459
2469
  return result;
2460
2470
  });
2461
2471
  }
2462
- async executeAgentCall(agent2, prompt, options, retryCount = 0, previousOutput, previousError, handoffMessages, usageCapture) {
2472
+ async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
2463
2473
  if (this.budgetContext?.exceeded) {
2464
2474
  const { limit, totalCost: spent, policy } = this.budgetContext;
2465
2475
  if (policy === "warn") {
@@ -2531,16 +2541,6 @@ var WorkflowContext = class _WorkflowContext {
2531
2541
 
2532
2542
  Respond with valid JSON matching this schema:
2533
2543
  ${JSON.stringify(jsonSchema, null, 2)}`;
2534
- }
2535
- if (previousOutput && previousError) {
2536
- userContent += `
2537
-
2538
- Your previous response was invalid:
2539
- ${previousOutput}
2540
-
2541
- Error: ${previousError}
2542
-
2543
- Please fix and try again.`;
2544
2544
  }
2545
2545
  messages.push({ role: "user", content: userContent });
2546
2546
  if (handoffMessages && handoffMessages.length > 0) {
@@ -2585,9 +2585,17 @@ Please fix and try again.`;
2585
2585
  const maxTurns = agent2._config.maxTurns ?? 25;
2586
2586
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
2587
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
+ }
2588
2594
  const currentMessages = [...messages];
2589
2595
  let turns = 0;
2590
2596
  let guardrailOutputRetries = 0;
2597
+ let schemaRetries = 0;
2598
+ let validateRetries = 0;
2591
2599
  while (turns < maxTurns) {
2592
2600
  if (Date.now() - startTime > timeoutMs) {
2593
2601
  throw new TimeoutError("ctx.ask()", timeoutMs);
@@ -2715,14 +2723,17 @@ Please fix and try again.`;
2715
2723
  }
2716
2724
  }
2717
2725
  const handoffStart = Date.now();
2718
- 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;
2719
2733
  const handoffFn = () => this.executeAgentCall(
2720
2734
  descriptor.agent,
2721
2735
  handoffPrompt,
2722
2736
  handoffOptions,
2723
- 0,
2724
- void 0,
2725
- void 0,
2726
2737
  currentMessages,
2727
2738
  usageCapture
2728
2739
  );
@@ -3056,26 +3067,26 @@ Please fix and try again.`;
3056
3067
  throw new GuardrailError("output", outputResult.reason ?? "Output blocked by guardrail");
3057
3068
  }
3058
3069
  }
3070
+ let validated = void 0;
3059
3071
  if (options?.schema) {
3060
3072
  try {
3061
3073
  const parsed = JSON.parse(stripMarkdownFences(content));
3062
- const validated = options.schema.parse(parsed);
3063
- this.pushAssistantToSessionHistory(content, response.providerMetadata);
3064
- return validated;
3074
+ validated = options.schema.parse(parsed);
3065
3075
  } catch (err) {
3066
- const maxRetries = options.retries ?? 3;
3067
- if (retryCount < maxRetries) {
3076
+ const maxSchemaRetries = options.retries ?? 3;
3077
+ if (schemaRetries < maxSchemaRetries) {
3078
+ schemaRetries++;
3068
3079
  const errorMsg = err instanceof Error ? err.message : String(err);
3069
- return this.executeAgentCall(
3070
- agent2,
3071
- prompt,
3072
- options,
3073
- retryCount + 1,
3080
+ currentMessages.push({
3081
+ role: "assistant",
3074
3082
  content,
3075
- errorMsg,
3076
- void 0,
3077
- usageCapture
3078
- );
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;
3079
3090
  }
3080
3091
  const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([
3081
3092
  {
@@ -3084,11 +3095,55 @@ Please fix and try again.`;
3084
3095
  message: err instanceof Error ? err.message : String(err)
3085
3096
  }
3086
3097
  ]);
3087
- 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
+ );
3088
3143
  }
3089
3144
  }
3090
3145
  this.pushAssistantToSessionHistory(content, response.providerMetadata);
3091
- return content;
3146
+ return validated ?? content;
3092
3147
  }
3093
3148
  throw new MaxTurnsError("ctx.ask()", maxTurns);
3094
3149
  }
@@ -3445,32 +3500,57 @@ ${summaryResponse.content}`
3445
3500
  // ── ctx.verify() ──────────────────────────────────────────────────────
3446
3501
  async verify(fn, schema, options) {
3447
3502
  const maxRetries = options?.retries ?? 3;
3448
- let lastOutput = void 0;
3449
- let lastErrorMessage = void 0;
3503
+ let lastRetry = void 0;
3450
3504
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3451
- let result;
3505
+ let rawOutput;
3452
3506
  try {
3453
- result = await fn(lastOutput, lastErrorMessage);
3454
- lastOutput = result;
3455
- 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;
3456
3529
  } catch (err) {
3457
- if (err instanceof import_zod.ZodError) {
3458
- lastErrorMessage = err.message;
3459
- } else if (err instanceof Error) {
3460
- lastErrorMessage = err.message;
3461
- } else {
3462
- 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;
3463
3541
  }
3542
+ const errorMsg = err instanceof import_zod.ZodError ? err.message : err instanceof Error ? err.message : String(err);
3543
+ lastRetry = { error: errorMsg, output: rawOutput };
3464
3544
  if (attempt === maxRetries) {
3465
3545
  if (options?.fallback !== void 0) return options.fallback;
3466
- const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([{ code: "custom", path: [], message: lastErrorMessage }]);
3467
- 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);
3468
3548
  }
3469
3549
  }
3470
3550
  }
3471
3551
  if (options?.fallback !== void 0) return options.fallback;
3472
3552
  throw new VerifyError(
3473
- lastOutput,
3553
+ lastRetry?.output,
3474
3554
  new import_zod.ZodError([{ code: "custom", path: [], message: "Verify failed" }]),
3475
3555
  maxRetries
3476
3556
  );
@@ -3572,7 +3652,7 @@ ${summaryResponse.content}`
3572
3652
  let remaining = fns.length;
3573
3653
  for (const fn of fns) {
3574
3654
  const p = signalStorage.run(composedSignal, fn);
3575
- p.then((value) => {
3655
+ p.then(async (value) => {
3576
3656
  if (settled) return;
3577
3657
  if (schema) {
3578
3658
  const parsed = schema.safeParse(value);
@@ -3585,6 +3665,33 @@ ${summaryResponse.content}`
3585
3665
  }
3586
3666
  return;
3587
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;
3588
3695
  settled = true;
3589
3696
  controller.abort();
3590
3697
  resolve(parsed.data);
@@ -3836,7 +3943,9 @@ ${summaryResponse.content}`
3836
3943
  return this.ask(agents[0], prompt, {
3837
3944
  schema: options?.schema,
3838
3945
  retries: options?.retries,
3839
- metadata: options?.metadata
3946
+ metadata: options?.metadata,
3947
+ validate: options?.validate,
3948
+ validateRetries: options?.validateRetries
3840
3949
  });
3841
3950
  }
3842
3951
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -3877,7 +3986,9 @@ ${summaryResponse.content}`
3877
3986
  return this.ask(routerAgent, prompt, {
3878
3987
  schema: options?.schema,
3879
3988
  retries: options?.retries,
3880
- metadata: options?.metadata
3989
+ metadata: options?.metadata,
3990
+ validate: options?.validate,
3991
+ validateRetries: options?.validateRetries
3881
3992
  });
3882
3993
  }
3883
3994
  // ── Private ───────────────────────────────────────────────────────────
@@ -6159,6 +6270,7 @@ function cosineSimilarity2(a, b) {
6159
6270
  SqliteVectorStore,
6160
6271
  TimeoutError,
6161
6272
  ToolDenied,
6273
+ ValidationError,
6162
6274
  VerifyError,
6163
6275
  WorkflowContext,
6164
6276
  agent,