@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.js CHANGED
@@ -2118,17 +2118,22 @@ function resolveOpenAIReasoningEffort(model, resolved) {
2118
2118
  }
2119
2119
  return resolved.thinkingDisabled ? clampReasoningEffort(model, "none") : void 0;
2120
2120
  }
2121
- var warnedChatMaxDowngrades = /* @__PURE__ */ new Set();
2122
2121
  function resolveOpenAIChatReasoningEffort(model, resolved) {
2123
2122
  const effort = resolveOpenAIReasoningEffort(model, resolved);
2124
- if (effort !== "max") return effort;
2125
- if (resolved.activeEffort === "max" && !warnedChatMaxDowngrades.has(model)) {
2126
- warnedChatMaxDowngrades.add(model);
2127
- console.warn(
2128
- `[axl] OpenAI Chat Completions does not accept effort "max" for ${model}; using "xhigh". Use the OpenAI Responses provider for native "max".`
2129
- );
2130
- }
2131
- return "xhigh";
2123
+ return effort === "max" ? "xhigh" : effort;
2124
+ }
2125
+ function resolveOpenAIEffortResolution(options, resolveWireEffort, endpoint) {
2126
+ const resolved = resolveThinkingOptions(options);
2127
+ if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
2128
+ const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
2129
+ const effective = resolveWireEffort(model, resolved);
2130
+ if (effective === void 0 || effective === resolved.effort) return void 0;
2131
+ return {
2132
+ requested: resolved.effort,
2133
+ effective,
2134
+ clamped: true,
2135
+ cause: `${endpoint} does not accept reasoning effort '${resolved.effort}' for ${model}; using '${effective}'`
2136
+ };
2132
2137
  }
2133
2138
  var openaiReasoningEmit = (body, resolved, model) => {
2134
2139
  const oSeries = isOSeriesModel(model);
@@ -2153,6 +2158,15 @@ var OpenAIProvider = class extends OpenAICompatibleProvider {
2153
2158
  constructor(options = {}) {
2154
2159
  super({ profile: OPENAI_PROFILE, ...options });
2155
2160
  }
2161
+ /** Report a clamped `effort` — Chat Completions caps `'max'` at `'xhigh'` and
2162
+ * rejects `'none'` before gpt-5.1 (clamped to `'minimal'`). */
2163
+ effortResolution(options) {
2164
+ return resolveOpenAIEffortResolution(
2165
+ options,
2166
+ resolveOpenAIChatReasoningEffort,
2167
+ "OpenAI Chat Completions"
2168
+ );
2169
+ }
2156
2170
  computeCost(model, usage, _reportedCost, context) {
2157
2171
  return usage ? estimateDirectOpenAICost(model, usage, { ...context, baseUrl: this.baseUrl }) : void 0;
2158
2172
  }
@@ -2199,6 +2213,12 @@ var OpenAIResponsesProvider = class {
2199
2213
  inputCapabilities(model) {
2200
2214
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
2201
2215
  }
2216
+ /** Report a clamped `effort`. Responses accepts the native `'max'` tier that
2217
+ * Chat Completions caps, but still clamps `'none'` before gpt-5.1, `'xhigh'`
2218
+ * below gpt-5.2, and everything to `'high'` on gpt-5-pro. */
2219
+ effortResolution(options) {
2220
+ return resolveOpenAIEffortResolution(options, resolveOpenAIReasoningEffort, "OpenAI Responses");
2221
+ }
2202
2222
  validateInput(request) {
2203
2223
  const modelOverride = request.providerOptions?.model;
2204
2224
  const effectiveModel = typeof modelOverride === "string" ? modelOverride : request.model;
@@ -2874,14 +2894,6 @@ function budgetToClaudeEffort(budget) {
2874
2894
  if (budget <= THINKING_BUDGETS.xhigh) return "xhigh";
2875
2895
  return "max";
2876
2896
  }
2877
- var warnedFableDisable = /* @__PURE__ */ new Set();
2878
- function warnFableDisable(model) {
2879
- if (warnedFableDisable.has(model)) return;
2880
- warnedFableDisable.add(model);
2881
- console.warn(
2882
- `[axl] thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'.`
2883
- );
2884
- }
2885
2897
  function resolveClaudeThinking(model, resolved) {
2886
2898
  const capability = resolveClaudeCapability(model);
2887
2899
  if (!capability) return { stripTemperature: false };
@@ -2901,11 +2913,14 @@ function resolveClaudeThinking(model, resolved) {
2901
2913
  }
2902
2914
  if (resolved.thinkingDisabled) {
2903
2915
  if (capability.thinking === "adaptive-always-on") {
2904
- warnFableDisable(model);
2905
2916
  return {
2906
2917
  thinking: { type: "adaptive" },
2907
2918
  outputConfig: { effort: "low" },
2908
- stripTemperature: true
2919
+ stripTemperature: true,
2920
+ clamp: {
2921
+ effective: "low",
2922
+ cause: `thinking cannot be disabled on Anthropic ${model}; using adaptive thinking at effort 'low'`
2923
+ }
2909
2924
  };
2910
2925
  }
2911
2926
  if (activeEffort && capability.disableAt && !isEffortAtOrBelow(activeEffort, capability.disableAt)) {
@@ -2967,7 +2982,16 @@ function resolveClaudeThinking(model, resolved) {
2967
2982
  return {
2968
2983
  thinking: { type: "enabled", budget_tokens: budget },
2969
2984
  manualBudget: budget,
2970
- stripTemperature: true
2985
+ stripTemperature: true,
2986
+ // Legacy models take `budget_tokens`, not an effort level, and have no
2987
+ // `effortLevels` — so `clampAnthropicEffort` folds 'max'/'xhigh' down to
2988
+ // the 'high' tier and the request silently carries that tier's budget.
2989
+ // Report it: an unreported budget-tier downgrade is exactly the silent
2990
+ // clamp this capability exists to expose.
2991
+ clamp: activeEffort === resolved.activeEffort ? void 0 : {
2992
+ effective: activeEffort,
2993
+ 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}`
2994
+ }
2971
2995
  };
2972
2996
  }
2973
2997
  return { stripTemperature: capability.stripTemperature ?? false };
@@ -3078,6 +3102,26 @@ function mergeAnthropicUsage(initial, update) {
3078
3102
  }
3079
3103
  var AnthropicProvider = class {
3080
3104
  name = "anthropic";
3105
+ /** Report a clamped `effort`. Anthropic clamps to each model's supported
3106
+ * `output_config.effort` levels, and models that always think fall back to
3107
+ * adaptive `'low'` when asked to disable thinking. Only reported when the
3108
+ * effective level is what the request body actually carries; an explicit
3109
+ * `thinkingBudget` is documented to override `effort`, so it is not a clamp. */
3110
+ effortResolution(options) {
3111
+ const resolved = resolveThinkingOptions(options);
3112
+ if (resolved.effort === void 0 || resolved.hasBudgetOverride) return void 0;
3113
+ const model = typeof options.providerOptions?.model === "string" ? options.providerOptions.model : options.model;
3114
+ const config = resolveClaudeThinking(model, resolved);
3115
+ const wireEffort = typeof config.outputConfig?.effort === "string" ? config.outputConfig.effort : void 0;
3116
+ const effective = config.clamp?.effective ?? wireEffort;
3117
+ if (effective === void 0 || effective === resolved.effort) return void 0;
3118
+ return {
3119
+ requested: resolved.effort,
3120
+ effective,
3121
+ clamped: true,
3122
+ cause: config.clamp?.cause ?? `Anthropic ${model} does not support effort '${resolved.effort}'; using '${effective}'`
3123
+ };
3124
+ }
3081
3125
  inputCapabilities(model) {
3082
3126
  return model.trim().length > 0 ? { image: { sources: ["url", "bytes", "base64", "provider-file"] } } : {};
3083
3127
  }
@@ -3928,13 +3972,71 @@ function budgetToThinkingLevel(budgetTokens) {
3928
3972
  function minThinkingLevel(model) {
3929
3973
  return GEMINI_3X_MIN_THINKING_LEVEL.get(model) ?? "minimal";
3930
3974
  }
3931
- var _warned3xEffortNone = /* @__PURE__ */ new Set();
3932
- function warnGemini3xEffortNone(model) {
3933
- if (_warned3xEffortNone.has(model)) return;
3934
- _warned3xEffortNone.add(model);
3935
- console.warn(
3936
- `[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.`
3937
- );
3975
+ function resolveGeminiThinking(model, resolved) {
3976
+ const is3x = isGemini3x(model);
3977
+ if (resolved.thinkingDisabled) {
3978
+ const minLevel = minThinkingLevel(model);
3979
+ if (is3x) {
3980
+ return {
3981
+ interactionLevel: minLevel,
3982
+ thinkingLevel: minLevel,
3983
+ thinkingBudget: void 0,
3984
+ attachThoughts: false,
3985
+ // A `thinkingBudget: 0` disable has no unified effort to report against;
3986
+ // only an explicit `effort: 'none'` is a clamp of a requested value.
3987
+ clamp: resolved.effort === "none" ? {
3988
+ effective: minLevel,
3989
+ cause: `Gemini 3.x models cannot disable thinking; using the model minimum thinking level '${minLevel}'`
3990
+ } : void 0
3991
+ };
3992
+ }
3993
+ return {
3994
+ interactionLevel: "minimal",
3995
+ thinkingLevel: void 0,
3996
+ thinkingBudget: 0,
3997
+ attachThoughts: false
3998
+ };
3999
+ }
4000
+ if (resolved.hasBudgetOverride) {
4001
+ const budget = resolved.thinkingBudget;
4002
+ const level = budgetToThinkingLevel(budget);
4003
+ return {
4004
+ interactionLevel: level,
4005
+ thinkingLevel: is3x ? level : void 0,
4006
+ thinkingBudget: is3x ? void 0 : budget,
4007
+ attachThoughts: resolved.includeThoughts
4008
+ };
4009
+ }
4010
+ if (resolved.activeEffort) {
4011
+ const effort = resolved.activeEffort;
4012
+ const budget = THINKING_BUDGETS2[effort] ?? 5e3;
4013
+ if (is3x) {
4014
+ const level = THINKING_LEVELS[effort] ?? "medium";
4015
+ return {
4016
+ interactionLevel: level,
4017
+ thinkingLevel: level,
4018
+ thinkingBudget: void 0,
4019
+ attachThoughts: resolved.includeThoughts,
4020
+ clamp: level === effort ? void 0 : {
4021
+ effective: level,
4022
+ cause: `Gemini 3.x thinking levels cap at 'high'; effort '${effort}' maps to '${level}'`
4023
+ }
4024
+ };
4025
+ }
4026
+ return {
4027
+ interactionLevel: budgetToThinkingLevel(budget),
4028
+ thinkingLevel: void 0,
4029
+ // 2.5 Pro supports a higher max budget (32768) than other 2.5 models (24576).
4030
+ thinkingBudget: effort === "max" && model === "gemini-2.5-pro" ? 32768 : budget,
4031
+ attachThoughts: resolved.includeThoughts
4032
+ };
4033
+ }
4034
+ return {
4035
+ interactionLevel: void 0,
4036
+ thinkingLevel: void 0,
4037
+ thinkingBudget: void 0,
4038
+ attachThoughts: resolved.includeThoughts
4039
+ };
3938
4040
  }
3939
4041
  var GeminiProvider = class {
3940
4042
  name = "google";
@@ -4003,6 +4105,21 @@ var GeminiProvider = class {
4003
4105
  nativeStructuredOutputSupport() {
4004
4106
  return "lossy";
4005
4107
  }
4108
+ /** Report a clamped `effort` (R7a). Gemini 3.x cannot disable thinking and
4109
+ * caps at `'high'`, so `'none'`, `'xhigh'` and `'max'` are not honored
4110
+ * verbatim there. Everything else — including 2.x disabled thinking — is
4111
+ * sent as asked, so there is nothing to report. */
4112
+ effortResolution(options) {
4113
+ const resolved = resolveThinkingOptions(options);
4114
+ if (resolved.effort === void 0) return void 0;
4115
+ const clamp = resolveGeminiThinking(this.requestModel(options), resolved).clamp;
4116
+ return clamp ? {
4117
+ requested: resolved.effort,
4118
+ effective: clamp.effective,
4119
+ clamped: true,
4120
+ cause: clamp.cause
4121
+ } : void 0;
4122
+ }
4006
4123
  baseUrl;
4007
4124
  apiKeySource;
4008
4125
  callCounter = 0;
@@ -4177,12 +4294,9 @@ var GeminiProvider = class {
4177
4294
  generationConfig.tool_choice = typeof options.toolChoice === "string" ? options.toolChoice === "required" ? "any" : options.toolChoice : { allowed_tools: { mode: "any", tools: [options.toolChoice.function.name] } };
4178
4295
  }
4179
4296
  const thinking = resolveThinkingOptions(options);
4180
- if (thinking.thinkingDisabled) {
4181
- generationConfig.thinking_level = isGemini3x(model) ? minThinkingLevel(model) : "minimal";
4182
- } else if (thinking.hasBudgetOverride) {
4183
- generationConfig.thinking_level = budgetToThinkingLevel(thinking.thinkingBudget);
4184
- } else if (thinking.activeEffort) {
4185
- generationConfig.thinking_level = isGemini3x(model) ? THINKING_LEVELS[thinking.activeEffort] ?? "medium" : budgetToThinkingLevel(THINKING_BUDGETS2[thinking.activeEffort] ?? 5e3);
4297
+ const thinkingPlan = resolveGeminiThinking(model, thinking);
4298
+ if (thinkingPlan.interactionLevel !== void 0) {
4299
+ generationConfig.thinking_level = thinkingPlan.interactionLevel;
4186
4300
  }
4187
4301
  if (thinking.includeThoughts) generationConfig.thinking_summaries = "auto";
4188
4302
  if (Object.keys(generationConfig).length) body.generation_config = generationConfig;
@@ -4618,50 +4732,17 @@ var GeminiProvider = class {
4618
4732
  if (Object.keys(generationConfig).length > 0) {
4619
4733
  body.generationConfig = generationConfig;
4620
4734
  }
4621
- const {
4622
- effort,
4623
- thinkingBudget,
4624
- includeThoughts,
4625
- thinkingDisabled,
4626
- activeEffort,
4627
- hasBudgetOverride
4628
- } = resolveThinkingOptions(options);
4629
- if (thinkingDisabled) {
4630
- if (isGemini3x(requestModel)) {
4631
- if (effort === "none") {
4632
- warnGemini3xEffortNone(requestModel);
4633
- }
4634
- generationConfig.thinkingConfig = { thinkingLevel: minThinkingLevel(requestModel) };
4635
- } else {
4636
- generationConfig.thinkingConfig = { thinkingBudget: 0 };
4637
- }
4638
- if (!body.generationConfig) body.generationConfig = generationConfig;
4639
- } else if (hasBudgetOverride) {
4640
- const config = {};
4641
- if (isGemini3x(requestModel)) {
4642
- config.thinkingLevel = budgetToThinkingLevel(thinkingBudget);
4643
- } else {
4644
- config.thinkingBudget = thinkingBudget;
4645
- }
4646
- if (includeThoughts) config.includeThoughts = true;
4647
- generationConfig.thinkingConfig = config;
4648
- if (!body.generationConfig) body.generationConfig = generationConfig;
4649
- } else if (activeEffort) {
4650
- const config = {};
4651
- if (isGemini3x(requestModel)) {
4652
- config.thinkingLevel = THINKING_LEVELS[activeEffort] ?? "medium";
4653
- } else {
4654
- if (activeEffort === "max" && requestModel === "gemini-2.5-pro") {
4655
- config.thinkingBudget = 32768;
4656
- } else {
4657
- config.thinkingBudget = THINKING_BUDGETS2[activeEffort] ?? 5e3;
4658
- }
4659
- }
4660
- if (includeThoughts) config.includeThoughts = true;
4661
- generationConfig.thinkingConfig = config;
4662
- if (!body.generationConfig) body.generationConfig = generationConfig;
4663
- } else if (includeThoughts) {
4664
- generationConfig.thinkingConfig = { includeThoughts: true };
4735
+ const thinkingPlan = resolveGeminiThinking(requestModel, resolveThinkingOptions(options));
4736
+ const thinkingConfig = {};
4737
+ if (thinkingPlan.thinkingLevel !== void 0) {
4738
+ thinkingConfig.thinkingLevel = thinkingPlan.thinkingLevel;
4739
+ }
4740
+ if (thinkingPlan.thinkingBudget !== void 0) {
4741
+ thinkingConfig.thinkingBudget = thinkingPlan.thinkingBudget;
4742
+ }
4743
+ if (thinkingPlan.attachThoughts) thinkingConfig.includeThoughts = true;
4744
+ if (Object.keys(thinkingConfig).length > 0) {
4745
+ generationConfig.thinkingConfig = thinkingConfig;
4665
4746
  if (!body.generationConfig) body.generationConfig = generationConfig;
4666
4747
  }
4667
4748
  if (options.toolChoice !== void 0) {
@@ -7296,6 +7377,9 @@ var AXL_EVENT_TYPES = [
7296
7377
  // structured-output cliffs (oversized appended schema, dropped refinements,
7297
7378
  // streaming disabled, zero-guidance `schemaPrompt:'none'`).
7298
7379
  "schema_diagnostic",
7380
+ // Provider capability diagnostics — the resolved provider could not honor a
7381
+ // portable request knob verbatim (first kind: a clamped `effort`).
7382
+ "provider_diagnostic",
7299
7383
  // Observability
7300
7384
  "log",
7301
7385
  "memory_remember",
@@ -7345,6 +7429,7 @@ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
7345
7429
  "pipeline",
7346
7430
  "verify",
7347
7431
  "schema_diagnostic",
7432
+ "provider_diagnostic",
7348
7433
  "workflow_start",
7349
7434
  "workflow_end",
7350
7435
  "transcription_start",
@@ -8273,6 +8358,10 @@ var LEGACY_REDACTION_RULES = {
8273
8358
  // which is user content or PII (field paths are schema-shape, preserved like
8274
8359
  // `string_delta.path`). Passthrough, as an explicit reviewed decision.
8275
8360
  schema_diagnostic: passthrough,
8361
+ // `provider_diagnostic` carries only provider capability metadata — a provider
8362
+ // name, a model id, and two effort level strings. No application data or user
8363
+ // content ever reaches it. Passthrough, as an explicit reviewed decision.
8364
+ provider_diagnostic: passthrough,
8276
8365
  log: (e) => {
8277
8366
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
8278
8367
  return { ...e, data: walkObjectOneLevel(e.data, LOG_PRESERVE_KEYS) };
@@ -8403,6 +8492,7 @@ var COMMON_V2_REDACTION_RULES = {
8403
8492
  string_delta: reuseLegacyRule("string_delta"),
8404
8493
  verify: reuseLegacyRule("verify"),
8405
8494
  schema_diagnostic: reuseLegacyRule("schema_diagnostic"),
8495
+ provider_diagnostic: reuseLegacyRule("provider_diagnostic"),
8406
8496
  log: reuseLegacyRule("log"),
8407
8497
  memory_remember: reuseLegacyRule("memory_remember"),
8408
8498
  memory_recall: reuseLegacyRule("memory_recall"),
@@ -8498,13 +8588,14 @@ function detectDroppedRefinements(schema) {
8498
8588
  var DOCS_URL = "docs/observability.md#schema-diagnostics";
8499
8589
  var MAX_WARNED_KEYS = 2048;
8500
8590
  var warnedDiagnosticKeys = /* @__PURE__ */ new Set();
8501
- function warnSchemaDiagnosticOnce(key, message, silent) {
8591
+ function warnDiagnosticOnce(key, message, silent, docs = DOCS_URL) {
8502
8592
  if (silent || process.env.AXL_DIAGNOSTICS_SILENT === "true") return;
8503
8593
  if (warnedDiagnosticKeys.has(key)) return;
8504
8594
  if (warnedDiagnosticKeys.size >= MAX_WARNED_KEYS) warnedDiagnosticKeys.clear();
8505
8595
  warnedDiagnosticKeys.add(key);
8506
- console.warn(`[axl] ${message} See ${DOCS_URL}.`);
8596
+ console.warn(`[axl] ${message} See ${docs}.`);
8507
8597
  }
8598
+ var warnSchemaDiagnosticOnce = warnDiagnosticOnce;
8508
8599
 
8509
8600
  // src/config.ts
8510
8601
  function defineConfig(config) {
@@ -9354,7 +9445,22 @@ function appendRetryMessages(messages, content, feedbackMessage, providerMetadat
9354
9445
  content,
9355
9446
  ...providerMetadata ? { providerMetadata } : {}
9356
9447
  });
9357
- messages.push({ role: "system", content: feedbackMessage });
9448
+ messages.push({ role: "user", content: feedbackMessage });
9449
+ }
9450
+ function isRetryAbort(value) {
9451
+ return typeof value === "object" && value !== null && value.retry === false;
9452
+ }
9453
+ function describeValue(value) {
9454
+ if (value === null) return "null";
9455
+ if (Array.isArray(value)) return `an array of length ${value.length}`;
9456
+ if (typeof value === "object") {
9457
+ const keys = Object.keys(value);
9458
+ return keys.length > 0 ? `an object with keys [${keys.join(", ")}]` : "an empty object";
9459
+ }
9460
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
9461
+ return `the ${typeof value} ${JSON.stringify(value)}`;
9462
+ }
9463
+ return `a value of type ${typeof value}`;
9358
9464
  }
9359
9465
  function estimateMessagesTokens(messages) {
9360
9466
  let total = 0;
@@ -10291,6 +10397,7 @@ ${appendedSchemaText}`;
10291
10397
  provider,
10292
10398
  model
10293
10399
  });
10400
+ this.emitProviderDiagnostics(agent2, options, { provider, model, providerOptions });
10294
10401
  if (handoffMessages && handoffMessages.length > 0) {
10295
10402
  const handoffContext = handoffMessages.filter(
10296
10403
  (m) => m.role === "assistant" || m.role === "tool"
@@ -10655,7 +10762,11 @@ ${appendedSchemaText}`;
10655
10762
  retries: options.retries,
10656
10763
  metadata: options.metadata,
10657
10764
  validate: options.validate,
10658
- validateRetries: options.validateRetries
10765
+ validateRetries: options.validateRetries,
10766
+ // This seam serves both callers of a handoff: a plain `ctx.ask()` on an
10767
+ // agent with `handoffs`, and the routed `ctx.delegate()` path, whose
10768
+ // terminal ask is the handoff target. The hook travels like `validate`.
10769
+ retryFeedback: options.retryFeedback
10659
10770
  } : void 0;
10660
10771
  const handoffFn = () => askStorage.run(targetFrame, async () => {
10661
10772
  const delegateOriginal = agent2._delegateOriginalInput;
@@ -10804,9 +10915,10 @@ ${appendedSchemaText}`;
10804
10915
  const attempt = guardrailOutputRetries + 1;
10805
10916
  const maxAttempts = maxGuardrailRetries + 1;
10806
10917
  const onBlock = guardrails.onBlock ?? "throw";
10918
+ const blockReason = outputResult.reason ?? "Output blocked by guardrail";
10807
10919
  let feedbackMessage;
10808
10920
  if (outputResult.block && onBlock === "retry" && guardrailOutputRetries < maxGuardrailRetries) {
10809
- feedbackMessage = `Your previous response was blocked by a safety guardrail: ${outputResult.reason ?? "Output blocked"}. Please provide a different response that complies with the guidelines.`;
10921
+ feedbackMessage = `Your previous response was blocked by a safety guardrail: ${blockReason}. Please provide a different response that complies with the guidelines.`;
10810
10922
  }
10811
10923
  this.emitEvent({
10812
10924
  type: "guardrail",
@@ -10829,31 +10941,43 @@ ${appendedSchemaText}`;
10829
10941
  });
10830
10942
  if (outputResult.block) {
10831
10943
  if (feedbackMessage) {
10832
- this.emitEvent({
10833
- type: "pipeline",
10834
- agent: agent2._name,
10835
- status: "failed",
10944
+ const decision = await this.resolveRetryFeedback(options?.retryFeedback, {
10836
10945
  stage: "guardrail",
10837
- attempt: guardrailOutputRetries + 1,
10838
- maxAttempts: maxGuardrailRetries + 1,
10839
- reason: feedbackMessage
10946
+ attempt,
10947
+ maxAttempts,
10948
+ output: content,
10949
+ reason: blockReason,
10950
+ defaultMessage: feedbackMessage
10840
10951
  });
10841
- guardrailOutputRetries++;
10842
- appendRetryMessages(
10843
- currentMessages,
10844
- content,
10845
- feedbackMessage,
10846
- response.providerMetadata
10847
- );
10848
- pendingRetryReason = "guardrail";
10849
- continue;
10952
+ if (decision.retry) {
10953
+ this.emitEvent({
10954
+ type: "pipeline",
10955
+ agent: agent2._name,
10956
+ status: "failed",
10957
+ stage: "guardrail",
10958
+ attempt,
10959
+ maxAttempts,
10960
+ reason: decision.message
10961
+ });
10962
+ guardrailOutputRetries++;
10963
+ appendRetryMessages(
10964
+ currentMessages,
10965
+ content,
10966
+ decision.message,
10967
+ response.providerMetadata
10968
+ );
10969
+ pendingRetryReason = "guardrail";
10970
+ continue;
10971
+ }
10972
+ this.currentSignal?.throwIfAborted();
10973
+ throw new GuardrailError("output", blockReason);
10850
10974
  }
10851
10975
  if (typeof onBlock === "function") {
10852
- return onBlock(outputResult.reason ?? "Output blocked by guardrail", {
10976
+ return onBlock(blockReason, {
10853
10977
  metadata: this.metadata
10854
10978
  });
10855
10979
  }
10856
- throw new GuardrailError("output", outputResult.reason ?? "Output blocked by guardrail");
10980
+ throw new GuardrailError("output", blockReason);
10857
10981
  }
10858
10982
  }
10859
10983
  let validated = void 0;
@@ -10873,7 +10997,7 @@ ${appendedSchemaText}`;
10873
10997
  schemaErr = err;
10874
10998
  schemaReason = err instanceof Error ? err.message : String(err);
10875
10999
  if (schemaRetries < maxSchemaRetries) {
10876
- schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Please fix and try again.`;
11000
+ schemaFeedback = `Your response was not valid JSON or did not match the required schema: ${schemaReason}. Return a corrected response that matches the required schema.`;
10877
11001
  }
10878
11002
  }
10879
11003
  this.emitEvent({
@@ -10895,24 +11019,36 @@ ${appendedSchemaText}`;
10895
11019
  });
10896
11020
  if (!schemaValid) {
10897
11021
  if (schemaFeedback) {
10898
- this.emitEvent({
10899
- type: "pipeline",
10900
- agent: agent2._name,
10901
- status: "failed",
11022
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
10902
11023
  stage: "schema",
10903
- attempt: schemaRetries + 1,
10904
- maxAttempts: (options.retries ?? 3) + 1,
10905
- reason: schemaFeedback
11024
+ attempt: schemaAttempt,
11025
+ maxAttempts: schemaMaxAttempts,
11026
+ output: content,
11027
+ reason: schemaReason ?? "Schema parse failed",
11028
+ ...schemaErr !== void 0 ? { error: schemaErr } : {},
11029
+ defaultMessage: schemaFeedback
10906
11030
  });
10907
- schemaRetries++;
10908
- appendRetryMessages(
10909
- currentMessages,
10910
- content,
10911
- schemaFeedback,
10912
- response.providerMetadata
10913
- );
10914
- pendingRetryReason = "schema";
10915
- continue;
11031
+ if (decision.retry) {
11032
+ this.emitEvent({
11033
+ type: "pipeline",
11034
+ agent: agent2._name,
11035
+ status: "failed",
11036
+ stage: "schema",
11037
+ attempt: schemaAttempt,
11038
+ maxAttempts: schemaMaxAttempts,
11039
+ reason: decision.message
11040
+ });
11041
+ schemaRetries++;
11042
+ appendRetryMessages(
11043
+ currentMessages,
11044
+ content,
11045
+ decision.message,
11046
+ response.providerMetadata
11047
+ );
11048
+ pendingRetryReason = "schema";
11049
+ continue;
11050
+ }
11051
+ this.currentSignal?.throwIfAborted();
10916
11052
  }
10917
11053
  const zodErr = schemaErr instanceof ZodError2 ? schemaErr : new ZodError2([
10918
11054
  {
@@ -10926,12 +11062,14 @@ ${appendedSchemaText}`;
10926
11062
  }
10927
11063
  if (options?.schema && options.validate) {
10928
11064
  let validateResult;
11065
+ let validateErr;
10929
11066
  try {
10930
11067
  validateResult = await options.validate(validated, {
10931
11068
  metadata: this.metadata
10932
11069
  });
10933
11070
  } catch (err) {
10934
11071
  rethrowEventStreamOverflow(err);
11072
+ validateErr = err;
10935
11073
  const reason = err instanceof Error ? err.message : String(err);
10936
11074
  validateResult = { valid: false, reason: `Validator error: ${reason}` };
10937
11075
  }
@@ -10940,7 +11078,7 @@ ${appendedSchemaText}`;
10940
11078
  const validateMaxAttempts = maxValidateRetries + 1;
10941
11079
  let validateFeedback;
10942
11080
  if (!validateResult.valid && validateRetries < maxValidateRetries) {
10943
- validateFeedback = `Your response parsed correctly but failed validation: ${validateResult.reason ?? "Validation failed"}. Previous attempts are visible above. Please fix and try again.`;
11081
+ validateFeedback = `Your previous response failed validation: ${validateResult.reason ?? "Validation failed"}. Return a corrected response that satisfies the required schema and this rule.`;
10944
11082
  }
10945
11083
  this.emitEvent({
10946
11084
  type: "validate",
@@ -10961,24 +11099,37 @@ ${appendedSchemaText}`;
10961
11099
  });
10962
11100
  if (!validateResult.valid) {
10963
11101
  if (validateFeedback) {
10964
- this.emitEvent({
10965
- type: "pipeline",
10966
- agent: agent2._name,
10967
- status: "failed",
11102
+ const decision = await this.resolveRetryFeedback(options.retryFeedback, {
10968
11103
  stage: "validate",
10969
- attempt: validateRetries + 1,
10970
- maxAttempts: (options.validateRetries ?? 2) + 1,
10971
- reason: validateFeedback
11104
+ attempt: validateAttempt,
11105
+ maxAttempts: validateMaxAttempts,
11106
+ output: content,
11107
+ parsed: validated,
11108
+ reason: validateResult.reason ?? "Validation failed",
11109
+ ...validateErr !== void 0 ? { error: validateErr } : {},
11110
+ defaultMessage: validateFeedback
10972
11111
  });
10973
- validateRetries++;
10974
- appendRetryMessages(
10975
- currentMessages,
10976
- content,
10977
- validateFeedback,
10978
- response.providerMetadata
10979
- );
10980
- pendingRetryReason = "validate";
10981
- continue;
11112
+ if (decision.retry) {
11113
+ this.emitEvent({
11114
+ type: "pipeline",
11115
+ agent: agent2._name,
11116
+ status: "failed",
11117
+ stage: "validate",
11118
+ attempt: validateAttempt,
11119
+ maxAttempts: validateMaxAttempts,
11120
+ reason: decision.message
11121
+ });
11122
+ validateRetries++;
11123
+ appendRetryMessages(
11124
+ currentMessages,
11125
+ content,
11126
+ decision.message,
11127
+ response.providerMetadata
11128
+ );
11129
+ pendingRetryReason = "validate";
11130
+ continue;
11131
+ }
11132
+ this.currentSignal?.throwIfAborted();
10982
11133
  }
10983
11134
  throw new ValidationError(
10984
11135
  validated,
@@ -11000,6 +11151,31 @@ ${appendedSchemaText}`;
11000
11151
  }
11001
11152
  throw new MaxTurnsError("ctx.ask()", maxTurns);
11002
11153
  }
11154
+ /**
11155
+ * Resolve the corrective feedback text for one rejected attempt, giving an optional
11156
+ * caller-supplied `retryFeedback` hook the chance to replace it or to stop retrying.
11157
+ *
11158
+ * Called by all three output gates, only while a retry is still permitted (never on the
11159
+ * exhausting attempt) and only after the gate's own event has been emitted, so a hook that
11160
+ * throws or aborts can never erase the evidence that the gate rejected. A non-empty string
11161
+ * replaces `defaultMessage` verbatim; `undefined` / `''` keeps it; `{ retry: false }` asks
11162
+ * the gate to throw its typed terminal error. Hook exceptions propagate — a bug in caller
11163
+ * code is not a model failure and must not be retried into silence, and neither is a
11164
+ * return value outside the contract: an unrecognized shape throws instead of silently
11165
+ * degrading to the default text, which would hide the defect behind extra token spend.
11166
+ */
11167
+ async resolveRetryFeedback(hook, info) {
11168
+ if (!hook) return { retry: true, message: info.defaultMessage };
11169
+ const result = await hook(info, { metadata: this.metadata });
11170
+ if (result === void 0 || result === "") {
11171
+ return { retry: true, message: info.defaultMessage };
11172
+ }
11173
+ if (typeof result === "string") return { retry: true, message: result };
11174
+ if (isRetryAbort(result)) return { retry: false };
11175
+ throw new TypeError(
11176
+ `retryFeedback must return a string, undefined, or { retry: false }; received ${describeValue(result)}.`
11177
+ );
11178
+ }
11003
11179
  /**
11004
11180
  * Push the final assistant message into session history, preserving providerMetadata
11005
11181
  * (e.g., Gemini thought signatures needed for multi-turn reasoning context) and
@@ -11155,6 +11331,57 @@ ${appendedSchemaText}`;
11155
11331
  }
11156
11332
  }
11157
11333
  }
11334
+ /**
11335
+ * Emit a `provider_diagnostic` when the resolved provider reports that it
11336
+ * could not honor the unified `effort` verbatim (P3/R6/R7).
11337
+ *
11338
+ * The adapter only *resolves* — it returns the requested and effective levels
11339
+ * from the same pure resolver its request builder uses. The runtime decides
11340
+ * how to surface that, because only the runtime can see the event bus and
11341
+ * `AxlConfig.diagnostics.silent` (adapters receive `ProviderConfig` only).
11342
+ *
11343
+ * Called once per ask, so a tool loop with N provider round trips still emits
11344
+ * one event; the `console.warn` dedupes once per process per distinct clamp.
11345
+ * A throwing `effortResolution` is a provider bug and propagates.
11346
+ */
11347
+ emitProviderDiagnostics(agent2, options, ctx) {
11348
+ if (!ctx.provider.effortResolution) return;
11349
+ const resolution = ctx.provider.effortResolution({
11350
+ model: ctx.model,
11351
+ effort: options?.effort ?? agent2._config.effort,
11352
+ thinkingBudget: options?.thinkingBudget ?? agent2._config.thinkingBudget,
11353
+ includeThoughts: options?.includeThoughts ?? agent2._config.includeThoughts,
11354
+ providerOptions: ctx.providerOptions
11355
+ });
11356
+ if (!resolution?.clamped) return;
11357
+ if (typeof resolution.effective !== "string" || resolution.effective.length === 0 || typeof resolution.requested !== "string" || resolution.requested.length === 0) {
11358
+ throw new TypeError(
11359
+ `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)}.`
11360
+ );
11361
+ }
11362
+ const cause = resolution.cause ?? `effort '${resolution.requested}' is not supported as asked`;
11363
+ this.emitEvent({
11364
+ type: "provider_diagnostic",
11365
+ agent: agent2._name,
11366
+ data: {
11367
+ kind: "effort_clamped",
11368
+ ...ctx.provider.name ? { provider: ctx.provider.name } : {},
11369
+ model: ctx.model,
11370
+ requested: resolution.requested,
11371
+ effective: resolution.effective,
11372
+ cause
11373
+ }
11374
+ });
11375
+ warnDiagnosticOnce(
11376
+ // One warning per distinct clamp, not per ask: the same clamp across 500
11377
+ // asks is one process-level surprise, while a different requested/effective
11378
+ // pair is a genuinely new one.
11379
+ `effort_clamped\0${ctx.provider.name ?? ""}\0${ctx.model}\0${resolution.requested}\0${resolution.effective}`,
11380
+ `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.`,
11381
+ this.config.diagnostics?.silent,
11382
+ "docs/observability.md#provider-diagnostics"
11383
+ );
11384
+ }
11158
11385
  buildToolDefs(agent2, resolvedHandoffs) {
11159
11386
  const defs = [];
11160
11387
  if (agent2._config.tools) {
@@ -12435,7 +12662,8 @@ ${summaryResponse.content}`
12435
12662
  retries: options?.retries,
12436
12663
  metadata: options?.metadata,
12437
12664
  validate: options?.validate,
12438
- validateRetries: options?.validateRetries
12665
+ validateRetries: options?.validateRetries,
12666
+ retryFeedback: options?.retryFeedback
12439
12667
  });
12440
12668
  }
12441
12669
  const resolveCtx = options?.metadata ? { metadata: { ...this.metadata, ...options.metadata } } : { metadata: this.metadata };
@@ -12483,7 +12711,8 @@ ${summaryResponse.content}`
12483
12711
  retries: options?.retries,
12484
12712
  metadata: options?.metadata,
12485
12713
  validate: options?.validate,
12486
- validateRetries: options?.validateRetries
12714
+ validateRetries: options?.validateRetries,
12715
+ retryFeedback: options?.retryFeedback
12487
12716
  });
12488
12717
  }
12489
12718
  /** Transcribe recorded audio through the dedicated transcription registry.