@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.
- package/README.md +1 -1
- package/dist/agents/conversation-agent.d.ts +3 -8
- package/dist/agents/conversation-agent.js +24 -4
- package/dist/agents/report-session-agent.d.ts +11 -14
- package/dist/agents/report-session-agent.js +30 -17
- package/dist/agents/sandbox/shared.d.ts +18 -13
- package/dist/agents/sandbox/shared.js +42 -27
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/agents/sandbox-catalog.d.ts +2 -0
- package/dist/agents/sandbox-catalog.js +30 -1
- package/dist/app/chat-turn-workflow.js +5 -6
- package/dist/app/synthesize-run.d.ts +10 -6
- package/dist/app/synthesize-run.js +4 -2
- package/dist/contracts/analogy-report.d.ts +46 -2
- package/dist/contracts/analogy-report.js +31 -25
- package/dist/contracts/chat-parts.d.ts +3 -14
- package/dist/contracts/report-reference.d.ts +2 -1
- package/dist/contracts/report-reference.js +2 -1
- package/dist/contracts/schemas/chat-parts.d.ts +0 -27
- package/dist/contracts/schemas/chat-parts.js +1 -3
- package/dist/execution/run-canceler.d.ts +2 -0
- package/dist/execution/run-canceler.js +9 -0
- package/dist/execution/run-synthesis.d.ts +36 -8
- package/dist/execution/run-synthesis.js +114 -32
- package/dist/index.d.ts +2 -5
- package/dist/index.js +3 -6
- package/dist/input-scan/enrich.d.ts +5 -10
- package/dist/input-scan/enrich.js +12 -8
- package/dist/lib/hooks.d.ts +9 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/markdown-pdf.d.ts +27 -0
- package/dist/lib/markdown-pdf.js +236 -0
- package/dist/lib/metrics.d.ts +31 -0
- package/dist/lib/metrics.js +64 -0
- package/dist/lib/pdf-text.d.ts +23 -0
- package/dist/lib/pdf-text.js +34 -0
- package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
- package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
- package/dist/loop/run-loop.d.ts +5 -0
- package/dist/loop/run-loop.js +30 -2
- package/dist/loop/sub-agent.js +5 -6
- package/dist/loop/tool-name-repair.d.ts +11 -0
- package/dist/loop/tool-name-repair.js +23 -0
- package/dist/loop/turn-credentials.d.ts +5 -0
- package/dist/loop/turn-credentials.js +17 -0
- package/dist/loop/types.d.ts +14 -0
- package/dist/memory/card-builders.d.ts +1 -1
- package/dist/prompts/SOUL.d.ts +6 -6
- package/dist/prompts/SOUL.js +22 -121
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +35 -101
- package/dist/prompts/briefing.d.ts +20 -11
- package/dist/prompts/briefing.js +36 -19
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +103 -383
- package/dist/prompts/planner.d.ts +1 -5
- package/dist/prompts/planner.js +45 -128
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +26 -57
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +4 -5
- package/dist/prompts/sandbox/data-profiler.js +11 -34
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +3 -1
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +3 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +3 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +3 -1
- package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
- package/dist/prompts/sandbox/scientific-executor.js +0 -12
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +3 -1
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
- package/dist/prompts/sandbox-standards.d.ts +15 -18
- package/dist/prompts/sandbox-standards.js +96 -305
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +24 -90
- package/dist/provenance/collector.d.ts +2 -5
- package/dist/provenance/collector.js +2 -5
- package/dist/provenance/seam.d.ts +36 -1
- package/dist/provenance/workspace-reads.d.ts +36 -0
- package/dist/provenance/workspace-reads.js +33 -0
- package/dist/providers/request-headers.js +12 -3
- package/dist/report-model/production-resolver.d.ts +22 -52
- package/dist/report-model/production-resolver.js +43 -112
- package/dist/report-render/chart-export.d.ts +9 -0
- package/dist/report-render/chart-export.js +1 -1
- package/dist/report-render/views/chart-view.d.ts +1 -0
- package/dist/report-render/views/chart-view.js +1 -0
- package/dist/report-render/views/lineage.d.ts +1 -0
- package/dist/report-render/views/page-view.d.ts +1 -0
- package/dist/report-render/views/page-view.js +1 -0
- package/dist/report-render/views/prose.d.ts +1 -0
- package/dist/report-render/views/prose.js +1 -0
- package/dist/report-render/views/references-view.d.ts +1 -0
- package/dist/report-render/views/values.d.ts +1 -0
- package/dist/report-render/views/values.js +1 -0
- package/dist/runtime/assemble.d.ts +20 -9
- package/dist/runtime/assemble.js +47 -38
- package/dist/sandbox/client.d.ts +5 -3
- package/dist/sandbox/create-sandbox.d.ts +7 -12
- package/dist/sandbox/create-sandbox.js +15 -37
- package/dist/sandbox/docker-client.d.ts +1 -3
- package/dist/sandbox/docker-client.js +3 -5
- package/dist/sandbox/exec-outcome.d.ts +11 -11
- package/dist/sandbox/exec-outcome.js +22 -22
- package/dist/sandbox/exec.d.ts +5 -0
- package/dist/sandbox/exec.js +9 -1
- package/dist/sandbox/identity.d.ts +4 -16
- package/dist/sandbox/identity.js +5 -21
- package/dist/sandbox/k8s-client.d.ts +0 -1
- package/dist/sandbox/k8s-client.js +8 -13
- package/dist/sandbox/lease-names.d.ts +6 -0
- package/dist/sandbox/lease-names.js +12 -0
- package/dist/sandbox/leases.d.ts +105 -0
- package/dist/sandbox/leases.js +519 -0
- package/dist/sandbox/liveness.d.ts +5 -2
- package/dist/sandbox/mount-plan.d.ts +10 -1
- package/dist/sandbox/mount-plan.js +7 -1
- package/dist/sandbox/reconciler.d.ts +58 -0
- package/dist/sandbox/reconciler.js +153 -0
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/types.d.ts +25 -18
- package/dist/sandbox/types.js +10 -4
- package/dist/schemas/plan-schemas.d.ts +0 -21
- package/dist/schemas/plan-schemas.js +2 -3
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/schemas/run-synthesis.js +3 -3
- package/dist/schemas/validate-plan.d.ts +1 -14
- package/dist/schemas/validate-plan.js +5 -24
- package/dist/schemas/workflow-state.d.ts +3 -32
- package/dist/schemas/workflow-state.js +4 -13
- package/dist/state/index.d.ts +0 -1
- package/dist/state/index.js +0 -1
- package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
- package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
- package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
- package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
- package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
- package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
- package/dist/state/migrations/index.js +6 -0
- package/dist/state/purge-analysis.d.ts +4 -2
- package/dist/state/purge-analysis.js +22 -11
- package/dist/state/sandbox-leases.d.ts +81 -0
- package/dist/state/sandbox-leases.js +247 -0
- package/dist/state/schema.d.ts +1 -13
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.d.ts +0 -2
- package/dist/state/step-executions.js +1 -4
- package/dist/tasks/data-profile.d.ts +40 -2
- package/dist/tasks/data-profile.js +89 -87
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +6 -1
- package/dist/tools/ad-hoc-router.d.ts +1 -6
- package/dist/tools/ad-hoc-router.js +5 -48
- package/dist/tools/bio/comptox.js +1 -1
- package/dist/tools/bio/drug-gene-interactions.js +1 -1
- package/dist/tools/bio/gene-disease-evidence.js +1 -1
- package/dist/tools/bio/keys.d.ts +1 -1
- package/dist/tools/bio/pubmed.js +31 -0
- package/dist/tools/bio/search-gene.js +17 -0
- package/dist/tools/bio/search-geo-datasets.js +13 -0
- package/dist/tools/bio/search-regulatory-approvals.js +1 -1
- package/dist/tools/define-tool.d.ts +25 -2
- package/dist/tools/define-tool.js +11 -1
- package/dist/tools/display/show-user.js +1 -1
- package/dist/tools/execute-analysis.d.ts +1 -3
- package/dist/tools/execute-analysis.js +5 -33
- package/dist/tools/report-session/index.d.ts +0 -1
- package/dist/tools/report-session/index.js +0 -1
- package/dist/tools/report-session/script-tools.d.ts +30 -0
- package/dist/tools/report-session/script-tools.js +177 -0
- package/dist/tools/research/generate-analogy-report.d.ts +16 -9
- package/dist/tools/research/generate-analogy-report.js +111 -49
- package/dist/tools/research/generate-plan.d.ts +102 -7
- package/dist/tools/research/generate-plan.js +74 -43
- package/dist/tools/research/index.d.ts +4 -3
- package/dist/tools/research/index.js +4 -3
- package/dist/tools/research/inspect-run.d.ts +0 -1
- package/dist/tools/research/inspect-run.js +5 -10
- package/dist/tools/research/search-arxiv.js +15 -0
- package/dist/tools/research/search-github-repos.js +13 -0
- package/dist/tools/research/search-semantic-scholar.js +16 -0
- package/dist/tools/research/synthesis-lookups.d.ts +21 -0
- package/dist/tools/research/synthesis-lookups.js +29 -0
- package/dist/tools/research/web-fetch.d.ts +1 -1
- package/dist/tools/research/web-fetch.js +14 -12
- package/dist/tools/sandbox/lease-tools.d.ts +32 -0
- package/dist/tools/sandbox/lease-tools.js +60 -0
- package/dist/tools/script/codemode-worker.d.ts +5 -0
- package/dist/tools/script/codemode-worker.js +5 -0
- package/dist/tools/script/file-tools.d.ts +100 -0
- package/dist/tools/script/file-tools.js +377 -0
- package/dist/tools/script/metrics.d.ts +21 -0
- package/dist/tools/script/metrics.js +38 -0
- package/dist/tools/script/quickjs.wasm +0 -0
- package/dist/tools/script/run-script.d.ts +76 -0
- package/dist/tools/script/run-script.js +326 -0
- package/dist/tools/script/scratch.d.ts +81 -0
- package/dist/tools/script/scratch.js +142 -0
- package/dist/tools/start-report-session.js +1 -1
- package/dist/tools/workspace/edit-file.js +2 -1
- package/dist/tools/workspace/execute-command.d.ts +9 -13
- package/dist/tools/workspace/execute-command.js +23 -19
- package/dist/tools/workspace/file-stat.js +1 -1
- package/dist/tools/workspace/grep.d.ts +5 -2
- package/dist/tools/workspace/grep.js +19 -10
- package/dist/tools/workspace/mutator.d.ts +48 -4
- package/dist/tools/workspace/mutator.js +43 -12
- package/dist/tools/workspace/read-file.d.ts +5 -2
- package/dist/tools/workspace/read-file.js +15 -11
- package/dist/tools/workspace/read-surface.d.ts +17 -0
- package/dist/tools/workspace/read-surface.js +17 -0
- package/dist/tools/workspace/result-bounds.d.ts +1 -0
- package/dist/tools/workspace/run-exec.d.ts +12 -13
- package/dist/tools/workspace/run-exec.js +13 -13
- package/dist/tools/workspace/scan-inputs.d.ts +2 -5
- package/dist/tools/workspace/scan-inputs.js +2 -3
- package/dist/tools/workspace/show-file.js +1 -1
- package/dist/tools/workspace/show-plan.js +4 -3
- package/dist/tools/workspace/write-file.js +2 -1
- package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
- package/dist/workflows/execute-analysis-scheduler.js +12 -32
- package/dist/workflows/execute-analysis.d.ts +17 -35
- package/dist/workflows/execute-analysis.js +13 -107
- package/dist/workflows/pause.js +29 -22
- package/dist/workflows/sandbox-step.d.ts +16 -31
- package/dist/workflows/sandbox-step.js +36 -71
- package/dist/workflows/test/sql-writes.d.ts +0 -1
- package/dist/workflows/test/sql-writes.js +0 -2
- package/dist/workspace/filesystem.d.ts +2 -2
- package/dist/workspace/filesystem.js +10 -5
- package/dist/workspace/paths.d.ts +19 -1
- package/dist/workspace/paths.js +22 -1
- package/package.json +6 -3
- package/dist/prompts/literature-reviewer.d.ts +0 -1
- package/dist/prompts/literature-reviewer.js +0 -114
- package/dist/sandbox/reaper.d.ts +0 -81
- package/dist/sandbox/reaper.js +0 -161
- package/dist/sandbox/step-sandbox.d.ts +0 -47
- package/dist/sandbox/step-sandbox.js +0 -53
- package/dist/state/active-sandboxes.d.ts +0 -33
- package/dist/state/active-sandboxes.js +0 -65
- package/dist/tasks/extract-values-script.d.ts +0 -64
- package/dist/tasks/extract-values-script.js +0 -180
- package/dist/tasks/extract-values.d.ts +0 -77
- package/dist/tasks/extract-values.js +0 -146
- package/dist/tools/report-session/derive-table.d.ts +0 -164
- package/dist/tools/report-session/derive-table.js +0 -419
- package/dist/tools/research/literature-reviewer.d.ts +0 -42
- package/dist/tools/research/literature-reviewer.js +0 -107
- package/dist/workflows/metrics.d.ts +0 -23
- package/dist/workflows/metrics.js +0 -38
|
@@ -0,0 +1,377 @@
|
|
|
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.
|
|
11
|
+
*
|
|
12
|
+
* Each tool gives the files that it read or wrote as its call record. `run_script` collects the records of the
|
|
13
|
+
* calls of one script, thus the provenance of the script chains each derived file to the tables that it read.
|
|
14
|
+
*/
|
|
15
|
+
import { ok, err } from "neverthrow";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { computeSha256 } from "../../lib/fs-helpers.js";
|
|
18
|
+
import { markdownImageSources, markdownToPdf } from "../../lib/markdown-pdf.js";
|
|
19
|
+
import { readPdfText } from "../../lib/pdf-text.js";
|
|
20
|
+
import { detectFormat, parseTable } from "../../report-model/production-resolver.js";
|
|
21
|
+
import { CHART_EXPORT_SIZES, SCATTER_CROWD_ROWS } from "../../report-render/design.js";
|
|
22
|
+
import { loadChartRuntime, plottedPoints, renderChartSvg } from "../../report-render/chart-export.js";
|
|
23
|
+
import { parseUnknownRecord, UnknownArraySchema } from "../../lib/unknown-containers.js";
|
|
24
|
+
import { resolveWorkspacePath } from "../../workspace/paths.js";
|
|
25
|
+
import { defineTool, withToolCallRecord } from "../define-tool.js";
|
|
26
|
+
import { normalizeEchartSpec } from "../display/normalize-echart-spec.js";
|
|
27
|
+
import { recordReadBytes, recordReadRefusal } from "./metrics.js";
|
|
28
|
+
/** The cap of one table read. It matches the cap of the report path, which reads with the same parsers. */
|
|
29
|
+
export const TABLE_READ_CAP_BYTES = 16 * 1024 * 1024;
|
|
30
|
+
export const ScriptFileEffectSchema = z.object({
|
|
31
|
+
reads: z.array(z.object({ path: z.string(), hash: z.string() })).optional(),
|
|
32
|
+
writes: z.array(z.object({ path: z.string(), hash: z.string(), size: z.number() })).optional(),
|
|
33
|
+
});
|
|
34
|
+
function refused(error) {
|
|
35
|
+
return err({ error, retryable: false });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A cell of a text table as a number, when the text is a plain decimal number. A text with a leading zero, such
|
|
39
|
+
* as an identifier `007`, stays text, because a number loses its zeros.
|
|
40
|
+
*/
|
|
41
|
+
function numericCell(text) {
|
|
42
|
+
if (text.length === 0 || text !== text.trim())
|
|
43
|
+
return text;
|
|
44
|
+
const value = Number(text);
|
|
45
|
+
if (!Number.isFinite(value))
|
|
46
|
+
return text;
|
|
47
|
+
const digits = text.startsWith("-") ? text.slice(1) : text;
|
|
48
|
+
if (digits.length > 1 && digits.startsWith("0") && digits[1] !== ".")
|
|
49
|
+
return text;
|
|
50
|
+
if (digits.includes("x") || digits.includes("X") || digits.includes("o") || digits.includes("b"))
|
|
51
|
+
return text;
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read one table through the read seam, parse it on the host, and keep the named columns. A refusal is the line
|
|
56
|
+
* that the script reads. `tooLarge` names where a table over the cap goes instead.
|
|
57
|
+
*/
|
|
58
|
+
export async function readTableRows(args) {
|
|
59
|
+
const { path, columns } = args;
|
|
60
|
+
const format = detectFormat(path);
|
|
61
|
+
if (format === undefined) {
|
|
62
|
+
recordReadRefusal("unsupported_format");
|
|
63
|
+
return err(`${path} has no table format that a script reads (csv, tsv, json, parquet); an analysis run reads it`);
|
|
64
|
+
}
|
|
65
|
+
const read = await args.workspaceFs.readFile({ session: args.session, path, maxBytes: TABLE_READ_CAP_BYTES });
|
|
66
|
+
if (read.isErr())
|
|
67
|
+
return err(`${path} could not be read`);
|
|
68
|
+
const file = read.value;
|
|
69
|
+
if (file.kind === "not_found")
|
|
70
|
+
return err(`${path} does not exist; find the path with workspace_search or list_files`);
|
|
71
|
+
if (file.kind === "out_of_scope")
|
|
72
|
+
return err(`${path} is outside this analysis`);
|
|
73
|
+
if (file.kind === "truncated") {
|
|
74
|
+
recordReadRefusal("too_large");
|
|
75
|
+
return err(`${path} is ${String(file.totalSize)} bytes, over the 16 MiB cap of a script read; ${args.tooLarge}`);
|
|
76
|
+
}
|
|
77
|
+
const parsed = await parseTable(format, file.content);
|
|
78
|
+
if (parsed === undefined) {
|
|
79
|
+
recordReadRefusal("unparsable");
|
|
80
|
+
return err(`${path} did not parse as ${format}: a ragged row, a repeated header, or a nested JSON value`);
|
|
81
|
+
}
|
|
82
|
+
recordReadBytes(file.content.length);
|
|
83
|
+
const header = [...new Set(parsed.flatMap((row) => Object.keys(row)))];
|
|
84
|
+
const missing = (columns ?? []).filter((column) => !header.includes(column));
|
|
85
|
+
if (missing.length > 0)
|
|
86
|
+
return err(`${path} has no column ${missing.join(", ")}; its columns are ${header.join(", ")}`);
|
|
87
|
+
const kept = columns === undefined ? header : [...columns];
|
|
88
|
+
const rows = parsed.map((row) => {
|
|
89
|
+
const out = {};
|
|
90
|
+
for (const column of kept) {
|
|
91
|
+
const cell = row[column];
|
|
92
|
+
if (cell !== undefined)
|
|
93
|
+
out[column] = typeof cell === "string" ? numericCell(cell) : cell;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
});
|
|
97
|
+
return ok({ content: file.content, columns: kept, rows });
|
|
98
|
+
}
|
|
99
|
+
const readTableInput = z.object({
|
|
100
|
+
path: z.string().min(1).describe("Analysis-rooted path of the table, for example 'runs/<runId>/<stepId>/output/de_results.csv'."),
|
|
101
|
+
columns: z.array(z.string()).min(1).optional().describe("Keep only these columns. Absent: each column."),
|
|
102
|
+
});
|
|
103
|
+
export const ReadTableOutputSchema = z.object({
|
|
104
|
+
path: z.string(),
|
|
105
|
+
columns: z.array(z.string()).readonly(),
|
|
106
|
+
rowCount: z.number(),
|
|
107
|
+
rows: z.array(z.record(z.string(), z.union([z.string(), z.number()]))).readonly(),
|
|
108
|
+
});
|
|
109
|
+
const WrittenOutputSchema = z.object({ path: z.string().describe("The analysis-rooted path of the written file."), bytes: z.number() });
|
|
110
|
+
function createReadTableTool(deps) {
|
|
111
|
+
return defineTool({
|
|
112
|
+
id: "read_table",
|
|
113
|
+
description: "Read one table of the workspace as rows: CSV, TSV, JSON (an array of flat objects), or parquet, up to 16 MiB. " +
|
|
114
|
+
"Each row is an object keyed by column, and a cell that holds a plain number reads as a number. " +
|
|
115
|
+
"A larger table, or another format such as h5ad, is work for an analysis run.",
|
|
116
|
+
inputSchema: readTableInput,
|
|
117
|
+
outputSchema: ReadTableOutputSchema,
|
|
118
|
+
describeCall: ({ path }) => path,
|
|
119
|
+
describeResult: (_input, result) => `${String(result.rowCount)} rows`,
|
|
120
|
+
execute: async ({ path, columns }, ctx) => {
|
|
121
|
+
const table = await readTableRows({ workspaceFs: deps.workspaceFs, session: ctx.session, path, columns, tooLarge: "an analysis run reads it" });
|
|
122
|
+
if (table.isErr())
|
|
123
|
+
return refused(table.error);
|
|
124
|
+
const { content, rows } = table.value;
|
|
125
|
+
const { analysisId } = ctx.session.scope;
|
|
126
|
+
const resolved = resolveWorkspacePath({ workspaceRoot: await deps.resolveWorkspaceRoot(analysisId), analysisId, path });
|
|
127
|
+
const relative = resolved.kind === "ok" ? resolved.relative.split("\\").join("/") : path;
|
|
128
|
+
const effect = { reads: [{ path: relative, hash: computeSha256(content) }] };
|
|
129
|
+
return ok(withToolCallRecord({ path: relative, columns: table.value.columns, rowCount: rows.length, rows }, effect));
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** The cap of one PDF read, and the pages that one call reads. */
|
|
134
|
+
const PDF_READ_CAP_BYTES = 64 * 1024 * 1024;
|
|
135
|
+
const PDF_PAGES_PER_READ = 50;
|
|
136
|
+
const readPdfInput = z.object({
|
|
137
|
+
path: z.string().min(1).describe("Analysis-rooted path of the PDF, for example 'data/inputs/<fileId>/paper.pdf'."),
|
|
138
|
+
firstPage: z.number().int().min(1).optional().describe("The first page to read, 1-based. Absent: 1."),
|
|
139
|
+
lastPage: z
|
|
140
|
+
.number()
|
|
141
|
+
.int()
|
|
142
|
+
.min(1)
|
|
143
|
+
.optional()
|
|
144
|
+
.describe(`The last page to read. One call reads at most ${String(PDF_PAGES_PER_READ)} pages. Absent: as many as that cap allows.`),
|
|
145
|
+
});
|
|
146
|
+
const ReadPdfOutputSchema = z.object({
|
|
147
|
+
path: z.string(),
|
|
148
|
+
totalPages: z.number(),
|
|
149
|
+
pages: z.array(z.object({ page: z.number(), text: z.string() })).readonly(),
|
|
150
|
+
});
|
|
151
|
+
/** `lineage` records each read in the lineage of a step. */
|
|
152
|
+
export function createReadPdfTool(deps) {
|
|
153
|
+
const { lineage } = deps;
|
|
154
|
+
return defineTool({
|
|
155
|
+
id: "read_pdf",
|
|
156
|
+
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. ` +
|
|
157
|
+
"`totalPages` gives the page count, thus a script reads a long PDF in more calls and keeps the pages that it needs. " +
|
|
158
|
+
"A scanned page with no text layer reads as an empty text.",
|
|
159
|
+
inputSchema: readPdfInput,
|
|
160
|
+
outputSchema: ReadPdfOutputSchema,
|
|
161
|
+
describeCall: ({ path, firstPage, lastPage }) => firstPage === undefined && lastPage === undefined ? path : `${path}, pages ${String(firstPage ?? 1)}-${String(lastPage ?? "")}`,
|
|
162
|
+
describeResult: (_input, result) => `${String(result.pages.length)} of ${String(result.totalPages)} pages`,
|
|
163
|
+
// `run_script` folds the record of a nested call only into a tool that folds records.
|
|
164
|
+
foldCallRecord: lineage === undefined ? undefined : (effect) => lineage.track(effect.reads ?? []),
|
|
165
|
+
execute: async ({ path, firstPage, lastPage }, ctx) => {
|
|
166
|
+
if (!path.toLowerCase().endsWith(".pdf")) {
|
|
167
|
+
recordReadRefusal("unsupported_format");
|
|
168
|
+
return refused(`${path} is not a PDF; read_table reads a table, and read_file reads text`);
|
|
169
|
+
}
|
|
170
|
+
const read = await deps.workspaceFs.readFile({ session: ctx.session, path, maxBytes: PDF_READ_CAP_BYTES });
|
|
171
|
+
if (read.isErr())
|
|
172
|
+
return refused(`${path} could not be read`);
|
|
173
|
+
const file = read.value;
|
|
174
|
+
if (file.kind === "not_found")
|
|
175
|
+
return refused(`${path} does not exist; find the path with workspace_search or list_files`);
|
|
176
|
+
if (file.kind === "out_of_scope")
|
|
177
|
+
return refused(`${path} is outside this analysis`);
|
|
178
|
+
if (file.kind === "truncated") {
|
|
179
|
+
recordReadRefusal("too_large");
|
|
180
|
+
return refused(`${path} is ${String(file.totalSize)} bytes, over the 64 MiB cap of a PDF read`);
|
|
181
|
+
}
|
|
182
|
+
const first = firstPage ?? 1;
|
|
183
|
+
const last = Math.min(lastPage ?? first + PDF_PAGES_PER_READ - 1, first + PDF_PAGES_PER_READ - 1);
|
|
184
|
+
let text;
|
|
185
|
+
try {
|
|
186
|
+
text = await readPdfText(file.content, { first, last });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
recordReadRefusal("unparsable");
|
|
190
|
+
return refused(`${path} did not open as a PDF: a damaged file, or a file with a password`);
|
|
191
|
+
}
|
|
192
|
+
recordReadBytes(file.content.length);
|
|
193
|
+
const { analysisId } = ctx.session.scope;
|
|
194
|
+
const resolved = resolveWorkspacePath({ workspaceRoot: await deps.resolveWorkspaceRoot(analysisId), analysisId, path });
|
|
195
|
+
const relative = resolved.kind === "ok" ? resolved.relative.split("\\").join("/") : path;
|
|
196
|
+
const effect = { reads: [{ path: relative, hash: computeSha256(file.content) }] };
|
|
197
|
+
return ok(withToolCallRecord({ path: relative, totalPages: text.totalPages, pages: text.pages }, effect));
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** One cell as delimited text: quoted when it holds the delimiter, a quote, or a line break. */
|
|
202
|
+
function delimitedCell(value, delimiter) {
|
|
203
|
+
if (value === null || value === undefined)
|
|
204
|
+
return "";
|
|
205
|
+
const text = String(value);
|
|
206
|
+
if (!text.includes(delimiter) && !text.includes('"') && !text.includes("\n") && !text.includes("\r"))
|
|
207
|
+
return text;
|
|
208
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
209
|
+
}
|
|
210
|
+
/** The rows as CSV or TSV: the header is each key in the order of its first row, then one line for each row. */
|
|
211
|
+
function delimitedText(rows, delimiter) {
|
|
212
|
+
const header = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
213
|
+
const lines = [header.map((name) => delimitedCell(name, delimiter)).join(delimiter)];
|
|
214
|
+
for (const row of rows)
|
|
215
|
+
lines.push(header.map((name) => delimitedCell(row[name], delimiter)).join(delimiter));
|
|
216
|
+
return `${lines.join("\n")}\n`;
|
|
217
|
+
}
|
|
218
|
+
/** The text of `rows` in the format that the extension of `name` names, or `undefined` for another extension. */
|
|
219
|
+
export function rowsText(name, rows) {
|
|
220
|
+
const lower = name.toLowerCase();
|
|
221
|
+
if (lower.endsWith(".csv"))
|
|
222
|
+
return delimitedText(rows, ",");
|
|
223
|
+
if (lower.endsWith(".tsv"))
|
|
224
|
+
return delimitedText(rows, "\t");
|
|
225
|
+
if (lower.endsWith(".json"))
|
|
226
|
+
return `${JSON.stringify(rows, null, 2)}\n`;
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const writeDerivedInput = z.object({
|
|
230
|
+
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."),
|
|
231
|
+
content: z.string().optional().describe("The text of the file. Give `content` or `rows`."),
|
|
232
|
+
rows: z
|
|
233
|
+
.array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])))
|
|
234
|
+
.optional()
|
|
235
|
+
.describe("Table rows, each an object keyed by column. Give `content` or `rows`."),
|
|
236
|
+
});
|
|
237
|
+
function written(file) {
|
|
238
|
+
return withToolCallRecord({ path: file.path, bytes: file.size }, { writes: [file] });
|
|
239
|
+
}
|
|
240
|
+
function createWriteDerivedTool(deps) {
|
|
241
|
+
return defineTool({
|
|
242
|
+
id: "write_derived",
|
|
243
|
+
description: "Save a file that the script derived, in the derived directory of this conversation, and show it to the user. " +
|
|
244
|
+
"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`. " +
|
|
245
|
+
"A name holds one file: a second write of the name with other content is refused.",
|
|
246
|
+
inputSchema: writeDerivedInput,
|
|
247
|
+
outputSchema: WrittenOutputSchema,
|
|
248
|
+
describeCall: ({ name }) => name,
|
|
249
|
+
describeResult: (_input, result) => result.path,
|
|
250
|
+
execute: async ({ name, content, rows }, ctx) => {
|
|
251
|
+
if ((content === undefined) === (rows === undefined))
|
|
252
|
+
return refused("give exactly one of `content` and `rows`");
|
|
253
|
+
const text = rows === undefined ? content : rowsText(name, rows);
|
|
254
|
+
if (text === undefined)
|
|
255
|
+
return refused(`${name} names no table format: with rows, end the name in .csv, .tsv, or .json`);
|
|
256
|
+
const write = await deps.scratch.writeDerived({ session: ctx.session, invocationId: ctx.invocationId, name, content: text });
|
|
257
|
+
if (write.status === "refused")
|
|
258
|
+
return refused(write.reason);
|
|
259
|
+
return ok(written(write.file));
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const renderChartInput = z.object({
|
|
264
|
+
name: z.string().min(1).describe("File name of the SVG, for example 'volcano.svg'."),
|
|
265
|
+
spec: z
|
|
266
|
+
.record(z.string(), z.unknown())
|
|
267
|
+
.describe("ECharts option, the same as `show_user` takes, with the rows inline in `dataset.source` or `series[].data`."),
|
|
268
|
+
width: z.enum(["single", "double"]).optional().describe("Journal column: single (89 mm) or double (183 mm). Default double."),
|
|
269
|
+
});
|
|
270
|
+
/** The rows of `dataset.source` of an option, or of each dataset of a list. A chart from a dataset plots each one. */
|
|
271
|
+
function datasetRows(spec) {
|
|
272
|
+
const datasets = UnknownArraySchema.safeParse(spec.dataset);
|
|
273
|
+
const list = datasets.success ? datasets.data : [spec.dataset];
|
|
274
|
+
let count = 0;
|
|
275
|
+
for (const dataset of list) {
|
|
276
|
+
const source = UnknownArraySchema.safeParse(parseUnknownRecord(dataset)?.source);
|
|
277
|
+
if (source.success)
|
|
278
|
+
count += source.data.length;
|
|
279
|
+
}
|
|
280
|
+
return count;
|
|
281
|
+
}
|
|
282
|
+
function createRenderChartTool(deps) {
|
|
283
|
+
return defineTool({
|
|
284
|
+
id: "render_chart",
|
|
285
|
+
description: "Render an ECharts option to an SVG file at a journal column width, in the derived directory of this conversation, for a document. " +
|
|
286
|
+
"To show a chart in the chat, use `show_user`.",
|
|
287
|
+
inputSchema: renderChartInput,
|
|
288
|
+
outputSchema: WrittenOutputSchema,
|
|
289
|
+
describeCall: ({ name }) => name,
|
|
290
|
+
describeResult: (_input, result) => result.path,
|
|
291
|
+
execute: async ({ name, spec, width }, ctx) => {
|
|
292
|
+
const fileName = name.toLowerCase().endsWith(".svg") ? name : `${name}.svg`;
|
|
293
|
+
const option = normalizeEchartSpec(spec);
|
|
294
|
+
const points = plottedPoints(option) + datasetRows(option);
|
|
295
|
+
if (points > SCATTER_CROWD_ROWS) {
|
|
296
|
+
return refused(`the chart draws ${String(points)} points, and an SVG file holds at most ${String(SCATTER_CROWD_ROWS)}; plot fewer points`);
|
|
297
|
+
}
|
|
298
|
+
const svg = renderChartSvg(await loadChartRuntime(), option, CHART_EXPORT_SIZES[width ?? "double"]);
|
|
299
|
+
if (svg.isErr())
|
|
300
|
+
return refused(`the chart runtime refused the option: ${svg.error}`);
|
|
301
|
+
const write = await deps.scratch.writeDerived({ session: ctx.session, invocationId: ctx.invocationId, name: fileName, content: svg.value });
|
|
302
|
+
if (write.status === "refused")
|
|
303
|
+
return refused(write.reason);
|
|
304
|
+
return ok(written(write.file));
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/** The file tools of a script, over one scratch directory. */
|
|
309
|
+
/** The cap of one image that a PDF embeds, and the cap of the markdown of one PDF. */
|
|
310
|
+
const PDF_IMAGE_CAP_BYTES = 16 * 1024 * 1024;
|
|
311
|
+
const PDF_MARKDOWN_CAP_CHARS = 1_000_000;
|
|
312
|
+
const exportPdfInput = z.object({
|
|
313
|
+
name: z.string().min(1).describe("File name of the PDF, for example 'summary.pdf'."),
|
|
314
|
+
markdown: z
|
|
315
|
+
.string()
|
|
316
|
+
.max(PDF_MARKDOWN_CAP_CHARS)
|
|
317
|
+
.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 " +
|
|
318
|
+
"by its analysis-rooted path, for example the path that `render_chart` gives; a chart SVG lands as vector graphics."),
|
|
319
|
+
title: z.string().optional().describe("The title on the first page and in the metadata of the PDF."),
|
|
320
|
+
});
|
|
321
|
+
const ExportPdfOutputSchema = WrittenOutputSchema.extend({
|
|
322
|
+
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."),
|
|
323
|
+
});
|
|
324
|
+
const IMAGE_KINDS = { ".svg": "svg", ".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg" };
|
|
325
|
+
function imageKindOf(path) {
|
|
326
|
+
const dot = path.lastIndexOf(".");
|
|
327
|
+
return dot === -1 ? undefined : IMAGE_KINDS[path.slice(dot).toLowerCase()];
|
|
328
|
+
}
|
|
329
|
+
function createExportPdfTool(deps) {
|
|
330
|
+
return defineTool({
|
|
331
|
+
id: "export_pdf",
|
|
332
|
+
description: "Make a PDF from markdown, in the derived directory of this conversation, and show it to the user. " +
|
|
333
|
+
"Charts that `render_chart` wrote can appear in it as images. The PDF reads no URL.",
|
|
334
|
+
inputSchema: exportPdfInput,
|
|
335
|
+
outputSchema: ExportPdfOutputSchema,
|
|
336
|
+
describeCall: ({ name }) => name,
|
|
337
|
+
describeResult: (_input, result) => result.path,
|
|
338
|
+
execute: async ({ name, markdown, title }, ctx) => {
|
|
339
|
+
const fileName = name.toLowerCase().endsWith(".pdf") ? name : `${name}.pdf`;
|
|
340
|
+
const { analysisId } = ctx.session.scope;
|
|
341
|
+
const workspaceRoot = await deps.resolveWorkspaceRoot(analysisId);
|
|
342
|
+
const images = new Map();
|
|
343
|
+
const reads = [];
|
|
344
|
+
const missingImages = [];
|
|
345
|
+
const scope = deps.imageScope?.(ctx.session);
|
|
346
|
+
for (const source of markdownImageSources(markdown)) {
|
|
347
|
+
const kind = scope !== undefined && !source.startsWith(`${scope}/`) ? undefined : imageKindOf(source);
|
|
348
|
+
const read = kind === undefined ? undefined : await deps.workspaceFs.readFile({ session: ctx.session, path: source, maxBytes: PDF_IMAGE_CAP_BYTES });
|
|
349
|
+
const file = read?.isOk() === true ? read.value : undefined;
|
|
350
|
+
if (kind === undefined || file?.kind !== "ok") {
|
|
351
|
+
missingImages.push(source);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
images.set(source, kind === "svg" ? { kind, svg: file.content.toString("utf8") } : { kind, bytes: file.content });
|
|
355
|
+
const resolved = resolveWorkspacePath({ workspaceRoot, analysisId, path: source });
|
|
356
|
+
reads.push({ path: resolved.kind === "ok" ? resolved.relative.split("\\").join("/") : source, hash: computeSha256(file.content) });
|
|
357
|
+
}
|
|
358
|
+
const pdf = await markdownToPdf({ markdown, title, images });
|
|
359
|
+
const write = await deps.scratch.writeDerived({ session: ctx.session, invocationId: ctx.invocationId, name: fileName, content: pdf });
|
|
360
|
+
if (write.status === "refused")
|
|
361
|
+
return refused(write.reason);
|
|
362
|
+
const effect = { reads, writes: [write.file] };
|
|
363
|
+
return ok(withToolCallRecord({ path: write.file.path, bytes: write.file.size, missingImages }, effect));
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
export function createScriptFileTools(deps) {
|
|
368
|
+
return [createReadTableTool(deps), createReadPdfTool(deps), createWriteDerivedTool(deps), ...createDocumentTools(deps)];
|
|
369
|
+
}
|
|
370
|
+
/** The script tools that read a table and a PDF of the workspace, and write nothing. */
|
|
371
|
+
export function createReadScriptTools(deps) {
|
|
372
|
+
return [createReadTableTool(deps), createReadPdfTool(deps)];
|
|
373
|
+
}
|
|
374
|
+
/** The tools that make a document in a script: a chart as an SVG file, and a PDF from markdown. */
|
|
375
|
+
export function createDocumentTools(deps) {
|
|
376
|
+
return [createRenderChartTool(deps), createExportPdfTool(deps)];
|
|
377
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 that a script read.
|
|
13
|
+
*
|
|
14
|
+
* A recovery replays the cached result of a script and runs no body, thus a replay never counts again. The
|
|
15
|
+
* instruments are made on the first use, thus they bind to whatever provider the host registers.
|
|
16
|
+
*/
|
|
17
|
+
export type ScriptFailure = "none" | "script" | "memory" | "timeout" | "aborted" | "sandbox";
|
|
18
|
+
export type ReadRefusal = "too_large" | "unsupported_format" | "unparsable";
|
|
19
|
+
export declare function recordScriptRun(agentId: string, failure: ScriptFailure): void;
|
|
20
|
+
export declare function recordReadRefusal(reason: ReadRefusal): void;
|
|
21
|
+
export declare function recordReadBytes(bytes: number): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
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 that a script read.
|
|
13
|
+
*
|
|
14
|
+
* A recovery replays the cached result of a script and runs no body, thus a replay never counts again. The
|
|
15
|
+
* instruments are made on the first use, thus they bind to whatever provider the host registers.
|
|
16
|
+
*/
|
|
17
|
+
import { metrics } from "@opentelemetry/api";
|
|
18
|
+
let instruments;
|
|
19
|
+
function getInstruments() {
|
|
20
|
+
if (instruments === undefined) {
|
|
21
|
+
const meter = metrics.getMeter("cortex.harness.script");
|
|
22
|
+
instruments = {
|
|
23
|
+
runs: meter.createCounter("cortex.harness.script.runs", { description: "Scripts that run_script ran" }),
|
|
24
|
+
readRefusals: meter.createCounter("cortex.harness.script.read_refusals", { description: "Table reads of a script that a script tool refused" }),
|
|
25
|
+
readBytes: meter.createHistogram("cortex.harness.script.read_bytes", { description: "Bytes of each table that a script read", unit: "By" }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return instruments;
|
|
29
|
+
}
|
|
30
|
+
export function recordScriptRun(agentId, failure) {
|
|
31
|
+
getInstruments().runs.add(1, { agent_id: agentId, outcome: failure === "none" ? "ok" : "failed", failure });
|
|
32
|
+
}
|
|
33
|
+
export function recordReadRefusal(reason) {
|
|
34
|
+
getInstruments().readRefusals.add(1, { reason });
|
|
35
|
+
}
|
|
36
|
+
export function recordReadBytes(bytes) {
|
|
37
|
+
getInstruments().readBytes.record(bytes);
|
|
38
|
+
}
|
|
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 {};
|