@inflexa-ai/harness 0.46.0 → 0.47.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 (280) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/conversation-agent.d.ts +3 -8
  3. package/dist/agents/conversation-agent.js +24 -4
  4. package/dist/agents/report-session-agent.d.ts +11 -14
  5. package/dist/agents/report-session-agent.js +30 -17
  6. package/dist/agents/sandbox/shared.d.ts +18 -13
  7. package/dist/agents/sandbox/shared.js +42 -27
  8. package/dist/agents/sandbox/types.d.ts +1 -1
  9. package/dist/agents/sandbox-catalog.d.ts +2 -0
  10. package/dist/agents/sandbox-catalog.js +30 -1
  11. package/dist/app/chat-turn-workflow.js +5 -6
  12. package/dist/app/synthesize-run.d.ts +10 -6
  13. package/dist/app/synthesize-run.js +4 -2
  14. package/dist/contracts/analogy-report.d.ts +46 -2
  15. package/dist/contracts/analogy-report.js +31 -25
  16. package/dist/contracts/chat-parts.d.ts +3 -14
  17. package/dist/contracts/report-reference.d.ts +2 -1
  18. package/dist/contracts/report-reference.js +2 -1
  19. package/dist/contracts/schemas/chat-parts.d.ts +0 -27
  20. package/dist/contracts/schemas/chat-parts.js +1 -3
  21. package/dist/execution/run-canceler.d.ts +2 -0
  22. package/dist/execution/run-canceler.js +9 -0
  23. package/dist/execution/run-synthesis.d.ts +36 -8
  24. package/dist/execution/run-synthesis.js +114 -32
  25. package/dist/index.d.ts +2 -5
  26. package/dist/index.js +3 -6
  27. package/dist/input-scan/enrich.d.ts +5 -10
  28. package/dist/input-scan/enrich.js +12 -8
  29. package/dist/lib/hooks.d.ts +9 -0
  30. package/dist/lib/hooks.js +6 -1
  31. package/dist/lib/markdown-pdf.d.ts +27 -0
  32. package/dist/lib/markdown-pdf.js +236 -0
  33. package/dist/lib/metrics.d.ts +31 -0
  34. package/dist/lib/metrics.js +64 -0
  35. package/dist/lib/pdf-text.d.ts +23 -0
  36. package/dist/lib/pdf-text.js +34 -0
  37. package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
  38. package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
  39. package/dist/loop/run-loop.d.ts +5 -0
  40. package/dist/loop/run-loop.js +30 -2
  41. package/dist/loop/sub-agent.js +5 -6
  42. package/dist/loop/tool-name-repair.d.ts +11 -0
  43. package/dist/loop/tool-name-repair.js +23 -0
  44. package/dist/loop/turn-credentials.d.ts +5 -0
  45. package/dist/loop/turn-credentials.js +17 -0
  46. package/dist/loop/types.d.ts +14 -0
  47. package/dist/memory/card-builders.d.ts +1 -1
  48. package/dist/prompts/SOUL.d.ts +6 -6
  49. package/dist/prompts/SOUL.js +22 -121
  50. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  51. package/dist/prompts/analogical-reasoner.js +35 -101
  52. package/dist/prompts/briefing.d.ts +20 -11
  53. package/dist/prompts/briefing.js +36 -19
  54. package/dist/prompts/conversation.d.ts +1 -1
  55. package/dist/prompts/conversation.js +103 -383
  56. package/dist/prompts/planner.d.ts +1 -5
  57. package/dist/prompts/planner.js +45 -128
  58. package/dist/prompts/report-session.d.ts +1 -1
  59. package/dist/prompts/report-session.js +26 -57
  60. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  61. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
  62. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  63. package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
  64. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  65. package/dist/prompts/sandbox/chromatin-agent.js +4 -5
  66. package/dist/prompts/sandbox/data-profiler.js +11 -34
  67. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
  69. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
  71. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  72. package/dist/prompts/sandbox/enrichment-agent.js +3 -1
  73. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  74. package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
  75. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  76. package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
  77. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  78. package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
  79. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  80. package/dist/prompts/sandbox/microbiome-agent.js +3 -1
  81. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  82. package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
  83. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  84. package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
  85. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  86. package/dist/prompts/sandbox/network-agent.js +3 -1
  87. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  88. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
  89. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/proteomics-agent.js +3 -1
  91. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  92. package/dist/prompts/sandbox/scientific-executor.js +0 -12
  93. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/single-cell-agent.js +3 -1
  95. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  96. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
  97. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  98. package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
  99. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  100. package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
  101. package/dist/prompts/sandbox-standards.d.ts +15 -18
  102. package/dist/prompts/sandbox-standards.js +96 -305
  103. package/dist/prompts/synthesis-agent.d.ts +1 -1
  104. package/dist/prompts/synthesis-agent.js +24 -90
  105. package/dist/provenance/collector.d.ts +2 -5
  106. package/dist/provenance/collector.js +2 -5
  107. package/dist/provenance/seam.d.ts +36 -1
  108. package/dist/provenance/workspace-reads.d.ts +36 -0
  109. package/dist/provenance/workspace-reads.js +33 -0
  110. package/dist/providers/request-headers.js +12 -3
  111. package/dist/report-model/production-resolver.d.ts +22 -52
  112. package/dist/report-model/production-resolver.js +43 -112
  113. package/dist/report-render/chart-export.d.ts +9 -0
  114. package/dist/report-render/chart-export.js +1 -1
  115. package/dist/report-render/views/chart-view.d.ts +1 -0
  116. package/dist/report-render/views/chart-view.js +1 -0
  117. package/dist/report-render/views/lineage.d.ts +1 -0
  118. package/dist/report-render/views/page-view.d.ts +1 -0
  119. package/dist/report-render/views/page-view.js +1 -0
  120. package/dist/report-render/views/prose.d.ts +1 -0
  121. package/dist/report-render/views/prose.js +1 -0
  122. package/dist/report-render/views/references-view.d.ts +1 -0
  123. package/dist/report-render/views/values.d.ts +1 -0
  124. package/dist/report-render/views/values.js +1 -0
  125. package/dist/runtime/assemble.d.ts +20 -9
  126. package/dist/runtime/assemble.js +47 -38
  127. package/dist/sandbox/client.d.ts +5 -3
  128. package/dist/sandbox/create-sandbox.d.ts +7 -12
  129. package/dist/sandbox/create-sandbox.js +15 -37
  130. package/dist/sandbox/docker-client.d.ts +1 -3
  131. package/dist/sandbox/docker-client.js +3 -5
  132. package/dist/sandbox/exec-outcome.d.ts +11 -11
  133. package/dist/sandbox/exec-outcome.js +22 -22
  134. package/dist/sandbox/exec.d.ts +5 -0
  135. package/dist/sandbox/exec.js +9 -1
  136. package/dist/sandbox/identity.d.ts +4 -16
  137. package/dist/sandbox/identity.js +5 -21
  138. package/dist/sandbox/k8s-client.d.ts +0 -1
  139. package/dist/sandbox/k8s-client.js +8 -13
  140. package/dist/sandbox/lease-names.d.ts +6 -0
  141. package/dist/sandbox/lease-names.js +12 -0
  142. package/dist/sandbox/leases.d.ts +105 -0
  143. package/dist/sandbox/leases.js +519 -0
  144. package/dist/sandbox/liveness.d.ts +5 -2
  145. package/dist/sandbox/mount-plan.d.ts +10 -1
  146. package/dist/sandbox/mount-plan.js +7 -1
  147. package/dist/sandbox/reconciler.d.ts +58 -0
  148. package/dist/sandbox/reconciler.js +153 -0
  149. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  150. package/dist/sandbox/types.d.ts +25 -18
  151. package/dist/sandbox/types.js +10 -4
  152. package/dist/schemas/plan-schemas.d.ts +0 -21
  153. package/dist/schemas/plan-schemas.js +2 -3
  154. package/dist/schemas/run-synthesis.d.ts +2 -2
  155. package/dist/schemas/run-synthesis.js +3 -3
  156. package/dist/schemas/validate-plan.d.ts +1 -14
  157. package/dist/schemas/validate-plan.js +5 -24
  158. package/dist/schemas/workflow-state.d.ts +3 -32
  159. package/dist/schemas/workflow-state.js +4 -13
  160. package/dist/state/index.d.ts +0 -1
  161. package/dist/state/index.js +0 -1
  162. package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
  163. package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
  164. package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
  165. package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
  166. package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
  167. package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
  168. package/dist/state/migrations/index.js +6 -0
  169. package/dist/state/purge-analysis.d.ts +4 -2
  170. package/dist/state/purge-analysis.js +22 -11
  171. package/dist/state/sandbox-leases.d.ts +81 -0
  172. package/dist/state/sandbox-leases.js +247 -0
  173. package/dist/state/schema.d.ts +1 -13
  174. package/dist/state/schema.js +1 -6
  175. package/dist/state/step-executions.d.ts +0 -2
  176. package/dist/state/step-executions.js +1 -4
  177. package/dist/tasks/data-profile.d.ts +40 -2
  178. package/dist/tasks/data-profile.js +89 -87
  179. package/dist/testing.d.ts +2 -0
  180. package/dist/testing.js +6 -1
  181. package/dist/tools/ad-hoc-router.d.ts +1 -6
  182. package/dist/tools/ad-hoc-router.js +5 -48
  183. package/dist/tools/bio/comptox.js +1 -1
  184. package/dist/tools/bio/drug-gene-interactions.js +1 -1
  185. package/dist/tools/bio/gene-disease-evidence.js +1 -1
  186. package/dist/tools/bio/keys.d.ts +1 -1
  187. package/dist/tools/bio/pubmed.js +31 -0
  188. package/dist/tools/bio/search-gene.js +17 -0
  189. package/dist/tools/bio/search-geo-datasets.js +13 -0
  190. package/dist/tools/bio/search-regulatory-approvals.js +1 -1
  191. package/dist/tools/define-tool.d.ts +25 -2
  192. package/dist/tools/define-tool.js +11 -1
  193. package/dist/tools/display/show-user.js +1 -1
  194. package/dist/tools/execute-analysis.d.ts +1 -3
  195. package/dist/tools/execute-analysis.js +5 -33
  196. package/dist/tools/report-session/index.d.ts +0 -1
  197. package/dist/tools/report-session/index.js +0 -1
  198. package/dist/tools/report-session/script-tools.d.ts +30 -0
  199. package/dist/tools/report-session/script-tools.js +177 -0
  200. package/dist/tools/research/generate-analogy-report.d.ts +16 -9
  201. package/dist/tools/research/generate-analogy-report.js +111 -49
  202. package/dist/tools/research/generate-plan.d.ts +102 -7
  203. package/dist/tools/research/generate-plan.js +74 -43
  204. package/dist/tools/research/index.d.ts +4 -3
  205. package/dist/tools/research/index.js +4 -3
  206. package/dist/tools/research/inspect-run.d.ts +0 -1
  207. package/dist/tools/research/inspect-run.js +5 -10
  208. package/dist/tools/research/search-arxiv.js +15 -0
  209. package/dist/tools/research/search-github-repos.js +13 -0
  210. package/dist/tools/research/search-semantic-scholar.js +16 -0
  211. package/dist/tools/research/synthesis-lookups.d.ts +21 -0
  212. package/dist/tools/research/synthesis-lookups.js +29 -0
  213. package/dist/tools/research/web-fetch.d.ts +1 -1
  214. package/dist/tools/research/web-fetch.js +14 -12
  215. package/dist/tools/sandbox/lease-tools.d.ts +32 -0
  216. package/dist/tools/sandbox/lease-tools.js +60 -0
  217. package/dist/tools/script/codemode-worker.d.ts +5 -0
  218. package/dist/tools/script/codemode-worker.js +5 -0
  219. package/dist/tools/script/file-tools.d.ts +100 -0
  220. package/dist/tools/script/file-tools.js +377 -0
  221. package/dist/tools/script/metrics.d.ts +21 -0
  222. package/dist/tools/script/metrics.js +38 -0
  223. package/dist/tools/script/quickjs.wasm +0 -0
  224. package/dist/tools/script/run-script.d.ts +76 -0
  225. package/dist/tools/script/run-script.js +326 -0
  226. package/dist/tools/script/scratch.d.ts +81 -0
  227. package/dist/tools/script/scratch.js +142 -0
  228. package/dist/tools/start-report-session.js +1 -1
  229. package/dist/tools/workspace/edit-file.js +2 -1
  230. package/dist/tools/workspace/execute-command.d.ts +9 -13
  231. package/dist/tools/workspace/execute-command.js +23 -19
  232. package/dist/tools/workspace/file-stat.js +1 -1
  233. package/dist/tools/workspace/grep.d.ts +5 -2
  234. package/dist/tools/workspace/grep.js +19 -10
  235. package/dist/tools/workspace/mutator.d.ts +48 -4
  236. package/dist/tools/workspace/mutator.js +43 -12
  237. package/dist/tools/workspace/read-file.d.ts +5 -2
  238. package/dist/tools/workspace/read-file.js +15 -11
  239. package/dist/tools/workspace/read-surface.d.ts +17 -0
  240. package/dist/tools/workspace/read-surface.js +17 -0
  241. package/dist/tools/workspace/result-bounds.d.ts +1 -0
  242. package/dist/tools/workspace/run-exec.d.ts +12 -13
  243. package/dist/tools/workspace/run-exec.js +13 -13
  244. package/dist/tools/workspace/scan-inputs.d.ts +2 -5
  245. package/dist/tools/workspace/scan-inputs.js +2 -3
  246. package/dist/tools/workspace/show-file.js +1 -1
  247. package/dist/tools/workspace/show-plan.js +4 -3
  248. package/dist/tools/workspace/write-file.js +2 -1
  249. package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
  250. package/dist/workflows/execute-analysis-scheduler.js +12 -32
  251. package/dist/workflows/execute-analysis.d.ts +17 -35
  252. package/dist/workflows/execute-analysis.js +13 -107
  253. package/dist/workflows/pause.js +29 -22
  254. package/dist/workflows/sandbox-step.d.ts +16 -31
  255. package/dist/workflows/sandbox-step.js +36 -71
  256. package/dist/workflows/test/sql-writes.d.ts +0 -1
  257. package/dist/workflows/test/sql-writes.js +0 -2
  258. package/dist/workspace/filesystem.d.ts +2 -2
  259. package/dist/workspace/filesystem.js +10 -5
  260. package/dist/workspace/paths.d.ts +19 -1
  261. package/dist/workspace/paths.js +22 -1
  262. package/package.json +6 -3
  263. package/dist/prompts/literature-reviewer.d.ts +0 -1
  264. package/dist/prompts/literature-reviewer.js +0 -114
  265. package/dist/sandbox/reaper.d.ts +0 -81
  266. package/dist/sandbox/reaper.js +0 -161
  267. package/dist/sandbox/step-sandbox.d.ts +0 -47
  268. package/dist/sandbox/step-sandbox.js +0 -53
  269. package/dist/state/active-sandboxes.d.ts +0 -33
  270. package/dist/state/active-sandboxes.js +0 -65
  271. package/dist/tasks/extract-values-script.d.ts +0 -64
  272. package/dist/tasks/extract-values-script.js +0 -180
  273. package/dist/tasks/extract-values.d.ts +0 -77
  274. package/dist/tasks/extract-values.js +0 -146
  275. package/dist/tools/report-session/derive-table.d.ts +0 -164
  276. package/dist/tools/report-session/derive-table.js +0 -419
  277. package/dist/tools/research/literature-reviewer.d.ts +0 -42
  278. package/dist/tools/research/literature-reviewer.js +0 -107
  279. package/dist/workflows/metrics.d.ts +0 -23
  280. package/dist/workflows/metrics.js +0 -38
@@ -2,12 +2,11 @@
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
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.
5
+ * Dependency-bearing factory (see the harness-durable-runtime spec): captures the
6
+ * lease of the step. The sandbox module gives the machine of the lease, or a new
7
+ * one when the machine went away, thus the durability, idempotency, and liveness
8
+ * story of `harness-sandbox-exec` holds for each command. A command with no lease
9
+ * gives an error result that names `acquire_sandbox`.
11
10
  *
12
11
  * The loop runs each call as one durable step, and the exec runs inside it,
13
12
  * thus the exec id is the id of that step and a replay returns the cached
@@ -26,7 +25,7 @@
26
25
  * code that ran.
27
26
  */
28
27
  import { posix as posixPath } from "node:path";
29
- import { ok } from "neverthrow";
28
+ import { err, ok } from "neverthrow";
30
29
  import { z } from "zod";
31
30
  import { defineTool, withToolCallRecord } from "../define-tool.js";
32
31
  import { feedExecFrame } from "../../provenance/exec-frame.js";
@@ -39,16 +38,17 @@ const ExecuteCommandInputSchema = z.object({
39
38
  .array(z.string().min(1))
40
39
  .min(1)
41
40
  .describe("argv array — e.g. ['python', 'scripts/run.py'] or ['head', '-n', '50', 'data/inputs/x.csv']. " +
42
- "A one-element array runs through `sh -c`, so pipes, `&&`, quoting, and globbing work; " +
43
- "an array of two or more elements runs directly, with no shell, and nothing is expanded."),
41
+ "A one-element array runs through `sh -c`, so pipes, `&&`, quoting, and globbing work, and a path that holds a space " +
42
+ "needs double quotes there; an array of two or more elements runs directly, with no shell, and nothing is expanded."),
44
43
  cwd: z
45
44
  .string()
46
45
  .optional()
47
46
  .describe("Working directory inside the sandbox. Defaults to your working " +
48
47
  "directory; a relative value is resolved against it, an absolute " +
49
- "'/<analysisId>/...' value is used as-is."),
48
+ "'/<analysisId>/...' value is used as-is. Each call starts here: a `cd` in an earlier call " +
49
+ "does not carry over, thus pass `cwd` or chain the commands with `&&` in one call."),
50
50
  env: z.record(z.string(), z.string()).optional().describe("Extra environment variables for this command."),
51
- timeoutSeconds: z.number().int().positive().optional().describe("Per-command timeout in seconds; capped by the step deadline."),
51
+ timeoutSeconds: z.number().int().positive().optional().describe("Per-command timeout in seconds. Default 3600."),
52
52
  });
53
53
  /** Extensions that mark an argv token as the thing being run, not a flag or a data path. */
54
54
  const SCRIPT_EXTENSIONS = [".py", ".R", ".r", ".sh", ".js", ".ts", ".ipynb"];
@@ -57,18 +57,21 @@ function scriptToken(command) {
57
57
  return command.find((arg) => SCRIPT_EXTENSIONS.some((extension) => arg.endsWith(extension)));
58
58
  }
59
59
  export function createExecuteCommandTool(deps) {
60
- const { sandboxClient, sandbox, deadlineMs, defaultCwd, lineageCollector, mountRoot } = deps;
60
+ const { lease, defaultCwd, lineageCollector, mountRoot } = deps;
61
61
  return defineTool({
62
62
  id: "execute_command",
63
+ ownStep: true,
63
64
  description: "Run a command in the sandbox and return its stdout/stderr/exit code. " +
64
65
  "Use for scripts, CLI tools, shell pipes, and anything the workspace " +
65
66
  `tools don't express. stdout and stderr are each capped at ${EXEC_STREAM_BYTE_CAP / (1024 * 1024)} MiB; the end ` +
66
67
  "of a longer stream is dropped, and the result carries its truncation flag. A long result comes back as an " +
67
68
  "excerpt of its start and its end, with a reference to read the rest when the harness keeps it. " +
68
69
  "Failures are returned as data, not thrown: `status` is `ok` whenever the call completed, so read " +
69
- "`exitCode` for the outcome of the command. `timedOut: true` means the timeout stopped the command, and a " +
70
- "`syntheticFailure` with `exitCode: null` means the sandbox died under the command (its reason names an " +
71
- "out-of-memory kill). stdout and stderr are not a deliverable: they are ephemeral, " +
70
+ "`exitCode` for the outcome of the command. `timedOut: true` means the timeout stopped the command. A " +
71
+ "`syntheticFailure` with `exitCode: null` means the sandbox ended under the command: `sandbox-oom-killed` " +
72
+ "(the command passed `memoryLimitGb`), `sandbox-dead`, or `sandbox-replaced`. The command may have left " +
73
+ "partial files, and nothing runs it again. The next command runs on a new sandbox of the same size; call " +
74
+ "`acquire_sandbox` with a larger size for more memory. stdout and stderr are not a deliverable: they are ephemeral, " +
72
75
  "so use them to confirm that a command ran. A result that you compute with 'python -c' or an inline " +
73
76
  "one-liner and read from stdout is lost. Write a script and persist its outputs to output/.",
74
77
  inputSchema: ExecuteCommandInputSchema,
@@ -110,16 +113,17 @@ export function createExecuteCommandTool(deps) {
110
113
  recordWrites.push(noted.write);
111
114
  }
112
115
  }
113
- const result = await runSandboxExec({
114
- sandboxClient,
115
- sandbox,
116
+ const ran = await runSandboxExec({
117
+ lease,
116
118
  command,
117
119
  cwd: effectiveCwd,
118
120
  env,
119
121
  timeoutSeconds,
120
- deadlineMs: deadlineMs(),
121
122
  emit: ctx.emit,
122
123
  });
124
+ if (ran.isErr())
125
+ return err(ran.error);
126
+ const result = ran.value;
123
127
  const bounded = { status: "ok", ...boundExecResult(result) };
124
128
  if ((!lineageCollector || !mountRoot) && recordWrites.length === 0)
125
129
  return ok(bounded);
@@ -19,7 +19,7 @@ export function createFileStatTool(fs, workingDir) {
19
19
  return defineTool({
20
20
  id: "file_stat",
21
21
  description: "Stat a workspace path: whether it is a file or directory and its size " +
22
- "in bytes. Use before reading to size a file. Missing and out-of-scope " +
22
+ "in bytes. It reads the workspace directly and runs no sandbox command. Use before reading to size a file. Missing and out-of-scope " +
23
23
  "paths return a data variant, not an error.",
24
24
  inputSchema: FileStatInputSchema,
25
25
  describeCall: "none",
@@ -8,7 +8,8 @@
8
8
  * `out_of_scope`. Only unexpected I/O failures throw.
9
9
  */
10
10
  import { z } from "zod";
11
- import { type Tool } from "../define-tool.js";
11
+ import { type Tool, type WithToolCallRecord } from "../define-tool.js";
12
+ import type { WorkspaceReadLineage, WorkspaceReadRecord } from "../../provenance/workspace-reads.js";
12
13
  import type { ListEntry, WorkspaceFilesystem } from "../../workspace/filesystem.js";
13
14
  declare const GrepInputSchema: z.ZodObject<{
14
15
  pattern: z.ZodString;
@@ -49,7 +50,9 @@ type GrepOutput = {
49
50
  pattern: string;
50
51
  matches: Match[];
51
52
  };
52
- export declare function createGrepTool(fs: WorkspaceFilesystem, workingDir?: string): Tool<z.infer<typeof GrepInputSchema>, GrepOutput>;
53
+ type GrepToolOutput = GrepOutput | WithToolCallRecord<GrepOutput, WorkspaceReadRecord>;
54
+ /** `lineage` records each file whose lines the result returns in the lineage of a step. */
55
+ export declare function createGrepTool(fs: WorkspaceFilesystem, workingDir?: string, lineage?: WorkspaceReadLineage): Tool<z.infer<typeof GrepInputSchema>, GrepToolOutput>;
53
56
  /** Exposed for tests — the caps `grep` enforces. */
54
57
  export declare const GREP_LIMITS: {
55
58
  readonly DEFAULT_MAX_MATCHES: 50;
@@ -14,7 +14,7 @@ import { ok } from "neverthrow";
14
14
  import { presentAt } from "../../lib/present.js";
15
15
  import { splitLines } from "../../lib/text.js";
16
16
  import { z } from "zod";
17
- import { defineTool } from "../define-tool.js";
17
+ import { defineTool, withToolCallRecord } from "../define-tool.js";
18
18
  import { unwrapOrThrow } from "../../lib/result.js";
19
19
  const DEFAULT_MAX_MATCHES = 50;
20
20
  const DEFAULT_MAX_MATCH_BYTES = 512;
@@ -33,11 +33,12 @@ const GrepInputSchema = z.object({
33
33
  .describe("Workspace path to a file or directory. Directories are searched " + "recursively up to a bounded depth."),
34
34
  ignoreCase: z.boolean().optional().describe("Case-insensitive match. Defaults to false."),
35
35
  });
36
- export function createGrepTool(fs, workingDir) {
36
+ /** `lineage` records each file whose lines the result returns in the lineage of a step. */
37
+ export function createGrepTool(fs, workingDir, lineage) {
37
38
  return defineTool({
38
39
  id: "grep",
39
40
  description: "Regex search over the text files of the workspace (input data, prior runs, step " +
40
- "outputs, summaries, syntheses). Pass a file or a " +
41
+ "outputs, summaries, syntheses). It reads the workspace directly and runs no sandbox command. Pass a file or a " +
41
42
  `directory path; directories are walked recursively, to a depth of ${MAX_DIR_DEPTH}. Results are ` +
42
43
  `capped at ${DEFAULT_MAX_MATCHES} matches, and each preview at ${DEFAULT_MAX_MATCH_BYTES} characters of its line. ` +
43
44
  `Only the first ${MAX_GREP_FILE_BYTES / (1024 * 1024)} MiB of each file is searched, and a file with a NUL byte ` +
@@ -47,6 +48,7 @@ export function createGrepTool(fs, workingDir) {
47
48
  // Both fields, because either alone loses the call: the same pattern over
48
49
  // two trees and two patterns over one tree are both ordinary sequences.
49
50
  describeCall: ({ pattern, path }) => `${pattern} in ${path}`,
51
+ foldCallRecord: lineage === undefined ? undefined : (record) => lineage.track(record.reads),
50
52
  execute: async ({ pattern, path, ignoreCase }, ctx) => {
51
53
  let regex;
52
54
  try {
@@ -68,6 +70,7 @@ export function createGrepTool(fs, workingDir) {
68
70
  return ok({ status: "not_found", path });
69
71
  }
70
72
  const matches = [];
73
+ const reads = [];
71
74
  let truncated = false;
72
75
  const session = ctx.session;
73
76
  async function searchFile(filePath) {
@@ -90,10 +93,11 @@ export function createGrepTool(fs, workingDir) {
90
93
  if (read.kind === "truncated")
91
94
  truncated = true;
92
95
  const lines = splitLines(text);
96
+ const before = matches.length;
93
97
  for (let i = 0; i < lines.length; i++) {
94
98
  if (matches.length >= DEFAULT_MAX_MATCHES) {
95
99
  truncated = true;
96
- return;
100
+ break;
97
101
  }
98
102
  const line = presentAt(lines, i, "the scanned line");
99
103
  if (regex.test(line)) {
@@ -104,6 +108,11 @@ export function createGrepTool(fs, workingDir) {
104
108
  });
105
109
  }
106
110
  }
111
+ if (matches.length === before)
112
+ return;
113
+ const noted = lineage?.readOf(filePath, read.kind === "ok" ? buf : null);
114
+ if (noted !== undefined)
115
+ reads.push(noted);
107
116
  }
108
117
  async function walk(dirPath, depth) {
109
118
  if (depth > MAX_DIR_DEPTH)
@@ -138,17 +147,17 @@ export function createGrepTool(fs, workingDir) {
138
147
  if (matches.length === 0) {
139
148
  return ok({ status: "no_matches", path, pattern });
140
149
  }
141
- // oxlint-disable-next-line no-unnecessary-condition -- tsgolint cannot see the walk/searchFile closures that set truncated; the check is live (it returns the truncated result).
142
- if (truncated) {
143
- return ok({
150
+ // oxlint-disable-next-line no-unnecessary-condition -- tsgolint cannot see the walk/searchFile closures that set truncated; the check is live (it selects the truncated result).
151
+ const output = truncated
152
+ ? {
144
153
  status: "truncated",
145
154
  path,
146
155
  pattern,
147
156
  matches,
148
157
  cap: { maxMatches: DEFAULT_MAX_MATCHES, maxBytesPerLine: DEFAULT_MAX_MATCH_BYTES },
149
- });
150
- }
151
- return ok({ status: "ok", path, pattern, matches });
158
+ }
159
+ : { status: "ok", path, pattern, matches };
160
+ return ok(reads.length === 0 ? output : withToolCallRecord(output, { reads }));
152
161
  },
153
162
  });
154
163
  }
@@ -19,9 +19,9 @@
19
19
  * closes over fixed per-step coordinates and records into the step-scoped
20
20
  * lineage collector (the sandbox agents). `createSessionWorkspaceMutator`
21
21
  * resolves its coordinates per call from the session's analysis scope — the
22
- * write prefix is the analysis root — and emits a `write-file` session event
23
- * through the provenance seam (the conversation agent, which runs under no
24
- * run and no step).
22
+ * write prefix is the analysis root, less the staged inputs — and emits a
23
+ * `write-file` session event through the provenance seam (the conversation
24
+ * agent, which runs under no run and no step).
25
25
  *
26
26
  * The landing is hardened beyond the lexical resolver: missing parent
27
27
  * directories are created only inside the write prefix, the deepest existing
@@ -53,6 +53,10 @@ export type WriteFileResult = {
53
53
  } | {
54
54
  readonly status: "symlink_denied";
55
55
  readonly path: string;
56
+ } | {
57
+ readonly status: "read_only";
58
+ readonly path: string;
59
+ readonly reason: string;
56
60
  };
57
61
  /**
58
62
  * What one landed write leaves for provenance: the hash and the size of the
@@ -136,6 +140,44 @@ export type WorkspaceMutator = {
136
140
  */
137
141
  recordWrite(record: WriteRecord): void;
138
142
  };
143
+ /** Outcome of the hardened landing — the disk-touching half of a confined write. */
144
+ type LandingStatus = "ok" | "out_of_prefix" | "symlink_denied" | "read_only";
145
+ /** One landed confined write: the paths in both frames, and the exact bytes on disk. */
146
+ export type ConfinedWriteOk = {
147
+ readonly status: "ok";
148
+ /** Analysis-root-relative, forward-slashed — the provenance-facing form. */
149
+ readonly relative: string;
150
+ /** The `/{analysisId}/…` frame-independent form — the agent-facing result path. */
151
+ readonly agentPath: string;
152
+ readonly bytes: Buffer;
153
+ };
154
+ /** A refused confined write: the status, and the reason when the status alone does not give it. */
155
+ type ConfinedWriteRefusal = {
156
+ readonly status: Exclude<LandingStatus, "ok" | "read_only"> | "out_of_scope";
157
+ } | {
158
+ readonly status: "read_only";
159
+ readonly reason: string;
160
+ };
161
+ /**
162
+ * The shared write gauntlet both realizations run: resolve + confine
163
+ * (`resolveForWrite`), then the hardened landing. A refusal comes back as data;
164
+ * an ok carries what the caller's provenance record needs — the
165
+ * analysis-relative path and the exact bytes that landed. `beforeLand` gets
166
+ * the same ok while the bytes wait in the temporary file. With
167
+ * `refuseStagedInputs`, a write under the staged inputs is refused before any
168
+ * I/O, and the landing refuses a symlink that aims into them.
169
+ */
170
+ export declare function confinedWrite(args: {
171
+ readonly workspaceRoot: string;
172
+ readonly analysisId: string;
173
+ readonly workingDir: string;
174
+ readonly path: string;
175
+ /** Text lands as UTF-8. */
176
+ readonly content: string | Uint8Array;
177
+ readonly invocationId: string;
178
+ readonly beforeLand: ((staged: ConfinedWriteOk) => Promise<void>) | undefined;
179
+ readonly refuseStagedInputs: boolean;
180
+ }): Promise<ConfinedWriteRefusal | ConfinedWriteOk>;
139
181
  export declare function createWorkspaceMutator(deps: WorkspaceMutatorDeps): WorkspaceMutator;
140
182
  /** Construction deps of the session-scoped realization — see {@link createSessionWorkspaceMutator}. */
141
183
  export type SessionWorkspaceMutatorDeps = {
@@ -158,10 +200,12 @@ export type SessionWorkspaceMutatorDeps = {
158
200
  * resolves the workspace root from the session's analysis scope, and the write
159
201
  * prefix IS that root — the agent modifies its own analysis tree freely, while
160
202
  * `..` traversal, a foreign analysis, and the symlink escapes stay refused by
161
- * the same gauntlet.
203
+ * the same gauntlet. The staged inputs are the one part of the tree that it
204
+ * cannot write: a write under them comes back `read_only`, with the reason.
162
205
  *
163
206
  * A write here runs under no run and no step, so its provenance is a session
164
207
  * observation, not a collector record: each successful write emits one
165
208
  * `write-file` session event (see `provenance/seam.ts`).
166
209
  */
167
210
  export declare function createSessionWorkspaceMutator(deps: SessionWorkspaceMutatorDeps): WorkspaceMutator;
211
+ export {};
@@ -19,9 +19,9 @@
19
19
  * closes over fixed per-step coordinates and records into the step-scoped
20
20
  * lineage collector (the sandbox agents). `createSessionWorkspaceMutator`
21
21
  * resolves its coordinates per call from the session's analysis scope — the
22
- * write prefix is the analysis root — and emits a `write-file` session event
23
- * through the provenance seam (the conversation agent, which runs under no
24
- * run and no step).
22
+ * write prefix is the analysis root, less the staged inputs — and emits a
23
+ * `write-file` session event through the provenance seam (the conversation
24
+ * agent, which runs under no run and no step).
25
25
  *
26
26
  * The landing is hardened beyond the lexical resolver: missing parent
27
27
  * directories are created only inside the write prefix, the deepest existing
@@ -43,7 +43,7 @@ import { computeSha256 } from "../../lib/fs-helpers.js";
43
43
  import { tryFs, tryFsWrite } from "../../lib/fs-result.js";
44
44
  import { unwrapOrThrow } from "../../lib/result.js";
45
45
  import { bindSessionEmit } from "../../provenance/seam.js";
46
- import { resolveForWrite } from "../../workspace/paths.js";
46
+ import { resolveForWrite, STAGED_INPUT_ROOT } from "../../workspace/paths.js";
47
47
  /**
48
48
  * Agent-visible name of the tool driving a confined write. Rides the write args
49
49
  * so a successful write is attributed to the invoking tool in provenance
@@ -51,6 +51,15 @@ import { resolveForWrite } from "../../workspace/paths.js";
51
51
  * inspecting its caller.
52
52
  */
53
53
  export const MUTATE_TOOL_NAMES = ["write_file", "edit_file", "knowledge_template", "execute_command"];
54
+ /** The refusal of a write that lands under the staged inputs. */
55
+ const STAGED_INPUT_REFUSAL = {
56
+ status: "read_only",
57
+ reason: `${STAGED_INPUT_ROOT}/ holds the staged inputs of the analysis, and they are immutable. Write the file to a different directory.`,
58
+ };
59
+ /** True when `path` is `dir` or a path under it. */
60
+ function isWithin(dir, path) {
61
+ return path === dir || path.startsWith(dir + sep);
62
+ }
54
63
  /**
55
64
  * Land `bytes` at `absolute`, hardened against a symlink re-aiming the write.
56
65
  * `absolute` is already lexically confined under `prefix` by the resolver;
@@ -70,12 +79,20 @@ export const MUTATE_TOOL_NAMES = ["write_file", "edit_file", "knowledge_template
70
79
  * temporary file beside it and rename that file over it. The rename
71
80
  * replaces a symlink raced in after the check, and never follows it.
72
81
  *
82
+ * `readOnlyDir` is a directory under `prefix` that takes no write. The landing
83
+ * refuses a canonical target under it as `read_only`, before the mkdir of
84
+ * step 3 and again before step 4. Thus a symlinked ancestor cannot aim a write
85
+ * into it.
86
+ *
73
87
  * Refusals are data; a genuine I/O failure throws (the loop's dispatch catch
74
88
  * maps it to an error tool result). A throw removes the temporary file.
75
89
  */
76
- async function landBytes(prefix, absolute, bytes, stagingKey, beforeRename) {
90
+ async function landBytes(prefix, absolute, bytes, stagingKey, beforeRename, readOnlyDir) {
77
91
  unwrapOrThrow(await tryFsWrite("mutator.mkdirPrefix", () => mkdir(prefix, { recursive: true }), { path: prefix }));
78
92
  const realPrefix = unwrapOrThrow(await tryFs("mutator.realpathPrefix", () => realpath(prefix), { path: prefix }));
93
+ const realReadOnly = readOnlyDir === undefined
94
+ ? undefined
95
+ : (unwrapOrThrow(await tryFs("mutator.realpathReadOnly", () => realpath(readOnlyDir), { path: readOnlyDir, onAbsent: () => null })) ?? joinPath(realPrefix, relativePath(prefix, readOnlyDir)));
79
96
  // Deepest existing ancestor of the target. The walk stops at the prefix,
80
97
  // which exists — the resolver already confined the lexical path under it.
81
98
  let deepest = dirname(absolute);
@@ -91,6 +108,8 @@ async function landBytes(prefix, absolute, bytes, stagingKey, beforeRename) {
91
108
  if (realDeepest !== realPrefix && !realDeepest.startsWith(realPrefix + sep))
92
109
  return "out_of_prefix";
93
110
  const targetDir = joinPath(realDeepest, relativePath(deepest, dirname(absolute)));
111
+ if (realReadOnly !== undefined && isWithin(realReadOnly, joinPath(targetDir, basename(absolute))))
112
+ return "read_only";
94
113
  let landingDir = realDeepest;
95
114
  if (targetDir !== realDeepest) {
96
115
  unwrapOrThrow(await tryFsWrite("mutator.mkdirParents", () => mkdir(targetDir, { recursive: true }), { path: targetDir }));
@@ -108,6 +127,8 @@ async function landBytes(prefix, absolute, bytes, stagingKey, beforeRename) {
108
127
  landingDir = realTarget;
109
128
  }
110
129
  const finalPath = joinPath(landingDir, basename(absolute));
130
+ if (realReadOnly !== undefined && isWithin(realReadOnly, finalPath))
131
+ return "read_only";
111
132
  const finalStat = unwrapOrThrow(await tryFs("mutator.lstatFinal", () => lstat(finalPath), { path: finalPath, onAbsent: () => null }));
112
133
  if (finalStat !== null && finalStat.isSymbolicLink())
113
134
  return "symlink_denied";
@@ -158,9 +179,11 @@ async function stageBytes(stagingPath, bytes, replacedMode) {
158
179
  * (`resolveForWrite`), then the hardened landing. A refusal comes back as data;
159
180
  * an ok carries what the caller's provenance record needs — the
160
181
  * analysis-relative path and the exact bytes that landed. `beforeLand` gets
161
- * the same ok while the bytes wait in the temporary file.
182
+ * the same ok while the bytes wait in the temporary file. With
183
+ * `refuseStagedInputs`, a write under the staged inputs is refused before any
184
+ * I/O, and the landing refuses a symlink that aims into them.
162
185
  */
163
- async function confinedWrite(args) {
186
+ export async function confinedWrite(args) {
164
187
  const scoped = resolveForWrite({
165
188
  workspaceRoot: args.workspaceRoot,
166
189
  analysisId: args.analysisId,
@@ -175,12 +198,17 @@ async function confinedWrite(args) {
175
198
  // path prepends the `/{analysisId}` frame; a provenance record uses
176
199
  // the bare tail (the collector normalizes it step-relative itself).
177
200
  const relative = scoped.relative.split("\\").join("/");
201
+ if (args.refuseStagedInputs && (relative === STAGED_INPUT_ROOT || relative.startsWith(`${STAGED_INPUT_ROOT}/`)))
202
+ return STAGED_INPUT_REFUSAL;
178
203
  const agentPath = `/${args.analysisId}/${relative}`;
179
- const bytes = Buffer.from(args.content, "utf8");
204
+ const bytes = typeof args.content === "string" ? Buffer.from(args.content, "utf8") : Buffer.from(args.content);
180
205
  const prefix = resolvePath(args.workingDir);
181
206
  const confined = { status: "ok", relative, agentPath, bytes };
182
207
  const { beforeLand } = args;
183
- const landed = await landBytes(prefix, scoped.absolute, bytes, args.invocationId, beforeLand === undefined ? undefined : () => beforeLand(confined));
208
+ const readOnlyDir = args.refuseStagedInputs ? resolvePath(args.workspaceRoot, STAGED_INPUT_ROOT) : undefined;
209
+ const landed = await landBytes(prefix, scoped.absolute, bytes, args.invocationId, beforeLand === undefined ? undefined : () => beforeLand(confined), readOnlyDir);
210
+ if (landed === "read_only")
211
+ return STAGED_INPUT_REFUSAL;
184
212
  if (landed !== "ok")
185
213
  return { status: landed };
186
214
  return confined;
@@ -212,9 +240,10 @@ export function createWorkspaceMutator(deps) {
212
240
  content,
213
241
  invocationId,
214
242
  beforeLand: beforeLand === undefined ? undefined : (staged) => beforeLand(landedWrite(staged)),
243
+ refuseStagedInputs: false,
215
244
  });
216
245
  if (landed.status !== "ok")
217
- return { status: landed.status, path };
246
+ return { ...landed, path };
218
247
  return landedWrite(landed);
219
248
  },
220
249
  recordWrite(record) {
@@ -235,7 +264,8 @@ export function createWorkspaceMutator(deps) {
235
264
  * resolves the workspace root from the session's analysis scope, and the write
236
265
  * prefix IS that root — the agent modifies its own analysis tree freely, while
237
266
  * `..` traversal, a foreign analysis, and the symlink escapes stay refused by
238
- * the same gauntlet.
267
+ * the same gauntlet. The staged inputs are the one part of the tree that it
268
+ * cannot write: a write under them comes back `read_only`, with the reason.
239
269
  *
240
270
  * A write here runs under no run and no step, so its provenance is a session
241
271
  * observation, not a collector record: each successful write emits one
@@ -271,9 +301,10 @@ export function createSessionWorkspaceMutator(deps) {
271
301
  content,
272
302
  invocationId,
273
303
  beforeLand: beforeLand === undefined ? undefined : (staged) => beforeLand(landedWrite(staged)),
304
+ refuseStagedInputs: true,
274
305
  });
275
306
  if (landed.status !== "ok")
276
- return { status: landed.status, path };
307
+ return { ...landed, path };
277
308
  return landedWrite(landed);
278
309
  },
279
310
  recordWrite(record) {
@@ -8,7 +8,8 @@
8
8
  * wraps them as `is_error`.
9
9
  */
10
10
  import { z } from "zod";
11
- import { type Tool } from "../define-tool.js";
11
+ import { type Tool, type WithToolCallRecord } from "../define-tool.js";
12
+ import type { WorkspaceReadLineage, WorkspaceReadRecord } from "../../provenance/workspace-reads.js";
12
13
  import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
13
14
  type ReadFileOutput = {
14
15
  status: "invalid_input";
@@ -43,5 +44,7 @@ declare const ReadFileInputSchema: z.ZodObject<{
43
44
  headLines: z.ZodOptional<z.ZodNumber>;
44
45
  tailLines: z.ZodOptional<z.ZodNumber>;
45
46
  }, z.core.$strip>;
46
- export declare function createReadFileTool(fs: WorkspaceFilesystem, workingDir?: string): Tool<z.infer<typeof ReadFileInputSchema>, ReadFileOutput>;
47
+ type ReadFileToolOutput = ReadFileOutput | WithToolCallRecord<ReadFileOutput, WorkspaceReadRecord>;
48
+ /** `lineage` records each read that gave text to the agent in the lineage of a step. */
49
+ export declare function createReadFileTool(fs: WorkspaceFilesystem, workingDir?: string, lineage?: WorkspaceReadLineage): Tool<z.infer<typeof ReadFileInputSchema>, ReadFileToolOutput>;
47
50
  export {};
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { ok } from "neverthrow";
11
11
  import { z } from "zod";
12
- import { defineTool } from "../define-tool.js";
12
+ import { defineTool, withToolCallRecord } from "../define-tool.js";
13
13
  import { unwrapOrThrow } from "../../lib/result.js";
14
14
  /** Default cap — large enough for typical CSV/MD/JSON, small enough to keep tool results out of context-blowing territory. */
15
15
  const DEFAULT_MAX_BYTES = 256 * 1024;
@@ -49,11 +49,17 @@ const ReadFileInputSchema = z.object({
49
49
  .optional()
50
50
  .describe("Read only the last N complete lines (tail). Use for logs and any " + "file where the tail is what matters. Mutually exclusive with headLines."),
51
51
  });
52
- export function createReadFileTool(fs, workingDir) {
52
+ /** `lineage` records each read that gave text to the agent in the lineage of a step. */
53
+ export function createReadFileTool(fs, workingDir, lineage) {
54
+ /** The output, with the read of `path` as its call record when a lineage records it. */
55
+ function withRead(output, path, wholeFile) {
56
+ const read = lineage?.readOf(path, wholeFile);
57
+ return read === undefined ? output : withToolCallRecord(output, { reads: [read] });
58
+ }
53
59
  return defineTool({
54
60
  id: "read_file",
55
61
  description: "Read a workspace file: input data, prior-run outputs, current-run " +
56
- "outputs, step summaries, or run syntheses. Returns the file content " +
62
+ "outputs, step summaries, or run syntheses. It reads the workspace directly and runs no sandbox command. Returns the file content " +
57
63
  "as UTF-8 text. Output is capped at " +
58
64
  `${DEFAULT_MAX_BYTES} bytes; pass headLines or tailLines to read a ` +
59
65
  "specific window of a large file (typical for bio CSV/TSV/log files). " +
@@ -70,6 +76,7 @@ export function createReadFileTool(fs, workingDir) {
70
76
  return `${path} (last ${tailLines} lines)`;
71
77
  return path;
72
78
  },
79
+ foldCallRecord: lineage === undefined ? undefined : (record) => lineage.track(record.reads),
73
80
  execute: async ({ path, headLines, tailLines }, ctx) => {
74
81
  if (headLines !== undefined && tailLines !== undefined) {
75
82
  return ok({
@@ -91,23 +98,20 @@ export function createReadFileTool(fs, workingDir) {
91
98
  case "ok":
92
99
  if (looksBinary(result.content))
93
100
  return ok({ status: "binary", path, mode });
94
- return ok({
95
- status: "ok",
96
- path,
97
- mode,
98
- content: result.content.toString("utf8"),
99
- });
101
+ return ok(withRead({ status: "ok", path, mode, content: result.content.toString("utf8") }, path,
102
+ // A line window is a part of the file, even when the window holds each line.
103
+ mode === "full" ? result.content : null));
100
104
  case "truncated":
101
105
  if (looksBinary(result.content))
102
106
  return ok({ status: "binary", path, mode, totalSize: result.totalSize });
103
- return ok({
107
+ return ok(withRead({
104
108
  status: "truncated",
105
109
  path,
106
110
  mode,
107
111
  content: result.content.toString("utf8"),
108
112
  totalSize: result.totalSize,
109
113
  returnedBytes: result.content.length,
110
- });
114
+ }, path, null));
111
115
  case "not_found":
112
116
  return ok({ status: "not_found", path });
113
117
  case "out_of_scope":
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The read-only surface of the workspace for an agent that plans or interprets the work of a run: the file reads,
3
+ * the semantic search, and the data profile. Each tool reads the tree of the analysis of its session, and no tool
4
+ * writes.
5
+ */
6
+ import type { Querier } from "../../state/db.js";
7
+ import type { EmbeddingProvider } from "../../providers/types.js";
8
+ import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
9
+ import type { Tool } from "../define-tool.js";
10
+ import type { WorkspaceSearchQuerier } from "./workspace-search.js";
11
+ export type ReadSurfaceDeps = {
12
+ readonly workspaceFs: WorkspaceFilesystem;
13
+ readonly pool: Querier & WorkspaceSearchQuerier;
14
+ /** Absent: the agent gets no `workspace_search`. */
15
+ readonly embedding?: EmbeddingProvider;
16
+ };
17
+ export declare function createReadSurfaceTools(deps: ReadSurfaceDeps): Tool[];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The read-only surface of the workspace for an agent that plans or interprets the work of a run: the file reads,
3
+ * the semantic search, and the data profile. Each tool reads the tree of the analysis of its session, and no tool
4
+ * writes.
5
+ */
6
+ import { createInspectDataProfileTool } from "../research/inspect-data-profile.js";
7
+ import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createWorkspaceSearchTool } from "./index.js";
8
+ export function createReadSurfaceTools(deps) {
9
+ return [
10
+ createReadFileTool(deps.workspaceFs),
11
+ createListFilesTool(deps.workspaceFs),
12
+ createFileStatTool(deps.workspaceFs),
13
+ createGrepTool(deps.workspaceFs),
14
+ ...(deps.embedding ? [createWorkspaceSearchTool(deps.pool, deps.embedding)] : []),
15
+ createInspectDataProfileTool(deps.pool),
16
+ ];
17
+ }
@@ -36,6 +36,7 @@ export type BoundedExecResult = {
36
36
  readonly timedOut: boolean;
37
37
  syntheticFailure?: {
38
38
  readonly reason: string;
39
+ readonly memoryLimitGb?: number;
39
40
  };
40
41
  };
41
42
  /**
@@ -1,23 +1,22 @@
1
1
  /**
2
- * Internal helper used by dependency-bearing tools to drive one sandbox exec
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.
2
+ * Internal helper of the tools that run a sandbox command through the lease of their step. It wraps each sandbox
3
+ * event in a `data-sandbox-event` part, thus `execute_command` and the container readout of `scan_inputs` report
4
+ * progress in one shape.
6
5
  *
7
- * Not a registered tool — `defineTool` is called by the user-facing tools
8
- * (`execute-command.ts`) which wrap this.
6
+ * Not a registered tool — `defineTool` is called by the user-facing tools (`execute-command.ts`) which wrap this.
9
7
  */
10
- import type { SandboxClient } from "../../sandbox/client.js";
11
- import type { ExecResult, SandboxRef } from "../../sandbox/types.js";
12
- import type { EmitFn } from "../define-tool.js";
8
+ import { type Result } from "neverthrow";
9
+ import type { BoundLease } from "../../sandbox/leases.js";
10
+ import type { ExecResult } from "../../sandbox/types.js";
11
+ import type { EmitFn, ToolError } from "../define-tool.js";
13
12
  export type RunExecArgs = {
14
- readonly sandboxClient: SandboxClient;
15
- readonly sandbox: SandboxRef;
13
+ readonly lease: BoundLease;
16
14
  readonly command: readonly string[];
17
15
  readonly cwd?: string;
18
16
  readonly env?: Record<string, string>;
19
17
  readonly timeoutSeconds?: number;
20
- readonly deadlineMs: number;
21
18
  readonly emit: EmitFn;
22
19
  };
23
- export declare function runSandboxExec(args: RunExecArgs): Promise<ExecResult>;
20
+ /** The error of an exec with no lease. The order "acquire, then exec" is a rule of the code, not of a prompt. */
21
+ export declare const NO_SANDBOX_ERROR = "No sandbox is held. Call acquire_sandbox with the size that the command needs, then run the command again.";
22
+ export declare function runSandboxExec(args: RunExecArgs): Promise<Result<ExecResult, ToolError>>;