@inflexa-ai/harness 0.40.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 (62) hide show
  1. package/dist/app/chat-turn.d.ts +42 -13
  2. package/dist/app/chat-turn.js +47 -16
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +1 -1
  5. package/dist/prompts/briefing.d.ts +28 -1
  6. package/dist/prompts/briefing.js +44 -1
  7. package/dist/prompts/compaction.d.ts +2 -2
  8. package/dist/prompts/compaction.js +9 -5
  9. package/dist/prompts/sandbox-standards.d.ts +1 -1
  10. package/dist/prompts/sandbox-standards.js +4 -0
  11. package/dist/runtime/assemble.d.ts +4 -2
  12. package/dist/runtime/assemble.js +8 -1
  13. package/dist/schemas/file-metadata.js +9 -9
  14. package/dist/schemas/plan-schemas.d.ts +3 -3
  15. package/dist/tools/ad-hoc-router.js +9 -4
  16. package/dist/tools/bio/chembl.js +8 -5
  17. package/dist/tools/bio/comptox.js +4 -4
  18. package/dist/tools/bio/drug-gene-interactions.js +5 -4
  19. package/dist/tools/bio/gene-disease-evidence.js +4 -4
  20. package/dist/tools/bio/lookup-annotation.js +7 -3
  21. package/dist/tools/bio/opentargets.js +8 -6
  22. package/dist/tools/bio/pubchem.js +4 -2
  23. package/dist/tools/bio/pubmed.d.ts +1 -1
  24. package/dist/tools/bio/pubmed.js +10 -6
  25. package/dist/tools/bio/search-clinical-trials.js +2 -1
  26. package/dist/tools/bio/search-gene.js +2 -1
  27. package/dist/tools/bio/search-interactions.js +8 -4
  28. package/dist/tools/bio/target-safety.js +2 -1
  29. package/dist/tools/display/show-user.js +1 -1
  30. package/dist/tools/execute-analysis.js +14 -4
  31. package/dist/tools/lib/opentargets-client.d.ts +8 -3
  32. package/dist/tools/lib/opentargets-client.js +12 -3
  33. package/dist/tools/read-tool-output.js +7 -4
  34. package/dist/tools/report-authoring/authoring-tools.js +22 -15
  35. package/dist/tools/report-session/derive-table.js +5 -1
  36. package/dist/tools/report-session/examine-page.js +5 -4
  37. package/dist/tools/report-session/preview-report.js +6 -1
  38. package/dist/tools/report-session/record-version.js +2 -1
  39. package/dist/tools/research/context7-docs.js +8 -2
  40. package/dist/tools/research/generate-analogy-report.js +15 -15
  41. package/dist/tools/research/generate-plan.js +12 -7
  42. package/dist/tools/research/inspect-data-profile.js +7 -5
  43. package/dist/tools/research/inspect-run.js +6 -2
  44. package/dist/tools/research/literature-reviewer.js +11 -6
  45. package/dist/tools/research/search-arxiv.js +2 -1
  46. package/dist/tools/research/search-github-repos.js +9 -3
  47. package/dist/tools/sandbox/list-available-refs.js +3 -2
  48. package/dist/tools/sandbox/report-blocker.js +6 -1
  49. package/dist/tools/sandbox/skills.js +4 -2
  50. package/dist/tools/start-report-session.js +4 -1
  51. package/dist/tools/workspace/edit-file.js +4 -3
  52. package/dist/tools/workspace/execute-command.js +6 -5
  53. package/dist/tools/workspace/grep.js +9 -7
  54. package/dist/tools/workspace/list-files.js +1 -1
  55. package/dist/tools/workspace/read-file.js +2 -1
  56. package/dist/tools/workspace/scan-inputs.js +3 -1
  57. package/dist/tools/workspace/show-file.js +2 -0
  58. package/dist/tools/workspace/workspace-search.js +3 -1
  59. package/dist/tools/workspace/write-file.js +5 -3
  60. package/dist/workflows/execute-analysis.d.ts +28 -5
  61. package/dist/workflows/execute-analysis.js +34 -10
  62. package/package.json +1 -1
@@ -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
  },
@@ -23,7 +23,8 @@ export const searchClinicalTrialsTool = defineTool({
23
23
  "protocol prose.\n" +
24
24
  "action 'details' takes ONE nctId and returns that study in full: the same trial record plus outcomes[] (each primary/secondary/other outcome " +
25
25
  "measure with its numeric effect where the sponsor posted results) and the reported adverse events with their per-arm counts. This is how you " +
26
- "read a result, as opposed to finding a trial.\n" +
26
+ "read a result, as opposed to finding a trial. The 'details' answer is not trimmed: a large trial with posted results carries hundreds of " +
27
+ "adverse-event terms, each with its per-arm counts, thus one answer can run to tens of thousands of characters.\n" +
27
28
  "ACCEPTED IDENTIFIERS: `query` is free text matched across the whole study record, so a gene symbol ('KRAS'), a drug or brand name ('imatinib', " +
28
29
  "'Gleevec'), a condition ('melanoma') or an NCT ID ('NCT00000102') all work. `nctId` takes one NCT ID and nothing else.\n" +
29
30
  "`status` is an optional server-side filter and `phase` narrows the returned page. Omit both to see the full landscape. totalFound reports the true " +
@@ -34,7 +34,8 @@ export const searchGeneTool = defineTool({
34
34
  "biotype, assembly, description; plus resolvedFrom[] and notFound[]. Gene-level only — no transcripts or exons. Batch up to 200 identifiers per " +
35
35
  "call.\n" +
36
36
  "ACCEPTED IDENTIFIERS, mixed freely in one call: a HUGO gene symbol ('BRCA1'), an Ensembl gene ID ('ENSG00000012048'), an HGNC ID ('HGNC:1100'), " +
37
- "a UniProt accession ('P38398') and a ChEMBL target ID ('CHEMBL5619'). An input that is not a symbol is anchored on the HGNC and UniProt " +
37
+ "a six-character UniProt accession ('P38398') and a ChEMBL target ID ('CHEMBL5619'); a ten-character accession ('A0A0B4J1Y9') is read " +
38
+ "as a symbol and lands in notFound. An input that is not a symbol is anchored on the HGNC and UniProt " +
38
39
  "registries first, and the approved symbol it yields is reported in resolvedFrom[] — that anchoring is human, so a non-symbol input with a " +
39
40
  "non-human `species` looks the ortholog up by the human approved symbol.\n" +
40
41
  "notFound is valid no-data (a deprecated, aliased or mistyped identifier, or one this species has no gene for) — report it and continue, do not " +
@@ -12,12 +12,16 @@ export const searchInteractionsTool = defineTool({
12
12
  "evidence into one confidence score — for interactions and gene-set functional enrichment. " +
13
13
  "'partners' — the one-hop interaction partners of the input proteins, score-sorted: 'what else does this protein work with?'. " +
14
14
  "'network' — the interactions AMONG the input proteins only, no new nodes: 'is my gene set actually connected?'. " +
15
- "'enrichment' — statistical over-representation of GO, KEGG, Reactome and Pfam terms in the input set, FDR-sorted: the enrichment TEST, as " +
15
+ "'enrichment' — statistical over-representation of terms in the input set against the whole genome, FDR-sorted, over every STRING " +
16
+ "category: GO Process / Function / Component, KEGG, Reactome (RCTM), Pfam, InterPro, UniProt keywords, COMPARTMENTS, DISEASES, HPO, " +
17
+ "WikiPathways and PubMed publications (PMID) among them, with `category` naming the source of each row. It is the enrichment TEST, as " +
16
18
  "opposed to lookup_annotation, which just reads a vocabulary. " +
17
19
  "ACCEPTED IDENTIFIERS: STRING resolves each one itself, so a HUGO gene symbol ('TP53'), a UniProt accession ('P04637'), an Ensembl protein ID " +
18
20
  "('ENSP00000269305') and a full protein name all work, mixed in one call. `species` takes an NCBI Taxonomy ID (9606 = human).\n" +
19
- "`limit` applies to all three actions and defaults small: a network over N proteins grows with N² edges. `totalEdges` / `totalTerms` give the " +
20
- "pre-trim counts. An empty result is valid no-data (unconnected set, no enriched term, unresolvable identifiers) — report it and continue, do " +
21
+ "`limit` applies to all three actions and defaults small: a network over N proteins grows with N² edges. For 'partners', STRING applies " +
22
+ "it to EACH input protein, thus N inputs give up to N × limit rows. `totalEdges` / `totalTerms` give the pre-trim counts of 'network' and " +
23
+ "'enrichment'; 'partners' gives no total. An empty result is valid no-data (unconnected set, no enriched term, unresolvable identifiers) — " +
24
+ "report it and continue, do " +
21
25
  "not retry.",
22
26
  inputSchema: z.object({
23
27
  identifiers: z.array(z.string()).min(1).max(100).describe("Protein/gene identifiers (e.g. ['TP53', 'BRCA1'])"),
@@ -36,7 +40,7 @@ export const searchInteractionsTool = defineTool({
36
40
  .min(1)
37
41
  .max(500)
38
42
  .default(DEFAULT_LIMIT)
39
- .describe(`Max records returned — partners, network edges, or enriched terms depending on action (default ${DEFAULT_LIMIT}, max 500). ` +
43
+ .describe(`Max records returned — partners PER INPUT PROTEIN, network edges, or enriched terms depending on action (default ${DEFAULT_LIMIT}, max 500). ` +
40
44
  "Rows are ordered best-first (score descending for interactions, FDR ascending for enrichment), so the default keeps the strongest."),
41
45
  }),
42
46
  describeCall: "none",
@@ -93,7 +93,8 @@ export const targetSafetyTool = defineTool({
93
93
  id: "target_safety",
94
94
  description: "TARGET-level, mechanism-based safety liability — the curated secondary-pharmacology panel and Open Targets' curated liabilities in one call. " +
95
95
  "Judges what engaging a target is likely to do to an organ system, before any specific molecule exists.\n" +
96
- "ACCEPTED IDENTIFIERS, mixed freely in one call: a HUGO gene symbol ('HERG', 'KCNH2'), a ChEMBL target ID ('CHEMBL240'), a UniProt accession " +
96
+ "ACCEPTED IDENTIFIERS, mixed freely in one call: an approved HUGO gene symbol ('KCNH2' — an alias such as 'hERG' matches no panel entry), a " +
97
+ "ChEMBL target ID ('CHEMBL240'), a UniProt accession " +
97
98
  "('Q12809') and an Ensembl gene ID ('ENSG00000055118').\n" +
98
99
  "This is the safety of the TARGET, not of a drug. For a marketed molecule's post-market adverse events use search_faers; for a chemical's " +
99
100
  "toxicology use comptox; for what removing the gene does in vivo use gene_preclinical_profile.\n" +
@@ -41,7 +41,7 @@ const ShowUserInputSchema = z.object({
41
41
  dataPath: z
42
42
  .string()
43
43
  .optional()
44
- .describe("echart only: analysis-rooted CSV artifact path (e.g. 'runs/run-abc/step-2/output/de-summary.csv') the host loads as the ECharts `dataset.source` at render time. Author `encode`/dimensions against the CSV's column names (read only its header) and omit `dataset.source` from the spec — the rows are never pulled through the context window. If the data is not chart-ready (needs aggregation, filtering, reshaping), do that in a sandbox step that writes a chart-ready CSV. Inline `dataset.source` is only for a handful of numbers you just computed in conversation that exist nowhere as an artifact."),
44
+ .describe("echart only: analysis-rooted CSV artifact path (e.g. 'runs/run-abc/step-2/output/de-summary.csv') the host loads as the ECharts `dataset.source` at render time. Author `encode`/dimensions against the CSV's column names (read only its header) and omit `dataset.source` from the spec — the rows are never pulled through the context window. The tool checks the shape of the path only, not that the file exists; a wrong path fails only when the card renders. If the data is not chart-ready (needs aggregation, filtering, reshaping), do that in a sandbox step that writes a chart-ready CSV. Inline `dataset.source` is only for a handful of numbers you just computed in conversation that exist nowhere as an artifact."),
45
45
  body: z.string().optional().describe("Markdown content to render (kind=markdown)"),
46
46
  code: z.string().optional().describe("Code content (kind=code)"),
47
47
  language: z.string().optional().describe("Language identifier, e.g. r, python, bash (kind=code)"),
@@ -23,9 +23,17 @@ import { callExtendAnalysisFarm } from "./sandbox/link-packages.js";
23
23
  const planIdSchema = z.string().regex(/^pln-[a-f0-9]{8}$/, "planId must be a pln-<8hex> value");
24
24
  const inputSchema = z
25
25
  .object({
26
- mode: z.enum(["plan", "adhoc"]),
27
- planId: planIdSchema.optional(),
28
- request: z.string().min(1).optional(),
26
+ mode: z
27
+ .enum(["plan", "adhoc"])
28
+ .describe("`plan` runs a stored plan that the user approved. `adhoc` runs one targeted step for an explicit user request."),
29
+ planId: planIdSchema
30
+ .optional()
31
+ .describe("mode=plan only, and required there: the `pln-<8hex>` id of the approved stored plan. Omit it for mode=adhoc."),
32
+ request: z
33
+ .string()
34
+ .min(1)
35
+ .optional()
36
+ .describe("mode=adhoc only, and required there: the computational request of the user, in the words of the user. Omit it for mode=plan."),
29
37
  })
30
38
  .superRefine((value, ctx) => {
31
39
  if (value.mode === "plan" && (value.planId === undefined || value.request !== undefined)) {
@@ -319,7 +327,9 @@ export function createExecuteAnalysisTool(deps) {
319
327
  description: "Launch analysis computation asynchronously. Use mode=plan with the planId only after the user approved that stored plan. " +
320
328
  "Use mode=adhoc with the user's exact targeted computational request when they explicitly asked to run/compute/test/compare it; " +
321
329
  "that explicit request is consent and needs no synthetic-plan approval. If computation is merely your suggestion, ask first. " +
322
- "You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId with status=in_progress; inspect results on a later turn.",
330
+ "You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId with status=in_progress; inspect results on a later turn. " +
331
+ "A plan that already has an active run returns that run, so a second run of one plan does not start. " +
332
+ "The launch fails with an error, and no run starts, when the plan is not found or fails validation, when a package of the plan cannot link, or when the run is not authorized.",
323
333
  inputSchema,
324
334
  // The mode is what a user most needs to see here — an approved plan
325
335
  // running is a different event from an ad hoc request being routed.
@@ -22,10 +22,15 @@ export interface Association {
22
22
  somaticMutationScore: number | null;
23
23
  literaturePmids: string[];
24
24
  }
25
+ /**
26
+ * The positive tractability buckets of each modality, by their Open Targets label
27
+ * ('Approved Drug', 'Structure with Ligand', 'UniProt loc high conf', …). An empty
28
+ * list means no positive bucket. `null` means Open Targets gives no row for the modality.
29
+ */
25
30
  export interface TargetTractability {
26
- smallMolecule: boolean | null;
27
- antibody: boolean | null;
28
- otherModalities: boolean | null;
31
+ smallMolecule: string[] | null;
32
+ antibody: string[] | null;
33
+ otherModalities: string[] | null;
29
34
  }
30
35
  export interface TargetInfo {
31
36
  ensemblId: string;
@@ -277,10 +277,19 @@ export async function searchTargetAssociations(ensemblId, limit = 25) {
277
277
  if (!target)
278
278
  return null;
279
279
  const tractabilityEntries = target.tractability ?? [];
280
+ // Open Targets gives one row for each bucket of a modality, from 'Approved Drug' down to a
281
+ // predicted pocket or a localization. The first row alone is the 'Approved Drug' bucket, thus
282
+ // each modality keeps the labels of all of its positive rows.
283
+ const positiveLabels = (modality) => {
284
+ const rows = tractabilityEntries.filter((t) => t.modality === modality);
285
+ if (rows.length === 0)
286
+ return null;
287
+ return rows.flatMap((t) => (t.value === true && t.label ? [t.label] : []));
288
+ };
280
289
  const tractability = {
281
- smallMolecule: tractabilityEntries.find((t) => t.modality === "SM")?.value ?? null,
282
- antibody: tractabilityEntries.find((t) => t.modality === "AB")?.value ?? null,
283
- otherModalities: tractabilityEntries.find((t) => t.modality === "OC")?.value ?? null,
290
+ smallMolecule: positiveLabels("SM"),
291
+ antibody: positiveLabels("AB"),
292
+ otherModalities: positiveLabels("OC"),
284
293
  };
285
294
  const rows = data.associations?.associatedDiseases?.rows ?? [];
286
295
  const associations = rows.map((row) => ({
@@ -5,7 +5,7 @@
5
5
  import { ok } from "neverthrow";
6
6
  import { z } from "zod";
7
7
  import { unwrapOrThrow } from "../lib/result.js";
8
- import { pairSafeIndex, READ_TOOL_OUTPUT_TOOL_ID, TOOL_RESULT_CAP } from "../loop/tool-output.js";
8
+ import { pairSafeIndex, READ_TOOL_OUTPUT_TOOL_ID, TOOL_OUTPUT_KEEP_MAX, TOOL_RESULT_CAP } from "../loop/tool-output.js";
9
9
  import { defineTool } from "./define-tool.js";
10
10
  const DEFAULT_PAGE_CHARS = 8 * 1024;
11
11
  const MAX_PAGE_CHARS = 16 * 1024;
@@ -105,9 +105,12 @@ function search(ref, text, start, limit, pattern) {
105
105
  export function createReadToolOutputTool(store) {
106
106
  return defineTool({
107
107
  id: READ_TOOL_OUTPUT_TOOL_ID,
108
- description: "Read the full text of a tool result that came back as an excerpt. A result longer than the limit of the context " +
109
- "comes back as its start and its end, and the excerpt gives a reference when the harness kept the whole text. " +
110
- "Pass that reference as `ref`. Read a window with `offset` and `limit`: an offset counts the characters " +
108
+ description: "Read the kept text of a tool result that came back as an excerpt. A tool result longer than " +
109
+ `${TOOL_RESULT_CAP} characters comes back as its start and its end, and the excerpt gives a reference when the harness kept the text. ` +
110
+ `Pass that reference as \`ref\`. The harness keeps at most ${TOOL_OUTPUT_KEEP_MAX} characters. Of a longer text, it keeps the first ` +
111
+ "half and the last half of that bound, with a line between them that gives the count of characters not kept. " +
112
+ "A reference that the harness did not keep gives the status `not_found`, and the rest of that result cannot be read. " +
113
+ "Read a window with `offset` and `limit`: an offset counts the characters " +
111
114
  `(UTF-16 code units) of the kept text from 0, and a page has at most ${MAX_PAGE_CHARS} characters, ` +
112
115
  `${DEFAULT_PAGE_CHARS} by default. Or give \`pattern\`, a JavaScript regular expression, to get at most ` +
113
116
  `${MAX_MATCHES} matches, each with its offset and the text around it. When a result gives \`more: true\`, ` +
@@ -54,13 +54,20 @@ const blockPayload = z.union([AuthoringBlockSchema, z.unknown()]);
54
54
  * present. Under `.optional()` each of those nulls fails the input parse, and the block never lands.
55
55
  */
56
56
  const absentable = (schema) => schema.nullish();
57
+ /** The meaning of each flat destination field. The add and the move share it. */
58
+ const DESTINATION_NOTES = {
59
+ parentId: "The id of the section that holds the destination. Leave it out for the root. An anchor in `before` or `after` implies its parent.",
60
+ place: "Put the block at the `start` or the `end` of the parent. The default is `end`. Give a place or an anchor, not both.",
61
+ before: "The id of a sibling block. The block lands directly before it.",
62
+ after: "The id of a sibling block. The block lands directly after it.",
63
+ };
57
64
  /** The flat destination fields. An anchor is `before` or `after`, and a place is `start` or `end`. */
58
65
  const addBlockInput = z.object({
59
66
  block: blockPayload,
60
- parentId: absentable(z.string()),
61
- place: absentable(z.enum(["start", "end"])),
62
- before: absentable(z.string()),
63
- after: absentable(z.string()),
67
+ parentId: absentable(z.string()).describe(DESTINATION_NOTES.parentId),
68
+ place: absentable(z.enum(["start", "end"])).describe(DESTINATION_NOTES.place),
69
+ before: absentable(z.string()).describe(DESTINATION_NOTES.before),
70
+ after: absentable(z.string()).describe(DESTINATION_NOTES.after),
64
71
  });
65
72
  /**
66
73
  * The change payload is a title for a section target, or a block for an atom target, and exactly one of
@@ -68,27 +75,27 @@ const addBlockInput = z.object({
68
75
  * `z.unknown()` as a required key, thus an optional marker is what makes the retitle call representable.
69
76
  */
70
77
  const changeBlockInput = z.object({
71
- targetId: z.string(),
72
- title: absentable(z.string()),
73
- block: blockPayload.optional(),
78
+ targetId: z.string().describe("The id of the block to change, as the outline gives it."),
79
+ title: absentable(z.string()).describe("The new title of a section target. Give a title or a block, not both."),
80
+ block: blockPayload.optional().describe("The replacement of an atom target. Give a title or a block, not both."),
74
81
  });
75
82
  const removeBlockInput = z.object({
76
- targetId: z.string(),
83
+ targetId: z.string().describe("The id of the block to remove, as the outline gives it."),
77
84
  });
78
85
  const moveBlockInput = z.object({
79
- targetId: z.string(),
80
- parentId: absentable(z.string()),
81
- place: absentable(z.enum(["start", "end"])),
82
- before: absentable(z.string()),
83
- after: absentable(z.string()),
86
+ targetId: z.string().describe("The id of the block to move, as the outline gives it."),
87
+ parentId: absentable(z.string()).describe(DESTINATION_NOTES.parentId),
88
+ place: absentable(z.enum(["start", "end"])).describe(DESTINATION_NOTES.place),
89
+ before: absentable(z.string()).describe(DESTINATION_NOTES.before),
90
+ after: absentable(z.string()).describe(DESTINATION_NOTES.after),
84
91
  });
85
92
  const readOutlineInput = z.object({});
86
93
  /** The read names its target with the same field name as each mutation, thus one id key serves them all. */
87
94
  const readBlockInput = z.object({
88
- targetId: z.string(),
95
+ targetId: z.string().describe("The id of the block to read, as the outline gives it."),
89
96
  });
90
97
  const setTitleInput = z.object({
91
- title: z.string().min(1),
98
+ title: z.string().min(1).describe("The title of the whole report."),
92
99
  });
93
100
  const finishDraftInput = z.object({});
94
101
  /**
@@ -144,7 +144,11 @@ export function createDeriveTableTool(deps) {
144
144
  "That directory is the one place that the script can write: the analysis mounts read-only, and the container has no network. " +
145
145
  "The standard output carries the logs of the script alone, thus a table that it prints there reaches nobody. " +
146
146
  "The derived table joins the pinned evidence of this session, thus a block binds its path the same way as a pinned artifact. " +
147
- "A name that this session already derived is refused, thus a second derivation of one table takes a new name.",
147
+ "A name that this session already derived is refused, thus a second derivation of one table takes a new name. " +
148
+ `Limits: the script holds at most ${String(SCRIPT_CAP_BYTES / 1024)} KiB, inputs holds at most ${String(INPUT_CAP)} paths, ` +
149
+ `and the run stops after ${String(DERIVATION_DEADLINE_MS / 1000)} seconds. ` +
150
+ "A failed script gives back exec-failed with the tail of its standard error. " +
151
+ "The outcome unavailable means that this host cannot run a derivation, and a repeat gives the same outcome.",
148
152
  inputSchema: deriveTableInput,
149
153
  executionMode: "inline",
150
154
  describeCall: (input) => `derive ${input.output}`,
@@ -226,13 +226,14 @@ export function createExaminePageTool(deps) {
226
226
  const releaseMs = deps.deadlines?.releaseMs ?? RELEASE_DEADLINE_MS;
227
227
  return defineTool({
228
228
  id: "examine_page",
229
- description: "Open the rendered report page in a real headless browser, and report what you see. " +
230
- "Give back one or more screenshots, the coverage of the look, the console errors, and the failed requests. " +
229
+ description: "Open the rendered report page in a real headless browser, and give back what a reviewer sees: " +
230
+ "one or more screenshots, the coverage of the look, the console errors, and the failed requests. " +
231
231
  "A tall page arrives as consecutive top-to-bottom slices in document order, and the tiles list names the rows of each slice. " +
232
232
  "The coverage says whether the pictures hold the whole page, a truncated top portion of it, or the top window alone. " +
233
233
  "Run it after the preview to look at the current page, and to confirm that the layout and the charts read clean. " +
234
- "The report tool records a version only after you look at the current page. " +
235
- "If the page has no confirmed render, run the preview again first.",
234
+ "A look at the page of the current draft is what lets record_report_version record. " +
235
+ "The outcomes no-page and missed-stamp mean that no preview rendered the current page: run preview_report, then look again. " +
236
+ "The outcome no-browser means that this host gives no browser, and a repeat gives the same outcome.",
236
237
  inputSchema: examinePageInput,
237
238
  executionMode: "inline",
238
239
  describeCall: "none",
@@ -393,9 +393,14 @@ export function createPreviewReportTool(deps) {
393
393
  const resolvePageAsset = deps.resolvePageAsset ?? resolvePageAssetFromInstallation;
394
394
  return defineTool({
395
395
  id: "preview_report",
396
- description: "Render the current draft to a self-contained HTML page, and give back the page path. " +
396
+ description: "Render the current draft to an HTML page in the session directory, and give back the page path. " +
397
+ "The page loads its figures, table data, and libraries from an assets directory that the tool stages beside it. " +
397
398
  "The tool finishes the draft first: an incomplete draft gives back the gap list, and no page renders. " +
398
399
  "On a pass it resolves each reference, stages each bound image beside the page, and writes the page. " +
400
+ "An unresolved reference gives back the block and the binding to repair. " +
401
+ "The outcome resolver-unavailable means that this host gives no reference resolver, and a repeat gives the same outcome. " +
402
+ "A rendered page carries pagePath, and access when the host serves the page: a URL with its expiry, or the reason that no URL was granted. " +
403
+ "The render marks the page that examine_page looks at next. " +
399
404
  "Use it to see the report, and to confirm that each reference resolves.",
400
405
  inputSchema: previewReportInput,
401
406
  executionMode: "inline",
@@ -128,7 +128,8 @@ export function createRecordVersionTool(deps) {
128
128
  id: "record_report_version",
129
129
  description: "Record the current draft as one report version. The tool runs the whole gate first: it finishes the draft, " +
130
130
  "resolves each reference, matches each chart encoding, and matches each assert. An incomplete draft gives back the gap list, " +
131
- "and a failed reference gives back the block that broke. The tool records a version only after the eyes look at the current page. " +
131
+ "and a failed reference gives back the block that broke. The tool records a version only after examine_page looked at the page of the current draft: " +
132
+ "with no look it gives back never-seen, and after an edit since the look it gives back stale-look. " +
132
133
  "A thread holds one version, thus a later record replaces it whole. Amend the draft, look at the page again, and record again. " +
133
134
  "After the version lands, the tool removes the file of each derived table that no block of the recorded report binds.",
134
135
  inputSchema: recordVersionInput,
@@ -43,7 +43,10 @@ function renderSnippets(snippets) {
43
43
  }
44
44
  export const resolveLibraryIdTool = defineTool({
45
45
  id: "resolve_library_id",
46
- description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to a Context7 library ID. Call this BEFORE `query_docs` to get the library ID needed for documentation lookup. Returns matching libraries ranked by relevance.",
46
+ description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to the Context7 library ID that `query_docs` needs. " +
47
+ "Returns the single best match as {found: true, libraryId, name, description}, or {found: false} when Context7 holds no library for the name. " +
48
+ "`query` ranks the candidates and only the top one comes back, thus make sure that `name` is the package you meant. " +
49
+ "A failed Context7 request returns an error result.",
47
50
  inputSchema: z.object({
48
51
  libraryName: z.string().describe("Package or library name to search for (e.g., 'scanpy', 'pysam', 'lifelines')."),
49
52
  query: z.string().describe("What you need help with — used to rank results by relevance (e.g., 'how to run differential expression')."),
@@ -69,7 +72,10 @@ export const resolveLibraryIdTool = defineTool({
69
72
  });
70
73
  export const queryDocsTool = defineTool({
71
74
  id: "query_docs",
72
- description: "Query up-to-date documentation and code examples for a library. You must call `resolve_library_id` first to get the libraryId. Use this to verify function signatures, parameters, and usage patterns before writing code.",
75
+ description: "Query up-to-date documentation and code examples for a library. The libraryId comes from `resolve_library_id`. " +
76
+ "Returns {found: true, documentation}: Markdown snippets, each with a title, a source id, a description, and code blocks. " +
77
+ "Returns {found: false} when Context7 holds no library at that ID or no snippet for the query. A failed Context7 request returns an error result. " +
78
+ "Use this to verify function signatures, parameters, and usage patterns before writing code.",
73
79
  inputSchema: z.object({
74
80
  libraryId: z.string().describe("Context7 library ID exactly as `resolve_library_id` gave it — an owner and a library, for example '/scverse/scanpy'."),
75
81
  query: z.string().describe("Specific question about the library (e.g., 'rank_genes_groups function parameters and usage')."),