@namzu/sdk 60.0.0 → 60.1.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 (130) hide show
  1. package/CHANGELOG.md +18 -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 +16 -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 +12 -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 +7 -1
  19. package/dist/contracts/session/api.d.ts.map +1 -1
  20. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  21. package/dist/manager/session/turn-recorder.js +1 -0
  22. package/dist/manager/session/turn-recorder.js.map +1 -1
  23. package/dist/provider/collect-chat-completion.d.ts.map +1 -1
  24. package/dist/provider/collect-chat-completion.js +4 -0
  25. package/dist/provider/collect-chat-completion.js.map +1 -1
  26. package/dist/provider/fallback.d.ts.map +1 -1
  27. package/dist/provider/fallback.js +11 -0
  28. package/dist/provider/fallback.js.map +1 -1
  29. package/dist/provider/idle-timeout.d.ts.map +1 -1
  30. package/dist/provider/idle-timeout.js +5 -0
  31. package/dist/provider/idle-timeout.js.map +1 -1
  32. package/dist/provider/mock.d.ts +2 -0
  33. package/dist/provider/mock.d.ts.map +1 -1
  34. package/dist/provider/mock.js +16 -8
  35. package/dist/provider/mock.js.map +1 -1
  36. package/dist/provider/model-window-suffix.d.ts.map +1 -1
  37. package/dist/provider/model-window-suffix.js +1 -0
  38. package/dist/provider/model-window-suffix.js.map +1 -1
  39. package/dist/provider/retry.d.ts.map +1 -1
  40. package/dist/provider/retry.js +5 -0
  41. package/dist/provider/retry.js.map +1 -1
  42. package/dist/runtime/query/index.d.ts +14 -0
  43. package/dist/runtime/query/index.d.ts.map +1 -1
  44. package/dist/runtime/query/index.js +1 -0
  45. package/dist/runtime/query/index.js.map +1 -1
  46. package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
  47. package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
  48. package/dist/runtime/query/iteration/effort-clamp.js +42 -0
  49. package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
  50. package/dist/runtime/query/iteration/index.d.ts +34 -0
  51. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  52. package/dist/runtime/query/iteration/index.js +199 -6
  53. package/dist/runtime/query/iteration/index.js.map +1 -1
  54. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  55. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  56. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  57. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  58. package/dist/runtime/query/iteration/stream-turn.js +6 -0
  59. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  60. package/dist/store/memory/disk.d.ts.map +1 -1
  61. package/dist/store/memory/disk.js +1 -1
  62. package/dist/store/memory/disk.js.map +1 -1
  63. package/dist/store/memory/markdown.d.ts.map +1 -1
  64. package/dist/store/memory/markdown.js +1 -1
  65. package/dist/store/memory/markdown.js.map +1 -1
  66. package/dist/store/memory/operation-lock.d.ts +20 -4
  67. package/dist/store/memory/operation-lock.d.ts.map +1 -1
  68. package/dist/store/memory/operation-lock.js +235 -58
  69. package/dist/store/memory/operation-lock.js.map +1 -1
  70. package/dist/turn/reporter.d.ts.map +1 -1
  71. package/dist/turn/reporter.js +11 -0
  72. package/dist/turn/reporter.js.map +1 -1
  73. package/dist/types/agent/query.d.ts +2 -0
  74. package/dist/types/agent/query.d.ts.map +1 -1
  75. package/dist/types/message/index.d.ts +1 -1
  76. package/dist/types/message/index.d.ts.map +1 -1
  77. package/dist/types/message/index.js +1 -0
  78. package/dist/types/message/index.js.map +1 -1
  79. package/dist/types/provider/chat.d.ts +19 -0
  80. package/dist/types/provider/chat.d.ts.map +1 -1
  81. package/dist/types/provider/config.d.ts +7 -0
  82. package/dist/types/provider/config.d.ts.map +1 -1
  83. package/dist/types/provider/index.d.ts +1 -1
  84. package/dist/types/provider/index.d.ts.map +1 -1
  85. package/dist/types/provider/interface.d.ts +15 -0
  86. package/dist/types/provider/interface.d.ts.map +1 -1
  87. package/dist/types/provider/stream.d.ts +6 -0
  88. package/dist/types/provider/stream.d.ts.map +1 -1
  89. package/dist/types/session/events.d.ts +34 -0
  90. package/dist/types/session/events.d.ts.map +1 -1
  91. package/dist/types/session/events.js.map +1 -1
  92. package/dist/types/session/records.d.ts +34 -34
  93. package/dist/types/session/records.d.ts.map +1 -1
  94. package/dist/types/session/records.js +3 -0
  95. package/dist/types/session/records.js.map +1 -1
  96. package/dist/types/session/stop-reason.d.ts +11 -0
  97. package/dist/types/session/stop-reason.d.ts.map +1 -1
  98. package/package.json +1 -1
  99. package/src/agents/QueryAgent.ts +1 -0
  100. package/src/bridge/a2a/mapper.ts +19 -0
  101. package/src/bridge/acp/update.ts +6 -1
  102. package/src/bridge/sse/mapper.ts +13 -0
  103. package/src/constants/continuation.ts +13 -0
  104. package/src/contracts/session/api.ts +6 -0
  105. package/src/manager/session/turn-recorder.ts +1 -0
  106. package/src/provider/collect-chat-completion.ts +3 -0
  107. package/src/provider/fallback.ts +17 -0
  108. package/src/provider/idle-timeout.ts +9 -0
  109. package/src/provider/mock.ts +19 -9
  110. package/src/provider/model-window-suffix.ts +1 -0
  111. package/src/provider/retry.ts +9 -0
  112. package/src/runtime/query/index.ts +15 -0
  113. package/src/runtime/query/iteration/effort-clamp.ts +46 -0
  114. package/src/runtime/query/iteration/index.ts +260 -6
  115. package/src/runtime/query/iteration/phases/context.ts +2 -0
  116. package/src/runtime/query/iteration/stream-turn.ts +5 -0
  117. package/src/store/memory/disk.ts +1 -0
  118. package/src/store/memory/markdown.ts +1 -0
  119. package/src/store/memory/operation-lock.ts +288 -57
  120. package/src/turn/reporter.ts +12 -0
  121. package/src/types/agent/query.ts +2 -0
  122. package/src/types/message/index.ts +5 -3
  123. package/src/types/provider/chat.ts +20 -0
  124. package/src/types/provider/config.ts +7 -0
  125. package/src/types/provider/index.ts +1 -0
  126. package/src/types/provider/interface.ts +20 -0
  127. package/src/types/provider/stream.ts +6 -0
  128. package/src/types/session/events.ts +65 -5
  129. package/src/types/session/records.ts +3 -0
  130. package/src/types/session/stop-reason.ts +11 -0
@@ -7,6 +7,8 @@ import { estimateMessageTokens } from '../../../compaction/token-estimate.js'
7
7
  import {
8
8
  AUTO_CONTINUATION_USER_MESSAGE,
9
9
  REASONING_CUT_USER_MESSAGE,
10
+ REFUSED_REPLY_CALL_RESULT,
11
+ REFUSED_REPLY_RETRY_MESSAGE,
10
12
  } from '../../../constants/continuation.js'
11
13
  import { CHARS_PER_TOKEN } from '../../../constants/limits.js'
12
14
  import {
@@ -37,6 +39,7 @@ import {
37
39
  createAssistantMessage,
38
40
  createRuntimeContextMessage,
39
41
  createSystemMessage,
42
+ createToolMessage,
40
43
  } from '../../../types/message/index.js'
41
44
  import { classifyProviderError, readOutputOverflow } from '../../../types/provider/errors.js'
42
45
  import type { ChatCompletionResponse } from '../../../types/provider/index.js'
@@ -71,6 +74,7 @@ import {
71
74
  projectRequestRichContent,
72
75
  } from '../request-rich-content.js'
73
76
  import { formatSteeringNote, isOperatorUserMessage } from '../steering.js'
77
+ import { effortForModel } from './effort-clamp.js'
74
78
  import { parseNativeCandidate } from './native-output.js'
75
79
  import {
76
80
  drainArrivedTaskCompletions,
@@ -225,7 +229,10 @@ export class IterationOrchestrator {
225
229
  * serving from the member it last moved to, so that member, not the head,
226
230
  * is the one whose reading of images counts.
227
231
  */
228
- private servingModel(model: string): { readonly providerId: string; readonly model: string } {
232
+ private servingModel(model: string): {
233
+ readonly providerId: string
234
+ readonly model: string
235
+ } {
229
236
  const serving = this.ctx.servingMember?.()
230
237
  return {
231
238
  providerId: serving?.providerId ?? this.ctx.provider.id,
@@ -291,6 +298,16 @@ export class IterationOrchestrator {
291
298
  private stopDeferredForOutstandingWork = false
292
299
  /** Nudges sent since the model last finished a call without hitting its output cap. */
293
300
  private outputContinuations = 0
301
+ /**
302
+ * The model a refused request moved to. Once set it replaces the model the
303
+ * step asked for, for the rest of the turn: the request that was refused
304
+ * would only be refused again.
305
+ */
306
+ private refusalPinnedModel: string | undefined
307
+ /** The request has already been re-run on another model once this turn. */
308
+ private refusalFallbackUsed = false
309
+ /** The request has already been re-run on the same model once this turn. */
310
+ private refusalRetryUsed = false
294
311
  /**
295
312
  * `turn_end` (and, for a child, `subagent_stop`) fired when the model
296
313
  * finished and no hook asked for more. `finalizeTurn` then does not fire
@@ -319,7 +336,11 @@ export class IterationOrchestrator {
319
336
  const feedback: string[] = []
320
337
  const ask = async (
321
338
  event: 'turn_end' | 'subagent_stop',
322
- extra: { parentSessionId?: SessionId; parentTurnId?: TurnId; agentType?: string },
339
+ extra: {
340
+ parentSessionId?: SessionId
341
+ parentTurnId?: TurnId
342
+ agentType?: string
343
+ },
323
344
  ): Promise<void> => {
324
345
  const results = await manager.executeHooks(
325
346
  event,
@@ -795,7 +816,7 @@ export class IterationOrchestrator {
795
816
  // Preparation inference belongs to the turn, not the main-model step.
796
817
  usageBefore = { ...recorder.tokenUsage }
797
818
  costBefore = { ...recorder.costInfo }
798
- stepModel = step.model ?? model
819
+ stepModel = this.refusalPinnedModel ?? step.model ?? model
799
820
  await selectContextModel(shaping, stepModel)
800
821
  // Preserve post-compaction preparation/recall semantics. A changed
801
822
  // model needs a second check against its own window; never replay
@@ -824,7 +845,7 @@ export class IterationOrchestrator {
824
845
  // still call any of them by name.
825
846
  this.ctx.toolExecutor.setStepAllowedTools(stepAllowedTools)
826
847
  const enforceToolInputSchema = enforcedModelInputToolNames(this.ctx.tools, llmTools)
827
- stepModel = step.model ?? model
848
+ stepModel = this.refusalPinnedModel ?? step.model ?? model
828
849
 
829
850
  // The closing directive is appended LAST, after every piece of
830
851
  // request-only context below, so it is the final thing the model
@@ -1051,6 +1072,25 @@ export class IterationOrchestrator {
1051
1072
  model: requestedMember.model ?? stepModel,
1052
1073
  chainIndex: requestedMember.index,
1053
1074
  }
1075
+ // The turn's effort was chosen for the turn's own model. A request
1076
+ // moved to another model (a refusal fallback, a live pick) carries
1077
+ // the nearest level that model offers, for this request only.
1078
+ let requestEffort = turnConfig.effort
1079
+ if (requestEffort && stepModel !== model) {
1080
+ requestEffort = effortForModel(
1081
+ this.ctx.provider,
1082
+ stepModel,
1083
+ requestEffort,
1084
+ turnConfig.thinking,
1085
+ )
1086
+ if (requestEffort !== turnConfig.effort) {
1087
+ this.ctx.log.debug('Effort lowered to a level this model offers', {
1088
+ 'namzu.runtime.model': stepModel,
1089
+ 'namzu.runtime.effort_requested': turnConfig.effort,
1090
+ 'namzu.runtime.effort_used': requestEffort,
1091
+ })
1092
+ }
1093
+ }
1054
1094
  const operatorInputAtDispatch = this.latestUserMessage
1055
1095
  const latestReviewUserMessage =
1056
1096
  (this.ctx.reviewAnswer || this.ctx.structuredOutput?.review) && operatorInputAtDispatch
@@ -1092,7 +1132,7 @@ export class IterationOrchestrator {
1092
1132
  outputCapOverride ?? step.maxResponseTokens ?? turnConfig.maxResponseTokens,
1093
1133
  cacheControl: { type: 'auto' },
1094
1134
  ...(turnConfig.thinking ? { thinking: turnConfig.thinking } : {}),
1095
- ...(turnConfig.effort ? { effort: turnConfig.effort } : {}),
1135
+ ...(requestEffort ? { effort: requestEffort } : {}),
1096
1136
  ...(!forceFinalize && turnConfig.webSearch
1097
1137
  ? { webSearch: turnConfig.webSearch }
1098
1138
  : {}),
@@ -1247,6 +1287,34 @@ export class IterationOrchestrator {
1247
1287
  ...contextFigures,
1248
1288
  })
1249
1289
 
1290
+ // A reply the provider declined to give is its own case. It is not
1291
+ // an empty completion: asking again with the same history on the
1292
+ // same model is refused again, at the full price of the prompt.
1293
+ // Recovered here, before the reply can enter the history.
1294
+ if (response.finishReason === 'content_filter') {
1295
+ iterSpan.setStatus({ code: SpanStatusCode.OK })
1296
+ if (iterationActivity) {
1297
+ this.ctx.activityStore.complete(iterationActivity.id, {
1298
+ content: response.message.content,
1299
+ hasToolCalls: false,
1300
+ })
1301
+ }
1302
+ const outcome = yield* this.recoverFromRefusal({
1303
+ response,
1304
+ messageId,
1305
+ stepModel,
1306
+ servedBy,
1307
+ forceFinalize,
1308
+ limitStopReason: forceFinalize ? guardResult.stopReason : undefined,
1309
+ iterationNum,
1310
+ stepStartedAt,
1311
+ usageBefore,
1312
+ costBefore,
1313
+ })
1314
+ if (outcome === 'retry') continue
1315
+ break
1316
+ }
1317
+
1250
1318
  // Durable reasoning and its adapter-private replay envelope ride
1251
1319
  // with the turn they belong to. Trimming therefore removes both;
1252
1320
  // retaining them gives the target adapter enough evidence to
@@ -1681,7 +1749,10 @@ export class IterationOrchestrator {
1681
1749
  [NAMZU.ITERATION]: iterationNum,
1682
1750
  'namzu.runtime.finish_reason': response.finishReason,
1683
1751
  })
1684
- closingStopReason = await this.requestFinalResponse(model, 'end_turn')
1752
+ closingStopReason = await this.requestFinalResponse(
1753
+ this.refusalPinnedModel ?? model,
1754
+ 'end_turn',
1755
+ )
1685
1756
  yield* this.ctx.drainPending()
1686
1757
  }
1687
1758
 
@@ -2873,6 +2944,179 @@ export class IterationOrchestrator {
2873
2944
  })
2874
2945
  }
2875
2946
 
2947
+ /**
2948
+ * A reply the provider declined to give.
2949
+ *
2950
+ * The refused reply never enters the history sent next, except on the
2951
+ * same-model retry, where the stopped partial reply stays so the model can
2952
+ * see what to avoid. At most one automatic retry of each kind per turn:
2953
+ *
2954
+ * 1. Another model of the same provider, when the provider names one and
2955
+ * the host has not turned `refusalFallback` off. The model is kept for
2956
+ * the rest of the turn and announced in a `model_refusal` event.
2957
+ * 2. The same model once more, only when the refused reply had partial
2958
+ * text or tool calls: unfinished calls get an error result and the
2959
+ * model is told the reply was stopped.
2960
+ * 3. Otherwise the turn settles `refused`.
2961
+ */
2962
+ private async *recoverFromRefusal(input: {
2963
+ response: ChatCompletionResponse
2964
+ messageId: MessageId
2965
+ stepModel: string
2966
+ servedBy: StepProvenance
2967
+ forceFinalize: boolean
2968
+ /** The limit that asked for this closing step; it stays the reason the turn ended. */
2969
+ limitStopReason?: StopReason | undefined
2970
+ iterationNum: number
2971
+ stepStartedAt: number
2972
+ usageBefore: TokenUsage
2973
+ costBefore: CostInfo
2974
+ }): AsyncGenerator<SessionEvent, 'retry' | 'settled'> {
2975
+ const { recorder } = this.ctx
2976
+ const { response, servedBy, iterationNum } = input
2977
+ const refusal = response.refusal ?? {}
2978
+ const toolCalls = response.message.toolCalls ?? []
2979
+ const hasPartial =
2980
+ (response.message.content !== null && response.message.content.length > 0) ||
2981
+ toolCalls.length > 0
2982
+
2983
+ this.ctx.log.warn('The model declined to answer', {
2984
+ [NAMZU.TURN_ID]: recorder.turnId,
2985
+ [NAMZU.ITERATION]: iterationNum,
2986
+ 'namzu.runtime.model': servedBy.model,
2987
+ 'namzu.runtime.refusal_category': refusal.category,
2988
+ 'namzu.runtime.has_partial_output': hasPartial,
2989
+ })
2990
+
2991
+ const announce = async (outcome: 'fallback' | 'retry' | 'declined', fallbackModel?: string) => {
2992
+ await this.ctx.emitEvent({
2993
+ type: 'model_refusal',
2994
+ turnId: recorder.turnId,
2995
+ iteration: iterationNum,
2996
+ model: servedBy.model,
2997
+ providerId: servedBy.providerId,
2998
+ outcome,
2999
+ ...(fallbackModel !== undefined ? { fallbackModel } : {}),
3000
+ ...(refusal.category ? { category: refusal.category } : {}),
3001
+ ...(refusal.explanation ? { explanation: refusal.explanation } : {}),
3002
+ })
3003
+ }
3004
+ const closeIteration = async () => {
3005
+ this.recordStep({
3006
+ stepNumber: iterationNum,
3007
+ model: input.stepModel,
3008
+ servedBy,
3009
+ messageId: input.messageId,
3010
+ response,
3011
+ toolResults: [],
3012
+ toolExecutionMs: 0,
3013
+ startedAt: input.stepStartedAt,
3014
+ usageBefore: input.usageBefore,
3015
+ costBefore: input.costBefore,
3016
+ })
3017
+ await this.ctx.emitEvent({
3018
+ type: 'iteration_completed',
3019
+ turnId: recorder.turnId,
3020
+ iteration: iterationNum,
3021
+ hasToolCalls: false,
3022
+ })
3023
+ }
3024
+
3025
+ if (this.ctx.abortController.signal.aborted) {
3026
+ recorder.setStopReason('cancelled')
3027
+ recorder.markCancelled()
3028
+ await closeIteration()
3029
+ yield* this.ctx.drainPending()
3030
+ return 'settled'
3031
+ }
3032
+
3033
+ if (!input.forceFinalize && !this.refusalFallbackUsed && this.ctx.refusalFallback !== 'off') {
3034
+ const next = await this.refusalFallbackModel(input.stepModel, refusal)
3035
+ if (next !== undefined) {
3036
+ this.refusalFallbackUsed = true
3037
+ this.refusalPinnedModel = next
3038
+ await announce('fallback', next)
3039
+ await closeIteration()
3040
+ yield* this.ctx.drainPending()
3041
+ return 'retry'
3042
+ }
3043
+ }
3044
+
3045
+ if (!input.forceFinalize && !this.refusalRetryUsed && hasPartial) {
3046
+ this.refusalRetryUsed = true
3047
+ recorder.pushMessage(
3048
+ withStreamedMessageIdentity(
3049
+ createAssistantMessage(
3050
+ response.message.content,
3051
+ toolCalls.length > 0 ? toolCalls : undefined,
3052
+ response.message.reasoning,
3053
+ response.message.citations,
3054
+ {
3055
+ type: 'model',
3056
+ ...servedBy,
3057
+ ...(response.message.replayState !== undefined
3058
+ ? { replayState: response.message.replayState }
3059
+ : {}),
3060
+ },
3061
+ response.message.textParts,
3062
+ ),
3063
+ input.messageId,
3064
+ ),
3065
+ )
3066
+ for (const call of toolCalls) {
3067
+ recorder.pushMessage(createToolMessage(REFUSED_REPLY_CALL_RESULT, call.id, true))
3068
+ }
3069
+ recorder.pushMessage(
3070
+ createRuntimeContextMessage(REFUSED_REPLY_RETRY_MESSAGE, 'refusal-retry'),
3071
+ )
3072
+ await announce('retry')
3073
+ await closeIteration()
3074
+ yield* this.ctx.drainPending()
3075
+ return 'retry'
3076
+ }
3077
+
3078
+ recorder.setLastError(
3079
+ `${servedBy.model} declined to answer this message${refusal.explanation ? `: ${refusal.explanation}` : '.'}`,
3080
+ )
3081
+ // A closing step that a limit asked for keeps that limit as the reason
3082
+ // the turn ended; the refusal is still announced.
3083
+ recorder.setStopReason(input.limitStopReason ?? 'refused')
3084
+ await announce('declined')
3085
+ await closeIteration()
3086
+ yield* this.ctx.drainPending()
3087
+ return 'settled'
3088
+ }
3089
+
3090
+ /**
3091
+ * Another model to try after `model` declined, or `undefined` when there is
3092
+ * none. Only the provider can say: it names its own recommendation or its
3093
+ * own declared order, checked against what this credential can serve. A
3094
+ * chain member declared with a model of its own cannot be swapped, because
3095
+ * the chain would put its own model back on the request.
3096
+ */
3097
+ private async refusalFallbackModel(
3098
+ model: string,
3099
+ refusal: NonNullable<ChatCompletionResponse['refusal']>,
3100
+ ): Promise<string | undefined> {
3101
+ if (this.ctx.servingMember?.()?.model !== undefined) return undefined
3102
+ const pick = this.ctx.provider.refusalFallbackFor
3103
+ if (!pick) return undefined
3104
+ try {
3105
+ const next = await pick.call(
3106
+ this.ctx.provider,
3107
+ model,
3108
+ refusal,
3109
+ this.ctx.abortController.signal,
3110
+ )
3111
+ return next !== undefined && next.length > 0 && next !== model ? next : undefined
3112
+ } catch (err) {
3113
+ this.ctx.log.warn('The provider could not name a model to retry a refusal on', {
3114
+ 'exception.message': toErrorMessage(err),
3115
+ })
3116
+ return undefined
3117
+ }
3118
+ }
3119
+
2876
3120
  private async requestFinalResponse(
2877
3121
  model: string,
2878
3122
  reason: StopReason,
@@ -2980,6 +3224,16 @@ export class IterationOrchestrator {
2980
3224
  model: servedBy.model,
2981
3225
  })
2982
3226
 
3227
+ // A closing request the provider declined has nothing to add, and its
3228
+ // empty reply must not be stored or sent again.
3229
+ if (response.finishReason === 'content_filter') {
3230
+ this.ctx.log.warn('The closing response was declined by the model', {
3231
+ [NAMZU.TURN_ID]: this.ctx.recorder.turnId,
3232
+ 'namzu.runtime.model': servedBy.model,
3233
+ })
3234
+ return undefined
3235
+ }
3236
+
2983
3237
  const finalMessageId = generateMessageId()
2984
3238
  const assistantMsg = withStreamedMessageIdentity(
2985
3239
  createAssistantMessage(
@@ -224,6 +224,8 @@ export interface IterationContext {
224
224
  readonly compactionConfig?: CompactionConfig
225
225
  /** See `QueryParams.maxOutputContinuations`. */
226
226
  readonly maxOutputContinuations?: number
227
+ /** See `QueryParams.refusalFallback`. */
228
+ readonly refusalFallback?: 'auto' | 'off'
227
229
 
228
230
  /**
229
231
  * What the driver said this model's context window is, resolved once.
@@ -284,6 +284,8 @@ export async function* streamProviderTurn(
284
284
  let reportedFinishReason: ChatCompletionResponse['finishReason'] | undefined
285
285
  // Which limit a 'length' finish reached, when it was the context window.
286
286
  let finishDetail: ChatCompletionResponse['finishDetail']
287
+ // What the provider said about a refusal, kept with the finish that carried it.
288
+ let refusal: ChatCompletionResponse['refusal']
287
289
  let usage: ChatCompletionResponse['usage'] = {
288
290
  promptTokens: 0,
289
291
  completionTokens: 0,
@@ -416,6 +418,7 @@ export async function* streamProviderTurn(
416
418
  finishReason = 'stop'
417
419
  reportedFinishReason = undefined
418
420
  finishDetail = undefined
421
+ refusal = undefined
419
422
  streamedLength = 0
420
423
  lastOutputCall = undefined
421
424
  usage = {
@@ -766,6 +769,7 @@ export async function* streamProviderTurn(
766
769
  reportedFinishReason = chunk.finishReason
767
770
  finishDetail = chunk.finishReason === 'length' ? chunk.finishDetail : undefined
768
771
  }
772
+ if (chunk.refusal) refusal = chunk.refusal
769
773
  // Merge (per-field max), not last-write-wins: a late usage frame that
770
774
  // omits input/cache tokens must not zero the counts seen earlier in the
771
775
  // stream, which would under-report this turn's accumulated usage.
@@ -1061,6 +1065,7 @@ export async function* streamProviderTurn(
1061
1065
  },
1062
1066
  finishReason: effectiveFinishReason,
1063
1067
  ...(effectiveFinishReason === 'length' && finishDetail ? { finishDetail } : {}),
1068
+ ...(effectiveFinishReason === 'content_filter' && refusal ? { refusal } : {}),
1064
1069
  usage,
1065
1070
  }
1066
1071
 
@@ -261,6 +261,7 @@ export class DiskMemoryStore implements ConditionalMemoryStore {
261
261
  const release = await acquireMemoryOperationLock(
262
262
  join(dirname(location.indexPath), 'operation.lock'),
263
263
  this.lockTimeoutMs,
264
+ { logger: this.log },
264
265
  )
265
266
  try {
266
267
  await this.reloadIndex(location)
@@ -418,6 +418,7 @@ export class MarkdownMemoryStore implements ConditionalMemoryStore {
418
418
  const release = await acquireMemoryOperationLock(
419
419
  join(dir, 'operation.lock'),
420
420
  this.lockTimeoutMs,
421
+ { logger: this.log },
421
422
  )
422
423
  try {
423
424
  const { loaded, superseded } = await this.load(dir, mode.importing ?? false)