@namzu/sdk 52.0.0 → 52.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 (157) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/dist/compaction/verifier.d.ts.map +1 -1
  4. package/dist/compaction/verifier.js +57 -17
  5. package/dist/compaction/verifier.js.map +1 -1
  6. package/dist/connector/tools/adapter.d.ts +2 -2
  7. package/dist/contracts/a2a.d.ts +94 -94
  8. package/dist/manager/resident/learning.d.ts +6 -6
  9. package/dist/manager/resident/store.d.ts +4 -4
  10. package/dist/pals/activity/subscription-schema.d.ts +48 -48
  11. package/dist/pals/communication/ingress-schema.d.ts +192 -192
  12. package/dist/pals/communication/schema.d.ts +18 -18
  13. package/dist/peers/protocol.d.ts +36 -36
  14. package/dist/provider/capabilities.d.ts +9 -0
  15. package/dist/provider/capabilities.d.ts.map +1 -1
  16. package/dist/provider/capabilities.js +15 -0
  17. package/dist/provider/capabilities.js.map +1 -1
  18. package/dist/provider/errors.d.ts.map +1 -1
  19. package/dist/provider/errors.js +13 -0
  20. package/dist/provider/errors.js.map +1 -1
  21. package/dist/provider/fallback.d.ts.map +1 -1
  22. package/dist/provider/fallback.js +10 -0
  23. package/dist/provider/fallback.js.map +1 -1
  24. package/dist/provider/idle-timeout.d.ts.map +1 -1
  25. package/dist/provider/idle-timeout.js +5 -0
  26. package/dist/provider/idle-timeout.js.map +1 -1
  27. package/dist/provider/image-input-refusal.d.ts +9 -0
  28. package/dist/provider/image-input-refusal.d.ts.map +1 -0
  29. package/dist/provider/image-input-refusal.js +54 -0
  30. package/dist/provider/image-input-refusal.js.map +1 -0
  31. package/dist/provider/index.d.ts +2 -1
  32. package/dist/provider/index.d.ts.map +1 -1
  33. package/dist/provider/index.js +2 -1
  34. package/dist/provider/index.js.map +1 -1
  35. package/dist/provider/retry.d.ts +7 -2
  36. package/dist/provider/retry.d.ts.map +1 -1
  37. package/dist/provider/retry.js +38 -4
  38. package/dist/provider/retry.js.map +1 -1
  39. package/dist/public-runtime.d.ts +2 -1
  40. package/dist/public-runtime.d.ts.map +1 -1
  41. package/dist/public-runtime.js +1 -1
  42. package/dist/public-runtime.js.map +1 -1
  43. package/dist/public-tools.d.ts +1 -0
  44. package/dist/public-tools.d.ts.map +1 -1
  45. package/dist/public-tools.js +1 -0
  46. package/dist/public-tools.js.map +1 -1
  47. package/dist/runtime/query/deferred-sandbox.d.ts +54 -0
  48. package/dist/runtime/query/deferred-sandbox.d.ts.map +1 -0
  49. package/dist/runtime/query/deferred-sandbox.js +75 -0
  50. package/dist/runtime/query/deferred-sandbox.js.map +1 -0
  51. package/dist/runtime/query/executor.d.ts +11 -0
  52. package/dist/runtime/query/executor.d.ts.map +1 -1
  53. package/dist/runtime/query/executor.js +25 -3
  54. package/dist/runtime/query/executor.js.map +1 -1
  55. package/dist/runtime/query/index.d.ts +2 -0
  56. package/dist/runtime/query/index.d.ts.map +1 -1
  57. package/dist/runtime/query/index.js +94 -25
  58. package/dist/runtime/query/index.js.map +1 -1
  59. package/dist/runtime/query/iteration/index.d.ts +26 -0
  60. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  61. package/dist/runtime/query/iteration/index.js +164 -21
  62. package/dist/runtime/query/iteration/index.js.map +1 -1
  63. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +10 -7
  64. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  65. package/dist/runtime/query/iteration/phases/compaction-breaker.js +10 -7
  66. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  67. package/dist/runtime/query/iteration/phases/compaction.d.ts +42 -1
  68. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  69. package/dist/runtime/query/iteration/phases/compaction.js +119 -21
  70. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  71. package/dist/runtime/query/iteration/provider-rejected-image.d.ts +21 -1
  72. package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -1
  73. package/dist/runtime/query/iteration/provider-rejected-image.js +53 -7
  74. package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -1
  75. package/dist/runtime/query/iteration/request-repair.d.ts +108 -0
  76. package/dist/runtime/query/iteration/request-repair.d.ts.map +1 -0
  77. package/dist/runtime/query/iteration/request-repair.js +593 -0
  78. package/dist/runtime/query/iteration/request-repair.js.map +1 -0
  79. package/dist/runtime/query/iteration/stream-turn.d.ts +6 -0
  80. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/stream-turn.js +2 -2
  82. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  83. package/dist/runtime/query/request-rich-content.d.ts +5 -1
  84. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  85. package/dist/runtime/query/request-rich-content.js +67 -25
  86. package/dist/runtime/query/request-rich-content.js.map +1 -1
  87. package/dist/runtime/query/tooling.d.ts +1 -0
  88. package/dist/runtime/query/tooling.d.ts.map +1 -1
  89. package/dist/runtime/query/tooling.js +1 -0
  90. package/dist/runtime/query/tooling.js.map +1 -1
  91. package/dist/tools/builtins/browser.d.ts +4 -4
  92. package/dist/tools/builtins/code-analysis.d.ts +49 -0
  93. package/dist/tools/builtins/code-analysis.d.ts.map +1 -0
  94. package/dist/tools/builtins/code-analysis.js +409 -0
  95. package/dist/tools/builtins/code-analysis.js.map +1 -0
  96. package/dist/tools/builtins/computer-use.d.ts +25 -25
  97. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  98. package/dist/tools/builtins/computer-use.js +12 -5
  99. package/dist/tools/builtins/computer-use.js.map +1 -1
  100. package/dist/tools/builtins/index.d.ts +1 -0
  101. package/dist/tools/builtins/index.d.ts.map +1 -1
  102. package/dist/tools/builtins/index.js +1 -0
  103. package/dist/tools/builtins/index.js.map +1 -1
  104. package/dist/tools/builtins/lsp.d.ts +1 -1
  105. package/dist/tools/builtins/skill.d.ts +2 -2
  106. package/dist/types/code-analysis/index.d.ts +299 -0
  107. package/dist/types/code-analysis/index.d.ts.map +1 -0
  108. package/dist/types/code-analysis/index.js +30 -0
  109. package/dist/types/code-analysis/index.js.map +1 -0
  110. package/dist/types/plugin/index.d.ts +6 -6
  111. package/dist/types/provider/errors.d.ts.map +1 -1
  112. package/dist/types/provider/errors.js +30 -5
  113. package/dist/types/provider/errors.js.map +1 -1
  114. package/dist/types/provider/interface.d.ts +12 -0
  115. package/dist/types/provider/interface.d.ts.map +1 -1
  116. package/dist/types/provider/stream.d.ts +1 -1
  117. package/dist/types/session/config.d.ts +14 -0
  118. package/dist/types/session/config.d.ts.map +1 -1
  119. package/dist/types/session/events.d.ts +20 -5
  120. package/dist/types/session/events.d.ts.map +1 -1
  121. package/dist/types/session/events.js.map +1 -1
  122. package/dist/types/session/records.d.ts +127 -127
  123. package/dist/types/tool/index.d.ts +9 -0
  124. package/dist/types/tool/index.d.ts.map +1 -1
  125. package/dist/types/tool/index.js.map +1 -1
  126. package/package.json +2 -2
  127. package/src/compaction/verifier.ts +57 -20
  128. package/src/provider/capabilities.ts +17 -0
  129. package/src/provider/errors.ts +13 -0
  130. package/src/provider/fallback.ts +10 -0
  131. package/src/provider/idle-timeout.ts +5 -0
  132. package/src/provider/image-input-refusal.ts +54 -0
  133. package/src/provider/index.ts +2 -0
  134. package/src/provider/retry.ts +42 -4
  135. package/src/public-runtime.ts +28 -0
  136. package/src/public-tools.ts +5 -0
  137. package/src/runtime/query/deferred-sandbox.ts +110 -0
  138. package/src/runtime/query/executor.ts +27 -3
  139. package/src/runtime/query/index.ts +127 -49
  140. package/src/runtime/query/iteration/index.ts +188 -23
  141. package/src/runtime/query/iteration/phases/compaction-breaker.ts +10 -8
  142. package/src/runtime/query/iteration/phases/compaction.ts +151 -35
  143. package/src/runtime/query/iteration/provider-rejected-image.ts +71 -5
  144. package/src/runtime/query/iteration/request-repair.ts +743 -0
  145. package/src/runtime/query/iteration/stream-turn.ts +11 -1
  146. package/src/runtime/query/request-rich-content.ts +77 -27
  147. package/src/runtime/query/tooling.ts +2 -0
  148. package/src/tools/builtins/code-analysis.ts +486 -0
  149. package/src/tools/builtins/computer-use.ts +13 -4
  150. package/src/tools/builtins/index.ts +5 -0
  151. package/src/types/code-analysis/index.ts +325 -0
  152. package/src/types/provider/errors.ts +33 -5
  153. package/src/types/provider/interface.ts +13 -0
  154. package/src/types/provider/stream.ts +1 -1
  155. package/src/types/session/config.ts +14 -0
  156. package/src/types/session/events.ts +25 -5
  157. package/src/types/tool/index.ts +9 -0
@@ -15,7 +15,7 @@ import {
15
15
  } from '../../../constants/tools/index.js'
16
16
  import { withStreamedMessageIdentity } from '../../../manager/session/streamed-message-identity.js'
17
17
  import { renderSkillsSection } from '../../../persona/assembler.js'
18
- import { resolveProviderCapabilities } from '../../../provider/capabilities.js'
18
+ import { modelReadsImages, resolveProviderCapabilities } from '../../../provider/capabilities.js'
19
19
  import { collectChatCompletion } from '../../../provider/collect-chat-completion.js'
20
20
  import { renderToolSchema } from '../../../registry/tool/schema.js'
21
21
  import { formatCompletionNotification } from '../../../scheduler/completion-inbox.js'
@@ -54,6 +54,7 @@ import { stableDigest } from '../../../utils/hash.js'
54
54
  import { generateMessageId } from '../../../utils/id.js'
55
55
  import { parseStructuredResultJson } from '../../../utils/structured-result-json.js'
56
56
  import { createCallbackInference } from '../callback-inference.js'
57
+ import { SandboxAcquisitionStopped } from '../deferred-sandbox.js'
57
58
  import { deliverDurableInbound } from '../durable-inbound.js'
58
59
  import type { ToolCallOutcome } from '../executor.js'
59
60
  import { projectObservationContext } from '../observation-context.js'
@@ -80,17 +81,27 @@ import { runAdvisoryPhase } from './phases/advisory.js'
80
81
  import { runIterationCheckpoint } from './phases/checkpoint.js'
81
82
  import { compactionBreaker } from './phases/compaction-breaker.js'
82
83
  import {
84
+ AUTOMATIC_COMPACTION_FAILED,
85
+ OVERFLOW_RELIEF_LIMIT,
83
86
  activeContextWindow,
87
+ clearCompactionErrorNote,
88
+ compactionErrorNote,
84
89
  measureContext,
85
90
  relieveOverflow,
86
91
  runCompactionCheck,
92
+ stopOnRefill,
87
93
  } from './phases/compaction.js'
88
94
  import { runHandoffPause } from './phases/handoff.js'
89
95
  import type { IterationContext } from './phases/index.js'
90
96
  import { runPlanGate } from './phases/plan.js'
91
97
  import { runToolReview } from './phases/tool-review.js'
92
98
  import { refreshWorkingMemory, splitWorkingMemoryForRequest } from './phases/working-memory.js'
93
- import { streamWithProviderRejectedImageRecovery } from './provider-rejected-image.js'
99
+ import {
100
+ type TextOnlyModelRecovery,
101
+ type TextOnlyModelRefusal,
102
+ streamWithProviderRejectedImageRecovery,
103
+ } from './provider-rejected-image.js'
104
+ import { type MediaRemoval, RequestRepair } from './request-repair.js'
94
105
  import {
95
106
  type StepShaping,
96
107
  appendWorkContext,
@@ -141,6 +152,11 @@ const CLOSING_RESPONSE_GUIDANCE =
141
152
 
142
153
  export { awaitedJobGraceMs, settleGraceMs } from './outstanding-work.js'
143
154
 
155
+ /** One provider's model, as {@link IterationOrchestrator} remembers it. */
156
+ function textOnlyKey(providerId: string, model: string): string {
157
+ return `${providerId}\u0000${model}`
158
+ }
159
+
144
160
  export class IterationOrchestrator {
145
161
  private ctx: IterationContext
146
162
  private advisoryTurn:
@@ -188,6 +204,73 @@ export class IterationOrchestrator {
188
204
  }
189
205
  /** Rich tool blocks already reported; durable history is scanned every turn. */
190
206
  private readonly warnedRichToolResults = new Set<string>()
207
+ /** What this run has learned about requests its provider rejects as invalid, and how to repair them. */
208
+ private readonly requestRepair = new RequestRepair({
209
+ debug: (message) => this.ctx.log.debug(message),
210
+ onMediaRemoved: (removal) => this.noteMediaRemoved(removal),
211
+ servedBy: () => this.ctx.servingMember?.(),
212
+ })
213
+ /**
214
+ * Provider and model pairs that refused a request for holding an image; they
215
+ * get none for the rest of the run. Keyed by both: one model id can be served
216
+ * by two members of a fallback chain, and only the one that refused is known.
217
+ */
218
+ private readonly textOnlyModels = new Set<string>()
219
+
220
+ /**
221
+ * The member a request for `model` goes to first. A fallback chain keeps
222
+ * serving from the member it last moved to, so that member, not the head,
223
+ * is the one whose reading of images counts.
224
+ */
225
+ private servingModel(model: string): { readonly providerId: string; readonly model: string } {
226
+ const serving = this.ctx.servingMember?.()
227
+ return {
228
+ providerId: serving?.providerId ?? this.ctx.provider.id,
229
+ model: serving?.model ?? model,
230
+ }
231
+ }
232
+
233
+ /** Whether a request for `model` may carry images: the driver's catalogue and this run's own evidence agree. */
234
+ private modelReadsImages(model: string): boolean {
235
+ const serving = this.servingModel(model)
236
+ return (
237
+ !this.textOnlyModels.has(textOnlyKey(serving.providerId, serving.model)) &&
238
+ modelReadsImages(this.ctx.provider, model)
239
+ )
240
+ }
241
+
242
+ /** What a request for `model` does when its model turns out to read text only. */
243
+ private textOnlyRecovery(model: string): TextOnlyModelRecovery {
244
+ return {
245
+ servedBy: () => this.servingModel(model),
246
+ onAccepted: (refused) => this.noteTextOnlyModel(refused),
247
+ }
248
+ }
249
+
250
+ /**
251
+ * A provider refused a request for holding an image its model cannot read, and
252
+ * the retry without images was accepted. Stop sending images to it, and say so
253
+ * once, so the host can remember the model beyond this run.
254
+ */
255
+ private async noteTextOnlyModel(refused: TextOnlyModelRefusal): Promise<void> {
256
+ const key = textOnlyKey(refused.providerId, refused.model ?? '')
257
+ if (this.textOnlyModels.has(key)) return
258
+ this.textOnlyModels.add(key)
259
+ const { providerId, model } = refused
260
+ this.ctx.log.warn('Capability mismatch: the model cannot read images', {
261
+ [GENAI.SYSTEM]: providerId,
262
+ [GENAI.REQUEST_MODEL]: model ?? '',
263
+ })
264
+ await this.ctx.emitEvent?.({
265
+ type: 'capability_warning',
266
+ turnId: this.ctx.recorder.turnId,
267
+ capability: 'vision',
268
+ contentSource: 'history',
269
+ providerId,
270
+ ...(model !== undefined ? { model } : {}),
271
+ message: `Model '${model ?? 'unknown'}' on provider '${providerId}' refused a request that held an image, so images are left out of its requests. They stay in the conversation and are sent again to a model that reads them.`,
272
+ })
273
+ }
191
274
  /**
192
275
  * The previous iteration held a `stopWhen` decision open for a worker.
193
276
  *
@@ -442,16 +525,19 @@ export class IterationOrchestrator {
442
525
  // checkpoint names the message the turn is answering.
443
526
  await this.ctx.checkpointMgr.markLoopStart?.(recorder)
444
527
 
445
- // One context-overflow relief per *stuck point*, not per turn.
528
+ // Context-overflow relief is a two-rung ladder per *stuck point*, not per
529
+ // turn, as in the reference terminal (at most two reactive compactions per
530
+ // overflow): the first relief, then one deeper pass if the retry still
531
+ // overflows.
446
532
  //
447
- // The latch exists so that a second overflow immediately after a
448
- // successful compaction — meaning the prompt is irreducible — stops
449
- // instead of looping. It was never meant to disarm the mechanism for
450
- // the rest of the turn, which is what a turn-scoped flag did: one
451
- // relief at iteration 3 left iteration 40 to die on an overflow with
452
- // obvious moves left. It is cleared by a turn that actually
453
- // succeeded, which is the evidence that the turn is no longer stuck.
454
- let overflowRelieved = false
533
+ // The limit exists so that an overflow that survives both rungs — an
534
+ // irreducible prompt — stops instead of looping. It was never meant to
535
+ // disarm the mechanism for the rest of the turn, which is what a
536
+ // turn-scoped flag did: one relief at iteration 3 left iteration 40 to die
537
+ // on an overflow with obvious moves left. The count is cleared by a turn
538
+ // that actually succeeded, which is the evidence that the turn is no
539
+ // longer stuck.
540
+ let overflowReliefs = 0
455
541
  // The output cap a provider told us fits beside this prompt. One request's
456
542
  // worth: it is cleared by a call that went through.
457
543
  let outputCapOverride: number | undefined
@@ -814,6 +900,7 @@ export class IterationOrchestrator {
814
900
  this.projectObservations(requestHistory),
815
901
  this.ctx.turnConfig.maxRequestRichContentBytes ??
816
902
  DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
903
+ { imagesReadable: this.modelReadsImages(stepModel) },
817
904
  )
818
905
  appendWorkContext(shaping, messages, iterationNum, step)
819
906
  if (closingDirective) messages.push(closingDirective)
@@ -989,6 +1076,8 @@ export class IterationOrchestrator {
989
1076
  stepMessageId,
990
1077
  {
991
1078
  onAccepted: (identity) => this.acceptProviderRejectedImage(identity),
1079
+ textOnly: this.textOnlyRecovery(stepModel),
1080
+ repair: this.requestRepair,
992
1081
  },
993
1082
  Boolean(
994
1083
  this.ctx.reviewAnswer || this.ctx.structuredOutput?.review || this.ctx.advisoryCtx,
@@ -1042,7 +1131,8 @@ export class IterationOrchestrator {
1042
1131
 
1043
1132
  // The turn went through, so the turn is not sitting on an
1044
1133
  // irreducible prompt any more. Re-arm relief for the next one.
1045
- overflowRelieved = false
1134
+ overflowReliefs = 0
1135
+ clearCompactionErrorNote(this.ctx)
1046
1136
  outputCapOverride = undefined
1047
1137
  if (response.finishReason !== 'length') this.outputContinuations = 0
1048
1138
 
@@ -1895,6 +1985,12 @@ export class IterationOrchestrator {
1895
1985
  yield* this.ctx.drainPending()
1896
1986
  } catch (err) {
1897
1987
  const cancelled = this.ctx.abortController.signal.aborted
1988
+ // A deferred sandbox that was cancelled or ran out of time. The
1989
+ // turn settles it as `cancelled` / `timeout` in `query()`, which is
1990
+ // not a failure of this iteration: it is recorded the way a turn
1991
+ // stopped by its own abort or timeout is (a cancelled step, no
1992
+ // error span), so the ledger and the stop reason agree.
1993
+ const sandboxStopped = err instanceof SandboxAcquisitionStopped
1898
1994
 
1899
1995
  // This iteration gets a step too, and it is the one the
1900
1996
  // argument three hundred lines above was actually about.
@@ -1955,12 +2051,13 @@ export class IterationOrchestrator {
1955
2051
  startedAt: stepStartedAt,
1956
2052
  usageBefore,
1957
2053
  costBefore,
1958
- unfinished: cancelled
1959
- ? { finishReason: 'cancelled' }
1960
- : {
1961
- finishReason: 'error',
1962
- failure: describeStepFailure(err, this.ctx.provider.id),
1963
- },
2054
+ unfinished:
2055
+ cancelled || sandboxStopped
2056
+ ? { finishReason: 'cancelled' }
2057
+ : {
2058
+ finishReason: 'error',
2059
+ failure: describeStepFailure(err, this.ctx.provider.id),
2060
+ },
1964
2061
  })
1965
2062
  }
1966
2063
 
@@ -2020,22 +2117,58 @@ export class IterationOrchestrator {
2020
2117
  }
2021
2118
 
2022
2119
  if (
2023
- !overflowRelieved &&
2024
2120
  !(err instanceof AnswerReviewFailure) &&
2025
2121
  classifyProviderError(err, this.ctx.provider.id).code === 'context_length_exceeded'
2026
2122
  ) {
2027
- overflowRelieved = true
2028
- const shed = await relieveOverflow(this.ctx)
2123
+ // The rapid-refill check comes first, before the first reactive relief
2124
+ // of an overflow, as in the reference terminal. A relief that follows
2125
+ // the previous compaction within the refill window counts toward the
2126
+ // limit, so a tool that re-fills the window every time it is called
2127
+ // stops the turn instead of being cleared and called again without end.
2128
+ // The deeper rung of the same overflow is not counted again.
2129
+ if (overflowReliefs === 0 && (await stopOnRefill(this.ctx))) {
2130
+ if (iterationActivity) {
2131
+ this.ctx.activityStore.fail(iterationActivity.id, toErrorMessage(err))
2132
+ }
2133
+ if (this.stopForCompactionBreaker()) break
2134
+ }
2135
+ // A rung that sheds too little to change the verdict is not worth a
2136
+ // provider call; climb to the next one at once.
2137
+ let shed = false
2138
+ while (!shed && overflowReliefs < OVERFLOW_RELIEF_LIMIT) {
2139
+ overflowReliefs++
2140
+ shed = await relieveOverflow(this.ctx, { deeper: overflowReliefs > 1 })
2141
+ }
2029
2142
  if (shed) {
2143
+ // Clearing results is a compaction too: only summaries and reducers
2144
+ // recorded themselves, so a clear-only relief was invisible to the
2145
+ // breaker.
2146
+ compactionBreaker(this.ctx)?.recordCompaction(iterationNum)
2030
2147
  this.ctx.log.info('Retrying the turn after relieving a context overflow', {
2031
2148
  [NAMZU.TURN_ID]: recorder.turnId,
2032
2149
  [NAMZU.ITERATION]: iterationNum,
2150
+ 'namzu.runtime.relief_rung': overflowReliefs,
2033
2151
  })
2034
2152
  if (iterationActivity) {
2035
2153
  this.ctx.activityStore.complete(iterationActivity.id)
2036
2154
  }
2037
2155
  continue
2038
2156
  }
2157
+ // Out of rungs. The conversation is kept; say why compaction could not
2158
+ // make room when it errored, as the reference does.
2159
+ const cause = compactionErrorNote(this.ctx)
2160
+ if (cause !== undefined && err instanceof Error) {
2161
+ try {
2162
+ err.message = `${err.message}${AUTOMATIC_COMPACTION_FAILED}${cause}`
2163
+ } catch {
2164
+ // A frozen error keeps its own message; the cause stays in the log.
2165
+ }
2166
+ }
2167
+ }
2168
+
2169
+ if (sandboxStopped) {
2170
+ if (iterationActivity) this.ctx.activityStore.complete(iterationActivity.id)
2171
+ throw err
2039
2172
  }
2040
2173
 
2041
2174
  if (iterationActivity) {
@@ -2643,6 +2776,32 @@ export class IterationOrchestrator {
2643
2776
  .some((t) => t.state !== 'completed' && t.state !== 'failed' && t.state !== 'canceled')
2644
2777
  }
2645
2778
 
2779
+ /** Media the provider could not process was left out of a request that then went through. */
2780
+ private async noteMediaRemoved(removal: MediaRemoval): Promise<void> {
2781
+ this.ctx.log.warn('Media the provider rejected was left out of the request', {
2782
+ [GENAI.SYSTEM]: this.ctx.provider.id,
2783
+ 'namzu.request.media_removed': removal.count,
2784
+ })
2785
+ const noun = removal.count === 1 ? 'a file' : `${removal.count} files`
2786
+ const it = removal.count === 1 ? 'it' : 'they'
2787
+ await this.ctx.emitEvent?.({
2788
+ type: 'capability_warning',
2789
+ turnId: this.ctx.recorder.turnId,
2790
+ capability: removal.kind === 'document' ? 'documents' : 'vision',
2791
+ // The person's own latest attachment is named as such; a count limit is
2792
+ // not a fault in any file.
2793
+ contentSource: removal.current
2794
+ ? 'media-removed-current'
2795
+ : removal.limit
2796
+ ? 'media-limit'
2797
+ : 'media-removed',
2798
+ providerId: this.ctx.provider.id,
2799
+ message: removal.limit
2800
+ ? `The conversation holds more files than the provider accepts at once, so ${noun} (the oldest) ${removal.count === 1 ? 'was' : 'were'} left out of the request. The conversation still holds ${it}.`
2801
+ : `The provider could not process ${noun}${removal.current ? ' attached to your latest message' : ' in this conversation'}, so ${it} ${removal.count === 1 ? 'was' : 'were'} left out of the request. The conversation still holds ${it}.`,
2802
+ })
2803
+ }
2804
+
2646
2805
  private async acceptProviderRejectedImage(identity: RequestImageIdentity): Promise<void> {
2647
2806
  const repaired = markProviderRejectedImage(this.ctx.recorder.messages, identity)
2648
2807
  if (repaired.count === 0) {
@@ -2700,6 +2859,7 @@ export class IterationOrchestrator {
2700
2859
  const finalMessages = projectRequestRichContent(
2701
2860
  this.projectObservations(finalHistory),
2702
2861
  this.ctx.turnConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
2862
+ { imagesReadable: this.modelReadsImages(model) },
2703
2863
  )
2704
2864
  appendWorkContext(this.stepShaping(), finalMessages, this.steps.length + 1, { model })
2705
2865
  // The closing directive is the last thing the model reads: after
@@ -2746,8 +2906,13 @@ export class IterationOrchestrator {
2746
2906
  signal: this.ctx.abortController.signal,
2747
2907
  } satisfies import('../../../types/provider/index.js').ChatCompletionParams
2748
2908
  const response = await collectChatCompletion(
2749
- streamWithProviderRejectedImageRecovery(this.ctx.provider, finalParams, (identity) =>
2750
- this.acceptProviderRejectedImage(identity),
2909
+ streamWithProviderRejectedImageRecovery(
2910
+ this.ctx.provider,
2911
+ finalParams,
2912
+ (identity) => this.acceptProviderRejectedImage(identity),
2913
+ undefined,
2914
+ this.textOnlyRecovery(model),
2915
+ this.requestRepair,
2751
2916
  ),
2752
2917
  )
2753
2918
 
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * - Three failures in a row (a pass that ran and shed nothing): every one of
5
5
  * them paid for a summary or a reducer call and left the history as it was,
6
- * so a fourth would do the same. Compaction is skipped from then on and the
7
- * turn carries on; a pass the provider forces by rejecting the prompt is not
8
- * gated.
6
+ * so a fourth would do the same. Proactive compaction is skipped from then on
7
+ * and the turn carries on, silently as far as the person is concerned (a log
8
+ * line and a `breaker_open` event, no sentence); a pass the provider forces
9
+ * by rejecting the prompt is not gated, so the reactive ladder still runs.
9
10
  * - Three compactions in a row, each followed by the context filling back up
10
11
  * within three iterations: something in the loop (a huge file read, a tool
11
12
  * that prints without limit) refills the window faster than any summary can
@@ -27,12 +28,13 @@ export const COMPACTION_REFILL_ITERATIONS = 3
27
28
  /** Consecutive refills that stop the turn. */
28
29
  export const COMPACTION_REFILL_LIMIT = 3
29
30
 
30
- /** One short line, shown once, when the turn stops on refills. */
31
+ /**
32
+ * One short line, shown once, when the turn stops on refills. It names the
33
+ * cause and a step that keeps the conversation: the reference terminal also
34
+ * offers to start over here, and Namzu never does.
35
+ */
31
36
  export const COMPACTION_REFILL_MESSAGE =
32
- 'The context filled up again right after being compacted, three times in a row. A file or tool output is probably too big for the window: read it in smaller parts, or start fresh with /clear.'
33
- /** One short line, shown once, when compaction stops after failures. */
34
- export const COMPACTION_FAILURE_MESSAGE =
35
- 'Automatic compaction stopped after three failed attempts. Run /compact yourself, or start fresh with /clear.'
37
+ 'The context filled up again right after being compacted, three times in a row. A file being read or a tool output is probably too large for the context window. Try reading it in smaller parts.'
36
38
 
37
39
  export class CompactionBreaker {
38
40
  private failures = 0
@@ -16,17 +16,14 @@ import { serializeState } from '../../../../compaction/serializer.js'
16
16
  import { buildCompactionMessage, isCompactionMessage } from '../../../../compaction/summary.js'
17
17
  import { estimateMessagesTokens } from '../../../../compaction/token-estimate.js'
18
18
  import { buildVerifiedSummaryWithBoundedProvider } from '../../../../compaction/verifier.js'
19
+ import type { CompactionConfig } from '../../../../config/runtime.js'
19
20
  import { CHARS_PER_TOKEN } from '../../../../constants/limits.js'
20
21
  import { NAMZU } from '../../../../constants/telemetry/index.js'
21
22
  import { invariants } from '../../../../invariants/index.js'
22
23
  import { resolveTaskModel } from '../../../../model-router/task-router.js'
23
24
  import type { Message } from '../../../../types/message/index.js'
24
25
  import { applyLifecycleHookResults } from '../../plugin-hooks.js'
25
- import {
26
- COMPACTION_FAILURE_MESSAGE,
27
- COMPACTION_REFILL_MESSAGE,
28
- compactionBreaker,
29
- } from './compaction-breaker.js'
26
+ import { COMPACTION_REFILL_MESSAGE, compactionBreaker } from './compaction-breaker.js'
30
27
  import type { IterationContext } from './context.js'
31
28
  import { isWorkingMemoryMessage } from './working-memory.js'
32
29
 
@@ -223,6 +220,73 @@ export function measureContext(ctx: IterationContext): {
223
220
  return { tokens: estimateTokens(ctx), source: 'estimate' }
224
221
  }
225
222
 
223
+ /**
224
+ * Reactive compactions allowed for one overflow, as in the reference
225
+ * terminal: the first relief, then at most one deeper pass.
226
+ */
227
+ export const OVERFLOW_RELIEF_LIMIT = 2
228
+
229
+ /** Round-groups that must remain for the deeper pass to be worth running. */
230
+ export const DEEPER_PASS_MIN_GROUPS = 3
231
+
232
+ export interface OverflowReliefOptions {
233
+ /**
234
+ * The second rung. Keep only the newest round-group verbatim instead of the
235
+ * configured recent window (when at least {@link DEEPER_PASS_MIN_GROUPS}
236
+ * groups remain), and clear oversized tool results even inside the recent
237
+ * window, with the same placeholder and spill pointer a stale result gets.
238
+ */
239
+ readonly deeper?: boolean
240
+ }
241
+
242
+ /**
243
+ * A round-group starts at each assistant message and runs to the next one, so
244
+ * its tool results stay with the call that made them. The newest group is the
245
+ * last assistant message and everything after it; the opening user messages
246
+ * ride with the first group.
247
+ */
248
+ function roundGroups(messages: readonly Message[]): number {
249
+ let groups = 0
250
+ for (const message of messages) if (message.role === 'assistant') groups++
251
+ return groups
252
+ }
253
+
254
+ /**
255
+ * The deeper rung's configuration: no tool result protected, and a recent
256
+ * window no larger than the first rung's. With three or more groups the
257
+ * window shrinks to the newest group, but never grows past what the
258
+ * configuration already kept: a newest group bigger than that would make the
259
+ * "deeper" pass shed less than the first one.
260
+ */
261
+ export function deepenConfig(
262
+ config: CompactionConfig,
263
+ messages: readonly Message[],
264
+ ): CompactionConfig {
265
+ let lastAssistant = -1
266
+ for (let i = messages.length - 1; i >= 0; i--) {
267
+ if (messages[i]?.role === 'assistant') {
268
+ lastAssistant = i
269
+ break
270
+ }
271
+ }
272
+ const deeper = { ...config, keepRecentToolResults: 0 }
273
+ if (lastAssistant < 0 || roundGroups(messages) < DEEPER_PASS_MIN_GROUPS) return deeper
274
+ const newest = messages.slice(lastAssistant)
275
+ if (config.keepRecentTokens !== undefined) {
276
+ return {
277
+ ...deeper,
278
+ keepRecentTokens: Math.max(
279
+ 1,
280
+ Math.min(config.keepRecentTokens, estimateMessagesTokens(newest)),
281
+ ),
282
+ }
283
+ }
284
+ return {
285
+ ...deeper,
286
+ keepRecentMessages: Math.max(1, Math.min(config.keepRecentMessages, newest.length)),
287
+ }
288
+ }
289
+
226
290
  /**
227
291
  * Shed history because the PROVIDER said the prompt is too long.
228
292
  *
@@ -241,12 +305,18 @@ export function measureContext(ctx: IterationContext): {
241
305
  * @returns whether anything was actually shed. `false` means retrying would
242
306
  * send the same prompt again, so the caller must not.
243
307
  */
244
- export async function relieveOverflow(ctx: IterationContext): Promise<boolean> {
308
+ export async function relieveOverflow(
309
+ ctx: IterationContext,
310
+ options: OverflowReliefOptions = {},
311
+ ): Promise<boolean> {
312
+ // The cause belongs to the rung that is about to run: an earlier rung's
313
+ // summary failure must not be blamed for a later rung that compacted cleanly.
314
+ clearCompactionErrorNote(ctx)
245
315
  const before = ctx.recorder.messages.length
246
316
  const beforeChars = totalChars(ctx.recorder.messages)
247
317
  const beforeTokens = estimateMessagesTokens(ctx.recorder.messages)
248
318
 
249
- await runCompactionCheck(ctx, { force: true })
319
+ await runCompactionCheck(ctx, { force: true, ...(options.deeper ? { deeper: true } : {}) })
250
320
 
251
321
  const shed = beforeChars - totalChars(ctx.recorder.messages)
252
322
  const tokensShed = beforeTokens - estimateMessagesTokens(ctx.recorder.messages)
@@ -278,6 +348,31 @@ export async function relieveOverflow(ctx: IterationContext): Promise<boolean> {
278
348
  return true
279
349
  }
280
350
 
351
+ /**
352
+ * Why the last compaction attempt errored, kept for the turn so that when
353
+ * relief runs out the failure it ends on can say so, as the reference
354
+ * terminal's " · automatic compaction failed: <cause>" does.
355
+ */
356
+ const compactionErrors = new WeakMap<object, string>()
357
+ const MAX_COMPACTION_CAUSE_CHARS = 300
358
+ /** The sentence the cause is appended with. */
359
+ export const AUTOMATIC_COMPACTION_FAILED = ' · automatic compaction failed: '
360
+
361
+ function noteCompactionError(ctx: IterationContext, error: unknown): void {
362
+ const text = error instanceof Error ? error.message : String(error)
363
+ if (text.length > 0) compactionErrors.set(ctx.recorder, text.slice(0, MAX_COMPACTION_CAUSE_CHARS))
364
+ }
365
+
366
+ /** The turn's last compaction error, or `undefined` when none errored. */
367
+ export function compactionErrorNote(ctx: IterationContext): string | undefined {
368
+ return compactionErrors.get(ctx.recorder)
369
+ }
370
+
371
+ /** A turn that got through has nothing left to explain. */
372
+ export function clearCompactionErrorNote(ctx: IterationContext): void {
373
+ compactionErrors.delete(ctx.recorder)
374
+ }
375
+
281
376
  function totalChars(messages: readonly { content: unknown }[]): number {
282
377
  let total = 0
283
378
  for (const msg of messages) total += measureContentChars(msg.content)
@@ -335,8 +430,11 @@ async function declined(
335
430
  /**
336
431
  * The third refill in a row: stop the turn instead of compacting again.
337
432
  * Returns true when this pass must not run. Only with `circuitBreaker`.
433
+ * The loop also asks it before the first reactive relief of an overflow, as
434
+ * the reference terminal does (`rhr` runs first in its withheld-overflow
435
+ * handling), so a re-read loop made only of overflows is counted too.
338
436
  */
339
- async function stopOnRefill(ctx: IterationContext): Promise<boolean> {
437
+ export async function stopOnRefill(ctx: IterationContext): Promise<boolean> {
340
438
  const breaker = compactionBreaker(ctx)
341
439
  if (!breaker) return false
342
440
  // Already announced: the turn is on its way out; do not say it twice.
@@ -418,6 +516,7 @@ async function applyReducer(
418
516
  'namzu.runtime.reason': reduction.reason,
419
517
  'exception.message': message,
420
518
  })
519
+ noteCompactionError(ctx, error)
421
520
  await declined(ctx, 'reducer_threw', before, message)
422
521
  return
423
522
  }
@@ -549,7 +648,7 @@ interface CompactionPass {
549
648
 
550
649
  export async function runCompactionCheck(
551
650
  ctx: IterationContext,
552
- options?: { force?: boolean },
651
+ options?: { force?: boolean; deeper?: boolean },
553
652
  ): Promise<void> {
554
653
  const pass: CompactionPass = {
555
654
  attempted: false,
@@ -585,11 +684,12 @@ export async function runCompactionCheck(
585
684
  async function runCompactionCheckInner(
586
685
  ctx: IterationContext,
587
686
  pass: CompactionPass,
588
- options?: { force?: boolean },
687
+ options?: { force?: boolean; deeper?: boolean },
589
688
  ): Promise<void> {
590
- const config = ctx.compactionConfig
591
- if (!config) return
592
- if (config.strategy === 'disabled') return
689
+ const configured = ctx.compactionConfig
690
+ if (!configured) return
691
+ if (configured.strategy === 'disabled') return
692
+ const config = options?.deeper ? deepenConfig(configured, ctx.recorder.messages) : configured
593
693
 
594
694
  const measured = measureContext(ctx)
595
695
  const estimatedTokens = measured.tokens
@@ -616,11 +716,14 @@ async function runCompactionCheckInner(
616
716
  const startAt = salience ? Math.min(config.softTarget ?? DEFAULT_SOFT_TARGET, trigger) : trigger
617
717
  if (!options?.force && usage < startAt) return
618
718
 
619
- // Three failed passes in a row: stop trying for the rest of the turn, and
620
- // say so once. A pass the provider forced is never gated.
719
+ // Three failed passes in a row: stop proactive compaction for the rest of
720
+ // the turn. A pass the provider forced is never gated.
621
721
  const breaker = compactionBreaker(ctx)
622
722
  if (breaker?.failuresOpen && !options?.force) {
623
723
  if (breaker.takeFailureNotice()) {
724
+ // Silent for the person, as in the reference terminal: a log line and the
725
+ // event for a host that wants it, but no sentence to show. Only the
726
+ // proactive pass stops; a real overflow still runs the reactive ladder.
624
727
  ctx.log.warn('Compaction failed three times in a row — switched off for this turn', {
625
728
  [NAMZU.TURN_ID]: ctx.recorder.turnId,
626
729
  })
@@ -630,7 +733,6 @@ async function runCompactionCheckInner(
630
733
  iteration: ctx.recorder.currentIteration,
631
734
  cause: 'breaker_open',
632
735
  messages: ctx.recorder.messages.length,
633
- error: COMPACTION_FAILURE_MESSAGE,
634
736
  })
635
737
  }
636
738
  return
@@ -851,25 +953,39 @@ async function runCompactionCheckInner(
851
953
  // to a cheap model while the bill is written against the expensive one
852
954
  // is a mistake with no symptom.
853
955
  const compactionModel = resolveTaskModel('compaction', ctx.taskRouter, ctx.turnConfig.model)
854
- compactedContent = await buildVerifiedSummaryWithBoundedProvider(
855
- manager,
856
- olderMessages as Message[],
857
- ctx.provider,
858
- config,
859
- (usage) =>
860
- ctx.recorder.accumulateUsage(usage, {
861
- providerId: ctx.recorder.servingProviderId,
862
- model: compactionModel,
863
- }),
864
- // The one model call a turn makes that the user never asked for. It
865
- // reads a transcript and writes a summary, which is the cheapest
866
- // thing a small model does well, and it fires on exactly the long
867
- // runs where the primary model is most expensive. `taskRouter` had
868
- // been accepted, validated and threaded through four types since it
869
- // was added, and nothing ever consulted it.
870
- compactionModel,
871
- ctx.abortController.signal,
872
- )
956
+ try {
957
+ compactedContent = await buildVerifiedSummaryWithBoundedProvider(
958
+ manager,
959
+ olderMessages as Message[],
960
+ ctx.provider,
961
+ config,
962
+ (usage) =>
963
+ ctx.recorder.accumulateUsage(usage, {
964
+ providerId: ctx.recorder.servingProviderId,
965
+ model: compactionModel,
966
+ }),
967
+ // The one model call a turn makes that the user never asked for. It
968
+ // reads a transcript and writes a summary, which is the cheapest
969
+ // thing a small model does well, and it fires on exactly the long
970
+ // runs where the primary model is most expensive. `taskRouter` had
971
+ // been accepted, validated and threaded through four types since it
972
+ // was added, and nothing ever consulted it.
973
+ compactionModel,
974
+ ctx.abortController.signal,
975
+ )
976
+ } catch (error) {
977
+ // The model step only ADDS to the extracted state, so a failed one (a
978
+ // timeout, a provider error, its own overflow after the ladder in the
979
+ // verifier ran out) is not a reason to leave the history as it is. Cancellation
980
+ // is the caller's, and still stops the pass.
981
+ if (ctx.abortController.signal.aborted) throw error
982
+ noteCompactionError(ctx, error)
983
+ ctx.log.warn('The compaction summary call failed — using the extracted state alone', {
984
+ [NAMZU.TURN_ID]: ctx.recorder.turnId,
985
+ 'exception.message': error instanceof Error ? error.message : String(error),
986
+ })
987
+ compactedContent = serializeState(manager.getState())
988
+ }
873
989
  } else {
874
990
  compactedContent = serializeState(manager.getState())
875
991
  }