@inflexa-ai/harness 0.41.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/prompts/briefing.d.ts +28 -1
  2. package/dist/prompts/briefing.js +44 -1
  3. package/dist/prompts/compaction.d.ts +2 -2
  4. package/dist/prompts/compaction.js +9 -5
  5. package/dist/prompts/sandbox-standards.d.ts +1 -1
  6. package/dist/prompts/sandbox-standards.js +4 -0
  7. package/dist/runtime/assemble.d.ts +4 -2
  8. package/dist/runtime/assemble.js +8 -1
  9. package/dist/schemas/file-metadata.js +9 -9
  10. package/dist/schemas/plan-schemas.d.ts +3 -3
  11. package/dist/tools/ad-hoc-router.js +9 -4
  12. package/dist/tools/bio/chembl.js +8 -5
  13. package/dist/tools/bio/comptox.js +4 -4
  14. package/dist/tools/bio/drug-gene-interactions.js +5 -4
  15. package/dist/tools/bio/gene-disease-evidence.js +4 -4
  16. package/dist/tools/bio/lookup-annotation.js +7 -3
  17. package/dist/tools/bio/opentargets.js +8 -6
  18. package/dist/tools/bio/pubchem.js +4 -2
  19. package/dist/tools/bio/pubmed.d.ts +1 -1
  20. package/dist/tools/bio/pubmed.js +10 -6
  21. package/dist/tools/bio/search-clinical-trials.js +2 -1
  22. package/dist/tools/bio/search-gene.js +2 -1
  23. package/dist/tools/bio/search-interactions.js +8 -4
  24. package/dist/tools/bio/target-safety.js +2 -1
  25. package/dist/tools/display/show-user.js +1 -1
  26. package/dist/tools/execute-analysis.js +14 -4
  27. package/dist/tools/lib/opentargets-client.d.ts +8 -3
  28. package/dist/tools/lib/opentargets-client.js +12 -3
  29. package/dist/tools/read-tool-output.js +7 -4
  30. package/dist/tools/report-authoring/authoring-tools.js +22 -15
  31. package/dist/tools/report-session/derive-table.js +5 -1
  32. package/dist/tools/report-session/examine-page.js +5 -4
  33. package/dist/tools/report-session/preview-report.js +6 -1
  34. package/dist/tools/report-session/record-version.js +2 -1
  35. package/dist/tools/research/context7-docs.js +8 -2
  36. package/dist/tools/research/generate-analogy-report.js +15 -15
  37. package/dist/tools/research/generate-plan.js +12 -7
  38. package/dist/tools/research/inspect-data-profile.js +7 -5
  39. package/dist/tools/research/inspect-run.js +6 -2
  40. package/dist/tools/research/literature-reviewer.js +11 -6
  41. package/dist/tools/research/search-arxiv.js +2 -1
  42. package/dist/tools/research/search-github-repos.js +9 -3
  43. package/dist/tools/sandbox/list-available-refs.js +3 -2
  44. package/dist/tools/sandbox/report-blocker.js +6 -1
  45. package/dist/tools/sandbox/skills.js +4 -2
  46. package/dist/tools/start-report-session.js +4 -1
  47. package/dist/tools/workspace/edit-file.js +4 -3
  48. package/dist/tools/workspace/execute-command.js +6 -5
  49. package/dist/tools/workspace/grep.js +9 -7
  50. package/dist/tools/workspace/list-files.js +1 -1
  51. package/dist/tools/workspace/read-file.js +2 -1
  52. package/dist/tools/workspace/scan-inputs.js +3 -1
  53. package/dist/tools/workspace/show-file.js +2 -0
  54. package/dist/tools/workspace/workspace-search.js +3 -1
  55. package/dist/tools/workspace/write-file.js +5 -3
  56. package/dist/workflows/execute-analysis.d.ts +28 -5
  57. package/dist/workflows/execute-analysis.js +34 -10
  58. package/package.json +1 -1
@@ -21,6 +21,7 @@
21
21
  * field so the field-coverage guard test fails when a new field is added to the
22
22
  * schema without a deliberate decision about whether the agent needs to see it.
23
23
  */
24
+ import { type WorkingMemory } from "../memory/working-memory.js";
24
25
  import type { DataProfileResult } from "../state/data-profile.js";
25
26
  import type { AnalysisStep } from "../schemas/workflow-state.js";
26
27
  /** Completed dependencies rendered as upstream blocks; the tail is counted, not rendered. */
@@ -50,6 +51,30 @@ export declare const STEP_NON_TASK_FIELDS: readonly ["id", "track", "step_type",
50
51
  * improvises the rest.
51
52
  */
52
53
  export declare function renderTask(step: AnalysisStep): string;
54
+ /**
55
+ * The heading of the analysis-memory section. The orient core of the sandbox
56
+ * prompt names this same text, thus a test pins the two together.
57
+ */
58
+ export declare const ANALYSIS_MEMORY_HEADING = "Analysis memory (read only)";
59
+ /**
60
+ * The part of the working memory a step agent sees: the goal and the
61
+ * constraints. Hypotheses and findings stay out — they are the interpretive
62
+ * state of the conversation, not a rule for a step.
63
+ */
64
+ export type StepMemory = Pick<WorkingMemory, "goal" | "constraints">;
65
+ /** A memory with no goal and no constraints — the section renders nothing. */
66
+ export declare function emptyStepMemory(): StepMemory;
67
+ /**
68
+ * The goal and the constraints of the analysis, frozen at dispatch. The heading
69
+ * differs from the "Constraints" section of {@link renderTask}: those are the
70
+ * requirements of this plan step, these are the rules of the whole analysis.
71
+ *
72
+ * Bounded to `WORKING_MEMORY_LIMITS` the same way the chat render is: a row
73
+ * written before the caps existed degrades to its newest constraints, and each
74
+ * text is clamped. No id is rendered, because the step agent cannot address an
75
+ * entry — it has no memory tool.
76
+ */
77
+ export declare function renderMemory(memory: StepMemory): string;
53
78
  /**
54
79
  * What anchors every path the agent writes or reads: the id the tree is mounted
55
80
  * at, and the step's writable directory.
@@ -109,10 +134,12 @@ export interface StepBriefing {
109
134
  readonly profile: DataProfileResult | null;
110
135
  /** The step's completed dependencies, in the plan's declared `depends_on` order. */
111
136
  readonly upstream: readonly UpstreamHandoff[];
137
+ /** The goal and the constraints of the analysis, read from the working memory at dispatch. */
138
+ readonly memory: StepMemory;
112
139
  }
113
140
  /**
114
141
  * Compose the seed. Absent sections collapse out entirely — an independent step
115
- * with no profile yields exactly the task and workspace sections, byte-identical
142
+ * with no profile and an empty memory yields exactly the task and workspace sections, byte-identical
116
143
  * to what a step got before any of this existed plus its paths.
117
144
  *
118
145
  * Pure: the same briefing composes to the same string, which is what makes the
@@ -22,6 +22,7 @@
22
22
  * schema without a deliberate decision about whether the agent needs to see it.
23
23
  */
24
24
  import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../app/data-profile-orientation.js";
25
+ import { WORKING_MEMORY_LIMITS } from "../memory/working-memory.js";
25
26
  // ── Bounds ────────────────────────────────────────────────────────────
26
27
  /** Completed dependencies rendered as upstream blocks; the tail is counted, not rendered. */
27
28
  export const MAX_UPSTREAM_DEPS = 5;
@@ -113,6 +114,47 @@ export function renderTask(step) {
113
114
  }
114
115
  return parts.join("\n\n");
115
116
  }
117
+ // ── (1b) Analysis memory ──────────────────────────────────────────────
118
+ /**
119
+ * The heading of the analysis-memory section. The orient core of the sandbox
120
+ * prompt names this same text, thus a test pins the two together.
121
+ */
122
+ export const ANALYSIS_MEMORY_HEADING = "Analysis memory (read only)";
123
+ /** A memory with no goal and no constraints — the section renders nothing. */
124
+ export function emptyStepMemory() {
125
+ return { goal: "", constraints: [] };
126
+ }
127
+ /**
128
+ * The goal and the constraints of the analysis, frozen at dispatch. The heading
129
+ * differs from the "Constraints" section of {@link renderTask}: those are the
130
+ * requirements of this plan step, these are the rules of the whole analysis.
131
+ *
132
+ * Bounded to `WORKING_MEMORY_LIMITS` the same way the chat render is: a row
133
+ * written before the caps existed degrades to its newest constraints, and each
134
+ * text is clamped. No id is rendered, because the step agent cannot address an
135
+ * entry — it has no memory tool.
136
+ */
137
+ export function renderMemory(memory) {
138
+ const goal = clamp(memory.goal.trim(), WORKING_MEMORY_LIMITS.goalChars);
139
+ const all = memory.constraints.filter((c) => c.text.trim().length > 0);
140
+ const shown = all.slice(Math.max(0, all.length - WORKING_MEMORY_LIMITS.constraints));
141
+ if (goal.length === 0 && shown.length === 0)
142
+ return "";
143
+ const parts = [
144
+ "These entries come from the working memory of the analysis, read when this step was dispatched. " +
145
+ "A constraint from the user is binding. A constraint from the agent is context.",
146
+ ];
147
+ if (goal.length > 0)
148
+ parts.push(`Goal: ${goal}`);
149
+ if (shown.length > 0) {
150
+ const lines = shown.map((c) => `(${c.origin}) ${clamp(c.text.trim(), WORKING_MEMORY_LIMITS.entryChars)}`);
151
+ const omitted = all.length - shown.length;
152
+ if (omitted > 0)
153
+ lines.push(`(+${omitted} older ${omitted === 1 ? "constraint" : "constraints"} not shown)`);
154
+ parts.push(`Constraints:\n${bullets(lines)}`);
155
+ }
156
+ return section(ANALYSIS_MEMORY_HEADING, parts.join("\n\n"));
157
+ }
116
158
  /** The in-sandbox analysis root of a frame — the read-only mount of the whole tree. */
117
159
  export function analysisRootOf(frame) {
118
160
  return `/${frame.analysisId}`;
@@ -190,7 +232,7 @@ function renderHandoff(handoff) {
190
232
  }
191
233
  /**
192
234
  * Compose the seed. Absent sections collapse out entirely — an independent step
193
- * with no profile yields exactly the task and workspace sections, byte-identical
235
+ * with no profile and an empty memory yields exactly the task and workspace sections, byte-identical
194
236
  * to what a step got before any of this existed plus its paths.
195
237
  *
196
238
  * Pure: the same briefing composes to the same string, which is what makes the
@@ -199,6 +241,7 @@ function renderHandoff(handoff) {
199
241
  export function composeStepBriefing(briefing) {
200
242
  return [
201
243
  renderTask(briefing.step),
244
+ renderMemory(briefing.memory),
202
245
  renderWorkspace(briefing.workspace),
203
246
  renderResources(briefing.step.resources),
204
247
  renderOrientation(briefing.profile, briefing.workspace.analysisId),
@@ -1,4 +1,4 @@
1
1
  /** The compaction request of a thread whose agent maintains the working memory. */
2
- export declare const MEMORY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary.\n\nFirst, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. No other tool can run now.\n\nThen reply in plain text with the summary. Leave out what the working memory holds. Give:\n- the goals, and the current request of the user in its exact words\n- the decisions, with their reasons\n- the open questions\n- the file paths and the run ids that the work uses\n- the state of the work in progress\n\nThe conversation continues from your summary, the working memory, and the messages after them.";
2
+ export declare const MEMORY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary.\n\nFirst, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. Keep each constraint that the user gave as it is, because the user set it. No other tool can run now. Send all the memory edits in at most 3 replies, and put many calls in one reply. The reply after the edits must be the summary: if it calls a tool, the harness gets no summary.\n\nThen reply in plain text with the summary. Leave out each fact that a memory edit accepted. If the memory refused an edit, put that fact in the summary. Give:\n- the current request of the user in its exact words\n- the decisions, with their reasons\n- the approaches that failed or that the user ruled out, with the reasons\n- the open questions\n- the file paths, the run ids, and the other exact values that the work uses\n- the state of the work in progress\n\nThe conversation continues from your summary, the working memory, and the messages after them.";
3
3
  /** The compaction request of a thread that reads a frozen copy of the working memory, for example a report thread. */
4
- export declare const SUMMARY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary. No tool can run now.\n\nReply in plain text with the summary. Leave out what the working memory holds. Give:\n- the goals, and the current request of the user in its exact words\n- the decisions, with their reasons\n- the open questions\n- the file paths and the run ids that the work uses\n- the state of the work in progress\n\nThe conversation continues from your summary, the working memory, and the messages after them.";
4
+ export declare const SUMMARY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary. No tool can run now.\n\nReply in plain text with the summary. Leave out what the working memory holds. Give:\n- the goals, and the current request of the user in its exact words\n- each change that the user made to the brief\n- the decisions, with their reasons\n- the approaches that failed or that the user ruled out, with the reasons\n- the open questions\n- the file paths, the run ids, and the other exact values that the work uses\n- the state of the work in progress\n\nThe conversation continues from your summary, the working memory, and the messages after them.";
@@ -1,13 +1,15 @@
1
+ import { COMPACTION_MAX_REQUESTS } from "../loop/compaction.js";
1
2
  /** The compaction request of a thread whose agent maintains the working memory. */
2
3
  export const MEMORY_COMPACTION_REQUEST = `The conversation is too long for the context window. The harness replaces the conversation above with your summary.
3
4
 
4
- First, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. No other tool can run now.
5
+ First, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. Keep each constraint that the user gave as it is, because the user set it. No other tool can run now. Send all the memory edits in at most ${COMPACTION_MAX_REQUESTS - 1} replies, and put many calls in one reply. The reply after the edits must be the summary: if it calls a tool, the harness gets no summary.
5
6
 
6
- Then reply in plain text with the summary. Leave out what the working memory holds. Give:
7
- - the goals, and the current request of the user in its exact words
7
+ Then reply in plain text with the summary. Leave out each fact that a memory edit accepted. If the memory refused an edit, put that fact in the summary. Give:
8
+ - the current request of the user in its exact words
8
9
  - the decisions, with their reasons
10
+ - the approaches that failed or that the user ruled out, with the reasons
9
11
  - the open questions
10
- - the file paths and the run ids that the work uses
12
+ - the file paths, the run ids, and the other exact values that the work uses
11
13
  - the state of the work in progress
12
14
 
13
15
  The conversation continues from your summary, the working memory, and the messages after them.`;
@@ -16,9 +18,11 @@ export const SUMMARY_COMPACTION_REQUEST = `The conversation is too long for the
16
18
 
17
19
  Reply in plain text with the summary. Leave out what the working memory holds. Give:
18
20
  - the goals, and the current request of the user in its exact words
21
+ - each change that the user made to the brief
19
22
  - the decisions, with their reasons
23
+ - the approaches that failed or that the user ruled out, with the reasons
20
24
  - the open questions
21
- - the file paths and the run ids that the work uses
25
+ - the file paths, the run ids, and the other exact values that the work uses
22
26
  - the state of the work in progress
23
27
 
24
28
  The conversation continues from your summary, the working memory, and the messages after them.`;
@@ -29,7 +29,7 @@
29
29
  * each composition stays byte-stable across its own steps.
30
30
  */
31
31
  import type { ToolchainSource } from "../sandbox/types.js";
32
- export declare const sandboxOrientCorePrompt = "# Sandbox Orient-Core\n\nThese apply to every agent that talks to a sandbox.\n\n## Your Briefing Is Authoritative\n\nYour first message is your briefing, composed for this step at the instant it was\ndispatched \u2014 after every step it depends on had finished. It names your task, your\nworking directory and the read-only analysis root, what the input dataset is\n(domain, organism, design, quality concerns, files), and what each upstream step\nproduced and where it put it. Work from it.\n\nDo NOT re-derive what it already gives you: no filesystem hunt for your inputs, no\nre-reading an upstream step whose summary you were handed, no re-deriving the\norganism or the dimensions from raw bytes. Reach further only where the briefing is\ngenuinely thin for what you must do:\n\n- `inspect_data_profile` \u2014 the full dataset profile: `scope:'overview'` for the\n identity and the file census, `scope:'groups'` for the groups and the dimensions\n that vary across them, `scope:'files'` for the members the profiler annotated\n individually. It is the ONLY record of what the input data IS; no file on disk\n carries it. Use it when the briefing's orientation is not enough, or when it\n carried none.\n- `read_file` on an upstream step's summary \u2014 its path is in your briefing \u2014 when\n the excerpt you were handed is not enough to build on.\n- `workspace_search({ query })` \u2014 natural-language search for a file the briefing\n does not name. Optional `type` filter (`input-group` | `input-dimension` |\n `input` | `output` | `summary` | `synthesis`) and `limit` (1-50, default 8).\n Returns paths, descriptions, and metadata \u2014 not file contents. Most input files\n carry no entry of their own; reach them through their group and `list_files`.\n\n## Your Workspace\n\nYour **working directory** \u2014 writable, and your cwd \u2014 and the read-only\n**analysis root** are named in your briefing. Use those paths exactly; do not\ninvent or guess one.\n\n- **Relative paths resolve against your working directory** \u2014 in `read_file`,\n `list_files`, `file_stat`, `grep`, `write_file`, `edit_file`,\n `execute_command`, and any script you run. `output/de.csv` means\n `<working directory>/output/de.csv` everywhere \u2014 a file you write at a\n relative path is read back at the same relative path.\n- **You may write only inside your working directory.** A write outside it\n comes back as an `out_of_prefix` result (no I/O performed); fix the path.\n- The rest of the analysis is mounted **read-only** at the analysis root. Reach\n input data (under `data/inputs/`) and other steps' outputs (under `runs/`)\n with an **absolute** path beneath it.\n- **Absolute analysis-root paths are canonical** \u2014 use one whenever you\n reference a file you didn't create in this step.\n\n`write_file` creates parent directories as needed \u2014 no separate mkdir.\n\n## Scope \u2014 Analysis Starts at Processed Data\n\nThis environment does **tertiary analysis**: turning processed data into\nbiological findings. Read processing \u2014 alignment, quantification, variant\ncalling, peak calling \u2014 belongs upstream, wherever the instrument output was\nhandled. No tool for it is installed, and none can be.\n\nYour inputs are count and expression matrices, called variants, peak sets,\nsignal tracks, and the metadata describing them. When a request arrives as raw\nreads or unprocessed instrument output, name the processed form you need and\nstop. Do not improvise a pipeline, do not substitute a Python reimplementation\nof an aligner or peak caller, and do not treat a missing upstream tool as an\nobstacle to route around \u2014 it is the boundary of the product, not a gap.\n\nData that has already been processed can still be **read**: `samtools`,\n`bcftools`, `tabix`, `bedtools` and `vcftools` are installed so you can\ninspect, subset, and convert existing BAM/CRAM, VCF and BED. Reading evidence at\nsites you already have is in scope; producing the calls is not.\n\n## Environment \u2014 No Network, No Installs\n\n**There is no egress from this sandbox.** Outbound connections are blocked at the\nfirewall, not merely discouraged: every download, API call, package install, and\n`ExperimentHub`/`AnnotationHub`/`models.download_*` style fetch fails outright.\nThere is no proxy, no mirror, and no retry that succeeds. Packages and reference\ndata were staged onto read-only mounts by the host **before** this container\nstarted, and that is the whole of what exists. Work that genuinely requires the\ninternet \u2014 fetching a dataset, querying an external API, literature lookup \u2014 is\ndone **outside** the sandbox through the host's tools, not from your scripts.\n\nSo: check what you need when you need it \u2014 a targeted lookup, never a catalog dump\nup front.\n\n- **Packages** \u2014 before importing a package you are not certain is present, look it\n up with `list_available_packages`, narrowed to the packages you actually intend\n to import. Importing one that isn't installed fails the script; the lookup costs\n one call.\n- **Reference data** \u2014 `list_available_refs` resolves reference data into a real\n path against the store mounted read-only at `/mnt/refs`. **Search by what the\n data IS, not by where you expect it** (\"TF-target regulons\", \"hallmark gene\n sets\", \"mouse\"): the store's directory layout is an installer detail, it differs\n between hosts, and no path you have seen before is guaranteed to exist here.\n Catalogued entries come back with the organism, the format, and the file's\n internal shape, so read the entry and use the reader and species it names rather\n than assuming \u2014 formats vary per dataset, and there is no guarantee any given\n collection ships in the format your tool prefers.\n The store is OPTIONAL and may be absent or hold none of what you want \u2014 a normal\n state, not an error. Never assume a reference path exists and never hardcode one:\n if a reference you need is not in the inventory, say so plainly and proceed with\n what you do have (or state what must be provisioned) rather than inventing a path\n or substituting something else unannounced. Pass reference paths EXPLICITLY to the\n library \u2014 nothing in the image points a library at the store, so a library that\n resolves data by name from an env var (CellTypist reads `$CELLTYPIST_FOLDER`)\n will not find it unless you set that variable in `execute_command`'s `env`\n for the command that needs it, using a path the inventory actually returned.\n- Do NOT call a library's built-in data fetcher \u2014 `dc.op.collectri()`,\n `dc.op.progeny()`, `dc.op.msigdb()` and every other `dc.op.*()`, CellTypist's\n `models.download_models()`, gseapy's Enrichr library names, `ExperimentHub`/\n `AnnotationHub` \u2014 they all reach for the network and will fail. Load from an\n inventory path instead. Do NOT download data or install packages at runtime.\n\n## CPU Budget and Parallelism\n\nYour briefing names this step's CPU quota and memory limit. The container's\ncore-count calls (`parallel::detectCores()`, `os.cpu_count()`) report the\nquota, so worker counts sized from them are safe.\n\nBLAS/OpenMP thread pools default to **1 thread** (`OMP_NUM_THREADS`,\n`OPENBLAS_NUM_THREADS`, `MKL_NUM_THREADS`, \u2026). A forked worker inherits its\nparent's pool size, so this default is what keeps `mclapply` /\n`multiprocessing` from running workers \u00D7 threads on a quota sized for one of\nthem. The rule: **workers \u00D7 threads-per-worker must not exceed the quota.** A\nmix is fine while the product stays inside it.\n\n- **Fork/worker parallelism** (`mclapply`, `BiocParallel`, `joblib`,\n `multiprocessing`): size the workers from the reported core count and leave\n the thread defaults alone \u2014 there is nothing to raise. `joblib` also\n divides its own thread budget across its workers; do not manage it.\n- **Thread parallelism** (one process: xgboost, WGCNA, data.table, polars, a\n single large BLAS fit): raise the thread variables for that command through\n `execute_command`'s `env`, e.g.\n `env: { \"OMP_NUM_THREADS\": \"4\", \"OPENBLAS_NUM_THREADS\": \"4\" }` on a 4-CPU\n quota.\n\nExceeding the quota does not kill the step \u2014 the cgroup throttles it \u2014 but an\noversubscribed step runs slower and can starve the exec server that runs your\ncommands.\n\n## Context7 \u2014 Documentation Lookup\n\nWhen you are not certain of a function's signature or behavior, look it up\nwith context7 before you write the call: `resolve_library_id` with the\npackage name, then `query_docs` with the library ID and the function.\nRapidly-evolving packages (scvi-tools, spatialdata, cellrank, pertpy, muon)\nchange their APIs between releases, so check them against the docs or the\nskill references rather than memory.\n\n## Command Execution\n\n`execute_command` runs analysis work \u2014 scripts, bioinformatics CLI tools,\nshell pipes, and anything the workspace tools don't express. It starts in your\nworking directory; a relative `cwd` argument is resolved against it.\nIts `command` is an argv array. A one-element array runs through `sh -c`,\nso pipes, `&&`, redirection, quoting, and globbing work there. An array of\ntwo or more elements runs directly, with no shell.\n\nFor these tasks, prefer the dedicated workspace tools \u2014 they are faster than\nshelling out and don't waste a turn on path discovery:\n\n| Task | Use this tool | Instead of |\n|-|-|-|\n| List files in a directory | `list_files` | `ls` / `ls -la` |\n| Read a text/source/result file | `read_file` | `cat` / `less` |\n| Search file contents by pattern | `grep` | shell `grep` / `rg` |\n| Size / type of a path | `file_stat` | `stat` / `wc -c` |\n\n**Use `execute_command` for everything else**, including:\n\n- **Running scripts** \u2014 `python scripts/run.py`, `Rscript scripts/de.R`,\n `ruff check scripts/foo.py`.\n- **Previewing huge / binary files** \u2014 `wc -l file.tsv`, `zcat file.gz | head`.\n For text files, prefer `read_file` with `headLines` / `tailLines`.\n- **Shell pipes / chaining** \u2014 `sort | uniq -c | sort -rn | head`.\n- **`find`** \u2014 a recursive search by name, age, or size: `find . -name '*.h5ad'`,\n `find . -size +10M`. `list_files` lists one directory only.\n- **Real CLI tools** \u2014 `samtools`, `bcftools`, `tabix`, `bedtools`,\n `vcftools`, version probes like `python -c \"import x; print(x.__version__)\"`.\n\n**Shell behavior to know:**\n\n- `cd` does NOT persist across calls \u2014 each exec starts in your working\n directory. For cross-call state, pass `cwd` or chain with `&&` in one exec.\n- Quote paths with spaces: `head \"data/inputs/My Folder/file.csv\"`. In Python,\n use `pathlib.Path` or `os.path`.\n\n## Editing Files\n\n`edit_file` replaces `old_string` with `new_string`. Read the file first to\nget the exact text. When `replace_all` is false (default), `old_string` must\noccur exactly once \u2014 include surrounding context to make it unique.\n";
32
+ export declare const sandboxOrientCorePrompt = "# Sandbox Orient-Core\n\nThese apply to every agent that talks to a sandbox.\n\n## Your Briefing Is Authoritative\n\nYour first message is your briefing, composed for this step at the instant it was\ndispatched \u2014 after every step it depends on had finished. It names your task, your\nworking directory and the read-only analysis root, what the input dataset is\n(domain, organism, design, quality concerns, files), and what each upstream step\nproduced and where it put it. Work from it.\n\nWhen the analysis has a recorded goal or constraints, the briefing carries them\nunder \"Analysis memory (read only)\": obey each constraint from the user, and treat\neach constraint from the agent as context. You cannot change that memory.\n\nDo NOT re-derive what it already gives you: no filesystem hunt for your inputs, no\nre-reading an upstream step whose summary you were handed, no re-deriving the\norganism or the dimensions from raw bytes. Reach further only where the briefing is\ngenuinely thin for what you must do:\n\n- `inspect_data_profile` \u2014 the full dataset profile: `scope:'overview'` for the\n identity and the file census, `scope:'groups'` for the groups and the dimensions\n that vary across them, `scope:'files'` for the members the profiler annotated\n individually. It is the ONLY record of what the input data IS; no file on disk\n carries it. Use it when the briefing's orientation is not enough, or when it\n carried none.\n- `read_file` on an upstream step's summary \u2014 its path is in your briefing \u2014 when\n the excerpt you were handed is not enough to build on.\n- `workspace_search({ query })` \u2014 natural-language search for a file the briefing\n does not name. Optional `type` filter (`input-group` | `input-dimension` |\n `input` | `output` | `summary` | `synthesis`) and `limit` (1-50, default 8).\n Returns paths, descriptions, and metadata \u2014 not file contents. Most input files\n carry no entry of their own; reach them through their group and `list_files`.\n\n## Your Workspace\n\nYour **working directory** \u2014 writable, and your cwd \u2014 and the read-only\n**analysis root** are named in your briefing. Use those paths exactly; do not\ninvent or guess one.\n\n- **Relative paths resolve against your working directory** \u2014 in `read_file`,\n `list_files`, `file_stat`, `grep`, `write_file`, `edit_file`,\n `execute_command`, and any script you run. `output/de.csv` means\n `<working directory>/output/de.csv` everywhere \u2014 a file you write at a\n relative path is read back at the same relative path.\n- **You may write only inside your working directory.** A write outside it\n comes back as an `out_of_prefix` result (no I/O performed); fix the path.\n- The rest of the analysis is mounted **read-only** at the analysis root. Reach\n input data (under `data/inputs/`) and other steps' outputs (under `runs/`)\n with an **absolute** path beneath it.\n- **Absolute analysis-root paths are canonical** \u2014 use one whenever you\n reference a file you didn't create in this step.\n\n`write_file` creates parent directories as needed \u2014 no separate mkdir.\n\n## Scope \u2014 Analysis Starts at Processed Data\n\nThis environment does **tertiary analysis**: turning processed data into\nbiological findings. Read processing \u2014 alignment, quantification, variant\ncalling, peak calling \u2014 belongs upstream, wherever the instrument output was\nhandled. No tool for it is installed, and none can be.\n\nYour inputs are count and expression matrices, called variants, peak sets,\nsignal tracks, and the metadata describing them. When a request arrives as raw\nreads or unprocessed instrument output, name the processed form you need and\nstop. Do not improvise a pipeline, do not substitute a Python reimplementation\nof an aligner or peak caller, and do not treat a missing upstream tool as an\nobstacle to route around \u2014 it is the boundary of the product, not a gap.\n\nData that has already been processed can still be **read**: `samtools`,\n`bcftools`, `tabix`, `bedtools` and `vcftools` are installed so you can\ninspect, subset, and convert existing BAM/CRAM, VCF and BED. Reading evidence at\nsites you already have is in scope; producing the calls is not.\n\n## Environment \u2014 No Network, No Installs\n\n**There is no egress from this sandbox.** Outbound connections are blocked at the\nfirewall, not merely discouraged: every download, API call, package install, and\n`ExperimentHub`/`AnnotationHub`/`models.download_*` style fetch fails outright.\nThere is no proxy, no mirror, and no retry that succeeds. Packages and reference\ndata were staged onto read-only mounts by the host **before** this container\nstarted, and that is the whole of what exists. Work that genuinely requires the\ninternet \u2014 fetching a dataset, querying an external API, literature lookup \u2014 is\ndone **outside** the sandbox through the host's tools, not from your scripts.\n\nSo: check what you need when you need it \u2014 a targeted lookup, never a catalog dump\nup front.\n\n- **Packages** \u2014 before importing a package you are not certain is present, look it\n up with `list_available_packages`, narrowed to the packages you actually intend\n to import. Importing one that isn't installed fails the script; the lookup costs\n one call.\n- **Reference data** \u2014 `list_available_refs` resolves reference data into a real\n path against the store mounted read-only at `/mnt/refs`. **Search by what the\n data IS, not by where you expect it** (\"TF-target regulons\", \"hallmark gene\n sets\", \"mouse\"): the store's directory layout is an installer detail, it differs\n between hosts, and no path you have seen before is guaranteed to exist here.\n Catalogued entries come back with the organism, the format, and the file's\n internal shape, so read the entry and use the reader and species it names rather\n than assuming \u2014 formats vary per dataset, and there is no guarantee any given\n collection ships in the format your tool prefers.\n The store is OPTIONAL and may be absent or hold none of what you want \u2014 a normal\n state, not an error. Never assume a reference path exists and never hardcode one:\n if a reference you need is not in the inventory, say so plainly and proceed with\n what you do have (or state what must be provisioned) rather than inventing a path\n or substituting something else unannounced. Pass reference paths EXPLICITLY to the\n library \u2014 nothing in the image points a library at the store, so a library that\n resolves data by name from an env var (CellTypist reads `$CELLTYPIST_FOLDER`)\n will not find it unless you set that variable in `execute_command`'s `env`\n for the command that needs it, using a path the inventory actually returned.\n- Do NOT call a library's built-in data fetcher \u2014 `dc.op.collectri()`,\n `dc.op.progeny()`, `dc.op.msigdb()` and every other `dc.op.*()`, CellTypist's\n `models.download_models()`, gseapy's Enrichr library names, `ExperimentHub`/\n `AnnotationHub` \u2014 they all reach for the network and will fail. Load from an\n inventory path instead. Do NOT download data or install packages at runtime.\n\n## CPU Budget and Parallelism\n\nYour briefing names this step's CPU quota and memory limit. The container's\ncore-count calls (`parallel::detectCores()`, `os.cpu_count()`) report the\nquota, so worker counts sized from them are safe.\n\nBLAS/OpenMP thread pools default to **1 thread** (`OMP_NUM_THREADS`,\n`OPENBLAS_NUM_THREADS`, `MKL_NUM_THREADS`, \u2026). A forked worker inherits its\nparent's pool size, so this default is what keeps `mclapply` /\n`multiprocessing` from running workers \u00D7 threads on a quota sized for one of\nthem. The rule: **workers \u00D7 threads-per-worker must not exceed the quota.** A\nmix is fine while the product stays inside it.\n\n- **Fork/worker parallelism** (`mclapply`, `BiocParallel`, `joblib`,\n `multiprocessing`): size the workers from the reported core count and leave\n the thread defaults alone \u2014 there is nothing to raise. `joblib` also\n divides its own thread budget across its workers; do not manage it.\n- **Thread parallelism** (one process: xgboost, WGCNA, data.table, polars, a\n single large BLAS fit): raise the thread variables for that command through\n `execute_command`'s `env`, e.g.\n `env: { \"OMP_NUM_THREADS\": \"4\", \"OPENBLAS_NUM_THREADS\": \"4\" }` on a 4-CPU\n quota.\n\nExceeding the quota does not kill the step \u2014 the cgroup throttles it \u2014 but an\noversubscribed step runs slower and can starve the exec server that runs your\ncommands.\n\n## Context7 \u2014 Documentation Lookup\n\nWhen you are not certain of a function's signature or behavior, look it up\nwith context7 before you write the call: `resolve_library_id` with the\npackage name, then `query_docs` with the library ID and the function.\nRapidly-evolving packages (scvi-tools, spatialdata, cellrank, pertpy, muon)\nchange their APIs between releases, so check them against the docs or the\nskill references rather than memory.\n\n## Command Execution\n\n`execute_command` runs analysis work \u2014 scripts, bioinformatics CLI tools,\nshell pipes, and anything the workspace tools don't express. It starts in your\nworking directory; a relative `cwd` argument is resolved against it.\nIts `command` is an argv array. A one-element array runs through `sh -c`,\nso pipes, `&&`, redirection, quoting, and globbing work there. An array of\ntwo or more elements runs directly, with no shell.\n\nFor these tasks, prefer the dedicated workspace tools \u2014 they are faster than\nshelling out and don't waste a turn on path discovery:\n\n| Task | Use this tool | Instead of |\n|-|-|-|\n| List files in a directory | `list_files` | `ls` / `ls -la` |\n| Read a text/source/result file | `read_file` | `cat` / `less` |\n| Search file contents by pattern | `grep` | shell `grep` / `rg` |\n| Size / type of a path | `file_stat` | `stat` / `wc -c` |\n\n**Use `execute_command` for everything else**, including:\n\n- **Running scripts** \u2014 `python scripts/run.py`, `Rscript scripts/de.R`,\n `ruff check scripts/foo.py`.\n- **Previewing huge / binary files** \u2014 `wc -l file.tsv`, `zcat file.gz | head`.\n For text files, prefer `read_file` with `headLines` / `tailLines`.\n- **Shell pipes / chaining** \u2014 `sort | uniq -c | sort -rn | head`.\n- **`find`** \u2014 a recursive search by name, age, or size: `find . -name '*.h5ad'`,\n `find . -size +10M`. `list_files` lists one directory only.\n- **Real CLI tools** \u2014 `samtools`, `bcftools`, `tabix`, `bedtools`,\n `vcftools`, version probes like `python -c \"import x; print(x.__version__)\"`.\n\n**Shell behavior to know:**\n\n- `cd` does NOT persist across calls \u2014 each exec starts in your working\n directory. For cross-call state, pass `cwd` or chain with `&&` in one exec.\n- Quote paths with spaces: `head \"data/inputs/My Folder/file.csv\"`. In Python,\n use `pathlib.Path` or `os.path`.\n\n## Editing Files\n\n`edit_file` replaces `old_string` with `new_string`. Read the file first to\nget the exact text. When `replace_all` is false (default), `old_string` must\noccur exactly once \u2014 include surrounding context to make it unique.\n";
33
33
  /**
34
34
  * The orient-core layer for one declared toolchain. `"image"` states that an
35
35
  * acquisition is a host action and directs the agent to report a missing
@@ -40,6 +40,10 @@ working directory and the read-only analysis root, what the input dataset is
40
40
  (domain, organism, design, quality concerns, files), and what each upstream step
41
41
  produced and where it put it. Work from it.
42
42
 
43
+ When the analysis has a recorded goal or constraints, the briefing carries them
44
+ under "Analysis memory (read only)": obey each constraint from the user, and treat
45
+ each constraint from the agent as context. You cannot change that memory.
46
+
43
47
  Do NOT re-derive what it already gives you: no filesystem hunt for your inputs, no
44
48
  re-reading an upstream step whose summary you were handed, no re-deriving the
45
49
  organism or the dimensions from raw bytes. Reach further only where the briefing is
@@ -50,11 +50,13 @@ export type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxSt
50
50
  * conversation agent does not — a half-wired ledger that reads as a complete
51
51
  * one. The protection has to hold on both sides of the assembly, not just the
52
52
  * conversation side. Each bag omits `toolOutputStore` for the same reason: each
53
- * loop must keep its texts in the one store that each read tool reads.
53
+ * loop must keep its texts in the one store that each read tool reads. The
54
+ * `executeAnalysis` bag omits `workingMemory`, because the step seeds must read
55
+ * the same memory that the conversation agent writes.
54
56
  */
55
57
  export interface CoreWorkflowDeps {
56
58
  readonly sandboxStep: Omit<SandboxStepDeps, "usageRecorder" | "citationResolver" | "toolOutputStore">;
57
- readonly buildExecuteAnalysis: (sandboxStep: SandboxStepCallable) => Omit<ExecuteAnalysisDeps, "usageRecorder" | "citationResolver" | "toolOutputStore">;
59
+ readonly buildExecuteAnalysis: (sandboxStep: SandboxStepCallable) => Omit<ExecuteAnalysisDeps, "usageRecorder" | "citationResolver" | "toolOutputStore" | "workingMemory">;
58
60
  readonly dataProfile: Omit<DataProfileDeps, "usageRecorder" | "toolOutputStore">;
59
61
  }
60
62
  /** The registered, callable workflow handles. */
@@ -28,6 +28,7 @@ import { bindExtractionTrigger, createExtractionArm, registerExtractValuesWorkfl
28
28
  import { registerDeriveTableExecWorkflow, triggerDeriveTableExec } from "../tasks/derive-table-exec.js";
29
29
  import { createCitationResolver } from "../citations/resolve.js";
30
30
  import { createThreadStore } from "../memory/thread-store.js";
31
+ import { createWorkingMemory } from "../memory/working-memory.js";
31
32
  import { createArtifactReadStore, createProductionResolver } from "../report-model/production-resolver.js";
32
33
  import { createReportVersionStore } from "../state/report-versions.js";
33
34
  import { createToolOutputStore } from "../state/tool-outputs.js";
@@ -86,7 +87,13 @@ export function assembleCoreRuntime(deps) {
86
87
  });
87
88
  const toolOutputStore = createToolOutputStore(conversation.pool);
88
89
  const sandboxStep = registerSandboxStep({ ...wf.sandboxStep, citationResolver, usageRecorder, toolOutputStore });
89
- const executeAnalysis = registerExecuteAnalysis({ ...wf.buildExecuteAnalysis(sandboxStep), citationResolver, usageRecorder, toolOutputStore });
90
+ const executeAnalysis = registerExecuteAnalysis({
91
+ ...wf.buildExecuteAnalysis(sandboxStep),
92
+ citationResolver,
93
+ usageRecorder,
94
+ toolOutputStore,
95
+ workingMemory: createWorkingMemory(conversation.pool),
96
+ });
90
97
  const dataProfile = registerDataProfileWorkflow({ ...wf.dataProfile, usageRecorder, toolOutputStore });
91
98
  // The extraction workflow shares the profile's sandbox and authorization rails, thus it draws the same
92
99
  // three seams from the profile deps. The report resolver factory binds the extraction arm over this
@@ -24,16 +24,16 @@ export const FileMetadataSchema = z
24
24
  */
25
25
  export const SubmittedFileDescriptionSchema = z.object({
26
26
  /** Exact path of the file being described, copied from the prompt's list. */
27
- path: z.string(),
28
- description: z.string(),
29
- dataType: z.string(),
30
- format: z.string(),
31
- rows: z.number().nullish(),
32
- cols: z.number().nullish(),
33
- tags: z.array(z.string()).optional(),
34
- warnings: z.array(z.string()).optional(),
27
+ path: z.string().describe("The path of the file, copied exactly from the list of files that the harness gave."),
28
+ description: z.string().describe("One sentence that says what the file contains. A semantic search of the workspace later finds the file by this text."),
29
+ dataType: z.string().describe('The semantic type of the data, for example "count matrix" or "QC report".'),
30
+ format: z.string().describe('The file format, for example "csv", "tsv", "h5ad", or "png".'),
31
+ rows: z.number().nullish().describe("The count of rows, when you know it."),
32
+ cols: z.number().nullish().describe("The count of columns, when you know it."),
33
+ tags: z.array(z.string()).optional().describe("Short labels that help a search find the file."),
34
+ warnings: z.array(z.string()).optional().describe("Known problems of the file that a reader must know before use."),
35
35
  });
36
36
  /** Input contract for the `submit_file_metadata` tool. */
37
37
  export const SubmitFileMetadataInputSchema = z.object({
38
- files: z.array(SubmittedFileDescriptionSchema),
38
+ files: z.array(SubmittedFileDescriptionSchema).describe("One entry per output file. You can cover the files in one call or in several."),
39
39
  });
@@ -11,10 +11,10 @@ export declare const PlanStepSchema: z.ZodObject<{
11
11
  context: z.ZodOptional<z.ZodString>;
12
12
  description: z.ZodOptional<z.ZodString>;
13
13
  id: z.ZodString;
14
+ constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
14
15
  track: z.ZodString;
15
16
  step_type: z.ZodString;
16
17
  question: z.ZodString;
17
- constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
18
  acceptance_criteria: z.ZodArray<z.ZodString>;
19
19
  caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
20
20
  packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
@@ -44,10 +44,10 @@ export declare const PlannerPlanSchema: z.ZodObject<{
44
44
  context: z.ZodOptional<z.ZodString>;
45
45
  description: z.ZodOptional<z.ZodString>;
46
46
  id: z.ZodString;
47
+ constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
47
48
  track: z.ZodString;
48
49
  step_type: z.ZodString;
49
50
  question: z.ZodString;
50
- constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
51
51
  acceptance_criteria: z.ZodArray<z.ZodString>;
52
52
  caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
53
53
  packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
@@ -86,10 +86,10 @@ export declare const PlanningAgentOutputSchema: z.ZodObject<{
86
86
  context: z.ZodOptional<z.ZodString>;
87
87
  description: z.ZodOptional<z.ZodString>;
88
88
  id: z.ZodString;
89
+ constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
89
90
  track: z.ZodString;
90
91
  step_type: z.ZodString;
91
92
  question: z.ZodString;
92
- constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
93
93
  acceptance_criteria: z.ZodArray<z.ZodString>;
94
94
  caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
95
95
  packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
@@ -23,10 +23,15 @@ const resourcesSchema = z.object({
23
23
  gpu: z.object({ count: z.number().int().positive() }).optional(),
24
24
  });
25
25
  const routeSchema = z.object({
26
- agentId: z.string().optional(),
27
- resources: resourcesSchema.optional(),
28
- rationale: z.string().optional(),
29
- packages: z.array(z.string()).optional(),
26
+ agentId: z.string().optional().describe("The id of one specialist from the eligible catalog, copied exactly."),
27
+ resources: resourcesSchema
28
+ .optional()
29
+ .describe("The sandbox resources of the step, inside the stated ranges. Omit to take the defaults; a value outside the ranges is replaced by the defaults."),
30
+ rationale: z.string().optional().describe("One or two sentences on why this specialist fits the request. The step agent reads it."),
31
+ packages: z
32
+ .array(z.string())
33
+ .optional()
34
+ .describe("The packages that the scripts of the step import, each as `[python:|r:]<name>[==<version>]`. An empty array is valid."),
30
35
  });
31
36
  export function defaultAdHocResources(policy) {
32
37
  return policy ? { cpu: Math.min(4, policy.perStep.maxCpu), memoryGb: Math.min(8, policy.perStep.maxMemoryGb) } : { cpu: 4, memoryGb: 8 };
@@ -38,10 +38,11 @@ const inputSchema = z
38
38
  .describe("Which ChEMBL lookup to run; each names its params and return fields.\n" +
39
39
  "'compounds' (query + searchType) — molecules by target, name or SMILES: resolve a named compound to its ID and structure, or list " +
40
40
  "what was assayed against a target. → chemblId, preferredCompoundName, canonicalSmiles, molecularWeight, alogp, molecularFormula.\n" +
41
- "'drug' (query) — approved drugs by indication or by drug name: 'what treats X?', 'is Y approved, since when?'. → moleculeChemblId, " +
42
- "preferredName, maxPhase (4 = approved), moleculeType, firstApproval, indication. A disease term reads the curated indication " +
43
- "registry, highest phase first; a query that matches no disease term reads the approved molecules (max_phase >= 4) instead, and " +
44
- "their indications come back with them.\n" +
41
+ "'drug' (query) — approved drugs by drug name or by indication: 'is Y approved, since when?', 'what treats X?'. → moleculeChemblId, " +
42
+ "preferredName, maxPhase (4 = approved), moleculeType, firstApproval, indication. The query is read as a molecule name first: the " +
43
+ "approved molecules (max_phase 4) that it names come back with their indications. A query that names no approved molecule is then " +
44
+ "read as a disease term against the curated indication registry, highest phase first, and those rows can hold clinical-stage " +
45
+ "molecules. Thus a clinical-stage compound queried by name gives an empty array; action 'compounds' resolves it.\n" +
45
46
  "'mechanism' (chemblId, molecule only) — curated mechanism of ONE molecule: 'how does X work?'. → mechanismOfAction, actionType " +
46
47
  "(INHIBITOR, AGONIST, …), targetChemblId + targetName, moleculeChemblId. Curated mainly for clinical/approved molecules, so tool " +
47
48
  "compounds often have none.\n" +
@@ -62,7 +63,9 @@ const inputSchema = z
62
63
  searchType: z
63
64
  .enum(["target", "compound", "smiles"])
64
65
  .optional()
65
- .describe("Required for 'compounds' — how to read `query`. 'target': resolve to a ChEMBL target, then return what was assayed against it. " +
66
+ .describe("Required for 'compounds' — how to read `query`. 'target': resolve `query` to the TOP ChEMBL target hit only — the result does not " +
67
+ "name that target, and a symbol can hit a non-human ortholog — then return what was assayed against it; a target ChEMBL ID from " +
68
+ "action 'targets' pins the target. " +
66
69
  "'compound': free-text over molecule names. 'smiles': flexmatch structure search."),
67
70
  chemblId: z
68
71
  .string()
@@ -45,8 +45,8 @@ const inputSchema = z
45
45
  "HTTK toxicokinetic parameters, reported functional uses, consumer-product composition."),
46
46
  query: z
47
47
  .string()
48
- .describe("Chemical identifier (all datasets). A DTXSID ('DTXSID7020182') is used directly; a CASRN ('80-05-7'), a name ('bisphenol A'), or — for " +
49
- "dataset 'chemical' — an InChIKey is resolved by EXACT match, so a non-canonical or misspelled name yields found: false."),
48
+ .describe("Chemical identifier (all datasets). A DTXSID ('DTXSID7020182') is used directly; a CASRN ('80-05-7'), a name ('bisphenol A'), or " +
49
+ "an InChIKey is resolved by EXACT match, so a non-canonical or misspelled name yields found: false."),
50
50
  dataType: z
51
51
  .enum(["toxval", "genetox", "cancer", "seem", "httk", "functional-use", "product-data", "all"])
52
52
  .optional()
@@ -103,8 +103,8 @@ export function createComptoxTool(deps) {
103
103
  "bioactivity), 'hazard' (in-vivo: " +
104
104
  "NOAEL/LOAEL/LD50, genotoxicity, cancer), 'chemical' (identity + physicochemical/ADMET), 'exposure' (SEEM predictions, toxicokinetics, uses, " +
105
105
  "products). See `dataset` for what each returns; all four resolve `query` to a DTXSID first.\n" +
106
- "ACCEPTED IDENTIFIERS: a DTXSID ('DTXSID7020182') used directly, or a CASRN ('80-05-7'), a chemical name ('bisphenol A') or — for dataset " +
107
- "'chemical' — an InChIKey, each resolved by EXACT match.\n" +
106
+ "ACCEPTED IDENTIFIERS, for every dataset: a DTXSID ('DTXSID7020182') used directly, or a CASRN ('80-05-7'), a chemical name " +
107
+ "('bisphenol A') or an InChIKey, each resolved by EXACT match.\n" +
108
108
  "Requires EPA_CCTE_API_KEY — a missing key fails terminally: do NOT retry, tell the user the key needs configuring and proceed without EPA data.\n" +
109
109
  "found: false means the query did not resolve to a chemical; a present-but-empty section is likewise valid no-data. Do not retry either.\n" +
110
110
  "This is environmental/industrial-chemical data: for drug-like compounds prefer PubChem or ChEMBL — dataset 'chemical' returns " +
@@ -106,11 +106,12 @@ export function createDrugGeneInteractionsTool(deps) {
106
106
  description: "Drug↔gene interactions across DGIdb (the Drug Gene Interaction Database), DrugBank and PharmGKB, in one call — 'what drugs hit these " +
107
107
  "genes?' and 'what genes does this drug act on?'. See `direction` and `sources` for the modes.\n" +
108
108
  "ACCEPTED IDENTIFIERS: a HUGO gene symbol ('EGFR') for direction 'gene_to_drugs'; a drug name ('imatinib'), a DrugBank ID ('DB00619') or a " +
109
- "DGIdb concept ID for 'drug_to_genes'. A brand name and a non-HUGO symbol match nothing — resolve the symbol with search_gene and the brand " +
110
- "name with search_faers({action:'label'}) first.\n" +
111
- "It answers whether an interaction is KNOWN, by whom, and of what type. For quotable POTENCY (IC50/Ki) use chembl({action:'bioactivity'}) instead.\n" +
109
+ "DGIdb concept ID for 'drug_to_genes'. A brand name and a non-HUGO symbol match nothing — resolve the symbol with search_gene, and pass " +
110
+ "the generic drug name, not the brand name.\n" +
111
+ "It answers whether an interaction is KNOWN, by whom, and of what type. It carries no potency value (IC50/Ki).\n" +
112
112
  "Verbose fields (`includeAttributes`, `includeDrugRecord`) are off by default — both cost a lot of context and are rarely what the question needs.\n" +
113
- "ALWAYS read `perSource` before concluding an interaction is unknown: 'no_data' means that corpus has nothing; 'unavailable' means it could " +
113
+ "`perSource` gives the outcome of each corpus, and a conclusion that an interaction is unknown rests on it: 'no_data' means that corpus " +
114
+ "has nothing; 'unavailable' means it could " +
114
115
  "not be reached (a missing DRUGBANK_API_KEY lands here — tell the user and proceed with DGIdb). A `found: false` entry means the identifier " +
115
116
  "is not in DGIdb at all, usually a non-HUGO symbol or a brand name. Neither is worth retrying unchanged.",
116
117
  inputSchema,
@@ -156,15 +156,15 @@ export function createGeneDiseaseEvidenceTool(deps) {
156
156
  id: "gene_disease_evidence",
157
157
  description: "Genetic evidence linking a gene to a disease or trait, across the NHGRI-EBI GWAS Catalog, DisGeNET, NCBI ClinVar and cBioPortal in one call " +
158
158
  "— 'is there human genetic support for this target in this indication?'. See `sources` for what each corpus carries.\n" +
159
- "For the integrated, pre-scored view prefer opentargets({action:'target'}) FIRST — it folds genetic association together with tractability " +
160
- "and the drug landscape. Reach here for the underlying records: actual SNPs, effect sizes, scores, pathogenicity calls and somatic mutation " +
161
- "frequencies with their studies and PMIDs.\n" +
159
+ "It returns the underlying records — actual SNPs, effect sizes, scores, pathogenicity calls and somatic mutation frequencies with their " +
160
+ "studies and PMIDs — not one integrated score across evidence types.\n" +
162
161
  "ACCEPTED IDENTIFIERS, each named by its `queryType`: a HUGO gene symbol or an Entrez ID ('PCSK9', '5008'); a disease or trait name or a UMLS " +
163
162
  "CUI ('asthma', 'C0006142'); a dbSNP rsID ('rs11591147'); a GWAS Catalog study accession ('GCST000392'); and a ClinVar accession " +
164
163
  "('VCV000012345'). The last two read one record back by the accession that an earlier result of this same tool reported. " +
165
164
  "The GWAS Catalog matches the EXACT EFO trait label (case-insensitive): 'asthma' resolves, a paraphrase such as 'LDL cholesterol' reads as " +
166
165
  "no_data there ('low density lipoprotein cholesterol measurement' is the label) while the other corpora still answer it.\n" +
167
- "ALWAYS read `perSource` before concluding anything is absent. 'no_data' means that corpus genuinely has nothing; 'unavailable' means it " +
166
+ "`perSource` gives the outcome of each corpus, and a conclusion of absence rests on it. 'no_data' means that corpus genuinely has nothing; " +
167
+ "'unavailable' means it " +
168
168
  "could not be reached (a missing DISGENET_API_KEY lands here — tell the user and proceed with the others); 'not_applicable' means that " +
169
169
  "corpus has no lookup for this queryType. Only 'no_data' is evidence of absence, and none is worth retrying unchanged.",
170
170
  inputSchema,
@@ -84,7 +84,9 @@ const inputSchema = z
84
84
  vocabulary: z
85
85
  .enum(["go", "kegg", "reactome", "pathways"])
86
86
  .describe("'go' — Gene Ontology via QuickGO (EBI); needs ONE of goId, query, or geneProductId. Returns terms[] { id, name, definition, aspect } " +
87
- "and/or annotations[] { geneProductId, goId, goName, aspect, evidenceCode, qualifier }. " +
87
+ "and/or annotations[] { geneProductId, goId, goName, aspect, evidenceCode, qualifier } — the first `limit` rows that QuickGO " +
88
+ "serves, one row for each term, evidence and reference, thus a term can repeat, a well-studied protein holds hundreds of rows, " +
89
+ "and the result gives no total. " +
88
90
  "'kegg' / 'reactome' — one pathway database; needs query or pathwayId. 'pathways' — both, in parallel; needs query or pathwayId. " +
89
91
  "Returns pathways[] { id, name, source, url, description?, genes? }."),
90
92
  query: z
@@ -110,8 +112,10 @@ const inputSchema = z
110
112
  includeGenes: z
111
113
  .boolean()
112
114
  .optional()
113
- .describe("Pathway vocabularies only. Default FALSE — member-gene lists cost one request per pathway and run to hundreds of symbols. True only when " +
114
- "you need the members, not the pathway identities."),
115
+ .describe("Pathway vocabularies only. Default FALSE — member lists cost one request per pathway and run to hundreds of entries. KEGG members " +
116
+ "are NCBI Gene (Entrez) IDs ('7157'), not symbols. Reactome members are participant entity names with their compartment " +
117
+ "('p-T389/412-RPS6KB1 [cytosol]', 'ATP [cytosol]'), small molecules included. True only when you need the members, not the " +
118
+ "pathway identities."),
115
119
  limit: z
116
120
  .number()
117
121
  .int()
@@ -32,7 +32,9 @@ const inputSchema = z
32
32
  action: z
33
33
  .enum(["target", "disease", "resolve_disease"])
34
34
  .describe("'target' (needs ensemblId) — the diseases associated with the gene: targetInfo (approvedSymbol, approvedName, " +
35
- "tractability across small molecule / antibody / other modalities) plus associations[], each with an overall score and " +
35
+ "tractability — for smallMolecule, antibody and otherModalities, the labels of the positive Open Targets buckets, from " +
36
+ "'Approved Drug' and 'Advanced Clinical' down to 'High-Quality Pocket' or 'UniProt loc high conf'; an empty list means no " +
37
+ "positive bucket and null means no assessment) plus associations[], each with an overall score and " +
36
38
  "its per-datatype breakdown — genetic association, known drug, literature, animal model, somatic mutation. " +
37
39
  "'disease' (needs efoId) — the targets ranked for that disease; each association carries targetId/targetSymbol/targetName " +
38
40
  "and the same score breakdown. " +
@@ -75,11 +77,11 @@ const inputSchema = z
75
77
  });
76
78
  export const openTargetsTool = defineTool({
77
79
  id: "opentargets",
78
- description: "Query the Open Targets Platform — the integrated target-disease evidence corpus of EMBL-EBI, GSK and their partners — the preferred FIRST call " +
79
- "for target assessment, since one 'target' query yields the genetic evidence, tractability, and drug landscape together, already scored and " +
80
- "ranked. See the action parameter for what each mode needs and returns.\n" +
81
- "It is the integrated view; the underlying records live elsewhere. For the raw genetic evidence (individual SNPs from the GWAS Catalog, DisGeNET " +
82
- "GDA scores, ClinVar variant pathogenicity) use gene_disease_evidence. For this target's mechanism-based safety liabilities use target_safety.\n" +
80
+ description: "Query the Open Targets Platform — the integrated target-disease evidence corpus of EMBL-EBI, GSK and their partners. One 'target' query " +
81
+ "gives the tractability of the target and its disease associations, already scored and ranked, each with a per-datatype score for genetic " +
82
+ "association, known drug, literature, animal model and somatic mutation. See the action parameter for what each mode needs and returns.\n" +
83
+ "It is the integrated view, not the underlying records: it names no drug, no SNP, no effect size, no ClinVar call and no PMID, and it " +
84
+ "holds no safety liability.\n" +
83
85
  "ACCEPTED IDENTIFIERS: an Ensembl gene ID for action 'target' (ENSG00000141510); an Open Targets disease ID for action 'disease' — EFO " +
84
86
  "(EFO_0000311), MONDO (MONDO_0005148) or HP (HP_0001250), because Open Targets keys a disease on EFO and EFO imports the other two; and a plain " +
85
87
  "disease NAME for action 'resolve_disease' ('type 2 diabetes'), which is how you obtain that disease ID. A bare gene symbol is NOT accepted by " +
@@ -19,14 +19,16 @@ const inputSchema = z
19
19
  action: z
20
20
  .enum(["compound", "crossrefs", "assays"])
21
21
  .describe("Which PubChem lookup to run. " +
22
- "'compound' — resolve `query` (in the namespace named by `searchBy`) to a compound. Returns results[]: identity (cid, canonicalSmiles, inchi, inchiKey, iupacName, " +
22
+ "'compound' — resolve `query` (in the namespace named by `searchBy`) to a compound. Returns results[]: identity (cid, canonicalSmiles — " +
23
+ "the PubChem connectivity SMILES, with no stereochemistry and no isotopes, thus inchi is the full structure — inchi, inchiKey, iupacName, " +
23
24
  "molecularFormula) and computed properties (molecularWeight, xlogp, tpsa, hbondDonorCount, hbondAcceptorCount, rotatableBondCount, complexity). " +
24
25
  "'crossrefs' — external registry ids for a `cid`. Returns crossRefs[]: a flat list of { source, id } entries. `source` names the registry that the id pattern " +
25
26
  "identifies — ChEMBL, ChEBI, DrugBank, KEGG, HMDB, CAS, PDB or UNII — and it is null for an id of any other registry; filter by `source` for the one you want. " +
26
27
  "This is the bridge out of PubChem. " +
27
28
  "'assays' — bioassay screening summaries for a `cid`; answers 'has this compound ever been screened, and against what?'. Returns assays[]: aid, assayName, targetName " +
28
29
  "(the target accession, for example a UniProt or PDB id), " +
29
- "activityOutcome (Active / Inactive / Inconclusive / Unspecified), activityValue. Broad HTS screening coverage, NOT curated potency — for numbers you intend to quote " +
30
+ "activityOutcome (Active / Inactive / Inconclusive / Unspecified), activityValue (in µM; null when the assay reports none). Broad HTS " +
31
+ "screening coverage, NOT curated potency — for numbers you intend to quote " +
30
32
  "(IC50/Ki), prefer chembl action='bioactivity'."),
31
33
  query: z
32
34
  .string()
@@ -37,7 +37,7 @@ type PubMedOutput = {
37
37
  } | ({
38
38
  pmcId: string;
39
39
  available: true;
40
- } & BoundedFullText);
40
+ } & Omit<BoundedFullText, "sections">);
41
41
  export declare function createPubMedTool(deps: {
42
42
  ncbiApiKey?: string;
43
43
  }): import("../define-tool.js").Tool<{
@@ -34,8 +34,9 @@ const inputSchema = z
34
34
  action: z
35
35
  .enum(["search", "details", "fulltext"])
36
36
  .describe("'search' (needs query) — returns totalFound plus, per hit, PMID, title, journal, year and an author summary. " +
37
- "'details' (needs pmids, max 20 per call) — each article's full abstract, journal, year, DOI, pmcId (non-null only when open-access " +
38
- "full text exists in PubMed Central), plus the leading authors and MeSH terms with authorCount / meshTermCount giving the true totals. " +
37
+ "'details' (needs pmids, max 20 per call) — each article's full abstract, journal, year, DOI, pmcId (non-null when the article is in " +
38
+ "PubMed Central, which includes articles outside the open-access subset), plus the leading authors and MeSH terms with authorCount / " +
39
+ "meshTermCount giving the true totals. " +
39
40
  "'fulltext' (needs pmcId, taken from a 'details' result) — the body of ONE open-access PMC article as plain text with section headers " +
40
41
  "preserved, trimmed to maxChars; `outline` lists every section with its size and whether it came back, and truncated says whether " +
41
42
  "anything was left out. available: false when the article is not open-access."),
@@ -122,10 +123,10 @@ export function createPubMedTool(deps) {
122
123
  "the U.S. National Library of Medicine, through the NCBI E-utilities. The three actions form a chain " +
123
124
  "— search, then details on the relevant hits, then fulltext; see the action parameter for what each needs and returns.\n" +
124
125
  "ACCEPTED IDENTIFIERS: an Entrez free-text query with optional field tags for 'search' ('PCSK9[Title] AND 2020:2024[dp]'); PMIDs for " +
125
- "'details' ('34567890'); and a PMID or a PMC ID for 'fulltext' ('PMC7096066'). For literature outside MEDLINE — a method, a model, a " +
126
- "preprint — use search_semantic_scholar or search_arxiv instead.\n" +
126
+ "'details' ('34567890'); and a PMC ID for 'fulltext' ('PMC7096066'), never a PMID — the PMC number of an article differs from its PMID. " +
127
+ "PubMed holds MEDLINE and PMC only, thus a method, a model or a preprint outside MEDLINE is often absent here.\n" +
127
128
  "Read " +
128
- "fulltext sparingly — only where the 'details' abstract is not enough, and only for articles that have a pmcId (open-access). " +
129
+ "fulltext sparingly — only where the 'details' abstract is not enough, and only for articles that have a pmcId. " +
129
130
  "A full article is larger than everything else this tool returns combined, so it comes back trimmed to a character budget: take the " +
130
131
  "`outline` from the first call and re-request the specific `sections` you need rather than raising maxChars. " +
131
132
  "An empty result set and available: false are expected outcomes, not errors — an article with no PMC record simply has no open full text. " +
@@ -172,7 +173,10 @@ export function createPubMedTool(deps) {
172
173
  // "Not open-access" is an expected outcome — a data variant, not an error.
173
174
  if (!parsed)
174
175
  return ok({ pmcId: input.pmcId, available: false });
175
- return ok({ pmcId: input.pmcId, available: true, ...parsed });
176
+ // `fullText` already holds each admitted section under its heading, thus the
177
+ // `sections` array would send the same text to the model a second time.
178
+ const { sections: _sections, ...bounded } = parsed;
179
+ return ok({ pmcId: input.pmcId, available: true, ...bounded });
176
180
  }
177
181
  }
178
182
  },