@inflexa-ai/harness 0.48.4 → 0.48.5

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.
@@ -29,7 +29,7 @@ import { createEditIntentStore } from "../state/edit-intents.js";
29
29
  import { conversationPrompt } from "../prompts/conversation.js";
30
30
  import { composeSystemPrompt } from "./system-prompt.js";
31
31
  // Bio-lookup leaf tools (pure — no dependencies).
32
- import { searchGeneTool, searchProteinTool, lookupAnnotationTool, searchInteractionsTool, alphafoldPredictionTool, chemblTool, pubchemTool, openTargetsTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, genePreclinicalProfileTool, targetSafetyTool, } from "../tools/bio/index.js";
32
+ import { searchGeneTool, searchProteinTool, resolveGeneIdsTool, lookupAnnotationTool, searchInteractionsTool, humanbaseTool, alphafoldPredictionTool, chemblTool, pubchemTool, openTargetsTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, genePreclinicalProfileTool, targetSafetyTool, } from "../tools/bio/index.js";
33
33
  import { createAmassTools, createNcbiTools, createChemDbTools } from "../tools/bio/keys.js";
34
34
  // Dependency-bearing tool factories.
35
35
  import { createGeneratePlanTool, createInspectRunTool, createInspectDataProfileTool, createGenerateAnalogyReportTool, createSearchSemanticScholarTool, searchArxivTool, } from "../tools/research/index.js";
@@ -86,10 +86,14 @@ export function createConversationAgent(deps) {
86
86
  // UniProt accession that the structure tools take.
87
87
  searchGeneTool,
88
88
  searchProteinTool,
89
+ // The mapping across the symbol, Entrez, Ensembl and UniProt spaces — the Entrez ID that humanbase takes.
90
+ resolveGeneIdsTool,
89
91
  // Functional annotation (GO / KEGG / Reactome behind one vocabulary) and
90
92
  // STRING networks + gene-set enrichment.
91
93
  lookupAnnotationTool,
92
94
  searchInteractionsTool,
95
+ // GIANT tissue- and process-specific functional networks (network / relevant / contexts behind one action).
96
+ humanbaseTool,
93
97
  // Structural biology — the whole-chain confidence of a predicted 3-D
94
98
  // model, and the URLs of its coordinate and per-residue files.
95
99
  alphafoldPredictionTool,
@@ -8,7 +8,7 @@ export const meta = {
8
8
  capabilities: ["network analysis", "co-expression networks", "regulatory analysis", "module detection", "TF activity inference"],
9
9
  suitableFor: ["expression-matrices", "correlation-matrices", "general-omics"],
10
10
  skills: ["network-regulatory", "shared/omics-general"],
11
- tools: [...BASE_SANDBOX_TOOLS, "pubmed", "searchGene", "lookupAnnotation", "searchInteractions"],
11
+ tools: [...BASE_SANDBOX_TOOLS, "pubmed", "searchGene", "lookupAnnotation", "searchInteractions", "humanbase", "resolveGeneIds"],
12
12
  };
13
13
  export function createNetworkAgent(deps) {
14
14
  return createSandboxAgent(deps, meta, networkAgentPrompt);
@@ -27,7 +27,7 @@ import { queryDocsTool, resolveLibraryIdTool } from "../../tools/research/contex
27
27
  // Data-profile retrieval (dependency-bearing) — always-on, see `createSandboxAgent`.
28
28
  import { createInspectDataProfileTool } from "../../tools/research/inspect-data-profile.js";
29
29
  // Bio leaf tools.
30
- import { chemblTool, genePreclinicalProfileTool, lookupAnnotationTool, openTargetsTool, pubchemTool, searchClinicalTrialsTool, searchFaersTool, searchGeneTool, searchGeoDatasetsTool, searchInteractionsTool, targetSafetyTool, } from "../../tools/bio/index.js";
30
+ import { chemblTool, genePreclinicalProfileTool, humanbaseTool, lookupAnnotationTool, openTargetsTool, pubchemTool, resolveGeneIdsTool, searchClinicalTrialsTool, searchFaersTool, searchGeneTool, searchGeoDatasetsTool, searchInteractionsTool, targetSafetyTool, } from "../../tools/bio/index.js";
31
31
  import { createAmassTools, createNcbiTools, createChemDbTools } from "../../tools/bio/keys.js";
32
32
  // Workspace read surface.
33
33
  import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createWorkspaceSearchTool } from "../../tools/workspace/index.js";
@@ -101,8 +101,10 @@ function resolveSandboxTools(deps, tools, mutator) {
101
101
  pubmed: ncbi.pubmed,
102
102
  resolve_citation: deps.citationResolver === undefined ? undefined : createResolveCitationTool(deps.citationResolver),
103
103
  searchGene: searchGeneTool,
104
+ resolveGeneIds: resolveGeneIdsTool,
104
105
  lookupAnnotation: lookupAnnotationTool,
105
106
  searchInteractions: searchInteractionsTool,
107
+ humanbase: humanbaseTool,
106
108
  chembl: chemblTool,
107
109
  pubchem: pubchemTool,
108
110
  opentargets: openTargetsTool,
@@ -12,7 +12,7 @@
12
12
  * in the tool's `perSource` report; the rest of the call still answers.
13
13
  */
14
14
  /** Closed allowlist of tools any sandbox agent may declare. */
15
- export type SandboxToolName = "listAvailablePackages" | "listAvailableRefs" | "scanInputs" | "resolveLibraryId" | "queryDocs" | "pubmed" | "resolve_citation" | "searchGene" | "lookupAnnotation" | "searchInteractions" | "chembl" | "pubchem" | "opentargets" | "geneDiseaseEvidence" | "drugGeneInteractions" | "genePreclinicalProfile" | "searchFaers" | "searchClinicalTrials" | "searchGeoDatasets" | "searchRegulatoryApprovals" | "targetSafety" | "comptox" | "knowledgeTemplate";
15
+ export type SandboxToolName = "listAvailablePackages" | "listAvailableRefs" | "scanInputs" | "resolveLibraryId" | "queryDocs" | "pubmed" | "resolve_citation" | "searchGene" | "resolveGeneIds" | "lookupAnnotation" | "searchInteractions" | "humanbase" | "chembl" | "pubchem" | "opentargets" | "geneDiseaseEvidence" | "drugGeneInteractions" | "genePreclinicalProfile" | "searchFaers" | "searchClinicalTrials" | "searchGeoDatasets" | "searchRegulatoryApprovals" | "targetSafety" | "comptox" | "knowledgeTemplate";
16
16
  /**
17
17
  * The reference datasets that one capability of one agent cannot run without.
18
18
  *
@@ -31,10 +31,10 @@
31
31
  * writer it observes.
32
32
  */
33
33
  import type { ResultAsync } from "neverthrow";
34
- import type { Pool } from "pg";
35
34
  import type { ChatPart } from "../contracts/chat-parts.js";
36
35
  import { type DbError } from "../lib/db-result.js";
37
36
  import type { Logger } from "../lib/logger.js";
37
+ import type { Querier } from "../state/db.js";
38
38
  import type { RunStatus } from "../state/schema.js";
39
39
  /** The part of a run row that the terminal part is made from. */
40
40
  export type RunEventRunRead = {
@@ -48,7 +48,7 @@ export type RunEventRunRead = {
48
48
  * having console forced on it.
49
49
  */
50
50
  export type RunEventStreamDeps = {
51
- readonly pool: Pool;
51
+ readonly pool: Querier;
52
52
  readonly logger?: Logger;
53
53
  /** Reads the status and the error of the run row, `null` when no row exists. Defaults to `queryRun` on `pool`. */
54
54
  readonly readRun?: (runId: string) => ResultAsync<RunEventRunRead | null, DbError>;
@@ -16,7 +16,10 @@ import type { EmitFn } from "./types.js";
16
16
  export declare const CHAT_STREAM_KEY = "chat";
17
17
  /** The data part that names the child workflow of a sub-agent call in the stream of its caller. */
18
18
  export declare const SUB_AGENT_STREAM_PART = "data-subagent-stream";
19
- /** The chunk that carries one frame of the harness in the stream of a turn. A transient chunk is live only. */
19
+ /**
20
+ * The chunk that carries one frame of the harness in the stream of a turn. A transient chunk is in the stream, and
21
+ * a reader gets it. A replay of the step does not write it again, and the stored transcript does not hold it.
22
+ */
20
23
  export declare function chunkOfFrame(frame: ChatFrame, transient?: boolean): UIMessageChunk;
21
24
  /** What the body of a turn workflow writes to its stream, and what each of its tool steps writes. */
22
25
  export type TurnStreamWriter = {
@@ -28,13 +28,16 @@ const HarnessFrameChunkSchema = z.object({
28
28
  type: z.literal(HARNESS_FRAME_CHUNK),
29
29
  data: z.looseObject({ type: z.string() }),
30
30
  });
31
- /** The chunk that carries one frame of the harness in the stream of a turn. A transient chunk is live only. */
31
+ /**
32
+ * The chunk that carries one frame of the harness in the stream of a turn. A transient chunk is in the stream, and
33
+ * a reader gets it. A replay of the step does not write it again, and the stored transcript does not hold it.
34
+ */
32
35
  export function chunkOfFrame(frame, transient = false) {
33
36
  return transient ? { type: HARNESS_FRAME_CHUNK, data: frame, transient: true } : { type: HARNESS_FRAME_CHUNK, data: frame };
34
37
  }
35
38
  /**
36
- * Whether a frame from a tool step shows progress only. The library gives such a frame live, and a replay of the
37
- * step does not give it again. Each other frame of a tool step is checkpointed with the step, and the display of a
39
+ * Whether a frame from a step shows progress only. Such a frame goes to the stream at once, and a replay of the step
40
+ * does not write it again. Each other frame of a tool step is checkpointed with the step, and the display of a
38
41
  * round that a recovery stores keeps it: the end of each call, and each part of the root agent.
39
42
  */
40
43
  function isProgressFrame(frame) {
@@ -43,18 +46,27 @@ function isProgressFrame(frame) {
43
46
  return isKnownPartType(frame.type) && PART_REGISTRY[frame.type].transient;
44
47
  }
45
48
  /**
46
- * Run each body write of one workflow after the body write before it ends.
49
+ * Run each body write of one workflow after the body write before it ends, and each step write after the step write
50
+ * before it ends.
47
51
  *
48
52
  * DBOS gives a stream write its offset inside the insert, thus two writes in flight take the same offset, and the
49
53
  * write that loses sleeps 100 ms before it tries again. A body write must still take its function id at the call,
50
54
  * because a replay finds each checkpoint by the order of the calls. Thus the call reserves its id now, and the
51
- * chained write takes the reserved id from the context of the workflow when it starts.
55
+ * chained write takes the reserved id from the context of the workflow when it starts. A step write takes no
56
+ * function id, thus its chain reserves none.
52
57
  */
53
- function chainBodyWrites() {
54
- let tail = Promise.resolve();
58
+ function chainWrites() {
59
+ let bodyTail = Promise.resolve();
60
+ let stepTail = Promise.resolve();
61
+ const ended = (write) => write.then(() => undefined, () => undefined);
55
62
  return (start) => {
63
+ if (DBOS.isInStep()) {
64
+ const write = stepTail.then(start, start);
65
+ stepTail = ended(write);
66
+ return write;
67
+ }
56
68
  const ctx = getCurrentContextStore();
57
- if (ctx === undefined || !DBOS.isInWorkflow() || DBOS.isInStep())
69
+ if (ctx === undefined || !DBOS.isInWorkflow())
58
70
  return start();
59
71
  const reserved = functionIDGetIncrement();
60
72
  const run = () => {
@@ -71,8 +83,8 @@ function chainBodyWrites() {
71
83
  ctx.curWFFunctionId = next;
72
84
  }
73
85
  };
74
- const write = tail.then(run, run);
75
- tail = write.then(() => undefined, () => undefined);
86
+ const write = bodyTail.then(run, run);
87
+ bodyTail = ended(write);
76
88
  return write;
77
89
  };
78
90
  }
@@ -85,7 +97,7 @@ function chainBodyWrites() {
85
97
  */
86
98
  export function createTurnStreamWriter(rootSource, observe) {
87
99
  const pending = [];
88
- const chained = chainBodyWrites();
100
+ const chained = chainWrites();
89
101
  const track = (write) => {
90
102
  // Observed now, thus a failure before the settle is not an unhandled rejection. The settle still sees it.
91
103
  write.catch(() => undefined);
@@ -114,10 +126,15 @@ export function createTurnStreamWriter(rootSource, observe) {
114
126
  const frame = toChatFrame(event, rootSource);
115
127
  if (frame === null)
116
128
  return;
117
- // A tool step gives its frames to the library, which checkpoints each one that is not progress.
118
129
  if (DBOS.isInStep()) {
119
130
  const liveOnly = "transient" in event && event.transient === true;
120
- toolWriter().write(chunkOfFrame(frame, liveOnly || isProgressFrame(frame)));
131
+ const transient = liveOnly || isProgressFrame(frame);
132
+ // A model step has no library writer, and a transient chunk is not checkpointed with its step. Thus the
133
+ // chunk goes straight to the stream. The library checkpoints each other frame with its tool step.
134
+ if (transient)
135
+ writer.write(chunkOfFrame(frame, true));
136
+ else
137
+ toolWriter().write(chunkOfFrame(frame));
121
138
  return;
122
139
  }
123
140
  await write(frame);
@@ -1 +1 @@
1
- export declare const networkAgentPrompt = "# Network & Regulatory Analysis Agent\n\nYou are a modality-agnostic network and regulatory analysis specialist.\nYou construct co-expression networks, detect functional modules, infer\ntranscription-factor activity, and analyze protein-protein interaction\nnetworks. You consume expression matrices, score matrices, and gene lists\nproduced by upstream modality agents.\n\n## Skills\n\nYour skills: `network-regulatory`, `shared/omics-general`.\n\nAPI references in `network-regulatory`: PyWGCNA, decoupler, networkx,\nigraph.\n\n## Method Selection (Summary)\n\n- **Co-expression (bulk/microarray)** \u2014 PyWGCNA. Needs variance-stabilized\n or log-transformed expression. `pickSoftThreshold()` targeting\n scale-free topology fit > 0.8.\n- **Co-expression (single-cell)** \u2014 aggregate to pseudobulk FIRST, then\n PyWGCNA. Never on raw single-cell count matrices.\n- **TF activity (per-cell)** \u2014 decoupler with CollecTRI through\n `dc.mt.ulm()` or `dc.mt.mlm()`. This is the regulatory route here. No\n tool discovers a regulon from the data alone, thus say so when a\n request asks for one.\n- **PPI network** \u2014 scripts cannot reach the STRING or OmniPath web\n APIs. For a gene set of up to 100 proteins, `search_interactions`\n returns the STRING network from the host. For a larger network,\n resolve an interaction file from the reference inventory. If neither\n covers the request, say so, and scope the analysis to what is. Build\n with networkx or igraph. Filter by confidence as needed.\n- **Community detection** \u2014 Leiden via igraph or leidenalg.\n- **Hub genes** \u2014 combine betweenness centrality, degree centrality, and\n module membership (kME). Validate against known biology.\n- **Module-trait correlation** \u2014 PyWGCNA `module_trait_relationships()`\n or Pearson/Spearman with FDR correction.\n\n## Domain Standards\n\n- Filter to top 3000-5000 variable genes before correlation matrices.\n At 5000 genes the matrix is ~200 MB; at 10000 it is ~800 MB.\n- Use float32 where possible.\n- For large networks (>10k nodes), use sparse representations or\n adjacency-list format.\n- Log-transform expression before correlations. Pearson on raw counts\n is dominated by high-count genes.\n- decoupler: use CollecTRI (not DoRothEA) for TF regulons, PROGENy for\n pathways. Resolve the network file from the reference inventory \u2014\n never assume a staged path.\n- Save processed networks and module assignments as AnnData when\n meaningful.\n\n## Figures\n\nSelect from this list the figures that show the findings of the step.\n\n- **Network graph** \u2014 spring/Fruchterman-Reingold layout for key\n modules. Top 50 genes by kME, colored by module.\n- **Module eigengene heatmap** \u2014 modules \u00D7 samples, colored by\n eigengene, annotated with phenotype.\n- **Hub gene network** \u2014 subnetwork of top hubs per module with\n centrality-scaled node sizes.\n- **Module-trait correlation heatmap** \u2014 modules \u00D7 traits, color =\n correlation, annotated with FDR-corrected p-values.\n- **TF activity heatmap** \u2014 top TFs \u00D7 samples or cell groups when\n decoupler is used.\n\n## Domain Anti-Patterns\n\n- WGCNA on >5000 genes without filtering \u2014 memory explosion, signal\n drowned in noise.\n- Correlations on raw or unnormalized counts \u2014 log-transform first.\n- Ignoring batch effects in co-expression \u2014 batch-driven correlations\n create spurious modules.\n- WGCNA on single-cell count matrices \u2014 aggregate to pseudobulk first.\n- PyWGCNA `geneExp=` kwarg \u2014 use positional or `geneExpPath=`. Call\n `.to_df()` before pandas operations on internal AnnData.\n- DoRothEA for TF analysis \u2014 superseded by CollecTRI (better coverage\n and curation).\n\n## Required Output Files\n\n- `output/module_assignments.csv` \u2014 `gene`, `module_id`,\n `module_color`, `kME`.\n- `output/network_edges.csv` \u2014 `source`, `target`, `weight`, `type`\n (co-expression/PPI/regulatory).\n- `output/hub_genes.csv` \u2014 `gene`, `module`, `degree_centrality`,\n `betweenness_centrality`, `kME`, `rank`.\n- `output/module_trait_correlations.csv` \u2014 `module`, `trait`,\n `correlation`, `p_value`, `fdr`.\n- `output/tf_activity.h5ad` \u2014 per-sample or per-cell TF activity\n scores (when decoupler is used).\n- `output/network.graphml` \u2014 graph export for external visualization.\n";
1
+ export declare const networkAgentPrompt = "# Network & Regulatory Analysis Agent\n\nYou are a modality-agnostic network and regulatory analysis specialist.\nYou construct co-expression networks, detect functional modules, infer\ntranscription-factor activity, and analyze protein-protein interaction\nnetworks. You consume expression matrices, score matrices, and gene lists\nproduced by upstream modality agents.\n\n## Skills\n\nYour skills: `network-regulatory`, `shared/omics-general`.\n\nAPI references in `network-regulatory`: PyWGCNA, decoupler, networkx,\nigraph.\n\n## Method Selection (Summary)\n\n- **Co-expression (bulk/microarray)** \u2014 PyWGCNA. Needs variance-stabilized\n or log-transformed expression. `pickSoftThreshold()` targeting\n scale-free topology fit > 0.8.\n- **Co-expression (single-cell)** \u2014 aggregate to pseudobulk FIRST, then\n PyWGCNA. Never on raw single-cell count matrices.\n- **TF activity (per-cell)** \u2014 decoupler with CollecTRI through\n `dc.mt.ulm()` or `dc.mt.mlm()`. This is the regulatory route here. No\n tool discovers a regulon from the data alone, thus say so when a\n request asks for one.\n- **PPI network** \u2014 scripts cannot reach the STRING or OmniPath web\n APIs. For a gene set of up to 100 proteins, `search_interactions`\n returns the STRING network from the host. For a larger network,\n resolve an interaction file from the reference inventory. If neither\n covers the request, say so, and scope the analysis to what is. Build\n with networkx or igraph. Filter by confidence as needed.\n- **Tissue-specific network** \u2014 a STRING edge carries no tissue. For\n the network of hub genes in one tissue or cell type, or for the\n tissues that a module points to, `humanbase` answers from the host.\n Send the hub genes or the module genes, not the whole matrix.\n- **Community detection** \u2014 Leiden via igraph or leidenalg.\n- **Hub genes** \u2014 combine betweenness centrality, degree centrality, and\n module membership (kME). Validate against known biology.\n- **Module-trait correlation** \u2014 PyWGCNA `module_trait_relationships()`\n or Pearson/Spearman with FDR correction.\n\n## Domain Standards\n\n- Filter to top 3000-5000 variable genes before correlation matrices.\n At 5000 genes the matrix is ~200 MB; at 10000 it is ~800 MB.\n- Use float32 where possible.\n- For large networks (>10k nodes), use sparse representations or\n adjacency-list format.\n- Log-transform expression before correlations. Pearson on raw counts\n is dominated by high-count genes.\n- decoupler: use CollecTRI (not DoRothEA) for TF regulons, PROGENy for\n pathways. Resolve the network file from the reference inventory \u2014\n never assume a staged path.\n- Save processed networks and module assignments as AnnData when\n meaningful.\n\n## Figures\n\nSelect from this list the figures that show the findings of the step.\n\n- **Network graph** \u2014 spring/Fruchterman-Reingold layout for key\n modules. Top 50 genes by kME, colored by module.\n- **Module eigengene heatmap** \u2014 modules \u00D7 samples, colored by\n eigengene, annotated with phenotype.\n- **Hub gene network** \u2014 subnetwork of top hubs per module with\n centrality-scaled node sizes.\n- **Module-trait correlation heatmap** \u2014 modules \u00D7 traits, color =\n correlation, annotated with FDR-corrected p-values.\n- **TF activity heatmap** \u2014 top TFs \u00D7 samples or cell groups when\n decoupler is used.\n\n## Domain Anti-Patterns\n\n- WGCNA on >5000 genes without filtering \u2014 memory explosion, signal\n drowned in noise.\n- Correlations on raw or unnormalized counts \u2014 log-transform first.\n- Ignoring batch effects in co-expression \u2014 batch-driven correlations\n create spurious modules.\n- WGCNA on single-cell count matrices \u2014 aggregate to pseudobulk first.\n- PyWGCNA `geneExp=` kwarg \u2014 use positional or `geneExpPath=`. Call\n `.to_df()` before pandas operations on internal AnnData.\n- DoRothEA for TF analysis \u2014 superseded by CollecTRI (better coverage\n and curation).\n\n## Required Output Files\n\n- `output/module_assignments.csv` \u2014 `gene`, `module_id`,\n `module_color`, `kME`.\n- `output/network_edges.csv` \u2014 `source`, `target`, `weight`, `type`\n (co-expression/PPI/regulatory).\n- `output/hub_genes.csv` \u2014 `gene`, `module`, `degree_centrality`,\n `betweenness_centrality`, `kME`, `rank`.\n- `output/module_trait_correlations.csv` \u2014 `module`, `trait`,\n `correlation`, `p_value`, `fdr`.\n- `output/tf_activity.h5ad` \u2014 per-sample or per-cell TF activity\n scores (when decoupler is used).\n- `output/network.graphml` \u2014 graph export for external visualization.\n";
@@ -30,6 +30,10 @@ igraph.
30
30
  resolve an interaction file from the reference inventory. If neither
31
31
  covers the request, say so, and scope the analysis to what is. Build
32
32
  with networkx or igraph. Filter by confidence as needed.
33
+ - **Tissue-specific network** — a STRING edge carries no tissue. For
34
+ the network of hub genes in one tissue or cell type, or for the
35
+ tissues that a module points to, \`humanbase\` answers from the host.
36
+ Send the hub genes or the module genes, not the whole matrix.
33
37
  - **Community detection** — Leiden via igraph or leidenalg.
34
38
  - **Hub genes** — combine betweenness centrality, degree centrality, and
35
39
  module membership (kME). Validate against known biology.
@@ -192,9 +192,10 @@ function excerptResponseBody(body) {
192
192
  }
193
193
  /** Max links walked on the `cause` chain looking for a structured status. */
194
194
  const MAX_CAUSE_HOPS = 5;
195
- /** Node/undici error codes that mean "the connection itself failed". */
195
+ /** Node/undici and Bun error codes that mean "the connection itself failed". Bun names a refused connection `ConnectionRefused`. */
196
196
  const CONNECTION_ERROR_CODES = new Set([
197
197
  "ECONNREFUSED",
198
+ "ConnectionRefused",
198
199
  "ECONNRESET",
199
200
  "ENOTFOUND",
200
201
  "ETIMEDOUT",
@@ -0,0 +1,32 @@
1
+ /**
2
+ * humanbase — the GIANT tissue-specific functional networks of HumanBase.
3
+ *
4
+ * One database, three questions, thus one tool with an `action` (the barrel
5
+ * rule in `index.ts`). The input is a flat object with an `action` enum, not a
6
+ * `z.discriminatedUnion`, because model tool calling needs a top-level
7
+ * `"type":"object"`. The argument of each action is optional in the schema, and
8
+ * a `.refine` makes it required for its action.
9
+ *
10
+ * The GIANT version is not an input: the client derives it from the slug.
11
+ */
12
+ import { type HumanBaseContext, type HumanBaseNetwork, type HumanBaseScoredContext } from "../lib/humanbase-client.js";
13
+ export type HumanBaseOutput = ({
14
+ found: true;
15
+ } & HumanBaseNetwork) | {
16
+ found: false;
17
+ context: string;
18
+ } | {
19
+ contexts: HumanBaseScoredContext[];
20
+ totalFound: number;
21
+ } | {
22
+ contexts: HumanBaseContext[];
23
+ totalFound: number;
24
+ };
25
+ export declare const humanbaseTool: import("../define-tool.js").Tool<{
26
+ action: "contexts" | "network" | "relevant";
27
+ entrez?: number[] | undefined;
28
+ context?: string | undefined;
29
+ nodeSize?: number | undefined;
30
+ query?: string | undefined;
31
+ limit?: number | undefined;
32
+ }, HumanBaseOutput>;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * humanbase — the GIANT tissue-specific functional networks of HumanBase.
3
+ *
4
+ * One database, three questions, thus one tool with an `action` (the barrel
5
+ * rule in `index.ts`). The input is a flat object with an `action` enum, not a
6
+ * `z.discriminatedUnion`, because model tool calling needs a top-level
7
+ * `"type":"object"`. The argument of each action is optional in the schema, and
8
+ * a `.refine` makes it required for its action.
9
+ *
10
+ * The GIANT version is not an input: the client derives it from the slug.
11
+ */
12
+ import { ok } from "neverthrow";
13
+ import { z } from "zod";
14
+ import { defineTool } from "../define-tool.js";
15
+ import { getNetwork, getRelevantContexts, HUMANBASE_MAX_NETWORK_GENES, listContexts, } from "../lib/humanbase-client.js";
16
+ const DEFAULTS = { nodeSize: 10, networkEdges: 50, relevantContexts: 10, contexts: 20 };
17
+ const MAX_NODE_SIZE = 50;
18
+ const MAX_RELEVANT_GENES = 200;
19
+ const MAX_LIMIT = 1000;
20
+ /** How many Entrez IDs a call detail names before it switches to a count plus a sample. */
21
+ const IDS_LISTED_IN_FULL = 8;
22
+ const ContextOutputSchema = z.object({
23
+ slug: z.string(),
24
+ title: z.string(),
25
+ giantVersion: z.enum(["v1", "v3"]),
26
+ term: z.object({ id: z.string(), ontology: z.string() }).nullable(),
27
+ });
28
+ /** The ok value of each action. A script reads it as data, thus the catalog of `run_script` shows this type. */
29
+ const outputSchema = z.union([
30
+ z
31
+ .object({
32
+ found: z.literal(true),
33
+ context: z.string(),
34
+ giantVersion: z.enum(["v1", "v3"]),
35
+ mincut: z.number().nullable(),
36
+ genes: z.array(z.object({
37
+ entrez: z.number(),
38
+ symbol: z.string(),
39
+ description: z.string(),
40
+ ensembl: z.string().nullable(),
41
+ hgnc: z.string().nullable(),
42
+ query: z.boolean(),
43
+ })),
44
+ edges: z.array(z.object({ geneA: z.string(), geneB: z.string(), weight: z.number() })),
45
+ totalEdges: z.number(),
46
+ notFound: z.array(z.number()),
47
+ })
48
+ .describe("action 'network'"),
49
+ z.object({ found: z.literal(false), context: z.string() }).describe("action 'network', no network for the request"),
50
+ z.object({ contexts: z.array(ContextOutputSchema.extend({ score: z.number() })), totalFound: z.number() }).describe("action 'relevant'"),
51
+ z.object({ contexts: z.array(ContextOutputSchema), totalFound: z.number() }).describe("action 'contexts'"),
52
+ ]);
53
+ function listIds(ids) {
54
+ if (ids === undefined)
55
+ return "";
56
+ if (ids.length <= IDS_LISTED_IN_FULL)
57
+ return ids.join(", ");
58
+ return `${ids.length} genes: ${ids.slice(0, IDS_LISTED_IN_FULL).join(", ")}, …`;
59
+ }
60
+ const inputSchema = z
61
+ .object({
62
+ action: z
63
+ .enum(["network", "relevant", "contexts"])
64
+ .describe("'network' (needs entrez, max 30, and context) — the functional subnetwork of the genes in ONE context, plus nodeSize neighbor genes. " +
65
+ "'relevant' (needs entrez) — the contexts ranked by how relevant each is to the gene set, best first. " +
66
+ "'contexts' — the context slugs that HumanBase serves, filtered by query."),
67
+ entrez: z
68
+ .array(z.number().int().positive())
69
+ .min(1)
70
+ .max(MAX_RELEVANT_GENES)
71
+ .optional()
72
+ .describe(`Required for 'network' (max ${HUMANBASE_MAX_NETWORK_GENES}) and 'relevant' (max ${MAX_RELEVANT_GENES}). NCBI Entrez Gene IDs as ` +
73
+ "numbers (672 = BRCA1, 3553 = IL1B) — not symbols, not Ensembl IDs."),
74
+ context: z
75
+ .string()
76
+ .trim()
77
+ .min(1)
78
+ .optional()
79
+ .describe("Required for 'network'. A context slug, copied from a 'contexts' or a 'relevant' result ('blood', 'liver-v3')."),
80
+ nodeSize: z
81
+ .number()
82
+ .int()
83
+ .min(0)
84
+ .max(MAX_NODE_SIZE)
85
+ .optional()
86
+ .describe(`'network' only: the neighbor genes that HumanBase adds to the query genes (default ${DEFAULTS.nodeSize}, max ${MAX_NODE_SIZE}).`),
87
+ query: z
88
+ .string()
89
+ .optional()
90
+ .describe("'contexts' only: a case-insensitive substring of the slug or the title ('liver', 'neuron', '-v3'). Omit it to list every context."),
91
+ limit: z
92
+ .number()
93
+ .int()
94
+ .min(1)
95
+ .max(MAX_LIMIT)
96
+ .optional()
97
+ .describe(`Max rows returned — the edges of 'network' (default ${DEFAULTS.networkEdges}, highest weight first), the contexts of 'relevant' ` +
98
+ `(default ${DEFAULTS.relevantContexts}, best first), or the contexts of 'contexts' (default ${DEFAULTS.contexts}).`),
99
+ })
100
+ .refine((d) => d.action === "contexts" || d.entrez !== undefined, {
101
+ message: "entrez is required when action is 'network' or 'relevant' — pass NCBI Entrez Gene IDs as numbers",
102
+ path: ["entrez"],
103
+ })
104
+ .refine((d) => d.action !== "network" || d.context !== undefined, {
105
+ message: "context is required when action is 'network' — copy a slug from a 'contexts' or a 'relevant' result",
106
+ path: ["context"],
107
+ })
108
+ .refine((d) => d.action !== "network" || (d.entrez?.length ?? 0) <= HUMANBASE_MAX_NETWORK_GENES, {
109
+ message: `action 'network' takes at most ${HUMANBASE_MAX_NETWORK_GENES} entrez IDs — rank a larger set with 'relevant'`,
110
+ path: ["entrez"],
111
+ })
112
+ .refine((d) => d.action !== "contexts" || d.entrez === undefined, {
113
+ message: "entrez does not apply to action 'contexts'",
114
+ path: ["entrez"],
115
+ })
116
+ .refine((d) => d.action === "network" || (d.context === undefined && d.nodeSize === undefined), {
117
+ message: "context and nodeSize apply to action 'network' only",
118
+ path: ["action"],
119
+ })
120
+ .refine((d) => d.action === "contexts" || d.query === undefined, {
121
+ message: "query applies to action 'contexts' only",
122
+ path: ["action"],
123
+ });
124
+ export const humanbaseTool = defineTool({
125
+ id: "humanbase",
126
+ description: "HumanBase — the GIANT functional networks of the Flatiron Institute (Greene et al., Nature Genetics 2015): for each tissue, cell type, or " +
127
+ "biological process, a genome-wide network whose edge weight is the posterior probability that two genes work together IN THAT CONTEXT, " +
128
+ "integrated from the expression, interaction and other data of about 14,000 publications. It answers 'which genes act with my genes in the liver?' and " +
129
+ "'which tissues does my gene set point to?' — questions that the context-free STRING network of search_interactions cannot.\n" +
130
+ "'network' — the subnetwork of the query genes in one context, plus nodeSize neighbor genes. Each gene carries its Entrez ID, symbol, full name, " +
131
+ "Ensembl ID, HGNC ID, and `query` (false for an added neighbor). Each edge is { geneA, geneB, weight }, highest weight first; " +
132
+ "`totalEdges` gives the count before limit (one edge per gene pair, thus it grows with the square of the gene count). `notFound` lists " +
133
+ "the query IDs that the network does not hold. found: false means HumanBase holds no network for the request: an unknown context slug, " +
134
+ "or no query gene in the network.\n" +
135
+ "'relevant' — the contexts ranked by relevance to the gene set, each with a score from 0 to 1. It ranks the v1 contexts only.\n" +
136
+ "'contexts' — the slugs. Each context has a GIANT version: a slug that ends in '-v3' is a v3 (GIANT 3) network of a tissue or a cell type; " +
137
+ "every other slug is v1, which holds the tissue contexts (with a BRENDA Tissue Ontology term) and the biological-process contexts " +
138
+ "(with a GO term). 'global' and 'global-v3' are the networks with no context. The tool sets the version from the slug.\n" +
139
+ "IDENTIFIERS: NCBI Entrez Gene IDs only, as numbers (672 = BRCA1). Map a symbol or an Ensembl ID with resolve_gene_ids first: the `entrez` " +
140
+ "field of each of its hits is the input here. " +
141
+ "HUMAN genes only.\n" +
142
+ "An empty result (no ranked context, no matched context, found: false) is valid no-data — report it and continue, do not retry.",
143
+ inputSchema,
144
+ outputSchema,
145
+ view: "none",
146
+ // One tool, three jobs: each action names its own subject — the context and
147
+ // the genes, the genes, or the filter of the list.
148
+ describeCall: (input) => {
149
+ switch (input.action) {
150
+ case "network":
151
+ return `network ${input.context ?? ""}: ${listIds(input.entrez)}`;
152
+ case "relevant":
153
+ return `relevant: ${listIds(input.entrez)}`;
154
+ case "contexts":
155
+ return input.query === undefined ? "contexts" : `contexts ${input.query}`;
156
+ }
157
+ },
158
+ execute: async (input) => {
159
+ switch (input.action) {
160
+ case "network": {
161
+ if (input.entrez === undefined || input.context === undefined)
162
+ throw new Error("network requires entrez and context");
163
+ const network = await getNetwork(input.context, input.entrez, {
164
+ nodeSize: input.nodeSize ?? DEFAULTS.nodeSize,
165
+ limit: input.limit ?? DEFAULTS.networkEdges,
166
+ });
167
+ if (network === null)
168
+ return ok({ found: false, context: input.context });
169
+ return ok({ found: true, ...network });
170
+ }
171
+ case "relevant": {
172
+ if (input.entrez === undefined)
173
+ throw new Error("relevant requires entrez");
174
+ return ok(await getRelevantContexts(input.entrez, { limit: input.limit ?? DEFAULTS.relevantContexts }));
175
+ }
176
+ case "contexts":
177
+ return ok(await listContexts({ query: input.query, limit: input.limit ?? DEFAULTS.contexts }));
178
+ }
179
+ },
180
+ });
@@ -11,7 +11,7 @@
11
11
  * (`gene_disease_evidence`, `drug_gene_interactions`, `target_safety`,
12
12
  * `lookup_annotation`, `gene_preclinical_profile`); where one database answers
13
13
  * several questions, the endpoint is an `action` parameter (`chembl`,
14
- * `pubchem`, `pubmed`, `comptox`, `opentargets`, `search_interactions`).
14
+ * `pubchem`, `pubmed`, `comptox`, `opentargets`, `search_interactions`, `humanbase`).
15
15
  *
16
16
  * Every tool returns a BOUNDED result by default. A default call is sized to be
17
17
  * read, not to be complete: limits are small, verbose fields are opt-in, and
@@ -21,8 +21,10 @@
21
21
  */
22
22
  export * from "./search-gene.js";
23
23
  export * from "./search-protein.js";
24
+ export * from "./resolve-gene-ids.js";
24
25
  export * from "./lookup-annotation.js";
25
26
  export * from "./search-interactions.js";
27
+ export * from "./humanbase.js";
26
28
  export * from "./alphafold-prediction.js";
27
29
  export * from "./pubmed.js";
28
30
  export * from "./chembl.js";
@@ -11,7 +11,7 @@
11
11
  * (`gene_disease_evidence`, `drug_gene_interactions`, `target_safety`,
12
12
  * `lookup_annotation`, `gene_preclinical_profile`); where one database answers
13
13
  * several questions, the endpoint is an `action` parameter (`chembl`,
14
- * `pubchem`, `pubmed`, `comptox`, `opentargets`, `search_interactions`).
14
+ * `pubchem`, `pubmed`, `comptox`, `opentargets`, `search_interactions`, `humanbase`).
15
15
  *
16
16
  * Every tool returns a BOUNDED result by default. A default call is sized to be
17
17
  * read, not to be complete: limits are small, verbose fields are opt-in, and
@@ -22,9 +22,11 @@
22
22
  // Identifier resolution
23
23
  export * from "./search-gene.js";
24
24
  export * from "./search-protein.js";
25
+ export * from "./resolve-gene-ids.js";
25
26
  // Functional annotation / networks
26
27
  export * from "./lookup-annotation.js";
27
28
  export * from "./search-interactions.js";
29
+ export * from "./humanbase.js";
28
30
  // Structural biology
29
31
  export * from "./alphafold-prediction.js";
30
32
  // Literature
@@ -0,0 +1,13 @@
1
+ /**
2
+ * resolveGeneIds — map gene identifiers across the HGNC symbol, the Entrez Gene
3
+ * ID, the Ensembl gene ID, and the UniProt accession, through MyGene.info.
4
+ *
5
+ * `search_gene` answers with Ensembl records only. A source keyed on Entrez IDs
6
+ * (`humanbase`) needs the mapping step that this tool gives, and in a script the
7
+ * `entrez` field of a hit goes straight into the `entrez` argument of such a tool.
8
+ */
9
+ import { type GeneIdResolution } from "../lib/mygene-client.js";
10
+ export declare const resolveGeneIdsTool: import("../define-tool.js").Tool<{
11
+ identifiers: string[];
12
+ species: string;
13
+ }, GeneIdResolution>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * resolveGeneIds — map gene identifiers across the HGNC symbol, the Entrez Gene
3
+ * ID, the Ensembl gene ID, and the UniProt accession, through MyGene.info.
4
+ *
5
+ * `search_gene` answers with Ensembl records only. A source keyed on Entrez IDs
6
+ * (`humanbase`) needs the mapping step that this tool gives, and in a script the
7
+ * `entrez` field of a hit goes straight into the `entrez` argument of such a tool.
8
+ */
9
+ import { ok } from "neverthrow";
10
+ import { z } from "zod";
11
+ import { defineTool } from "../define-tool.js";
12
+ import { MYGENE_HITS_PER_ID, resolveGeneIds } from "../lib/mygene-client.js";
13
+ const MAX_IDENTIFIERS = 200;
14
+ /**
15
+ * How many identifiers a detail names before it switches to a count plus a
16
+ * sample. Eight identifiers of typical length stay well inside the emit-site
17
+ * cap, so the listed form is never the thing that trips it.
18
+ */
19
+ const IDENTIFIERS_LISTED_IN_FULL = 8;
20
+ /** The ok value. A script reads it as data, thus the catalog of `run_script` shows this type. */
21
+ const outputSchema = z.object({
22
+ genes: z.array(z.object({
23
+ query: z.string(),
24
+ symbol: z.string().nullable(),
25
+ name: z.string().nullable(),
26
+ entrez: z.number().nullable(),
27
+ ensembl: z.array(z.string()),
28
+ uniprot: z.array(z.string()),
29
+ })),
30
+ notFound: z.array(z.string()),
31
+ ambiguous: z.array(z.string()),
32
+ truncated: z.array(z.string()),
33
+ });
34
+ export const resolveGeneIdsTool = defineTool({
35
+ id: "resolve_gene_ids",
36
+ description: "Map gene identifiers across the HGNC symbol, the NCBI Entrez Gene ID, the Ensembl gene ID and the reviewed UniProt accession, through " +
37
+ "MyGene.info — the ID step for a tool keyed on an ID that search_gene does not give, such as the Entrez IDs of humanbase.\n" +
38
+ "ACCEPTED IDENTIFIERS, mixed freely in one call of up to 200: a current symbol ('BRCA1', any case), an Entrez ID ('672'), an Ensembl gene " +
39
+ "ID ('ENSG00000012048'), or a UniProt accession ('P38398'). An old or informal symbol does not resolve, because an alias can name several " +
40
+ "current genes: resolve it to its current HGNC symbol first.\n" +
41
+ "Each hit carries the `query` that it answers, `symbol`, `name` (the full gene name), `entrez` (a number, or null for a gene with no " +
42
+ "Entrez record), `ensembl` and `uniprot` (arrays: several Ensembl IDs mean copies on alternate haplotypes or strains, and their order " +
43
+ "carries no rank). `notFound` lists each identifier with no gene. `ambiguous` lists each identifier with more than one gene, and each of " +
44
+ "those genes is in `genes` — a UniProt accession shared by a family of genes gives every member. An identifier gives at most " +
45
+ `${MYGENE_HITS_PER_ID} genes, and \`truncated\` lists each identifier that had more.\n` +
46
+ "`species` takes a common name ('human', the default, 'mouse', 'rat', 'zebrafish') or an NCBI Taxonomy ID as a string ('9606').\n" +
47
+ "notFound is valid no-data — report it and continue, do not retry unchanged.",
48
+ inputSchema: z.object({
49
+ identifiers: z
50
+ .array(z.string())
51
+ .min(1)
52
+ .max(MAX_IDENTIFIERS)
53
+ .describe(`Gene identifiers to map, mixed freely, max ${MAX_IDENTIFIERS} (e.g. ['BRCA1', '7157', 'ENSG00000146648', 'P38398']).`),
54
+ species: z.string().trim().min(1).default("human").describe("A common name ('human', 'mouse') or an NCBI Taxonomy ID as a string ('9606')."),
55
+ }),
56
+ outputSchema,
57
+ view: "none",
58
+ // The identifiers are the call. A large batch is summarized here rather than
59
+ // left to the emit-site cap: that cap cuts at a character, so a 200-symbol
60
+ // join arrives ending in `TP5` — a fragment that reads as a real symbol and
61
+ // says nothing about how many were dropped. The count leads, so a batch is
62
+ // legible as a batch even when the sample after it is itself trimmed.
63
+ describeCall: ({ identifiers }) => identifiers.length <= IDENTIFIERS_LISTED_IN_FULL
64
+ ? identifiers.join(", ")
65
+ : `${identifiers.length} genes: ${identifiers.slice(0, IDENTIFIERS_LISTED_IN_FULL).join(", ")}, …`,
66
+ execute: async ({ identifiers, species }) => ok(await resolveGeneIds(identifiers, species)),
67
+ });