@inflexa-ai/harness 0.46.2 → 0.48.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 (279) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/conversation-agent.d.ts +7 -6
  3. package/dist/agents/conversation-agent.js +26 -4
  4. package/dist/agents/report-session-agent.d.ts +14 -14
  5. package/dist/agents/report-session-agent.js +40 -26
  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/synthesize-run.d.ts +10 -6
  12. package/dist/app/synthesize-run.js +4 -2
  13. package/dist/contracts/analogy-report.d.ts +46 -2
  14. package/dist/contracts/analogy-report.js +31 -25
  15. package/dist/contracts/chat-parts.d.ts +3 -14
  16. package/dist/contracts/report-reference.d.ts +2 -1
  17. package/dist/contracts/report-reference.js +2 -1
  18. package/dist/contracts/schemas/chat-parts.d.ts +0 -27
  19. package/dist/contracts/schemas/chat-parts.js +1 -3
  20. package/dist/execution/run-canceler.d.ts +2 -0
  21. package/dist/execution/run-canceler.js +9 -0
  22. package/dist/execution/run-synthesis.d.ts +36 -8
  23. package/dist/execution/run-synthesis.js +114 -32
  24. package/dist/index.d.ts +5 -6
  25. package/dist/index.js +5 -7
  26. package/dist/input-scan/enrich.d.ts +5 -10
  27. package/dist/input-scan/enrich.js +12 -8
  28. package/dist/lib/hooks.d.ts +1 -1
  29. package/dist/lib/markdown-pdf.d.ts +27 -0
  30. package/dist/lib/markdown-pdf.js +236 -0
  31. package/dist/lib/metrics.d.ts +31 -0
  32. package/dist/lib/metrics.js +64 -0
  33. package/dist/lib/pdf-text.d.ts +23 -0
  34. package/dist/lib/pdf-text.js +34 -0
  35. package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
  36. package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
  37. package/dist/loop/chat-stream.js +2 -1
  38. package/dist/loop/run-loop.d.ts +5 -0
  39. package/dist/loop/run-loop.js +30 -2
  40. package/dist/loop/tool-name-repair.d.ts +11 -0
  41. package/dist/loop/tool-name-repair.js +23 -0
  42. package/dist/loop/types.d.ts +19 -0
  43. package/dist/memory/card-builders.d.ts +1 -1
  44. package/dist/prompts/SOUL.d.ts +6 -6
  45. package/dist/prompts/SOUL.js +22 -121
  46. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  47. package/dist/prompts/analogical-reasoner.js +35 -101
  48. package/dist/prompts/briefing.d.ts +20 -11
  49. package/dist/prompts/briefing.js +36 -19
  50. package/dist/prompts/conversation.d.ts +1 -1
  51. package/dist/prompts/conversation.js +103 -383
  52. package/dist/prompts/planner.d.ts +1 -5
  53. package/dist/prompts/planner.js +45 -128
  54. package/dist/prompts/report-session.d.ts +1 -1
  55. package/dist/prompts/report-session.js +29 -58
  56. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
  58. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  59. package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
  60. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  61. package/dist/prompts/sandbox/chromatin-agent.js +4 -5
  62. package/dist/prompts/sandbox/data-profiler.js +11 -34
  63. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  64. package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
  65. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  66. package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
  67. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/enrichment-agent.js +3 -1
  69. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
  71. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  72. package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
  73. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  74. package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
  75. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  76. package/dist/prompts/sandbox/microbiome-agent.js +3 -1
  77. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  78. package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
  79. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  80. package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
  81. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  82. package/dist/prompts/sandbox/network-agent.js +3 -1
  83. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  84. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
  85. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  86. package/dist/prompts/sandbox/proteomics-agent.js +3 -1
  87. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  88. package/dist/prompts/sandbox/scientific-executor.js +0 -12
  89. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/single-cell-agent.js +3 -1
  91. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
  93. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
  95. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  96. package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
  97. package/dist/prompts/sandbox-standards.d.ts +15 -18
  98. package/dist/prompts/sandbox-standards.js +96 -305
  99. package/dist/prompts/synthesis-agent.d.ts +1 -1
  100. package/dist/prompts/synthesis-agent.js +24 -90
  101. package/dist/provenance/collector.d.ts +2 -5
  102. package/dist/provenance/collector.js +2 -5
  103. package/dist/provenance/seam.d.ts +36 -1
  104. package/dist/provenance/workspace-reads.d.ts +36 -0
  105. package/dist/provenance/workspace-reads.js +33 -0
  106. package/dist/report-model/production-resolver.d.ts +22 -52
  107. package/dist/report-model/production-resolver.js +43 -112
  108. package/dist/report-render/chart-export.d.ts +9 -0
  109. package/dist/report-render/chart-export.js +1 -1
  110. package/dist/report-render/views/chart-view.d.ts +1 -0
  111. package/dist/report-render/views/chart-view.js +1 -0
  112. package/dist/report-render/views/lineage.d.ts +1 -0
  113. package/dist/report-render/views/page-view.d.ts +1 -0
  114. package/dist/report-render/views/page-view.js +1 -0
  115. package/dist/report-render/views/prose.d.ts +1 -0
  116. package/dist/report-render/views/prose.js +1 -0
  117. package/dist/report-render/views/references-view.d.ts +1 -0
  118. package/dist/report-render/views/values.d.ts +1 -0
  119. package/dist/report-render/views/values.js +1 -0
  120. package/dist/runtime/assemble.d.ts +32 -12
  121. package/dist/runtime/assemble.js +49 -38
  122. package/dist/sandbox/client.d.ts +5 -3
  123. package/dist/sandbox/create-sandbox.d.ts +7 -12
  124. package/dist/sandbox/create-sandbox.js +15 -37
  125. package/dist/sandbox/docker-client.d.ts +1 -3
  126. package/dist/sandbox/docker-client.js +3 -5
  127. package/dist/sandbox/exec-outcome.d.ts +11 -11
  128. package/dist/sandbox/exec-outcome.js +22 -22
  129. package/dist/sandbox/exec.d.ts +5 -0
  130. package/dist/sandbox/exec.js +9 -1
  131. package/dist/sandbox/identity.d.ts +4 -16
  132. package/dist/sandbox/identity.js +5 -21
  133. package/dist/sandbox/k8s-client.d.ts +0 -1
  134. package/dist/sandbox/k8s-client.js +8 -13
  135. package/dist/sandbox/lease-names.d.ts +6 -0
  136. package/dist/sandbox/lease-names.js +12 -0
  137. package/dist/sandbox/leases.d.ts +105 -0
  138. package/dist/sandbox/leases.js +519 -0
  139. package/dist/sandbox/liveness.d.ts +5 -2
  140. package/dist/sandbox/mount-plan.d.ts +10 -1
  141. package/dist/sandbox/mount-plan.js +7 -1
  142. package/dist/sandbox/reconciler.d.ts +58 -0
  143. package/dist/sandbox/reconciler.js +153 -0
  144. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  145. package/dist/sandbox/types.d.ts +25 -18
  146. package/dist/sandbox/types.js +10 -4
  147. package/dist/schemas/plan-schemas.d.ts +0 -21
  148. package/dist/schemas/plan-schemas.js +2 -3
  149. package/dist/schemas/run-synthesis.d.ts +2 -2
  150. package/dist/schemas/run-synthesis.js +3 -3
  151. package/dist/schemas/validate-plan.d.ts +1 -14
  152. package/dist/schemas/validate-plan.js +5 -24
  153. package/dist/schemas/workflow-state.d.ts +3 -32
  154. package/dist/schemas/workflow-state.js +4 -13
  155. package/dist/state/index.d.ts +2 -3
  156. package/dist/state/index.js +1 -2
  157. package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
  158. package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
  159. package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
  160. package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
  161. package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
  162. package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
  163. package/dist/state/migrations/index.js +6 -0
  164. package/dist/state/purge-analysis.d.ts +4 -2
  165. package/dist/state/purge-analysis.js +22 -11
  166. package/dist/state/sandbox-leases.d.ts +81 -0
  167. package/dist/state/sandbox-leases.js +247 -0
  168. package/dist/state/schema.d.ts +1 -13
  169. package/dist/state/schema.js +1 -6
  170. package/dist/state/step-executions.d.ts +13 -2
  171. package/dist/state/step-executions.js +23 -4
  172. package/dist/tasks/data-profile.d.ts +40 -2
  173. package/dist/tasks/data-profile.js +89 -87
  174. package/dist/tools/ad-hoc-router.d.ts +1 -6
  175. package/dist/tools/ad-hoc-router.js +5 -48
  176. package/dist/tools/bio/comptox.js +1 -1
  177. package/dist/tools/bio/drug-gene-interactions.js +1 -1
  178. package/dist/tools/bio/gene-disease-evidence.js +1 -1
  179. package/dist/tools/bio/keys.d.ts +1 -1
  180. package/dist/tools/bio/pubmed.js +31 -0
  181. package/dist/tools/bio/search-gene.js +17 -0
  182. package/dist/tools/bio/search-geo-datasets.js +13 -0
  183. package/dist/tools/bio/search-regulatory-approvals.js +1 -1
  184. package/dist/tools/define-tool.d.ts +25 -2
  185. package/dist/tools/define-tool.js +11 -1
  186. package/dist/tools/display/show-user.js +1 -1
  187. package/dist/tools/execute-analysis.d.ts +1 -3
  188. package/dist/tools/execute-analysis.js +5 -33
  189. package/dist/tools/report-authoring/authoring-tools.d.ts +2 -2
  190. package/dist/tools/report-authoring/authoring-tools.js +31 -6
  191. package/dist/tools/report-session/index.d.ts +0 -1
  192. package/dist/tools/report-session/index.js +0 -1
  193. package/dist/tools/report-session/script-tools.d.ts +33 -0
  194. package/dist/tools/report-session/script-tools.js +186 -0
  195. package/dist/tools/research/generate-analogy-report.d.ts +16 -9
  196. package/dist/tools/research/generate-analogy-report.js +111 -49
  197. package/dist/tools/research/generate-plan.d.ts +102 -7
  198. package/dist/tools/research/generate-plan.js +74 -43
  199. package/dist/tools/research/index.d.ts +4 -3
  200. package/dist/tools/research/index.js +4 -3
  201. package/dist/tools/research/inspect-run.d.ts +0 -1
  202. package/dist/tools/research/inspect-run.js +5 -10
  203. package/dist/tools/research/search-arxiv.js +15 -0
  204. package/dist/tools/research/search-github-repos.js +13 -0
  205. package/dist/tools/research/search-semantic-scholar.js +16 -0
  206. package/dist/tools/research/synthesis-lookups.d.ts +21 -0
  207. package/dist/tools/research/synthesis-lookups.js +29 -0
  208. package/dist/tools/research/web-fetch.d.ts +1 -1
  209. package/dist/tools/research/web-fetch.js +14 -12
  210. package/dist/tools/sandbox/lease-tools.d.ts +32 -0
  211. package/dist/tools/sandbox/lease-tools.js +60 -0
  212. package/dist/tools/script/archive.d.ts +34 -0
  213. package/dist/tools/script/archive.js +182 -0
  214. package/dist/tools/script/codemode-worker.d.ts +5 -0
  215. package/dist/tools/script/codemode-worker.js +5 -0
  216. package/dist/tools/script/file-tools.d.ts +106 -0
  217. package/dist/tools/script/file-tools.js +491 -0
  218. package/dist/tools/script/metrics.d.ts +30 -0
  219. package/dist/tools/script/metrics.js +53 -0
  220. package/dist/tools/script/quickjs.wasm +0 -0
  221. package/dist/tools/script/run-script.d.ts +76 -0
  222. package/dist/tools/script/run-script.js +413 -0
  223. package/dist/tools/script/scratch.d.ts +84 -0
  224. package/dist/tools/script/scratch.js +145 -0
  225. package/dist/tools/start-report-session.js +1 -1
  226. package/dist/tools/workspace/edit-file.js +2 -1
  227. package/dist/tools/workspace/execute-command.d.ts +9 -13
  228. package/dist/tools/workspace/execute-command.js +23 -19
  229. package/dist/tools/workspace/file-stat.js +1 -1
  230. package/dist/tools/workspace/grep.d.ts +5 -2
  231. package/dist/tools/workspace/grep.js +19 -10
  232. package/dist/tools/workspace/mutator.d.ts +51 -4
  233. package/dist/tools/workspace/mutator.js +54 -13
  234. package/dist/tools/workspace/read-file.d.ts +5 -2
  235. package/dist/tools/workspace/read-file.js +15 -11
  236. package/dist/tools/workspace/read-surface.d.ts +17 -0
  237. package/dist/tools/workspace/read-surface.js +17 -0
  238. package/dist/tools/workspace/result-bounds.d.ts +1 -0
  239. package/dist/tools/workspace/run-exec.d.ts +12 -13
  240. package/dist/tools/workspace/run-exec.js +13 -13
  241. package/dist/tools/workspace/scan-inputs.d.ts +2 -5
  242. package/dist/tools/workspace/scan-inputs.js +2 -3
  243. package/dist/tools/workspace/show-file.js +1 -1
  244. package/dist/tools/workspace/show-plan.js +4 -3
  245. package/dist/tools/workspace/write-file.js +2 -1
  246. package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
  247. package/dist/workflows/execute-analysis-scheduler.js +12 -32
  248. package/dist/workflows/execute-analysis.d.ts +17 -35
  249. package/dist/workflows/execute-analysis.js +13 -107
  250. package/dist/workflows/pause.js +29 -22
  251. package/dist/workflows/sandbox-step.d.ts +16 -31
  252. package/dist/workflows/sandbox-step.js +36 -71
  253. package/dist/workflows/test/sql-writes.d.ts +0 -1
  254. package/dist/workflows/test/sql-writes.js +0 -2
  255. package/dist/workspace/filesystem.d.ts +2 -2
  256. package/dist/workspace/filesystem.js +10 -5
  257. package/dist/workspace/paths.d.ts +19 -1
  258. package/dist/workspace/paths.js +22 -1
  259. package/dist/workspace/session-files.d.ts +35 -0
  260. package/dist/workspace/session-files.js +16 -0
  261. package/package.json +7 -3
  262. package/dist/prompts/literature-reviewer.d.ts +0 -1
  263. package/dist/prompts/literature-reviewer.js +0 -114
  264. package/dist/sandbox/reaper.d.ts +0 -81
  265. package/dist/sandbox/reaper.js +0 -161
  266. package/dist/sandbox/step-sandbox.d.ts +0 -47
  267. package/dist/sandbox/step-sandbox.js +0 -53
  268. package/dist/state/active-sandboxes.d.ts +0 -33
  269. package/dist/state/active-sandboxes.js +0 -65
  270. package/dist/tasks/extract-values-script.d.ts +0 -64
  271. package/dist/tasks/extract-values-script.js +0 -180
  272. package/dist/tasks/extract-values.d.ts +0 -77
  273. package/dist/tasks/extract-values.js +0 -146
  274. package/dist/tools/report-session/derive-table.d.ts +0 -164
  275. package/dist/tools/report-session/derive-table.js +0 -419
  276. package/dist/tools/research/literature-reviewer.d.ts +0 -42
  277. package/dist/tools/research/literature-reviewer.js +0 -107
  278. package/dist/workflows/metrics.d.ts +0 -23
  279. package/dist/workflows/metrics.js +0 -38
@@ -0,0 +1,491 @@
1
+ /**
2
+ * The file tools of a script: `read_table`, `read_pdf`, `write_derived`, `render_chart`, and `export_pdf`.
3
+ *
4
+ * The VM of `run_script` has no file system. These tools are its only reach into the workspace, thus each one is
5
+ * the boundary. `read_table` reads through the read seam of the workspace, which confines a path to the analysis
6
+ * of the call and refuses a symbolic link. `write_derived` and `render_chart` write only into the `derived/`
7
+ * directory of the scratch of the session.
8
+ *
9
+ * A table read holds at most 16 MiB, and the parse runs on the host. A larger table, or a format with no parser
10
+ * here, is work for an analysis run, and the refusal says so. `list_archive` lists the files of a compressed file, and
11
+ * a read names one of them as its `entry`. A gzip-compressed table reads with no entry.
12
+ *
13
+ * Each tool gives the files that it read or wrote as its call record. `run_script` collects the records of the
14
+ * calls of one script, thus the provenance of the script chains each derived file to the tables that it read.
15
+ */
16
+ import { ok, err } from "neverthrow";
17
+ import { z } from "zod";
18
+ import { computeSha256 } from "../../lib/fs-helpers.js";
19
+ import { markdownImageSources, markdownToPdf } from "../../lib/markdown-pdf.js";
20
+ import { readPdfText } from "../../lib/pdf-text.js";
21
+ import { detectFormat, parseTable } from "../../report-model/production-resolver.js";
22
+ import { CHART_EXPORT_SIZES, SCATTER_CROWD_ROWS } from "../../report-render/design.js";
23
+ import { loadChartRuntime, plottedPoints, renderChartSvg } from "../../report-render/chart-export.js";
24
+ import { parseUnknownRecord, UnknownArraySchema } from "../../lib/unknown-containers.js";
25
+ import { resolveWorkspacePath } from "../../workspace/paths.js";
26
+ import { defineTool, withToolCallRecord } from "../define-tool.js";
27
+ import { normalizeEchartSpec } from "../display/normalize-echart-spec.js";
28
+ import { ARCHIVE_SCAN_CAP_BYTES, archiveKindOf, gzipEntryName, listArchive, readArchiveEntry } from "./archive.js";
29
+ import { recordReadBytes, recordReadRefusal } from "./metrics.js";
30
+ /** The cap of one table read. It matches the cap of the report path, which reads with the same parsers. */
31
+ export const TABLE_READ_CAP_BYTES = 16 * 1024 * 1024;
32
+ export const ScriptFileEffectSchema = z.object({
33
+ reads: z.array(z.object({ path: z.string(), hash: z.string() })).optional(),
34
+ writes: z.array(z.object({ path: z.string(), hash: z.string(), size: z.number() })).optional(),
35
+ });
36
+ function refused(error) {
37
+ return err({ error, retryable: false });
38
+ }
39
+ /**
40
+ * A cell of a text table as a number, when the text is a plain decimal number. A text with a leading zero, such
41
+ * as an identifier `007`, stays text, because a number loses its zeros.
42
+ */
43
+ function numericCell(text) {
44
+ if (text.length === 0 || text !== text.trim())
45
+ return text;
46
+ const value = Number(text);
47
+ if (!Number.isFinite(value))
48
+ return text;
49
+ const digits = text.startsWith("-") ? text.slice(1) : text;
50
+ if (digits.length > 1 && digits.startsWith("0") && digits[1] !== ".")
51
+ return text;
52
+ if (digits.includes("x") || digits.includes("X") || digits.includes("o") || digits.includes("b"))
53
+ return text;
54
+ return value;
55
+ }
56
+ /** The cap of the compressed file that a read of an entry reads. */
57
+ const ARCHIVE_READ_CAP_BYTES = 64 * 1024 * 1024;
58
+ const entryInput = z
59
+ .string()
60
+ .min(1)
61
+ .optional()
62
+ .describe("The file inside a compressed file at `path` (zip, tar, tar.gz), as `list_archive` names it. Absent: `path` itself.");
63
+ /**
64
+ * Read one file of the workspace through the read seam, or one entry of a compressed file. A gzip-compressed file
65
+ * with no entry reads as its one entry. A refusal is the line that the script reads.
66
+ */
67
+ async function readScriptFile(args) {
68
+ const { path, maxBytes } = args;
69
+ const kind = archiveKindOf(path);
70
+ const entry = args.entry ?? (kind === "gzip" ? gzipEntryName(path) : undefined);
71
+ if (entry !== undefined && kind === undefined)
72
+ return err(`${path} is not a compressed file that a script opens (zip, tar, tar.gz, gz)`);
73
+ const cap = kind === undefined ? maxBytes : ARCHIVE_READ_CAP_BYTES;
74
+ const read = await args.workspaceFs.readFile({ session: args.session, path, maxBytes: cap });
75
+ if (read.isErr())
76
+ return err(`${path} could not be read`);
77
+ const file = read.value;
78
+ if (file.kind === "not_found")
79
+ return err(`${path} does not exist; find the path with workspace_search or list_files`);
80
+ if (file.kind === "out_of_scope")
81
+ return err(`${path} is outside this analysis`);
82
+ if (file.kind === "truncated") {
83
+ recordReadRefusal("too_large");
84
+ return err(`${path} is ${String(file.totalSize)} bytes, over the ${String(cap / (1024 * 1024))} MiB cap of a script read; ${args.tooLarge}`);
85
+ }
86
+ if (kind === undefined || entry === undefined)
87
+ return ok({ content: file.content, source: file.content });
88
+ const opened = readArchiveEntry(kind, path, file.content, entry, maxBytes);
89
+ if (opened.isOk())
90
+ return ok({ content: opened.value, source: file.content });
91
+ switch (opened.error.kind) {
92
+ case "not_found":
93
+ return err(`${path} holds no file ${entry}; list_archive gives its files`);
94
+ case "too_large":
95
+ recordReadRefusal("too_large");
96
+ return err(`${entry} in ${path} is over the ${String(maxBytes / (1024 * 1024))} MiB cap of a script read, or its archive decompresses past ${String(ARCHIVE_SCAN_CAP_BYTES / (1024 * 1024))} MiB; ${args.tooLarge}`);
97
+ case "unparsable":
98
+ recordReadRefusal("unparsable");
99
+ return err(`${path} did not open: ${opened.error.detail}`);
100
+ default: {
101
+ const unhandled = opened.error;
102
+ throw new Error(`unhandled entry refusal: ${JSON.stringify(unhandled)}`);
103
+ }
104
+ }
105
+ }
106
+ /**
107
+ * Read one table through the read seam, parse it on the host, and keep the named columns. A refusal is the line
108
+ * that the script reads. `tooLarge` names where a table over the cap goes instead.
109
+ */
110
+ export async function readTableRows(args) {
111
+ const { path, columns } = args;
112
+ const kind = archiveKindOf(path);
113
+ const name = args.entry ?? (kind === "gzip" ? gzipEntryName(path) : path);
114
+ const format = detectFormat(name);
115
+ if (format === undefined) {
116
+ recordReadRefusal("unsupported_format");
117
+ if (kind !== undefined && kind !== "gzip" && args.entry === undefined)
118
+ return err(`${path} is a compressed file; list_archive gives its files, and an entry names one`);
119
+ return err(`${name} has no table format that a script reads (csv, tsv, json, parquet); an analysis run reads it`);
120
+ }
121
+ const read = await readScriptFile({ ...args, entry: args.entry, maxBytes: TABLE_READ_CAP_BYTES });
122
+ if (read.isErr())
123
+ return err(read.error);
124
+ const { content, source } = read.value;
125
+ const parsed = await parseTable(format, content);
126
+ if (parsed === undefined) {
127
+ recordReadRefusal("unparsable");
128
+ return err(`${name} did not parse as ${format}: a ragged row, a repeated header, or a nested JSON value`);
129
+ }
130
+ recordReadBytes(content.length);
131
+ const header = [...new Set(parsed.flatMap((row) => Object.keys(row)))];
132
+ const missing = (columns ?? []).filter((column) => !header.includes(column));
133
+ if (missing.length > 0)
134
+ return err(`${path} has no column ${missing.join(", ")}; its columns are ${header.join(", ")}`);
135
+ const kept = columns === undefined ? header : [...columns];
136
+ const rows = parsed.map((row) => {
137
+ const out = {};
138
+ for (const column of kept) {
139
+ const cell = row[column];
140
+ if (cell !== undefined)
141
+ out[column] = typeof cell === "string" ? numericCell(cell) : cell;
142
+ }
143
+ return out;
144
+ });
145
+ return ok({ source, columns: kept, rows });
146
+ }
147
+ const readTableInput = z.object({
148
+ path: z.string().min(1).describe("Analysis-rooted path of the table, for example 'runs/<runId>/<stepId>/output/de_results.csv'."),
149
+ entry: entryInput,
150
+ columns: z.array(z.string()).min(1).optional().describe("Keep only these columns. Absent: each column."),
151
+ });
152
+ export const ReadTableOutputSchema = z.object({
153
+ path: z.string(),
154
+ columns: z.array(z.string()).readonly(),
155
+ rowCount: z.number(),
156
+ rows: z.array(z.record(z.string(), z.union([z.string(), z.number()]))).readonly(),
157
+ });
158
+ const WrittenOutputSchema = z.object({ path: z.string().describe("The analysis-rooted path of the written file."), bytes: z.number() });
159
+ function createReadTableTool(deps) {
160
+ return defineTool({
161
+ id: "read_table",
162
+ description: "Read one table of the workspace as rows: CSV, TSV, JSON (an array of flat objects), or parquet, up to 16 MiB. " +
163
+ "Each row is an object keyed by column, and a cell that holds a plain number reads as a number. " +
164
+ "A gzip-compressed table reads as it is, and `entry` reads one table inside a zip or a tar. " +
165
+ "A larger table, or another format such as h5ad, is work for an analysis run.",
166
+ inputSchema: readTableInput,
167
+ outputSchema: ReadTableOutputSchema,
168
+ describeCall: ({ path, entry }) => (entry === undefined ? path : `${path} : ${entry}`),
169
+ describeResult: (_input, result) => `${String(result.rowCount)} rows`,
170
+ execute: async ({ path, entry, columns }, ctx) => {
171
+ const table = await readTableRows({
172
+ workspaceFs: deps.workspaceFs,
173
+ session: ctx.session,
174
+ path,
175
+ entry,
176
+ columns,
177
+ tooLarge: "an analysis run reads it",
178
+ });
179
+ if (table.isErr())
180
+ return refused(table.error);
181
+ const { source, rows } = table.value;
182
+ const { analysisId } = ctx.session.scope;
183
+ const resolved = resolveWorkspacePath({ workspaceRoot: await deps.resolveWorkspaceRoot(analysisId), analysisId, path });
184
+ const relative = resolved.kind === "ok" ? resolved.relative.split("\\").join("/") : path;
185
+ const effect = { reads: [{ path: relative, hash: computeSha256(source) }] };
186
+ return ok(withToolCallRecord({ path: relative, columns: table.value.columns, rowCount: rows.length, rows }, effect));
187
+ },
188
+ });
189
+ }
190
+ const listArchiveInput = z.object({
191
+ path: z.string().min(1).describe("Analysis-rooted path of a compressed file: .zip, .tar, .tar.gz, .tgz, or .gz."),
192
+ });
193
+ const ListArchiveOutputSchema = z.object({
194
+ path: z.string(),
195
+ entries: z.array(z.object({ name: z.string(), bytes: z.number() })).readonly(),
196
+ });
197
+ function createListArchiveTool(deps) {
198
+ return defineTool({
199
+ id: "list_archive",
200
+ description: "List the files inside a compressed file of the workspace (zip, tar, tar.gz, gz), each with its decompressed size. " +
201
+ "`read_table` and `read_pdf` then read one of them as their `entry`.",
202
+ inputSchema: listArchiveInput,
203
+ outputSchema: ListArchiveOutputSchema,
204
+ describeCall: ({ path }) => path,
205
+ describeResult: (_input, result) => `${String(result.entries.length)} files`,
206
+ execute: async ({ path }, ctx) => {
207
+ const kind = archiveKindOf(path);
208
+ if (kind === undefined) {
209
+ recordReadRefusal("unsupported_format");
210
+ return refused(`${path} is not a compressed file that a script opens (zip, tar, tar.gz, gz)`);
211
+ }
212
+ const read = await readScriptFile({
213
+ workspaceFs: deps.workspaceFs,
214
+ session: ctx.session,
215
+ path,
216
+ entry: undefined,
217
+ maxBytes: ARCHIVE_READ_CAP_BYTES,
218
+ tooLarge: "an analysis run opens it",
219
+ });
220
+ if (read.isErr())
221
+ return refused(read.error);
222
+ const { source } = read.value;
223
+ const listed = listArchive(kind, path, source);
224
+ if (listed.isErr()) {
225
+ if (listed.error.kind === "too_large") {
226
+ recordReadRefusal("too_large");
227
+ return refused(`${path} decompresses past ${String(ARCHIVE_SCAN_CAP_BYTES / (1024 * 1024))} MiB; an analysis run opens it`);
228
+ }
229
+ recordReadRefusal("unparsable");
230
+ return refused(`${path} did not open${listed.error.kind === "unparsable" ? `: ${listed.error.detail}` : ""}`);
231
+ }
232
+ const { analysisId } = ctx.session.scope;
233
+ const resolved = resolveWorkspacePath({ workspaceRoot: await deps.resolveWorkspaceRoot(analysisId), analysisId, path });
234
+ const relative = resolved.kind === "ok" ? resolved.relative.split("\\").join("/") : path;
235
+ const effect = { reads: [{ path: relative, hash: computeSha256(source) }] };
236
+ return ok(withToolCallRecord({ path: relative, entries: listed.value }, effect));
237
+ },
238
+ });
239
+ }
240
+ /** The cap of one PDF read, and the pages that one call reads. */
241
+ const PDF_READ_CAP_BYTES = 64 * 1024 * 1024;
242
+ const PDF_PAGES_PER_READ = 50;
243
+ const readPdfInput = z.object({
244
+ path: z.string().min(1).describe("Analysis-rooted path of the PDF, for example 'data/inputs/<fileId>/paper.pdf'."),
245
+ entry: entryInput,
246
+ firstPage: z.number().int().min(1).optional().describe("The first page to read, 1-based. Absent: 1."),
247
+ lastPage: z
248
+ .number()
249
+ .int()
250
+ .min(1)
251
+ .optional()
252
+ .describe(`The last page to read. One call reads at most ${String(PDF_PAGES_PER_READ)} pages. Absent: as many as that cap allows.`),
253
+ });
254
+ const ReadPdfOutputSchema = z.object({
255
+ path: z.string(),
256
+ totalPages: z.number(),
257
+ pages: z.array(z.object({ page: z.number(), text: z.string() })).readonly(),
258
+ });
259
+ /** `lineage` records each read in the lineage of a step. */
260
+ export function createReadPdfTool(deps) {
261
+ const { lineage } = deps;
262
+ return defineTool({
263
+ id: "read_pdf",
264
+ description: `Read the text of a PDF of the workspace page by page, up to 64 MiB and ${String(PDF_PAGES_PER_READ)} pages for each call. ` +
265
+ "`totalPages` gives the page count, thus a script reads a long PDF in more calls and keeps the pages that it needs. " +
266
+ "A scanned page with no text layer reads as an empty text.",
267
+ inputSchema: readPdfInput,
268
+ outputSchema: ReadPdfOutputSchema,
269
+ describeCall: ({ path, entry, firstPage, lastPage }) => {
270
+ const file = entry === undefined ? path : `${path} : ${entry}`;
271
+ return firstPage === undefined && lastPage === undefined ? file : `${file}, pages ${String(firstPage ?? 1)}-${String(lastPage ?? "")}`;
272
+ },
273
+ describeResult: (_input, result) => `${String(result.pages.length)} of ${String(result.totalPages)} pages`,
274
+ // `run_script` folds the record of a nested call only into a tool that folds records.
275
+ foldCallRecord: lineage === undefined ? undefined : (effect) => lineage.track(effect.reads ?? []),
276
+ execute: async ({ path, entry, firstPage, lastPage }, ctx) => {
277
+ const kind = archiveKindOf(path);
278
+ const name = entry ?? (kind === "gzip" ? gzipEntryName(path) : path);
279
+ if (!name.toLowerCase().endsWith(".pdf")) {
280
+ recordReadRefusal("unsupported_format");
281
+ if (kind !== undefined && kind !== "gzip" && entry === undefined)
282
+ return refused(`${path} is a compressed file; list_archive gives its files, and an entry names one`);
283
+ return refused(`${name} is not a PDF; read_table reads a table, and read_file reads text`);
284
+ }
285
+ const read = await readScriptFile({
286
+ workspaceFs: deps.workspaceFs,
287
+ session: ctx.session,
288
+ path,
289
+ entry,
290
+ maxBytes: PDF_READ_CAP_BYTES,
291
+ tooLarge: "an analysis run reads it",
292
+ });
293
+ if (read.isErr())
294
+ return refused(read.error);
295
+ const { content, source } = read.value;
296
+ const first = firstPage ?? 1;
297
+ const last = Math.min(lastPage ?? first + PDF_PAGES_PER_READ - 1, first + PDF_PAGES_PER_READ - 1);
298
+ let text;
299
+ try {
300
+ text = await readPdfText(content, { first, last });
301
+ }
302
+ catch {
303
+ recordReadRefusal("unparsable");
304
+ return refused(`${path} did not open as a PDF: a damaged file, or a file with a password`);
305
+ }
306
+ recordReadBytes(content.length);
307
+ const { analysisId } = ctx.session.scope;
308
+ const resolved = resolveWorkspacePath({ workspaceRoot: await deps.resolveWorkspaceRoot(analysisId), analysisId, path });
309
+ const relative = resolved.kind === "ok" ? resolved.relative.split("\\").join("/") : path;
310
+ const effect = { reads: [{ path: relative, hash: computeSha256(source) }] };
311
+ return ok(withToolCallRecord({ path: relative, totalPages: text.totalPages, pages: text.pages }, effect));
312
+ },
313
+ });
314
+ }
315
+ /** One cell as delimited text: quoted when it holds the delimiter, a quote, or a line break. */
316
+ function delimitedCell(value, delimiter) {
317
+ if (value === null || value === undefined)
318
+ return "";
319
+ const text = String(value);
320
+ if (!text.includes(delimiter) && !text.includes('"') && !text.includes("\n") && !text.includes("\r"))
321
+ return text;
322
+ return `"${text.replaceAll('"', '""')}"`;
323
+ }
324
+ /** The rows as CSV or TSV: the header is each key in the order of its first row, then one line for each row. */
325
+ function delimitedText(rows, delimiter) {
326
+ const header = [...new Set(rows.flatMap((row) => Object.keys(row)))];
327
+ const lines = [header.map((name) => delimitedCell(name, delimiter)).join(delimiter)];
328
+ for (const row of rows)
329
+ lines.push(header.map((name) => delimitedCell(row[name], delimiter)).join(delimiter));
330
+ return `${lines.join("\n")}\n`;
331
+ }
332
+ /** The text of `rows` in the format that the extension of `name` names, or `undefined` for another extension. */
333
+ export function rowsText(name, rows) {
334
+ const lower = name.toLowerCase();
335
+ if (lower.endsWith(".csv"))
336
+ return delimitedText(rows, ",");
337
+ if (lower.endsWith(".tsv"))
338
+ return delimitedText(rows, "\t");
339
+ if (lower.endsWith(".json"))
340
+ return `${JSON.stringify(rows, null, 2)}\n`;
341
+ return undefined;
342
+ }
343
+ const writeDerivedInput = z.object({
344
+ name: z.string().min(1).describe("One file name, for example 'de_top50.csv'. With `rows`, the extension picks the format: .csv, .tsv, or .json."),
345
+ content: z.string().optional().describe("The text of the file. Give `content` or `rows`."),
346
+ rows: z
347
+ .array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])))
348
+ .optional()
349
+ .describe("Table rows, each an object keyed by column. Give `content` or `rows`."),
350
+ });
351
+ function written(file) {
352
+ return withToolCallRecord({ path: file.path, bytes: file.size }, { writes: [file] });
353
+ }
354
+ function createWriteDerivedTool(deps) {
355
+ return defineTool({
356
+ id: "write_derived",
357
+ description: "Save a file that the script derived, in the derived directory of this conversation, and show it to the user. " +
358
+ "The file lands in that directory only, never at a path that the user names: for such a path, return the content and write it with `write_file`. " +
359
+ "A name holds one file: a second write of the name with other content is refused.",
360
+ inputSchema: writeDerivedInput,
361
+ outputSchema: WrittenOutputSchema,
362
+ describeCall: ({ name }) => name,
363
+ describeResult: (_input, result) => result.path,
364
+ execute: async ({ name, content, rows }, ctx) => {
365
+ if ((content === undefined) === (rows === undefined))
366
+ return refused("give exactly one of `content` and `rows`");
367
+ const text = rows === undefined ? content : rowsText(name, rows);
368
+ if (text === undefined)
369
+ return refused(`${name} names no table format: with rows, end the name in .csv, .tsv, or .json`);
370
+ const write = await deps.scratch.writeDerived({ session: ctx.session, invocationId: ctx.invocationId, name, content: text });
371
+ if (write.status === "refused")
372
+ return refused(write.reason);
373
+ return ok(written(write.file));
374
+ },
375
+ });
376
+ }
377
+ const renderChartInput = z.object({
378
+ name: z.string().min(1).describe("File name of the SVG, for example 'volcano.svg'."),
379
+ spec: z
380
+ .record(z.string(), z.unknown())
381
+ .describe("ECharts option, the same as `show_user` takes, with the rows inline in `dataset.source` or `series[].data`."),
382
+ width: z.enum(["single", "double"]).optional().describe("Journal column: single (89 mm) or double (183 mm). Default double."),
383
+ });
384
+ /** The rows of `dataset.source` of an option, or of each dataset of a list. A chart from a dataset plots each one. */
385
+ function datasetRows(spec) {
386
+ const datasets = UnknownArraySchema.safeParse(spec.dataset);
387
+ const list = datasets.success ? datasets.data : [spec.dataset];
388
+ let count = 0;
389
+ for (const dataset of list) {
390
+ const source = UnknownArraySchema.safeParse(parseUnknownRecord(dataset)?.source);
391
+ if (source.success)
392
+ count += source.data.length;
393
+ }
394
+ return count;
395
+ }
396
+ function createRenderChartTool(deps) {
397
+ return defineTool({
398
+ id: "render_chart",
399
+ description: "Render an ECharts option to an SVG file at a journal column width, in the derived directory of this conversation, for a document. " +
400
+ "To show a chart in the chat, use `show_user`.",
401
+ inputSchema: renderChartInput,
402
+ outputSchema: WrittenOutputSchema,
403
+ describeCall: ({ name }) => name,
404
+ describeResult: (_input, result) => result.path,
405
+ execute: async ({ name, spec, width }, ctx) => {
406
+ const fileName = name.toLowerCase().endsWith(".svg") ? name : `${name}.svg`;
407
+ const option = normalizeEchartSpec(spec);
408
+ const points = plottedPoints(option) + datasetRows(option);
409
+ if (points > SCATTER_CROWD_ROWS) {
410
+ return refused(`the chart draws ${String(points)} points, and an SVG file holds at most ${String(SCATTER_CROWD_ROWS)}; plot fewer points`);
411
+ }
412
+ const svg = renderChartSvg(await loadChartRuntime(), option, CHART_EXPORT_SIZES[width ?? "double"]);
413
+ if (svg.isErr())
414
+ return refused(`the chart runtime refused the option: ${svg.error}`);
415
+ const write = await deps.scratch.writeDerived({ session: ctx.session, invocationId: ctx.invocationId, name: fileName, content: svg.value });
416
+ if (write.status === "refused")
417
+ return refused(write.reason);
418
+ return ok(written(write.file));
419
+ },
420
+ });
421
+ }
422
+ /** The file tools of a script, over one scratch directory. */
423
+ /** The cap of one image that a PDF embeds, and the cap of the markdown of one PDF. */
424
+ const PDF_IMAGE_CAP_BYTES = 16 * 1024 * 1024;
425
+ const PDF_MARKDOWN_CAP_CHARS = 1_000_000;
426
+ const exportPdfInput = z.object({
427
+ name: z.string().min(1).describe("File name of the PDF, for example 'summary.pdf'."),
428
+ markdown: z
429
+ .string()
430
+ .max(PDF_MARKDOWN_CAP_CHARS)
431
+ .describe("The document as markdown: headings, paragraphs, lists, tables, code, quotes, and images. An image names an SVG, PNG, or JPEG file of the workspace " +
432
+ "by its analysis-rooted path, for example the path that `render_chart` gives; a chart SVG lands as vector graphics."),
433
+ title: z.string().optional().describe("The title on the first page and in the metadata of the PDF."),
434
+ });
435
+ const ExportPdfOutputSchema = WrittenOutputSchema.extend({
436
+ missingImages: z.array(z.string()).describe("Each image that the PDF shows as its alt text, because no such file of the workspace was readable."),
437
+ });
438
+ const IMAGE_KINDS = { ".svg": "svg", ".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg" };
439
+ function imageKindOf(path) {
440
+ const dot = path.lastIndexOf(".");
441
+ return dot === -1 ? undefined : IMAGE_KINDS[path.slice(dot).toLowerCase()];
442
+ }
443
+ function createExportPdfTool(deps) {
444
+ return defineTool({
445
+ id: "export_pdf",
446
+ description: "Make a PDF from markdown, in the derived directory of this conversation, and show it to the user. " +
447
+ "Charts that `render_chart` wrote can appear in it as images. The PDF reads no URL.",
448
+ inputSchema: exportPdfInput,
449
+ outputSchema: ExportPdfOutputSchema,
450
+ describeCall: ({ name }) => name,
451
+ describeResult: (_input, result) => result.path,
452
+ execute: async ({ name, markdown, title }, ctx) => {
453
+ const fileName = name.toLowerCase().endsWith(".pdf") ? name : `${name}.pdf`;
454
+ const { analysisId } = ctx.session.scope;
455
+ const workspaceRoot = await deps.resolveWorkspaceRoot(analysisId);
456
+ const images = new Map();
457
+ const reads = [];
458
+ const missingImages = [];
459
+ const scope = deps.imageScope?.(ctx.session);
460
+ for (const source of markdownImageSources(markdown)) {
461
+ const kind = scope !== undefined && !source.startsWith(`${scope}/`) ? undefined : imageKindOf(source);
462
+ const read = kind === undefined ? undefined : await deps.workspaceFs.readFile({ session: ctx.session, path: source, maxBytes: PDF_IMAGE_CAP_BYTES });
463
+ const file = read?.isOk() === true ? read.value : undefined;
464
+ if (kind === undefined || file?.kind !== "ok") {
465
+ missingImages.push(source);
466
+ continue;
467
+ }
468
+ images.set(source, kind === "svg" ? { kind, svg: file.content.toString("utf8") } : { kind, bytes: file.content });
469
+ const resolved = resolveWorkspacePath({ workspaceRoot, analysisId, path: source });
470
+ reads.push({ path: resolved.kind === "ok" ? resolved.relative.split("\\").join("/") : source, hash: computeSha256(file.content) });
471
+ }
472
+ const pdf = await markdownToPdf({ markdown, title, images });
473
+ const write = await deps.scratch.writeDerived({ session: ctx.session, invocationId: ctx.invocationId, name: fileName, content: pdf });
474
+ if (write.status === "refused")
475
+ return refused(write.reason);
476
+ const effect = { reads, writes: [write.file] };
477
+ return ok(withToolCallRecord({ path: write.file.path, bytes: write.file.size, missingImages }, effect));
478
+ },
479
+ });
480
+ }
481
+ export function createScriptFileTools(deps) {
482
+ return [createReadTableTool(deps), createReadPdfTool(deps), createListArchiveTool(deps), createWriteDerivedTool(deps), ...createDocumentTools(deps)];
483
+ }
484
+ /** The script tools that read a table, a PDF, and the files of a compressed file of the workspace, and write nothing. */
485
+ export function createReadScriptTools(deps) {
486
+ return [createReadTableTool(deps), createReadPdfTool(deps), createListArchiveTool(deps)];
487
+ }
488
+ /** The tools that make a document in a script: a chart as an SVG file, and a PDF from markdown. */
489
+ export function createDocumentTools(deps) {
490
+ return [createRenderChartTool(deps), createExportPdfTool(deps)];
491
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The OTel metrics of `run_script`, exported through the global MeterProvider.
3
+ *
4
+ * They answer one question: does the light work of an agent fit the script VM, or does it want a sandbox?
5
+ *
6
+ * - cortex.harness.script.runs — counter, one for each script, with `agent_id`, `outcome` (`ok` or `failed`), and
7
+ * `failure` (`none`, `script`, `memory`, `timeout`, `aborted`, or `sandbox`). A rise of `memory` and `timeout`
8
+ * says that scripts outgrow the VM.
9
+ * - cortex.harness.script.read_refusals — counter, one for each table read that a script tool refused, with
10
+ * `reason` (`too_large`, `unsupported_format`, or `unparsable`). A rise of `too_large` and `unsupported_format`
11
+ * says that the work wants the tables that only a sandbox reads.
12
+ * - cortex.harness.script.read_bytes — histogram of the bytes of each table or PDF that a script read, in buckets
13
+ * from 1 KiB up to the PDF read cap.
14
+ *
15
+ * A recovery replays the cached result of a script and runs no body, thus a replay never counts again. The
16
+ * instruments are made on the first use, thus they bind to whatever provider the host registers.
17
+ */
18
+ export type ScriptFailure = "none" | "script" | "memory" | "timeout" | "aborted" | "sandbox";
19
+ export type ReadRefusal = "too_large" | "unsupported_format" | "unparsable";
20
+ /**
21
+ * Bucket bounds for the bytes of one read: 1 KiB to 64 MiB, a factor of 4 apart. The top bound is the PDF read cap,
22
+ * thus the overflow bucket stays empty. Without them the default bounds stop at 10000, and each larger read lands in
23
+ * the overflow bucket.
24
+ */
25
+ export declare const SCRIPT_READ_BOUNDS_BYTES: readonly number[];
26
+ export declare function recordScriptRun(agentId: string, failure: ScriptFailure): void;
27
+ export declare function recordReadRefusal(reason: ReadRefusal): void;
28
+ export declare function recordReadBytes(bytes: number): void;
29
+ /** Drop the instruments, thus the next record binds to the provider that a test registered. */
30
+ export declare function __resetScriptMetricsForTest(): void;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The OTel metrics of `run_script`, exported through the global MeterProvider.
3
+ *
4
+ * They answer one question: does the light work of an agent fit the script VM, or does it want a sandbox?
5
+ *
6
+ * - cortex.harness.script.runs — counter, one for each script, with `agent_id`, `outcome` (`ok` or `failed`), and
7
+ * `failure` (`none`, `script`, `memory`, `timeout`, `aborted`, or `sandbox`). A rise of `memory` and `timeout`
8
+ * says that scripts outgrow the VM.
9
+ * - cortex.harness.script.read_refusals — counter, one for each table read that a script tool refused, with
10
+ * `reason` (`too_large`, `unsupported_format`, or `unparsable`). A rise of `too_large` and `unsupported_format`
11
+ * says that the work wants the tables that only a sandbox reads.
12
+ * - cortex.harness.script.read_bytes — histogram of the bytes of each table or PDF that a script read, in buckets
13
+ * from 1 KiB up to the PDF read cap.
14
+ *
15
+ * A recovery replays the cached result of a script and runs no body, thus a replay never counts again. The
16
+ * instruments are made on the first use, thus they bind to whatever provider the host registers.
17
+ */
18
+ import { metrics } from "@opentelemetry/api";
19
+ /**
20
+ * Bucket bounds for the bytes of one read: 1 KiB to 64 MiB, a factor of 4 apart. The top bound is the PDF read cap,
21
+ * thus the overflow bucket stays empty. Without them the default bounds stop at 10000, and each larger read lands in
22
+ * the overflow bucket.
23
+ */
24
+ export const SCRIPT_READ_BOUNDS_BYTES = [1_024, 4_096, 16_384, 65_536, 262_144, 1_048_576, 4_194_304, 16_777_216, 67_108_864];
25
+ let instruments;
26
+ function getInstruments() {
27
+ if (instruments === undefined) {
28
+ const meter = metrics.getMeter("cortex.harness.script");
29
+ instruments = {
30
+ runs: meter.createCounter("cortex.harness.script.runs", { description: "Scripts that run_script ran" }),
31
+ readRefusals: meter.createCounter("cortex.harness.script.read_refusals", { description: "Table reads of a script that a script tool refused" }),
32
+ readBytes: meter.createHistogram("cortex.harness.script.read_bytes", {
33
+ description: "Bytes of each table or PDF that a script read",
34
+ unit: "By",
35
+ advice: { explicitBucketBoundaries: [...SCRIPT_READ_BOUNDS_BYTES] },
36
+ }),
37
+ };
38
+ }
39
+ return instruments;
40
+ }
41
+ export function recordScriptRun(agentId, failure) {
42
+ getInstruments().runs.add(1, { agent_id: agentId, outcome: failure === "none" ? "ok" : "failed", failure });
43
+ }
44
+ export function recordReadRefusal(reason) {
45
+ getInstruments().readRefusals.add(1, { reason });
46
+ }
47
+ export function recordReadBytes(bytes) {
48
+ getInstruments().readBytes.record(bytes);
49
+ }
50
+ /** Drop the instruments, thus the next record binds to the provider that a test registered. */
51
+ export function __resetScriptMetricsForTest() {
52
+ instruments = undefined;
53
+ }
Binary file
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `run_script` — the agent writes one JavaScript script that calls its script tools as functions.
3
+ *
4
+ * The script runs in QuickJS (`@earendil-works/pi-codemode`), a WASM VM in a worker thread. The VM has no files, no
5
+ * network, and no timers. Its only capability is a call of a script tool, and each call runs the `execute` of that
6
+ * tool with the context of the `run_script` call. Only what the script prints or returns reaches the model, thus a
7
+ * script can join and filter large lookup results and tables before the model reads them.
8
+ *
9
+ * The whole script is one durable step: a replay returns the cached result and runs no lookup again. A step that
10
+ * did not complete runs the whole script again, thus a script tool that writes must accept the same write again.
11
+ *
12
+ * Each nested call streams its start and its end under the `run_script` call, thus a surface shows what the script
13
+ * does while it runs. With a scratch directory, the script is saved before it runs, each derived file is shown to
14
+ * the user in one file card with the script, and the files that the script read and wrote reach provenance.
15
+ */
16
+ import { z } from "zod";
17
+ import type { Logger } from "../../lib/logger.js";
18
+ import { type Tool, type WithToolCallRecord } from "../define-tool.js";
19
+ import type { ScriptRunRecord, ScriptScratch } from "./scratch.js";
20
+ export declare const RUN_SCRIPT_TOOL_ID = "run_script";
21
+ declare const inputSchema: z.ZodObject<{
22
+ description: z.ZodOptional<z.ZodString>;
23
+ code: z.ZodString;
24
+ }, z.core.$strip>;
25
+ /**
26
+ * Where the VM of a script comes from. A host that runs the harness from `node_modules` gives nothing, and the VM loads
27
+ * its wasm and its worker from there. A bundled host, for example a Bun single-file binary, has no such files on disk:
28
+ * it embeds `tools/script/quickjs.wasm` and the entry `tools/script/codemode-worker.js` of the harness, and gives their
29
+ * embedded paths.
30
+ */
31
+ export type ScriptRuntime = {
32
+ readonly wasmPath?: string;
33
+ readonly workerUrl?: string | URL;
34
+ };
35
+ /** What `run_script` needs beside its tools: the scratch directory of the session, and the logger. */
36
+ export type RunScriptOptions = {
37
+ readonly scratch?: ScriptScratch;
38
+ readonly logger?: Logger;
39
+ };
40
+ export type ScriptCall = {
41
+ readonly tool: string;
42
+ readonly ok: boolean;
43
+ };
44
+ /** The call record of one nested call, kept for a tool that folds its records. */
45
+ type NestedRecord = {
46
+ readonly tool: string;
47
+ readonly record: unknown;
48
+ };
49
+ /**
50
+ * The call record of a script: the run, for the provenance of the scratch directory, and the record of each nested
51
+ * call whose tool folds records, in call order. A replay folds the same records in the same order, thus the state
52
+ * that a fold rebuilds is the same as in a direct call.
53
+ */
54
+ type RunScriptRecord = {
55
+ readonly run?: ScriptRunRecord;
56
+ readonly nested: readonly NestedRecord[];
57
+ };
58
+ export type RunScriptOutput = {
59
+ readonly ok: boolean;
60
+ /** What the script printed, in order. */
61
+ readonly output: string;
62
+ /** The JSON of the returned value. Absent for a failed script and for a script that returned nothing. */
63
+ readonly value?: unknown;
64
+ /** The kind and the message of the failure of a failed script. */
65
+ readonly error?: string;
66
+ /** Each tool call of the script, in the order of the calls. */
67
+ readonly calls: readonly ScriptCall[];
68
+ /** The saved script, analysis-root-relative. Absent with no scratch directory. */
69
+ readonly script?: string;
70
+ /** Each file that the script derived, analysis-root-relative. */
71
+ readonly files?: readonly string[];
72
+ };
73
+ type RunScriptResult = RunScriptOutput | WithToolCallRecord<RunScriptOutput, RunScriptRecord>;
74
+ /** The `run_script` tool over the script tools of an agent. */
75
+ export declare function createRunScriptTool(scriptTools: readonly Tool[], runtime?: ScriptRuntime, options?: RunScriptOptions): Tool<z.infer<typeof inputSchema>, RunScriptResult>;
76
+ export {};