@namzu/sdk 31.0.0 → 32.0.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 (133) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +4 -0
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/SupervisorAgent.js +4 -0
  7. package/dist/agents/SupervisorAgent.js.map +1 -1
  8. package/dist/agents/runAgent.d.ts +4 -0
  9. package/dist/agents/runAgent.d.ts.map +1 -1
  10. package/dist/agents/runAgent.js +4 -0
  11. package/dist/agents/runAgent.js.map +1 -1
  12. package/dist/directory/derive-supervisor.d.ts.map +1 -1
  13. package/dist/directory/derive-supervisor.js +5 -0
  14. package/dist/directory/derive-supervisor.js.map +1 -1
  15. package/dist/directory/derive.d.ts.map +1 -1
  16. package/dist/directory/derive.js +5 -0
  17. package/dist/directory/derive.js.map +1 -1
  18. package/dist/directory/load.d.ts.map +1 -1
  19. package/dist/directory/load.js +1 -0
  20. package/dist/directory/load.js.map +1 -1
  21. package/dist/directory/types.d.ts +2 -0
  22. package/dist/directory/types.d.ts.map +1 -1
  23. package/dist/execution/code-runtime/types.d.ts +11 -1
  24. package/dist/execution/code-runtime/types.d.ts.map +1 -1
  25. package/dist/execution/code-runtime/types.js.map +1 -1
  26. package/dist/execution/code-runtime/worker.d.ts.map +1 -1
  27. package/dist/execution/code-runtime/worker.js +67 -9
  28. package/dist/execution/code-runtime/worker.js.map +1 -1
  29. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  30. package/dist/manager/agent/lifecycle.js +1 -0
  31. package/dist/manager/agent/lifecycle.js.map +1 -1
  32. package/dist/provider/errors.d.ts +1 -0
  33. package/dist/provider/errors.d.ts.map +1 -1
  34. package/dist/provider/errors.js +39 -1
  35. package/dist/provider/errors.js.map +1 -1
  36. package/dist/public-runtime.d.ts +5 -3
  37. package/dist/public-runtime.d.ts.map +1 -1
  38. package/dist/public-runtime.js +7 -2
  39. package/dist/public-runtime.js.map +1 -1
  40. package/dist/public-types.d.ts +2 -1
  41. package/dist/public-types.d.ts.map +1 -1
  42. package/dist/run/reporter.d.ts.map +1 -1
  43. package/dist/run/reporter.js +6 -1
  44. package/dist/run/reporter.js.map +1 -1
  45. package/dist/runtime/query/executor.d.ts.map +1 -1
  46. package/dist/runtime/query/executor.js +214 -29
  47. package/dist/runtime/query/executor.js.map +1 -1
  48. package/dist/runtime/query/index.d.ts +5 -0
  49. package/dist/runtime/query/index.d.ts.map +1 -1
  50. package/dist/runtime/query/index.js +1 -0
  51. package/dist/runtime/query/index.js.map +1 -1
  52. package/dist/runtime/query/iteration/index.d.ts +1 -0
  53. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  54. package/dist/runtime/query/iteration/index.js +34 -14
  55. package/dist/runtime/query/iteration/index.js.map +1 -1
  56. package/dist/runtime/query/iteration/phases/advisory.js +2 -2
  57. package/dist/runtime/query/iteration/phases/advisory.js.map +1 -1
  58. package/dist/runtime/query/iteration/provider-rejected-image.d.ts +13 -0
  59. package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -0
  60. package/dist/runtime/query/iteration/provider-rejected-image.js +91 -0
  61. package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -0
  62. package/dist/runtime/query/iteration/stream-turn.d.ts +4 -1
  63. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  64. package/dist/runtime/query/iteration/stream-turn.js +7 -3
  65. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  66. package/dist/runtime/query/request-rich-content.d.ts +13 -0
  67. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  68. package/dist/runtime/query/request-rich-content.js +153 -17
  69. package/dist/runtime/query/request-rich-content.js.map +1 -1
  70. package/dist/runtime/query/result.d.ts.map +1 -1
  71. package/dist/runtime/query/result.js +1 -0
  72. package/dist/runtime/query/result.js.map +1 -1
  73. package/dist/store/attachment/index.d.ts +24 -2
  74. package/dist/store/attachment/index.d.ts.map +1 -1
  75. package/dist/store/attachment/index.js +52 -5
  76. package/dist/store/attachment/index.js.map +1 -1
  77. package/dist/tools/builtins/lsp.d.ts +1 -1
  78. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  79. package/dist/tools/builtins/run-code.js +15 -5
  80. package/dist/tools/builtins/run-code.js.map +1 -1
  81. package/dist/types/agent/base.d.ts +4 -0
  82. package/dist/types/agent/base.d.ts.map +1 -1
  83. package/dist/types/agent/factory.d.ts +1 -0
  84. package/dist/types/agent/factory.d.ts.map +1 -1
  85. package/dist/types/message/index.d.ts +35 -4
  86. package/dist/types/message/index.d.ts.map +1 -1
  87. package/dist/types/message/index.js +42 -0
  88. package/dist/types/message/index.js.map +1 -1
  89. package/dist/types/provider/error.d.ts +4 -1
  90. package/dist/types/provider/error.d.ts.map +1 -1
  91. package/dist/types/provider/index.d.ts +1 -1
  92. package/dist/types/provider/index.d.ts.map +1 -1
  93. package/dist/types/provider/model.d.ts +12 -0
  94. package/dist/types/provider/model.d.ts.map +1 -1
  95. package/dist/types/run/events.d.ts +10 -2
  96. package/dist/types/run/events.d.ts.map +1 -1
  97. package/dist/types/run/events.js.map +1 -1
  98. package/dist/types/tool/index.d.ts +52 -6
  99. package/dist/types/tool/index.d.ts.map +1 -1
  100. package/dist/types/tool/index.js.map +1 -1
  101. package/package.json +1 -1
  102. package/src/agents/ReactiveAgent.ts +4 -0
  103. package/src/agents/SupervisorAgent.ts +4 -0
  104. package/src/agents/runAgent.ts +8 -0
  105. package/src/directory/derive-supervisor.ts +5 -0
  106. package/src/directory/derive.ts +5 -0
  107. package/src/directory/load.ts +1 -0
  108. package/src/directory/types.ts +2 -0
  109. package/src/execution/code-runtime/types.ts +15 -1
  110. package/src/execution/code-runtime/worker.ts +72 -9
  111. package/src/manager/agent/lifecycle.ts +1 -0
  112. package/src/provider/errors.ts +40 -1
  113. package/src/public-runtime.ts +16 -1
  114. package/src/public-types.ts +11 -0
  115. package/src/run/reporter.ts +7 -1
  116. package/src/runtime/query/executor.ts +235 -26
  117. package/src/runtime/query/index.ts +6 -0
  118. package/src/runtime/query/iteration/index.ts +69 -29
  119. package/src/runtime/query/iteration/phases/advisory.ts +2 -2
  120. package/src/runtime/query/iteration/provider-rejected-image.ts +106 -0
  121. package/src/runtime/query/iteration/stream-turn.ts +12 -2
  122. package/src/runtime/query/request-rich-content.ts +188 -15
  123. package/src/runtime/query/result.ts +1 -0
  124. package/src/store/attachment/index.ts +100 -15
  125. package/src/tools/builtins/run-code.ts +15 -5
  126. package/src/types/agent/base.ts +4 -0
  127. package/src/types/agent/factory.ts +1 -0
  128. package/src/types/message/index.ts +81 -4
  129. package/src/types/provider/error.ts +4 -1
  130. package/src/types/provider/index.ts +1 -1
  131. package/src/types/provider/model.ts +13 -0
  132. package/src/types/run/events.ts +18 -4
  133. package/src/types/tool/index.ts +74 -9
@@ -29,6 +29,7 @@ import type {
29
29
  RequestToolPause,
30
30
  SkillRegistryRef,
31
31
  ToolContext,
32
+ ToolDispatchOptions,
32
33
  ToolRegistryContract,
33
34
  ToolResult,
34
35
  } from '../../types/tool/index.js'
@@ -71,6 +72,103 @@ export const DEFAULT_TOOL_TIMEOUT_MS = 120_000
71
72
  */
72
73
  export const DEFAULT_TOOL_CONCURRENCY = 8
73
74
 
75
+ /** Maximum UTF-8 size of one ephemeral tool-progress update. */
76
+ const MAX_TOOL_PROGRESS_BYTES = 8 * 1024
77
+
78
+ /** A visible marker: this event is a display projection, not durable output. */
79
+ const TOOL_PROGRESS_OMISSION = '… '
80
+
81
+ function boundedToolProgress(message: string): string {
82
+ if (Buffer.byteLength(message, 'utf8') <= MAX_TOOL_PROGRESS_BYTES) return message
83
+
84
+ const tailBudget = MAX_TOOL_PROGRESS_BYTES - Buffer.byteLength(TOOL_PROGRESS_OMISSION, 'utf8')
85
+ let low = 0
86
+ let high = message.length
87
+ while (low < high) {
88
+ const middle = Math.floor((low + high) / 2)
89
+ if (Buffer.byteLength(message.slice(middle), 'utf8') > tailBudget) low = middle + 1
90
+ else high = middle
91
+ }
92
+ // Do not turn the low half of a retained surrogate pair into U+FFFD.
93
+ if (
94
+ low > 0 &&
95
+ low < message.length &&
96
+ message.charCodeAt(low) >= 0xdc00 &&
97
+ message.charCodeAt(low) <= 0xdfff &&
98
+ message.charCodeAt(low - 1) >= 0xd800 &&
99
+ message.charCodeAt(low - 1) <= 0xdbff
100
+ ) {
101
+ low += 1
102
+ }
103
+ return `${TOOL_PROGRESS_OMISSION}${message.slice(low)}`
104
+ }
105
+
106
+ /**
107
+ * Latest-state publisher for one tool call.
108
+ *
109
+ * `ToolContext.report()` is synchronous by contract, while host event
110
+ * listeners may be arbitrarily slow. Starting one promise per report makes a
111
+ * chatty tool an unbounded allocation source. Progress is state rather than a
112
+ * transcript, so one in-flight update plus the latest pending update is the
113
+ * complete useful working set; intermediate states may be replaced.
114
+ */
115
+ class ToolProgressPublisher {
116
+ private pending: { readonly message: string; readonly fraction?: number } | undefined
117
+ private draining: Promise<void> | null = null
118
+ private accepting = true
119
+
120
+ constructor(
121
+ private readonly emitEvent: EmitEvent,
122
+ private readonly base: Omit<
123
+ Extract<RunEvent, { type: 'tool_progress' }>,
124
+ 'message' | 'fraction'
125
+ >,
126
+ ) {}
127
+
128
+ report(message: string, fraction?: number): void {
129
+ if (!this.accepting) return
130
+ this.pending = {
131
+ message,
132
+ ...(fraction !== undefined ? { fraction: Math.min(1, Math.max(0, fraction)) } : {}),
133
+ }
134
+ this.startDrain()
135
+ }
136
+
137
+ async close(): Promise<void> {
138
+ this.accepting = false
139
+ this.startDrain()
140
+ while (this.draining) await this.draining
141
+ }
142
+
143
+ private startDrain(): void {
144
+ if (this.draining || !this.pending) return
145
+ const current = this.drain()
146
+ this.draining = current
147
+ void current.finally(() => {
148
+ if (this.draining !== current) return
149
+ this.draining = null
150
+ // A report can land after the final loop check but before this
151
+ // settlement callback. It is still an accepted update and must be
152
+ // drained even when close() has since stopped new reports.
153
+ this.startDrain()
154
+ })
155
+ }
156
+
157
+ private async drain(): Promise<void> {
158
+ while (this.pending) {
159
+ const update = this.pending
160
+ this.pending = undefined
161
+ // A progress observer is diagnostic. Its failure cannot become a tool
162
+ // failure, and report() never hands a rejection back to the tool.
163
+ await this.emitEvent({
164
+ ...this.base,
165
+ message: boundedToolProgress(update.message),
166
+ ...(update.fraction !== undefined ? { fraction: update.fraction } : {}),
167
+ }).catch(() => {})
168
+ }
169
+ }
170
+ }
171
+
74
172
  /**
75
173
  * Re-runs granted to a `post_tool_use` hook that returns `{action:'retry'}`
76
174
  * on a tool which did not opt into {@link ToolDefinition.maxRetries}.
@@ -548,33 +646,35 @@ export class ToolExecutor {
548
646
  // Per-call, because the event has to name which call it is about:
549
647
  // a batch can run several tools at once and a host rendering them
550
648
  // side by side needs to know whose progress this is.
649
+ const progress = new ToolProgressPublisher(this.emitEvent, {
650
+ type: 'tool_progress',
651
+ runId: this.config.runId,
652
+ toolUseId: toolCall.id as ToolUseId,
653
+ toolName: toolCall.function.name,
654
+ })
551
655
  const ctx: ToolContext = {
552
656
  ...baseContext,
553
657
  toolUseId: toolCall.id,
658
+ source: { kind: 'direct' },
554
659
  // Overridden per call, so a nested dispatch can name the call
555
660
  // that made it. The base context has no `toolUseId`, and a
556
661
  // closure built there would report every nested call as
557
662
  // parentless.
558
- dispatchTool: (name, input) => this.dispatchNested(name, input, ctx, recordObservation),
663
+ dispatchTool: (name, input, options) =>
664
+ this.dispatchNested(name, input, ctx, recordObservation, toolCall.function.name, options),
559
665
  // Per-call for the same reason: a pause has to be routed back
560
666
  // to the call that raised it, and a batch can raise several.
561
667
  ...(this.config.toolPause ? { requestPause: this.config.toolPause(toolCall.id) } : {}),
562
- report: (message: string, fraction?: number) => {
563
- // Fire-and-forget: a tool reporting progress must never be
564
- // able to fail because the host's listener threw, and must
565
- // never have to await the emit mid-work.
566
- void this.emitEvent({
567
- type: 'tool_progress',
568
- runId: this.config.runId,
569
- toolUseId: toolCall.id as ToolUseId,
570
- toolName: toolCall.function.name,
571
- message,
572
- ...(fraction !== undefined ? { fraction: Math.min(1, Math.max(0, fraction)) } : {}),
573
- }).catch(() => {})
574
- },
668
+ report: (message: string, fraction?: number) => progress.report(message, fraction),
575
669
  }
576
670
  const run = async () => {
577
- results[i] = await this.executeSingle(toolCall, ctx, recordObservation)
671
+ try {
672
+ results[i] = await this.executeSingle(toolCall, ctx, recordObservation, () =>
673
+ progress.close(),
674
+ )
675
+ } finally {
676
+ await progress.close()
677
+ }
578
678
  }
579
679
  const gated = async () => {
580
680
  await gate.acquire()
@@ -651,15 +751,64 @@ export class ToolExecutor {
651
751
  input: unknown,
652
752
  context: ToolContext,
653
753
  recordObservation: (observation: ToolResultObservation) => void,
754
+ parentToolName?: string,
755
+ options?: ToolDispatchOptions,
654
756
  ): Promise<ToolResult> {
655
757
  const parent = context.toolUseId
656
- const via = parent ? { tool: name, toolUseId: parent as ToolUseId } : undefined
758
+ const via =
759
+ parent && parentToolName
760
+ ? {
761
+ tool: parentToolName,
762
+ toolUseId: parent as ToolUseId,
763
+ ...(options?.runtimeToolCallId ? { runtimeToolCallId: options.runtimeToolCallId } : {}),
764
+ }
765
+ : undefined
657
766
  // Its own id, minted here. Reusing the parent's would make two
658
767
  // different calls indistinguishable in any log keyed by it, which is
659
768
  // exactly how a nested write gets attributed to the program that ran
660
769
  // it rather than to itself.
661
770
  const nestedId = generateToolCallId() as unknown as ToolUseId
662
771
  const startedAt = Date.now()
772
+ const signal = options?.signal
773
+ ? AbortSignal.any([context.abortSignal, options.signal])
774
+ : context.abortSignal
775
+ const source = parent
776
+ ? options?.runtimeToolCallId
777
+ ? {
778
+ kind: 'code' as const,
779
+ parentToolUseId: parent,
780
+ runtimeToolCallId: options.runtimeToolCallId,
781
+ }
782
+ : { kind: 'nested' as const, parentToolUseId: parent }
783
+ : { kind: 'direct' as const }
784
+ const progress = new ToolProgressPublisher(this.emitEvent, {
785
+ type: 'tool_progress',
786
+ runId: this.config.runId,
787
+ toolUseId: nestedId,
788
+ toolName: name,
789
+ })
790
+ const childContext: ToolContext = {
791
+ ...context,
792
+ abortSignal: signal,
793
+ toolUseId: nestedId,
794
+ source,
795
+ dispatchTool: (childName, childInput, childOptions) =>
796
+ this.dispatchNested(
797
+ childName,
798
+ childInput,
799
+ childContext,
800
+ recordObservation,
801
+ name,
802
+ childOptions,
803
+ ),
804
+ // A nested execution has its own event/progress identity, but its
805
+ // durable pause belongs to the nearest model-issued ancestor. The
806
+ // checkpoint transcript contains that ancestor call and not this
807
+ // ephemeral child id; minting a pause route for `nestedId` makes the
808
+ // answer impossible to match after process restart. `...context`
809
+ // intentionally preserves the ancestor route here.
810
+ report: (message: string, fraction?: number) => progress.report(message, fraction),
811
+ }
663
812
 
664
813
  await this.emitEvent({
665
814
  type: 'tool_executing',
@@ -670,16 +819,54 @@ export class ToolExecutor {
670
819
  ...(via ? { via } : {}),
671
820
  })
672
821
 
673
- const result = await this.config.tools.execute(name, input, context)
822
+ let result: ToolResult
823
+ try {
824
+ // Use the same deadline layer as a model-issued call. Calling the
825
+ // registry directly made nested tools the only tools whose own
826
+ // `timeoutMs` declaration was ignored.
827
+ result = await this.runOnce(name, input, childContext)
828
+ } finally {
829
+ await progress.close()
830
+ }
831
+
832
+ const rawOutput = result.success
833
+ ? result.output
834
+ : formatFailedToolOutput(result.output, result.error)
835
+ const budgeted = applyToolOutputBudget({
836
+ toolName: name,
837
+ toolUseId: nestedId,
838
+ output: rawOutput,
839
+ maxChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
840
+ spillDir: this.config.toolOutputDir,
841
+ onError: (message) =>
842
+ this.log.warn('Failed to spill oversized nested tool output', {
843
+ [NAMZU.RUN_ID]: this.config.runId,
844
+ [GENAI.TOOL_NAME]: name,
845
+ 'exception.message': message,
846
+ }),
847
+ })
848
+ const visibleResult: ToolResult = result.success
849
+ ? { ...result, output: budgeted.output }
850
+ : {
851
+ ...result,
852
+ output: '',
853
+ // `run_code` sends `error`, not `output`, across the worker
854
+ // bridge on a failed host call. Leaving the raw error here would
855
+ // make the success path bounded and the failure path unbounded.
856
+ error: budgeted.output,
857
+ }
674
858
 
675
859
  await this.emitEvent({
676
860
  type: 'tool_completed',
677
861
  runId: this.config.runId,
678
862
  toolUseId: nestedId,
679
863
  toolName: name,
680
- result: result.success ? result.output : (result.error ?? 'failed'),
864
+ result: budgeted.output,
681
865
  isError: !result.success,
682
866
  durationMs: Date.now() - startedAt,
867
+ outputLength: budgeted.originalLength,
868
+ ...(budgeted.truncated ? { outputTruncated: true } : {}),
869
+ ...(budgeted.spillPath ? { outputSpillPath: budgeted.spillPath } : {}),
683
870
  ...(via ? { via } : {}),
684
871
  })
685
872
  recordObservation({
@@ -691,7 +878,7 @@ export class ToolExecutor {
691
878
  ...(parent ? { parentToolUseId: parent } : {}),
692
879
  })
693
880
 
694
- return result
881
+ return visibleResult
695
882
  }
696
883
 
697
884
  private buildToolContext(
@@ -729,7 +916,8 @@ export class ToolExecutor {
729
916
  // dispatching outside a batch has no parent call to name. The
730
917
  // per-call context below overrides it with one that does; see
731
918
  // `dispatchNested`.
732
- dispatchTool: (name, input) => this.dispatchNested(name, input, context, recordObservation),
919
+ dispatchTool: (name, input, options) =>
920
+ this.dispatchNested(name, input, context, recordObservation, undefined, options),
733
921
  sandbox: this.config.sandbox,
734
922
  fileReadTracker: this.fileReadTracker,
735
923
  // Bound to this run, once. Binding here rather than passing the
@@ -759,6 +947,7 @@ export class ToolExecutor {
759
947
  toolCall: ToolCall,
760
948
  toolContext: ToolContext,
761
949
  recordObservation: (observation: ToolResultObservation) => void,
950
+ settleProgress: () => Promise<void>,
762
951
  ): Promise<ToolCallOutcome> {
763
952
  let toolName = toolCall.function.name
764
953
 
@@ -1095,6 +1284,11 @@ export class ToolExecutor {
1095
1284
  }
1096
1285
  }
1097
1286
 
1287
+ // The terminal event closes the live row. Every accepted progress update
1288
+ // must settle before it, otherwise a slow host can receive an update for a
1289
+ // call it has already removed. Detached work reporting after a timeout is
1290
+ // ignored because close() also revokes the publisher.
1291
+ await settleProgress()
1098
1292
  await this.emitEvent({
1099
1293
  type: 'tool_completed',
1100
1294
  runId: this.config.runId,
@@ -1180,10 +1374,10 @@ export class ToolExecutor {
1180
1374
  }
1181
1375
 
1182
1376
  const controller = new AbortController()
1183
- const runSignal = this.config.abortSignal
1184
- const onRunAbort = () => controller.abort(runSignal.reason)
1185
- if (runSignal.aborted) controller.abort(runSignal.reason)
1186
- else runSignal.addEventListener('abort', onRunAbort, { once: true })
1377
+ const parentSignal = toolContext.abortSignal
1378
+ const onParentAbort = () => controller.abort(parentSignal.reason)
1379
+ if (parentSignal.aborted) controller.abort(parentSignal.reason)
1380
+ else parentSignal.addEventListener('abort', onParentAbort, { once: true })
1187
1381
 
1188
1382
  let timer: ReturnType<typeof setTimeout> | undefined
1189
1383
  let timedOut = false
@@ -1211,9 +1405,21 @@ export class ToolExecutor {
1211
1405
  )
1212
1406
  })
1213
1407
 
1408
+ const inheritedDispatch = toolContext.dispatchTool
1214
1409
  const execution = this.config.tools.execute(toolName, input, {
1215
1410
  ...toolContext,
1216
1411
  abortSignal: controller.signal,
1412
+ ...(inheritedDispatch
1413
+ ? {
1414
+ dispatchTool: (name: string, nestedInput: unknown, options?: ToolDispatchOptions) =>
1415
+ inheritedDispatch(name, nestedInput, {
1416
+ ...options,
1417
+ signal: options?.signal
1418
+ ? AbortSignal.any([controller.signal, options.signal])
1419
+ : controller.signal,
1420
+ }),
1421
+ }
1422
+ : {}),
1217
1423
  })
1218
1424
  // The loser of the race may still reject later; neutralize it so
1219
1425
  // it is never an unhandled rejection.
@@ -1254,7 +1460,7 @@ export class ToolExecutor {
1254
1460
  return outcome
1255
1461
  } finally {
1256
1462
  if (timer !== undefined) clearTimeout(timer)
1257
- runSignal.removeEventListener('abort', onRunAbort)
1463
+ parentSignal.removeEventListener('abort', onParentAbort)
1258
1464
  }
1259
1465
  }
1260
1466
 
@@ -1457,7 +1663,10 @@ export class ToolExecutor {
1457
1663
  reason: failure.reason,
1458
1664
  message: failure.message,
1459
1665
  ...(tool
1460
- ? { tool, jsonSchema: tool.modelInputSchema ?? renderToolSchema(tool.inputSchema) }
1666
+ ? {
1667
+ tool,
1668
+ jsonSchema: tool.modelInputSchema ?? renderToolSchema(tool.inputSchema),
1669
+ }
1461
1670
  : {}),
1462
1671
  availableTools: this.config.tools.listNames(),
1463
1672
  })
@@ -302,6 +302,11 @@ export interface QueryParams {
302
302
  * store REFUSES rather than dropping the attachment.
303
303
  */
304
304
  attachmentStore?: import('../../store/attachment/index.js').AttachmentStore
305
+ /**
306
+ * Maximum wall-clock time for resolving the run's stored attachments.
307
+ * Defaults to one minute; `0` retains the prior unbounded wait.
308
+ */
309
+ attachmentResolveTimeoutMs?: number
305
310
 
306
311
  /**
307
312
  * How this run reaches the web.
@@ -1192,6 +1197,7 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1192
1197
  resolvedInitialMessages = [
1193
1198
  ...(await resolveAttachments(seeded, params.attachmentStore, {
1194
1199
  signal: params.signal,
1200
+ timeoutMs: params.attachmentResolveTimeoutMs,
1195
1201
  })),
1196
1202
  ]
1197
1203
  params.signal?.throwIfAborted()
@@ -22,8 +22,8 @@ import type { CostInfo, TokenUsage } from '../../../types/common/index.js'
22
22
  import type { MessageId } from '../../../types/ids/index.js'
23
23
  import {
24
24
  createAssistantMessage,
25
+ createRuntimeContextMessage,
25
26
  createSystemMessage,
26
- createUserMessage,
27
27
  } from '../../../types/message/index.js'
28
28
  import type { ToolChoice } from '../../../types/provider/chat.js'
29
29
  import { classifyProviderError } from '../../../types/provider/errors.js'
@@ -47,6 +47,8 @@ import type { ToolCallOutcome } from '../executor.js'
47
47
  import { applyLifecycleHookResults } from '../plugin-hooks.js'
48
48
  import {
49
49
  DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
50
+ type RequestImageIdentity,
51
+ markProviderRejectedImage,
50
52
  projectRequestRichContent,
51
53
  } from '../request-rich-content.js'
52
54
  import { formatSteeringNote } from '../steering.js'
@@ -57,6 +59,7 @@ import type { IterationContext } from './phases/index.js'
57
59
  import { runPlanGate } from './phases/plan.js'
58
60
  import { runToolReview } from './phases/tool-review.js'
59
61
  import { refreshWorkingMemory } from './phases/working-memory.js'
62
+ import { streamWithProviderRejectedImageRecovery } from './provider-rejected-image.js'
60
63
  import { streamProviderTurn } from './stream-turn.js'
61
64
 
62
65
  export type { IterationContext } from './phases/index.js'
@@ -394,8 +397,9 @@ export class IterationOrchestrator {
394
397
  const baseMessages = forceFinalize
395
398
  ? [
396
399
  ...runMgr.messages,
397
- createUserMessage(
400
+ createRuntimeContextMessage(
398
401
  '[SYSTEM] You are approaching your resource limits. Provide your final, comprehensive response now based on everything you have gathered so far. Do not request any more tool calls.',
402
+ 'limit-finalization',
399
403
  ),
400
404
  ]
401
405
  : runMgr.messages
@@ -589,6 +593,9 @@ export class IterationOrchestrator {
589
593
  this.ctx.log,
590
594
  iterSpan,
591
595
  stepMessageId,
596
+ {
597
+ onAccepted: (identity) => this.acceptProviderRejectedImage(identity),
598
+ },
592
599
  )
593
600
  stepResponse = response
594
601
 
@@ -880,7 +887,9 @@ export class IterationOrchestrator {
880
887
  [NAMZU.ITERATION]: iterationNum,
881
888
  [GENAI.USAGE_OUTPUT_TOKENS]: response.usage.completionTokens,
882
889
  })
883
- runMgr.pushMessage(createUserMessage(AUTO_CONTINUATION_USER_MESSAGE))
890
+ runMgr.pushMessage(
891
+ createRuntimeContextMessage(AUTO_CONTINUATION_USER_MESSAGE, 'auto-continuation'),
892
+ )
884
893
  await this.ctx.emitEvent({
885
894
  type: 'iteration_completed',
886
895
  runId: runMgr.id,
@@ -912,7 +921,9 @@ export class IterationOrchestrator {
912
921
  'namzu.retry.attempt': attempt,
913
922
  'namzu.runtime.limit': limit,
914
923
  })
915
- runMgr.pushMessage(createUserMessage(STRUCTURED_OUTPUT_REPROMPT))
924
+ runMgr.pushMessage(
925
+ createRuntimeContextMessage(STRUCTURED_OUTPUT_REPROMPT, 'structured-output'),
926
+ )
916
927
  await this.ctx.emitEvent({
917
928
  type: 'iteration_completed',
918
929
  runId: runMgr.id,
@@ -955,7 +966,7 @@ export class IterationOrchestrator {
955
966
  'namzu.retry.attempt': attempt,
956
967
  'namzu.runtime.limit': limit,
957
968
  })
958
- runMgr.pushMessage(createUserMessage(review.feedback))
969
+ runMgr.pushMessage(createRuntimeContextMessage(review.feedback, 'answer-review'))
959
970
  await this.ctx.emitEvent({
960
971
  type: 'iteration_completed',
961
972
  runId: runMgr.id,
@@ -1188,7 +1199,9 @@ export class IterationOrchestrator {
1188
1199
  [NAMZU.ITERATION]: iterationNum,
1189
1200
  'namzu.runtime.tasks': unheard.map((h) => h.taskId),
1190
1201
  })
1191
- runMgr.pushMessage(createUserMessage(formatCompletionNotification(unheard)))
1202
+ runMgr.pushMessage(
1203
+ createRuntimeContextMessage(formatCompletionNotification(unheard), 'task-completion'),
1204
+ )
1192
1205
  }
1193
1206
 
1194
1207
  // The same seam, for the two channels that could accept text
@@ -1388,7 +1401,9 @@ export class IterationOrchestrator {
1388
1401
  const arrived = this.ctx.completionInbox.drain()
1389
1402
  if (arrived.length === 0) return false
1390
1403
 
1391
- this.ctx.runMgr.pushMessage(createUserMessage(formatCompletionNotification(arrived)))
1404
+ this.ctx.runMgr.pushMessage(
1405
+ createRuntimeContextMessage(formatCompletionNotification(arrived), 'task-completion'),
1406
+ )
1392
1407
  await this.ctx.emitEvent({
1393
1408
  type: 'iteration_completed',
1394
1409
  runId: this.ctx.runMgr.id,
@@ -1467,7 +1482,9 @@ export class IterationOrchestrator {
1467
1482
  [NAMZU.RUN_ID]: this.ctx.runMgr.id,
1468
1483
  'namzu.runtime.tasks': unheard.map((h) => h.taskId),
1469
1484
  })
1470
- this.ctx.runMgr.pushMessage(createUserMessage(formatCompletionNotification(unheard)))
1485
+ this.ctx.runMgr.pushMessage(
1486
+ createRuntimeContextMessage(formatCompletionNotification(unheard), 'task-completion'),
1487
+ )
1471
1488
  }
1472
1489
 
1473
1490
  /**
@@ -1633,7 +1650,9 @@ export class IterationOrchestrator {
1633
1650
  // guidance from a turn that had no result to attach it to.
1634
1651
  const stranded = this.ctx.steering?.drain()
1635
1652
  if (stranded) {
1636
- this.ctx.runMgr.pushMessage(createUserMessage(formatSteeringNote(stranded)))
1653
+ this.ctx.runMgr.pushMessage(
1654
+ createRuntimeContextMessage(formatSteeringNote(stranded), 'steering'),
1655
+ )
1637
1656
  }
1638
1657
 
1639
1658
  return queued.length + (stranded ? 1 : 0)
@@ -1922,6 +1941,23 @@ export class IterationOrchestrator {
1922
1941
  .some((t) => t.state !== 'completed' && t.state !== 'failed' && t.state !== 'canceled')
1923
1942
  }
1924
1943
 
1944
+ private async acceptProviderRejectedImage(identity: RequestImageIdentity): Promise<void> {
1945
+ const repaired = markProviderRejectedImage(this.ctx.runMgr.messages, identity)
1946
+ if (repaired.count === 0) {
1947
+ throw new Error('Provider-rejected image recovery could not find its durable source image')
1948
+ }
1949
+ this.ctx.runMgr.replaceMessages(repaired.messages)
1950
+ await this.ctx.emitEvent?.({
1951
+ type: 'message_history_repaired',
1952
+ runId: this.ctx.runMgr.id,
1953
+ source: 'provider-rejected-image',
1954
+ duplicateToolResultsRemoved: 0,
1955
+ orphanedToolResultsRemoved: 0,
1956
+ syntheticToolResultsInserted: 0,
1957
+ providerRejectedImagesSuppressed: repaired.count,
1958
+ })
1959
+ }
1960
+
1925
1961
  private async requestFinalResponse(model: string, reason: StopReason): Promise<void> {
1926
1962
  const lastAssistant = [...this.ctx.runMgr.messages]
1927
1963
  .reverse()
@@ -1941,8 +1977,9 @@ export class IterationOrchestrator {
1941
1977
  try {
1942
1978
  const finalHistory = [
1943
1979
  ...this.ctx.runMgr.messages,
1944
- createUserMessage(
1980
+ createRuntimeContextMessage(
1945
1981
  `[SYSTEM] Run is ending due to ${reason}. You MUST provide a final response now summarizing all your findings and work so far. Do not use any tools.`,
1982
+ 'limit-finalization',
1946
1983
  ),
1947
1984
  ]
1948
1985
  const finalMessages = projectRequestRichContent(
@@ -1964,26 +2001,29 @@ export class IterationOrchestrator {
1964
2001
  model: requestedMember.model ?? model,
1965
2002
  chainIndex: requestedMember.index,
1966
2003
  }
2004
+ const finalParams = {
2005
+ model,
2006
+ providerRoute: requestedRoute,
2007
+ messages: finalMessages,
2008
+ tools: finalTools.length > 0 ? finalTools : undefined,
2009
+ ...(finalEnforced ? { enforceToolInputSchema: finalEnforced } : {}),
2010
+ toolChoice: finalTools.length > 0 ? 'none' : undefined,
2011
+ temperature: this.ctx.runConfig.temperature,
2012
+ maxTokens: this.ctx.runConfig.maxResponseTokens,
2013
+ cacheControl: { type: 'auto' },
2014
+ ...(this.ctx.runConfig.thinking ? { thinking: this.ctx.runConfig.thinking } : {}),
2015
+ // This turn is a hand-maintained duplicate of the one above, which
2016
+ // is exactly the shape a field goes missing from — so it is tested
2017
+ // separately rather than assumed to have been kept in step.
2018
+ ...(this.ctx.runConfig.effort ? { effort: this.ctx.runConfig.effort } : {}),
2019
+ // Cancellable too: a Stop during the closing summary must not
2020
+ // stream to completion.
2021
+ signal: this.ctx.abortController.signal,
2022
+ } satisfies import('../../../types/provider/index.js').ChatCompletionParams
1967
2023
  const response = await collectChatCompletion(
1968
- this.ctx.provider.chatStream({
1969
- model,
1970
- providerRoute: requestedRoute,
1971
- messages: finalMessages,
1972
- tools: finalTools.length > 0 ? finalTools : undefined,
1973
- ...(finalEnforced ? { enforceToolInputSchema: finalEnforced } : {}),
1974
- toolChoice: finalTools.length > 0 ? 'none' : undefined,
1975
- temperature: this.ctx.runConfig.temperature,
1976
- maxTokens: this.ctx.runConfig.maxResponseTokens,
1977
- cacheControl: { type: 'auto' },
1978
- ...(this.ctx.runConfig.thinking ? { thinking: this.ctx.runConfig.thinking } : {}),
1979
- // This turn is a hand-maintained duplicate of the one above, which
1980
- // is exactly the shape a field goes missing from — so it is tested
1981
- // separately rather than assumed to have been kept in step.
1982
- ...(this.ctx.runConfig.effort ? { effort: this.ctx.runConfig.effort } : {}),
1983
- // Cancellable too: a Stop during the closing summary must not
1984
- // stream to completion.
1985
- signal: this.ctx.abortController.signal,
1986
- }),
2024
+ streamWithProviderRejectedImageRecovery(this.ctx.provider, finalParams, (identity) =>
2025
+ this.acceptProviderRejectedImage(identity),
2026
+ ),
1987
2027
  )
1988
2028
 
1989
2029
  const servingMember = this.ctx.servingMember?.() ?? requestedMember
@@ -2,7 +2,7 @@ import { serializeState } from '../../../../compaction/serializer.js'
2
2
  import { NAMZU } from '../../../../constants/telemetry/index.js'
3
3
  import type { AdvisoryRequest, TriggerEvaluationState } from '../../../../types/advisory/index.js'
4
4
  import { toolResultToText } from '../../../../types/message/content.js'
5
- import { createUserMessage } from '../../../../types/message/index.js'
5
+ import { createRuntimeContextMessage } from '../../../../types/message/index.js'
6
6
  import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
7
7
  import { toErrorMessage } from '../../../../utils/error.js'
8
8
  import type { IterationContext } from './context.js'
@@ -169,7 +169,7 @@ export async function runAdvisoryPhase(
169
169
 
170
170
  sections.push('</advisory-result>')
171
171
 
172
- ctx.runMgr.pushMessage(createUserMessage(sections.join('\n')))
172
+ ctx.runMgr.pushMessage(createRuntimeContextMessage(sections.join('\n'), 'advisory'))
173
173
 
174
174
  ctx.log.info('Advisory phase completed', {
175
175
  [NAMZU.RUN_ID]: ctx.runMgr.id,