@axlsdk/axl 0.7.6 → 0.9.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
@@ -7,9 +7,11 @@ Core SDK for orchestrating agentic systems in TypeScript. Part of the [Axl](http
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- npm install @axlsdk/axl zod
10
+ npm install @axlsdk/axl zod@^4
11
11
  ```
12
12
 
13
+ > **Note:** `zod` is a peer dependency — your application and Axl share a single Zod instance. Zod v4 (`^4.0.0`) is required.
14
+
13
15
  ## Project Structure
14
16
 
15
17
  The recommended pattern separates config, tools, agents, workflows, and runtime into their own modules. Dependencies flow one direction: tools → agents → workflows → runtime.
@@ -320,7 +322,7 @@ for await (const event of sessionStream) {
320
322
  All available on `ctx` inside workflow handlers. See the [API Reference](../../docs/api-reference.md) for complete option types, valid values, and defaults.
321
323
 
322
324
  ```typescript
323
- // Invoke an agent (schema retries rebuild the call with the failed output + error in the prompt)
325
+ // Invoke an agent (schema/validate retries accumulate LLM sees all previous failed attempts)
324
326
  const answer = await ctx.ask(agent, 'prompt', { schema, retries });
325
327
 
326
328
  // Run 3 agents in parallel — each gets the same question independently
@@ -329,11 +331,11 @@ const results = await ctx.spawn(3, async (i) => ctx.ask(agent, prompts[i]));
329
331
  // Pick the answer that appeared most often — also supports LLM-as-judge via scorer
330
332
  const winner = await ctx.vote(results, { strategy: 'majority', key: 'answer' });
331
333
 
332
- // Generic retry-until-valid loop (not conversation-aware you decide how to use the error)
334
+ // Retry-until-valid loop for APIs, pipelines, or as a repair fallback for ctx.ask()
333
335
  const valid = await ctx.verify(
334
- async (lastOutput, error) => ctx.ask(agent, error ? `Fix: ${error}` : prompt),
335
- schema,
336
- { retries: 3, fallback: defaultValue },
336
+ async () => fetchRouteFromAPI(origin, destination),
337
+ RouteSchema,
338
+ { retries: 3, fallback: defaultRoute },
337
339
  );
338
340
 
339
341
  // Cost control — returns { value, budgetExceeded, totalCost }
@@ -477,7 +479,29 @@ const safe = agent({
477
479
  });
478
480
  ```
479
481
 
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'`.
482
+ 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'`.
483
+
484
+ For **business rule validation** on the parsed typed object (not raw text), use `validate` on `ctx.ask()`:
485
+
486
+ ```typescript
487
+ const UserSchema = z.object({
488
+ name: z.string(),
489
+ email: z.string(),
490
+ role: z.enum(['admin', 'editor', 'viewer']),
491
+ });
492
+
493
+ const result = await ctx.ask(extractAgent, 'Extract user from this text', {
494
+ schema: UserSchema,
495
+ validate: (user) => {
496
+ if (user.role === 'admin' && !user.email.endsWith('@company.com')) {
497
+ return { valid: false, reason: 'Admin users must have a company email' };
498
+ }
499
+ return { valid: true };
500
+ },
501
+ });
502
+ ```
503
+
504
+ `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
505
 
482
506
  ### State Stores
483
507
 
@@ -556,6 +580,7 @@ import {
556
580
  MaxTurnsError, // Agent exceeded max tool-calling turns
557
581
  BudgetExceededError, // Budget limit exceeded
558
582
  GuardrailError, // Guardrail blocked input or output
583
+ ValidationError, // Post-schema business rule validation failed after retries
559
584
  ToolDenied, // Agent tried to call unauthorized tool
560
585
  } from '@axlsdk/axl';
561
586
  ```
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;
@@ -2235,48 +2248,9 @@ function resolveConfig(config) {
2235
2248
  // src/context.ts
2236
2249
  var signalStorage = new import_node_async_hooks.AsyncLocalStorage();
2237
2250
  function zodToJsonSchema(schema) {
2238
- const def = schema._def;
2239
- if (!def || !def.typeName) return {};
2240
- switch (def.typeName) {
2241
- case "ZodString":
2242
- return { type: "string" };
2243
- case "ZodNumber":
2244
- return { type: "number" };
2245
- case "ZodBoolean":
2246
- return { type: "boolean" };
2247
- case "ZodArray":
2248
- return { type: "array", items: zodToJsonSchema(def.type) };
2249
- case "ZodObject": {
2250
- const shape = def.shape?.() ?? {};
2251
- const properties = {};
2252
- const required = [];
2253
- for (const [key, value] of Object.entries(shape)) {
2254
- properties[key] = zodToJsonSchema(value);
2255
- const innerDef = value._def;
2256
- if (innerDef?.typeName !== "ZodOptional" && innerDef?.typeName !== "ZodDefault") {
2257
- required.push(key);
2258
- }
2259
- }
2260
- return { type: "object", properties, required: required.length > 0 ? required : void 0 };
2261
- }
2262
- case "ZodOptional":
2263
- return zodToJsonSchema(def.innerType);
2264
- case "ZodDefault":
2265
- return zodToJsonSchema(def.innerType);
2266
- case "ZodEnum":
2267
- return { type: "string", enum: def.values };
2268
- case "ZodLiteral": {
2269
- const v = def.value;
2270
- const t = v === null ? "null" : typeof v;
2271
- return { type: t, const: v };
2272
- }
2273
- case "ZodUnion":
2274
- return { oneOf: def.options.map((o) => zodToJsonSchema(o)) };
2275
- case "ZodNullable":
2276
- return { ...zodToJsonSchema(def.innerType), nullable: true };
2277
- default:
2278
- return {};
2279
- }
2251
+ const result = import_zod.z.toJSONSchema(schema, { unrepresentable: "any" });
2252
+ delete result.$schema;
2253
+ return result;
2280
2254
  }
2281
2255
  function estimateTokens(text) {
2282
2256
  return Math.ceil(text.length / 4);
@@ -2407,9 +2381,6 @@ var WorkflowContext = class _WorkflowContext {
2407
2381
  agent2,
2408
2382
  prompt,
2409
2383
  options,
2410
- 0,
2411
- void 0,
2412
- void 0,
2413
2384
  void 0,
2414
2385
  usageCapture
2415
2386
  );
@@ -2459,7 +2430,7 @@ var WorkflowContext = class _WorkflowContext {
2459
2430
  return result;
2460
2431
  });
2461
2432
  }
2462
- async executeAgentCall(agent2, prompt, options, retryCount = 0, previousOutput, previousError, handoffMessages, usageCapture) {
2433
+ async executeAgentCall(agent2, prompt, options, handoffMessages, usageCapture) {
2463
2434
  if (this.budgetContext?.exceeded) {
2464
2435
  const { limit, totalCost: spent, policy } = this.budgetContext;
2465
2436
  if (policy === "warn") {
@@ -2531,16 +2502,6 @@ var WorkflowContext = class _WorkflowContext {
2531
2502
 
2532
2503
  Respond with valid JSON matching this schema:
2533
2504
  ${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
2505
  }
2545
2506
  messages.push({ role: "user", content: userContent });
2546
2507
  if (handoffMessages && handoffMessages.length > 0) {
@@ -2585,9 +2546,17 @@ Please fix and try again.`;
2585
2546
  const maxTurns = agent2._config.maxTurns ?? 25;
2586
2547
  const timeoutMs = parseDuration(agent2._config.timeout ?? "60s");
2587
2548
  const startTime = Date.now();
2549
+ if (this.onToken && options?.validate) {
2550
+ throw new AxlError(
2551
+ "INVALID_CONFIG",
2552
+ "Cannot use validate with streaming. Validate requires schema (JSON output) which does not benefit from token streaming. Use a non-streaming call instead."
2553
+ );
2554
+ }
2588
2555
  const currentMessages = [...messages];
2589
2556
  let turns = 0;
2590
2557
  let guardrailOutputRetries = 0;
2558
+ let schemaRetries = 0;
2559
+ let validateRetries = 0;
2591
2560
  while (turns < maxTurns) {
2592
2561
  if (Date.now() - startTime > timeoutMs) {
2593
2562
  throw new TimeoutError("ctx.ask()", timeoutMs);
@@ -2715,14 +2684,17 @@ Please fix and try again.`;
2715
2684
  }
2716
2685
  }
2717
2686
  const handoffStart = Date.now();
2718
- const handoffOptions = options ? { schema: options.schema, retries: options.retries, metadata: options.metadata } : void 0;
2687
+ const handoffOptions = options ? {
2688
+ schema: options.schema,
2689
+ retries: options.retries,
2690
+ metadata: options.metadata,
2691
+ validate: options.validate,
2692
+ validateRetries: options.validateRetries
2693
+ } : void 0;
2719
2694
  const handoffFn = () => this.executeAgentCall(
2720
2695
  descriptor.agent,
2721
2696
  handoffPrompt,
2722
2697
  handoffOptions,
2723
- 0,
2724
- void 0,
2725
- void 0,
2726
2698
  currentMessages,
2727
2699
  usageCapture
2728
2700
  );
@@ -3056,26 +3028,26 @@ Please fix and try again.`;
3056
3028
  throw new GuardrailError("output", outputResult.reason ?? "Output blocked by guardrail");
3057
3029
  }
3058
3030
  }
3031
+ let validated = void 0;
3059
3032
  if (options?.schema) {
3060
3033
  try {
3061
3034
  const parsed = JSON.parse(stripMarkdownFences(content));
3062
- const validated = options.schema.parse(parsed);
3063
- this.pushAssistantToSessionHistory(content, response.providerMetadata);
3064
- return validated;
3035
+ validated = options.schema.parse(parsed);
3065
3036
  } catch (err) {
3066
- const maxRetries = options.retries ?? 3;
3067
- if (retryCount < maxRetries) {
3037
+ const maxSchemaRetries = options.retries ?? 3;
3038
+ if (schemaRetries < maxSchemaRetries) {
3039
+ schemaRetries++;
3068
3040
  const errorMsg = err instanceof Error ? err.message : String(err);
3069
- return this.executeAgentCall(
3070
- agent2,
3071
- prompt,
3072
- options,
3073
- retryCount + 1,
3041
+ currentMessages.push({
3042
+ role: "assistant",
3074
3043
  content,
3075
- errorMsg,
3076
- void 0,
3077
- usageCapture
3078
- );
3044
+ ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3045
+ });
3046
+ currentMessages.push({
3047
+ role: "system",
3048
+ content: `Your response was not valid JSON or did not match the required schema: ${errorMsg}. Please fix and try again.`
3049
+ });
3050
+ continue;
3079
3051
  }
3080
3052
  const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([
3081
3053
  {
@@ -3084,11 +3056,55 @@ Please fix and try again.`;
3084
3056
  message: err instanceof Error ? err.message : String(err)
3085
3057
  }
3086
3058
  ]);
3087
- throw new VerifyError(content, zodErr, maxRetries);
3059
+ throw new VerifyError(content, zodErr, maxSchemaRetries);
3060
+ }
3061
+ }
3062
+ if (options?.schema && options.validate) {
3063
+ let validateResult;
3064
+ try {
3065
+ validateResult = await options.validate(validated, {
3066
+ metadata: this.metadata
3067
+ });
3068
+ } catch (err) {
3069
+ const reason = err instanceof Error ? err.message : String(err);
3070
+ validateResult = { valid: false, reason: `Validator error: ${reason}` };
3071
+ }
3072
+ this.emitTrace({
3073
+ type: "validate",
3074
+ agent: agent2._name,
3075
+ data: {
3076
+ valid: validateResult.valid,
3077
+ ...validateResult.reason ? { reason: validateResult.reason } : {}
3078
+ }
3079
+ });
3080
+ this.spanManager?.addEventToActiveSpan("axl.validate.check", {
3081
+ "axl.validate.valid": validateResult.valid,
3082
+ ...validateResult.reason ? { "axl.validate.reason": validateResult.reason } : {}
3083
+ });
3084
+ if (!validateResult.valid) {
3085
+ const maxValidateRetries = options.validateRetries ?? 2;
3086
+ if (validateRetries < maxValidateRetries) {
3087
+ validateRetries++;
3088
+ currentMessages.push({
3089
+ role: "assistant",
3090
+ content,
3091
+ ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
3092
+ });
3093
+ currentMessages.push({
3094
+ role: "system",
3095
+ content: `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`
3096
+ });
3097
+ continue;
3098
+ }
3099
+ throw new ValidationError(
3100
+ validated,
3101
+ validateResult.reason ?? "Validation failed",
3102
+ maxValidateRetries
3103
+ );
3088
3104
  }
3089
3105
  }
3090
3106
  this.pushAssistantToSessionHistory(content, response.providerMetadata);
3091
- return content;
3107
+ return validated ?? content;
3092
3108
  }
3093
3109
  throw new MaxTurnsError("ctx.ask()", maxTurns);
3094
3110
  }
@@ -3445,32 +3461,57 @@ ${summaryResponse.content}`
3445
3461
  // ── ctx.verify() ──────────────────────────────────────────────────────
3446
3462
  async verify(fn, schema, options) {
3447
3463
  const maxRetries = options?.retries ?? 3;
3448
- let lastOutput = void 0;
3449
- let lastErrorMessage = void 0;
3464
+ let lastRetry = void 0;
3450
3465
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3451
- let result;
3466
+ let rawOutput;
3452
3467
  try {
3453
- result = await fn(lastOutput, lastErrorMessage);
3454
- lastOutput = result;
3455
- return schema.parse(result);
3468
+ const result = await fn(lastRetry);
3469
+ rawOutput = result;
3470
+ const parsed = schema.parse(result);
3471
+ if (options?.validate) {
3472
+ let validateResult;
3473
+ try {
3474
+ validateResult = await options.validate(parsed, { metadata: this.metadata });
3475
+ } catch (err) {
3476
+ const reason = err instanceof Error ? err.message : String(err);
3477
+ validateResult = { valid: false, reason: `Validator error: ${reason}` };
3478
+ }
3479
+ if (!validateResult.valid) {
3480
+ const errorMsg = validateResult.reason ?? "Validation failed";
3481
+ lastRetry = { error: errorMsg, output: rawOutput, parsed };
3482
+ if (attempt === maxRetries) {
3483
+ if (options?.fallback !== void 0) return options.fallback;
3484
+ throw new ValidationError(parsed, errorMsg, maxRetries);
3485
+ }
3486
+ continue;
3487
+ }
3488
+ }
3489
+ return parsed;
3456
3490
  } 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);
3491
+ if (err instanceof ValidationError) {
3492
+ lastRetry = {
3493
+ error: err.reason,
3494
+ output: rawOutput,
3495
+ parsed: err.lastOutput
3496
+ };
3497
+ if (attempt === maxRetries) {
3498
+ if (options?.fallback !== void 0) return options.fallback;
3499
+ throw err;
3500
+ }
3501
+ continue;
3463
3502
  }
3503
+ const errorMsg = err instanceof import_zod.ZodError ? err.message : err instanceof Error ? err.message : String(err);
3504
+ lastRetry = { error: errorMsg, output: rawOutput };
3464
3505
  if (attempt === maxRetries) {
3465
3506
  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);
3507
+ const zodErr = err instanceof import_zod.ZodError ? err : new import_zod.ZodError([{ code: "custom", path: [], message: errorMsg }]);
3508
+ throw new VerifyError(rawOutput, zodErr, maxRetries);
3468
3509
  }
3469
3510
  }
3470
3511
  }
3471
3512
  if (options?.fallback !== void 0) return options.fallback;
3472
3513
  throw new VerifyError(
3473
- lastOutput,
3514
+ lastRetry?.output,
3474
3515
  new import_zod.ZodError([{ code: "custom", path: [], message: "Verify failed" }]),
3475
3516
  maxRetries
3476
3517
  );
@@ -3572,7 +3613,7 @@ ${summaryResponse.content}`
3572
3613
  let remaining = fns.length;
3573
3614
  for (const fn of fns) {
3574
3615
  const p = signalStorage.run(composedSignal, fn);
3575
- p.then((value) => {
3616
+ p.then(async (value) => {
3576
3617
  if (settled) return;
3577
3618
  if (schema) {
3578
3619
  const parsed = schema.safeParse(value);
@@ -3585,6 +3626,33 @@ ${summaryResponse.content}`
3585
3626
  }
3586
3627
  return;
3587
3628
  }
3629
+ if (options?.validate) {
3630
+ try {
3631
+ const validateResult = await options.validate(parsed.data, {
3632
+ metadata: this.metadata
3633
+ });
3634
+ if (!validateResult.valid) {
3635
+ remaining--;
3636
+ lastError = new Error(
3637
+ `Validation failed: ${validateResult.reason ?? "Validation failed"}`
3638
+ );
3639
+ if (remaining === 0 && !settled) {
3640
+ settled = true;
3641
+ reject(lastError);
3642
+ }
3643
+ return;
3644
+ }
3645
+ } catch (err) {
3646
+ remaining--;
3647
+ lastError = err instanceof Error ? err : new Error(`Validator error: ${String(err)}`);
3648
+ if (remaining === 0 && !settled) {
3649
+ settled = true;
3650
+ reject(lastError);
3651
+ }
3652
+ return;
3653
+ }
3654
+ }
3655
+ if (settled) return;
3588
3656
  settled = true;
3589
3657
  controller.abort();
3590
3658
  resolve(parsed.data);
@@ -3836,7 +3904,9 @@ ${summaryResponse.content}`
3836
3904
  return this.ask(agents[0], prompt, {
3837
3905
  schema: options?.schema,
3838
3906
  retries: options?.retries,
3839
- metadata: options?.metadata
3907
+ metadata: options?.metadata,
3908
+ validate: options?.validate,
3909
+ validateRetries: options?.validateRetries
3840
3910
  });
3841
3911
  }
3842
3912
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -3877,7 +3947,9 @@ ${summaryResponse.content}`
3877
3947
  return this.ask(routerAgent, prompt, {
3878
3948
  schema: options?.schema,
3879
3949
  retries: options?.retries,
3880
- metadata: options?.metadata
3950
+ metadata: options?.metadata,
3951
+ validate: options?.validate,
3952
+ validateRetries: options?.validateRetries
3881
3953
  });
3882
3954
  }
3883
3955
  // ── Private ───────────────────────────────────────────────────────────
@@ -6159,6 +6231,7 @@ function cosineSimilarity2(a, b) {
6159
6231
  SqliteVectorStore,
6160
6232
  TimeoutError,
6161
6233
  ToolDenied,
6234
+ ValidationError,
6162
6235
  VerifyError,
6163
6236
  WorkflowContext,
6164
6237
  agent,