@namzu/sdk 60.0.0 → 60.2.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.
Files changed (203) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +1 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  6. package/dist/bridge/a2a/mapper.js +19 -0
  7. package/dist/bridge/a2a/mapper.js.map +1 -1
  8. package/dist/bridge/acp/update.d.ts.map +1 -1
  9. package/dist/bridge/acp/update.js +1 -0
  10. package/dist/bridge/acp/update.js.map +1 -1
  11. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  12. package/dist/bridge/sse/mapper.js +28 -0
  13. package/dist/bridge/sse/mapper.js.map +1 -1
  14. package/dist/constants/continuation.d.ts +9 -0
  15. package/dist/constants/continuation.d.ts.map +1 -1
  16. package/dist/constants/continuation.js +9 -0
  17. package/dist/constants/continuation.js.map +1 -1
  18. package/dist/contracts/session/api.d.ts +8 -2
  19. package/dist/contracts/session/api.d.ts.map +1 -1
  20. package/dist/manager/session/turn-recorder.d.ts +7 -1
  21. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  22. package/dist/manager/session/turn-recorder.js +20 -0
  23. package/dist/manager/session/turn-recorder.js.map +1 -1
  24. package/dist/peers/dir.d.ts.map +1 -1
  25. package/dist/peers/dir.js +3 -0
  26. package/dist/peers/dir.js.map +1 -1
  27. package/dist/provider/collect-chat-completion.d.ts.map +1 -1
  28. package/dist/provider/collect-chat-completion.js +4 -0
  29. package/dist/provider/collect-chat-completion.js.map +1 -1
  30. package/dist/provider/fallback.d.ts.map +1 -1
  31. package/dist/provider/fallback.js +11 -0
  32. package/dist/provider/fallback.js.map +1 -1
  33. package/dist/provider/idle-timeout.d.ts.map +1 -1
  34. package/dist/provider/idle-timeout.js +5 -0
  35. package/dist/provider/idle-timeout.js.map +1 -1
  36. package/dist/provider/mock.d.ts +2 -0
  37. package/dist/provider/mock.d.ts.map +1 -1
  38. package/dist/provider/mock.js +16 -8
  39. package/dist/provider/mock.js.map +1 -1
  40. package/dist/provider/model-window-suffix.d.ts.map +1 -1
  41. package/dist/provider/model-window-suffix.js +1 -0
  42. package/dist/provider/model-window-suffix.js.map +1 -1
  43. package/dist/provider/retry.d.ts.map +1 -1
  44. package/dist/provider/retry.js +5 -0
  45. package/dist/provider/retry.js.map +1 -1
  46. package/dist/public-runtime.d.ts +1 -0
  47. package/dist/public-runtime.d.ts.map +1 -1
  48. package/dist/public-runtime.js +1 -0
  49. package/dist/public-runtime.js.map +1 -1
  50. package/dist/runtime/query/events.d.ts.map +1 -1
  51. package/dist/runtime/query/events.js +17 -0
  52. package/dist/runtime/query/events.js.map +1 -1
  53. package/dist/runtime/query/executor.d.ts +23 -0
  54. package/dist/runtime/query/executor.d.ts.map +1 -1
  55. package/dist/runtime/query/executor.js +88 -9
  56. package/dist/runtime/query/executor.js.map +1 -1
  57. package/dist/runtime/query/index.d.ts +14 -0
  58. package/dist/runtime/query/index.d.ts.map +1 -1
  59. package/dist/runtime/query/index.js +14 -0
  60. package/dist/runtime/query/index.js.map +1 -1
  61. package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
  62. package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
  63. package/dist/runtime/query/iteration/effort-clamp.js +42 -0
  64. package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
  65. package/dist/runtime/query/iteration/index.d.ts +34 -0
  66. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  67. package/dist/runtime/query/iteration/index.js +199 -6
  68. package/dist/runtime/query/iteration/index.js.map +1 -1
  69. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  70. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  71. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  72. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  73. package/dist/runtime/query/iteration/phases/tool-review.js +6 -0
  74. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  75. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  76. package/dist/runtime/query/iteration/stream-turn.js +6 -0
  77. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  78. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  79. package/dist/runtime/query/review-policy.js +12 -2
  80. package/dist/runtime/query/review-policy.js.map +1 -1
  81. package/dist/runtime/query/tooling.d.ts +2 -0
  82. package/dist/runtime/query/tooling.d.ts.map +1 -1
  83. package/dist/runtime/query/tooling.js +1 -0
  84. package/dist/runtime/query/tooling.js.map +1 -1
  85. package/dist/sandbox/provider/local.d.ts.map +1 -1
  86. package/dist/sandbox/provider/local.js +6 -5
  87. package/dist/sandbox/provider/local.js.map +1 -1
  88. package/dist/session/paths.d.ts +15 -22
  89. package/dist/session/paths.d.ts.map +1 -1
  90. package/dist/session/paths.js +19 -46
  91. package/dist/session/paths.js.map +1 -1
  92. package/dist/store/memory/disk.d.ts.map +1 -1
  93. package/dist/store/memory/disk.js +1 -1
  94. package/dist/store/memory/disk.js.map +1 -1
  95. package/dist/store/memory/markdown.d.ts.map +1 -1
  96. package/dist/store/memory/markdown.js +1 -1
  97. package/dist/store/memory/markdown.js.map +1 -1
  98. package/dist/store/memory/operation-lock.d.ts +20 -4
  99. package/dist/store/memory/operation-lock.d.ts.map +1 -1
  100. package/dist/store/memory/operation-lock.js +235 -58
  101. package/dist/store/memory/operation-lock.js.map +1 -1
  102. package/dist/store/session-log/core.d.ts +43 -0
  103. package/dist/store/session-log/core.d.ts.map +1 -1
  104. package/dist/store/session-log/core.js +190 -3
  105. package/dist/store/session-log/core.js.map +1 -1
  106. package/dist/store/session-log/disk.d.ts +1 -1
  107. package/dist/store/session-log/disk.d.ts.map +1 -1
  108. package/dist/store/session-log/disk.js +1 -0
  109. package/dist/store/session-log/disk.js.map +1 -1
  110. package/dist/store/session-log/index.d.ts +1 -1
  111. package/dist/store/session-log/index.d.ts.map +1 -1
  112. package/dist/store/session-log/index.js +1 -1
  113. package/dist/store/session-log/index.js.map +1 -1
  114. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -1
  115. package/dist/tools/builtins/bash-cwd.js +2 -3
  116. package/dist/tools/builtins/bash-cwd.js.map +1 -1
  117. package/dist/tools/builtins/read-media.js +3 -3
  118. package/dist/tools/builtins/read-media.js.map +1 -1
  119. package/dist/turn/reporter.d.ts.map +1 -1
  120. package/dist/turn/reporter.js +13 -0
  121. package/dist/turn/reporter.js.map +1 -1
  122. package/dist/types/agent/query.d.ts +2 -0
  123. package/dist/types/agent/query.d.ts.map +1 -1
  124. package/dist/types/hitl/index.d.ts +8 -0
  125. package/dist/types/hitl/index.d.ts.map +1 -1
  126. package/dist/types/hitl/index.js.map +1 -1
  127. package/dist/types/message/index.d.ts +1 -1
  128. package/dist/types/message/index.d.ts.map +1 -1
  129. package/dist/types/message/index.js +1 -0
  130. package/dist/types/message/index.js.map +1 -1
  131. package/dist/types/provider/chat.d.ts +19 -0
  132. package/dist/types/provider/chat.d.ts.map +1 -1
  133. package/dist/types/provider/config.d.ts +7 -0
  134. package/dist/types/provider/config.d.ts.map +1 -1
  135. package/dist/types/provider/index.d.ts +1 -1
  136. package/dist/types/provider/index.d.ts.map +1 -1
  137. package/dist/types/provider/interface.d.ts +15 -0
  138. package/dist/types/provider/interface.d.ts.map +1 -1
  139. package/dist/types/provider/stream.d.ts +6 -0
  140. package/dist/types/provider/stream.d.ts.map +1 -1
  141. package/dist/types/session/events.d.ts +72 -2
  142. package/dist/types/session/events.d.ts.map +1 -1
  143. package/dist/types/session/events.js +4 -0
  144. package/dist/types/session/events.js.map +1 -1
  145. package/dist/types/session/records.d.ts +34 -34
  146. package/dist/types/session/records.d.ts.map +1 -1
  147. package/dist/types/session/records.js +5 -0
  148. package/dist/types/session/records.js.map +1 -1
  149. package/dist/types/session/stop-reason.d.ts +11 -0
  150. package/dist/types/session/stop-reason.d.ts.map +1 -1
  151. package/dist/utils/temp.d.ts +82 -0
  152. package/dist/utils/temp.d.ts.map +1 -0
  153. package/dist/utils/temp.js +196 -0
  154. package/dist/utils/temp.js.map +1 -0
  155. package/package.json +1 -1
  156. package/src/agents/QueryAgent.ts +1 -0
  157. package/src/bridge/a2a/mapper.ts +22 -0
  158. package/src/bridge/acp/update.ts +6 -1
  159. package/src/bridge/sse/mapper.ts +29 -0
  160. package/src/constants/continuation.ts +13 -0
  161. package/src/contracts/session/api.ts +8 -0
  162. package/src/manager/session/turn-recorder.ts +22 -0
  163. package/src/peers/dir.ts +3 -0
  164. package/src/provider/collect-chat-completion.ts +3 -0
  165. package/src/provider/fallback.ts +17 -0
  166. package/src/provider/idle-timeout.ts +9 -0
  167. package/src/provider/mock.ts +19 -9
  168. package/src/provider/model-window-suffix.ts +1 -0
  169. package/src/provider/retry.ts +9 -0
  170. package/src/public-runtime.ts +13 -0
  171. package/src/runtime/query/events.ts +19 -0
  172. package/src/runtime/query/executor.ts +92 -8
  173. package/src/runtime/query/index.ts +27 -0
  174. package/src/runtime/query/iteration/effort-clamp.ts +46 -0
  175. package/src/runtime/query/iteration/index.ts +260 -6
  176. package/src/runtime/query/iteration/phases/context.ts +2 -0
  177. package/src/runtime/query/iteration/phases/tool-review.ts +6 -0
  178. package/src/runtime/query/iteration/stream-turn.ts +5 -0
  179. package/src/runtime/query/review-policy.ts +13 -2
  180. package/src/runtime/query/tooling.ts +3 -0
  181. package/src/sandbox/provider/local.ts +8 -5
  182. package/src/session/paths.ts +32 -57
  183. package/src/store/memory/disk.ts +1 -0
  184. package/src/store/memory/markdown.ts +1 -0
  185. package/src/store/memory/operation-lock.ts +288 -57
  186. package/src/store/session-log/core.ts +235 -3
  187. package/src/store/session-log/disk.ts +7 -1
  188. package/src/store/session-log/index.ts +4 -0
  189. package/src/tools/builtins/bash-cwd.ts +2 -3
  190. package/src/tools/builtins/read-media.ts +3 -3
  191. package/src/turn/reporter.ts +14 -0
  192. package/src/types/agent/query.ts +2 -0
  193. package/src/types/hitl/index.ts +8 -0
  194. package/src/types/message/index.ts +5 -3
  195. package/src/types/provider/chat.ts +20 -0
  196. package/src/types/provider/config.ts +7 -0
  197. package/src/types/provider/index.ts +1 -0
  198. package/src/types/provider/interface.ts +20 -0
  199. package/src/types/provider/stream.ts +6 -0
  200. package/src/types/session/events.ts +106 -6
  201. package/src/types/session/records.ts +5 -0
  202. package/src/types/session/stop-reason.ts +11 -0
  203. package/src/utils/temp.ts +250 -0
@@ -1,7 +1,7 @@
1
1
  import { SpanStatusCode } from '@opentelemetry/api';
2
2
  import { extractFromAssistantMessage, extractFromUserMessage, } from '../../../compaction/extractor.js';
3
3
  import { estimateMessageTokens } from '../../../compaction/token-estimate.js';
4
- import { AUTO_CONTINUATION_USER_MESSAGE, REASONING_CUT_USER_MESSAGE, } from '../../../constants/continuation.js';
4
+ import { AUTO_CONTINUATION_USER_MESSAGE, REASONING_CUT_USER_MESSAGE, REFUSED_REPLY_CALL_RESULT, REFUSED_REPLY_RETRY_MESSAGE, } from '../../../constants/continuation.js';
5
5
  import { CHARS_PER_TOKEN } from '../../../constants/limits.js';
6
6
  import { DEFAULT_STRUCTURED_OUTPUT_RETRIES, STRUCTURED_OUTPUT_REPROMPT, } from '../../../constants/tools/index.js';
7
7
  import { withStreamedMessageIdentity } from '../../../manager/session/streamed-message-identity.js';
@@ -14,7 +14,7 @@ import { GENAI, NAMZU, agentIterationSpanName, parentContext, } from '../../../t
14
14
  import { getTracer } from '../../../telemetry/runtime-accessors.js';
15
15
  import { STRUCTURED_OUTPUT_TOOL_NAME } from '../../../tools/builtins/structuredOutput.js';
16
16
  import { NamzuError } from '../../../types/errors/index.js';
17
- import { createAssistantMessage, createRuntimeContextMessage, createSystemMessage, } from '../../../types/message/index.js';
17
+ import { createAssistantMessage, createRuntimeContextMessage, createSystemMessage, createToolMessage, } from '../../../types/message/index.js';
18
18
  import { classifyProviderError, readOutputOverflow } from '../../../types/provider/errors.js';
19
19
  import { toErrorMessage } from '../../../utils/error.js';
20
20
  import { stableDigest } from '../../../utils/hash.js';
@@ -28,6 +28,7 @@ import { applyLifecycleHookResults } from '../plugin-hooks.js';
28
28
  import { diffRequestContext, snapshotRequestContext, } from '../request-context.js';
29
29
  import { DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES, markProviderRejectedImage, projectRequestRichContent, } from '../request-rich-content.js';
30
30
  import { formatSteeringNote, isOperatorUserMessage } from '../steering.js';
31
+ import { effortForModel } from './effort-clamp.js';
31
32
  import { parseNativeCandidate } from './native-output.js';
32
33
  import { drainArrivedTaskCompletions, holdForOutstandingWork, settleOutstandingWork, } from './outstanding-work.js';
33
34
  import { runAdvisoryPhase } from './phases/advisory.js';
@@ -191,6 +192,16 @@ export class IterationOrchestrator {
191
192
  stopDeferredForOutstandingWork = false;
192
193
  /** Nudges sent since the model last finished a call without hitting its output cap. */
193
194
  outputContinuations = 0;
195
+ /**
196
+ * The model a refused request moved to. Once set it replaces the model the
197
+ * step asked for, for the rest of the turn: the request that was refused
198
+ * would only be refused again.
199
+ */
200
+ refusalPinnedModel;
201
+ /** The request has already been re-run on another model once this turn. */
202
+ refusalFallbackUsed = false;
203
+ /** The request has already been re-run on the same model once this turn. */
204
+ refusalRetryUsed = false;
194
205
  /**
195
206
  * `turn_end` (and, for a child, `subagent_stop`) fired when the model
196
207
  * finished and no hook asked for more. `finalizeTurn` then does not fire
@@ -637,7 +648,7 @@ export class IterationOrchestrator {
637
648
  // Preparation inference belongs to the turn, not the main-model step.
638
649
  usageBefore = { ...recorder.tokenUsage };
639
650
  costBefore = { ...recorder.costInfo };
640
- stepModel = step.model ?? model;
651
+ stepModel = this.refusalPinnedModel ?? step.model ?? model;
641
652
  await selectContextModel(shaping, stepModel);
642
653
  // Preserve post-compaction preparation/recall semantics. A changed
643
654
  // model needs a second check against its own window; never replay
@@ -664,7 +675,7 @@ export class IterationOrchestrator {
664
675
  // still call any of them by name.
665
676
  this.ctx.toolExecutor.setStepAllowedTools(stepAllowedTools);
666
677
  const enforceToolInputSchema = enforcedModelInputToolNames(this.ctx.tools, llmTools);
667
- stepModel = step.model ?? model;
678
+ stepModel = this.refusalPinnedModel ?? step.model ?? model;
668
679
  // The closing directive is appended LAST, after every piece of
669
680
  // request-only context below, so it is the final thing the model
670
681
  // reads on a forced-final step.
@@ -869,6 +880,20 @@ export class IterationOrchestrator {
869
880
  model: requestedMember.model ?? stepModel,
870
881
  chainIndex: requestedMember.index,
871
882
  };
883
+ // The turn's effort was chosen for the turn's own model. A request
884
+ // moved to another model (a refusal fallback, a live pick) carries
885
+ // the nearest level that model offers, for this request only.
886
+ let requestEffort = turnConfig.effort;
887
+ if (requestEffort && stepModel !== model) {
888
+ requestEffort = effortForModel(this.ctx.provider, stepModel, requestEffort, turnConfig.thinking);
889
+ if (requestEffort !== turnConfig.effort) {
890
+ this.ctx.log.debug('Effort lowered to a level this model offers', {
891
+ 'namzu.runtime.model': stepModel,
892
+ 'namzu.runtime.effort_requested': turnConfig.effort,
893
+ 'namzu.runtime.effort_used': requestEffort,
894
+ });
895
+ }
896
+ }
872
897
  const operatorInputAtDispatch = this.latestUserMessage;
873
898
  const latestReviewUserMessage = (this.ctx.reviewAnswer || this.ctx.structuredOutput?.review) && operatorInputAtDispatch
874
899
  ? structuredClone(operatorInputAtDispatch)
@@ -905,7 +930,7 @@ export class IterationOrchestrator {
905
930
  maxTokens: outputCapOverride ?? step.maxResponseTokens ?? turnConfig.maxResponseTokens,
906
931
  cacheControl: { type: 'auto' },
907
932
  ...(turnConfig.thinking ? { thinking: turnConfig.thinking } : {}),
908
- ...(turnConfig.effort ? { effort: turnConfig.effort } : {}),
933
+ ...(requestEffort ? { effort: requestEffort } : {}),
909
934
  ...(!forceFinalize && turnConfig.webSearch
910
935
  ? { webSearch: turnConfig.webSearch }
911
936
  : {}),
@@ -1032,6 +1057,34 @@ export class IterationOrchestrator {
1032
1057
  : {}),
1033
1058
  ...contextFigures,
1034
1059
  });
1060
+ // A reply the provider declined to give is its own case. It is not
1061
+ // an empty completion: asking again with the same history on the
1062
+ // same model is refused again, at the full price of the prompt.
1063
+ // Recovered here, before the reply can enter the history.
1064
+ if (response.finishReason === 'content_filter') {
1065
+ iterSpan.setStatus({ code: SpanStatusCode.OK });
1066
+ if (iterationActivity) {
1067
+ this.ctx.activityStore.complete(iterationActivity.id, {
1068
+ content: response.message.content,
1069
+ hasToolCalls: false,
1070
+ });
1071
+ }
1072
+ const outcome = yield* this.recoverFromRefusal({
1073
+ response,
1074
+ messageId,
1075
+ stepModel,
1076
+ servedBy,
1077
+ forceFinalize,
1078
+ limitStopReason: forceFinalize ? guardResult.stopReason : undefined,
1079
+ iterationNum,
1080
+ stepStartedAt,
1081
+ usageBefore,
1082
+ costBefore,
1083
+ });
1084
+ if (outcome === 'retry')
1085
+ continue;
1086
+ break;
1087
+ }
1035
1088
  // Durable reasoning and its adapter-private replay envelope ride
1036
1089
  // with the turn they belong to. Trimming therefore removes both;
1037
1090
  // retaining them gives the target adapter enough evidence to
@@ -1396,7 +1449,7 @@ export class IterationOrchestrator {
1396
1449
  [NAMZU.ITERATION]: iterationNum,
1397
1450
  'namzu.runtime.finish_reason': response.finishReason,
1398
1451
  });
1399
- closingStopReason = await this.requestFinalResponse(model, 'end_turn');
1452
+ closingStopReason = await this.requestFinalResponse(this.refusalPinnedModel ?? model, 'end_turn');
1400
1453
  yield* this.ctx.drainPending();
1401
1454
  }
1402
1455
  await this.ctx.emitEvent({
@@ -2460,6 +2513,137 @@ export class IterationOrchestrator {
2460
2513
  providerRejectedImagesSuppressed: repaired.count,
2461
2514
  });
2462
2515
  }
2516
+ /**
2517
+ * A reply the provider declined to give.
2518
+ *
2519
+ * The refused reply never enters the history sent next, except on the
2520
+ * same-model retry, where the stopped partial reply stays so the model can
2521
+ * see what to avoid. At most one automatic retry of each kind per turn:
2522
+ *
2523
+ * 1. Another model of the same provider, when the provider names one and
2524
+ * the host has not turned `refusalFallback` off. The model is kept for
2525
+ * the rest of the turn and announced in a `model_refusal` event.
2526
+ * 2. The same model once more, only when the refused reply had partial
2527
+ * text or tool calls: unfinished calls get an error result and the
2528
+ * model is told the reply was stopped.
2529
+ * 3. Otherwise the turn settles `refused`.
2530
+ */
2531
+ async *recoverFromRefusal(input) {
2532
+ const { recorder } = this.ctx;
2533
+ const { response, servedBy, iterationNum } = input;
2534
+ const refusal = response.refusal ?? {};
2535
+ const toolCalls = response.message.toolCalls ?? [];
2536
+ const hasPartial = (response.message.content !== null && response.message.content.length > 0) ||
2537
+ toolCalls.length > 0;
2538
+ this.ctx.log.warn('The model declined to answer', {
2539
+ [NAMZU.TURN_ID]: recorder.turnId,
2540
+ [NAMZU.ITERATION]: iterationNum,
2541
+ 'namzu.runtime.model': servedBy.model,
2542
+ 'namzu.runtime.refusal_category': refusal.category,
2543
+ 'namzu.runtime.has_partial_output': hasPartial,
2544
+ });
2545
+ const announce = async (outcome, fallbackModel) => {
2546
+ await this.ctx.emitEvent({
2547
+ type: 'model_refusal',
2548
+ turnId: recorder.turnId,
2549
+ iteration: iterationNum,
2550
+ model: servedBy.model,
2551
+ providerId: servedBy.providerId,
2552
+ outcome,
2553
+ ...(fallbackModel !== undefined ? { fallbackModel } : {}),
2554
+ ...(refusal.category ? { category: refusal.category } : {}),
2555
+ ...(refusal.explanation ? { explanation: refusal.explanation } : {}),
2556
+ });
2557
+ };
2558
+ const closeIteration = async () => {
2559
+ this.recordStep({
2560
+ stepNumber: iterationNum,
2561
+ model: input.stepModel,
2562
+ servedBy,
2563
+ messageId: input.messageId,
2564
+ response,
2565
+ toolResults: [],
2566
+ toolExecutionMs: 0,
2567
+ startedAt: input.stepStartedAt,
2568
+ usageBefore: input.usageBefore,
2569
+ costBefore: input.costBefore,
2570
+ });
2571
+ await this.ctx.emitEvent({
2572
+ type: 'iteration_completed',
2573
+ turnId: recorder.turnId,
2574
+ iteration: iterationNum,
2575
+ hasToolCalls: false,
2576
+ });
2577
+ };
2578
+ if (this.ctx.abortController.signal.aborted) {
2579
+ recorder.setStopReason('cancelled');
2580
+ recorder.markCancelled();
2581
+ await closeIteration();
2582
+ yield* this.ctx.drainPending();
2583
+ return 'settled';
2584
+ }
2585
+ if (!input.forceFinalize && !this.refusalFallbackUsed && this.ctx.refusalFallback !== 'off') {
2586
+ const next = await this.refusalFallbackModel(input.stepModel, refusal);
2587
+ if (next !== undefined) {
2588
+ this.refusalFallbackUsed = true;
2589
+ this.refusalPinnedModel = next;
2590
+ await announce('fallback', next);
2591
+ await closeIteration();
2592
+ yield* this.ctx.drainPending();
2593
+ return 'retry';
2594
+ }
2595
+ }
2596
+ if (!input.forceFinalize && !this.refusalRetryUsed && hasPartial) {
2597
+ this.refusalRetryUsed = true;
2598
+ recorder.pushMessage(withStreamedMessageIdentity(createAssistantMessage(response.message.content, toolCalls.length > 0 ? toolCalls : undefined, response.message.reasoning, response.message.citations, {
2599
+ type: 'model',
2600
+ ...servedBy,
2601
+ ...(response.message.replayState !== undefined
2602
+ ? { replayState: response.message.replayState }
2603
+ : {}),
2604
+ }, response.message.textParts), input.messageId));
2605
+ for (const call of toolCalls) {
2606
+ recorder.pushMessage(createToolMessage(REFUSED_REPLY_CALL_RESULT, call.id, true));
2607
+ }
2608
+ recorder.pushMessage(createRuntimeContextMessage(REFUSED_REPLY_RETRY_MESSAGE, 'refusal-retry'));
2609
+ await announce('retry');
2610
+ await closeIteration();
2611
+ yield* this.ctx.drainPending();
2612
+ return 'retry';
2613
+ }
2614
+ recorder.setLastError(`${servedBy.model} declined to answer this message${refusal.explanation ? `: ${refusal.explanation}` : '.'}`);
2615
+ // A closing step that a limit asked for keeps that limit as the reason
2616
+ // the turn ended; the refusal is still announced.
2617
+ recorder.setStopReason(input.limitStopReason ?? 'refused');
2618
+ await announce('declined');
2619
+ await closeIteration();
2620
+ yield* this.ctx.drainPending();
2621
+ return 'settled';
2622
+ }
2623
+ /**
2624
+ * Another model to try after `model` declined, or `undefined` when there is
2625
+ * none. Only the provider can say: it names its own recommendation or its
2626
+ * own declared order, checked against what this credential can serve. A
2627
+ * chain member declared with a model of its own cannot be swapped, because
2628
+ * the chain would put its own model back on the request.
2629
+ */
2630
+ async refusalFallbackModel(model, refusal) {
2631
+ if (this.ctx.servingMember?.()?.model !== undefined)
2632
+ return undefined;
2633
+ const pick = this.ctx.provider.refusalFallbackFor;
2634
+ if (!pick)
2635
+ return undefined;
2636
+ try {
2637
+ const next = await pick.call(this.ctx.provider, model, refusal, this.ctx.abortController.signal);
2638
+ return next !== undefined && next.length > 0 && next !== model ? next : undefined;
2639
+ }
2640
+ catch (err) {
2641
+ this.ctx.log.warn('The provider could not name a model to retry a refusal on', {
2642
+ 'exception.message': toErrorMessage(err),
2643
+ });
2644
+ return undefined;
2645
+ }
2646
+ }
2463
2647
  async requestFinalResponse(model, reason) {
2464
2648
  if (this.ctx.structuredOutput?.mode === 'native')
2465
2649
  return 'structured_output_failed';
@@ -2537,6 +2721,15 @@ export class IterationOrchestrator {
2537
2721
  providerId: servedBy.providerId,
2538
2722
  model: servedBy.model,
2539
2723
  });
2724
+ // A closing request the provider declined has nothing to add, and its
2725
+ // empty reply must not be stored or sent again.
2726
+ if (response.finishReason === 'content_filter') {
2727
+ this.ctx.log.warn('The closing response was declined by the model', {
2728
+ [NAMZU.TURN_ID]: this.ctx.recorder.turnId,
2729
+ 'namzu.runtime.model': servedBy.model,
2730
+ });
2731
+ return undefined;
2732
+ }
2540
2733
  const finalMessageId = generateMessageId();
2541
2734
  const assistantMsg = withStreamedMessageIdentity(createAssistantMessage(response.message.content, undefined, response.message.reasoning, response.message.citations, {
2542
2735
  type: 'model',