@axlsdk/axl 0.22.2 → 0.22.3

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
@@ -2340,17 +2340,22 @@ function resolveOpenAIReasoningEffort(model, resolved) {
2340
2340
  }
2341
2341
  return resolved.thinkingDisabled ? clampReasoningEffort(model, "none") : void 0;
2342
2342
  }
2343
- var warnedChatMaxDowngrades = /* @__PURE__ */ new Set();
2344
2343
  function resolveOpenAIChatReasoningEffort(model, resolved) {
2345
2344
  const effort = resolveOpenAIReasoningEffort(model, resolved);
2346
- if (effort !== "max") return effort;
2347
- if (resolved.activeEffort === "max" && !warnedChatMaxDowngrades.has(model)) {
2348
- warnedChatMaxDowngrades.add(model);
2349
- console.warn(
2350
- `[axl] OpenAI Chat Completions does not accept effort "max" for ${model}; using "xhigh". Use the OpenAI Responses provider for native "max".`
2351
- );
2352
- }
2353
- return "xhigh";
2345
+ return effort === "max" ? "xhigh" : effort;
2346
+ }
2347
+ function resolveOpenAIEffortResolution(options, resolveWireEffort, endpoint) {
2348
+ const resolved = resolveThinkingOptions(options);
2349
+ if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
2350
+ const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
2351
+ const effective = resolveWireEffort(model, resolved);
2352
+ if (effective === void 0 || effective === resolved.effort) return void 0;
2353
+ return {
2354
+ requested: resolved.effort,
2355
+ effective,
2356
+ clamped: true,
2357
+ cause: `${endpoint} does not accept reasoning effort '${resolved.effort}' for ${model}; using '${effective}'`
2358
+ };
2354
2359
  }
2355
2360
  var openaiReasoningEmit = (body, resolved, model) => {
2356
2361
  const oSeries = isOSeriesModel(model);
@@ -2375,6 +2380,15 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
2375
2380
  constructor(options = {}) {
2376
2381
  super({ profile: OPENAI_PROFILE, ...options });
2377
2382
  }
2383
+ /** Report a clamped `effort` — Chat Completions caps `'max'` at `'xhigh'` and
2384
+ * rejects `'none'` before gpt-5.1 (clamped to `'minimal'`). */
2385
+ effortResolution(options) {
2386
+ return resolveOpenAIEffortResolution(
2387
+ options,
2388
+ resolveOpenAIChatReasoningEffort,
2389
+ "OpenAI Chat Completions"
2390
+ );
2391
+ }
2378
2392
  computeCost(model, usage, _reportedCost, context) {
2379
2393
  return usage ? estimateDirectOpenAICost(model, usage, { ...context, baseUrl: this.baseUrl }) : void 0;
2380
2394
  }
@@ -2421,6 +2435,12 @@ var OpenAIResponsesProvider = class {
2421
2435
  inputCapabilities(model) {
2422
2436
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2423
2437
  }
2438
+ /** Report a clamped `effort`. Responses accepts the native `'max'` tier that
2439
+ * Chat Completions caps, but still clamps `'none'` before gpt-5.1, `'xhigh'`
2440
+ * below gpt-5.2, and everything to `'high'` on gpt-5-pro. */
2441
+ effortResolution(options) {
2442
+ return resolveOpenAIEffortResolution(options, resolveOpenAIReasoningEffort, "OpenAI Responses");
2443
+ }
2424
2444
  validateInput(request) {
2425
2445
  const modelOverride = request.providerOptions?.model;
2426
2446
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
@@ -3096,14 +3116,6 @@ function budgetToClaudeEffort(budget) {
3096
3116
  if (budget <= THINKING_BUDGETS.xhigh) return "xhigh";
3097
3117
  return "max";
3098
3118
  }
3099
- var warnedFableDisable = /* @__PURE__ */ new Set();
3100
- function warnFableDisable(model) {
3101
- if (warnedFableDisable.has(model)) return;
3102
- warnedFableDisable.add(model);
3103
- console.warn(
3104
- `[axl] thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'.`
3105
- );
3106
- }
3107
3119
  function resolveClaudeThinking(model, resolved) {
3108
3120
  const capability = resolveClaudeCapability(model);
3109
3121
  if (!capability) return { stripTemperature: false };
@@ -3123,11 +3135,14 @@ function resolveClaudeThinking(model, resolved) {
3123
3135
  }
3124
3136
  if (resolved.thinkingDisabled) {
3125
3137
  if (capability.thinking === "adaptive-always-on") {
3126
- warnFableDisable(model);
3127
3138
  return {
3128
3139
  thinking: { type: "adaptive" },
3129
3140
  outputConfig: { effort: "low" },
3130
- stripTemperature: true
3141
+ stripTemperature: true,
3142
+ clamp: {
3143
+ effective: "low",
3144
+ cause: `thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'`
3145
+ }
3131
3146
  };
3132
3147
  }
3133
3148
  if (activeEffort && capability.disableAt && !isEffortAtOrBelow(activeEffort, capability.disableAt)) {
@@ -3189,7 +3204,16 @@ function resolveClaudeThinking(model, resolved) {
3189
3204
  return {
3190
3205
  thinking: { type: "enabled", budget_tokens: budget },
3191
3206
  manualBudget: budget,
3192
- stripTemperature: true
3207
+ stripTemperature: true,
3208
+ // Legacy models take `budget_tokens`, not an effort level, and have no
3209
+ // `effortLevels` — so `clampAnthropicEffort` folds 'max'/'xhigh' down to
3210
+ // the 'high' tier and the request silently carries that tier's budget.
3211
+ // Report it: an unreported budget-tier downgrade is exactly the silent
3212
+ // clamp this capability exists to expose.
3213
+ clamp: activeEffort === resolved.activeEffort ? void 0 : {
3214
+ effective: activeEffort,
3215
+ cause: `Anthropic ${model} takes thinking budget_tokens rather than an effort level, and its ceiling tier is '${activeEffort}'; effort '${resolved.activeEffort}' sends budget_tokens ${budget}`
3216
+ }
3193
3217
  };
3194
3218
  }
3195
3219
  return { stripTemperature: capability.stripTemperature ?? false };
@@ -3300,6 +3324,26 @@ function mergeAnthropicUsage(initial, update) {
3300
3324
  }
3301
3325
  var AnthropicProvider = class {
3302
3326
  name = "anthropic";
3327
+ /** Report a clamped `effort`. Anthropic clamps to each model's supported
3328
+ * `output_config.effort` levels, and models that always think fall back to
3329
+ * adaptive `'low'` when asked to disable thinking. Only reported when the
3330
+ * effective level is what the request body actually carries; an explicit
3331
+ * `thinkingBudget` is documented to override `effort`, so it is not a clamp. */
3332
+ effortResolution(options) {
3333
+ const resolved = resolveThinkingOptions(options);
3334
+ if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
3335
+ const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3336
+ const config = resolveClaudeThinking(model, resolved);
3337
+ const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
3338
+ const effective = config.clamp?.effective ?? wireEffort;
3339
+ if (effective === void 0 || effective === resolved.effort) return void 0;
3340
+ return {
3341
+ requested: resolved.effort,
3342
+ effective,
3343
+ clamped: true,
3344
+ cause: config.clamp?.cause ?? `Anthropic ${model} does not support effort '${resolved.effort}'; using '${effective}'`
3345
+ };
3346
+ }
3303
3347
  inputCapabilities(model) {
3304
3348
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
3305
3349
  }
@@ -4150,13 +4194,71 @@ function budgetToThinkingLevel(budgetTokens) {
4150
4194
  function minThinkingLevel(model) {
4151
4195
  return GEMINI_3X_MIN_THINKING_LEVEL.get(model) ?? "minimal";
4152
4196
  }
4153
- var _warned3xEffortNone = /* @__PURE__ */ new Set();
4154
- function warnGemini3xEffortNone(model) {
4155
- if (_warned3xEffortNone.has(model)) return;
4156
- _warned3xEffortNone.add(model);
4157
- console.warn(
4158
- `[axl] effort: 'none' on Gemini 3.x (${model}) maps to the model's minimum thinking level ('${minThinkingLevel(model)}'), not fully disabled. Gemini 3.x models cannot disable thinking entirely.`
4159
- );
4197
+ function resolveGeminiThinking(model, resolved) {
4198
+ const is3x = isGemini3x(model);
4199
+ if (resolved.thinkingDisabled) {
4200
+ const minLevel = minThinkingLevel(model);
4201
+ if (is3x) {
4202
+ return {
4203
+ interactionLevel: minLevel,
4204
+ thinkingLevel: minLevel,
4205
+ thinkingBudget: void 0,
4206
+ attachThoughts: false,
4207
+ // A `thinkingBudget: 0` disable has no unified effort to report against;
4208
+ // only an explicit `effort: 'none'` is a clamp of a requested value.
4209
+ clamp: resolved.effort === "none" ? {
4210
+ effective: minLevel,
4211
+ cause: `Gemini 3.x models cannot disable thinking; using the model minimum thinking level '${minLevel}'`
4212
+ } : void 0
4213
+ };
4214
+ }
4215
+ return {
4216
+ interactionLevel: "minimal",
4217
+ thinkingLevel: void 0,
4218
+ thinkingBudget: 0,
4219
+ attachThoughts: false
4220
+ };
4221
+ }
4222
+ if (resolved.hasBudgetOverride) {
4223
+ const budget = resolved.thinkingBudget;
4224
+ const level = budgetToThinkingLevel(budget);
4225
+ return {
4226
+ interactionLevel: level,
4227
+ thinkingLevel: is3x ? level : void 0,
4228
+ thinkingBudget: is3x ? void 0 : budget,
4229
+ attachThoughts: resolved.includeThoughts
4230
+ };
4231
+ }
4232
+ if (resolved.activeEffort) {
4233
+ const effort = resolved.activeEffort;
4234
+ const budget = THINKING_BUDGETS2[effort] ?? 5e3;
4235
+ if (is3x) {
4236
+ const level = THINKING_LEVELS[effort] ?? "medium";
4237
+ return {
4238
+ interactionLevel: level,
4239
+ thinkingLevel: level,
4240
+ thinkingBudget: void 0,
4241
+ attachThoughts: resolved.includeThoughts,
4242
+ clamp: level === effort ? void 0 : {
4243
+ effective: level,
4244
+ cause: `Gemini 3.x thinking levels cap at 'high'; effort '${effort}' maps to '${level}'`
4245
+ }
4246
+ };
4247
+ }
4248
+ return {
4249
+ interactionLevel: budgetToThinkingLevel(budget),
4250
+ thinkingLevel: void 0,
4251
+ // 2.5 Pro supports a higher max budget (32768) than other 2.5 models (24576).
4252
+ thinkingBudget: effort === "max" && model === "gemini-2.5-pro" ? 32768 : budget,
4253
+ attachThoughts: resolved.includeThoughts
4254
+ };
4255
+ }
4256
+ return {
4257
+ interactionLevel: void 0,
4258
+ thinkingLevel: void 0,
4259
+ thinkingBudget: void 0,
4260
+ attachThoughts: resolved.includeThoughts
4261
+ };
4160
4262
  }
4161
4263
  var GeminiProvider = class {
4162
4264
  name = "google";
@@ -4225,6 +4327,21 @@ var GeminiProvider = class {
4225
4327
  nativeStructuredOutputSupport() {
4226
4328
  return "lossy";
4227
4329
  }
4330
+ /** Report a clamped `effort` (R7a). Gemini 3.x cannot disable thinking and
4331
+ * caps at `'high'`, so `'none'`, `'xhigh'` and `'max'` are not honored
4332
+ * verbatim there. Everything else — including 2.x disabled thinking — is
4333
+ * sent as asked, so there is nothing to report. */
4334
+ effortResolution(options) {
4335
+ const resolved = resolveThinkingOptions(options);
4336
+ if (resolved.effort === void 0) return void 0;
4337
+ const clamp = resolveGeminiThinking(this.requestModel(options), resolved).clamp;
4338
+ return clamp ? {
4339
+ requested: resolved.effort,
4340
+ effective: clamp.effective,
4341
+ clamped: true,
4342
+ cause: clamp.cause
4343
+ } : void 0;
4344
+ }
4228
4345
  baseUrl;
4229
4346
  apiKeySource;
4230
4347
  callCounter = 0;
@@ -4399,12 +4516,9 @@ var GeminiProvider = class {
4399
4516
  generationConfig.tool_choice = typeof options.toolChoice === "string" ? options.toolChoice === "required" ? "any" : options.toolChoice : { allowed_tools: { mode: "any", tools: [options.toolChoice.function.name] } };
4400
4517
  }
4401
4518
  const thinking = resolveThinkingOptions(options);
4402
- if (thinking.thinkingDisabled) {
4403
- generationConfig.thinking_level = isGemini3x(model) ? minThinkingLevel(model) : "minimal";
4404
- } else if (thinking.hasBudgetOverride) {
4405
- generationConfig.thinking_level = budgetToThinkingLevel(thinking.thinkingBudget);
4406
- } else if (thinking.activeEffort) {
4407
- generationConfig.thinking_level = isGemini3x(model) ? THINKING_LEVELS[thinking.activeEffort] ?? "medium" : budgetToThinkingLevel(THINKING_BUDGETS2[thinking.activeEffort] ?? 5e3);
4519
+ const thinkingPlan = resolveGeminiThinking(model, thinking);
4520
+ if (thinkingPlan.interactionLevel !== void 0) {
4521
+ generationConfig.thinking_level = thinkingPlan.interactionLevel;
4408
4522
  }
4409
4523
  if (thinking.includeThoughts) generationConfig.thinking_summaries = "auto";
4410
4524
  if (Object.keys(generationConfig).length) body.generation_config = generationConfig;
@@ -4840,50 +4954,17 @@ var GeminiProvider = class {
4840
4954
  if (Object.keys(generationConfig).length > 0) {
4841
4955
  body.generationConfig = generationConfig;
4842
4956
  }
4843
- const {
4844
- effort,
4845
- thinkingBudget,
4846
- includeThoughts,
4847
- thinkingDisabled,
4848
- activeEffort,
4849
- hasBudgetOverride
4850
- } = resolveThinkingOptions(options);
4851
- if (thinkingDisabled) {
4852
- if (isGemini3x(requestModel)) {
4853
- if (effort === "none") {
4854
- warnGemini3xEffortNone(requestModel);
4855
- }
4856
- generationConfig.thinkingConfig = { thinkingLevel: minThinkingLevel(requestModel) };
4857
- } else {
4858
- generationConfig.thinkingConfig = { thinkingBudget: 0 };
4859
- }
4860
- if (!body.generationConfig) body.generationConfig = generationConfig;
4861
- } else if (hasBudgetOverride) {
4862
- const config = {};
4863
- if (isGemini3x(requestModel)) {
4864
- config.thinkingLevel = budgetToThinkingLevel(thinkingBudget);
4865
- } else {
4866
- config.thinkingBudget = thinkingBudget;
4867
- }
4868
- if (includeThoughts) config.includeThoughts = true;
4869
- generationConfig.thinkingConfig = config;
4870
- if (!body.generationConfig) body.generationConfig = generationConfig;
4871
- } else if (activeEffort) {
4872
- const config = {};
4873
- if (isGemini3x(requestModel)) {
4874
- config.thinkingLevel = THINKING_LEVELS[activeEffort] ?? "medium";
4875
- } else {
4876
- if (activeEffort === "max" && requestModel === "gemini-2.5-pro") {
4877
- config.thinkingBudget = 32768;
4878
- } else {
4879
- config.thinkingBudget = THINKING_BUDGETS2[activeEffort] ?? 5e3;
4880
- }
4881
- }
4882
- if (includeThoughts) config.includeThoughts = true;
4883
- generationConfig.thinkingConfig = config;
4884
- if (!body.generationConfig) body.generationConfig = generationConfig;
4885
- } else if (includeThoughts) {
4886
- generationConfig.thinkingConfig = { includeThoughts: true };
4957
+ const thinkingPlan = resolveGeminiThinking(requestModel, resolveThinkingOptions(options));
4958
+ const thinkingConfig = {};
4959
+ if (thinkingPlan.thinkingLevel !== void 0) {
4960
+ thinkingConfig.thinkingLevel = thinkingPlan.thinkingLevel;
4961
+ }
4962
+ if (thinkingPlan.thinkingBudget !== void 0) {
4963
+ thinkingConfig.thinkingBudget = thinkingPlan.thinkingBudget;
4964
+ }
4965
+ if (thinkingPlan.attachThoughts) thinkingConfig.includeThoughts = true;
4966
+ if (Object.keys(thinkingConfig).length > 0) {
4967
+ generationConfig.thinkingConfig = thinkingConfig;
4887
4968
  if (!body.generationConfig) body.generationConfig = generationConfig;
4888
4969
  }
4889
4970
  if (options.toolChoice !== void 0) {
@@ -7518,6 +7599,9 @@ var AXL_EVENT_TYPES = [
7518
7599
  // structured-output cliffs (oversized appended schema, dropped refinements,
7519
7600
  // streaming disabled, zero-guidance `schemaPrompt:'none'`).
7520
7601
  "schema_diagnostic",
7602
+ // Provider capability diagnostics — the resolved provider could not honor a
7603
+ // portable request knob verbatim (first kind: a clamped `effort`).
7604
+ "provider_diagnostic",
7521
7605
  // Observability
7522
7606
  "log",
7523
7607
  "memory_remember",
@@ -7567,6 +7651,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
7567
7651
  "pipeline",
7568
7652
  "verify",
7569
7653
  "schema_diagnostic",
7654
+ "provider_diagnostic",
7570
7655
  "workflow_start",
7571
7656
  "workflow_end",
7572
7657
  "transcription_start",
@@ -8495,6 +8580,10 @@ var LEGACY_REDACTION_RULES = {
8495
8580
  // which is user content or PII (field paths are schema-shape, preserved like
8496
8581
  // `string_delta.path`). Passthrough, as an explicit reviewed decision.
8497
8582
  schema_diagnostic: passthrough,
8583
+ // `provider_diagnostic` carries only provider capability metadata — a provider
8584
+ // name, a model id, and two effort level strings. No application data or user
8585
+ // content ever reaches it. Passthrough, as an explicit reviewed decision.
8586
+ provider_diagnostic: passthrough,
8498
8587
  log: (e) => {
8499
8588
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
8500
8589
  return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
@@ -8625,6 +8714,7 @@ var COMMON_V2_REDACTION_RULES = {
8625
8714
  string_delta: reuseLegacyRule("string_delta"),
8626
8715
  verify: reuseLegacyRule("verify"),
8627
8716
  schema_diagnostic: reuseLegacyRule("schema_diagnostic"),
8717
+ provider_diagnostic: reuseLegacyRule("provider_diagnostic"),
8628
8718
  log: reuseLegacyRule("log"),
8629
8719
  memory_remember: reuseLegacyRule("memory_remember"),
8630
8720
  memory_recall: reuseLegacyRule("memory_recall"),
@@ -8720,13 +8810,14 @@ function detectDroppedRefinements(schema) {
8720
8810
  var DOCS_URL = "docs/observability.md#schema-diagnostics";
8721
8811
  var MAX_WARNED_KEYS = 2048;
8722
8812
  var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
8723
- function warnSchemaDiagnosticOnce(key, message, silent) {
8813
+ function warnDiagnosticOnce(key, message, silent, docs = DOCS_URL) {
8724
8814
  if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
8725
8815
  if (warnedDiagnosticKeys.has(key)) return;
8726
8816
  if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
8727
8817
  warnedDiagnosticKeys.add(key);
8728
- console.warn(`[axl] ${message} See ${DOCS_URL}.`);
8818
+ console.warn(`[axl] ${message} See ${docs}.`);
8729
8819
  }
8820
+ var warnSchemaDiagnosticOnce = warnDiagnosticOnce;
8730
8821
 
8731
8822
  // src/config.ts
8732
8823
  function defineConfig(config) {
@@ -9576,7 +9667,22 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
9576
9667
  content,
9577
9668
  ...providerMetadata ? { providerMetadata } : {}
9578
9669
  });
9579
- messages.push({ role: "system", content: feedbackMessage });
9670
+ messages.push({ role: "user", content: feedbackMessage });
9671
+ }
9672
+ function isRetryAbort(value) {
9673
+ return typeof value === "object" && value !== null && value.retry === false;
9674
+ }
9675
+ function describeValue(value) {
9676
+ if (value === null) return "null";
9677
+ if (Array.isArray(value)) return `an array of length ${value.length}`;
9678
+ if (typeof value === "object") {
9679
+ const keys = Object.keys(value);
9680
+ return keys.length > 0 ? `an object with keys [${keys.join(", ")}]` : "an empty object";
9681
+ }
9682
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
9683
+ return `the ${typeof value} ${JSON.stringify(value)}`;
9684
+ }
9685
+ return `a value of type ${typeof value}`;
9580
9686
  }
9581
9687
  function estimateMessagesTokens(messages) {
9582
9688
  let total = 0;
@@ -10513,6 +10619,7 @@ ${appendedSchemaText}`;
10513
10619
  provider,
10514
10620
  model
10515
10621
  });
10622
+ this.emitProviderDiagnostics(agent2, options, { provider, model, providerOptions });
10516
10623
  if (handoffMessages && handoffMessages.length > 0) {
10517
10624
  const handoffContext = handoffMessages.filter(
10518
10625
  (m) => m.role === "assistant" || m.role === "tool"
@@ -10877,7 +10984,11 @@ ${appendedSchemaText}`;
10877
10984
  retries: options.retries,
10878
10985
  metadata: options.metadata,
10879
10986
  validate: options.validate,
10880
- validateRetries: options.validateRetries
10987
+ validateRetries: options.validateRetries,
10988
+ // This seam serves both callers of a handoff: a plain `ctx.ask()` on an
10989
+ // agent with `handoffs`, and the routed `ctx.delegate()` path, whose
10990
+ // terminal ask is the handoff target. The hook travels like `validate`.
10991
+ retryFeedback: options.retryFeedback
10881
10992
  } : void 0;
10882
10993
  const handoffFn = () => askStorage.run(targetFrame, async () => {
10883
10994
  const delegateOriginal = agent2._delegateOriginalInput;
@@ -11026,9 +11137,10 @@ ${appendedSchemaText}`;
11026
11137
  const attempt = guardrailOutputRetries + 1;
11027
11138
  const maxAttempts = maxGuardrailRetries + 1;
11028
11139
  const onBlock = guardrails.onBlock ?? "throw";
11140
+ const blockReason = outputResult.reason ?? "Output blocked by guardrail";
11029
11141
  let feedbackMessage;
11030
11142
  if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
11031
- feedbackMessage = `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
11143
+ feedbackMessage = `Your previous response was blocked by a safety guardrail: ${blockReason}. Please provide a different response that complies with the guidelines.`;
11032
11144
  }
11033
11145
  this.emitEvent({
11034
11146
  type: "guardrail",
@@ -11051,31 +11163,43 @@ ${appendedSchemaText}`;
11051
11163
  });
11052
11164
  if (outputResult.block) {
11053
11165
  if (feedbackMessage) {
11054
- this.emitEvent({
11055
- type: "pipeline",
11056
- agent: agent2._name,
11057
- status: "failed",
11166
+ const decision = await this.resolveRetryFeedback(options?.retryFeedback, {
11058
11167
  stage: "guardrail",
11059
- attempt: guardrailOutputRetries + 1,
11060
- maxAttempts: maxGuardrailRetries + 1,
11061
- reason: feedbackMessage
11168
+ attempt,
11169
+ maxAttempts,
11170
+ output: content,
11171
+ reason: blockReason,
11172
+ defaultMessage: feedbackMessage
11062
11173
  });
11063
- guardrailOutputRetries++;
11064
- appendRetryMessages(
11065
- currentMessages,
11066
- content,
11067
- feedbackMessage,
11068
- response.providerMetadata
11069
- );
11070
- pendingRetryReason = "guardrail";
11071
- continue;
11174
+ if (decision.retry) {
11175
+ this.emitEvent({
11176
+ type: "pipeline",
11177
+ agent: agent2._name,
11178
+ status: "failed",
11179
+ stage: "guardrail",
11180
+ attempt,
11181
+ maxAttempts,
11182
+ reason: decision.message
11183
+ });
11184
+ guardrailOutputRetries++;
11185
+ appendRetryMessages(
11186
+ currentMessages,
11187
+ content,
11188
+ decision.message,
11189
+ response.providerMetadata
11190
+ );
11191
+ pendingRetryReason = "guardrail";
11192
+ continue;
11193
+ }
11194
+ this.currentSignal?.throwIfAborted();
11195
+ throw new GuardrailError("output", blockReason);
11072
11196
  }
11073
11197
  if (typeof onBlock === "function") {
11074
- return onBlock(outputResult.reason ?? "Output blocked by guardrail", {
11198
+ return onBlock(blockReason, {
11075
11199
  metadata: this.metadata
11076
11200
  });
11077
11201
  }
11078
- throw new GuardrailError("output", outputResult.reason ?? "Output blocked by guardrail");
11202
+ throw new GuardrailError("output", blockReason);
11079
11203
  }
11080
11204
  }
11081
11205
  let validated = void 0;
@@ -11095,7 +11219,7 @@ ${appendedSchemaText}`;
11095
11219
  schemaErr = err;
11096
11220
  schemaReason = err instanceof Error ? err.message : String(err);
11097
11221
  if (schemaRetries < maxSchemaRetries) {
11098
- schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
11222
+ schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Return a corrected response that matches the required schema.`;
11099
11223
  }
11100
11224
  }
11101
11225
  this.emitEvent({
@@ -11117,24 +11241,36 @@ ${appendedSchemaText}`;
11117
11241
  });
11118
11242
  if (!schemaValid) {
11119
11243
  if (schemaFeedback) {
11120
- this.emitEvent({
11121
- type: "pipeline",
11122
- agent: agent2._name,
11123
- status: "failed",
11244
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
11124
11245
  stage: "schema",
11125
- attempt: schemaRetries + 1,
11126
- maxAttempts: (options.retries ?? 3) + 1,
11127
- reason: schemaFeedback
11246
+ attempt: schemaAttempt,
11247
+ maxAttempts: schemaMaxAttempts,
11248
+ output: content,
11249
+ reason: schemaReason ?? "Schema parse failed",
11250
+ ...schemaErr !== void 0 ? { error: schemaErr } : {},
11251
+ defaultMessage: schemaFeedback
11128
11252
  });
11129
- schemaRetries++;
11130
- appendRetryMessages(
11131
- currentMessages,
11132
- content,
11133
- schemaFeedback,
11134
- response.providerMetadata
11135
- );
11136
- pendingRetryReason = "schema";
11137
- continue;
11253
+ if (decision.retry) {
11254
+ this.emitEvent({
11255
+ type: "pipeline",
11256
+ agent: agent2._name,
11257
+ status: "failed",
11258
+ stage: "schema",
11259
+ attempt: schemaAttempt,
11260
+ maxAttempts: schemaMaxAttempts,
11261
+ reason: decision.message
11262
+ });
11263
+ schemaRetries++;
11264
+ appendRetryMessages(
11265
+ currentMessages,
11266
+ content,
11267
+ decision.message,
11268
+ response.providerMetadata
11269
+ );
11270
+ pendingRetryReason = "schema";
11271
+ continue;
11272
+ }
11273
+ this.currentSignal?.throwIfAborted();
11138
11274
  }
11139
11275
  const zodErr = schemaErr instanceof import_zod2.ZodError ? schemaErr : new import_zod2.ZodError([
11140
11276
  {
@@ -11148,12 +11284,14 @@ ${appendedSchemaText}`;
11148
11284
  }
11149
11285
  if (options?.schema && options.validate) {
11150
11286
  let validateResult;
11287
+ let validateErr;
11151
11288
  try {
11152
11289
  validateResult = await options.validate(validated, {
11153
11290
  metadata: this.metadata
11154
11291
  });
11155
11292
  } catch (err) {
11156
11293
  rethrowEventStreamOverflow(err);
11294
+ validateErr = err;
11157
11295
  const reason = err instanceof Error ? err.message : String(err);
11158
11296
  validateResult = { valid: false, reason: `Validator error: ${reason}` };
11159
11297
  }
@@ -11162,7 +11300,7 @@ ${appendedSchemaText}`;
11162
11300
  const validateMaxAttempts = maxValidateRetries + 1;
11163
11301
  let validateFeedback;
11164
11302
  if (!validateResult.valid && validateRetries < maxValidateRetries) {
11165
- validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
11303
+ validateFeedback = `Your previous response failed validation: ${validateResult.reason ?? "Validation failed"}. Return a corrected response that satisfies the required schema and this rule.`;
11166
11304
  }
11167
11305
  this.emitEvent({
11168
11306
  type: "validate",
@@ -11183,24 +11321,37 @@ ${appendedSchemaText}`;
11183
11321
  });
11184
11322
  if (!validateResult.valid) {
11185
11323
  if (validateFeedback) {
11186
- this.emitEvent({
11187
- type: "pipeline",
11188
- agent: agent2._name,
11189
- status: "failed",
11324
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
11190
11325
  stage: "validate",
11191
- attempt: validateRetries + 1,
11192
- maxAttempts: (options.validateRetries ?? 2) + 1,
11193
- reason: validateFeedback
11326
+ attempt: validateAttempt,
11327
+ maxAttempts: validateMaxAttempts,
11328
+ output: content,
11329
+ parsed: validated,
11330
+ reason: validateResult.reason ?? "Validation failed",
11331
+ ...validateErr !== void 0 ? { error: validateErr } : {},
11332
+ defaultMessage: validateFeedback
11194
11333
  });
11195
- validateRetries++;
11196
- appendRetryMessages(
11197
- currentMessages,
11198
- content,
11199
- validateFeedback,
11200
- response.providerMetadata
11201
- );
11202
- pendingRetryReason = "validate";
11203
- continue;
11334
+ if (decision.retry) {
11335
+ this.emitEvent({
11336
+ type: "pipeline",
11337
+ agent: agent2._name,
11338
+ status: "failed",
11339
+ stage: "validate",
11340
+ attempt: validateAttempt,
11341
+ maxAttempts: validateMaxAttempts,
11342
+ reason: decision.message
11343
+ });
11344
+ validateRetries++;
11345
+ appendRetryMessages(
11346
+ currentMessages,
11347
+ content,
11348
+ decision.message,
11349
+ response.providerMetadata
11350
+ );
11351
+ pendingRetryReason = "validate";
11352
+ continue;
11353
+ }
11354
+ this.currentSignal?.throwIfAborted();
11204
11355
  }
11205
11356
  throw new ValidationError(
11206
11357
  validated,
@@ -11222,6 +11373,31 @@ ${appendedSchemaText}`;
11222
11373
  }
11223
11374
  throw new MaxTurnsError("ctx.ask()", maxTurns);
11224
11375
  }
11376
+ /**
11377
+ * Resolve the corrective feedback text for one rejected attempt, giving an optional
11378
+ * caller-supplied `retryFeedback` hook the chance to replace it or to stop retrying.
11379
+ *
11380
+ * Called by all three output gates, only while a retry is still permitted (never on the
11381
+ * exhausting attempt) and only after the gate's own event has been emitted, so a hook that
11382
+ * throws or aborts can never erase the evidence that the gate rejected. A non-empty string
11383
+ * replaces `defaultMessage` verbatim; `undefined` / `''` keeps it; `{ retry: false }` asks
11384
+ * the gate to throw its typed terminal error. Hook exceptions propagate — a bug in caller
11385
+ * code is not a model failure and must not be retried into silence, and neither is a
11386
+ * return value outside the contract: an unrecognized shape throws instead of silently
11387
+ * degrading to the default text, which would hide the defect behind extra token spend.
11388
+ */
11389
+ async resolveRetryFeedback(hook, info) {
11390
+ if (!hook) return { retry: true, message: info.defaultMessage };
11391
+ const result = await hook(info, { metadata: this.metadata });
11392
+ if (result === void 0 || result === "") {
11393
+ return { retry: true, message: info.defaultMessage };
11394
+ }
11395
+ if (typeof result === "string") return { retry: true, message: result };
11396
+ if (isRetryAbort(result)) return { retry: false };
11397
+ throw new TypeError(
11398
+ `retryFeedback must return a string, undefined, or { retry: false }; received ${describeValue(result)}.`
11399
+ );
11400
+ }
11225
11401
  /**
11226
11402
  * Push the final assistant message into session history, preserving providerMetadata
11227
11403
  * (e.g., Gemini thought signatures needed for multi-turn reasoning context) and
@@ -11377,6 +11553,57 @@ ${appendedSchemaText}`;
11377
11553
  }
11378
11554
  }
11379
11555
  }
11556
+ /**
11557
+ * Emit a `provider_diagnostic` when the resolved provider reports that it
11558
+ * could not honor the unified `effort` verbatim (P3/R6/R7).
11559
+ *
11560
+ * The adapter only *resolves* — it returns the requested and effective levels
11561
+ * from the same pure resolver its request builder uses. The runtime decides
11562
+ * how to surface that, because only the runtime can see the event bus and
11563
+ * `AxlConfig.diagnostics.silent` (adapters receive `ProviderConfig` only).
11564
+ *
11565
+ * Called once per ask, so a tool loop with N provider round trips still emits
11566
+ * one event; the `console.warn` dedupes once per process per distinct clamp.
11567
+ * A throwing `effortResolution` is a provider bug and propagates.
11568
+ */
11569
+ emitProviderDiagnostics(agent2, options, ctx) {
11570
+ if (!ctx.provider.effortResolution) return;
11571
+ const resolution = ctx.provider.effortResolution({
11572
+ model: ctx.model,
11573
+ effort: options?.effort ?? agent2._config.effort,
11574
+ thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
11575
+ includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
11576
+ providerOptions: ctx.providerOptions
11577
+ });
11578
+ if (!resolution?.clamped) return;
11579
+ if (typeof resolution.effective !== "string" || resolution.effective.length === 0 || typeof resolution.requested !== "string" || resolution.requested.length === 0) {
11580
+ throw new TypeError(
11581
+ `Provider '${ctx.provider.name ?? "unknown"}' returned a malformed EffortResolution for model '${ctx.model}': 'requested' and 'effective' must be non-empty strings when 'clamped' is true. Received ${JSON.stringify(resolution)}.`
11582
+ );
11583
+ }
11584
+ const cause = resolution.cause ?? `effort '${resolution.requested}' is not supported as asked`;
11585
+ this.emitEvent({
11586
+ type: "provider_diagnostic",
11587
+ agent: agent2._name,
11588
+ data: {
11589
+ kind: "effort_clamped",
11590
+ ...ctx.provider.name ? { provider: ctx.provider.name } : {},
11591
+ model: ctx.model,
11592
+ requested: resolution.requested,
11593
+ effective: resolution.effective,
11594
+ cause
11595
+ }
11596
+ });
11597
+ warnDiagnosticOnce(
11598
+ // One warning per distinct clamp, not per ask: the same clamp across 500
11599
+ // asks is one process-level surprise, while a different requested/effective
11600
+ // pair is a genuinely new one.
11601
+ `effort_clamped\0${ctx.provider.name ?? ""}\0${ctx.model}\0${resolution.requested}\0${resolution.effective}`,
11602
+ `effort '${resolution.requested}' was clamped to '${resolution.effective}' on ${ctx.provider.name ? `${ctx.provider.name} ` : ""}${ctx.model} \u2014 ${cause}. A 'provider_diagnostic' event carries the same detail per ask.`,
11603
+ this.config.diagnostics?.silent,
11604
+ "docs/observability.md#provider-diagnostics"
11605
+ );
11606
+ }
11380
11607
  buildToolDefs(agent2, resolvedHandoffs) {
11381
11608
  const defs = [];
11382
11609
  if (agent2._config.tools) {
@@ -12657,7 +12884,8 @@ ${summaryResponse.content}`
12657
12884
  retries: options?.retries,
12658
12885
  metadata: options?.metadata,
12659
12886
  validate: options?.validate,
12660
- validateRetries: options?.validateRetries
12887
+ validateRetries: options?.validateRetries,
12888
+ retryFeedback: options?.retryFeedback
12661
12889
  });
12662
12890
  }
12663
12891
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -12705,7 +12933,8 @@ ${summaryResponse.content}`
12705
12933
  retries: options?.retries,
12706
12934
  metadata: options?.metadata,
12707
12935
  validate: options?.validate,
12708
- validateRetries: options?.validateRetries
12936
+ validateRetries: options?.validateRetries,
12937
+ retryFeedback: options?.retryFeedback
12709
12938
  });
12710
12939
  }
12711
12940
  /** Transcribe recorded audio through the dedicated transcription registry.