@inflexa-ai/harness 0.46.2 → 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/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/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/tool-name-repair.d.ts +11 -0
- package/dist/loop/tool-name-repair.js +23 -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/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/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
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The derivation tool of a report session.
|
|
3
|
-
*
|
|
4
|
-
* A run writes statistical tables, and not plot-ready ones. This tool reshapes the pinned evidence into one
|
|
5
|
-
* table that a block can bind: a join, a pivot, or an aggregate. The agent writes the script, and the record
|
|
6
|
-
* chains the derived table back to the evidence that made it.
|
|
7
|
-
*
|
|
8
|
-
* A session derivation is not an analysis run. It mints no run id, no mandate, and no artifact, and it writes
|
|
9
|
-
* under the session directory alone. The rails are the rails of the value tier (`tasks/extract-values.ts`):
|
|
10
|
-
* one ephemeral container inside the step of the call (`sandbox/step-sandbox.ts`), which goes away with the
|
|
11
|
-
* work. The container stays until the record lands, thus a rerun of the step before the record adopts the
|
|
12
|
-
* container and the exec record of the first attempt, and the script does not run again. A rerun after the
|
|
13
|
-
* record finds the record of its call, and it gives that table back.
|
|
14
|
-
*
|
|
15
|
-
* The container mounts the analysis tree read-only, and one write mount covers the `derived/` directory of
|
|
16
|
-
* the session alone. The script writes its table into that mount directly, thus a large table makes no
|
|
17
|
-
* roundtrip and no write of the script can reach the workspace tree. The working directory of the exec is
|
|
18
|
-
* that same mount, and the standard output carries the logs of the script alone.
|
|
19
|
-
*
|
|
20
|
-
* The host then reads the file that the container wrote. A path inside the mount is still untrusted bytes: a
|
|
21
|
-
* script can leave a symbolic link in place of its output. Thus the host classifies the file against the
|
|
22
|
-
* workspace root before it hashes one byte, and an escape refuses as typed data.
|
|
23
|
-
*
|
|
24
|
-
* Each declared input must sit in the served membership, and its hash comes from there. The declaration is
|
|
25
|
-
* what the record pins, thus a derived cell traces to a pinned artifact and to the script that read it.
|
|
26
|
-
*
|
|
27
|
-
* The tool refuses an output name that the membership already holds. A record is immutable, and a second
|
|
28
|
-
* derivation of one table takes a new name.
|
|
29
|
-
*
|
|
30
|
-
* A composition with no sandbox client cannot derive at all. The tool reports that condition one time, up
|
|
31
|
-
* front, the same discipline as the eyes. A per-attempt failure would instead read as a transient fault, and
|
|
32
|
-
* it would invite a repeat of a call that can never pass.
|
|
33
|
-
*/
|
|
34
|
-
import { ok } from "neverthrow";
|
|
35
|
-
import { join } from "node:path";
|
|
36
|
-
import { z } from "zod";
|
|
37
|
-
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
38
|
-
import { classifyWithinRoot, computeSha256, computeSha256File } from "../../lib/fs-helpers.js";
|
|
39
|
-
import { defaultErrorFields } from "../../lib/logger.js";
|
|
40
|
-
import { snapshotEntry } from "../../report-model/reference-resolver.js";
|
|
41
|
-
import { isWorkflowCancellation } from "../../runtime/dbos.js";
|
|
42
|
-
import { passSpawnSession, withStepSandbox } from "../../sandbox/step-sandbox.js";
|
|
43
|
-
import { isSafeId, reportSessionDerivedDir, toSandboxPath } from "../../workspace/paths.js";
|
|
44
|
-
import { defineTool } from "../define-tool.js";
|
|
45
|
-
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
46
|
-
import { bindSessionEmit } from "../../provenance/seam.js";
|
|
47
|
-
import { readHeaderColumns } from "./list-artifacts.js";
|
|
48
|
-
/** The input of one derivation: the script, the declared pinned inputs, and the name of the output file. */
|
|
49
|
-
const deriveTableInput = z.object({
|
|
50
|
-
script: z.string().min(1),
|
|
51
|
-
inputs: z.array(z.string()).min(1),
|
|
52
|
-
output: z.string().min(1),
|
|
53
|
-
});
|
|
54
|
-
/** The synthetic run id and step id of one derivation. Both are constants, and neither names a run row. */
|
|
55
|
-
const DERIVE_RUN_LITERAL = "derive-table";
|
|
56
|
-
const DERIVE_STEP_LITERAL = "derive";
|
|
57
|
-
/** The provenance agent id. No agent loop runs in the container, thus this names the derivation pass. */
|
|
58
|
-
const DERIVE_AGENT_ID = "table-deriver";
|
|
59
|
-
/** The exec budget of one derivation. It matches the budget of the value tier. */
|
|
60
|
-
const DERIVATION_DEADLINE_MS = 300_000;
|
|
61
|
-
/**
|
|
62
|
-
* The container size of one derivation. A join or a pivot loads each declared input into pandas at one
|
|
63
|
-
* time, thus the container needs the headroom of the value tier.
|
|
64
|
-
*/
|
|
65
|
-
const DERIVATION_RESOURCES = { cpu: 2, memoryGb: 8 };
|
|
66
|
-
/** The cap of the script, in bytes. A reshaping script sits far under it, and the command line carries it. */
|
|
67
|
-
const SCRIPT_CAP_BYTES = 64 * 1024;
|
|
68
|
-
/** The cap of the declared inputs. One derivation reads the evidence of a few artifacts, and never a tree. */
|
|
69
|
-
const INPUT_CAP = 20;
|
|
70
|
-
/** The environment variable that carries the declared inputs into the container. */
|
|
71
|
-
export const DERIVE_INPUT_ENV = "DERIVE_INPUTS";
|
|
72
|
-
/** The environment variable that carries the container path of the output file into the container. */
|
|
73
|
-
export const DERIVE_OUTPUT_ENV = "DERIVE_OUTPUT";
|
|
74
|
-
/**
|
|
75
|
-
* The shell of the exec: clear the output path, then run the script. The script rides as the first
|
|
76
|
-
* positional argument, thus `python3 -c` gets it byte for byte.
|
|
77
|
-
*
|
|
78
|
-
* The clearance belongs to the exec, and not to the host. A failed derivation can leave a file at the name
|
|
79
|
-
* that this one declares, and a script that writes nothing must not adopt those bytes as its table.
|
|
80
|
-
* sandbox-server runs one exec id one time, thus a rerun that attaches to the exec of an earlier attempt
|
|
81
|
-
* keeps the table of that exec. A clearance on the host would remove it.
|
|
82
|
-
*/
|
|
83
|
-
const CLEAR_THEN_RUN = `rm -f -- "$${DERIVE_OUTPUT_ENV}" && exec python3 -c "$1"`;
|
|
84
|
-
/** The cap of the failure detail that the tool copies out of the standard error of the script. */
|
|
85
|
-
const STDERR_TAIL_CAP = 2_000;
|
|
86
|
-
/** The line that the agent reads when the composition binds no sandbox rails. */
|
|
87
|
-
const NO_SANDBOX_DETAIL = "the composition gives no sandbox, thus this session cannot derive a table";
|
|
88
|
-
/**
|
|
89
|
-
* Build the sandbox exec of one derivation. The command clears the output path, and then it runs the script
|
|
90
|
-
* of the agent through `python3 -c`.
|
|
91
|
-
*
|
|
92
|
-
* The working directory is the write mount, thus the script writes its table beside where it stands. Each
|
|
93
|
-
* declared input rides as its mounted path, thus the script opens the file wherever it works from. The
|
|
94
|
-
* output path rides too, thus the script names no directory of its own.
|
|
95
|
-
*
|
|
96
|
-
* The function is pure, thus a test asserts the command shape without a sandbox.
|
|
97
|
-
*/
|
|
98
|
-
export function buildDerivationExec(args) {
|
|
99
|
-
return {
|
|
100
|
-
command: ["sh", "-c", CLEAR_THEN_RUN, "derive_table", args.script],
|
|
101
|
-
cwd: args.workingDir,
|
|
102
|
-
env: {
|
|
103
|
-
[DERIVE_INPUT_ENV]: JSON.stringify(args.inputs.map((input) => ({ path: input.path, hash: input.hash }))),
|
|
104
|
-
[DERIVE_OUTPUT_ENV]: args.output,
|
|
105
|
-
},
|
|
106
|
-
timeoutSeconds: Math.floor(DERIVATION_DEADLINE_MS / 1000),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Give the failure of an exec result, or `undefined` when the script ran to a clean exit.
|
|
111
|
-
*
|
|
112
|
-
* A synthetic failure, a timeout, and a non-zero exit each mean that no table came back. The detail of a
|
|
113
|
-
* non-zero exit carries the tail of the standard error, thus the agent repairs the script from what the
|
|
114
|
-
* container reported.
|
|
115
|
-
*
|
|
116
|
-
* The function is pure, thus a test asserts the mapping without a sandbox.
|
|
117
|
-
*/
|
|
118
|
-
export function describeExecFailure(result) {
|
|
119
|
-
if (result.syntheticFailure !== undefined) {
|
|
120
|
-
return `the derivation sandbox failed: ${result.syntheticFailure.reason}`;
|
|
121
|
-
}
|
|
122
|
-
if (result.timedOut) {
|
|
123
|
-
return "the derivation reached the deadline";
|
|
124
|
-
}
|
|
125
|
-
if (result.exitCode !== 0) {
|
|
126
|
-
const tail = result.stderr.slice(-STDERR_TAIL_CAP).trim();
|
|
127
|
-
const exit = `the script exited with code ${String(result.exitCode)}`;
|
|
128
|
-
return tail.length === 0 ? exit : `${exit}: ${tail}`;
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Make the derivation tool over the session-state gateway, the derivation ledger, and the sandbox rails.
|
|
134
|
-
*
|
|
135
|
-
* The tool reads the thread id from the scope of the call, and it loads the membership through the gateway.
|
|
136
|
-
* Thus one factory serves every thread, and the tool holds no per-session value.
|
|
137
|
-
*/
|
|
138
|
-
export function createDeriveTableTool(deps) {
|
|
139
|
-
const logger = (deps.logger ?? createNoopLogger()).named("derive-table");
|
|
140
|
-
const observe = bindSessionEmit(deps.provenance, logger);
|
|
141
|
-
const observeDerived = (analysisId, threadId, result) => {
|
|
142
|
-
if (result.outcome !== "derived")
|
|
143
|
-
return;
|
|
144
|
-
observe({
|
|
145
|
-
type: "run-derivation",
|
|
146
|
-
analysisId,
|
|
147
|
-
threadId,
|
|
148
|
-
outputPath: result.path,
|
|
149
|
-
outputHash: result.hash,
|
|
150
|
-
scriptHash: result.scriptHash,
|
|
151
|
-
sources: result.sources,
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
const { sandboxClient } = deps;
|
|
155
|
-
return defineTool({
|
|
156
|
-
id: "derive_table",
|
|
157
|
-
description: "Derive one table from the pinned evidence with a Python script, and pin the result to this session. " +
|
|
158
|
-
"Reach for it when a real reshaping stands between the evidence and the block: a join of two tables, a pivot, or an aggregate. " +
|
|
159
|
-
"A per-row transform is not a derivation, because a chart block reads the column that it needs. " +
|
|
160
|
-
"Name each pinned path that the script reads in `inputs`: each one must sit in the pinned evidence, and the record pins its hash. " +
|
|
161
|
-
'Give the file name of the result in "output": one name, with no directory and no separator, for example "yield_by_group.csv". ' +
|
|
162
|
-
`The script reads the ${DERIVE_INPUT_ENV} environment variable for its inputs: a JSON list that gives the mounted path and the hash of each declared one. ` +
|
|
163
|
-
`It writes the whole table as CSV to the path in the ${DERIVE_OUTPUT_ENV} environment variable, which sits in the working directory of the script. ` +
|
|
164
|
-
"That directory is the one place that the script can write: the analysis mounts read-only, and the container has no network. " +
|
|
165
|
-
"The standard output carries the logs of the script alone, thus a table that it prints there reaches nobody. " +
|
|
166
|
-
"The derived table joins the pinned evidence of this session, thus a block binds its path the same way as a pinned artifact. " +
|
|
167
|
-
"A name that this session already derived is refused, thus a second derivation of one table takes a new name. " +
|
|
168
|
-
`Limits: the script holds at most ${String(SCRIPT_CAP_BYTES / 1024)} KiB, inputs holds at most ${String(INPUT_CAP)} paths, ` +
|
|
169
|
-
`and the run stops after ${String(DERIVATION_DEADLINE_MS / 1000)} seconds. ` +
|
|
170
|
-
"A failed script gives back exec-failed with the tail of its standard error. " +
|
|
171
|
-
"The outcome unavailable means that this host cannot run a derivation, and a repeat gives the same outcome.",
|
|
172
|
-
inputSchema: deriveTableInput,
|
|
173
|
-
describeCall: (input) => `derive ${input.output}`,
|
|
174
|
-
// The derived path is the one durable product of the call, thus the derived arm names it. Each other
|
|
175
|
-
// arm is a condition that refused, and the kind of the arm is what a watcher must read.
|
|
176
|
-
describeResult: (_input, result) => (result.outcome === "derived" ? result.path : result.outcome),
|
|
177
|
-
execute: async (input, ctx) => {
|
|
178
|
-
// The check runs before every read, thus one clear signal replaces a failure for each attempt.
|
|
179
|
-
// It also narrows the rail below, thus the exec needs no assertion.
|
|
180
|
-
if (sandboxClient === undefined) {
|
|
181
|
-
logger.warn("the composition gives no sandbox, thus no derivation can run");
|
|
182
|
-
return ok({ outcome: "unavailable", detail: NO_SANDBOX_DETAIL });
|
|
183
|
-
}
|
|
184
|
-
const opened = await openReportThread(deps.gateway, ctx.session.scope);
|
|
185
|
-
if (opened.isErr()) {
|
|
186
|
-
return ok({ outcome: "refused", refusal: opened.error });
|
|
187
|
-
}
|
|
188
|
-
const { threadId, analysisId, state, derivations } = opened.value;
|
|
189
|
-
const bytes = Buffer.byteLength(input.script, "utf8");
|
|
190
|
-
if (bytes > SCRIPT_CAP_BYTES) {
|
|
191
|
-
return ok({
|
|
192
|
-
outcome: "script-too-large",
|
|
193
|
-
bytes,
|
|
194
|
-
cap: SCRIPT_CAP_BYTES,
|
|
195
|
-
detail: `the script is ${String(bytes)} bytes, and the cap is ${String(SCRIPT_CAP_BYTES)} bytes`,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
// A repeated path names one source, thus the list keeps the first occurrence and the record pins
|
|
199
|
-
// each source one time.
|
|
200
|
-
const declared = [...new Set(input.inputs)];
|
|
201
|
-
if (declared.length > INPUT_CAP) {
|
|
202
|
-
return ok({
|
|
203
|
-
outcome: "too-many-inputs",
|
|
204
|
-
count: declared.length,
|
|
205
|
-
cap: INPUT_CAP,
|
|
206
|
-
detail: `the call declares ${String(declared.length)} inputs, and the cap is ${String(INPUT_CAP)}`,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
if (!isSafeId(input.output)) {
|
|
210
|
-
return ok({
|
|
211
|
-
outcome: "unsafe-name",
|
|
212
|
-
detail: "the output names one file, with no directory, no separator, and no traversal",
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
// Each declared input takes its hash from the served membership. A path that the membership does
|
|
216
|
-
// not hold has no pinned hash, thus the record could chain nothing to it.
|
|
217
|
-
const sources = [];
|
|
218
|
-
for (const path of declared) {
|
|
219
|
-
const entry = snapshotEntry(state.snapshot, path);
|
|
220
|
-
if (entry === undefined) {
|
|
221
|
-
return ok({
|
|
222
|
-
outcome: "absent-input",
|
|
223
|
-
path,
|
|
224
|
-
detail: `the pinned evidence of this session holds no artifact at ${path}`,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
sources.push({ path, hash: entry.hash });
|
|
228
|
-
}
|
|
229
|
-
// The write tail is the one writable mount of the container, and the output sits inside it. Both
|
|
230
|
-
// compose from the session-directory builder, thus the layout has one owner.
|
|
231
|
-
const writableTail = reportSessionDerivedDir(threadId);
|
|
232
|
-
const outputPath = `${writableTail}/${input.output}`;
|
|
233
|
-
let root;
|
|
234
|
-
try {
|
|
235
|
-
root = await deps.resolveWorkspaceRoot(analysisId);
|
|
236
|
-
}
|
|
237
|
-
catch (cause) {
|
|
238
|
-
logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
239
|
-
return ok({ outcome: "unavailable", detail: "the workspace root did not resolve, thus the derived table has no place to land" });
|
|
240
|
-
}
|
|
241
|
-
// A rerun of a call whose record landed gives that record back. The membership holds the output of
|
|
242
|
-
// the record by now, thus the name test below would refuse the call its own table.
|
|
243
|
-
const recorded = derivations.find((record) => record.toolCallId === ctx.invocationId);
|
|
244
|
-
if (recorded !== undefined) {
|
|
245
|
-
const replayed = {
|
|
246
|
-
outcome: "derived",
|
|
247
|
-
path: recorded.outputPath,
|
|
248
|
-
hash: recorded.outputHash,
|
|
249
|
-
scriptHash: recorded.scriptHash,
|
|
250
|
-
sources: [...recorded.sources],
|
|
251
|
-
columns: await readColumnsQuietly(outputPath, join(root, outputPath), logger),
|
|
252
|
-
};
|
|
253
|
-
// A crash between the record and the emit of the earlier run loses its event, thus the rerun
|
|
254
|
-
// emits again. A consumer can get the act two times, never zero times.
|
|
255
|
-
observeDerived(analysisId, threadId, replayed);
|
|
256
|
-
return ok(replayed);
|
|
257
|
-
}
|
|
258
|
-
// The served membership carries each derivation of the session, thus this one test refuses a
|
|
259
|
-
// repeated name and a collision with a pinned artifact alike.
|
|
260
|
-
if (snapshotEntry(state.snapshot, outputPath) !== undefined) {
|
|
261
|
-
return ok({
|
|
262
|
-
outcome: "repeated-name",
|
|
263
|
-
outputPath,
|
|
264
|
-
detail: `the evidence of this session already holds ${outputPath}, thus this derivation takes a new name`,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
const result = await derive({
|
|
268
|
-
sandboxClient,
|
|
269
|
-
session: passSpawnSession(ctx.session, {
|
|
270
|
-
analysisId,
|
|
271
|
-
runId: DERIVE_RUN_LITERAL,
|
|
272
|
-
stepId: DERIVE_STEP_LITERAL,
|
|
273
|
-
agentId: DERIVE_AGENT_ID,
|
|
274
|
-
}),
|
|
275
|
-
derivations: deps.derivations,
|
|
276
|
-
root,
|
|
277
|
-
threadId,
|
|
278
|
-
analysisId,
|
|
279
|
-
toolCallId: ctx.invocationId,
|
|
280
|
-
script: input.script,
|
|
281
|
-
sources,
|
|
282
|
-
writableTail,
|
|
283
|
-
outputPath,
|
|
284
|
-
outputName: input.output,
|
|
285
|
-
logger,
|
|
286
|
-
});
|
|
287
|
-
// The record of the derivation lands before this point, thus the event states a table that the
|
|
288
|
-
// session already holds as evidence. Each other arm derived nothing.
|
|
289
|
-
observeDerived(analysisId, threadId, result);
|
|
290
|
-
return ok(result);
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Run one derivation and record what the script wrote.
|
|
296
|
-
*
|
|
297
|
-
* The order is the order of the evidence. The script writes into the write mount, the host classifies the
|
|
298
|
-
* file, the hash comes off the disk, and the record lands last. Thus the recorded hash is the hash of the
|
|
299
|
-
* file that a verifier reads. The container goes away after the record.
|
|
300
|
-
*
|
|
301
|
-
* The classification is the guard of the read. A script can leave a symbolic link at the output name, and a
|
|
302
|
-
* link that resolves outside the tree would otherwise carry a host file into the evidence of the session.
|
|
303
|
-
*/
|
|
304
|
-
async function derive(args) {
|
|
305
|
-
const absolute = join(args.root, args.outputPath);
|
|
306
|
-
// The container sees the tree at its own mount point, thus each path that the script reads maps through
|
|
307
|
-
// the one host-to-container mapper and never through a formula here. The mapper refuses a stored path
|
|
308
|
-
// that escapes the root with a throw. It runs before the try, thus that refusal stays a throw and it never
|
|
309
|
-
// reads as a fault of the container.
|
|
310
|
-
const request = buildDerivationExec({
|
|
311
|
-
script: args.script,
|
|
312
|
-
workingDir: toSandboxPath(args.root, args.analysisId, join(args.root, args.writableTail)),
|
|
313
|
-
inputs: args.sources.map((source) => ({ path: toSandboxPath(args.root, args.analysisId, join(args.root, source.path)), hash: source.hash })),
|
|
314
|
-
output: toSandboxPath(args.root, args.analysisId, absolute),
|
|
315
|
-
});
|
|
316
|
-
let ran;
|
|
317
|
-
try {
|
|
318
|
-
ran = await withStepSandbox(args.sandboxClient, { session: args.session, spec: { resources: DERIVATION_RESOURCES, writableTail: args.writableTail } }, args.logger, async (exec) => {
|
|
319
|
-
const failure = describeExecFailure(await exec(request, DERIVATION_DEADLINE_MS));
|
|
320
|
-
if (failure !== undefined) {
|
|
321
|
-
return { outcome: "exec-failed", detail: failure };
|
|
322
|
-
}
|
|
323
|
-
return recordDerivedTable({ ...args, absolute });
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
catch (cause) {
|
|
327
|
-
if (isWorkflowCancellation(cause))
|
|
328
|
-
throw cause;
|
|
329
|
-
args.logger.error("the derivation exec did not complete", { threadId: args.threadId, ...defaultErrorFields(cause) });
|
|
330
|
-
return { outcome: "exec-failed", detail: "the derivation exec did not complete" };
|
|
331
|
-
}
|
|
332
|
-
if (ran.isErr()) {
|
|
333
|
-
return {
|
|
334
|
-
outcome: "suspended",
|
|
335
|
-
reason: ran.error.reason,
|
|
336
|
-
detail: `the host refused the sandbox (${ran.error.reason}), thus the derivation did not run`,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
return ran.value;
|
|
340
|
-
}
|
|
341
|
-
/** Classify, hash, and record the table that a green script wrote. */
|
|
342
|
-
async function recordDerivedTable(args) {
|
|
343
|
-
const verdict = await classifyWithinRoot(args.root, args.absolute).catch((cause) => {
|
|
344
|
-
args.logger.error("the derived table did not classify", { threadId: args.threadId, ...defaultErrorFields(cause) });
|
|
345
|
-
return "escaped";
|
|
346
|
-
});
|
|
347
|
-
if (verdict === "absent") {
|
|
348
|
-
// A clean exit with no file at the output path means that the script wrote nothing, or that it wrote
|
|
349
|
-
// under a different name inside the mount. Either way this derivation has no table.
|
|
350
|
-
return {
|
|
351
|
-
outcome: "no-output",
|
|
352
|
-
detail: `the script ran to a clean exit and it wrote no file at ${args.outputName}`,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
if (verdict === "escaped") {
|
|
356
|
-
args.logger.error("the derived table resolves outside the workspace root", { threadId: args.threadId, path: args.outputPath });
|
|
357
|
-
return { outcome: "exec-failed", detail: `${args.outputName} resolves outside the workspace tree, thus it is not evidence of this session` };
|
|
358
|
-
}
|
|
359
|
-
let outputHash;
|
|
360
|
-
try {
|
|
361
|
-
outputHash = await computeSha256File(args.absolute);
|
|
362
|
-
}
|
|
363
|
-
catch (cause) {
|
|
364
|
-
args.logger.error("the derived table did not hash", { threadId: args.threadId, ...defaultErrorFields(cause) });
|
|
365
|
-
return { outcome: "exec-failed", detail: "the derived table did not hash" };
|
|
366
|
-
}
|
|
367
|
-
const record = {
|
|
368
|
-
outputPath: args.outputPath,
|
|
369
|
-
outputHash,
|
|
370
|
-
sources: [...args.sources],
|
|
371
|
-
scriptHash: computeSha256(Buffer.from(args.script, "utf8")),
|
|
372
|
-
script: args.script,
|
|
373
|
-
toolCallId: args.toolCallId,
|
|
374
|
-
};
|
|
375
|
-
const appended = await args.derivations.appendDerivation(args.threadId, record);
|
|
376
|
-
if (appended.isErr()) {
|
|
377
|
-
args.logger.error("the derivation record did not land", { threadId: args.threadId, ...args.logger.errorFields(appended.error.cause) });
|
|
378
|
-
return { outcome: "exec-failed", detail: "the derivation record did not land, thus the table is not evidence of this session" };
|
|
379
|
-
}
|
|
380
|
-
if (appended.value === "duplicate") {
|
|
381
|
-
// The membership test above already refused a repeated name. A duplicate here means that another
|
|
382
|
-
// turn landed the same name inside this call, thus the name rule of the store is the last word.
|
|
383
|
-
return {
|
|
384
|
-
outcome: "repeated-name",
|
|
385
|
-
outputPath: args.outputPath,
|
|
386
|
-
detail: `another turn recorded ${args.outputPath} first, thus this derivation takes a new name`,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
if (appended.value === "absent") {
|
|
390
|
-
args.logger.error("the derivation record matched no session row", { threadId: args.threadId });
|
|
391
|
-
return { outcome: "exec-failed", detail: "no report session state row exists to hold the derivation" };
|
|
392
|
-
}
|
|
393
|
-
// The columns describe the derived table the same way as the listing describes a pinned one. A file that
|
|
394
|
-
// gives no header gives no columns, and absence is a normal condition here.
|
|
395
|
-
const columns = await readColumnsQuietly(args.outputPath, args.absolute, args.logger);
|
|
396
|
-
return {
|
|
397
|
-
outcome: "derived",
|
|
398
|
-
path: args.outputPath,
|
|
399
|
-
hash: outputHash,
|
|
400
|
-
scriptHash: record.scriptHash,
|
|
401
|
-
sources: [...args.sources],
|
|
402
|
-
columns,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* The header columns of the derived table, or `undefined` when the read gives none.
|
|
407
|
-
*
|
|
408
|
-
* The derivation already landed at this point, thus a read fault costs the columns alone. The read never
|
|
409
|
-
* throws for an absent file, and this guard covers a genuine fault of the disk.
|
|
410
|
-
*/
|
|
411
|
-
async function readColumnsQuietly(path, absolute, logger) {
|
|
412
|
-
try {
|
|
413
|
-
return await readHeaderColumns(path, absolute);
|
|
414
|
-
}
|
|
415
|
-
catch (cause) {
|
|
416
|
-
logger.warn("the derived table gave no columns", { path, ...defaultErrorFields(cause) });
|
|
417
|
-
return undefined;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* literatureReviewer — a sub-agent packaged as a loop-driving tool.
|
|
3
|
-
*
|
|
4
|
-
* The conversation agent delegates batch literature/biology research by
|
|
5
|
-
* calling this tool. `execute` derives a child `Session` (`forSubAgent`)
|
|
6
|
-
* and runs `runLoop` over a focused literature-reviewer agent: a research
|
|
7
|
-
* brief in, a structured evidence report out. The child's transcript is
|
|
8
|
-
* ephemeral — it lives only for this call and is never persisted (see the harness-thread-store spec).
|
|
9
|
-
*
|
|
10
|
-
* This is the whole of sub-agent delegation in the harness. There is no
|
|
11
|
-
* `agents: {}` config, no `messageFilter`, no `stripParentToolParts`: the
|
|
12
|
-
* child cannot see the parent's tool history because it is simply handed
|
|
13
|
-
* none — only the brief. Inside the body of a workflow the call starts a
|
|
14
|
-
* child workflow (`loop/sub-agent.ts`), thus each call of the child is a step
|
|
15
|
-
* of its own.
|
|
16
|
-
*/
|
|
17
|
-
import type { SubAgentRegistrar } from "../../loop/sub-agent.js";
|
|
18
|
-
import type { ToolOutputStore } from "../../loop/tool-output.js";
|
|
19
|
-
import { type ChatProvider } from "../../providers/types.js";
|
|
20
|
-
import { type Tool } from "../define-tool.js";
|
|
21
|
-
import { type BioToolKeys } from "../bio/keys.js";
|
|
22
|
-
import type { Logger } from "../../lib/logger.js";
|
|
23
|
-
import type { UsageRecorder } from "../../billing/usage-recorder.js";
|
|
24
|
-
import type { CitationResolver } from "../../citations/types.js";
|
|
25
|
-
export type LiteratureReviewerDeps = {
|
|
26
|
-
/** Operational logging seam; omitted falls back to no-op. */
|
|
27
|
-
readonly logger?: Logger;
|
|
28
|
-
/** The LLM seam the child loop runs on. */
|
|
29
|
-
readonly provider: ChatProvider;
|
|
30
|
-
/** Model id — provenance / metric label; the provider owns the wire model. */
|
|
31
|
-
readonly model: string;
|
|
32
|
-
/** API keys for the bio/chem data sources the reviewer searches. */
|
|
33
|
-
readonly bioKeys: BioToolKeys;
|
|
34
|
-
/** LLM usage-accounting seam for the child loop; omitted falls back to the no-op recorder. */
|
|
35
|
-
readonly usageRecorder?: UsageRecorder;
|
|
36
|
-
/** The store of the child loop and of its read tool. */
|
|
37
|
-
readonly toolOutputStore?: ToolOutputStore;
|
|
38
|
-
/** Shared bibliographic verification service; all agent surfaces share its pacing/cache. */
|
|
39
|
-
readonly citationResolver: CitationResolver;
|
|
40
|
-
};
|
|
41
|
-
/** Build the `literature_reviewer` delegation tool bound to its provider. */
|
|
42
|
-
export declare function createLiteratureReviewerTool(deps: LiteratureReviewerDeps, registerSubAgent: SubAgentRegistrar): Tool;
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* literatureReviewer — a sub-agent packaged as a loop-driving tool.
|
|
3
|
-
*
|
|
4
|
-
* The conversation agent delegates batch literature/biology research by
|
|
5
|
-
* calling this tool. `execute` derives a child `Session` (`forSubAgent`)
|
|
6
|
-
* and runs `runLoop` over a focused literature-reviewer agent: a research
|
|
7
|
-
* brief in, a structured evidence report out. The child's transcript is
|
|
8
|
-
* ephemeral — it lives only for this call and is never persisted (see the harness-thread-store spec).
|
|
9
|
-
*
|
|
10
|
-
* This is the whole of sub-agent delegation in the harness. There is no
|
|
11
|
-
* `agents: {}` config, no `messageFilter`, no `stripParentToolParts`: the
|
|
12
|
-
* child cannot see the parent's tool history because it is simply handed
|
|
13
|
-
* none — only the brief. Inside the body of a workflow the call starts a
|
|
14
|
-
* child workflow (`loop/sub-agent.ts`), thus each call of the child is a step
|
|
15
|
-
* of its own.
|
|
16
|
-
*/
|
|
17
|
-
import { err, ok } from "neverthrow";
|
|
18
|
-
import { z } from "zod";
|
|
19
|
-
import { finalText, runLoop } from "../../loop/run-loop.js";
|
|
20
|
-
import { literatureReviewerPrompt } from "../../prompts/literature-reviewer.js";
|
|
21
|
-
import { composeSystemPrompt } from "../../agents/system-prompt.js";
|
|
22
|
-
import { forSubAgent } from "../../auth/types.js";
|
|
23
|
-
import { defineTool } from "../define-tool.js";
|
|
24
|
-
import { createReadToolOutputTool } from "../read-tool-output.js";
|
|
25
|
-
import { createChemDbTools, createNcbiTools } from "../bio/keys.js";
|
|
26
|
-
import { genePreclinicalProfileTool } from "../bio/gene-preclinical-profile.js";
|
|
27
|
-
import { lookupAnnotationTool } from "../bio/lookup-annotation.js";
|
|
28
|
-
import { searchGeneTool } from "../bio/search-gene.js";
|
|
29
|
-
import { searchProteinTool } from "../bio/search-protein.js";
|
|
30
|
-
import { searchInteractionsTool } from "../bio/search-interactions.js";
|
|
31
|
-
import { createResolveCitationTool } from "./resolve-citation.js";
|
|
32
|
-
/** Sub-agent identity — appended to `callPath`, set as `agentId`. */
|
|
33
|
-
const AGENT_ID = "literature-reviewer";
|
|
34
|
-
/** Multi-tool research budget — search → read → synthesize over many genes. */
|
|
35
|
-
const REVIEWER_MAX_ITERATIONS = 30;
|
|
36
|
-
/** Build the `literature_reviewer` delegation tool bound to its provider. */
|
|
37
|
-
export function createLiteratureReviewerTool(deps, registerSubAgent) {
|
|
38
|
-
const ncbi = createNcbiTools(deps.bioKeys);
|
|
39
|
-
const chemDb = createChemDbTools(deps.bioKeys, { logger: deps.logger });
|
|
40
|
-
const reviewerTools = [
|
|
41
|
-
searchGeneTool,
|
|
42
|
-
searchProteinTool,
|
|
43
|
-
lookupAnnotationTool,
|
|
44
|
-
searchInteractionsTool,
|
|
45
|
-
ncbi.pubmed,
|
|
46
|
-
createResolveCitationTool(deps.citationResolver),
|
|
47
|
-
chemDb.drugGeneInteractions,
|
|
48
|
-
genePreclinicalProfileTool,
|
|
49
|
-
...(deps.toolOutputStore ? [createReadToolOutputTool(deps.toolOutputStore)] : []),
|
|
50
|
-
];
|
|
51
|
-
const agent = {
|
|
52
|
-
id: AGENT_ID,
|
|
53
|
-
systemPrompt: composeSystemPrompt(literatureReviewerPrompt),
|
|
54
|
-
model: deps.model,
|
|
55
|
-
tools: reviewerTools,
|
|
56
|
-
maxIterations: REVIEWER_MAX_ITERATIONS,
|
|
57
|
-
};
|
|
58
|
-
// A review ends with a report, or with the finish reason of a run that gave none.
|
|
59
|
-
const review = registerSubAgent(AGENT_ID, async ({ payload, ctx, loop }) => {
|
|
60
|
-
const { messages: transcript, finish } = await runLoop(agent, [{ role: "user", content: payload.brief }], forSubAgent(ctx.session, AGENT_ID), {
|
|
61
|
-
...loop,
|
|
62
|
-
provider: deps.provider,
|
|
63
|
-
usageRecorder: deps.usageRecorder,
|
|
64
|
-
toolOutputStore: deps.toolOutputStore,
|
|
65
|
-
});
|
|
66
|
-
const report = finalText(transcript);
|
|
67
|
-
return report.trim() === "" ? { finishReason: finish.reason } : { report };
|
|
68
|
-
});
|
|
69
|
-
return defineTool({
|
|
70
|
-
id: "literature_reviewer",
|
|
71
|
-
description: "Delegate batch literature/biology research to a sub-agent: looking up " +
|
|
72
|
-
"multiple genes or pathways, validating findings against published " +
|
|
73
|
-
"knowledge, or building evidence profiles. The reviewer holds gene, " +
|
|
74
|
-
"protein, annotation, interaction, PubMed, citation-verification, " +
|
|
75
|
-
"drug-gene and preclinical-profile lookups. It cannot see this " +
|
|
76
|
-
"conversation, the workspace, or the analysis files, thus pass a " +
|
|
77
|
-
"self-contained brief. One call runs a multi-step research loop of up " +
|
|
78
|
-
"to 30 model turns. Returns {report}: a Markdown evidence report with " +
|
|
79
|
-
"an evidence summary per target, cross-target patterns, and key " +
|
|
80
|
-
"references with PMIDs. A run that ends with no report returns a " +
|
|
81
|
-
"retryable error that names the finish reason.",
|
|
82
|
-
inputSchema: z.object({
|
|
83
|
-
brief: z
|
|
84
|
-
.string()
|
|
85
|
-
.min(1)
|
|
86
|
-
.describe("A self-contained research brief: the genes, pathways, or " +
|
|
87
|
-
"biological features to investigate and the experimental " +
|
|
88
|
-
"context. The reviewer sees only this — include everything it needs."),
|
|
89
|
-
}),
|
|
90
|
-
describeCall: "none",
|
|
91
|
-
startsWorkflow: true,
|
|
92
|
-
execute: async ({ brief }, ctx) => {
|
|
93
|
-
const reviewed = await review({ brief }, ctx);
|
|
94
|
-
// A child run can end with no final text. Examples are a terminal
|
|
95
|
-
// reply with no text block, and a run that reached its step cap. An
|
|
96
|
-
// ok result with an empty report hides that failure, so the tool
|
|
97
|
-
// returns an error that names the finish reason.
|
|
98
|
-
if ("finishReason" in reviewed) {
|
|
99
|
-
return err({
|
|
100
|
-
error: `The literature reviewer gave no report (finish reason: ${reviewed.finishReason}). Retry, or narrow the brief.`,
|
|
101
|
-
retryable: true,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
return ok({ report: reviewed.report });
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow-side OTel metrics.
|
|
3
|
-
*
|
|
4
|
-
* - cortex.workflow.parent.cancelled_children — counter, incremented once
|
|
5
|
-
* per child the fail-fast or pause cascade reaps. A baseline near 0
|
|
6
|
-
* means runs land cleanly; a spike means siblings keep colliding with
|
|
7
|
-
* a single failing step.
|
|
8
|
-
*
|
|
9
|
-
* Instruments are lazy so the workflow body binds to whichever
|
|
10
|
-
* `MeterProvider` is globally registered at startup.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Record one child cancellation. The parent calls this once per
|
|
14
|
-
* `DBOS.cancelWorkflow(childWorkflowId)` in the cascade (fail-fast OR
|
|
15
|
-
* suspension). `cause` is `fail_fast`, `external_cancel`, or the reason of
|
|
16
|
-
* the host for a suspension, carried unread, so dashboards can split the
|
|
17
|
-
* counter by the reason that the host gave.
|
|
18
|
-
*/
|
|
19
|
-
export declare function recordCancelledChild(args: {
|
|
20
|
-
readonly cause: string;
|
|
21
|
-
}): void;
|
|
22
|
-
/** Test hook — drop memoised instruments to rebind a fresh MeterProvider. */
|
|
23
|
-
export declare function __resetWorkflowMetricsForTest(): void;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow-side OTel metrics.
|
|
3
|
-
*
|
|
4
|
-
* - cortex.workflow.parent.cancelled_children — counter, incremented once
|
|
5
|
-
* per child the fail-fast or pause cascade reaps. A baseline near 0
|
|
6
|
-
* means runs land cleanly; a spike means siblings keep colliding with
|
|
7
|
-
* a single failing step.
|
|
8
|
-
*
|
|
9
|
-
* Instruments are lazy so the workflow body binds to whichever
|
|
10
|
-
* `MeterProvider` is globally registered at startup.
|
|
11
|
-
*/
|
|
12
|
-
import { metrics } from "@opentelemetry/api";
|
|
13
|
-
let instruments;
|
|
14
|
-
function getInstruments() {
|
|
15
|
-
if (instruments === undefined) {
|
|
16
|
-
const meter = metrics.getMeter("cortex.workflow");
|
|
17
|
-
instruments = {
|
|
18
|
-
cancelledChildren: meter.createCounter("cortex.workflow.parent.cancelled_children", {
|
|
19
|
-
description: "Children cancelled by the parent's fail-fast or suspension cascade",
|
|
20
|
-
}),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
return instruments;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Record one child cancellation. The parent calls this once per
|
|
27
|
-
* `DBOS.cancelWorkflow(childWorkflowId)` in the cascade (fail-fast OR
|
|
28
|
-
* suspension). `cause` is `fail_fast`, `external_cancel`, or the reason of
|
|
29
|
-
* the host for a suspension, carried unread, so dashboards can split the
|
|
30
|
-
* counter by the reason that the host gave.
|
|
31
|
-
*/
|
|
32
|
-
export function recordCancelledChild(args) {
|
|
33
|
-
getInstruments().cancelledChildren.add(1, { cause: args.cause });
|
|
34
|
-
}
|
|
35
|
-
/** Test hook — drop memoised instruments to rebind a fresh MeterProvider. */
|
|
36
|
-
export function __resetWorkflowMetricsForTest() {
|
|
37
|
-
instruments = undefined;
|
|
38
|
-
}
|