@inflexa-ai/harness 0.15.1 → 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.
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +5 -2
- package/dist/agents/sandbox/shared.d.ts +3 -0
- package/dist/agents/sandbox/shared.js +5 -0
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/chat-turn.d.ts +2 -0
- package/dist/app/chat-turn.js +11 -2
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +2 -1
- package/dist/citations/__fixtures__/resolver.d.ts +3 -0
- package/dist/citations/__fixtures__/resolver.js +9 -0
- package/dist/citations/aggregate.d.ts +3 -0
- package/dist/citations/aggregate.js +44 -0
- package/dist/citations/clients/arxiv.d.ts +6 -0
- package/dist/citations/clients/arxiv.js +34 -0
- package/dist/citations/clients/common.d.ts +11 -0
- package/dist/citations/clients/common.js +43 -0
- package/dist/citations/clients/crossref.d.ts +8 -0
- package/dist/citations/clients/crossref.js +98 -0
- package/dist/citations/clients/doi-registry.d.ts +3 -0
- package/dist/citations/clients/doi-registry.js +95 -0
- package/dist/citations/clients/pubmed.d.ts +6 -0
- package/dist/citations/clients/pubmed.js +83 -0
- package/dist/citations/clients/semantic-scholar.d.ts +6 -0
- package/dist/citations/clients/semantic-scholar.js +60 -0
- package/dist/citations/compare.d.ts +9 -0
- package/dist/citations/compare.js +72 -0
- package/dist/citations/match.d.ts +11 -0
- package/dist/citations/match.js +173 -0
- package/dist/citations/normalize.d.ts +31 -0
- package/dist/citations/normalize.js +187 -0
- package/dist/citations/plan.d.ts +2 -0
- package/dist/citations/plan.js +59 -0
- package/dist/citations/resolve.d.ts +28 -0
- package/dist/citations/resolve.js +238 -0
- package/dist/citations/types.d.ts +583 -0
- package/dist/citations/types.js +129 -0
- package/dist/contracts/chat-events.d.ts +16 -0
- package/dist/contracts/message.d.ts +13 -8
- package/dist/contracts/schemas/chat-events.d.ts +7 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/schemas/chat-parts.d.ts +8 -8
- package/dist/contracts/target-dossier.d.ts +20 -20
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +1 -0
- package/dist/index.d.ts +14 -10
- package/dist/index.js +20 -16
- package/dist/lib/cache.d.ts +8 -0
- package/dist/lib/cache.js +35 -0
- package/dist/lib/rate-limit.d.ts +35 -0
- package/dist/lib/rate-limit.js +121 -0
- package/dist/literature/sources/arxiv.d.ts +22 -0
- package/dist/literature/sources/arxiv.js +103 -0
- package/dist/literature/sources/http.d.ts +28 -0
- package/dist/literature/sources/http.js +97 -0
- package/dist/literature/sources/pubmed.d.ts +68 -0
- package/dist/literature/sources/pubmed.js +266 -0
- package/dist/literature/sources/semantic-scholar.d.ts +24 -0
- package/dist/literature/sources/semantic-scholar.js +71 -0
- package/dist/memory/content-to-cortex.d.ts +12 -11
- package/dist/memory/content-to-cortex.js +17 -14
- package/dist/memory/conversation-display-backfill.d.ts +25 -0
- package/dist/memory/conversation-display-backfill.js +97 -0
- package/dist/memory/conversation-display-recorder.d.ts +17 -0
- package/dist/memory/conversation-display-recorder.js +156 -0
- package/dist/memory/conversation-display-replay.d.ts +19 -0
- package/dist/memory/conversation-display-replay.js +39 -0
- package/dist/memory/conversation-display-storage.d.ts +86 -0
- package/dist/memory/conversation-display-storage.js +168 -0
- package/dist/memory/reconstruct-cards.d.ts +7 -1
- package/dist/memory/reconstruct-cards.js +5 -1
- package/dist/memory/thread-history.d.ts +46 -11
- package/dist/memory/thread-history.js +55 -13
- package/dist/memory/thread-store.d.ts +142 -25
- package/dist/memory/thread-store.js +228 -40
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +2 -0
- package/dist/runtime/assemble.d.ts +50 -4
- package/dist/runtime/assemble.js +47 -3
- package/dist/runtime/boot.d.ts +8 -2
- package/dist/runtime/boot.js +20 -2
- package/dist/state/init.js +50 -1
- package/dist/state/schema.d.ts +2 -2
- package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
- package/dist/tools/bio/keys.d.ts +1 -0
- package/dist/tools/bio/pubmed.js +4 -4
- package/dist/tools/lib/ncbi-utils.d.ts +3 -58
- package/dist/tools/lib/ncbi-utils.js +2 -286
- package/dist/tools/lib/pubmed-client.d.ts +5 -12
- package/dist/tools/lib/pubmed-client.js +20 -79
- package/dist/tools/research/generate-analogy-report.js +2 -2
- package/dist/tools/research/index.d.ts +1 -0
- package/dist/tools/research/index.js +1 -0
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +2 -0
- package/dist/tools/research/resolve-citation.d.ts +3 -0
- package/dist/tools/research/resolve-citation.js +15 -0
- package/dist/tools/research/search-arxiv.d.ts +3 -19
- package/dist/tools/research/search-arxiv.js +9 -105
- package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
- package/dist/tools/research/search-semantic-scholar.js +29 -88
- package/dist/tools/sandbox/list-available-refs.js +1 -2
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +1 -0
- package/dist/workflows/sandbox-step.d.ts +5 -0
- package/dist/workflows/sandbox-step.js +1 -0
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2 -2
- package/dist/workflows/target-assessment/schemas.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { requestJson, requestText } from "./http.js";
|
|
4
|
+
export const NCBI_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
|
|
5
|
+
export const NCBI_IDCONV = "https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0";
|
|
6
|
+
const EsearchResponseSchema = z.object({
|
|
7
|
+
esearchresult: z.object({ idlist: z.array(z.string()).optional(), count: z.string().optional() }).optional(),
|
|
8
|
+
});
|
|
9
|
+
const IdConvResponseSchema = z.object({
|
|
10
|
+
records: z.array(z.object({ pmid: z.string().optional(), pmcid: z.string().optional() })).optional(),
|
|
11
|
+
});
|
|
12
|
+
export function ncbiUrl(apiKey, base, params) {
|
|
13
|
+
const url = new URL(base);
|
|
14
|
+
if (apiKey !== undefined)
|
|
15
|
+
url.searchParams.set("api_key", apiKey);
|
|
16
|
+
for (const [key, value] of Object.entries(params))
|
|
17
|
+
if (value !== undefined)
|
|
18
|
+
url.searchParams.set(key, String(value));
|
|
19
|
+
return url.toString();
|
|
20
|
+
}
|
|
21
|
+
const xmlParser = new XMLParser({
|
|
22
|
+
ignoreAttributes: false,
|
|
23
|
+
attributeNamePrefix: "@_",
|
|
24
|
+
isArray: (name) => ["PubmedArticle", "Author", "MeshHeading", "AbstractText", "sec", "p", "record"].includes(name),
|
|
25
|
+
textNodeName: "#text",
|
|
26
|
+
});
|
|
27
|
+
function getItemValue(items, name) {
|
|
28
|
+
if (items === undefined)
|
|
29
|
+
return "";
|
|
30
|
+
const item = (Array.isArray(items) ? items : [items]).find((candidate) => candidate["@_Name"] === name);
|
|
31
|
+
return item?.["#text"] ?? "";
|
|
32
|
+
}
|
|
33
|
+
function getAuthorsFromDocSum(items) {
|
|
34
|
+
if (items === undefined)
|
|
35
|
+
return "";
|
|
36
|
+
const authorList = (Array.isArray(items) ? items : [items]).find((item) => item["@_Name"] === "AuthorList");
|
|
37
|
+
if (authorList?.Item === undefined)
|
|
38
|
+
return "";
|
|
39
|
+
const names = (Array.isArray(authorList.Item) ? authorList.Item : [authorList.Item]).map((author) => author["#text"] ?? "").filter(Boolean);
|
|
40
|
+
if (names.length <= 3)
|
|
41
|
+
return names.join(", ");
|
|
42
|
+
return `${names[0]} et al.`;
|
|
43
|
+
}
|
|
44
|
+
export function parseEsummary(xml) {
|
|
45
|
+
const parsed = xmlParser.parse(xml);
|
|
46
|
+
const docSums = (parsed?.eSummaryResult ?? parsed)?.DocSum;
|
|
47
|
+
if (docSums === undefined)
|
|
48
|
+
return [];
|
|
49
|
+
const documents = Array.isArray(docSums) ? docSums : [docSums];
|
|
50
|
+
return documents.map((document) => {
|
|
51
|
+
const publicationDate = getItemValue(document.Item, "PubDate");
|
|
52
|
+
return {
|
|
53
|
+
pmid: String(document.Id ?? ""),
|
|
54
|
+
title: getItemValue(document.Item, "Title"),
|
|
55
|
+
journal: getItemValue(document.Item, "FullJournalName") || getItemValue(document.Item, "Source"),
|
|
56
|
+
year: publicationDate.slice(0, 4),
|
|
57
|
+
authors: getAuthorsFromDocSum(document.Item),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function parseEfetch(xml) {
|
|
62
|
+
const articleSet = xmlParser.parse(xml)?.PubmedArticleSet;
|
|
63
|
+
if (articleSet === undefined)
|
|
64
|
+
return [];
|
|
65
|
+
const entries = Array.isArray(articleSet.PubmedArticle)
|
|
66
|
+
? articleSet.PubmedArticle
|
|
67
|
+
: articleSet.PubmedArticle === undefined
|
|
68
|
+
? []
|
|
69
|
+
: [articleSet.PubmedArticle];
|
|
70
|
+
return entries.map((entry) => {
|
|
71
|
+
const article = entry;
|
|
72
|
+
const citation = article.MedlineCitation;
|
|
73
|
+
const articleData = citation?.Article;
|
|
74
|
+
const pmidValue = citation?.PMID;
|
|
75
|
+
const pmid = typeof pmidValue === "object" && pmidValue !== null ? String(pmidValue["#text"] ?? "") : String(pmidValue ?? "");
|
|
76
|
+
const abstractValue = articleData?.Abstract?.AbstractText;
|
|
77
|
+
const abstract = (abstractValue === undefined ? [] : Array.isArray(abstractValue) ? abstractValue : [abstractValue])
|
|
78
|
+
.map((value) => {
|
|
79
|
+
if (typeof value === "string")
|
|
80
|
+
return value;
|
|
81
|
+
if (typeof value !== "object" || value === null)
|
|
82
|
+
return String(value);
|
|
83
|
+
const object = value;
|
|
84
|
+
const text = String(object["#text"] ?? "");
|
|
85
|
+
return object["@_Label"] === undefined ? text : `${String(object["@_Label"])}: ${text}`;
|
|
86
|
+
})
|
|
87
|
+
.join("\n\n");
|
|
88
|
+
const authorValue = articleData?.AuthorList?.Author;
|
|
89
|
+
const authors = (authorValue === undefined ? [] : Array.isArray(authorValue) ? authorValue : [authorValue]).map((author) => author.CollectiveName ?? [author.ForeName ?? author.Initials, author.LastName].filter(Boolean).join(" "));
|
|
90
|
+
const journalData = articleData?.Journal;
|
|
91
|
+
const publicationDate = journalData?.JournalIssue?.PubDate;
|
|
92
|
+
let doi = "";
|
|
93
|
+
const articleIds = article.PubmedData?.ArticleIdList?.ArticleId;
|
|
94
|
+
for (const id of articleIds === undefined ? [] : Array.isArray(articleIds) ? articleIds : [articleIds]) {
|
|
95
|
+
if (typeof id === "object" && id !== null && id["@_IdType"] === "doi") {
|
|
96
|
+
doi = String(id["#text"] ?? "");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const meshValue = citation?.MeshHeadingList?.MeshHeading;
|
|
100
|
+
const meshTerms = (meshValue === undefined ? [] : Array.isArray(meshValue) ? meshValue : [meshValue])
|
|
101
|
+
.map((heading) => {
|
|
102
|
+
const descriptor = heading.DescriptorName;
|
|
103
|
+
return typeof descriptor === "string" ? descriptor : (descriptor?.["#text"] ?? "");
|
|
104
|
+
})
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
return {
|
|
107
|
+
pmid,
|
|
108
|
+
title: String(articleData?.ArticleTitle ?? ""),
|
|
109
|
+
abstract,
|
|
110
|
+
authors,
|
|
111
|
+
journal: String(journalData?.Title ?? journalData?.ISOAbbreviation ?? ""),
|
|
112
|
+
year: String(publicationDate?.Year ?? ""),
|
|
113
|
+
doi,
|
|
114
|
+
meshTerms,
|
|
115
|
+
pmcId: null,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export function parseIdConvResponse(data) {
|
|
120
|
+
const result = new Map();
|
|
121
|
+
const records = data?.records ?? [];
|
|
122
|
+
for (const record of records)
|
|
123
|
+
if (record.pmid !== undefined && record.pmcid !== undefined)
|
|
124
|
+
result.set(record.pmid, record.pmcid);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function flattenTextContent(node) {
|
|
128
|
+
if (typeof node === "string" || typeof node === "number")
|
|
129
|
+
return String(node);
|
|
130
|
+
if (node === null || typeof node !== "object")
|
|
131
|
+
return "";
|
|
132
|
+
return Object.entries(node)
|
|
133
|
+
.flatMap(([key, value]) => {
|
|
134
|
+
if (key.startsWith("@_") || key === "fig" || key === "table-wrap" || key === "supplementary-material")
|
|
135
|
+
return [];
|
|
136
|
+
if (key === "#text")
|
|
137
|
+
return [String(value)];
|
|
138
|
+
return Array.isArray(value) ? value.map(flattenTextContent) : [flattenTextContent(value)];
|
|
139
|
+
})
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join("");
|
|
142
|
+
}
|
|
143
|
+
function extractParagraphs(container) {
|
|
144
|
+
const paragraphs = container.p;
|
|
145
|
+
if (paragraphs === undefined)
|
|
146
|
+
return "";
|
|
147
|
+
return (Array.isArray(paragraphs) ? paragraphs : [paragraphs])
|
|
148
|
+
.map((paragraph) => (typeof paragraph === "string" ? paragraph : flattenTextContent(paragraph)))
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join("\n\n");
|
|
151
|
+
}
|
|
152
|
+
function extractSection(section, output) {
|
|
153
|
+
if (section === null || typeof section !== "object")
|
|
154
|
+
return;
|
|
155
|
+
const object = section;
|
|
156
|
+
const text = extractParagraphs(object);
|
|
157
|
+
if (text)
|
|
158
|
+
output.push({ heading: typeof object.title === "string" ? object.title : "", text });
|
|
159
|
+
const nested = object.sec;
|
|
160
|
+
for (const child of nested === undefined ? [] : Array.isArray(nested) ? nested : [nested])
|
|
161
|
+
extractSection(child, output);
|
|
162
|
+
}
|
|
163
|
+
export function parsePmcFullText(xml) {
|
|
164
|
+
const article = xmlParser.parse(xml)?.["pmc-articleset"]?.article;
|
|
165
|
+
const body = article?.body;
|
|
166
|
+
if (body === undefined)
|
|
167
|
+
return null;
|
|
168
|
+
const sections = [];
|
|
169
|
+
for (const section of body.sec === undefined ? [] : Array.isArray(body.sec) ? body.sec : [body.sec])
|
|
170
|
+
extractSection(section, sections);
|
|
171
|
+
if (sections.length === 0) {
|
|
172
|
+
const text = extractParagraphs(body);
|
|
173
|
+
if (text)
|
|
174
|
+
sections.push({ heading: "", text });
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
fullText: sections.map((section) => (section.heading ? `## ${section.heading}\n\n${section.text}` : section.text)).join("\n\n"),
|
|
178
|
+
sections,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function createPubmedSource(options = {}) {
|
|
182
|
+
const url = (endpoint, params) => ncbiUrl(options.apiKey, endpoint, params);
|
|
183
|
+
async function searchIdsWithTotal(query, searchOptions, signal) {
|
|
184
|
+
const params = {
|
|
185
|
+
db: "pubmed",
|
|
186
|
+
term: query,
|
|
187
|
+
retmax: searchOptions.maxResults ?? 10,
|
|
188
|
+
retmode: "json",
|
|
189
|
+
sort: searchOptions.sort === "date" ? "pub+date" : "relevance",
|
|
190
|
+
};
|
|
191
|
+
if (searchOptions.dateRange !== undefined) {
|
|
192
|
+
params.mindate = searchOptions.dateRange.from;
|
|
193
|
+
params.maxdate = searchOptions.dateRange.to;
|
|
194
|
+
params.datetype = "pdat";
|
|
195
|
+
}
|
|
196
|
+
const result = await requestJson(url(`${NCBI_BASE}/esearch.fcgi`, params), EsearchResponseSchema, options, signal);
|
|
197
|
+
if (result.status !== "ok")
|
|
198
|
+
return result;
|
|
199
|
+
return {
|
|
200
|
+
status: "ok",
|
|
201
|
+
value: {
|
|
202
|
+
totalFound: parseInt(result.value.esearchresult?.count ?? "0", 10),
|
|
203
|
+
ids: result.value.esearchresult?.idlist ?? [],
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async function fetchArticles(pmids, signal) {
|
|
208
|
+
const result = await requestText(url(`${NCBI_BASE}/efetch.fcgi`, { db: "pubmed", id: pmids.join(","), rettype: "xml", retmode: "xml" }), options, signal);
|
|
209
|
+
if (result.status !== "ok")
|
|
210
|
+
return result;
|
|
211
|
+
if (!/<PubmedArticleSet\b/.test(result.value))
|
|
212
|
+
return { status: "unavailable", detail: "response schema mismatch: missing PubmedArticleSet" };
|
|
213
|
+
return { status: "ok", value: parseEfetch(result.value) };
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
async search(query, searchOptions = {}, signal) {
|
|
217
|
+
const search = await searchIdsWithTotal(query, searchOptions, signal);
|
|
218
|
+
if (search.status !== "ok")
|
|
219
|
+
return search;
|
|
220
|
+
if (search.value.ids.length === 0)
|
|
221
|
+
return { status: "ok", value: { totalFound: search.value.totalFound, results: [] } };
|
|
222
|
+
const summary = await requestText(url(`${NCBI_BASE}/esummary.fcgi`, { db: "pubmed", id: search.value.ids.join(","), retmode: "xml" }), options, signal);
|
|
223
|
+
if (summary.status !== "ok")
|
|
224
|
+
return summary;
|
|
225
|
+
return { status: "ok", value: { totalFound: search.value.totalFound, results: parseEsummary(summary.value) } };
|
|
226
|
+
},
|
|
227
|
+
async searchIds(query, maxResults, signal) {
|
|
228
|
+
const result = await searchIdsWithTotal(query, { maxResults }, signal);
|
|
229
|
+
return result.status === "ok" ? { status: "ok", value: result.value.ids } : result;
|
|
230
|
+
},
|
|
231
|
+
async matchCitation(fields, signal) {
|
|
232
|
+
const bdata = [fields.venue ?? "", fields.year ?? "", fields.volume ?? "", fields.firstPage ?? "", fields.firstAuthor ?? "", fields.key, ""].join("|");
|
|
233
|
+
const result = await requestText(url(`${NCBI_BASE}/ecitmatch.cgi`, { db: "pubmed", rettype: "xml", bdata }), options, signal);
|
|
234
|
+
if (result.status !== "ok")
|
|
235
|
+
return result;
|
|
236
|
+
const pmids = result.value
|
|
237
|
+
.trim()
|
|
238
|
+
.split(/\r?\n/)
|
|
239
|
+
.flatMap((line) => {
|
|
240
|
+
const value = line.split("|").at(-1)?.trim();
|
|
241
|
+
return value !== undefined && /^\d+$/.test(value) ? [value] : [];
|
|
242
|
+
});
|
|
243
|
+
return { status: "ok", value: pmids };
|
|
244
|
+
},
|
|
245
|
+
fetchArticles,
|
|
246
|
+
async articleDetails(pmids, signal) {
|
|
247
|
+
const [articlesResult, conversionResult] = await Promise.all([
|
|
248
|
+
fetchArticles(pmids, signal),
|
|
249
|
+
requestJson(url(NCBI_IDCONV, { ids: pmids.join(","), format: "json" }), IdConvResponseSchema, options, signal),
|
|
250
|
+
]);
|
|
251
|
+
if (articlesResult.status !== "ok")
|
|
252
|
+
return articlesResult;
|
|
253
|
+
if (conversionResult.status === "ok") {
|
|
254
|
+
const conversions = parseIdConvResponse(conversionResult.value);
|
|
255
|
+
for (const article of articlesResult.value)
|
|
256
|
+
article.pmcId = conversions.get(article.pmid) ?? null;
|
|
257
|
+
}
|
|
258
|
+
const found = new Set(articlesResult.value.map((article) => article.pmid));
|
|
259
|
+
return { status: "ok", value: { articles: articlesResult.value, notFound: pmids.filter((pmid) => !found.has(pmid)) } };
|
|
260
|
+
},
|
|
261
|
+
async fetchFullText(pmcId, signal) {
|
|
262
|
+
const result = await requestText(url(`${NCBI_BASE}/efetch.fcgi`, { db: "pmc", id: pmcId.replace(/^PMC/i, ""), rettype: "xml", retmode: "xml" }), options, signal);
|
|
263
|
+
return result.status === "ok" ? { status: "ok", value: parsePmcFullText(result.value) } : result;
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type SourceHttpOptions, type SourceHttpResult } from "./http.js";
|
|
2
|
+
export interface SemanticScholarPaper {
|
|
3
|
+
id: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
abstract?: string;
|
|
6
|
+
year?: number;
|
|
7
|
+
venue?: string;
|
|
8
|
+
citationCount?: number;
|
|
9
|
+
url?: string;
|
|
10
|
+
authors: string[];
|
|
11
|
+
externalIds?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export interface SemanticScholarSearchPaper extends SemanticScholarPaper {
|
|
14
|
+
title: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SemanticScholarSourceOptions extends SourceHttpOptions {
|
|
17
|
+
readonly apiKey?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SemanticScholarSource {
|
|
20
|
+
search(query: string, limit: number, signal?: AbortSignal): Promise<SourceHttpResult<SemanticScholarSearchPaper[]>>;
|
|
21
|
+
lookupIdentifier(identifier: string, signal?: AbortSignal): Promise<SourceHttpResult<SemanticScholarPaper>>;
|
|
22
|
+
}
|
|
23
|
+
export declare function parseSemanticScholarResponse(raw: unknown): SemanticScholarSearchPaper[];
|
|
24
|
+
export declare function createSemanticScholarSource(options?: SemanticScholarSourceOptions): SemanticScholarSource;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { requestJson } from "./http.js";
|
|
3
|
+
const BASE_URL = "https://api.semanticscholar.org/graph/v1";
|
|
4
|
+
const FIELDS = "paperId,title,abstract,year,venue,citationCount,url,authors,externalIds";
|
|
5
|
+
const WirePaperSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
paperId: z.string().optional(),
|
|
8
|
+
title: z.string().nullable().optional(),
|
|
9
|
+
abstract: z.string().nullable().optional(),
|
|
10
|
+
year: z.number().nullable().optional(),
|
|
11
|
+
venue: z.string().nullable().optional(),
|
|
12
|
+
citationCount: z.number().nullable().optional(),
|
|
13
|
+
url: z.string().nullable().optional(),
|
|
14
|
+
authors: z.array(z.object({ name: z.string().nullable().optional() })).optional(),
|
|
15
|
+
externalIds: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
16
|
+
})
|
|
17
|
+
.passthrough();
|
|
18
|
+
const ExactPaperSchema = WirePaperSchema.extend({ paperId: z.string() });
|
|
19
|
+
const SearchResponseSchema = z.object({ data: z.array(WirePaperSchema).optional() });
|
|
20
|
+
function stringExternalIds(value) {
|
|
21
|
+
if (value === null || value === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
24
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
25
|
+
}
|
|
26
|
+
function mapPaper(raw) {
|
|
27
|
+
if (raw.paperId === undefined || raw.paperId.trim() === "")
|
|
28
|
+
return undefined;
|
|
29
|
+
const authors = raw.authors?.flatMap((author) => (typeof author.name === "string" && author.name.trim() ? [author.name] : [])) ?? [];
|
|
30
|
+
const externalIds = stringExternalIds(raw.externalIds);
|
|
31
|
+
return {
|
|
32
|
+
id: raw.paperId,
|
|
33
|
+
...(typeof raw.title === "string" ? { title: raw.title } : {}),
|
|
34
|
+
...(typeof raw.abstract === "string" ? { abstract: raw.abstract } : {}),
|
|
35
|
+
...(typeof raw.year === "number" ? { year: raw.year } : {}),
|
|
36
|
+
...(typeof raw.venue === "string" ? { venue: raw.venue } : {}),
|
|
37
|
+
...(typeof raw.citationCount === "number" ? { citationCount: raw.citationCount } : {}),
|
|
38
|
+
...(typeof raw.url === "string" ? { url: raw.url } : {}),
|
|
39
|
+
authors,
|
|
40
|
+
...(externalIds === undefined ? {} : { externalIds }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function parseSemanticScholarResponse(raw) {
|
|
44
|
+
const parsed = SearchResponseSchema.safeParse(raw);
|
|
45
|
+
if (!parsed.success)
|
|
46
|
+
return [];
|
|
47
|
+
return (parsed.data.data ?? []).flatMap((paper) => {
|
|
48
|
+
const mapped = mapPaper(paper);
|
|
49
|
+
return mapped?.title === undefined ? [] : [mapped];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function createSemanticScholarSource(options = {}) {
|
|
53
|
+
const headers = options.apiKey === undefined ? {} : { "x-api-key": options.apiKey };
|
|
54
|
+
return {
|
|
55
|
+
async search(query, limit, signal) {
|
|
56
|
+
const params = new URLSearchParams({ query, limit: String(limit), fields: FIELDS });
|
|
57
|
+
const result = await requestJson(`${BASE_URL}/paper/search?${params}`, SearchResponseSchema, options, signal, { headers });
|
|
58
|
+
if (result.status !== "ok")
|
|
59
|
+
return result;
|
|
60
|
+
return { status: "ok", value: parseSemanticScholarResponse(result.value) };
|
|
61
|
+
},
|
|
62
|
+
async lookupIdentifier(identifier, signal) {
|
|
63
|
+
const url = `${BASE_URL}/paper/${encodeURIComponent(identifier)}?fields=${encodeURIComponent(FIELDS)}`;
|
|
64
|
+
const result = await requestJson(url, ExactPaperSchema, options, signal, { headers });
|
|
65
|
+
if (result.status !== "ok")
|
|
66
|
+
return result;
|
|
67
|
+
const paper = mapPaper(result.value);
|
|
68
|
+
return paper === undefined ? { status: "unavailable", detail: "response schema mismatch: missing paperId" } : { status: "ok", value: paper };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The MIGRATION renderer: stored AI SDK model messages → `CortexMessage[]`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* is
|
|
6
|
-
*
|
|
4
|
+
* This is how a conversation written before display projections were persisted
|
|
5
|
+
* is rendered ONCE, at startup, so it can be frozen into a display envelope
|
|
6
|
+
* (`conversation-display-backfill.ts`). It is not a runtime read path — the
|
|
7
|
+
* runtime reads the stored projection (`conversation-display-replay.ts`) and
|
|
8
|
+
* never consults a tool name, a card builder, or the filesystem.
|
|
9
|
+
*
|
|
10
|
+
* Everything it does is inference from a transcript that was never meant to
|
|
11
|
+
* carry display, which is why it belongs to the migration and nothing else:
|
|
7
12
|
*
|
|
8
13
|
* - text part → text part
|
|
9
14
|
* - tool-call part → a reconstructed display card (`data-plan`,
|
|
10
15
|
* `data-presentation`) when `resolveCard` recognises
|
|
11
|
-
* the tool, else a generic tool-call part
|
|
16
|
+
* the tool, else a generic tool-call part whose detail
|
|
17
|
+
* `resolveDetail` recomputes from the persisted input
|
|
12
18
|
* - everything else → dropped (reasoning, tool-result, etc.) — the UI
|
|
13
19
|
* does not render them
|
|
14
20
|
*
|
|
15
|
-
* Display cards are emitted live over the chat SSE stream but never persisted
|
|
16
|
-
* (storage holds only the AI SDK model-message transcript). `resolveCard`
|
|
17
|
-
* rebuilds them from the persisted tool-call part so they reappear on reload,
|
|
18
|
-
* and `resolveDetail` rebuilds each call's one-line detail the same way.
|
|
19
|
-
*
|
|
20
21
|
* A `tool-result` block is not rendered, but it is not ignored either: it is the
|
|
21
22
|
* only record of how its call ended. The blocks are indexed by `toolCallId`
|
|
22
23
|
* before conversion so each tool-call part can report its own outcome — without
|
|
23
|
-
* that pass every
|
|
24
|
+
* that pass every migrated call would report success, including the failed ones.
|
|
24
25
|
*
|
|
25
26
|
* A row that yields no renderable parts (e.g. a `tool`-role message carrying
|
|
26
27
|
* only tool-result continuation parts) is omitted entirely, so the rendered
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The MIGRATION renderer: stored AI SDK model messages → `CortexMessage[]`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* is
|
|
6
|
-
*
|
|
4
|
+
* This is how a conversation written before display projections were persisted
|
|
5
|
+
* is rendered ONCE, at startup, so it can be frozen into a display envelope
|
|
6
|
+
* (`conversation-display-backfill.ts`). It is not a runtime read path — the
|
|
7
|
+
* runtime reads the stored projection (`conversation-display-replay.ts`) and
|
|
8
|
+
* never consults a tool name, a card builder, or the filesystem.
|
|
9
|
+
*
|
|
10
|
+
* Everything it does is inference from a transcript that was never meant to
|
|
11
|
+
* carry display, which is why it belongs to the migration and nothing else:
|
|
7
12
|
*
|
|
8
13
|
* - text part → text part
|
|
9
14
|
* - tool-call part → a reconstructed display card (`data-plan`,
|
|
10
15
|
* `data-presentation`) when `resolveCard` recognises
|
|
11
|
-
* the tool, else a generic tool-call part
|
|
16
|
+
* the tool, else a generic tool-call part whose detail
|
|
17
|
+
* `resolveDetail` recomputes from the persisted input
|
|
12
18
|
* - everything else → dropped (reasoning, tool-result, etc.) — the UI
|
|
13
19
|
* does not render them
|
|
14
20
|
*
|
|
15
|
-
* Display cards are emitted live over the chat SSE stream but never persisted
|
|
16
|
-
* (storage holds only the AI SDK model-message transcript). `resolveCard`
|
|
17
|
-
* rebuilds them from the persisted tool-call part so they reappear on reload,
|
|
18
|
-
* and `resolveDetail` rebuilds each call's one-line detail the same way.
|
|
19
|
-
*
|
|
20
21
|
* A `tool-result` block is not rendered, but it is not ignored either: it is the
|
|
21
22
|
* only record of how its call ended. The blocks are indexed by `toolCallId`
|
|
22
23
|
* before conversion so each tool-call part can report its own outcome — without
|
|
23
|
-
* that pass every
|
|
24
|
+
* that pass every migrated call would report success, including the failed ones.
|
|
24
25
|
*
|
|
25
26
|
* A row that yields no renderable parts (e.g. a `tool`-role message carrying
|
|
26
27
|
* only tool-result continuation parts) is omitted entirely, so the rendered
|
|
@@ -50,14 +51,16 @@ function indexOutcomes(messages) {
|
|
|
50
51
|
}
|
|
51
52
|
function genericToolCall(block, outcomes, resolveDetail) {
|
|
52
53
|
// No paired result is the shape of a transcript whose `tool` row was never
|
|
53
|
-
// appended
|
|
54
|
-
|
|
54
|
+
// appended — an aborted turn. That is not a success: the call was dispatched and
|
|
55
|
+
// never completed, which is exactly `incomplete`. Reporting `ok` would claim a
|
|
56
|
+
// result the tool never returned, and it is the one outcome the transcript
|
|
57
|
+
// genuinely cannot supply, so it is named rather than guessed at.
|
|
58
|
+
const outcome = outcomes.get(block.toolCallId) ?? "incomplete";
|
|
55
59
|
const detail = resolveDetail?.(block.toolName, block.input);
|
|
56
60
|
return {
|
|
57
61
|
type: "tool-call",
|
|
58
62
|
toolCallId: block.toolCallId,
|
|
59
63
|
toolName: block.toolName,
|
|
60
|
-
status: "finished",
|
|
61
64
|
outcome,
|
|
62
65
|
...(detail === undefined ? {} : { detail }),
|
|
63
66
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import type { Tool } from "../tools/define-tool.js";
|
|
3
|
+
import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
4
|
+
export interface ConversationDisplayBackfillDeps {
|
|
5
|
+
readonly pool: Pool;
|
|
6
|
+
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
7
|
+
/**
|
|
8
|
+
* The conversation agent's composed roster, used to recover each legacy call's
|
|
9
|
+
* one-line detail from its persisted input. Tools contributed through the
|
|
10
|
+
* host-tools seam are invisible to any map the harness holds on its own, so the
|
|
11
|
+
* list has to come from the assembled runtime.
|
|
12
|
+
*/
|
|
13
|
+
readonly tools: readonly Tool[];
|
|
14
|
+
readonly batchSize?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Freeze the migration renderer's output into display envelopes, once per legacy
|
|
18
|
+
* turn, so the runtime read path never has to reconstruct anything.
|
|
19
|
+
*
|
|
20
|
+
* Missing mutable resources degrade to a generic, cardless display — a workspace
|
|
21
|
+
* that has moved is normal historical absence, not a fault. Malformed stored rows
|
|
22
|
+
* and database faults remain startup-fatal: a turn that cannot be migrated must
|
|
23
|
+
* not be silently skipped and then read as if it had no display.
|
|
24
|
+
*/
|
|
25
|
+
export declare function backfillConversationDisplayEnvelopes(deps: ConversationDisplayBackfillDeps): Promise<number>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createDetailResolver } from "../tools/detail-resolver.js";
|
|
2
|
+
import { envelopeDisplayMessages, cortexMessagesToConversationUI, parseStoredDisplayEnvelope } from "./conversation-display-storage.js";
|
|
3
|
+
import { parseStoredMessageEnvelope } from "./ai-sdk-message-storage.js";
|
|
4
|
+
import { contentToCortexMessages } from "./content-to-cortex.js";
|
|
5
|
+
import { createCardResolver } from "./reconstruct-cards.js";
|
|
6
|
+
const BATCH_SIZE = 100;
|
|
7
|
+
const GENUINE_USER_SQL = `m.message_envelope->'message'->>'role' = 'user'
|
|
8
|
+
AND m.message_envelope->'message'->'providerOptions'->'cortex'->>'synthetic' IS DISTINCT FROM 'true'`;
|
|
9
|
+
/**
|
|
10
|
+
* Freeze the migration renderer's output into display envelopes, once per legacy
|
|
11
|
+
* turn, so the runtime read path never has to reconstruct anything.
|
|
12
|
+
*
|
|
13
|
+
* Missing mutable resources degrade to a generic, cardless display — a workspace
|
|
14
|
+
* that has moved is normal historical absence, not a fault. Malformed stored rows
|
|
15
|
+
* and database faults remain startup-fatal: a turn that cannot be migrated must
|
|
16
|
+
* not be silently skipped and then read as if it had no display.
|
|
17
|
+
*/
|
|
18
|
+
export async function backfillConversationDisplayEnvelopes(deps) {
|
|
19
|
+
const batchSize = Math.max(1, deps.batchSize ?? BATCH_SIZE);
|
|
20
|
+
const resolveDetail = createDetailResolver(deps.tools);
|
|
21
|
+
let migrated = 0;
|
|
22
|
+
let validationOffset = 0;
|
|
23
|
+
for (;;) {
|
|
24
|
+
const { rows } = await deps.pool.query(`SELECT thread_id, seq::text AS seq, display_envelope
|
|
25
|
+
FROM messages
|
|
26
|
+
WHERE display_envelope IS NOT NULL
|
|
27
|
+
ORDER BY thread_id, seq
|
|
28
|
+
LIMIT $1 OFFSET $2`, [batchSize, validationOffset]);
|
|
29
|
+
await Promise.all(rows.map((row) => parseStoredDisplayEnvelope(row.display_envelope, `${row.thread_id}/${row.seq}/display`)));
|
|
30
|
+
if (rows.length < batchSize)
|
|
31
|
+
break;
|
|
32
|
+
validationOffset += rows.length;
|
|
33
|
+
}
|
|
34
|
+
for (;;) {
|
|
35
|
+
const { rows: heads } = await deps.pool.query(`SELECT m.thread_id, m.seq::text AS seq, t.analysis_id
|
|
36
|
+
FROM messages m
|
|
37
|
+
LEFT JOIN cortex_analysis_threads t ON t.thread_id = m.thread_id
|
|
38
|
+
WHERE ${GENUINE_USER_SQL}
|
|
39
|
+
AND m.display_envelope IS NULL
|
|
40
|
+
ORDER BY m.thread_id, m.seq
|
|
41
|
+
LIMIT $1`, [batchSize]);
|
|
42
|
+
if (heads.length === 0)
|
|
43
|
+
return migrated;
|
|
44
|
+
for (const head of heads) {
|
|
45
|
+
const { rows } = await deps.pool.query(`SELECT m.seq::text AS seq, m.message_envelope
|
|
46
|
+
FROM messages m
|
|
47
|
+
WHERE m.thread_id = $1
|
|
48
|
+
AND m.seq >= $2::bigint
|
|
49
|
+
AND m.seq < COALESCE(
|
|
50
|
+
(
|
|
51
|
+
SELECT MIN(next.seq)
|
|
52
|
+
FROM messages next
|
|
53
|
+
WHERE next.thread_id = m.thread_id
|
|
54
|
+
AND next.seq > $2::bigint
|
|
55
|
+
AND next.message_envelope->'message'->>'role' = 'user'
|
|
56
|
+
AND next.message_envelope->'message'->'providerOptions'->'cortex'->>'synthetic' IS DISTINCT FROM 'true'
|
|
57
|
+
),
|
|
58
|
+
9223372036854775807
|
|
59
|
+
)
|
|
60
|
+
ORDER BY m.seq`, [head.thread_id, head.seq]);
|
|
61
|
+
const stored = rows.map((row) => {
|
|
62
|
+
const envelope = parseStoredMessageEnvelope(row.message_envelope, `${head.thread_id}/${row.seq}`);
|
|
63
|
+
return { seq: Number(row.seq), envelope, message: envelope.message };
|
|
64
|
+
});
|
|
65
|
+
let resolveCard;
|
|
66
|
+
if (head.analysis_id !== null) {
|
|
67
|
+
try {
|
|
68
|
+
resolveCard = createCardResolver(deps.pool, head.analysis_id, deps.resolveWorkspaceRoot(head.analysis_id));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// A removed/moved workspace is normal historical absence.
|
|
72
|
+
resolveCard = undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const cortex = await contentToCortexMessages(stored, { resolveCard, resolveDetail });
|
|
76
|
+
const uiMessages = cortexMessagesToConversationUI(cortex);
|
|
77
|
+
// A genuine turn always contributes at least its user message. Keep a
|
|
78
|
+
// defensive model-derived fallback so absence can never strand a NULL
|
|
79
|
+
// envelope and make startup retry forever.
|
|
80
|
+
const display = uiMessages.length > 0
|
|
81
|
+
? uiMessages
|
|
82
|
+
: [
|
|
83
|
+
{
|
|
84
|
+
id: head.seq,
|
|
85
|
+
role: "user",
|
|
86
|
+
parts: [{ type: "text", text: "", state: "done" }],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
const envelope = envelopeDisplayMessages(display);
|
|
90
|
+
const result = await deps.pool.query(`UPDATE messages
|
|
91
|
+
SET display_envelope = $1::jsonb
|
|
92
|
+
WHERE thread_id = $2 AND seq = $3::bigint
|
|
93
|
+
AND display_envelope IS NULL`, [JSON.stringify(envelope), head.thread_id, head.seq]);
|
|
94
|
+
migrated += result.rowCount ?? 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EmitFn } from "../loop/types.js";
|
|
2
|
+
import { type ConversationUIMessage } from "./conversation-display-storage.js";
|
|
3
|
+
export interface ConversationDisplayRecorder {
|
|
4
|
+
readonly emit: EmitFn;
|
|
5
|
+
finish(options?: {
|
|
6
|
+
readonly fallbackText?: string;
|
|
7
|
+
readonly interrupted?: boolean;
|
|
8
|
+
}): ConversationUIMessage[];
|
|
9
|
+
}
|
|
10
|
+
export interface ConversationDisplayRecorderOptions {
|
|
11
|
+
readonly userText: string;
|
|
12
|
+
readonly topLevelCallPath: readonly string[];
|
|
13
|
+
readonly sink: EmitFn;
|
|
14
|
+
readonly userMessageId?: string;
|
|
15
|
+
readonly assistantMessageId?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function createConversationDisplayRecorder(options: ConversationDisplayRecorderOptions): ConversationDisplayRecorder;
|