salmon-loop 0.2.13 → 0.2.16

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 (218) hide show
  1. package/dist/cli/argv/headless-detection.js +27 -0
  2. package/dist/cli/chat-flow.js +11 -0
  3. package/dist/cli/chat.js +160 -24
  4. package/dist/cli/commands/chat.js +14 -7
  5. package/dist/cli/commands/flow-mode.js +63 -0
  6. package/dist/cli/commands/registry.js +2 -0
  7. package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
  8. package/dist/cli/commands/run/early-errors.js +23 -0
  9. package/dist/cli/commands/run/handler.js +115 -27
  10. package/dist/cli/commands/run/headless-error-writer.js +8 -0
  11. package/dist/cli/commands/run/loop-params.js +2 -0
  12. package/dist/cli/commands/run/mode.js +2 -5
  13. package/dist/cli/commands/run/parse-options.js +16 -0
  14. package/dist/cli/commands/run/persist-session.js +10 -1
  15. package/dist/cli/commands/run/preflight.js +10 -0
  16. package/dist/cli/commands/run/reporter-factory.js +4 -0
  17. package/dist/cli/commands/run/runtime-llm.js +38 -11
  18. package/dist/cli/commands/run/runtime-options.js +2 -2
  19. package/dist/cli/commands/serve.js +91 -71
  20. package/dist/cli/commands/tool-names.js +78 -78
  21. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
  22. package/dist/cli/headless/json-protocol.js +37 -0
  23. package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
  24. package/dist/cli/headless/protocol-metadata.js +22 -0
  25. package/dist/cli/headless/stream-json-protocol.js +34 -1
  26. package/dist/cli/index.js +6 -4
  27. package/dist/cli/locales/en.js +30 -6
  28. package/dist/cli/program-bootstrap.js +8 -3
  29. package/dist/cli/program-commands.js +5 -1
  30. package/dist/cli/reporters/anthropic-stream.js +7 -1
  31. package/dist/cli/reporters/json.js +4 -0
  32. package/dist/cli/reporters/stream-json.js +17 -2
  33. package/dist/cli/run-cli.js +5 -3
  34. package/dist/cli/slash/runtime.js +27 -12
  35. package/dist/cli/ui/components/CommandInput.js +7 -3
  36. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  37. package/dist/cli/utils/command-option-source.js +13 -0
  38. package/dist/cli/utils/verify-resolver.js +8 -4
  39. package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
  40. package/dist/core/adapters/fs/file-adapter.js +6 -0
  41. package/dist/core/adapters/fs/filesystem.js +2 -1
  42. package/dist/core/adapters/git/git-adapter.js +78 -1
  43. package/dist/core/benchmark/patch-artifact.js +124 -0
  44. package/dist/core/benchmark/swe-bench.js +25 -0
  45. package/dist/core/config/load.js +18 -11
  46. package/dist/core/config/resolve-llm.js +12 -0
  47. package/dist/core/config/resolvers/server.js +0 -6
  48. package/dist/core/config/validate.js +73 -21
  49. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  50. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  51. package/dist/core/context/keywords.js +18 -4
  52. package/dist/core/context/service-deps.js +2 -2
  53. package/dist/core/context/service.js +8 -0
  54. package/dist/core/context/steps/context-gather.js +38 -0
  55. package/dist/core/context/summarization/summarizer.js +55 -12
  56. package/dist/core/context/targeting/target-resolver.js +4 -4
  57. package/dist/core/extensions/index.js +23 -5
  58. package/dist/core/extensions/paths.js +31 -0
  59. package/dist/core/extensions/schemas.js +8 -5
  60. package/dist/core/facades/cli-chat.js +6 -2
  61. package/dist/core/facades/cli-command-chat.js +1 -0
  62. package/dist/core/facades/cli-command-tool-names.js +2 -0
  63. package/dist/core/facades/cli-observability.js +1 -1
  64. package/dist/core/facades/cli-run-handler.js +4 -2
  65. package/dist/core/facades/cli-run-persist-session.js +1 -0
  66. package/dist/core/facades/cli-serve.js +2 -4
  67. package/dist/core/facades/cli-utils-worktree.js +1 -1
  68. package/dist/core/failure/diagnostics.js +53 -1
  69. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  70. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  71. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  72. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  73. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  74. package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
  75. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  76. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  77. package/dist/core/grizzco/steps/answer.js +13 -14
  78. package/dist/core/grizzco/steps/autopilot.js +396 -0
  79. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  80. package/dist/core/grizzco/steps/explore.js +37 -21
  81. package/dist/core/grizzco/steps/generateReview.js +2 -5
  82. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  83. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  84. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  85. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  86. package/dist/core/grizzco/steps/patch.js +105 -146
  87. package/dist/core/grizzco/steps/plan.js +101 -25
  88. package/dist/core/grizzco/steps/preflight.js +5 -6
  89. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  90. package/dist/core/grizzco/steps/research.js +39 -36
  91. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  92. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  93. package/dist/core/grizzco/steps/verify.js +13 -21
  94. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  95. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  96. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  97. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  98. package/dist/core/llm/ai-sdk/request-params.js +73 -0
  99. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  100. package/dist/core/llm/ai-sdk.js +112 -27
  101. package/dist/core/llm/capabilities.js +12 -0
  102. package/dist/core/llm/contracts/repair.js +36 -30
  103. package/dist/core/llm/errors.js +83 -2
  104. package/dist/core/llm/message-composition.js +7 -22
  105. package/dist/core/llm/phase-router.js +29 -10
  106. package/dist/core/llm/redact.js +28 -3
  107. package/dist/core/llm/registry.js +2 -0
  108. package/dist/core/llm/request-augmentation.js +55 -0
  109. package/dist/core/llm/request-envelope.js +334 -0
  110. package/dist/core/llm/shared-request-assembly.js +35 -0
  111. package/dist/core/llm/stream-utils.js +13 -4
  112. package/dist/core/llm/utils.js +18 -29
  113. package/dist/core/memory/relevant-retrieval.js +144 -0
  114. package/dist/core/observability/logger.js +11 -2
  115. package/dist/core/patch/diff.js +1 -0
  116. package/dist/core/prompts/registry.js +39 -2
  117. package/dist/core/prompts/runtime.js +50 -12
  118. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  119. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  120. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  121. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  122. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  123. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  124. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  125. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  126. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  127. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  128. package/dist/core/protocols/a2a/agent-card.js +3 -2
  129. package/dist/core/protocols/a2a/sdk/executor.js +2 -1
  130. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  131. package/dist/core/protocols/acp/formal-agent.js +74 -51
  132. package/dist/core/protocols/acp/handlers.js +5 -1
  133. package/dist/core/protocols/acp/permission-provider.js +1 -1
  134. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  135. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  136. package/dist/core/public-capabilities/projections.js +29 -0
  137. package/dist/core/public-capabilities/registry.js +26 -0
  138. package/dist/core/public-capabilities/types.js +2 -0
  139. package/dist/core/runtime/agent-server-runtime.js +47 -43
  140. package/dist/core/runtime/execution-profile.js +67 -0
  141. package/dist/core/session/artifact-state.js +160 -0
  142. package/dist/core/session/compaction/index.js +183 -0
  143. package/dist/core/session/compaction/microcompact.js +78 -0
  144. package/dist/core/session/compaction/tracking.js +48 -0
  145. package/dist/core/session/compaction/types.js +11 -0
  146. package/dist/core/session/compression.js +8 -0
  147. package/dist/core/session/manager.js +244 -8
  148. package/dist/core/session/pruning-strategy.js +55 -9
  149. package/dist/core/session/replacement-preview-provider.js +24 -0
  150. package/dist/core/session/replacement-state.js +131 -0
  151. package/dist/core/session/resume-repair/pipeline.js +79 -0
  152. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  153. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  154. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  155. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  156. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  157. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  158. package/dist/core/session/resume-repair/types.js +2 -0
  159. package/dist/core/session/summary-sync.js +164 -13
  160. package/dist/core/session/token-tracker.js +6 -0
  161. package/dist/core/skills/audit.js +34 -0
  162. package/dist/core/skills/bridge.js +84 -7
  163. package/dist/core/skills/discovery.js +94 -0
  164. package/dist/core/skills/feature-flags.js +52 -0
  165. package/dist/core/skills/index.js +1 -1
  166. package/dist/core/skills/loader.js +195 -20
  167. package/dist/core/skills/parser.js +296 -24
  168. package/dist/core/skills/permissions.js +117 -0
  169. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  170. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  171. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  172. package/dist/core/strata/layers/worktree.js +67 -10
  173. package/dist/core/strata/runtime/synchronizer.js +29 -2
  174. package/dist/core/streaming/stream-assembler.js +75 -31
  175. package/dist/core/sub-agent/context-snapshot.js +156 -0
  176. package/dist/core/sub-agent/core/loop.js +1 -1
  177. package/dist/core/sub-agent/core/manager.js +119 -20
  178. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  179. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  180. package/dist/core/sub-agent/registry-defaults.js +4 -0
  181. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  182. package/dist/core/sub-agent/types.js +134 -5
  183. package/dist/core/tools/audit.js +13 -4
  184. package/dist/core/tools/builtin/ast-grep.js +1 -1
  185. package/dist/core/tools/builtin/ast.js +1 -1
  186. package/dist/core/tools/builtin/benchmark.js +360 -0
  187. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  188. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  189. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  190. package/dist/core/tools/builtin/fs.js +256 -23
  191. package/dist/core/tools/builtin/git.js +2 -2
  192. package/dist/core/tools/builtin/index.js +51 -2
  193. package/dist/core/tools/builtin/interaction.js +8 -1
  194. package/dist/core/tools/builtin/plan.js +37 -15
  195. package/dist/core/tools/builtin/shell.js +1 -1
  196. package/dist/core/tools/loader.js +39 -16
  197. package/dist/core/tools/mapper.js +17 -3
  198. package/dist/core/tools/parallel/scheduler.js +35 -4
  199. package/dist/core/tools/permissions/permission-rules.js +5 -10
  200. package/dist/core/tools/policy.js +6 -1
  201. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  202. package/dist/core/tools/router.js +24 -6
  203. package/dist/core/tools/session.js +458 -48
  204. package/dist/core/tools/tool-visibility.js +62 -0
  205. package/dist/core/tools/types.js +9 -1
  206. package/dist/core/types/execution.js +4 -0
  207. package/dist/core/types/flow-mode.js +8 -0
  208. package/dist/core/utils/path.js +52 -0
  209. package/dist/core/verification/runner.js +4 -1
  210. package/dist/languages/typescript/index.js +4 -1
  211. package/dist/locales/en.js +35 -2
  212. package/dist/utils/eol.js +1 -1
  213. package/package.json +13 -6
  214. package/scripts/fix-es-abstract-compat.js +77 -0
  215. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  216. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  217. package/dist/core/runtime/sidecar-paths.js +0 -47
  218. package/dist/core/runtime/sidecar-route-catalog.js +0 -103
@@ -6,6 +6,7 @@ import { recordAuditEvent } from '../observability/audit-trail.js';
6
6
  import { getLogger } from '../observability/logger.js';
7
7
  import { CanonicalResponsesEventEmitter, } from '../streaming/canonical/canonical-responses-event-emitter.js';
8
8
  import { mapLlmStreamChunkToCanonicalStreamParts } from '../streaming/canonical/parts-from-llm-stream-chunk.js';
9
+ import { ArtifactStore } from '../sub-agent/artifacts/store.js';
9
10
  import { Phase } from '../types/runtime.js';
10
11
  import { isSafeRelativePath, normalizePath } from '../utils/path.js';
11
12
  import { buildHeadlessToolInputPayload } from './headless-payload.js';
@@ -13,7 +14,9 @@ import { toolToOpenAI } from './mapper.js';
13
14
  import { InMemoryLockManager } from './parallel/lock-manager.js';
14
15
  import { PlanPersistence } from './parallel/persistence.js';
15
16
  import { ParallelScheduler } from './parallel/scheduler.js';
17
+ import { isRecoverableToolInputErrorCode } from './recoverable-tool-errors.js';
16
18
  import { ToolCallAccumulator } from './streaming/ToolCallAccumulator.js';
19
+ import { resolveVisibleToolSpecs } from './tool-visibility.js';
17
20
  function safeParseJson(argsText) {
18
21
  if (typeof argsText !== 'string') {
19
22
  return { ok: true, value: argsText };
@@ -80,7 +83,156 @@ function safeStringifyForAudit(value) {
80
83
  return '[Unserializable]';
81
84
  }
82
85
  }
86
+ function buildToolCorrectionHint(result) {
87
+ const errorCode = result.error?.code;
88
+ const tool = result.toolName;
89
+ if (!isRecoverableToolInputErrorCode(errorCode) || !tool)
90
+ return undefined;
91
+ return {
92
+ kind: 'adjust_arguments',
93
+ tool,
94
+ hint: result.error?.message ||
95
+ `Adjust the arguments for ${tool} and retry with a valid JSON object.`,
96
+ retryable: true,
97
+ };
98
+ }
99
+ function normalizeRecoverableToolResult(result) {
100
+ if (!result.error)
101
+ return result;
102
+ const retryHint = buildToolCorrectionHint(result);
103
+ if (!retryHint)
104
+ return result;
105
+ const nextMeta = {
106
+ ...(result.meta ?? {}),
107
+ retryHint,
108
+ };
109
+ const nextError = {
110
+ ...result.error,
111
+ retryable: retryHint.retryable || result.error.retryable,
112
+ };
113
+ return {
114
+ ...result,
115
+ meta: nextMeta,
116
+ error: nextError,
117
+ };
118
+ }
119
+ function isArtifactHandleRecord(value) {
120
+ if (!value || typeof value !== 'object')
121
+ return false;
122
+ const candidate = value;
123
+ return (typeof candidate.handle === 'string' &&
124
+ typeof candidate.mimeType === 'string' &&
125
+ typeof candidate.sha256 === 'string' &&
126
+ typeof candidate.size === 'number');
127
+ }
128
+ function extractArtifactHandlesFromToolOutput(output) {
129
+ if (!isObjectRecord(output)) {
130
+ return {};
131
+ }
132
+ const patchArtifact = isArtifactHandleRecord(output.patchArtifact)
133
+ ? output.patchArtifact
134
+ : undefined;
135
+ const auditArtifact = isArtifactHandleRecord(output.auditArtifact)
136
+ ? output.auditArtifact
137
+ : undefined;
138
+ return {
139
+ patchArtifact,
140
+ auditArtifact,
141
+ };
142
+ }
143
+ function extractRecentReadResult(params) {
144
+ if (params.toolName !== 'fs.read' && params.toolName !== 'code.read') {
145
+ return undefined;
146
+ }
147
+ if (!isObjectRecord(params.output) || typeof params.output.content !== 'string') {
148
+ return undefined;
149
+ }
150
+ const args = safeParseJson(params.rawArgs);
151
+ const argsValue = args.ok ? args.value : params.rawArgs;
152
+ if (!isObjectRecord(argsValue))
153
+ return undefined;
154
+ const file = argsValue.file ?? argsValue.file_path ?? argsValue.filePath ?? argsValue.path;
155
+ if (typeof file !== 'string' || !file.trim())
156
+ return undefined;
157
+ return {
158
+ path: file,
159
+ content: params.output.content,
160
+ };
161
+ }
162
+ async function persistRecentReadArtifact(params) {
163
+ const readResult = extractRecentReadResult(params);
164
+ if (!readResult)
165
+ return undefined;
166
+ const ext = path.extname(readResult.path).replace(/^\./, '') || 'txt';
167
+ const artifact = await ArtifactStore.saveText({
168
+ content: readResult.content,
169
+ mimeType: 'text/plain',
170
+ fileExt: ext,
171
+ });
172
+ return {
173
+ path: readResult.path,
174
+ artifact,
175
+ };
176
+ }
177
+ const TOOL_RESULT_PREVIEW_MIN_CHARS = 1200;
178
+ function sanitizeToolResultPreviewLabel(label) {
179
+ const oneLine = label.replace(/\s+/g, ' ').trim();
180
+ if (!oneLine)
181
+ return 'Tool result preview';
182
+ if (oneLine.length <= 120)
183
+ return oneLine;
184
+ return `${oneLine.slice(0, 119).trimEnd()}…`;
185
+ }
186
+ function serializeToolResultOutputForArtifact(output) {
187
+ if (typeof output === 'string') {
188
+ const text = output.trim();
189
+ if (!text)
190
+ return undefined;
191
+ return {
192
+ content: output,
193
+ mimeType: 'text/plain',
194
+ fileExt: 'txt',
195
+ };
196
+ }
197
+ if (output === undefined)
198
+ return undefined;
199
+ try {
200
+ const json = JSON.stringify(output, null, 2);
201
+ if (!json || !json.trim())
202
+ return undefined;
203
+ return {
204
+ content: json,
205
+ mimeType: 'application/json',
206
+ fileExt: 'json',
207
+ };
208
+ }
209
+ catch {
210
+ return undefined;
211
+ }
212
+ }
213
+ async function persistToolResultPreviewArtifact(params) {
214
+ if (params.toolName === 'fs.read' || params.toolName === 'code.read') {
215
+ return undefined;
216
+ }
217
+ const serialized = serializeToolResultOutputForArtifact(params.output);
218
+ if (!serialized)
219
+ return undefined;
220
+ if (serialized.content.length < TOOL_RESULT_PREVIEW_MIN_CHARS)
221
+ return undefined;
222
+ const artifact = await ArtifactStore.saveText({
223
+ content: serialized.content,
224
+ mimeType: serialized.mimeType,
225
+ fileExt: serialized.fileExt,
226
+ });
227
+ const detail = params.outputSummary ?? params.summary ?? `${params.toolName} output`;
228
+ return {
229
+ label: sanitizeToolResultPreviewLabel(`Tool result preview: ${detail}`),
230
+ artifact,
231
+ };
232
+ }
83
233
  function defaultMaxToolCallsTotalForPhase(phase) {
234
+ if (phase === Phase.AUTOPILOT)
235
+ return 32;
84
236
  if (phase === Phase.EXPLORE)
85
237
  return 18;
86
238
  if (phase === Phase.PLAN)
@@ -90,6 +242,8 @@ function defaultMaxToolCallsTotalForPhase(phase) {
90
242
  return 10;
91
243
  }
92
244
  function defaultMaxToolCallsPerRoundForPhase(phase) {
245
+ if (phase === Phase.AUTOPILOT)
246
+ return 8;
93
247
  if (phase === Phase.EXPLORE)
94
248
  return 6;
95
249
  if (phase === Phase.PLAN)
@@ -98,7 +252,25 @@ function defaultMaxToolCallsPerRoundForPhase(phase) {
98
252
  return 4;
99
253
  return 4;
100
254
  }
101
- function getToolCallBudget(session) {
255
+ function defaultMaxAgentToolCallsTotalForPhase(phase) {
256
+ if (phase === Phase.AUTOPILOT)
257
+ return 4;
258
+ if (phase === Phase.PLAN)
259
+ return 2;
260
+ if (phase === Phase.CONTEXT)
261
+ return 1;
262
+ return 1;
263
+ }
264
+ function defaultMaxAgentToolCallsPerRoundForPhase(phase) {
265
+ if (phase === Phase.AUTOPILOT)
266
+ return 2;
267
+ if (phase === Phase.PLAN)
268
+ return 1;
269
+ if (phase === Phase.CONTEXT)
270
+ return 1;
271
+ return 1;
272
+ }
273
+ function getRegularToolCallBudget(session) {
102
274
  const maxTotal = session.maxToolCallsTotal ?? defaultMaxToolCallsTotalForPhase(session.phase);
103
275
  const maxPerRound = session.maxToolCallsPerRound ?? defaultMaxToolCallsPerRoundForPhase(session.phase);
104
276
  return {
@@ -106,39 +278,62 @@ function getToolCallBudget(session) {
106
278
  maxPerRound: Math.max(0, Math.floor(maxPerRound)),
107
279
  };
108
280
  }
281
+ function getAgentToolCallBudget(session) {
282
+ const maxTotal = session.maxAgentToolCallsTotal ?? defaultMaxAgentToolCallsTotalForPhase(session.phase);
283
+ const maxPerRound = session.maxAgentToolCallsPerRound ?? defaultMaxAgentToolCallsPerRoundForPhase(session.phase);
284
+ return {
285
+ maxTotal: Math.max(0, Math.floor(maxTotal)),
286
+ maxPerRound: Math.max(0, Math.floor(maxPerRound)),
287
+ };
288
+ }
289
+ function createToolCallBudgetBucketsState(session) {
290
+ return {
291
+ regular: { used: 0, ...getRegularToolCallBudget(session) },
292
+ agent: { used: 0, ...getAgentToolCallBudget(session) },
293
+ };
294
+ }
109
295
  function resetToolCallBudgetState(session) {
110
- const budget = getToolCallBudget(session);
111
- const state = { used: 0, ...budget };
296
+ const state = createToolCallBudgetBucketsState(session);
112
297
  session.__toolCallBudgetState = state;
113
298
  return state;
114
299
  }
115
300
  function getToolCallBudgetState(session) {
116
- const budget = getToolCallBudget(session);
117
301
  const anySession = session;
118
302
  const existing = anySession.__toolCallBudgetState;
119
303
  if (!existing) {
120
- const created = { used: 0, ...budget };
304
+ const created = createToolCallBudgetBucketsState(session);
121
305
  anySession.__toolCallBudgetState = created;
122
306
  return created;
123
307
  }
124
308
  // Ensure runtime overrides are respected.
125
- existing.maxTotal = budget.maxTotal;
126
- existing.maxPerRound = budget.maxPerRound;
309
+ const regular = getRegularToolCallBudget(session);
310
+ existing.regular.maxTotal = regular.maxTotal;
311
+ existing.regular.maxPerRound = regular.maxPerRound;
312
+ const agent = getAgentToolCallBudget(session);
313
+ existing.agent.maxTotal = agent.maxTotal;
314
+ existing.agent.maxPerRound = agent.maxPerRound;
127
315
  return existing;
128
316
  }
129
317
  function initToolCallRoundBudget(params) {
130
318
  const budgetState = getToolCallBudgetState(params.session);
131
- const roundCap = Math.min(budgetState.maxPerRound, Math.max(0, budgetState.maxTotal - budgetState.used));
132
- budgetState.used += params.preparedCount;
133
- if (params.preparedCount > roundCap) {
319
+ const roundCaps = {
320
+ regular: Math.min(budgetState.regular.maxPerRound, Math.max(0, budgetState.regular.maxTotal - budgetState.regular.used)),
321
+ agent: Math.min(budgetState.agent.maxPerRound, Math.max(0, budgetState.agent.maxTotal - budgetState.agent.used)),
322
+ };
323
+ budgetState.regular.used += params.preparedCounts.regular;
324
+ budgetState.agent.used += params.preparedCounts.agent;
325
+ for (const bucket of ['regular', 'agent']) {
326
+ const denied = params.preparedCounts[bucket] - roundCaps[bucket];
327
+ if (denied <= 0)
328
+ continue;
134
329
  params.session.emit?.({
135
330
  type: 'log',
136
331
  level: 'warn',
137
- message: `Tool call budget exceeded; denying ${params.preparedCount - roundCap} tool calls (phase=${params.phase}, round=${params.round})`,
332
+ message: `Tool call budget exceeded; denying ${denied} ${bucket} tool calls (phase=${params.phase}, round=${params.round})`,
138
333
  timestamp: new Date(),
139
334
  });
140
335
  }
141
- return { roundCap, budgetState };
336
+ return { roundCaps, budgetState };
142
337
  }
143
338
  function isFunctionCallStreamPart(part) {
144
339
  switch (part.type) {
@@ -192,6 +387,7 @@ async function consumeAssistantStreamTurn(params) {
192
387
  toolChoice: params.openAITools.length > 0 ? 'auto' : undefined,
193
388
  });
194
389
  let content = '';
390
+ let reasoningContent = '';
195
391
  let finishReason;
196
392
  let finishUsage;
197
393
  for await (const chunk of stream) {
@@ -233,6 +429,9 @@ async function consumeAssistantStreamTurn(params) {
233
429
  }
234
430
  content += chunk.contentDelta;
235
431
  }
432
+ if (typeof chunk?.reasoningDelta === 'string' && chunk.reasoningDelta) {
433
+ reasoningContent += chunk.reasoningDelta;
434
+ }
236
435
  params.toolCalls.append(chunk);
237
436
  if (chunk?.done) {
238
437
  finishReason = chunk.finishReason;
@@ -244,11 +443,21 @@ async function consumeAssistantStreamTurn(params) {
244
443
  break;
245
444
  }
246
445
  }
247
- return { content, finishReason, finishUsage };
446
+ return {
447
+ content,
448
+ reasoningContent: reasoningContent.length > 0 ? reasoningContent : undefined,
449
+ finishReason,
450
+ finishUsage,
451
+ };
248
452
  }
249
453
  async function applyEmptyStreamFallback(params) {
250
454
  if (params.content.trim() !== '' || params.collectedToolCalls.length > 0) {
251
- return { usedFallback: false, content: params.content, toolCalls: params.collectedToolCalls };
455
+ return {
456
+ usedFallback: false,
457
+ content: params.content,
458
+ reasoningContent: params.reasoningContent,
459
+ toolCalls: params.collectedToolCalls,
460
+ };
252
461
  }
253
462
  recordAuditEvent('llm.stream.empty_fallback', { phase: params.phase, round: params.round }, { source: 'llm', severity: 'low', scope: 'session', phase: params.phase });
254
463
  const fallback = await params.session.llm.chat(params.messages, {
@@ -259,6 +468,7 @@ async function applyEmptyStreamFallback(params) {
259
468
  toolChoice: params.openAITools.length > 0 ? 'auto' : undefined,
260
469
  });
261
470
  const finalContent = fallback.content || '';
471
+ const finalReasoningContent = fallback.reasoning_content;
262
472
  const finalCalls = Array.isArray(fallback.tool_calls) ? fallback.tool_calls : [];
263
473
  if (params.session.llmOutput && finalContent) {
264
474
  emitLlmOutput({
@@ -269,7 +479,12 @@ async function applyEmptyStreamFallback(params) {
269
479
  content: finalContent,
270
480
  });
271
481
  }
272
- return { usedFallback: true, content: finalContent, toolCalls: finalCalls };
482
+ return {
483
+ usedFallback: true,
484
+ content: finalContent,
485
+ reasoningContent: finalReasoningContent,
486
+ toolCalls: finalCalls,
487
+ };
273
488
  }
274
489
  function emitSynthesizedFunctionCallClosures(params) {
275
490
  if (!params.canonicalEmitter)
@@ -335,22 +550,59 @@ function emitSynthesizedFunctionCallClosures(params) {
335
550
  });
336
551
  }
337
552
  }
338
- function resolveToolCallingForSession(session, phase) {
339
- const allowedSpecs = session.toolstack.registry.listAll().filter((spec) => {
340
- return session.toolstack.policy.decide(phase, spec, {
341
- worktreeRoot: session.runtime.worktreeRoot,
342
- }).allowed;
553
+ const PLAN_RUNTIME_UNAVAILABLE_MARKER = 'No plan.* tools are available in this session.';
554
+ const PLAN_RUNTIME_SESSION_ID_PATTERN = /- sessionId:\s*([^\s]+)/;
555
+ const PLAN_RUNTIME_PATH_HINT_PATTERN = /- planPathHint:\s*([^\s]+)/;
556
+ function inferToolVisibilityRuntimeFromMessages(messages) {
557
+ const systemMessage = messages.find((msg) => msg.role === 'system');
558
+ if (!systemMessage || typeof systemMessage.content !== 'string')
559
+ return undefined;
560
+ const content = systemMessage.content;
561
+ if (content.includes(PLAN_RUNTIME_UNAVAILABLE_MARKER)) {
562
+ return undefined;
563
+ }
564
+ const sessionIdMatch = content.match(PLAN_RUNTIME_SESSION_ID_PATTERN);
565
+ const planPathHintMatch = content.match(PLAN_RUNTIME_PATH_HINT_PATTERN);
566
+ if (!sessionIdMatch || !planPathHintMatch)
567
+ return undefined;
568
+ return {
569
+ plan: {
570
+ sessionId: sessionIdMatch[1],
571
+ planPathHint: planPathHintMatch[1],
572
+ },
573
+ };
574
+ }
575
+ function resolveToolVisibilityRuntime(session, messages) {
576
+ if (session.toolVisibility)
577
+ return session.toolVisibility;
578
+ if (!messages || messages.length === 0)
579
+ return undefined;
580
+ return inferToolVisibilityRuntimeFromMessages(messages);
581
+ }
582
+ function resolveToolCallingForSession(params) {
583
+ const visibilityRuntime = resolveToolVisibilityRuntime(params.session, params.messages);
584
+ const visibleSpecs = resolveVisibleToolSpecs({
585
+ phase: params.phase,
586
+ toolstack: params.session.toolstack,
587
+ worktreeRoot: params.session.runtime.worktreeRoot,
588
+ flowMode: params.session.runtime.flowMode,
589
+ runtime: visibilityRuntime,
343
590
  });
344
- const openAITools = allowedSpecs.map(toolToOpenAI);
345
- return { allowedSpecs, openAITools };
591
+ const openAITools = visibleSpecs.map(toolToOpenAI);
592
+ return { allowedSpecs: visibleSpecs, openAITools };
346
593
  }
347
- function emitToolCallingEnabledLogIfNeeded(session, openAITools) {
594
+ function emitToolCallingEnabledLogIfNeeded(session, openAITools, allowedSpecs) {
348
595
  if (openAITools.length === 0)
349
596
  return;
597
+ const toolNames = allowedSpecs.map((spec) => spec.name).sort();
598
+ const maxNames = 24;
599
+ const visible = toolNames.slice(0, maxNames);
600
+ const overflow = toolNames.length - visible.length;
601
+ const suffix = overflow > 0 ? ` (+${overflow} more)` : '';
350
602
  session.emit?.({
351
603
  type: 'log',
352
604
  level: 'debug',
353
- message: `Tool calling enabled (${openAITools.length} tools available)`,
605
+ message: `Tool calling enabled (${openAITools.length} tools available): ${visible.join(', ')}${suffix}`,
354
606
  timestamp: new Date(),
355
607
  });
356
608
  }
@@ -400,9 +652,13 @@ export async function chatWithTools(initialMessages, chatOptions, session) {
400
652
  const maxRounds = session.maxRounds ?? 6;
401
653
  const phase = session.phase;
402
654
  resetToolCallBudgetState(session);
403
- const { allowedSpecs, openAITools } = resolveToolCallingForSession(session, phase);
404
- emitToolCallingEnabledLogIfNeeded(session, openAITools);
405
655
  const messages = [...initialMessages];
656
+ const { allowedSpecs, openAITools } = resolveToolCallingForSession({
657
+ session,
658
+ phase,
659
+ messages,
660
+ });
661
+ emitToolCallingEnabledLogIfNeeded(session, openAITools, allowedSpecs);
406
662
  for (let round = 0; round < maxRounds; round++) {
407
663
  // Check for abort before starting a new round
408
664
  if (chatOptions.signal?.aborted) {
@@ -443,6 +699,7 @@ export async function chatWithTools(initialMessages, chatOptions, session) {
443
699
  messages.push({
444
700
  role: 'assistant',
445
701
  content: assistant.content || '',
702
+ reasoning_content: assistant.reasoning_content,
446
703
  tool_calls: assistant.tool_calls,
447
704
  });
448
705
  const toolCalls = assistant.tool_calls || [];
@@ -486,6 +743,52 @@ export async function chatWithTools(initialMessages, chatOptions, session) {
486
743
  function isObjectRecord(value) {
487
744
  return typeof value === 'object' && value !== null && !Array.isArray(value);
488
745
  }
746
+ function isPlainObject(value) {
747
+ if (!isObjectRecord(value))
748
+ return false;
749
+ const proto = Object.getPrototypeOf(value);
750
+ return proto === Object.prototype || proto === null;
751
+ }
752
+ function describeValueType(value) {
753
+ if (value === null)
754
+ return 'null';
755
+ if (Array.isArray(value))
756
+ return 'array';
757
+ return typeof value;
758
+ }
759
+ function formatPlanUpdatePatchTypeError(actualType) {
760
+ return (`Invalid field: patch (received ${actualType}). ` +
761
+ 'Expected object with optional keys: status, checkbox, appendSubtasks, note. ' +
762
+ 'Do not JSON-stringify patch.');
763
+ }
764
+ function coercePlanUpdatePatch(args) {
765
+ if (!Object.prototype.hasOwnProperty.call(args, 'patch'))
766
+ return { args };
767
+ const patch = args.patch;
768
+ if (isPlainObject(patch))
769
+ return { args };
770
+ if (typeof patch === 'string') {
771
+ const trimmed = patch.trim();
772
+ const looksLikeObjectLiteral = trimmed.startsWith('{') && trimmed.endsWith('}');
773
+ if (!looksLikeObjectLiteral) {
774
+ return { args, error: formatPlanUpdatePatchTypeError('string') };
775
+ }
776
+ try {
777
+ const parsed = JSON.parse(trimmed);
778
+ if (!isPlainObject(parsed)) {
779
+ return { args, error: formatPlanUpdatePatchTypeError(describeValueType(parsed)) };
780
+ }
781
+ return {
782
+ args: { ...args, patch: parsed },
783
+ coercedPatchSource: 'stringified',
784
+ };
785
+ }
786
+ catch {
787
+ return { args, error: formatPlanUpdatePatchTypeError('string') };
788
+ }
789
+ }
790
+ return { args, error: formatPlanUpdatePatchTypeError(describeValueType(patch)) };
791
+ }
489
792
  function unwrapRetryError(err) {
490
793
  if (!err || typeof err !== 'object')
491
794
  return err;
@@ -680,18 +983,29 @@ function applyStrictToolOutputSchemaValidation(params) {
680
983
  }
681
984
  async function executeToolCalls(session, phase, round, calls, messages, signal) {
682
985
  const prepared = prepareToolCallRequests(calls);
683
- const { roundCap } = initToolCallRoundBudget({
986
+ const bucketByCallId = new Map();
987
+ const preparedCounts = { regular: 0, agent: 0 };
988
+ for (const item of prepared) {
989
+ const spec = typeof item.toolName === 'string'
990
+ ? session.toolstack.registry.listAll().find((s) => s.name === item.toolName)
991
+ : undefined;
992
+ const bucket = spec?.intent === 'AGENT' ? 'agent' : 'regular';
993
+ bucketByCallId.set(item.callId, bucket);
994
+ preparedCounts[bucket]++;
995
+ }
996
+ const { roundCaps } = initToolCallRoundBudget({
684
997
  session,
685
998
  phase,
686
999
  round,
687
- preparedCount: prepared.length,
1000
+ preparedCounts,
688
1001
  });
689
1002
  const toolResults = new Map();
690
1003
  const nodes = [];
691
1004
  const toolArgsPreviewByCallId = new Map();
692
1005
  const rawArgsPreviewByCallId = new Map();
693
1006
  const rawArgsTypeByCallId = new Map();
694
- let allowedUsed = 0;
1007
+ const patchCoercionByCallId = new Map();
1008
+ const allowedUsed = { regular: 0, agent: 0 };
695
1009
  for (const item of prepared) {
696
1010
  const { callId, toolName, rawArgs } = item;
697
1011
  const normalizedToolName = typeof toolName === 'string' ? toolName : 'unknown';
@@ -725,6 +1039,17 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
725
1039
  argsValue = { ...argsValue, file: inferred[0] };
726
1040
  }
727
1041
  }
1042
+ let planUpdatePatchError;
1043
+ if (parsedArgsOk && normalizedToolName === 'plan.update' && isObjectRecord(argsValue)) {
1044
+ const patchGuard = coercePlanUpdatePatch(argsValue);
1045
+ argsValue = patchGuard.args;
1046
+ if (patchGuard.coercedPatchSource) {
1047
+ patchCoercionByCallId.set(callId, patchGuard.coercedPatchSource);
1048
+ }
1049
+ if (patchGuard.error) {
1050
+ planUpdatePatchError = patchGuard.error;
1051
+ }
1052
+ }
728
1053
  if (parsedArgsOk) {
729
1054
  toolArgsPreviewByCallId.set(callId, safeStringifyForAudit(argsValue));
730
1055
  }
@@ -754,7 +1079,8 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
754
1079
  });
755
1080
  // Hard budget: deny tool execution once the session exceeds the configured budget.
756
1081
  // We still return a tool result for protocol completeness and observability.
757
- if (allowedUsed >= roundCap) {
1082
+ const budgetBucket = bucketByCallId.get(callId) ?? 'regular';
1083
+ if (allowedUsed[budgetBucket] >= roundCaps[budgetBucket]) {
758
1084
  toolResults.set(callId, {
759
1085
  id: callId,
760
1086
  toolName: typeof toolName === 'string' ? toolName : 'unknown',
@@ -762,7 +1088,9 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
762
1088
  status: 'error',
763
1089
  error: {
764
1090
  code: 'TOOL_CALL_BUDGET_EXCEEDED',
765
- message: 'Tool call denied: tool calling budget exceeded for this session. Continue without additional tool calls.',
1091
+ message: budgetBucket === 'agent'
1092
+ ? 'Agent delegation denied: delegation budget exceeded for this session. Continue without additional agent dispatches or complete the task directly.'
1093
+ : 'Tool call denied: tool calling budget exceeded for this session. Continue without additional tool calls.',
766
1094
  retryable: false,
767
1095
  failurePhase: phase,
768
1096
  },
@@ -770,7 +1098,7 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
770
1098
  });
771
1099
  continue;
772
1100
  }
773
- allowedUsed++;
1101
+ allowedUsed[budgetBucket]++;
774
1102
  if (!toolName || typeof toolName !== 'string') {
775
1103
  getLogger().warn('Received malformed tool call (missing function.name)');
776
1104
  session.toolCallingAudit?.event({
@@ -835,7 +1163,7 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
835
1163
  });
836
1164
  continue;
837
1165
  }
838
- session.toolCallingAudit?.event({
1166
+ const parsedAuditEntry = {
839
1167
  timestamp: new Date().toISOString(),
840
1168
  phase,
841
1169
  round,
@@ -846,7 +1174,28 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
846
1174
  rawArgsPreview: typeof rawArgs === 'string' ? redactJsonString(rawArgs) : undefined,
847
1175
  parsedArgsOk: true,
848
1176
  parsedArgsPreview: safeStringifyForAudit(argsValue),
849
- });
1177
+ };
1178
+ const patchCoercionSource = patchCoercionByCallId.get(callId);
1179
+ if (patchCoercionSource) {
1180
+ parsedAuditEntry.coercedPatchSource = patchCoercionSource;
1181
+ }
1182
+ session.toolCallingAudit?.event(parsedAuditEntry);
1183
+ if (planUpdatePatchError) {
1184
+ toolResults.set(callId, normalizeRecoverableToolResult({
1185
+ id: callId,
1186
+ toolName,
1187
+ source: 'builtin',
1188
+ status: 'error',
1189
+ error: {
1190
+ code: 'INVALID_INPUT',
1191
+ message: planUpdatePatchError,
1192
+ retryable: true,
1193
+ failurePhase: phase,
1194
+ },
1195
+ durationMs: 0,
1196
+ }));
1197
+ continue;
1198
+ }
850
1199
  nodes.push({ id: callId, toolName, args: argsValue, deps: [] });
851
1200
  }
852
1201
  if (nodes.length > 0) {
@@ -885,7 +1234,11 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
885
1234
  }
886
1235
  for (const item of prepared) {
887
1236
  const { callId, toolName, rawArgs } = item;
888
- const result = toolResults.get(callId);
1237
+ const rawResult = toolResults.get(callId);
1238
+ const result = rawResult ? normalizeRecoverableToolResult(rawResult) : undefined;
1239
+ if (result && result !== rawResult) {
1240
+ toolResults.set(callId, result);
1241
+ }
889
1242
  if (!result)
890
1243
  continue;
891
1244
  // Strict output schema validation
@@ -924,7 +1277,7 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
924
1277
  if (result.status !== 'ok') {
925
1278
  const errorCode = result.error?.code;
926
1279
  const attachArgsPreview = errorCode === 'INVALID_INPUT';
927
- session.toolCallingAudit?.event({
1280
+ const errorAuditEntry = {
928
1281
  timestamp: new Date().toISOString(),
929
1282
  phase,
930
1283
  round,
@@ -939,6 +1292,45 @@ async function executeToolCalls(session, phase, round, calls, messages, signal)
939
1292
  : undefined,
940
1293
  rawArgsPreview: attachArgsPreview ? rawArgsPreviewByCallId.get(callId) : undefined,
941
1294
  parsedArgsPreview: attachArgsPreview ? toolArgsPreviewByCallId.get(callId) : undefined,
1295
+ };
1296
+ const patchCoercionSource = patchCoercionByCallId.get(callId);
1297
+ if (patchCoercionSource) {
1298
+ errorAuditEntry.coercedPatchSource = patchCoercionSource;
1299
+ }
1300
+ session.toolCallingAudit?.event(errorAuditEntry);
1301
+ }
1302
+ else {
1303
+ const toolResultOutputOk = isObjectRecord(result.output) && typeof result.output.ok === 'boolean'
1304
+ ? result.output.ok
1305
+ : undefined;
1306
+ const artifacts = extractArtifactHandlesFromToolOutput(result.output);
1307
+ const recentReadArtifact = await persistRecentReadArtifact({
1308
+ toolName: typeof toolName === 'string' ? toolName : 'unknown',
1309
+ rawArgs,
1310
+ output: result.output,
1311
+ });
1312
+ const toolResultPreviewArtifact = await persistToolResultPreviewArtifact({
1313
+ toolName: typeof toolName === 'string' ? toolName : 'unknown',
1314
+ output: result.output,
1315
+ summary: result.summary,
1316
+ outputSummary: result.outputSummary,
1317
+ });
1318
+ session.toolCallingAudit?.event({
1319
+ timestamp: new Date().toISOString(),
1320
+ phase,
1321
+ round,
1322
+ callId,
1323
+ toolName: typeof toolName === 'string' ? toolName : 'unknown',
1324
+ rawArgsType: rawArgsTypeByCallId.get(callId) ?? typeof rawArgs,
1325
+ parsedArgsOk: true,
1326
+ toolResultOutputOk,
1327
+ toolResultStatus: result.status,
1328
+ toolResultPatchArtifact: artifacts.patchArtifact,
1329
+ toolResultAuditArtifact: artifacts.auditArtifact,
1330
+ toolResultReadArtifact: recentReadArtifact?.artifact,
1331
+ toolResultReadArtifactPath: recentReadArtifact?.path,
1332
+ toolResultPreviewArtifact: toolResultPreviewArtifact?.artifact,
1333
+ toolResultPreviewLabel: toolResultPreviewArtifact?.label,
942
1334
  });
943
1335
  }
944
1336
  messages.push({
@@ -965,9 +1357,13 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
965
1357
  const maxRounds = session.maxRounds ?? 6;
966
1358
  const phase = session.phase;
967
1359
  resetToolCallBudgetState(session);
968
- const { allowedSpecs, openAITools } = resolveToolCallingForSession(session, phase);
969
- emitToolCallingEnabledLogIfNeeded(session, openAITools);
970
1360
  const messages = [...initialMessages];
1361
+ const { allowedSpecs, openAITools } = resolveToolCallingForSession({
1362
+ session,
1363
+ phase,
1364
+ messages,
1365
+ });
1366
+ emitToolCallingEnabledLogIfNeeded(session, openAITools, allowedSpecs);
971
1367
  const canonicalEmitters = createCanonicalEmitterRegistry(session);
972
1368
  try {
973
1369
  for (let round = 0; round < maxRounds; round++) {
@@ -980,6 +1376,7 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
980
1376
  let finishReason;
981
1377
  try {
982
1378
  let streamContent = '';
1379
+ let streamReasoningContent;
983
1380
  let finishUsage;
984
1381
  try {
985
1382
  const consumed = await consumeAssistantStreamTurn({
@@ -996,6 +1393,7 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
996
1393
  toolCalls,
997
1394
  });
998
1395
  streamContent = consumed.content;
1396
+ streamReasoningContent = consumed.reasoningContent;
999
1397
  finishReason = consumed.finishReason;
1000
1398
  finishUsage = consumed.finishUsage;
1001
1399
  }
@@ -1036,12 +1434,14 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
1036
1434
  phase,
1037
1435
  round,
1038
1436
  content: streamContent,
1437
+ reasoningContent: streamReasoningContent,
1039
1438
  collectedToolCalls: drainedToolCalls,
1040
1439
  });
1041
1440
  usedFallback = fallback.usedFallback;
1042
1441
  const assistant = {
1043
1442
  role: 'assistant',
1044
1443
  content: fallback.content,
1444
+ reasoning_content: fallback.reasoningContent,
1045
1445
  tool_calls: fallback.toolCalls.length > 0 ? fallback.toolCalls : undefined,
1046
1446
  };
1047
1447
  if (session.emit && session.llmOutput) {
@@ -1056,16 +1456,6 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
1056
1456
  emittedModelToolCallIds,
1057
1457
  });
1058
1458
  }
1059
- if (session.llmOutput) {
1060
- emitLlmStreamEnd({
1061
- emit: session.emit,
1062
- policy: session.llmOutput.policy,
1063
- kind: session.llmOutput.kind,
1064
- step: session.llmOutput.step,
1065
- streamId,
1066
- finishReason,
1067
- });
1068
- }
1069
1459
  recordAuditEvent('llm.round', {
1070
1460
  status: 'ok',
1071
1461
  streamed: true,
@@ -1081,9 +1471,29 @@ export async function chatWithToolsStreaming(initialMessages, chatOptions, sessi
1081
1471
  messages.push(assistant);
1082
1472
  const calls = assistant.tool_calls || [];
1083
1473
  if (!Array.isArray(calls) || calls.length === 0) {
1474
+ if (session.llmOutput) {
1475
+ emitLlmStreamEnd({
1476
+ emit: session.emit,
1477
+ policy: session.llmOutput.policy,
1478
+ kind: session.llmOutput.kind,
1479
+ step: session.llmOutput.step,
1480
+ streamId,
1481
+ finishReason,
1482
+ });
1483
+ }
1084
1484
  return assistant;
1085
1485
  }
1086
1486
  await executeToolCalls(session, phase, round, calls, messages, chatOptions.signal);
1487
+ if (session.llmOutput) {
1488
+ emitLlmStreamEnd({
1489
+ emit: session.emit,
1490
+ policy: session.llmOutput.policy,
1491
+ kind: session.llmOutput.kind,
1492
+ step: session.llmOutput.step,
1493
+ streamId,
1494
+ finishReason,
1495
+ });
1496
+ }
1087
1497
  }
1088
1498
  finally {
1089
1499
  canonicalEmitters.release(streamId);