@inflexa-ai/harness 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/agents/conversation-agent.d.ts +3 -0
  2. package/dist/agents/conversation-agent.js +5 -2
  3. package/dist/agents/sandbox/shared.d.ts +3 -0
  4. package/dist/agents/sandbox/shared.js +5 -0
  5. package/dist/agents/sandbox/types.d.ts +1 -1
  6. package/dist/app/chat-turn.d.ts +2 -0
  7. package/dist/app/chat-turn.js +11 -2
  8. package/dist/app/synthesize-run.d.ts +3 -0
  9. package/dist/app/synthesize-run.js +2 -1
  10. package/dist/citations/__fixtures__/resolver.d.ts +3 -0
  11. package/dist/citations/__fixtures__/resolver.js +9 -0
  12. package/dist/citations/aggregate.d.ts +3 -0
  13. package/dist/citations/aggregate.js +44 -0
  14. package/dist/citations/clients/arxiv.d.ts +6 -0
  15. package/dist/citations/clients/arxiv.js +34 -0
  16. package/dist/citations/clients/common.d.ts +11 -0
  17. package/dist/citations/clients/common.js +43 -0
  18. package/dist/citations/clients/crossref.d.ts +8 -0
  19. package/dist/citations/clients/crossref.js +98 -0
  20. package/dist/citations/clients/doi-registry.d.ts +3 -0
  21. package/dist/citations/clients/doi-registry.js +95 -0
  22. package/dist/citations/clients/pubmed.d.ts +6 -0
  23. package/dist/citations/clients/pubmed.js +83 -0
  24. package/dist/citations/clients/semantic-scholar.d.ts +6 -0
  25. package/dist/citations/clients/semantic-scholar.js +60 -0
  26. package/dist/citations/compare.d.ts +9 -0
  27. package/dist/citations/compare.js +72 -0
  28. package/dist/citations/match.d.ts +11 -0
  29. package/dist/citations/match.js +173 -0
  30. package/dist/citations/normalize.d.ts +31 -0
  31. package/dist/citations/normalize.js +187 -0
  32. package/dist/citations/plan.d.ts +2 -0
  33. package/dist/citations/plan.js +59 -0
  34. package/dist/citations/resolve.d.ts +28 -0
  35. package/dist/citations/resolve.js +238 -0
  36. package/dist/citations/types.d.ts +583 -0
  37. package/dist/citations/types.js +129 -0
  38. package/dist/contracts/schemas/chat-parts.d.ts +8 -8
  39. package/dist/contracts/target-dossier.d.ts +20 -20
  40. package/dist/execution/run-synthesis.d.ts +3 -0
  41. package/dist/execution/run-synthesis.js +1 -0
  42. package/dist/index.d.ts +6 -1
  43. package/dist/index.js +3 -0
  44. package/dist/lib/cache.d.ts +8 -0
  45. package/dist/lib/cache.js +35 -0
  46. package/dist/lib/rate-limit.d.ts +35 -0
  47. package/dist/lib/rate-limit.js +121 -0
  48. package/dist/literature/sources/arxiv.d.ts +22 -0
  49. package/dist/literature/sources/arxiv.js +103 -0
  50. package/dist/literature/sources/http.d.ts +28 -0
  51. package/dist/literature/sources/http.js +97 -0
  52. package/dist/literature/sources/pubmed.d.ts +68 -0
  53. package/dist/literature/sources/pubmed.js +266 -0
  54. package/dist/literature/sources/semantic-scholar.d.ts +24 -0
  55. package/dist/literature/sources/semantic-scholar.js +71 -0
  56. package/dist/memory/thread-store.d.ts +142 -25
  57. package/dist/memory/thread-store.js +228 -40
  58. package/dist/prompts/literature-reviewer.d.ts +1 -1
  59. package/dist/prompts/literature-reviewer.js +2 -0
  60. package/dist/runtime/assemble.d.ts +50 -4
  61. package/dist/runtime/assemble.js +47 -3
  62. package/dist/runtime/boot.js +7 -1
  63. package/dist/state/init.js +46 -1
  64. package/dist/state/schema.d.ts +2 -2
  65. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
  66. package/dist/tools/bio/keys.d.ts +1 -0
  67. package/dist/tools/bio/pubmed.js +4 -4
  68. package/dist/tools/lib/ncbi-utils.d.ts +3 -58
  69. package/dist/tools/lib/ncbi-utils.js +2 -286
  70. package/dist/tools/lib/pubmed-client.d.ts +5 -12
  71. package/dist/tools/lib/pubmed-client.js +20 -79
  72. package/dist/tools/research/generate-analogy-report.js +2 -2
  73. package/dist/tools/research/index.d.ts +1 -0
  74. package/dist/tools/research/index.js +1 -0
  75. package/dist/tools/research/inspect-run.d.ts +1 -1
  76. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  77. package/dist/tools/research/literature-reviewer.js +2 -0
  78. package/dist/tools/research/resolve-citation.d.ts +3 -0
  79. package/dist/tools/research/resolve-citation.js +15 -0
  80. package/dist/tools/research/search-arxiv.d.ts +3 -19
  81. package/dist/tools/research/search-arxiv.js +9 -105
  82. package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
  83. package/dist/tools/research/search-semantic-scholar.js +29 -88
  84. package/dist/tools/sandbox/list-available-refs.js +1 -2
  85. package/dist/workflows/execute-analysis.d.ts +3 -0
  86. package/dist/workflows/execute-analysis.js +1 -0
  87. package/dist/workflows/sandbox-step.d.ts +5 -0
  88. package/dist/workflows/sandbox-step.js +1 -0
  89. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2 -2
  90. package/dist/workflows/target-assessment/schemas.d.ts +2 -2
  91. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -1
  92. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  93. package/package.json +1 -1
@@ -1,286 +1,2 @@
1
- /**
2
- * Shared NCBI E-utilities helpers for PubMed/PMC tools.
3
- *
4
- * Provides base URL constants, API key injection, and XML-to-article
5
- * mapping functions for PubMed and PMC DTD structures.
6
- */
7
- import { XMLParser } from "fast-xml-parser";
8
- // ── NCBI E-utilities base URLs ──────────────────────────────────────
9
- export const NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
10
- export const NCBI_IDCONV = "https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0";
11
- // ── API key injection ───────────────────────────────────────────────
12
- /**
13
- * Append `api_key` and optional extra params to a base URL.
14
- * Omits the key when `apiKey` is undefined.
15
- */
16
- export function ncbiUrl(apiKey, base, params) {
17
- const url = new URL(base);
18
- if (apiKey) {
19
- url.searchParams.set("api_key", apiKey);
20
- }
21
- for (const [k, v] of Object.entries(params)) {
22
- if (v !== undefined) {
23
- url.searchParams.set(k, String(v));
24
- }
25
- }
26
- return url.toString();
27
- }
28
- // ── XML parser (shared, configured once) ────────────────────────────
29
- const xmlParser = new XMLParser({
30
- ignoreAttributes: false,
31
- attributeNamePrefix: "@_",
32
- isArray: (name) => ["PubmedArticle", "Author", "MeshHeading", "AbstractText", "sec", "p", "record"].includes(name),
33
- textNodeName: "#text",
34
- });
35
- function getItemValue(items, name) {
36
- if (!items)
37
- return "";
38
- const arr = Array.isArray(items) ? items : [items];
39
- const item = arr.find((i) => i["@_Name"] === name);
40
- return item?.["#text"] ?? "";
41
- }
42
- function getAuthorsFromDocSum(items) {
43
- if (!items)
44
- return "";
45
- const arr = Array.isArray(items) ? items : [items];
46
- const authorList = arr.find((i) => i["@_Name"] === "AuthorList");
47
- if (!authorList?.Item)
48
- return "";
49
- const authors = Array.isArray(authorList.Item) ? authorList.Item : [authorList.Item];
50
- const names = authors.map((a) => a["#text"] ?? "").filter(Boolean);
51
- if (names.length === 0)
52
- return "";
53
- if (names.length <= 3)
54
- return names.join(", ");
55
- return `${names[0]} et al.`;
56
- }
57
- /**
58
- * Parse esummary XML into PubMedSummary records.
59
- */
60
- export function parseEsummary(xml) {
61
- const parsed = xmlParser.parse(xml);
62
- const result = parsed?.eSummaryResult ?? parsed;
63
- const docSums = result?.DocSum;
64
- if (!docSums)
65
- return [];
66
- const docs = Array.isArray(docSums) ? docSums : [docSums];
67
- return docs.map((doc) => {
68
- const items = doc.Item;
69
- const pubDate = getItemValue(items, "PubDate");
70
- return {
71
- pmid: String(doc.Id ?? ""),
72
- title: getItemValue(items, "Title"),
73
- journal: getItemValue(items, "FullJournalName") || getItemValue(items, "Source"),
74
- year: pubDate ? String(pubDate).slice(0, 4) : "",
75
- authors: getAuthorsFromDocSum(items),
76
- };
77
- });
78
- }
79
- /**
80
- * Parse efetch PubMed XML into ArticleDetail records.
81
- * `pmcId` is left null here — resolved separately via ID Converter.
82
- */
83
- export function parseEfetch(xml) {
84
- const parsed = xmlParser.parse(xml);
85
- const articleSet = parsed?.PubmedArticleSet;
86
- if (!articleSet)
87
- return [];
88
- const articles = Array.isArray(articleSet.PubmedArticle) ? articleSet.PubmedArticle : articleSet.PubmedArticle ? [articleSet.PubmedArticle] : [];
89
- return articles.map((entry) => {
90
- const article = entry;
91
- const medlineCitation = article.MedlineCitation;
92
- const articleData = medlineCitation?.Article;
93
- // PMID
94
- const pmidObj = medlineCitation?.PMID;
95
- const pmid = typeof pmidObj === "object" && pmidObj !== null ? String(pmidObj["#text"] ?? "") : String(pmidObj ?? "");
96
- // Title
97
- const title = String(articleData?.ArticleTitle ?? "");
98
- // Abstract
99
- const abstractObj = articleData?.Abstract;
100
- let abstract = "";
101
- if (abstractObj?.AbstractText) {
102
- const texts = Array.isArray(abstractObj.AbstractText) ? abstractObj.AbstractText : [abstractObj.AbstractText];
103
- abstract = texts
104
- .map((t) => {
105
- if (typeof t === "string")
106
- return t;
107
- if (typeof t === "object" && t !== null) {
108
- const label = t["@_Label"];
109
- const text = t["#text"] ?? "";
110
- return label ? `${label}: ${text}` : String(text);
111
- }
112
- return String(t);
113
- })
114
- .join("\n\n");
115
- }
116
- // Authors
117
- const authorListObj = articleData?.AuthorList;
118
- const rawAuthors = authorListObj?.Author;
119
- const authorArr = Array.isArray(rawAuthors) ? rawAuthors : rawAuthors ? [rawAuthors] : [];
120
- const authors = authorArr.map((a) => {
121
- if (a.CollectiveName)
122
- return a.CollectiveName;
123
- return [a.ForeName ?? a.Initials, a.LastName].filter(Boolean).join(" ");
124
- });
125
- // Journal + year
126
- const journalObj = articleData?.Journal;
127
- const journal = String(journalObj?.Title ?? journalObj?.ISOAbbreviation ?? "");
128
- const journalIssue = journalObj?.JournalIssue;
129
- const pubDate = journalIssue?.PubDate;
130
- const year = String(pubDate?.Year ?? "");
131
- // DOI
132
- let doi = "";
133
- const articleIdList = article.PubmedData?.ArticleIdList;
134
- if (articleIdList?.ArticleId) {
135
- const ids = Array.isArray(articleIdList.ArticleId) ? articleIdList.ArticleId : [articleIdList.ArticleId];
136
- for (const idObj of ids) {
137
- if (typeof idObj === "object" && idObj !== null) {
138
- const typed = idObj;
139
- if (typed["@_IdType"] === "doi") {
140
- doi = String(typed["#text"] ?? "");
141
- }
142
- }
143
- }
144
- }
145
- // MeSH terms
146
- const meshList = medlineCitation?.MeshHeadingList;
147
- const meshHeadings = meshList?.MeshHeading
148
- ? Array.isArray(meshList.MeshHeading)
149
- ? meshList.MeshHeading
150
- : [meshList.MeshHeading]
151
- : [];
152
- const meshTerms = meshHeadings
153
- .map((h) => {
154
- const desc = h.DescriptorName;
155
- if (typeof desc === "string")
156
- return desc;
157
- if (typeof desc === "object" && desc !== null)
158
- return desc["#text"] ?? "";
159
- return "";
160
- })
161
- .filter(Boolean);
162
- return {
163
- pmid,
164
- title,
165
- abstract,
166
- authors,
167
- journal,
168
- year,
169
- doi,
170
- meshTerms,
171
- pmcId: null, // resolved separately via ID Converter
172
- };
173
- });
174
- }
175
- // ── NCBI ID Converter mapping ───────────────────────────────────────
176
- /**
177
- * Parse ID Converter JSON response into a PMID → PMC ID map.
178
- * Returns only entries that have a PMC ID.
179
- */
180
- export function parseIdConvResponse(data) {
181
- const map = new Map();
182
- const typed = data;
183
- if (!typed?.records)
184
- return map;
185
- for (const rec of typed.records) {
186
- if (rec.pmid && rec.pmcid) {
187
- map.set(rec.pmid, rec.pmcid);
188
- }
189
- }
190
- return map;
191
- }
192
- /**
193
- * Parse PMC efetch XML into plain text with section structure.
194
- * Omits figures, tables, and supplementary material markup.
195
- */
196
- export function parsePmcFullText(xml) {
197
- const parsed = xmlParser.parse(xml);
198
- const articleSet = parsed?.["pmc-articleset"];
199
- const article = articleSet?.article;
200
- if (!article)
201
- return null;
202
- const body = article.body;
203
- if (!body)
204
- return null;
205
- const sections = [];
206
- const rawSecs = body.sec ? (Array.isArray(body.sec) ? body.sec : [body.sec]) : [];
207
- for (const sec of rawSecs) {
208
- extractSection(sec, sections);
209
- }
210
- // If no sections found, try to extract body text directly
211
- if (sections.length === 0) {
212
- const bodyText = extractParagraphs(body);
213
- if (bodyText) {
214
- sections.push({ heading: "", text: bodyText });
215
- }
216
- }
217
- const fullText = sections.map((s) => (s.heading ? `## ${s.heading}\n\n${s.text}` : s.text)).join("\n\n");
218
- return { fullText, sections };
219
- }
220
- function extractSection(sec, out) {
221
- if (!sec || typeof sec !== "object")
222
- return;
223
- const s = sec;
224
- const heading = typeof s.title === "string" ? s.title : "";
225
- const text = extractParagraphs(s);
226
- if (text) {
227
- out.push({ heading, text });
228
- }
229
- // Recurse into nested sections
230
- if (s.sec) {
231
- const nested = Array.isArray(s.sec) ? s.sec : [s.sec];
232
- for (const sub of nested) {
233
- extractSection(sub, out);
234
- }
235
- }
236
- }
237
- function extractParagraphs(container) {
238
- const paragraphs = container.p;
239
- if (!paragraphs)
240
- return "";
241
- const pArr = Array.isArray(paragraphs) ? paragraphs : [paragraphs];
242
- return pArr
243
- .map((p) => {
244
- if (typeof p === "string")
245
- return p;
246
- if (typeof p === "object" && p !== null) {
247
- return flattenTextContent(p);
248
- }
249
- return String(p);
250
- })
251
- .filter(Boolean)
252
- .join("\n\n");
253
- }
254
- /**
255
- * Recursively extract text content from a parsed XML node,
256
- * ignoring markup like <fig>, <table-wrap>, <xref>, etc.
257
- */
258
- function flattenTextContent(node) {
259
- if (typeof node === "string")
260
- return node;
261
- if (typeof node === "number")
262
- return String(node);
263
- if (!node || typeof node !== "object")
264
- return "";
265
- const obj = node;
266
- const parts = [];
267
- for (const [key, value] of Object.entries(obj)) {
268
- // Skip attributes and figure/table elements
269
- if (key.startsWith("@_"))
270
- continue;
271
- if (key === "fig" || key === "table-wrap" || key === "supplementary-material")
272
- continue;
273
- if (key === "#text") {
274
- parts.push(String(value));
275
- }
276
- else if (Array.isArray(value)) {
277
- for (const item of value) {
278
- parts.push(flattenTextContent(item));
279
- }
280
- }
281
- else {
282
- parts.push(flattenTextContent(value));
283
- }
284
- }
285
- return parts.filter(Boolean).join("");
286
- }
1
+ /** @deprecated Import NCBI authority contracts from literature/sources/pubmed. */
2
+ export { NCBI_BASE, NCBI_IDCONV, ncbiUrl, parseEfetch, parseEsummary, parseIdConvResponse, parsePmcFullText } from "../../literature/sources/pubmed.js";
@@ -4,18 +4,11 @@
4
4
  * Used by §3.11 (Key Papers), §3.10.4 (Preclinical literature), and the
5
5
  * pubmed-index Phase-1 collector.
6
6
  */
7
- import { type ArticleDetail, type ArticleSection, type PubMedSummary, type FullTextResult } from "./ncbi-utils.js";
7
+ import { type ArticleDetail, type ArticleSection, type FullTextResult, type PubmedSearchOptions, type PubMedSummary } from "../../literature/sources/pubmed.js";
8
8
  export type { PubMedSummary, ArticleDetail, ArticleSection, FullTextResult };
9
- export interface SearchOptions {
10
- maxResults?: number;
11
- sort?: "relevance" | "date";
12
- dateRange?: {
13
- from: string;
14
- to: string;
15
- };
16
- }
9
+ export type SearchOptions = PubmedSearchOptions;
17
10
  /** PubMed search via esearch + esummary. */
18
- export declare function searchPubmed(ncbiApiKey: string | undefined, query: string, options?: SearchOptions): Promise<{
11
+ export declare function searchPubmed(ncbiApiKey: string | undefined, query: string, options?: SearchOptions, signal?: AbortSignal): Promise<{
19
12
  totalFound: number;
20
13
  results: PubMedSummary[];
21
14
  }>;
@@ -36,7 +29,7 @@ export interface BoundedArticleDetail extends ArticleDetail {
36
29
  meshTermCount: number;
37
30
  }
38
31
  /** Fetch full PubMed article details (efetch + ID Converter for PMC). */
39
- export declare function getArticleDetails(ncbiApiKey: string | undefined, pmids: string[]): Promise<{
32
+ export declare function getArticleDetails(ncbiApiKey: string | undefined, pmids: string[], signal?: AbortSignal): Promise<{
40
33
  articles: ArticleDetail[];
41
34
  notFound: string[];
42
35
  }>;
@@ -76,6 +69,6 @@ export interface BoundedFullText {
76
69
  * Methods is worse than none) and admitted in document order until the budget
77
70
  * would be exceeded.
78
71
  */
79
- export declare function getArticleFullText(ncbiApiKey: string | undefined, pmcId: string, options?: FullTextOptions): Promise<BoundedFullText | null>;
72
+ export declare function getArticleFullText(ncbiApiKey: string | undefined, pmcId: string, options?: FullTextOptions, signal?: AbortSignal): Promise<BoundedFullText | null>;
80
73
  /** Apply the section filter and character budget to a parsed full text. */
81
74
  export declare function boundFullText(parsed: FullTextResult, options?: FullTextOptions): BoundedFullText;
@@ -4,76 +4,27 @@
4
4
  * Used by §3.11 (Key Papers), §3.10.4 (Preclinical literature), and the
5
5
  * pubmed-index Phase-1 collector.
6
6
  */
7
- import { z } from "zod";
8
- import { apiFetch, apiFetchValidated, describeApiError } from "./api-utils.js";
9
- import { NCBI_BASE, NCBI_IDCONV, ncbiUrl, parseEsummary, parseEfetch, parseIdConvResponse, parsePmcFullText, } from "./ncbi-utils.js";
10
- // NCBI E-utilities JSON payloads, validated at the fetch boundary. Every field
11
- // is optional because the API omits absent values.
12
- const EsearchResponseSchema = z.object({
13
- esearchresult: z.object({ idlist: z.array(z.string()).optional(), count: z.string().optional() }).optional(),
14
- });
15
- const IdConvResponseSchema = z.object({
16
- records: z.array(z.object({ pmid: z.string().optional(), pmcid: z.string().optional() })).optional(),
17
- });
18
- /** PubMed search via esearch + esummary. */
19
- export async function searchPubmed(ncbiApiKey, query, options = {}) {
20
- const maxResults = options.maxResults ?? 10;
21
- const sort = options.sort ?? "relevance";
22
- const esearchParams = {
23
- db: "pubmed",
24
- term: query,
25
- retmax: maxResults,
26
- retmode: "json",
27
- sort: sort === "date" ? "pub+date" : "relevance",
28
- };
29
- if (options.dateRange) {
30
- esearchParams.mindate = options.dateRange.from;
31
- esearchParams.maxdate = options.dateRange.to;
32
- esearchParams.datetype = "pdat";
33
- }
34
- const searchUrl = ncbiUrl(ncbiApiKey, `${NCBI_BASE}/esearch.fcgi`, esearchParams);
35
- const searchResult = await apiFetchValidated(searchUrl, EsearchResponseSchema);
36
- if (searchResult.isErr())
37
- throw new Error(describeApiError(searchResult.error));
38
- const idList = searchResult.value.esearchresult?.idlist ?? [];
39
- const totalFound = parseInt(searchResult.value.esearchresult?.count ?? "0", 10);
40
- if (idList.length === 0)
41
- return { totalFound, results: [] };
42
- const summaryUrl = ncbiUrl(ncbiApiKey, `${NCBI_BASE}/esummary.fcgi`, {
43
- db: "pubmed",
44
- id: idList.join(","),
45
- retmode: "xml",
7
+ import { createPubmedSource, } from "../../literature/sources/pubmed.js";
8
+ function pubmedSource(ncbiApiKey) {
9
+ return createPubmedSource({
10
+ ...(ncbiApiKey === undefined ? {} : { apiKey: ncbiApiKey }),
11
+ maxRetries: 3,
12
+ retryDelayMs: 1_000,
13
+ timeoutMs: 90_000,
46
14
  });
47
- const summaryResult = await apiFetch(summaryUrl, { parseAs: "text" });
48
- if (summaryResult.isErr())
49
- throw new Error(describeApiError(summaryResult.error));
50
- return { totalFound, results: parseEsummary(summaryResult.value) };
15
+ }
16
+ function unwrap(result) {
17
+ if (result.status !== "ok")
18
+ throw new Error(result.detail);
19
+ return result.value;
20
+ }
21
+ /** PubMed search via esearch + esummary. */
22
+ export async function searchPubmed(ncbiApiKey, query, options = {}, signal) {
23
+ return unwrap(await pubmedSource(ncbiApiKey).search(query, options, signal));
51
24
  }
52
25
  /** Fetch full PubMed article details (efetch + ID Converter for PMC). */
53
- export async function getArticleDetails(ncbiApiKey, pmids) {
54
- const idString = pmids.join(",");
55
- const efetchAsync = apiFetch(ncbiUrl(ncbiApiKey, `${NCBI_BASE}/efetch.fcgi`, {
56
- db: "pubmed",
57
- id: idString,
58
- rettype: "xml",
59
- retmode: "xml",
60
- }), { parseAs: "text" });
61
- const idConvAsync = apiFetchValidated(ncbiUrl(ncbiApiKey, NCBI_IDCONV, { ids: idString, format: "json" }), IdConvResponseSchema);
62
- // Both requests run concurrently; each Result is awaited and handled below.
63
- const efetchResult = await efetchAsync;
64
- const idConvResult = await idConvAsync;
65
- if (efetchResult.isErr())
66
- throw new Error(describeApiError(efetchResult.error));
67
- const articles = parseEfetch(efetchResult.value);
68
- if (idConvResult.isOk()) {
69
- const pmcMap = parseIdConvResponse(idConvResult.value);
70
- for (const article of articles) {
71
- article.pmcId = pmcMap.get(article.pmid) ?? null;
72
- }
73
- }
74
- const foundPmids = new Set(articles.map((a) => a.pmid));
75
- const notFound = pmids.filter((id) => !foundPmids.has(id));
76
- return { articles, notFound };
26
+ export async function getArticleDetails(ncbiApiKey, pmids, signal) {
27
+ return unwrap(await pubmedSource(ncbiApiKey).articleDetails(pmids, signal));
77
28
  }
78
29
  /** Trim the two unbounded list fields on each article, recording their true sizes. */
79
30
  export function boundArticleDetails(articles, options = {}) {
@@ -97,18 +48,8 @@ export function boundArticleDetails(articles, options = {}) {
97
48
  * Methods is worse than none) and admitted in document order until the budget
98
49
  * would be exceeded.
99
50
  */
100
- export async function getArticleFullText(ncbiApiKey, pmcId, options = {}) {
101
- const numericId = pmcId.replace(/^PMC/i, "");
102
- const url = ncbiUrl(ncbiApiKey, `${NCBI_BASE}/efetch.fcgi`, {
103
- db: "pmc",
104
- id: numericId,
105
- rettype: "xml",
106
- retmode: "xml",
107
- });
108
- const result = await apiFetch(url, { parseAs: "text" });
109
- if (result.isErr())
110
- throw new Error(describeApiError(result.error));
111
- const parsed = parsePmcFullText(result.value);
51
+ export async function getArticleFullText(ncbiApiKey, pmcId, options = {}, signal) {
52
+ const parsed = unwrap(await pubmedSource(ncbiApiKey).fetchFullText(pmcId, signal));
112
53
  if (!parsed)
113
54
  return null;
114
55
  return boundFullText(parsed, options);
@@ -23,7 +23,7 @@ import { finalText, runAgent } from "../../loop/run-agent.js";
23
23
  import { passthroughStep } from "../../loop/run-step.js";
24
24
  import { defineTool } from "../define-tool.js";
25
25
  // Cross-domain search tools the analogical-reasoner uses.
26
- import { searchSemanticScholarTool } from "./search-semantic-scholar.js";
26
+ import { createSearchSemanticScholarTool } from "./search-semantic-scholar.js";
27
27
  import { searchArxivTool } from "./search-arxiv.js";
28
28
  import { createSearchGithubReposTool } from "./search-github-repos.js";
29
29
  // Biology literature — reused for analogies that land back in biology.
@@ -156,7 +156,7 @@ function buildConversionPrompt(rawOutput) {
156
156
  export function createGenerateAnalogyReportTool(deps) {
157
157
  const ncbi = createNcbiTools(deps.bioKeys);
158
158
  const reasonerTools = [
159
- searchSemanticScholarTool,
159
+ createSearchSemanticScholarTool({ ...(deps.bioKeys.semanticScholar === undefined ? {} : { apiKey: deps.bioKeys.semanticScholar }) }),
160
160
  searchArxivTool,
161
161
  createSearchGithubReposTool({ githubToken: deps.bioKeys.github }),
162
162
  ncbi.pubmed,
@@ -14,3 +14,4 @@ export * from "./context7-docs.js";
14
14
  export * from "./search-semantic-scholar.js";
15
15
  export * from "./search-arxiv.js";
16
16
  export * from "./search-github-repos.js";
17
+ export * from "./resolve-citation.js";
@@ -14,3 +14,4 @@ export * from "./context7-docs.js";
14
14
  export * from "./search-semantic-scholar.js";
15
15
  export * from "./search-arxiv.js";
16
16
  export * from "./search-github-repos.js";
17
+ export * from "./resolve-citation.js";
@@ -53,7 +53,7 @@ declare function formatRun(r: CortexRunRow, verbose: boolean, exposePaths?: bool
53
53
  synthesisReason?: string | undefined;
54
54
  runId: string;
55
55
  workflowName: string;
56
- status: "running" | "completed" | "failed" | "partial" | "canceled" | "suspended_insufficient_funds";
56
+ status: "partial" | "running" | "completed" | "failed" | "canceled" | "suspended_insufficient_funds";
57
57
  startedAt: string;
58
58
  completedAt: string | null;
59
59
  planId: string | null;
@@ -19,6 +19,7 @@ import { type Tool } from "../define-tool.js";
19
19
  import { type BioToolKeys } from "../bio/keys.js";
20
20
  import type { Logger } from "../../lib/logger.js";
21
21
  import type { UsageRecorder } from "../../billing/usage-recorder.js";
22
+ import type { CitationResolver } from "../../citations/types.js";
22
23
  export interface LiteratureReviewerDeps {
23
24
  /** Operational logging seam; omitted falls back to no-op. */
24
25
  readonly logger?: Logger;
@@ -30,6 +31,8 @@ export interface LiteratureReviewerDeps {
30
31
  readonly bioKeys: BioToolKeys;
31
32
  /** LLM usage-accounting seam for the child loop; omitted falls back to the no-op recorder. */
32
33
  readonly usageRecorder?: UsageRecorder;
34
+ /** Shared bibliographic verification service; all agent surfaces share its pacing/cache. */
35
+ readonly citationResolver: CitationResolver;
33
36
  }
34
37
  /** Build the `literature_reviewer` delegation tool bound to its provider. */
35
38
  export declare function createLiteratureReviewerTool(deps: LiteratureReviewerDeps): Tool;
@@ -27,6 +27,7 @@ import { genePreclinicalProfileTool } from "../bio/gene-preclinical-profile.js";
27
27
  import { lookupAnnotationTool } from "../bio/lookup-annotation.js";
28
28
  import { searchGeneTool } from "../bio/search-gene.js";
29
29
  import { searchInteractionsTool } from "../bio/search-interactions.js";
30
+ import { createResolveCitationTool } from "./resolve-citation.js";
30
31
  /** Sub-agent identity — appended to `callPath`, set as `agentId`. */
31
32
  const AGENT_ID = "literature-reviewer";
32
33
  /** Multi-tool research budget — search → read → synthesize over many genes. */
@@ -40,6 +41,7 @@ export function createLiteratureReviewerTool(deps) {
40
41
  lookupAnnotationTool,
41
42
  searchInteractionsTool,
42
43
  ncbi.pubmed,
44
+ createResolveCitationTool(deps.citationResolver),
43
45
  chemDb.drugGeneInteractions,
44
46
  genePreclinicalProfileTool,
45
47
  ];
@@ -0,0 +1,3 @@
1
+ import { type CitationResolver } from "../../citations/types.js";
2
+ import { type Tool } from "../define-tool.js";
3
+ export declare function createResolveCitationTool(resolver: CitationResolver): Tool;
@@ -0,0 +1,15 @@
1
+ import { ok } from "neverthrow";
2
+ import { CitationInputSchema } from "../../citations/types.js";
3
+ import { defineTool } from "../define-tool.js";
4
+ export function createResolveCitationTool(resolver) {
5
+ return defineTool({
6
+ id: "resolve_citation",
7
+ description: "Verify one supplied citation against applicable bibliographic authorities. " +
8
+ "Use this for 'does this reference exist and does its metadata agree?', not for topical literature discovery. " +
9
+ "Returns source-labelled records, field comparisons, conflicts, coverage, and one of verified, metadata_mismatch, " +
10
+ "not_found, unverifiable, or inconclusive. Source failure never becomes not_found.",
11
+ inputSchema: CitationInputSchema,
12
+ describeCall: ({ citation }) => `verify ${citation}`,
13
+ execute: async (input, ctx) => ok(await resolver.resolveOne(input, { signal: ctx.signal })),
14
+ });
15
+ }
@@ -1,10 +1,6 @@
1
- /**
2
- * Search arXiv for preprints. Returns Atom XML which we parse without a
3
- * dependency. arXiv is the primary source for ML / physics / math /
4
- * control-theory preprints; Semantic Scholar lags it by months.
5
- *
6
- * Same wire call and envelope as the legacy tool. Stateless HTTP; no deps.
7
- */
1
+ /** Search arXiv for preprints through the shared authority client. */
2
+ import { type ArxivPaper } from "../../literature/sources/arxiv.js";
3
+ export { parseArxivAtom, type ArxivPaper } from "../../literature/sources/arxiv.js";
8
4
  type SearchArxivOutput = {
9
5
  success: false;
10
6
  error: string;
@@ -13,20 +9,8 @@ type SearchArxivOutput = {
13
9
  success: true;
14
10
  papers: ArxivPaper[];
15
11
  };
16
- export interface ArxivPaper {
17
- id: string;
18
- title: string;
19
- abstract: string;
20
- authors: string[];
21
- published: string;
22
- categories: string[];
23
- url: string;
24
- pdfUrl?: string;
25
- }
26
- export declare function parseArxivAtom(xml: string): ArxivPaper[];
27
12
  export declare const searchArxivTool: import("../define-tool.js").Tool<{
28
13
  query: string;
29
14
  limit: number;
30
15
  categories?: string[] | undefined;
31
16
  }, SearchArxivOutput>;
32
- export {};