@inflexa-ai/harness 0.42.1 → 0.43.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 (217) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/conversation-agent.js +5 -1
  3. package/dist/agents/sandbox/drug-repurposing-agent.js +1 -0
  4. package/dist/agents/sandbox/shared.d.ts +1 -6
  5. package/dist/agents/sandbox/shared.js +3 -16
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/app/chat-turn.js +13 -1
  8. package/dist/config/environment-stores.d.ts +4 -2
  9. package/dist/contracts/chat-events.d.ts +9 -1
  10. package/dist/contracts/chat-frame.d.ts +3 -3
  11. package/dist/contracts/chat-frame.js +8 -3
  12. package/dist/contracts/chat-parts.d.ts +4 -1
  13. package/dist/contracts/data-profile.d.ts +4 -6
  14. package/dist/contracts/index.d.ts +1 -1
  15. package/dist/contracts/report-blocks.d.ts +1364 -75
  16. package/dist/contracts/report-blocks.js +221 -29
  17. package/dist/contracts/report-reference.js +1 -1
  18. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  19. package/dist/contracts/schemas/chat-events.js +5 -0
  20. package/dist/contracts/schemas/chat-parts.d.ts +2 -2
  21. package/dist/contracts/schemas/index.d.ts +1 -1
  22. package/dist/contracts/schemas/index.js +1 -1
  23. package/dist/execution/input-signature.d.ts +7 -13
  24. package/dist/execution/input-signature.js +0 -0
  25. package/dist/execution/run-canceler.d.ts +3 -2
  26. package/dist/execution/run-canceler.js +3 -2
  27. package/dist/execution/run-event-stream.d.ts +16 -2
  28. package/dist/execution/run-event-stream.js +54 -3
  29. package/dist/index.d.ts +1 -8
  30. package/dist/index.js +5 -16
  31. package/dist/input-scan/enrich.d.ts +0 -2
  32. package/dist/input-scan/enrich.js +0 -1
  33. package/dist/lib/metrics.js +6 -7
  34. package/dist/lib/otel-spans.d.ts +4 -9
  35. package/dist/lib/otel-spans.js +3 -8
  36. package/dist/loop/run-agent.d.ts +4 -2
  37. package/dist/loop/run-agent.js +38 -23
  38. package/dist/memory/thread-history.d.ts +1 -1
  39. package/dist/prompts/report-session.d.ts +1 -1
  40. package/dist/prompts/report-session.js +17 -0
  41. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  42. package/dist/prompts/sandbox/drug-repurposing-agent.js +7 -0
  43. package/dist/provenance/exec-frame.d.ts +1 -1
  44. package/dist/report-model/authoring-grammar.d.ts +416 -17
  45. package/dist/report-model/authoring-grammar.js +25 -4
  46. package/dist/report-model/block-walk.d.ts +20 -4
  47. package/dist/report-model/block-walk.js +64 -7
  48. package/dist/report-model/draft-finish.d.ts +6 -2
  49. package/dist/report-model/draft-finish.js +6 -1
  50. package/dist/report-model/production-resolver.d.ts +8 -0
  51. package/dist/report-model/production-resolver.js +1 -1
  52. package/dist/report-model/validate.d.ts +20 -10
  53. package/dist/report-model/validate.js +29 -13
  54. package/dist/report-render/chart-export.d.ts +52 -0
  55. package/dist/report-render/chart-export.js +192 -0
  56. package/dist/report-render/chart-presets.d.ts +46 -47
  57. package/dist/report-render/chart-presets.js +31 -101
  58. package/dist/report-render/chart-renderers.d.ts +163 -0
  59. package/dist/report-render/chart-renderers.js +1055 -0
  60. package/dist/report-render/chart-toolbox.d.ts +124 -0
  61. package/dist/report-render/chart-toolbox.js +595 -0
  62. package/dist/report-render/chart.d.ts +60 -27
  63. package/dist/report-render/chart.js +2323 -524
  64. package/dist/report-render/design.d.ts +476 -16
  65. package/dist/report-render/design.js +390 -56
  66. package/dist/report-render/figures/common.d.ts +321 -0
  67. package/dist/report-render/figures/common.js +759 -0
  68. package/dist/report-render/figures/dendrogram.d.ts +36 -0
  69. package/dist/report-render/figures/dendrogram.js +130 -0
  70. package/dist/report-render/figures/dense.d.ts +111 -0
  71. package/dist/report-render/figures/dense.js +204 -0
  72. package/dist/report-render/figures/dotplot.d.ts +28 -0
  73. package/dist/report-render/figures/dotplot.js +234 -0
  74. package/dist/report-render/figures/embedding.d.ts +43 -0
  75. package/dist/report-render/figures/embedding.js +360 -0
  76. package/dist/report-render/figures/equal-units.d.ts +101 -0
  77. package/dist/report-render/figures/equal-units.js +215 -0
  78. package/dist/report-render/figures/forest.d.ts +20 -0
  79. package/dist/report-render/figures/forest.js +398 -0
  80. package/dist/report-render/figures/gsea.d.ts +30 -0
  81. package/dist/report-render/figures/gsea.js +391 -0
  82. package/dist/report-render/figures/heatmap.d.ts +40 -0
  83. package/dist/report-render/figures/heatmap.js +395 -0
  84. package/dist/report-render/figures/index.d.ts +135 -0
  85. package/dist/report-render/figures/index.js +100 -0
  86. package/dist/report-render/figures/km.d.ts +16 -0
  87. package/dist/report-render/figures/km.js +406 -0
  88. package/dist/report-render/figures/label-room.d.ts +130 -0
  89. package/dist/report-render/figures/label-room.js +341 -0
  90. package/dist/report-render/figures/locuszoom.d.ts +50 -0
  91. package/dist/report-render/figures/locuszoom.js +675 -0
  92. package/dist/report-render/figures/lollipop.d.ts +16 -0
  93. package/dist/report-render/figures/lollipop.js +247 -0
  94. package/dist/report-render/figures/ma.d.ts +20 -0
  95. package/dist/report-render/figures/ma.js +114 -0
  96. package/dist/report-render/figures/manhattan.d.ts +60 -0
  97. package/dist/report-render/figures/manhattan.js +343 -0
  98. package/dist/report-render/figures/oncoprint.d.ts +41 -0
  99. package/dist/report-render/figures/oncoprint.js +420 -0
  100. package/dist/report-render/figures/pca.d.ts +37 -0
  101. package/dist/report-render/figures/pca.js +241 -0
  102. package/dist/report-render/figures/qq.d.ts +30 -0
  103. package/dist/report-render/figures/qq.js +182 -0
  104. package/dist/report-render/figures/roc.d.ts +17 -0
  105. package/dist/report-render/figures/roc.js +116 -0
  106. package/dist/report-render/figures/sankey.d.ts +48 -0
  107. package/dist/report-render/figures/sankey.js +353 -0
  108. package/dist/report-render/figures/upset.d.ts +60 -0
  109. package/dist/report-render/figures/upset.js +316 -0
  110. package/dist/report-render/figures/volcano.d.ts +25 -0
  111. package/dist/report-render/figures/volcano.js +166 -0
  112. package/dist/report-render/hybrid-svg.d.ts +75 -0
  113. package/dist/report-render/hybrid-svg.js +504 -0
  114. package/dist/report-render/number-format.d.ts +19 -0
  115. package/dist/report-render/number-format.js +25 -1
  116. package/dist/report-render/page.d.ts +41 -4
  117. package/dist/report-render/page.js +247 -12
  118. package/dist/report-render/render.d.ts +13 -4
  119. package/dist/report-render/render.js +37 -6
  120. package/dist/report-render/types.d.ts +23 -2
  121. package/dist/report-render/value-bridge.d.ts +38 -3
  122. package/dist/report-render/value-bridge.js +137 -6
  123. package/dist/report-render/views/chart-view.d.ts +42 -6
  124. package/dist/report-render/views/chart-view.js +70 -9
  125. package/dist/report-render/views/page-view.js +9 -10
  126. package/dist/runtime/assemble.js +2 -2
  127. package/dist/sandbox/client.d.ts +23 -32
  128. package/dist/sandbox/client.js +3 -11
  129. package/dist/sandbox/create-sandbox.d.ts +9 -24
  130. package/dist/sandbox/create-sandbox.js +15 -30
  131. package/dist/sandbox/docker-client.d.ts +12 -22
  132. package/dist/sandbox/docker-client.js +29 -43
  133. package/dist/sandbox/exec.d.ts +58 -0
  134. package/dist/sandbox/exec.js +197 -0
  135. package/dist/sandbox/identity.d.ts +5 -5
  136. package/dist/sandbox/identity.js +7 -10
  137. package/dist/sandbox/k8s-client.d.ts +4 -10
  138. package/dist/sandbox/k8s-client.js +5 -12
  139. package/dist/sandbox/liveness.d.ts +5 -12
  140. package/dist/sandbox/liveness.js +5 -12
  141. package/dist/sandbox/notification-sweep.d.ts +4 -7
  142. package/dist/sandbox/notification-sweep.js +4 -7
  143. package/dist/sandbox/reaper.d.ts +3 -3
  144. package/dist/sandbox/reaper.js +3 -3
  145. package/dist/sandbox/sandbox-error.d.ts +10 -11
  146. package/dist/sandbox/sandbox-error.js +10 -11
  147. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  148. package/dist/sandbox/sandbox-step-translate.js +1 -1
  149. package/dist/sandbox/types.d.ts +31 -99
  150. package/dist/sandbox/types.js +17 -84
  151. package/dist/state/active-sandboxes.d.ts +7 -32
  152. package/dist/state/active-sandboxes.js +11 -46
  153. package/dist/state/index.d.ts +1 -2
  154. package/dist/state/index.js +1 -1
  155. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
  156. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
  157. package/dist/state/migrations/index.js +2 -0
  158. package/dist/state/schema.d.ts +1 -5
  159. package/dist/state/schema.js +1 -6
  160. package/dist/state/step-executions.js +1 -2
  161. package/dist/tasks/data-profile.js +7 -20
  162. package/dist/tasks/derive-table-exec.d.ts +4 -14
  163. package/dist/tasks/derive-table-exec.js +9 -24
  164. package/dist/tasks/extract-values.d.ts +2 -2
  165. package/dist/tasks/extract-values.js +4 -7
  166. package/dist/tools/bio/index.d.ts +2 -0
  167. package/dist/tools/bio/index.js +3 -0
  168. package/dist/tools/bio/keys.d.ts +11 -0
  169. package/dist/tools/bio/keys.js +15 -0
  170. package/dist/tools/bio/search-clinical-trials.d.ts +1 -1
  171. package/dist/tools/bio/search-patents.d.ts +46 -0
  172. package/dist/tools/bio/search-patents.js +134 -0
  173. package/dist/tools/bio/search-regulatory-approvals.d.ts +49 -0
  174. package/dist/tools/bio/search-regulatory-approvals.js +158 -0
  175. package/dist/tools/define-tool.d.ts +56 -13
  176. package/dist/tools/define-tool.js +28 -1
  177. package/dist/tools/execute-analysis.js +3 -3
  178. package/dist/tools/lib/amass-client.d.ts +586 -0
  179. package/dist/tools/lib/amass-client.js +492 -0
  180. package/dist/tools/lib/amass-config.d.ts +2 -0
  181. package/dist/tools/lib/amass-config.js +10 -0
  182. package/dist/tools/report-session/derive-table.d.ts +8 -9
  183. package/dist/tools/report-session/derive-table.js +3 -4
  184. package/dist/tools/report-session/preview-report.js +4 -49
  185. package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
  186. package/dist/tools/sandbox/list-available-packages.js +5 -4
  187. package/dist/tools/workspace/edit-file.d.ts +5 -4
  188. package/dist/tools/workspace/edit-file.js +10 -13
  189. package/dist/tools/workspace/execute-command.d.ts +27 -42
  190. package/dist/tools/workspace/execute-command.js +42 -38
  191. package/dist/tools/workspace/index.d.ts +5 -6
  192. package/dist/tools/workspace/index.js +5 -6
  193. package/dist/tools/workspace/mutator.d.ts +43 -8
  194. package/dist/tools/workspace/mutator.js +45 -34
  195. package/dist/tools/workspace/run-exec.d.ts +3 -12
  196. package/dist/tools/workspace/run-exec.js +7 -24
  197. package/dist/tools/workspace/scan-inputs.d.ts +0 -3
  198. package/dist/tools/workspace/scan-inputs.js +0 -1
  199. package/dist/tools/workspace/write-file.d.ts +12 -3
  200. package/dist/tools/workspace/write-file.js +17 -13
  201. package/dist/workflows/sandbox-step.d.ts +7 -11
  202. package/dist/workflows/sandbox-step.js +24 -27
  203. package/package.json +3 -1
  204. package/dist/sandbox/await-exec.d.ts +0 -143
  205. package/dist/sandbox/await-exec.js +0 -417
  206. package/dist/sandbox/deliver-exec-event.d.ts +0 -25
  207. package/dist/sandbox/deliver-exec-event.js +0 -29
  208. package/dist/sandbox/digest.d.ts +0 -14
  209. package/dist/sandbox/digest.js +0 -17
  210. package/dist/sandbox/exec-id.d.ts +0 -12
  211. package/dist/sandbox/exec-id.js +0 -20
  212. package/dist/sandbox/hmac.d.ts +0 -62
  213. package/dist/sandbox/hmac.js +0 -72
  214. package/dist/sandbox/submit-exec.d.ts +0 -38
  215. package/dist/sandbox/submit-exec.js +0 -80
  216. package/dist/sandbox/watchdog.d.ts +0 -71
  217. package/dist/sandbox/watchdog.js +0 -161
@@ -400,7 +400,7 @@ export function queryPackages(rawSections, { names, query, language, limit }) {
400
400
  * here, thus the census a model sees and the check a launch runs never
401
401
  * disagree about what the source holds.
402
402
  */
403
- export async function readInventorySections(deps) {
403
+ export async function readInventorySections(deps, analysisId) {
404
404
  // The image record merges in when the host can read a valid one. An absent
405
405
  // record and an invalid record both merge nothing, and neither is an
406
406
  // error: the record is an enrichment, and the farm or pool inventory stays
@@ -422,8 +422,9 @@ export async function readInventorySections(deps) {
422
422
  }
423
423
  // Both farm container paths are tried when the host injects none, because
424
424
  // the path keys on the declared toolchain and this read carries none.
425
+ const farmLockFile = typeof deps.farmLockFile === "function" ? deps.farmLockFile(analysisId) : deps.farmLockFile;
425
426
  let lock = null;
426
- for (const candidate of deps.farmLockFile ? [deps.farmLockFile] : DEFAULT_FARM_LOCK_FILES) {
427
+ for (const candidate of farmLockFile ? [farmLockFile] : DEFAULT_FARM_LOCK_FILES) {
427
428
  lock = readFarmLockFile(candidate).unwrapOr(null);
428
429
  if (lock !== null)
429
430
  break;
@@ -502,8 +503,8 @@ export function createListAvailablePackagesTool(deps = {}) {
502
503
  return `${language} packages`;
503
504
  return "full package list";
504
505
  },
505
- execute: async (input) => {
506
- const read = await readInventorySections(deps);
506
+ execute: async (input, ctx) => {
507
+ const read = await readInventorySections(deps, ctx.session.scope.analysisId);
507
508
  // An unreadable inventory is an expected environment state — model it as an
508
509
  // `available: false` data variant telling the caller the set is UNKNOWN,
509
510
  // WITH the reason: without it, a structural fault (a damaged dependency
@@ -11,10 +11,11 @@
11
11
  * (compiled with the `gm` flags — multiline anchors), `new_string` may use
12
12
  * capture-group references, and exactly one of `replace_all=true` or
13
13
  * `expected_matches` gates the write. Provenance hashes the post-edit
14
- * content — recorded by the mutator.
14
+ * content — the mutator records it through the fold of this tool.
15
15
  */
16
+ import { type WithToolCallRecord } from "../define-tool.js";
16
17
  import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
17
- import type { WorkspaceMutator, WriteFileResult } from "./mutator.js";
18
+ import type { WorkspaceMutator, WriteFileResult, WriteRecord } from "./mutator.js";
18
19
  /** Outcome of an `edit_file` call — expected outcomes are data variants, never throws. */
19
20
  export type EditFileResult = {
20
21
  readonly status: "file_not_found";
@@ -43,14 +44,14 @@ export type EditFileResult = {
43
44
  readonly lines: readonly number[];
44
45
  } | Exclude<WriteFileResult, {
45
46
  status: "ok";
46
- }> | {
47
+ }> | WithToolCallRecord<{
47
48
  readonly status: "ok";
48
49
  readonly path: string;
49
50
  readonly replacements: number;
50
51
  readonly bytesWritten: number;
51
52
  /** Regex mode only: 1-based line numbers where the matches started. */
52
53
  readonly lines?: readonly number[];
53
- };
54
+ }, WriteRecord>;
54
55
  export interface EditFileDeps {
55
56
  readonly mutator: WorkspaceMutator;
56
57
  /** Read seam used to fetch current file content for search/replace. */
@@ -11,11 +11,11 @@
11
11
  * (compiled with the `gm` flags — multiline anchors), `new_string` may use
12
12
  * capture-group references, and exactly one of `replace_all=true` or
13
13
  * `expected_matches` gates the write. Provenance hashes the post-edit
14
- * content — recorded by the mutator.
14
+ * content — the mutator records it through the fold of this tool.
15
15
  */
16
16
  import { ok } from "neverthrow";
17
17
  import { z } from "zod";
18
- import { defineTool } from "../define-tool.js";
18
+ import { defineTool, withToolCallRecord } from "../define-tool.js";
19
19
  import { unwrapOrThrow } from "../../lib/result.js";
20
20
  const EditFileInputSchema = z.object({
21
21
  path: z
@@ -108,9 +108,6 @@ function replaceRegex(content, pattern, replacement, expected) {
108
108
  export function createEditFileTool(deps) {
109
109
  return defineTool({
110
110
  id: "edit_file",
111
- // The mutator wraps the disk mutation in `ctx.runStep` itself, so the
112
- // body runs unwrapped in the workflow body (see the harness-tools spec).
113
- executionMode: "workflow",
114
111
  description: "Edit a file in your working directory by replacing specific text. " +
115
112
  "Read the file first to get the exact text. When replace_all is false " +
116
113
  "(default), old_string must occur exactly once. For a bulk pattern " +
@@ -166,24 +163,24 @@ export function createEditFileTool(deps) {
166
163
  lines: replaced.lines,
167
164
  });
168
165
  }
169
- const result = await deps.mutator.writeFile({
166
+ const write = await deps.mutator.writeFile({
170
167
  path,
171
168
  content: replaced.content,
172
169
  toolName: "edit_file",
173
170
  invocationId: ctx.invocationId,
174
- runStep: ctx.runStep,
175
171
  session: ctx.session,
176
172
  });
177
- if (result.status !== "ok")
178
- return ok(result);
173
+ if (write.status !== "ok")
174
+ return ok(write);
179
175
  const lines = replaced.lines;
180
- return ok({
176
+ return ok(withToolCallRecord({
181
177
  status: "ok",
182
- path: result.path,
178
+ path: write.path,
183
179
  replacements: replaced.replacements,
184
- bytesWritten: result.bytesWritten,
180
+ bytesWritten: write.bytesWritten,
185
181
  ...(lines === undefined ? {} : { lines }),
186
- });
182
+ }, write.record));
187
183
  },
184
+ foldCallRecord: (record) => deps.mutator.recordWrite(record),
188
185
  });
189
186
  }
@@ -2,45 +2,46 @@
2
2
  * `execute_command` — the single chokepoint through which sandbox commands
3
3
  * run.
4
4
  *
5
- * Dependency-bearing factory (see the harness-durable-runtime spec): captures a `SandboxClient`, the
6
- * live `SandboxRef` for the step, and the per-call execId minter. No other
7
- * tool, agent, or workflow step is permitted to POST to sandbox-server's
8
- * `/exec` directly — `SandboxClient` is intentionally injected only here so
9
- * the durability / idempotency / liveness story `harness-sandbox-exec`
10
- * owns holds uniformly across all sandbox command execution.
5
+ * Dependency-bearing factory (see the harness-durable-runtime spec): captures a
6
+ * `SandboxClient` and the live `SandboxRef` for the step. No other tool, agent,
7
+ * or workflow step is permitted to POST to sandbox-server's `/exec` directly —
8
+ * `SandboxClient` is intentionally injected only here so the durability /
9
+ * idempotency / liveness story `harness-sandbox-exec` owns holds uniformly
10
+ * across all sandbox command execution.
11
11
  *
12
- * `execId` is derived as `${workflowId}:${stepId}:${functionId}` so replays
13
- * land on the cached DBOS step output rather than re-submitting; the same
14
- * `execId` is the step name `submitExec` uses (`sandbox.submit-exec.${execId}`).
12
+ * The loop runs each call as one durable step, and the exec runs inside it,
13
+ * thus the exec id is the id of that step and a replay returns the cached
14
+ * result with no submit.
15
15
  *
16
16
  * Provenance for files written *inside* the sandbox by the executed command
17
17
  * is NOT recorded here — sandbox-server emits provenance frames (see
18
18
  * `sandbox-provenance-tracking`), and artifact registration reconciles them.
19
- * The harness-side mutate tools (`write_file`, `edit_file`) hash and record
20
- * the writes *they* perform, but `execute_command`'s opaque commands do not.
19
+ * The frame of each exec rides the call record, and the fold feeds it to the
20
+ * lineage collector of the step.
21
21
  */
22
+ import { type WithToolCallRecord } from "../define-tool.js";
22
23
  import type { SandboxClient } from "../../sandbox/client.js";
23
- import type { SandboxRef } from "../../sandbox/types.js";
24
+ import type { ProvenanceFrame, SandboxRef } from "../../sandbox/types.js";
24
25
  import type { ProvenanceCollector } from "../../provenance/collector.js";
26
+ import { type BoundedExecResult } from "./result-bounds.js";
25
27
  import type { Logger } from "../../lib/logger.js";
28
+ /** What one exec leaves for the lineage collector of the step. */
29
+ interface ExecCallRecord {
30
+ readonly command: readonly string[];
31
+ readonly exitCode: number | null;
32
+ readonly durationMs: number | null;
33
+ readonly provenance?: ProvenanceFrame;
34
+ }
35
+ type ExecuteCommandResult = {
36
+ readonly status: "ok";
37
+ } & BoundedExecResult;
26
38
  export interface ExecuteCommandDeps {
27
39
  /** Operational logging seam; omitted falls back to no-op. */
28
40
  readonly logger?: Logger;
29
41
  readonly sandboxClient: SandboxClient;
30
42
  /** Live sandbox handle for the step. Created once per step at composition root. */
31
43
  readonly sandbox: SandboxRef;
32
- /** Stable across replay. The workflow's DBOS workflow id. */
33
- readonly workflowId: string;
34
- /** Stable across replay. The sandbox step's identifier within the workflow. */
35
- readonly stepId: string;
36
- /**
37
- * Mints a stable per-call function id. The composition root closes over a
38
- * monotonic counter; the agent loop's tool-call order is replay-deterministic
39
- * (the LLM call wrapping it is a cached DBOS step), so the counter reaches
40
- * the same value at each call site on replay.
41
- */
42
- readonly nextFunctionId: () => string;
43
- /** Absolute unix-ms deadline for `awaitExec`. Typically derived from `step.timeout`. */
44
+ /** Absolute unix-ms deadline of the exec. Typically derived from `step.timeout`. */
44
45
  readonly deadlineMs: () => number;
45
46
  /**
46
47
  * In-sandbox absolute path of the agent's working directory (e.g.
@@ -49,8 +50,6 @@ export interface ExecuteCommandDeps {
49
50
  * and as the base a supplied relative `cwd` resolves against (see the harness-workspace-tools spec).
50
51
  */
51
52
  readonly defaultCwd: string;
52
- /** Tag the active-sandbox row with the in-flight execId for the liveness watchdog. */
53
- readonly markExecActive?: (execId: string) => Promise<void>;
54
53
  /**
55
54
  * Step-scoped lineage collector. Each exec's `ExecResult.provenance`
56
55
  * frame is fed here (reads → inputs, writes → outputs) so post-step
@@ -65,19 +64,5 @@ export declare function createExecuteCommandTool(deps: ExecuteCommandDeps): impo
65
64
  cwd?: string | undefined;
66
65
  env?: Record<string, string> | undefined;
67
66
  timeoutSeconds?: number | undefined;
68
- }, {
69
- execId: string;
70
- exitCode: number | null;
71
- stdout: string;
72
- stderr: string;
73
- stdoutTruncated: boolean;
74
- stderrTruncated: boolean;
75
- stdoutTotalLength: number;
76
- stderrTotalLength: number;
77
- durationMs: number | null;
78
- timedOut: boolean;
79
- syntheticFailure?: {
80
- readonly reason: string;
81
- };
82
- status: "ok";
83
- }>;
67
+ }, ExecuteCommandResult | WithToolCallRecord<ExecuteCommandResult, ExecCallRecord>>;
68
+ export {};
@@ -2,27 +2,27 @@
2
2
  * `execute_command` — the single chokepoint through which sandbox commands
3
3
  * run.
4
4
  *
5
- * Dependency-bearing factory (see the harness-durable-runtime spec): captures a `SandboxClient`, the
6
- * live `SandboxRef` for the step, and the per-call execId minter. No other
7
- * tool, agent, or workflow step is permitted to POST to sandbox-server's
8
- * `/exec` directly — `SandboxClient` is intentionally injected only here so
9
- * the durability / idempotency / liveness story `harness-sandbox-exec`
10
- * owns holds uniformly across all sandbox command execution.
5
+ * Dependency-bearing factory (see the harness-durable-runtime spec): captures a
6
+ * `SandboxClient` and the live `SandboxRef` for the step. No other tool, agent,
7
+ * or workflow step is permitted to POST to sandbox-server's `/exec` directly —
8
+ * `SandboxClient` is intentionally injected only here so the durability /
9
+ * idempotency / liveness story `harness-sandbox-exec` owns holds uniformly
10
+ * across all sandbox command execution.
11
11
  *
12
- * `execId` is derived as `${workflowId}:${stepId}:${functionId}` so replays
13
- * land on the cached DBOS step output rather than re-submitting; the same
14
- * `execId` is the step name `submitExec` uses (`sandbox.submit-exec.${execId}`).
12
+ * The loop runs each call as one durable step, and the exec runs inside it,
13
+ * thus the exec id is the id of that step and a replay returns the cached
14
+ * result with no submit.
15
15
  *
16
16
  * Provenance for files written *inside* the sandbox by the executed command
17
17
  * is NOT recorded here — sandbox-server emits provenance frames (see
18
18
  * `sandbox-provenance-tracking`), and artifact registration reconciles them.
19
- * The harness-side mutate tools (`write_file`, `edit_file`) hash and record
20
- * the writes *they* perform, but `execute_command`'s opaque commands do not.
19
+ * The frame of each exec rides the call record, and the fold feeds it to the
20
+ * lineage collector of the step.
21
21
  */
22
22
  import { posix as posixPath } from "node:path";
23
23
  import { ok } from "neverthrow";
24
24
  import { z } from "zod";
25
- import { defineTool } from "../define-tool.js";
25
+ import { defineTool, withToolCallRecord } from "../define-tool.js";
26
26
  import { feedExecFrame } from "../../provenance/exec-frame.js";
27
27
  import { EXEC_STREAM_BYTE_CAP, boundExecResult } from "./result-bounds.js";
28
28
  import { runSandboxExec } from "./run-exec.js";
@@ -50,12 +50,9 @@ function scriptToken(command) {
50
50
  return command.find((arg) => SCRIPT_EXTENSIONS.test(arg));
51
51
  }
52
52
  export function createExecuteCommandTool(deps) {
53
- const { sandboxClient, sandbox, workflowId, stepId, nextFunctionId, deadlineMs, defaultCwd, markExecActive, lineageCollector, mountRoot } = deps;
53
+ const { sandboxClient, sandbox, deadlineMs, defaultCwd, lineageCollector, mountRoot } = deps;
54
54
  return defineTool({
55
55
  id: "execute_command",
56
- // `awaitExec`-recv is body-only (`DBOS.recv`), so this runs unwrapped in
57
- // the workflow body; durability is self-owned (submit step + body recv). See the harness-tools spec.
58
- executionMode: "workflow",
59
56
  description: "Run a command in the sandbox and return its stdout/stderr/exit code. " +
60
57
  "Use for scripts, CLI tools, shell pipes, and anything the workspace " +
61
58
  `tools don't express. stdout and stderr are each capped at ${EXEC_STREAM_BYTE_CAP / (1024 * 1024)} MiB; the end ` +
@@ -88,40 +85,47 @@ export function createExecuteCommandTool(deps) {
88
85
  // outlives the turn that ran it.
89
86
  describeCall: ({ command }) => scriptToken(command) ?? command.join(" "),
90
87
  execute: async ({ command, cwd, env, timeoutSeconds }, ctx) => {
91
- const execId = `${workflowId}:${stepId}:${nextFunctionId()}`;
92
88
  const effectiveCwd = cwd === undefined ? defaultCwd : cwd.startsWith("/") ? cwd : posixPath.join(defaultCwd, cwd);
93
89
  const result = await runSandboxExec({
94
90
  sandboxClient,
95
91
  sandbox,
96
- execId,
97
92
  command,
98
93
  cwd: effectiveCwd,
99
94
  ...(env === undefined ? {} : { env }),
100
95
  ...(timeoutSeconds === undefined ? {} : { timeoutSeconds }),
101
96
  deadlineMs: deadlineMs(),
102
97
  emit: ctx.emit,
103
- ...(markExecActive ? { markExecActive } : {}),
104
98
  });
105
- // Thread the runtime file-I/O frame into the step's lineage collector.
106
- // Best-effort: a collector failure must never fail the exec.
107
- if (lineageCollector && mountRoot) {
108
- try {
109
- feedExecFrame({
110
- collector: lineageCollector,
111
- mountRoot,
112
- command,
113
- exitCode: result.exitCode,
114
- durationMs: result.durationMs,
115
- ...(deps.logger ? { logger: deps.logger } : {}),
116
- ...(result.provenance ? { provenance: result.provenance } : {}),
117
- });
118
- }
119
- catch (err) {
120
- const logger = (deps.logger ?? createNoopLogger()).named("execute_command");
121
- logger.warn("provenance frame handling failed (non-fatal)", { execId, ...logger.errorFields(err) });
122
- }
99
+ const bounded = { status: "ok", ...boundExecResult(result) };
100
+ if (!lineageCollector || !mountRoot)
101
+ return ok(bounded);
102
+ const record = {
103
+ command,
104
+ exitCode: result.exitCode,
105
+ durationMs: result.durationMs,
106
+ ...(result.provenance ? { provenance: result.provenance } : {}),
107
+ };
108
+ return ok(withToolCallRecord(bounded, record));
109
+ },
110
+ // Best-effort: a collector failure must never fail the exec.
111
+ foldCallRecord: (record) => {
112
+ if (!lineageCollector || !mountRoot)
113
+ return;
114
+ try {
115
+ feedExecFrame({
116
+ collector: lineageCollector,
117
+ mountRoot,
118
+ command: record.command,
119
+ exitCode: record.exitCode,
120
+ durationMs: record.durationMs,
121
+ ...(deps.logger ? { logger: deps.logger } : {}),
122
+ ...(record.provenance ? { provenance: record.provenance } : {}),
123
+ });
124
+ }
125
+ catch (err) {
126
+ const logger = (deps.logger ?? createNoopLogger()).named("execute_command");
127
+ logger.warn("provenance frame handling failed (non-fatal)", logger.errorFields(err));
123
128
  }
124
- return ok({ status: "ok", ...boundExecResult(result) });
125
129
  },
126
130
  });
127
131
  }
@@ -4,12 +4,11 @@
4
4
  * Read surface (sandbox-independent): semantic file search, raw read + grep
5
5
  * over the workspace filesystem seam, plus file/plan display.
6
6
  *
7
- * Mutate surface (sandbox-gated): `execute_command` (single chokepoint for
8
- * sandbox commands), `write_file` and `edit_file` scoped to the step's
9
- * writable artifact directory. Each mutate factory captures a `SandboxClient`
10
- * and the step coordinates; only `execute_command` is permitted to drive
11
- * sandbox-server's `/exec` — `write_file` / `edit_file` ride the same
12
- * submit/await path through `runSandboxExec`.
7
+ * Mutate surface: `execute_command` (single chokepoint for sandbox commands,
8
+ * sandbox-gated), `write_file` and `edit_file` scoped to the writable
9
+ * directory of the agent. `execute_command` drives sandbox-server's `/exec`
10
+ * through `runSandboxExec`; `write_file` / `edit_file` write host-side through
11
+ * the workspace mutator.
13
12
  */
14
13
  export * from "./workspace-search.js";
15
14
  export * from "./show-file.js";
@@ -4,12 +4,11 @@
4
4
  * Read surface (sandbox-independent): semantic file search, raw read + grep
5
5
  * over the workspace filesystem seam, plus file/plan display.
6
6
  *
7
- * Mutate surface (sandbox-gated): `execute_command` (single chokepoint for
8
- * sandbox commands), `write_file` and `edit_file` scoped to the step's
9
- * writable artifact directory. Each mutate factory captures a `SandboxClient`
10
- * and the step coordinates; only `execute_command` is permitted to drive
11
- * sandbox-server's `/exec` — `write_file` / `edit_file` ride the same
12
- * submit/await path through `runSandboxExec`.
7
+ * Mutate surface: `execute_command` (single chokepoint for sandbox commands,
8
+ * sandbox-gated), `write_file` and `edit_file` scoped to the writable
9
+ * directory of the agent. `execute_command` drives sandbox-server's `/exec`
10
+ * through `runSandboxExec`; `write_file` / `edit_file` write host-side through
11
+ * the workspace mutator.
13
12
  */
14
13
  // Read surface
15
14
  export * from "./workspace-search.js";
@@ -9,6 +9,12 @@
9
9
  * invariant is concentrated in one place instead of being a per-tool
10
10
  * convention.
11
11
  *
12
+ * The write and its provenance record are two calls. `writeFile` lands the
13
+ * bytes and gives back the record of the write; the tool carries the record as
14
+ * its call record, and its fold hands it to `recordWrite`. The loop caches the
15
+ * tool call as one step, thus a replay never lands the bytes again, and the
16
+ * fold still records the write into fresh process state.
17
+ *
12
18
  * Two realizations serve the two agent contexts. `createWorkspaceMutator`
13
19
  * closes over fixed per-step coordinates and records into the step-scoped
14
20
  * lineage collector (the sandbox agents). `createSessionWorkspaceMutator`
@@ -25,7 +31,6 @@
25
31
  */
26
32
  import type { AgentSession } from "../../auth/types.js";
27
33
  import type { Logger } from "../../lib/logger.js";
28
- import type { RunStep } from "../../loop/types.js";
29
34
  import type { ProvenanceCollector } from "../../provenance/collector.js";
30
35
  import { type ProvenanceSeam } from "../../provenance/seam.js";
31
36
  import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
@@ -44,6 +49,31 @@ export type WriteFileResult = {
44
49
  readonly status: "symlink_denied";
45
50
  readonly path: string;
46
51
  };
52
+ /**
53
+ * What one landed write leaves for provenance: the hash and the size of the
54
+ * exact bytes on disk, computed at the landing. Plain JSON, because the step
55
+ * cache of the tool call holds it.
56
+ */
57
+ export interface WriteRecord {
58
+ readonly analysisId: string;
59
+ /** The thread of a session write; absent under a run. */
60
+ readonly threadId?: string;
61
+ /** Analysis-root-relative, forward-slashed. */
62
+ readonly path: string;
63
+ readonly hash: string;
64
+ readonly size: number;
65
+ readonly toolName: MutateToolName;
66
+ readonly invocationId: string;
67
+ }
68
+ /** `writeFile` gives back the result, and on success the record of the write for `recordWrite`. */
69
+ export type WriteOutcome = {
70
+ readonly status: "ok";
71
+ readonly path: string;
72
+ readonly bytesWritten: number;
73
+ readonly record: WriteRecord;
74
+ } | Exclude<WriteFileResult, {
75
+ status: "ok";
76
+ }>;
47
77
  /**
48
78
  * Agent-visible name of the tool driving a confined write. Rides the write args
49
79
  * so a successful write is attributed to the invoking tool in provenance
@@ -73,20 +103,25 @@ export interface WorkspaceMutator {
73
103
  * the invoking tool so a successful write is attributed to it in
74
104
  * provenance. `invocationId` is the tool call's loop id — pass the tool
75
105
  * context's `invocationId`: it rides the provenance record so the write's
76
- * call activity gets a deterministic identity. `runStep` wraps the disk
77
- * mutation in a replay-cached step — pass the tool context's `runStep`.
78
- * `session` is the calling agent's session — pass the tool context's
79
- * `session`: the step-scoped realization ignores it, the session-scoped
80
- * one resolves its coordinates and its provenance attribution from it.
106
+ * call activity gets a deterministic identity. `session` is the calling
107
+ * agent's session — pass the tool context's `session`: the step-scoped
108
+ * realization ignores it, the session-scoped one resolves its coordinates
109
+ * and its provenance attribution from it.
110
+ *
111
+ * Records nothing: a successful write gives back its record for `recordWrite`.
81
112
  */
82
113
  writeFile(args: {
83
114
  readonly path: string;
84
115
  readonly content: string;
85
116
  readonly toolName: MutateToolName;
86
117
  readonly invocationId: string;
87
- readonly runStep: RunStep;
88
118
  readonly session: AgentSession;
89
- }): Promise<WriteFileResult>;
119
+ }): Promise<WriteOutcome>;
120
+ /**
121
+ * Record one landed write in provenance. A tool calls it from its call-record
122
+ * fold, thus the record reaches process-local state on each replay too.
123
+ */
124
+ recordWrite(record: WriteRecord): void;
90
125
  }
91
126
  export declare function createWorkspaceMutator(deps: WorkspaceMutatorDeps): WorkspaceMutator;
92
127
  /** Construction deps of the session-scoped realization — see {@link createSessionWorkspaceMutator}. */
@@ -9,6 +9,12 @@
9
9
  * invariant is concentrated in one place instead of being a per-tool
10
10
  * convention.
11
11
  *
12
+ * The write and its provenance record are two calls. `writeFile` lands the
13
+ * bytes and gives back the record of the write; the tool carries the record as
14
+ * its call record, and its fold hands it to `recordWrite`. The loop caches the
15
+ * tool call as one step, thus a replay never lands the bytes again, and the
16
+ * fold still records the write into fresh process state.
17
+ *
12
18
  * Two realizations serve the two agent contexts. `createWorkspaceMutator`
13
19
  * closes over fixed per-step coordinates and records into the step-scoped
14
20
  * lineage collector (the sandbox agents). `createSessionWorkspaceMutator`
@@ -107,9 +113,9 @@ async function landBytes(prefix, absolute, bytes) {
107
113
  }
108
114
  /**
109
115
  * The shared write gauntlet both realizations run: resolve + confine
110
- * (`resolveForWrite`), then the hardened landing wrapped in `runStep`. A
111
- * refusal comes back as data; an ok carries what the caller's provenance
112
- * record needs — the analysis-relative path and the exact bytes that landed.
116
+ * (`resolveForWrite`), then the hardened landing. A refusal comes back as data;
117
+ * an ok carries what the caller's provenance record needs — the
118
+ * analysis-relative path and the exact bytes that landed.
113
119
  */
114
120
  async function confinedWrite(args) {
115
121
  const scoped = resolveForWrite({
@@ -129,51 +135,46 @@ async function confinedWrite(args) {
129
135
  const agentPath = `/${args.analysisId}/${relative}`;
130
136
  const bytes = Buffer.from(args.content, "utf8");
131
137
  const prefix = resolvePath(args.workingDir);
132
- // The loop dispatches a workflow-mode tool body unwrapped (see
133
- // `dispatchTools` in loop/run-agent.ts), so a DBOS replay re-runs
134
- // this body. The step wrapper caches the landing: a replay returns
135
- // the recorded outcome instead of touching the disk again. On the
136
- // chat route the injected `runStep` is the passthrough — the same
137
- // call, no durability, no fork of this path.
138
- const landed = await args.runStep("write", () => landBytes(prefix, scoped.absolute, bytes));
138
+ const landed = await landBytes(prefix, scoped.absolute, bytes);
139
139
  if (landed !== "ok")
140
140
  return { status: landed };
141
141
  return { status: "ok", relative, agentPath, bytes };
142
142
  }
143
143
  export function createWorkspaceMutator(deps) {
144
144
  return {
145
- async writeFile({ path, content, toolName, invocationId, runStep }) {
145
+ async writeFile({ path, content, toolName, invocationId }) {
146
146
  const landed = await confinedWrite({
147
147
  workspaceRoot: deps.workspaceRoot,
148
148
  analysisId: deps.analysisId,
149
149
  workingDir: deps.workingDir,
150
150
  path,
151
151
  content,
152
- runStep,
153
152
  });
154
153
  if (landed.status !== "ok")
155
154
  return { status: landed.status, path };
156
- // Attest the write in-process from the exact bytes just written — the
157
- // seam owns write provenance the same way it owns confinement. The
158
- // record lands outside the write step on purpose: the collector is
159
- // process-local state, so a recovered replay must re-record into the
160
- // fresh collector even while the cached step skips the disk write.
161
- //
162
155
  // `invocationId` is replay-stable: the model turn that minted the tool
163
156
  // call is a durably cached step, so a re-execution replays the same id.
164
157
  // That is what lets the bridge key a DETERMINISTIC call-activity
165
158
  // identifier on it — a wall-clock stamp here would be re-minted per
166
159
  // replay and must never reach an identifier or a formal PROV position.
167
- if (deps.lineageCollector) {
168
- deps.lineageCollector.recordFileToolWrite({
169
- path: landed.relative,
170
- hash: computeSha256(landed.bytes),
171
- size: landed.bytes.length,
172
- toolName,
173
- invocationId,
174
- });
175
- }
176
- return { status: "ok", path: landed.agentPath, bytesWritten: landed.bytes.length };
160
+ const record = {
161
+ analysisId: deps.analysisId,
162
+ path: landed.relative,
163
+ hash: computeSha256(landed.bytes),
164
+ size: landed.bytes.length,
165
+ toolName,
166
+ invocationId,
167
+ };
168
+ return { status: "ok", path: landed.agentPath, bytesWritten: landed.bytes.length, record };
169
+ },
170
+ recordWrite(record) {
171
+ deps.lineageCollector?.recordFileToolWrite({
172
+ path: record.path,
173
+ hash: record.hash,
174
+ size: record.size,
175
+ toolName: record.toolName,
176
+ invocationId: record.invocationId,
177
+ });
177
178
  },
178
179
  };
179
180
  }
@@ -194,7 +195,7 @@ export function createSessionWorkspaceMutator(deps) {
194
195
  const logger = (deps.logger ?? createNoopLogger()).named("workspace-mutator");
195
196
  const observe = bindSessionEmit(deps.provenance, logger);
196
197
  return {
197
- async writeFile({ path, content, toolName, invocationId, runStep, session }) {
198
+ async writeFile({ path, content, toolName, invocationId, session }) {
198
199
  // Only an analysis scope has a workspace tree; under any other
199
200
  // scope there is nothing in scope to write.
200
201
  const scope = session.scope;
@@ -209,21 +210,31 @@ export function createSessionWorkspaceMutator(deps) {
209
210
  workingDir: workspaceRoot,
210
211
  path,
211
212
  content,
212
- runStep,
213
213
  });
214
214
  if (landed.status !== "ok")
215
215
  return { status: landed.status, path };
216
- observe({
217
- type: "write-file",
216
+ const record = {
218
217
  analysisId: scope.analysisId,
219
218
  ...(scope.threadId !== undefined ? { threadId: scope.threadId } : {}),
220
219
  path: landed.relative,
221
220
  hash: computeSha256(landed.bytes),
222
221
  size: landed.bytes.length,
223
- tool: toolName,
222
+ toolName,
224
223
  invocationId,
224
+ };
225
+ return { status: "ok", path: landed.agentPath, bytesWritten: landed.bytes.length, record };
226
+ },
227
+ recordWrite(record) {
228
+ observe({
229
+ type: "write-file",
230
+ analysisId: record.analysisId,
231
+ ...(record.threadId !== undefined ? { threadId: record.threadId } : {}),
232
+ path: record.path,
233
+ hash: record.hash,
234
+ size: record.size,
235
+ tool: record.toolName,
236
+ invocationId: record.invocationId,
225
237
  });
226
- return { status: "ok", path: landed.agentPath, bytesWritten: landed.bytes.length };
227
238
  },
228
239
  };
229
240
  }
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Internal helper used by dependency-bearing tools to drive one sandbox exec
3
- * through the `SandboxClient`. Centralises the submit/await pair, exec-id
4
- * derivation, and intermediate-event forwarding in one chokepoint.
5
- * `execute_command` and environment introspection tools such as
6
- * `list_available_refs` use this same replay-safe submit/await path rather
7
- * than a parallel protocol.
3
+ * through the `SandboxClient`. It wraps each sandbox event in a
4
+ * `data-sandbox-event` part, thus `execute_command` and the container readout
5
+ * of `scan_inputs` report progress in one shape.
8
6
  *
9
7
  * Not a registered tool — `defineTool` is called by the user-facing tools
10
8
  * (`execute-command.ts`) which wrap this.
@@ -15,18 +13,11 @@ import type { EmitFn } from "../define-tool.js";
15
13
  export interface RunExecArgs {
16
14
  readonly sandboxClient: SandboxClient;
17
15
  readonly sandbox: SandboxRef;
18
- readonly execId: string;
19
16
  readonly command: readonly string[];
20
17
  readonly cwd?: string;
21
18
  readonly env?: Record<string, string>;
22
19
  readonly timeoutSeconds?: number;
23
20
  readonly deadlineMs: number;
24
21
  readonly emit: EmitFn;
25
- /**
26
- * Tag the active-sandbox registry row with this execId before awaiting, so
27
- * the liveness watchdog can target the in-flight exec when the sandbox dies
28
- * mid-command. Best-effort: a registry write failure must not fail the exec.
29
- */
30
- readonly markExecActive?: (execId: string) => Promise<void>;
31
22
  }
32
23
  export declare function runSandboxExec(args: RunExecArgs): Promise<ExecResult>;