salmon-loop 0.2.13 → 0.3.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 (224) 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 +97 -77
  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 +10 -5
  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/backends/salmon-loop/task-executor.js +1 -0
  44. package/dist/core/benchmark/patch-artifact.js +124 -0
  45. package/dist/core/benchmark/swe-bench.js +25 -0
  46. package/dist/core/config/load.js +18 -11
  47. package/dist/core/config/resolve-llm.js +12 -0
  48. package/dist/core/config/resolvers/server.js +0 -6
  49. package/dist/core/config/validate.js +73 -21
  50. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  51. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  52. package/dist/core/context/keywords.js +18 -4
  53. package/dist/core/context/service-deps.js +2 -2
  54. package/dist/core/context/service.js +8 -0
  55. package/dist/core/context/steps/context-gather.js +38 -0
  56. package/dist/core/context/summarization/summarizer.js +55 -12
  57. package/dist/core/context/targeting/target-resolver.js +4 -4
  58. package/dist/core/extensions/index.js +23 -5
  59. package/dist/core/extensions/merge.js +14 -0
  60. package/dist/core/extensions/paths.js +31 -0
  61. package/dist/core/extensions/schemas.js +8 -5
  62. package/dist/core/facades/cli-chat.js +6 -2
  63. package/dist/core/facades/cli-command-chat.js +1 -0
  64. package/dist/core/facades/cli-command-tool-names.js +2 -0
  65. package/dist/core/facades/cli-observability.js +1 -1
  66. package/dist/core/facades/cli-program-bootstrap.js +1 -0
  67. package/dist/core/facades/cli-run-handler.js +4 -2
  68. package/dist/core/facades/cli-run-persist-session.js +1 -0
  69. package/dist/core/facades/cli-serve.js +4 -4
  70. package/dist/core/facades/cli-utils-worktree.js +1 -1
  71. package/dist/core/failure/diagnostics.js +53 -1
  72. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  73. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  74. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  75. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  76. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  77. package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
  78. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  79. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  80. package/dist/core/grizzco/steps/answer.js +13 -14
  81. package/dist/core/grizzco/steps/autopilot.js +396 -0
  82. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  83. package/dist/core/grizzco/steps/explore.js +37 -21
  84. package/dist/core/grizzco/steps/generateReview.js +2 -5
  85. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  86. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  87. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  88. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  89. package/dist/core/grizzco/steps/patch.js +105 -146
  90. package/dist/core/grizzco/steps/plan.js +101 -25
  91. package/dist/core/grizzco/steps/preflight.js +5 -6
  92. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  93. package/dist/core/grizzco/steps/research.js +39 -36
  94. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  95. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  96. package/dist/core/grizzco/steps/verify.js +13 -21
  97. package/dist/core/interaction/orchestration/facade.js +1 -1
  98. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  99. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  100. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  101. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  102. package/dist/core/llm/ai-sdk/request-params.js +113 -1
  103. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  104. package/dist/core/llm/ai-sdk.js +112 -27
  105. package/dist/core/llm/capabilities.js +12 -0
  106. package/dist/core/llm/contracts/repair.js +36 -30
  107. package/dist/core/llm/errors.js +83 -2
  108. package/dist/core/llm/message-composition.js +7 -22
  109. package/dist/core/llm/phase-router.js +29 -10
  110. package/dist/core/llm/redact.js +28 -3
  111. package/dist/core/llm/registry.js +2 -0
  112. package/dist/core/llm/request-augmentation.js +55 -0
  113. package/dist/core/llm/request-envelope.js +334 -0
  114. package/dist/core/llm/shared-request-assembly.js +35 -0
  115. package/dist/core/llm/stream-utils.js +13 -4
  116. package/dist/core/llm/utils.js +18 -29
  117. package/dist/core/memory/relevant-retrieval.js +144 -0
  118. package/dist/core/observability/logger.js +11 -2
  119. package/dist/core/patch/diff.js +1 -0
  120. package/dist/core/prompts/registry.js +39 -2
  121. package/dist/core/prompts/runtime.js +50 -12
  122. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  123. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  124. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  125. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  126. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  127. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  128. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  129. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  130. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  131. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  132. package/dist/core/protocols/a2a/agent-card.js +5 -3
  133. package/dist/core/protocols/a2a/sdk/executor.js +2 -1
  134. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  135. package/dist/core/protocols/acp/formal-agent.js +300 -58
  136. package/dist/core/protocols/acp/handlers.js +5 -1
  137. package/dist/core/protocols/acp/permission-provider.js +1 -1
  138. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  139. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  140. package/dist/core/public-capabilities/projections.js +29 -0
  141. package/dist/core/public-capabilities/registry.js +26 -0
  142. package/dist/core/public-capabilities/types.js +2 -0
  143. package/dist/core/runtime/agent-server-runtime.js +47 -43
  144. package/dist/core/runtime/execution-profile.js +67 -0
  145. package/dist/core/session/artifact-state.js +160 -0
  146. package/dist/core/session/compaction/index.js +183 -0
  147. package/dist/core/session/compaction/microcompact.js +78 -0
  148. package/dist/core/session/compaction/tracking.js +48 -0
  149. package/dist/core/session/compaction/types.js +11 -0
  150. package/dist/core/session/compression.js +8 -0
  151. package/dist/core/session/manager.js +244 -8
  152. package/dist/core/session/pruning-strategy.js +55 -9
  153. package/dist/core/session/replacement-preview-provider.js +24 -0
  154. package/dist/core/session/replacement-state.js +131 -0
  155. package/dist/core/session/resume-repair/pipeline.js +79 -0
  156. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  157. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  158. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  159. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  160. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  161. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  162. package/dist/core/session/resume-repair/types.js +2 -0
  163. package/dist/core/session/summary-sync.js +164 -13
  164. package/dist/core/session/token-tracker.js +6 -0
  165. package/dist/core/skills/audit.js +34 -0
  166. package/dist/core/skills/bridge.js +84 -7
  167. package/dist/core/skills/discovery.js +94 -0
  168. package/dist/core/skills/feature-flags.js +52 -0
  169. package/dist/core/skills/index.js +1 -1
  170. package/dist/core/skills/loader.js +195 -20
  171. package/dist/core/skills/parser.js +296 -24
  172. package/dist/core/skills/permissions.js +117 -0
  173. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  174. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  175. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  176. package/dist/core/strata/layers/worktree.js +67 -10
  177. package/dist/core/strata/runtime/synchronizer.js +29 -2
  178. package/dist/core/streaming/stream-assembler.js +75 -31
  179. package/dist/core/sub-agent/context-snapshot.js +156 -0
  180. package/dist/core/sub-agent/core/loop.js +1 -1
  181. package/dist/core/sub-agent/core/manager.js +119 -20
  182. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  183. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  184. package/dist/core/sub-agent/registry-defaults.js +4 -0
  185. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  186. package/dist/core/sub-agent/types.js +134 -5
  187. package/dist/core/tools/audit.js +13 -4
  188. package/dist/core/tools/builtin/ast-grep.js +1 -1
  189. package/dist/core/tools/builtin/ast.js +1 -1
  190. package/dist/core/tools/builtin/benchmark.js +360 -0
  191. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  192. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  193. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  194. package/dist/core/tools/builtin/fs.js +256 -23
  195. package/dist/core/tools/builtin/git.js +2 -2
  196. package/dist/core/tools/builtin/index.js +51 -2
  197. package/dist/core/tools/builtin/interaction.js +8 -1
  198. package/dist/core/tools/builtin/plan.js +37 -15
  199. package/dist/core/tools/builtin/shell.js +1 -1
  200. package/dist/core/tools/loader.js +39 -16
  201. package/dist/core/tools/mapper.js +17 -3
  202. package/dist/core/tools/mcp/client.js +2 -1
  203. package/dist/core/tools/parallel/scheduler.js +35 -4
  204. package/dist/core/tools/permissions/permission-rules.js +5 -10
  205. package/dist/core/tools/policy.js +6 -1
  206. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  207. package/dist/core/tools/router.js +24 -6
  208. package/dist/core/tools/session.js +458 -48
  209. package/dist/core/tools/tool-visibility.js +62 -0
  210. package/dist/core/tools/types.js +9 -1
  211. package/dist/core/types/execution.js +4 -0
  212. package/dist/core/types/flow-mode.js +8 -0
  213. package/dist/core/utils/path.js +52 -0
  214. package/dist/core/verification/runner.js +4 -1
  215. package/dist/core/version.js +17 -0
  216. package/dist/languages/typescript/index.js +4 -1
  217. package/dist/locales/en.js +35 -2
  218. package/dist/utils/eol.js +1 -1
  219. package/package.json +14 -7
  220. package/scripts/fix-es-abstract-compat.js +77 -0
  221. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  222. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  223. package/dist/core/runtime/sidecar-paths.js +0 -47
  224. package/dist/core/runtime/sidecar-route-catalog.js +0 -103
@@ -2,12 +2,15 @@ import { getExitCode } from '../runtime/exit-codes.js';
2
2
  import { normalizeStopReason } from './normalized-events.js';
3
3
  export class StreamAssembler {
4
4
  clock;
5
+ deferToolRequestsUntilExecutionInput;
5
6
  streams = new Map();
6
7
  canonicalTextStreams = new Set();
7
8
  canonicalClosedTextStreams = new Set();
8
9
  toolCallStates = new Map();
9
10
  constructor(options = {}) {
10
11
  this.clock = options.clock ?? (() => new Date());
12
+ this.deferToolRequestsUntilExecutionInput =
13
+ options.deferToolRequestsUntilExecutionInput ?? false;
11
14
  }
12
15
  push(event) {
13
16
  if (event.type === 'llm.responses.event') {
@@ -27,33 +30,42 @@ export class StreamAssembler {
27
30
  return [];
28
31
  }
29
32
  this.canonicalTextStreams.delete(event.streamId);
30
- return this.handleTextEnd(event.streamId, event.timestamp, event.finishReason);
33
+ return [
34
+ ...this.flushPendingToolRequests(event.streamId),
35
+ ...this.handleTextEnd(event.streamId, event.timestamp, event.finishReason),
36
+ ];
31
37
  }
32
38
  if (event.type === 'tool.call.start') {
33
39
  const out = [];
34
40
  const st = this.getToolCallState(event.callId);
35
41
  if (!st.requestStarted) {
36
42
  st.requestStarted = true;
37
- out.push({
43
+ st.request = {
38
44
  type: 'normalized.tool_request_start',
39
45
  callId: event.callId,
40
46
  toolName: event.toolName,
41
47
  phase: event.phase,
42
48
  round: event.round,
49
+ ...(event.input === undefined ? {} : { input: event.input }),
43
50
  timestamp: event.timestamp,
44
- });
51
+ };
45
52
  }
53
+ else if (event.input !== undefined && st.request && st.request.input === undefined) {
54
+ st.request = { ...st.request, input: event.input };
55
+ }
56
+ out.push(...this.emitPendingToolRequest(st));
46
57
  if (!st.requestEnded) {
47
58
  st.requestEnded = true;
48
- out.push({
59
+ st.requestEnd = {
49
60
  type: 'normalized.tool_request_end',
50
61
  callId: event.callId,
51
62
  toolName: event.toolName,
52
63
  phase: event.phase,
53
64
  round: event.round,
54
65
  timestamp: event.timestamp,
55
- });
66
+ };
56
67
  }
68
+ out.push(...this.emitPendingToolRequestEnd(st));
57
69
  if (st.executionStarted)
58
70
  return out;
59
71
  st.executionStarted = true;
@@ -73,26 +85,28 @@ export class StreamAssembler {
73
85
  const st = this.getToolCallState(event.callId);
74
86
  if (!st.requestStarted) {
75
87
  st.requestStarted = true;
76
- out.push({
88
+ st.request = {
77
89
  type: 'normalized.tool_request_start',
78
90
  callId: event.callId,
79
91
  toolName: event.toolName,
80
92
  phase: event.phase,
81
93
  round: event.round,
82
94
  timestamp: event.timestamp,
83
- });
95
+ };
84
96
  }
97
+ out.push(...this.emitPendingToolRequest(st));
85
98
  if (!st.requestEnded) {
86
99
  st.requestEnded = true;
87
- out.push({
100
+ st.requestEnd = {
88
101
  type: 'normalized.tool_request_end',
89
102
  callId: event.callId,
90
103
  toolName: event.toolName,
91
104
  phase: event.phase,
92
105
  round: event.round,
93
106
  timestamp: event.timestamp,
94
- });
107
+ };
95
108
  }
109
+ out.push(...this.emitPendingToolRequestEnd(st));
96
110
  out.push({
97
111
  type: 'normalized.tool_call_end',
98
112
  callId: event.callId,
@@ -211,7 +225,10 @@ export class StreamAssembler {
211
225
  return [];
212
226
  this.canonicalClosedTextStreams.add(event.streamId);
213
227
  this.canonicalTextStreams.delete(event.streamId);
214
- return this.handleTextEnd(event.streamId, event.timestamp, undefined);
228
+ return [
229
+ ...this.flushPendingToolRequests(event.streamId),
230
+ ...this.handleTextEnd(event.streamId, event.timestamp, undefined),
231
+ ];
215
232
  }
216
233
  if (isOutputItemAddedMessageEvent(event.event) ||
217
234
  isContentPartAddedOutputTextEvent(event.event)) {
@@ -223,7 +240,10 @@ export class StreamAssembler {
223
240
  return [];
224
241
  this.canonicalClosedTextStreams.add(event.streamId);
225
242
  this.canonicalTextStreams.delete(event.streamId);
226
- return this.handleTextEnd(event.streamId, event.timestamp, undefined);
243
+ return [
244
+ ...this.flushPendingToolRequests(event.streamId),
245
+ ...this.handleTextEnd(event.streamId, event.timestamp, undefined),
246
+ ];
227
247
  }
228
248
  if (isOutputItemAddedFunctionCallEvent(event.event)) {
229
249
  if (!event.phase || typeof event.round !== 'number')
@@ -231,38 +251,38 @@ export class StreamAssembler {
231
251
  const callId = event.event.item.call_id;
232
252
  const toolName = event.event.item.name;
233
253
  const st = this.getToolCallState(callId);
254
+ st.streamId ??= event.streamId;
234
255
  if (st.requestStarted)
235
256
  return [];
236
257
  st.requestStarted = true;
237
- return [
238
- {
239
- type: 'normalized.tool_request_start',
240
- callId,
241
- toolName,
242
- phase: event.phase,
243
- round: event.round,
244
- timestamp: event.timestamp,
245
- },
246
- ];
258
+ st.request = {
259
+ type: 'normalized.tool_request_start',
260
+ callId,
261
+ toolName,
262
+ phase: event.phase,
263
+ round: event.round,
264
+ timestamp: event.timestamp,
265
+ };
266
+ return this.deferToolRequestsUntilExecutionInput ? [] : this.emitPendingToolRequest(st);
247
267
  }
248
268
  if (isOutputItemDoneFunctionCallEvent(event.event)) {
249
269
  if (!event.phase || typeof event.round !== 'number')
250
270
  return [];
251
271
  const callId = event.event.item.call_id;
252
272
  const st = this.getToolCallState(callId);
273
+ st.streamId ??= event.streamId;
253
274
  if (st.requestEnded)
254
275
  return [];
255
276
  st.requestEnded = true;
256
- return [
257
- {
258
- type: 'normalized.tool_request_end',
259
- callId,
260
- toolName: event.event.item.name,
261
- phase: event.phase,
262
- round: event.round,
263
- timestamp: event.timestamp,
264
- },
265
- ];
277
+ st.requestEnd = {
278
+ type: 'normalized.tool_request_end',
279
+ callId,
280
+ toolName: event.event.item.name,
281
+ phase: event.phase,
282
+ round: event.round,
283
+ timestamp: event.timestamp,
284
+ };
285
+ return this.emitPendingToolRequestEnd(st);
266
286
  }
267
287
  return [];
268
288
  }
@@ -274,10 +294,34 @@ export class StreamAssembler {
274
294
  requestStarted: false,
275
295
  requestEnded: false,
276
296
  executionStarted: false,
297
+ emittedRequest: false,
298
+ emittedRequestEnd: false,
277
299
  };
278
300
  this.toolCallStates.set(callId, created);
279
301
  return created;
280
302
  }
303
+ emitPendingToolRequest(st) {
304
+ if (st.emittedRequest || !st.request)
305
+ return [];
306
+ st.emittedRequest = true;
307
+ return [st.request];
308
+ }
309
+ emitPendingToolRequestEnd(st) {
310
+ if (st.emittedRequestEnd || !st.requestEnd || !st.emittedRequest)
311
+ return [];
312
+ st.emittedRequestEnd = true;
313
+ return [st.requestEnd];
314
+ }
315
+ flushPendingToolRequests(streamId) {
316
+ const out = [];
317
+ for (const st of this.toolCallStates.values()) {
318
+ if (st.streamId !== streamId)
319
+ continue;
320
+ out.push(...this.emitPendingToolRequest(st));
321
+ out.push(...this.emitPendingToolRequestEnd(st));
322
+ }
323
+ return out;
324
+ }
281
325
  }
282
326
  function isOutputTextDeltaEvent(event) {
283
327
  return (event.type === 'response.output_text.delta' &&
@@ -0,0 +1,156 @@
1
+ import { normalizeToolResultReplacementState, } from '../session/replacement-state.js';
2
+ import { SUB_AGENT_CONTEXT_SNAPSHOT_VERSION, SUB_AGENT_CONTEXT_SNAPSHOT_FIELD_SEMANTICS, } from './types.js';
3
+ function deepClone(value) {
4
+ if (typeof structuredClone === 'function') {
5
+ return structuredClone(value);
6
+ }
7
+ return JSON.parse(JSON.stringify(value));
8
+ }
9
+ function cloneArtifactHandle(artifact) {
10
+ if (!artifact)
11
+ return undefined;
12
+ return {
13
+ handle: artifact.handle,
14
+ mimeType: artifact.mimeType,
15
+ sha256: artifact.sha256,
16
+ size: artifact.size,
17
+ };
18
+ }
19
+ function cloneConversationContext(messages) {
20
+ if (!Array.isArray(messages) || messages.length === 0)
21
+ return undefined;
22
+ return messages.map((message) => {
23
+ const cloned = {
24
+ role: message.role,
25
+ content: message.content,
26
+ };
27
+ if (message.name !== undefined)
28
+ cloned.name = message.name;
29
+ if (message.reasoning_content !== undefined)
30
+ cloned.reasoning_content = message.reasoning_content;
31
+ if (message.tool_call_id !== undefined)
32
+ cloned.tool_call_id = message.tool_call_id;
33
+ if (Array.isArray(message.tool_calls)) {
34
+ cloned.tool_calls = deepClone(message.tool_calls);
35
+ }
36
+ return cloned;
37
+ });
38
+ }
39
+ function cloneToolCallingAudit(entries) {
40
+ if (!Array.isArray(entries) || entries.length === 0)
41
+ return undefined;
42
+ return entries.map((entry) => deepClone(entry));
43
+ }
44
+ function cloneArtifactHints(hints) {
45
+ if (!hints)
46
+ return undefined;
47
+ const verifyArtifact = cloneArtifactHandle(hints.verifyArtifact);
48
+ const subAgentPatchArtifacts = hints.subAgentPatchArtifacts?.map((artifact) => cloneArtifactHandle(artifact));
49
+ const subAgentAuditArtifacts = hints.subAgentAuditArtifacts?.map((artifact) => cloneArtifactHandle(artifact));
50
+ const recentReadArtifacts = hints.recentReadArtifacts?.map((item) => ({
51
+ path: item.path,
52
+ artifact: cloneArtifactHandle(item.artifact),
53
+ }));
54
+ const toolResultPreviewArtifacts = hints.toolResultPreviewArtifacts?.map((item) => ({
55
+ label: item.label,
56
+ artifact: cloneArtifactHandle(item.artifact),
57
+ }));
58
+ if (!verifyArtifact &&
59
+ !subAgentPatchArtifacts?.length &&
60
+ !subAgentAuditArtifacts?.length &&
61
+ !recentReadArtifacts?.length &&
62
+ !toolResultPreviewArtifacts?.length) {
63
+ return undefined;
64
+ }
65
+ return {
66
+ verifyArtifact,
67
+ subAgentPatchArtifacts,
68
+ subAgentAuditArtifacts,
69
+ recentReadArtifacts,
70
+ toolResultPreviewArtifacts,
71
+ };
72
+ }
73
+ function cloneReplacementState(state) {
74
+ const normalized = normalizeToolResultReplacementState(state);
75
+ if (!normalized)
76
+ return undefined;
77
+ return {
78
+ schemaVersion: normalized.schemaVersion,
79
+ entries: Object.fromEntries(Object.entries(normalized.entries).map(([key, value]) => [
80
+ key,
81
+ {
82
+ toolResultId: value.toolResultId,
83
+ decision: value.decision,
84
+ preview: value.preview,
85
+ frozenAt: value.frozenAt,
86
+ sourceArtifactHandle: value.sourceArtifactHandle,
87
+ identityVersion: value.identityVersion,
88
+ hashAlgorithm: value.hashAlgorithm,
89
+ },
90
+ ])),
91
+ };
92
+ }
93
+ function hasAnySnapshotData(snapshot) {
94
+ return Boolean(snapshot.conversationContext ||
95
+ snapshot.artifactHints ||
96
+ snapshot.toolCallingAudit ||
97
+ snapshot.replacementState ||
98
+ snapshot.planRuntime ||
99
+ snapshot.cacheSharing);
100
+ }
101
+ function normalizeSnapshotVersion(snapshot) {
102
+ const version = snapshot.version ?? SUB_AGENT_CONTEXT_SNAPSHOT_VERSION;
103
+ if (version !== SUB_AGENT_CONTEXT_SNAPSHOT_VERSION) {
104
+ throw new Error(`Unsupported sub-agent context snapshot version: ${version}`);
105
+ }
106
+ return version;
107
+ }
108
+ function assertSupportedSnapshotFields(snapshot) {
109
+ const supportedFields = new Set([
110
+ 'version',
111
+ ...Object.keys(SUB_AGENT_CONTEXT_SNAPSHOT_FIELD_SEMANTICS),
112
+ ]);
113
+ const unknownFields = Object.keys(snapshot).filter((key) => !supportedFields.has(key));
114
+ if (unknownFields.length > 0) {
115
+ throw new Error(`Unsupported sub-agent context snapshot fields: ${unknownFields.sort().join(', ')}`);
116
+ }
117
+ }
118
+ /**
119
+ * Applies the Stage 5 protocol:
120
+ * - mutable runtime state is cloned by default
121
+ * - session infrastructure metadata remains shared by reference
122
+ */
123
+ export function cloneSubAgentContextSnapshot(snapshot) {
124
+ if (!snapshot)
125
+ return undefined;
126
+ assertSupportedSnapshotFields(snapshot);
127
+ const version = normalizeSnapshotVersion(snapshot);
128
+ const cloned = {
129
+ version,
130
+ conversationContext: cloneConversationContext(snapshot.conversationContext),
131
+ artifactHints: cloneArtifactHints(snapshot.artifactHints),
132
+ toolCallingAudit: cloneToolCallingAudit(snapshot.toolCallingAudit),
133
+ replacementState: cloneReplacementState(snapshot.replacementState),
134
+ planRuntime: snapshot.planRuntime,
135
+ cacheSharing: snapshot.cacheSharing,
136
+ };
137
+ if (!hasAnySnapshotData(cloned)) {
138
+ return undefined;
139
+ }
140
+ return cloned;
141
+ }
142
+ export function mergeSubAgentContextSnapshot(requestSnapshot, runtimeSnapshot) {
143
+ const normalizedRequestVersion = requestSnapshot && normalizeSnapshotVersion(requestSnapshot);
144
+ const normalizedRuntimeVersion = runtimeSnapshot && normalizeSnapshotVersion(runtimeSnapshot);
145
+ const merged = {
146
+ version: normalizedRuntimeVersion ?? normalizedRequestVersion ?? SUB_AGENT_CONTEXT_SNAPSHOT_VERSION,
147
+ conversationContext: runtimeSnapshot?.conversationContext ?? requestSnapshot?.conversationContext,
148
+ artifactHints: runtimeSnapshot?.artifactHints ?? requestSnapshot?.artifactHints,
149
+ toolCallingAudit: runtimeSnapshot?.toolCallingAudit ?? requestSnapshot?.toolCallingAudit,
150
+ replacementState: runtimeSnapshot?.replacementState ?? requestSnapshot?.replacementState,
151
+ planRuntime: runtimeSnapshot?.planRuntime ?? requestSnapshot?.planRuntime,
152
+ cacheSharing: runtimeSnapshot?.cacheSharing ?? requestSnapshot?.cacheSharing,
153
+ };
154
+ return cloneSubAgentContextSnapshot(merged);
155
+ }
156
+ //# sourceMappingURL=context-snapshot.js.map
@@ -25,7 +25,7 @@ export class SmallfryLoop {
25
25
  * Run the recursive loop based on the stratagem.
26
26
  */
27
27
  async execute(initCtx) {
28
- getLogger().info(`[SmallfryLoop] ${text.smallfry.status.working} (${this.profile.name})`);
28
+ getLogger().debug(`[SmallfryLoop] ${text.smallfry.status.working} (${this.profile.name})`);
29
29
  let pipeline = Pipeline.of(initCtx);
30
30
  // Dynamic Phase Injection based on Stratagem
31
31
  pipeline = pipeline.step('PREFLIGHT', runPreflight);
@@ -3,11 +3,15 @@ import { text } from '../../../locales/index.js';
3
3
  import { createFileSystemAdapter } from '../../adapters/fs/index.js';
4
4
  import * as fs from '../../adapters/fs/node-fs.js';
5
5
  import { GitAdapter } from '../../adapters/git/git-adapter.js';
6
+ import { recordAuditEvent } from '../../observability/audit-trail.js';
6
7
  import { getLogger } from '../../observability/logger.js';
7
8
  import { FileStateResolver } from '../../strata/layers/file-state-resolver.js';
8
9
  import { RuntimeEnvironment } from '../../strata/runtime/environment.js';
9
10
  import { ErrorType } from '../../types/index.js';
10
11
  import { ArtifactStore } from '../artifacts/store.js';
12
+ import { cloneSubAgentContextSnapshot } from '../context-snapshot.js';
13
+ import { isReadOnlySubAgentContext, resolveSubAgentDryRun } from '../dispatch-policy.js';
14
+ import { validateSharedPrefixConsistency } from '../prefix-consistency.js';
11
15
  import { getSubAgentRegistry } from '../registry.js';
12
16
  import { SmallfryLoop } from './loop.js';
13
17
  /**
@@ -33,12 +37,39 @@ export class SubAgentManager {
33
37
  * Spawns a new sub-agent and monitors its execution.
34
38
  */
35
39
  async execute(request) {
36
- const profile = this.deps.registry.get(request.agent_ref);
40
+ const normalizedRequest = request.session_target === 'shared'
41
+ ? (() => {
42
+ const consistency = validateSharedPrefixConsistency({
43
+ requestSnapshot: request.contextSnapshot,
44
+ runtimeSnapshot: this.ctx.contextSnapshot,
45
+ });
46
+ if (consistency.compatible)
47
+ return request;
48
+ recordAuditEvent('sub_agent.shared.prefix_consistency_failed', {
49
+ metric: 'shared_fallback_rate',
50
+ fallbackMode: 'isolated',
51
+ reason: consistency.reason,
52
+ expected: consistency.expected,
53
+ actual: consistency.actual,
54
+ }, {
55
+ source: 'smallfry',
56
+ severity: 'medium',
57
+ scope: 'session',
58
+ phase: this.ctx.phase,
59
+ });
60
+ return {
61
+ ...request,
62
+ session_target: 'isolated',
63
+ contextSnapshot: undefined,
64
+ };
65
+ })()
66
+ : request;
67
+ const profile = this.deps.registry.get(normalizedRequest.agent_ref);
37
68
  if (!profile) {
38
- return this.fail(request.agent_ref, text.smallfry.errors.profileNotFound(request.agent_ref), 'LOOP_FAILED');
69
+ return this.fail(normalizedRequest.agent_ref, text.smallfry.errors.profileNotFound(normalizedRequest.agent_ref), 'LOOP_FAILED');
39
70
  }
40
71
  const agentId = `smallfry-${randomBytes(4).toString('hex')}`;
41
- const currentDepth = request.recursionDepth || 0;
72
+ const currentDepth = normalizedRequest.recursionDepth || 0;
42
73
  const MAX_RECURSION_DEPTH = 2;
43
74
  if (currentDepth >= MAX_RECURSION_DEPTH) {
44
75
  const msg = text.smallfry.errors.recursionLimitExceeded(currentDepth, MAX_RECURSION_DEPTH);
@@ -47,7 +78,7 @@ export class SubAgentManager {
47
78
  }
48
79
  this.activeAgents.set(agentId, { profile, status: 'hiring' });
49
80
  this.controller.registerAgent(agentId, profile, 'hiring');
50
- getLogger().info(`[SubAgentManager] ${text.smallfry.status.spawning} (ID: ${agentId}, Role: ${profile.role})`);
81
+ getLogger().debug(`[SubAgentManager] ${text.smallfry.status.spawning} (ID: ${agentId}, Role: ${profile.role})`);
51
82
  const llm = this.ctx.llm;
52
83
  if (!llm) {
53
84
  const msg = text.smallfry.errors.dispatchMissingRuntimeLlm;
@@ -59,7 +90,12 @@ export class SubAgentManager {
59
90
  if (this.controller.isStopRequested(agentId)) {
60
91
  throw new Error('Stop requested before launching Smallfry');
61
92
  }
62
- const runtimeEnv = await this.setupIsolatedEnvironment(request, llm, agentId);
93
+ const effectiveDryRun = resolveSubAgentDryRun({
94
+ parentDryRun: this.ctx.dryRun,
95
+ flowMode: this.ctx.flowMode,
96
+ phase: this.ctx.phase,
97
+ });
98
+ const runtimeEnv = await this.setupIsolatedEnvironment(normalizedRequest, llm, agentId, effectiveDryRun);
63
99
  try {
64
100
  const workspace = runtimeEnv.workspace;
65
101
  const activePath = workspace.workPath;
@@ -68,21 +104,23 @@ export class SubAgentManager {
68
104
  const flowMode = 'patch';
69
105
  const fsAdapter = createFileSystemAdapter(flowMode);
70
106
  // 2. Construct InitCtx for the smallfry
71
- const initCtx = {
107
+ const initCtx = this.applyContextSnapshot(normalizedRequest.contextSnapshot, {
72
108
  workspace: {
73
109
  workPath: activePath,
74
110
  baseRepoPath: workspace.baseRepoPath,
75
111
  strategy: workspace.strategy,
76
112
  },
77
113
  options: {
78
- instruction: request.task,
114
+ instruction: normalizedRequest.task,
79
115
  repoPath: activePath,
80
- dryRun: this.ctx.dryRun,
81
- contextFiles: request.contextFiles || [],
116
+ dryRun: effectiveDryRun,
117
+ contextFiles: normalizedRequest.contextFiles || [],
82
118
  llm,
83
119
  recursionDepth: currentDepth + 1, // Increment depth for child
84
- allowedToolNames: this.filterAllowedTools(profile.allowedTools),
85
- timeoutMs: request.timeout_seconds ? request.timeout_seconds * 1000 : profile.timeoutMs,
120
+ allowedToolNames: this.filterAllowedTools(profile.allowedTools, this.ctx.phase),
121
+ timeoutMs: normalizedRequest.timeout_seconds
122
+ ? normalizedRequest.timeout_seconds * 1000
123
+ : profile.timeoutMs,
86
124
  },
87
125
  mode: flowMode,
88
126
  fs: fsAdapter,
@@ -100,7 +138,7 @@ export class SubAgentManager {
100
138
  },
101
139
  fileStateResolver: resolver,
102
140
  shadowInitialRef: runtimeEnv?.initialSnapshotHash || 'HEAD',
103
- };
141
+ });
104
142
  // 3. Launch the "Little Fry"
105
143
  const subLoop = new SmallfryLoop(profile);
106
144
  const result = await subLoop.execute(initCtx);
@@ -133,6 +171,23 @@ export class SubAgentManager {
133
171
  async spawn(request) {
134
172
  return this.execute(request);
135
173
  }
174
+ applyContextSnapshot(snapshot, initCtx) {
175
+ const normalized = cloneSubAgentContextSnapshot(snapshot);
176
+ if (!normalized)
177
+ return initCtx;
178
+ return {
179
+ ...initCtx,
180
+ cacheSharing: normalized.cacheSharing ?? initCtx.cacheSharing,
181
+ planRuntime: normalized.planRuntime ?? initCtx.planRuntime,
182
+ toolCallingAudit: normalized.toolCallingAudit ?? initCtx.toolCallingAudit,
183
+ replacementState: normalized.replacementState ?? initCtx.replacementState,
184
+ artifactHints: normalized.artifactHints ?? initCtx.artifactHints,
185
+ options: {
186
+ ...initCtx.options,
187
+ conversationContext: normalized.conversationContext ?? initCtx.options.conversationContext,
188
+ },
189
+ };
190
+ }
136
191
  updateStatus(id, status) {
137
192
  const entry = this.activeAgents.get(id);
138
193
  if (entry) {
@@ -154,13 +209,29 @@ export class SubAgentManager {
154
209
  errorType: ErrorType.UNKNOWN,
155
210
  };
156
211
  }
157
- async setupIsolatedEnvironment(request, llm, agentId) {
212
+ async setupIsolatedEnvironment(request, llm, agentId, effectiveDryRun) {
213
+ if (isReadOnlySubAgentContext({
214
+ flowMode: this.ctx.flowMode,
215
+ phase: this.ctx.phase,
216
+ }) &&
217
+ request.session_target !== 'isolated') {
218
+ recordAuditEvent('sub_agent.dispatch.read_only_forced_isolated', {
219
+ requestedSessionTarget: request.session_target,
220
+ effectiveSessionTarget: 'isolated',
221
+ }, {
222
+ source: 'smallfry',
223
+ severity: 'low',
224
+ scope: 'session',
225
+ phase: this.ctx.phase,
226
+ });
227
+ }
158
228
  const baseRepoPath = this.ctx.persistenceRoot || this.ctx.repoRoot;
159
229
  const options = {
160
230
  instruction: request.task,
161
231
  repoPath: baseRepoPath,
162
232
  llm,
163
- dryRun: this.ctx.dryRun,
233
+ // CRITICAL SAFETY: read-only model phases force sub-agent dryRun.
234
+ dryRun: effectiveDryRun,
164
235
  verify: undefined,
165
236
  strategy: 'worktree',
166
237
  contextFiles: request.contextFiles,
@@ -187,15 +258,20 @@ export class SubAgentManager {
187
258
  }
188
259
  async persistArtifacts(agentId, result) {
189
260
  const patch = result.finalPatch;
190
- if (!patch || typeof patch !== 'string')
191
- return result;
261
+ const { finalPatch: _ignored, ...rest } = result;
262
+ const auditArtifact = await this.persistAuditArtifact(rest.auditPath);
263
+ if (!patch || typeof patch !== 'string') {
264
+ return {
265
+ ...rest,
266
+ auditPath: auditArtifact?.handle ?? rest.auditPath,
267
+ auditArtifact: auditArtifact ?? rest.auditArtifact,
268
+ };
269
+ }
192
270
  const saved = await this.deps.artifactStore.saveText({
193
271
  content: patch,
194
272
  mimeType: 'text/x-diff',
195
273
  fileExt: 'patch',
196
274
  });
197
- const { finalPatch: _ignored, ...rest } = result;
198
- const auditArtifact = await this.persistAuditArtifact(rest.auditPath);
199
275
  return {
200
276
  ...rest,
201
277
  auditPath: auditArtifact?.handle ?? rest.auditPath,
@@ -203,7 +279,7 @@ export class SubAgentManager {
203
279
  patchArtifact: saved,
204
280
  };
205
281
  }
206
- filterAllowedTools(allowed) {
282
+ filterAllowedTools(allowed, phase) {
207
283
  const safeReadOnlyTools = new Set([
208
284
  'agent_dispatch',
209
285
  'code.search',
@@ -213,7 +289,30 @@ export class SubAgentManager {
213
289
  'git.cat',
214
290
  'artifact.read',
215
291
  ]);
216
- return allowed.filter((name) => safeReadOnlyTools.has(name));
292
+ const readOnlyPlanTools = new Set(['plan.init', 'plan.read', 'plan.update']);
293
+ const readOnlyPhase = isReadOnlySubAgentContext({
294
+ flowMode: this.ctx.flowMode,
295
+ phase,
296
+ });
297
+ if (!readOnlyPhase) {
298
+ return allowed;
299
+ }
300
+ const filtered = allowed.filter((name) => safeReadOnlyTools.has(name) || (readOnlyPhase && readOnlyPlanTools.has(name)));
301
+ if (readOnlyPhase) {
302
+ const removed = allowed.filter((name) => !filtered.includes(name));
303
+ if (removed.length > 0) {
304
+ recordAuditEvent('sub_agent.dispatch.read_only_tool_guard_filtered', {
305
+ removedTools: removed,
306
+ retainedTools: filtered,
307
+ }, {
308
+ source: 'smallfry',
309
+ severity: 'medium',
310
+ scope: 'session',
311
+ phase,
312
+ });
313
+ }
314
+ }
315
+ return filtered;
217
316
  }
218
317
  async persistAuditArtifact(auditPath) {
219
318
  if (!auditPath || typeof auditPath !== 'string')
@@ -0,0 +1,29 @@
1
+ import { resolveExecutionProfile } from '../runtime/execution-profile.js';
2
+ import { Phase } from '../types/runtime.js';
3
+ const READ_ONLY_MODEL_PHASE_SET = new Set([Phase.EXPLORE, Phase.PLAN, Phase.PATCH]);
4
+ /**
5
+ * Read-only model phases in which sub-agent dispatch must never cause workspace mutation.
6
+ */
7
+ export function isReadOnlyModelPhase(phase) {
8
+ return phase !== undefined && READ_ONLY_MODEL_PHASE_SET.has(phase);
9
+ }
10
+ export function isReadOnlySubAgentContext({ flowMode, phase }) {
11
+ if (!flowMode) {
12
+ return isReadOnlyModelPhase(phase);
13
+ }
14
+ const profile = resolveExecutionProfile(flowMode);
15
+ if (profile.driver === 'agent') {
16
+ return false;
17
+ }
18
+ return isReadOnlyModelPhase(phase);
19
+ }
20
+ /**
21
+ * In read-only model phases, sub-agent execution must run in dry-run mode regardless of parent value.
22
+ */
23
+ export function resolveSubAgentDryRun({ parentDryRun, flowMode, phase, }) {
24
+ if (isReadOnlySubAgentContext({ flowMode, phase })) {
25
+ return true;
26
+ }
27
+ return parentDryRun;
28
+ }
29
+ //# sourceMappingURL=dispatch-policy.js.map