@inflexa-ai/harness 0.48.2 → 0.48.4
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/sandbox/index.js +3 -0
- package/dist/agents/sandbox/mdv-preparation-agent.d.ts +9 -0
- package/dist/agents/sandbox/mdv-preparation-agent.js +37 -0
- package/dist/app/chat-turn-workflow.d.ts +3 -1
- package/dist/app/chat-turn-workflow.js +19 -3
- package/dist/app/chat-turn.d.ts +4 -2
- package/dist/app/chat-turn.js +70 -44
- package/dist/app/in-process-turn.js +3 -6
- package/dist/app/work.d.ts +7 -3
- package/dist/app/work.js +17 -16
- package/dist/contracts/chat-events.d.ts +5 -0
- package/dist/contracts/chat-parts.d.ts +64 -5
- package/dist/contracts/chat-parts.js +3 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +2 -0
- package/dist/contracts/schemas/chat-events.d.ts +2 -0
- package/dist/contracts/schemas/chat-events.js +1 -0
- package/dist/contracts/schemas/chat-parts.d.ts +197 -5
- package/dist/contracts/schemas/chat-parts.js +32 -2
- package/dist/index.d.ts +1 -1
- package/dist/lib/regulatory-corpus/fetch-ich.js +1 -16
- package/dist/lib/slug.d.ts +18 -0
- package/dist/lib/slug.js +29 -0
- package/dist/lib/test/pdf-fixture.d.ts +5 -0
- package/dist/lib/test/pdf-fixture.js +14 -0
- package/dist/loop/chat-stream.js +1 -1
- package/dist/loop/run-loop.js +39 -35
- package/dist/loop/sub-agent.d.ts +2 -2
- package/dist/loop/sub-agent.js +1 -1
- package/dist/loop/test/metric-reader.d.ts +9 -0
- package/dist/loop/test/metric-reader.js +33 -0
- package/dist/loop/test/unnamed-model.d.ts +6 -0
- package/dist/loop/test/unnamed-model.js +24 -0
- package/dist/memory/conversation-display-recorder.d.ts +2 -0
- package/dist/memory/conversation-display-recorder.js +6 -2
- package/dist/memory/conversation-display-storage.d.ts +5 -2
- package/dist/memory/conversation-display-storage.js +28 -1
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +6 -4
- package/dist/prompts/sandbox/mdv-preparation-agent.d.ts +1 -0
- package/dist/prompts/sandbox/mdv-preparation-agent.js +56 -0
- package/dist/providers/ai-sdk.d.ts +17 -14
- package/dist/providers/ai-sdk.js +14 -16
- package/dist/report-model/fixture-resolver.js +9 -135
- package/dist/report-model/production-resolver.d.ts +3 -3
- package/dist/report-model/production-resolver.js +12 -127
- package/dist/report-model/reference-read.d.ts +38 -0
- package/dist/report-model/reference-read.js +149 -0
- package/dist/report-model/reference-resolver.d.ts +7 -0
- package/dist/report-model/reference-resolver.js +1 -1
- package/dist/report-render/chart.js +48 -46
- package/dist/report-render/figures/common.d.ts +34 -0
- package/dist/report-render/figures/common.js +4 -1
- package/dist/report-render/figures/equal-units.js +2 -4
- package/dist/report-render/figures/sankey.js +1 -3
- package/dist/report-render/figures/upset.js +2 -5
- package/dist/runtime/assemble.js +2 -1
- package/dist/sandbox/execution-id.js +2 -14
- package/dist/sandbox/leases.js +46 -9
- package/dist/schemas/validate-plan.js +2 -14
- package/dist/state/active-turns.d.ts +6 -2
- package/dist/state/active-turns.js +11 -7
- package/dist/state/data-profile.d.ts +8 -0
- package/dist/state/data-profile.js +18 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.d.ts +6 -0
- package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.js +10 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/paused-work.d.ts +5 -3
- package/dist/state/paused-work.js +11 -7
- package/dist/state/runs.d.ts +3 -2
- package/dist/state/runs.js +4 -3
- package/dist/tasks/data-profile-index.js +3 -17
- package/dist/tasks/data-profile-resolve.js +3 -17
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/lib/opentargets-client.js +15 -25
- package/dist/tools/report-authoring/authoring-tools.d.ts +3 -3
- package/dist/tools/report-authoring/authoring-tools.js +7 -6
- package/dist/tools/research/generate-analogy-report.js +10 -2
- package/dist/workflows/execute-analysis.js +4 -3
- package/dist/workflows/pause.d.ts +3 -3
- package/dist/workflows/pause.js +15 -9
- package/dist/workflows/sandbox-step.js +1 -0
- package/package.json +7 -4
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One ChatProvider whose stream answers one text delta and one finish that carries the given input-token
|
|
3
|
+
* total and no output tokens. The usage suites feed it to the loop and read the counters back.
|
|
4
|
+
*/
|
|
5
|
+
import { convertArrayToReadableStream, MockLanguageModelV4 } from "ai/test";
|
|
6
|
+
import { createAiSdkProvider } from "../../providers/ai-sdk.js";
|
|
7
|
+
export function unnamedModel(inputTokens) {
|
|
8
|
+
const parts = [
|
|
9
|
+
{ type: "stream-start", warnings: [] },
|
|
10
|
+
{ type: "text-start", id: "p0" },
|
|
11
|
+
{ type: "text-delta", id: "p0", delta: "done" },
|
|
12
|
+
{ type: "text-end", id: "p0" },
|
|
13
|
+
{
|
|
14
|
+
type: "finish",
|
|
15
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
16
|
+
usage: {
|
|
17
|
+
inputTokens: { total: inputTokens, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
|
|
18
|
+
outputTokens: { total: undefined, text: undefined, reasoning: undefined },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
const model = new MockLanguageModelV4({ provider: "unnamed", doStream: () => Promise.resolve({ stream: convertArrayToReadableStream(parts) }) });
|
|
23
|
+
return createAiSdkProvider({ model, maxRetries: 0 });
|
|
24
|
+
}
|
|
@@ -17,6 +17,8 @@ export type ConversationDisplayRecorder = {
|
|
|
17
17
|
takeOpening(): ConversationUIMessage[];
|
|
18
18
|
/** The assistant message of one round, when the recorder got parts after the last take, and the divider of a marker of the round. */
|
|
19
19
|
takeRound(messages: readonly ModelMessage[]): ConversationUIMessage[];
|
|
20
|
+
/** The `data-pause` parts of the open round, in one assistant message with the id of the round, or none. The round stays open. */
|
|
21
|
+
openRoundPauses(): ConversationUIMessage[];
|
|
20
22
|
/** The whole turn in one projection. `runChatTurn` stores each round with `takeOpening` and `takeRound` instead. */
|
|
21
23
|
finish(options?: {
|
|
22
24
|
readonly fallbackText?: string;
|
|
@@ -8,7 +8,7 @@ function jsonCopy(value) {
|
|
|
8
8
|
return structuredClone(value);
|
|
9
9
|
}
|
|
10
10
|
const DURABLE_CONVERSATION_PART_TYPES = new Set(Object.entries(PART_REGISTRY)
|
|
11
|
-
.filter(([, descriptor]) => descriptor.
|
|
11
|
+
.filter(([, descriptor]) => descriptor.consumer === "conversation" && !descriptor.transient)
|
|
12
12
|
.map(([type]) => type));
|
|
13
13
|
function durableConversationType(type) {
|
|
14
14
|
return DURABLE_CONVERSATION_PART_TYPES.has(type);
|
|
@@ -167,6 +167,10 @@ export function createConversationDisplayRecorder(options) {
|
|
|
167
167
|
}
|
|
168
168
|
return taken;
|
|
169
169
|
}
|
|
170
|
+
function openRoundPauses() {
|
|
171
|
+
const parts = displayParts(roundParts().filter((part) => part.type === "data-pause"));
|
|
172
|
+
return parts.length === 0 ? [] : [{ id: assistantMessageId, role: "assistant", parts: jsonCopy(parts) }];
|
|
173
|
+
}
|
|
170
174
|
function finish(finishOptions) {
|
|
171
175
|
if (!finished) {
|
|
172
176
|
finished = true;
|
|
@@ -187,5 +191,5 @@ export function createConversationDisplayRecorder(options) {
|
|
|
187
191
|
}
|
|
188
192
|
return messages;
|
|
189
193
|
}
|
|
190
|
-
return { emit, observe, assistantMessageId: () => assistantMessageId, userMessageId: () => userMessageId, takeOpening, takeRound, finish };
|
|
194
|
+
return { emit, observe, assistantMessageId: () => assistantMessageId, userMessageId: () => userMessageId, takeOpening, takeRound, openRoundPauses, finish };
|
|
191
195
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* reconstruction.
|
|
16
16
|
*/
|
|
17
17
|
import { type DataUIPart, type UIMessage } from "ai";
|
|
18
|
-
import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
|
|
18
|
+
import type { AnalogyReportPart, AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, ModelRefusal, ModelRefusalPart, PausePart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
|
|
19
19
|
import type { ToolCallOutcome } from "../contracts/chat-events.js";
|
|
20
20
|
import type { ChatMessage, MessagePart, ToolCallPart } from "../contracts/message.js";
|
|
21
21
|
import type { Logger } from "../lib/logger.js";
|
|
@@ -35,6 +35,7 @@ export type ConversationUIData = {
|
|
|
35
35
|
outcome: ToolCallOutcome;
|
|
36
36
|
};
|
|
37
37
|
presentation: Payload<PresentationPart>;
|
|
38
|
+
"analogy-report": Payload<AnalogyReportPart>;
|
|
38
39
|
plan: Payload<PlanPart>;
|
|
39
40
|
"run-card": Payload<RunCardPart>;
|
|
40
41
|
"plan-approval": Payload<PlanApprovalPart>;
|
|
@@ -43,6 +44,8 @@ export type ConversationUIData = {
|
|
|
43
44
|
"child-session-started": Payload<ChildSessionStartedPart>;
|
|
44
45
|
"report-rendered": Payload<ReportRenderedPart>;
|
|
45
46
|
compaction: Payload<CompactionPart>;
|
|
47
|
+
"model-refusal": ModelRefusal;
|
|
48
|
+
pause: Payload<PausePart>;
|
|
46
49
|
"subagent-activity": Omit<Payload<SubAgentActivityPart>, "entries"> & {
|
|
47
50
|
entries: (Omit<SubAgentActivityEntry, "outcome"> & {
|
|
48
51
|
outcome: ToolCallOutcome;
|
|
@@ -65,7 +68,7 @@ export declare function parseStoredDisplayEnvelope(value: unknown, identity: str
|
|
|
65
68
|
export type StoredSourcePart = Extract<Exclude<MessagePart, {
|
|
66
69
|
type: "text";
|
|
67
70
|
}>, {
|
|
68
|
-
type: ToolCallPart["type"] | PresentationPart["type"] | PlanPart["type"] | RunCardPart["type"] | PlanApprovalPart["type"] | FileReferencePart["type"] | AskPart["type"] | ChildSessionStartedPart["type"] | ReportRenderedPart["type"] | CompactionPart["type"] | SubAgentActivityPart["type"];
|
|
71
|
+
type: ToolCallPart["type"] | PresentationPart["type"] | AnalogyReportPart["type"] | PlanPart["type"] | RunCardPart["type"] | PlanApprovalPart["type"] | FileReferencePart["type"] | AskPart["type"] | ChildSessionStartedPart["type"] | ReportRenderedPart["type"] | CompactionPart["type"] | ModelRefusalPart["type"] | PausePart["type"] | SubAgentActivityPart["type"];
|
|
69
72
|
}>;
|
|
70
73
|
/** Whether `part` is an arm the stored vocabulary can hold; anything else cannot become a stored card. */
|
|
71
74
|
export declare function isStoredSourcePart(part: Exclude<MessagePart, {
|
|
@@ -19,7 +19,7 @@ import { textOf } from "../lib/unknown-text.js";
|
|
|
19
19
|
import { isDataUIPart, validateUIMessages } from "ai";
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { parseUnknownRecord } from "../lib/unknown-containers.js";
|
|
22
|
-
import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
22
|
+
import { AnalogyReportPartSchema, AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, ModelRefusalPayloadSchema, PausePartSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
23
23
|
import { ToolCallOutcomeSchema, ToolCallValueSchema } from "../contracts/schemas/chat-events.js";
|
|
24
24
|
export const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
|
|
25
25
|
const DisplayEnvelopeHeaderSchema = z.object({
|
|
@@ -69,6 +69,7 @@ const ToolCallDisplaySchema = z.object({
|
|
|
69
69
|
const dataSchemas = {
|
|
70
70
|
"tool-call": ToolCallDisplaySchema,
|
|
71
71
|
presentation: PresentationPartSchema.omit({ type: true }),
|
|
72
|
+
"analogy-report": AnalogyReportPartSchema.omit({ type: true }),
|
|
72
73
|
plan: PlanPartSchema.omit({ type: true }),
|
|
73
74
|
"run-card": RunCardPartSchema.omit({ type: true }),
|
|
74
75
|
"plan-approval": PlanApprovalPartSchema.omit({ type: true }),
|
|
@@ -77,6 +78,8 @@ const dataSchemas = {
|
|
|
77
78
|
"child-session-started": ChildSessionStartedPartSchema.omit({ type: true }),
|
|
78
79
|
"report-rendered": ReportRenderedPartSchema.omit({ type: true }),
|
|
79
80
|
compaction: CompactionPartSchema.omit({ type: true }),
|
|
81
|
+
"model-refusal": ModelRefusalPayloadSchema,
|
|
82
|
+
pause: PausePartSchema.omit({ type: true }),
|
|
80
83
|
"subagent-activity": SubAgentActivityPartSchema.omit({ type: true }).extend({
|
|
81
84
|
entries: z.array(SubAgentActivityEntrySchema.extend({ outcome: ToolCallOutcomeSchema })),
|
|
82
85
|
}),
|
|
@@ -174,6 +177,7 @@ export async function parseStoredDisplayEnvelope(value, identity, logger) {
|
|
|
174
177
|
const STORED_SOURCE_TYPES = [
|
|
175
178
|
"tool-call",
|
|
176
179
|
"data-presentation",
|
|
180
|
+
"data-analogy-report",
|
|
177
181
|
"data-plan",
|
|
178
182
|
"data-run-card",
|
|
179
183
|
"data-plan-approval",
|
|
@@ -182,6 +186,8 @@ const STORED_SOURCE_TYPES = [
|
|
|
182
186
|
"data-child-session-started",
|
|
183
187
|
"data-report-rendered",
|
|
184
188
|
"data-compaction",
|
|
189
|
+
"data-model-refusal",
|
|
190
|
+
"data-pause",
|
|
185
191
|
"data-subagent-activity",
|
|
186
192
|
];
|
|
187
193
|
const STORED_SOURCE_TYPE_SET = new Set(STORED_SOURCE_TYPES);
|
|
@@ -223,6 +229,10 @@ export function conversationDisplayPart(part) {
|
|
|
223
229
|
const { type: _type, ...data } = part;
|
|
224
230
|
return { type: "data-presentation", id, data };
|
|
225
231
|
}
|
|
232
|
+
case "data-analogy-report": {
|
|
233
|
+
const { type: _type, ...data } = part;
|
|
234
|
+
return { type: "data-analogy-report", id, data };
|
|
235
|
+
}
|
|
226
236
|
case "data-plan": {
|
|
227
237
|
const { type: _type, ...data } = part;
|
|
228
238
|
return { type: "data-plan", id, data };
|
|
@@ -255,6 +265,14 @@ export function conversationDisplayPart(part) {
|
|
|
255
265
|
const { type: _type, ...data } = part;
|
|
256
266
|
return { type: "data-compaction", id, data };
|
|
257
267
|
}
|
|
268
|
+
case "data-model-refusal": {
|
|
269
|
+
const { type: _type, ...data } = part;
|
|
270
|
+
return { type: "data-model-refusal", id, data };
|
|
271
|
+
}
|
|
272
|
+
case "data-pause": {
|
|
273
|
+
const { type: _type, ...data } = part;
|
|
274
|
+
return { type: "data-pause", id, data };
|
|
275
|
+
}
|
|
258
276
|
case "data-subagent-activity": {
|
|
259
277
|
const { type: _type, ...payload } = part;
|
|
260
278
|
return { type: "data-subagent-activity", id, data: { ...payload, entries: part.entries.map(closedEntry) } };
|
|
@@ -291,6 +309,9 @@ export function conversationUIToChatMessages(messages) {
|
|
|
291
309
|
case "data-presentation":
|
|
292
310
|
parts.push({ type: "data-presentation", ...part.data });
|
|
293
311
|
break;
|
|
312
|
+
case "data-analogy-report":
|
|
313
|
+
parts.push({ type: "data-analogy-report", ...part.data });
|
|
314
|
+
break;
|
|
294
315
|
case "data-plan":
|
|
295
316
|
parts.push({ type: "data-plan", ...part.data });
|
|
296
317
|
break;
|
|
@@ -315,6 +336,12 @@ export function conversationUIToChatMessages(messages) {
|
|
|
315
336
|
case "data-compaction":
|
|
316
337
|
parts.push({ type: "data-compaction", ...part.data });
|
|
317
338
|
break;
|
|
339
|
+
case "data-model-refusal":
|
|
340
|
+
parts.push({ type: "data-model-refusal", ...part.data });
|
|
341
|
+
break;
|
|
342
|
+
case "data-pause":
|
|
343
|
+
parts.push({ type: "data-pause", ...part.data });
|
|
344
|
+
break;
|
|
318
345
|
case "data-subagent-activity":
|
|
319
346
|
parts.push({ type: "data-subagent-activity", ...part.data });
|
|
320
347
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion of a bioinformatics analysis platform. You\nhelp researchers understand their data, run analyses, interpret results, and\ndevelop testable hypotheses.\n\nYour tool results stay in this conversation across turns. Reuse what you\nalready read or searched, and look again only for something new, for example\na run that completed since.\n\n## Data and Files\n\nYou can read the files of this analysis. `inspect_data_profile` is the record\nof what the dataset is. `workspace_search` finds files by their description,\nand `read_file` gives the values.\n\nWhen the user names data that is not yet an input of this analysis, add it with\nthe input tool if you hold one; its call asks the user. Without one, say that\nthe data must be added as an input. Never tell the user to copy files into a\nfolder to make them visible.\n\n## Analysis Runs\n\nA run starts a sandbox with a specialist agent, and it takes minutes to hours.\nStart one only for computation on the data that needs that environment:\nstatistics, models, R or Python packages, reference data, a table over 16 MiB.\nA lookup, a file read or write, a summary of results that you can read, and\nlight work on a table (a filter, a join, a count, a small test, a reshape, a\nderived file or chart) are not runs. Do them yourself, with `run_script` for\nthe tables.\n\n- **One sandbox computation that the user explicitly asks for** \u2192\n `execute_analysis` in ad hoc mode, with the request in their words. The\n request is the consent: do not plan it and do not ask again. Keep one request\n in one call.\n- **Work of several dependent steps** \u2192 `generate_plan`. It shows the plan\n card. Explain the plan, and ask for approval. On approval, call\n `execute_analysis` in plan mode. A thread record that says that the person\n approved a plan means that its run started already.\n- **A computation that only you suggest** \u2192 ask first.\n\nRuns are asynchronous. Read their results on a later turn with `inspect_run`.\n\n## Interpreting Results\n\nA completed run has one summary for each step. When the run had summaries and\nno blocker, it also has a run synthesis: conclusions, key findings, themes,\nlimitations, and references. `inspect_run` gives the `synthesisStatus` and\nthe `synthesisPath`. Start from the synthesis, and read a step summary for the\nevidence behind a finding. Without a synthesis, integrate the step summaries\nyourself. Group findings by biological theme, and keep well-supported findings\napart from speculation.\n\nFor a translational finding, give the landscape of existing drugs for a target,\nkeep predictive and prognostic biomarkers apart, name the validation that is\nmissing, and rest no claim on one source.\n\n## Literature and Biology\n\nA question about biology or the literature gets a lookup, not an answer from\nmemory. The lookups are cheap API calls in `run_script`: put independent\nlookups in one script, and print only what you need. When a source has nothing,\nsay so and continue with the others.\n\n## Hypotheses\n\nA hypothesis is grounded in data that you read, falsifiable, and specific: the\ngenes, pathways, comparisons, and expected effects. Make sure that the data can\ntest it, rank the candidates by impact, testability, novelty, and evidence, and\ndrop what cannot be tested. Analogies from `generate_analogy_report` are\ncandidate seeds; ground each one in the data before you use it. When the user\nasks to go deeper on a hypothesis, refine it into sharper sub-hypotheses and\ngive the full updated list. To test a hypothesis, plan an analysis for it.\n\nThe analogy report
|
|
1
|
+
export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion of a bioinformatics analysis platform. You\nhelp researchers understand their data, run analyses, interpret results, and\ndevelop testable hypotheses.\n\nYour tool results stay in this conversation across turns. Reuse what you\nalready read or searched, and look again only for something new, for example\na run that completed since.\n\n## Data and Files\n\nYou can read the files of this analysis. `inspect_data_profile` is the record\nof what the dataset is. `workspace_search` finds files by their description,\nand `read_file` gives the values.\n\nWhen the user names data that is not yet an input of this analysis, add it with\nthe input tool if you hold one; its call asks the user. Without one, say that\nthe data must be added as an input. Never tell the user to copy files into a\nfolder to make them visible.\n\n## Analysis Runs\n\nA run starts a sandbox with a specialist agent, and it takes minutes to hours.\nStart one only for computation on the data that needs that environment:\nstatistics, models, R or Python packages, reference data, a table over 16 MiB.\nA lookup, a file read or write, a summary of results that you can read, and\nlight work on a table (a filter, a join, a count, a small test, a reshape, a\nderived file or chart) are not runs. Do them yourself, with `run_script` for\nthe tables.\n\n- **One sandbox computation that the user explicitly asks for** \u2192\n `execute_analysis` in ad hoc mode, with the request in their words. The\n request is the consent: do not plan it and do not ask again. Keep one request\n in one call.\n- **Work of several dependent steps** \u2192 `generate_plan`. It shows the plan\n card. Explain the plan, and ask for approval. On approval, call\n `execute_analysis` in plan mode. A thread record that says that the person\n approved a plan means that its run started already.\n- **A computation that only you suggest** \u2192 ask first.\n\nRuns are asynchronous. Read their results on a later turn with `inspect_run`.\n\n## Interpreting Results\n\nA completed run has one summary for each step. When the run had summaries and\nno blocker, it also has a run synthesis: conclusions, key findings, themes,\nlimitations, and references. `inspect_run` gives the `synthesisStatus` and\nthe `synthesisPath`. Start from the synthesis, and read a step summary for the\nevidence behind a finding. Without a synthesis, integrate the step summaries\nyourself. Group findings by biological theme, and keep well-supported findings\napart from speculation.\n\nFor a translational finding, give the landscape of existing drugs for a target,\nkeep predictive and prognostic biomarkers apart, name the validation that is\nmissing, and rest no claim on one source.\n\n## Literature and Biology\n\nA question about biology or the literature gets a lookup, not an answer from\nmemory. The lookups are cheap API calls in `run_script`: put independent\nlookups in one script, and print only what you need. When a source has nothing,\nsay so and continue with the others.\n\n## Hypotheses\n\nA hypothesis is grounded in data that you read, falsifiable, and specific: the\ngenes, pathways, comparisons, and expected effects. Make sure that the data can\ntest it, rank the candidates by impact, testability, novelty, and evidence, and\ndrop what cannot be tested. Analogies from `generate_analogy_report` are\ncandidate seeds; ground each one in the data before you use it. When the user\nasks to go deeper on a hypothesis, refine it into sharper sub-hypotheses and\ngive the full updated list. To test a hypothesis, plan an analysis for it.\n\nThe analogy block shows the full report to the user, thus do not copy the\nreport into your reply. Name the analogies that matter by their\n`analogyTitle`, say why they matter for this data, and end with one question\nabout which one to pursue. For an `extraction-failed` envelope, give\nthe message of the error and ask the user to narrow the problem. Keep\nanalogies out of `generate_plan`, `execute_analysis`, and each other workflow\ninput: the planner and the sandbox agents do not know analogy reports.\n\n## Reports\n\nWhen the user asks for a report, call `start_report_session` with no\nconfirmation, unless the scope is truly ambiguous: several analyses to choose\nfrom, or an audience that matters and is not known. Then tell the user that the\nreport takes shape in that chat. Do not compose the report here. A request to\nput content into a file, such as a markdown summary or a PDF, is a file that you\nwrite: `write_file` for text, and `export_pdf` in a script for a PDF. The user\nmay not know that report sessions exist: offer one when a structured, cited\nreport would serve them, for example after a run completes.\n\n## Ground Rules\n\n- Cite a value only from a synthesis, a step summary, or a file that you or a\n script read. A `workspace_search` hit gives a description, not the values.\n If you cannot find the data behind a claim, say so.\n- A constraint in the working memory binds the rest of the analysis. If a plan\n or a choice would break one, raise the conflict with the user. Never just\n proceed.\n- Data profiling runs each time the inputs change, thus a plan needs no step\n that explores the data structure.\n- Before you call a package or a reference dataset unavailable, find out\n whether a tool that you hold can provide it.\n- When a tool fails, say so briefly, name a cause that the user can act on, and\n offer a retry or an alternative. Keep the internal details out.\n- When results conflict, give both sides.\n- Give a hypothesis with its ID, claim, rationale, test design, expected\n outcomes, and priority.\n";
|
|
@@ -72,11 +72,13 @@ candidate seeds; ground each one in the data before you use it. When the user
|
|
|
72
72
|
asks to go deeper on a hypothesis, refine it into sharper sub-hypotheses and
|
|
73
73
|
give the full updated list. To test a hypothesis, plan an analysis for it.
|
|
74
74
|
|
|
75
|
-
The analogy
|
|
75
|
+
The analogy block shows the full report to the user, thus do not copy the
|
|
76
|
+
report into your reply. Name the analogies that matter by their
|
|
76
77
|
\`analogyTitle\`, say why they matter for this data, and end with one question
|
|
77
|
-
about which one to pursue.
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
about which one to pursue. For an \`extraction-failed\` envelope, give
|
|
79
|
+
the message of the error and ask the user to narrow the problem. Keep
|
|
80
|
+
analogies out of \`generate_plan\`, \`execute_analysis\`, and each other workflow
|
|
81
|
+
input: the planner and the sandbox agents do not know analogy reports.
|
|
80
82
|
|
|
81
83
|
## Reports
|
|
82
84
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const mdvPreparationAgentPrompt = "# MDV Preparation Agent\n\nYou prepare biological datasets as assets for MDV (Multi-Dimensional Viewer). You take\nscRNA-seq, snRNA-seq, scATAC-seq, multiome, spatial transcriptomics (CosMx, Xenium, Visium,\nMERFISH) and spatial proteomics (CODEX, IMC, MIBI) data, in raw or processed form. You deliver an\nMDV-ready AnnData that MDV's upload converts into readable tables, the context tables, and a\nREADME, so that a person who did not make them can use them in MDV.\n\n## Skills\n\nYour skills: `mdv-preparation`, `single-cell`, `spatial-omics`, `chromatin-regulation`,\n`shared/omics-general`.\n\nRead `mdv-preparation` first. Its workflow is the order of your work, and its references\nhold the code to copy: the asset helpers and the validator. Use the modality packs for QC,\nnormalisation, embedding, and clustering when the input is raw.\n\n## Workflow (Summary)\n\n1. **Inventory** \u2014 identify the modality, the platform, the input form (raw instrument output,\n count matrix, processed AnnData, MuData, SpatialData), and the size. Estimate the memory\n before you load a large matrix.\n2. **Build** \u2014 one analysis-ready AnnData: counts in, QC, log-normalised sparse X, UMAP,\n clusters, and readable labels with their evidence.\n3. **Curate** \u2014 keep the columns a person needs, label each with words and units (the label is\n the column name in MDV), keep grouping columns at 256 categories or fewer.\n4. **Write** \u2014 the asset, the context tables, and the README with a column glossary, the\n processing record, and the first charts to make.\n5. **Validate** \u2014 run the validator of the pack. Zero errors before delivery.\n\n## Domain Standards\n\n- The deliverable is an asset, not an MDV project. You do not need mdvtools.\n- Spatial data: slide coordinates in micrometres as two columns, y flipped upwards. For raw CosMx\n runs, measure the FOV orientation before you trust the layout.\n- Inferred labels are putative: say so in the column label and keep the evidence.\n- Fix every seed. The same input gives the same asset.\n\n## Figures\n\nThe asset is the deliverable. Write one preview PNG only if the step needs a figure for the\nreport: the tissue or the UMAP coloured by cell type.\n\n## Domain Anti-Patterns\n\n- Raw snake_case keys or bare cluster ids as the labels people see.\n- Dense or scaled matrices, extra layers, or PCA columns in the asset.\n- Grouping columns above 256 categories.\n- Skipping the validator because the file \"loads\".\n\n## Required Output Files\n\n- The asset `output/*.mdv.h5ad`, its context tables `output/*.tsv`, and `output/README.md`.\n- The validator report `output/*.mdv.validation.json`, with zero errors.\n- The analysis AnnData `output/*.h5ad` with standard keys and raw counts.\n";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const mdvPreparationAgentPrompt = `# MDV Preparation Agent
|
|
2
|
+
|
|
3
|
+
You prepare biological datasets as assets for MDV (Multi-Dimensional Viewer). You take
|
|
4
|
+
scRNA-seq, snRNA-seq, scATAC-seq, multiome, spatial transcriptomics (CosMx, Xenium, Visium,
|
|
5
|
+
MERFISH) and spatial proteomics (CODEX, IMC, MIBI) data, in raw or processed form. You deliver an
|
|
6
|
+
MDV-ready AnnData that MDV's upload converts into readable tables, the context tables, and a
|
|
7
|
+
README, so that a person who did not make them can use them in MDV.
|
|
8
|
+
|
|
9
|
+
## Skills
|
|
10
|
+
|
|
11
|
+
Your skills: \`mdv-preparation\`, \`single-cell\`, \`spatial-omics\`, \`chromatin-regulation\`,
|
|
12
|
+
\`shared/omics-general\`.
|
|
13
|
+
|
|
14
|
+
Read \`mdv-preparation\` first. Its workflow is the order of your work, and its references
|
|
15
|
+
hold the code to copy: the asset helpers and the validator. Use the modality packs for QC,
|
|
16
|
+
normalisation, embedding, and clustering when the input is raw.
|
|
17
|
+
|
|
18
|
+
## Workflow (Summary)
|
|
19
|
+
|
|
20
|
+
1. **Inventory** — identify the modality, the platform, the input form (raw instrument output,
|
|
21
|
+
count matrix, processed AnnData, MuData, SpatialData), and the size. Estimate the memory
|
|
22
|
+
before you load a large matrix.
|
|
23
|
+
2. **Build** — one analysis-ready AnnData: counts in, QC, log-normalised sparse X, UMAP,
|
|
24
|
+
clusters, and readable labels with their evidence.
|
|
25
|
+
3. **Curate** — keep the columns a person needs, label each with words and units (the label is
|
|
26
|
+
the column name in MDV), keep grouping columns at 256 categories or fewer.
|
|
27
|
+
4. **Write** — the asset, the context tables, and the README with a column glossary, the
|
|
28
|
+
processing record, and the first charts to make.
|
|
29
|
+
5. **Validate** — run the validator of the pack. Zero errors before delivery.
|
|
30
|
+
|
|
31
|
+
## Domain Standards
|
|
32
|
+
|
|
33
|
+
- The deliverable is an asset, not an MDV project. You do not need mdvtools.
|
|
34
|
+
- Spatial data: slide coordinates in micrometres as two columns, y flipped upwards. For raw CosMx
|
|
35
|
+
runs, measure the FOV orientation before you trust the layout.
|
|
36
|
+
- Inferred labels are putative: say so in the column label and keep the evidence.
|
|
37
|
+
- Fix every seed. The same input gives the same asset.
|
|
38
|
+
|
|
39
|
+
## Figures
|
|
40
|
+
|
|
41
|
+
The asset is the deliverable. Write one preview PNG only if the step needs a figure for the
|
|
42
|
+
report: the tissue or the UMAP coloured by cell type.
|
|
43
|
+
|
|
44
|
+
## Domain Anti-Patterns
|
|
45
|
+
|
|
46
|
+
- Raw snake_case keys or bare cluster ids as the labels people see.
|
|
47
|
+
- Dense or scaled matrices, extra layers, or PCA columns in the asset.
|
|
48
|
+
- Grouping columns above 256 categories.
|
|
49
|
+
- Skipping the validator because the file "loads".
|
|
50
|
+
|
|
51
|
+
## Required Output Files
|
|
52
|
+
|
|
53
|
+
- The asset \`output/*.mdv.h5ad\`, its context tables \`output/*.tsv\`, and \`output/README.md\`.
|
|
54
|
+
- The validator report \`output/*.mdv.validation.json\`, with zero errors.
|
|
55
|
+
- The analysis AnnData \`output/*.h5ad\` with standard keys and raw counts.
|
|
56
|
+
`;
|
|
@@ -118,6 +118,12 @@ export type AiSdkProviderConfig = {
|
|
|
118
118
|
* Needs the `thinking-binding-controls-2026-08-01` beta header.
|
|
119
119
|
*/
|
|
120
120
|
readonly thinkingBinding?: "drop_block" | "error" | "off";
|
|
121
|
+
/**
|
|
122
|
+
* `default`: when a safety classifier declines a request, the API sends it again to the model that it
|
|
123
|
+
* recommends for the category of the refusal, inside the same call. When the field is absent, a declined
|
|
124
|
+
* request ends with the content filter. A model with no fallback target keeps its refusal.
|
|
125
|
+
*/
|
|
126
|
+
readonly fallbacks?: "default";
|
|
121
127
|
} | {
|
|
122
128
|
readonly kind: "openai";
|
|
123
129
|
readonly baseURL?: string;
|
|
@@ -252,22 +258,19 @@ export declare function sessionKeyDigestOf(session: Pick<AgentSession, "scope" |
|
|
|
252
258
|
* A model can refuse a request by category — a biology prompt is one such category —
|
|
253
259
|
* and the API then re-serves it on another model. It reports the hop TWICE: as a
|
|
254
260
|
* `fallback` content block inside the assistant turn, and on
|
|
255
|
-
* `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
261
|
+
* `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps the block as a
|
|
262
|
+
* custom part and sends it back on the next request, but it keeps each thinking block
|
|
263
|
+
* before it too. The API accepts no thinking block from before the hop, and a turn
|
|
264
|
+
* whose thinking blocks changed earns `thinking ... blocks in the latest assistant
|
|
265
|
+
* message cannot be modified`, which is a 400 that no retry can pass.
|
|
260
266
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* way back, thus the loop records the reply of a hop with no thinking block, and the next
|
|
265
|
-
* request continues. The cost is the reasoning of this one turn and the cache write that
|
|
266
|
-
* its new prefix pays for.
|
|
267
|
+
* A thinking block is optional on the way back, thus the loop records the reply of a
|
|
268
|
+
* hop with no thinking block, and the next request continues. The cost is the
|
|
269
|
+
* reasoning of this one turn and the cache write that its new prefix pays for.
|
|
267
270
|
*
|
|
268
|
-
* The response metadata of the package carries `message_start.message.model`, and
|
|
269
|
-
*
|
|
270
|
-
* answered, thus it wins for the usage record.
|
|
271
|
+
* The response metadata of the package carries `message_start.message.model`, and
|
|
272
|
+
* after a hop in the middle of a reply that field keeps the REQUESTED model. The
|
|
273
|
+
* iteration names the model that answered, thus it wins for the usage record.
|
|
271
274
|
*/
|
|
272
275
|
export declare function fallbackModelOf(metadata: ProviderMetadata | undefined): string | undefined;
|
|
273
276
|
/**
|
package/dist/providers/ai-sdk.js
CHANGED
|
@@ -108,22 +108,19 @@ function thinkingWithBinding(reasoning, mode) {
|
|
|
108
108
|
* A model can refuse a request by category — a biology prompt is one such category —
|
|
109
109
|
* and the API then re-serves it on another model. It reports the hop TWICE: as a
|
|
110
110
|
* `fallback` content block inside the assistant turn, and on
|
|
111
|
-
* `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
111
|
+
* `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps the block as a
|
|
112
|
+
* custom part and sends it back on the next request, but it keeps each thinking block
|
|
113
|
+
* before it too. The API accepts no thinking block from before the hop, and a turn
|
|
114
|
+
* whose thinking blocks changed earns `thinking ... blocks in the latest assistant
|
|
115
|
+
* message cannot be modified`, which is a 400 that no retry can pass.
|
|
116
116
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* way back, thus the loop records the reply of a hop with no thinking block, and the next
|
|
121
|
-
* request continues. The cost is the reasoning of this one turn and the cache write that
|
|
122
|
-
* its new prefix pays for.
|
|
117
|
+
* A thinking block is optional on the way back, thus the loop records the reply of a
|
|
118
|
+
* hop with no thinking block, and the next request continues. The cost is the
|
|
119
|
+
* reasoning of this one turn and the cache write that its new prefix pays for.
|
|
123
120
|
*
|
|
124
|
-
* The response metadata of the package carries `message_start.message.model`, and
|
|
125
|
-
*
|
|
126
|
-
* answered, thus it wins for the usage record.
|
|
121
|
+
* The response metadata of the package carries `message_start.message.model`, and
|
|
122
|
+
* after a hop in the middle of a reply that field keeps the REQUESTED model. The
|
|
123
|
+
* iteration names the model that answered, thus it wins for the usage record.
|
|
127
124
|
*/
|
|
128
125
|
export function fallbackModelOf(metadata) {
|
|
129
126
|
const entries = metadata?.["anthropic"]?.["iterations"];
|
|
@@ -717,12 +714,13 @@ export function createConfiguredAiSdkProvider(deps) {
|
|
|
717
714
|
maxRetries: config.maxRetries,
|
|
718
715
|
requestTimeoutMs,
|
|
719
716
|
reasoning: config.reasoning,
|
|
720
|
-
// The package sends the key as `metadata.user_id`,
|
|
721
|
-
// `thinking.block_binding
|
|
717
|
+
// The package sends the key as `metadata.user_id`, the binding as
|
|
718
|
+
// `thinking.block_binding`, and the default fallback with its beta header.
|
|
722
719
|
providerOptionsFor: ({ session, reasoning }) => ({
|
|
723
720
|
anthropic: {
|
|
724
721
|
metadata: { userId: sessionKeyDigestOf(session) },
|
|
725
722
|
thinking: binding === undefined ? undefined : thinkingWithBinding(reasoning, binding),
|
|
723
|
+
fallbacks: config.fallbacks,
|
|
726
724
|
},
|
|
727
725
|
}),
|
|
728
726
|
});
|
|
@@ -4,17 +4,9 @@
|
|
|
4
4
|
* assertion rules without a host.
|
|
5
5
|
*/
|
|
6
6
|
import { err, ok } from "neverthrow";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
function fail(reference, reason, detail) {
|
|
11
|
-
const failure = detail !== undefined ? { reference, reason, detail } : { reference, reason };
|
|
12
|
-
return err(failure);
|
|
13
|
-
}
|
|
14
|
-
/** A short account of a derivation input, for a detail that names which input broke the arithmetic. */
|
|
15
|
-
function describeReference(reference) {
|
|
16
|
-
return `artifact value at ${reference.path} column ${reference.locator.column}`;
|
|
17
|
-
}
|
|
7
|
+
import { checkValueAssertion } from "./assert-rules.js";
|
|
8
|
+
import { fail, readArtifactTable, resolveCitation, resolveDerivation, selectRow } from "./reference-read.js";
|
|
9
|
+
import { fileTypeHoldsNoCell, snapshotEntry, snapshotRows } from "./reference-resolver.js";
|
|
18
10
|
function resolveArtifactValue(reference, snapshot) {
|
|
19
11
|
const artifact = snapshotEntry(snapshot, reference.path);
|
|
20
12
|
if (artifact === undefined) {
|
|
@@ -35,53 +27,22 @@ function resolveArtifactValue(reference, snapshot) {
|
|
|
35
27
|
if (readRows.isErr()) {
|
|
36
28
|
return fail(reference, "unreadable-artifact", `the artifact at ${reference.path} holds a malformed row: ${readRows.error}`);
|
|
37
29
|
}
|
|
38
|
-
const rows = readRows.value;
|
|
39
30
|
const locator = reference.locator;
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
if (row === undefined) {
|
|
44
|
-
return fail(reference, "locator-out-of-range", `row ${locator.row} is outside the ${rows.length} rows`);
|
|
45
|
-
}
|
|
46
|
-
selectedRow = row;
|
|
47
|
-
}
|
|
48
|
-
else if (locator.rowFilter !== undefined) {
|
|
49
|
-
const filter = locator.rowFilter;
|
|
50
|
-
const matches = rows.filter((row) => cellMatchesFilterValue(row[filter.column], filter.value));
|
|
51
|
-
const match = matches[0];
|
|
52
|
-
if (match === undefined) {
|
|
53
|
-
return fail(reference, "locator-out-of-range", `no row where ${filter.column} equals ${String(filter.value)}`);
|
|
54
|
-
}
|
|
55
|
-
if (matches.length > 1) {
|
|
56
|
-
return fail(reference, "ambiguous-match", `${matches.length} rows where ${filter.column} equals ${String(filter.value)}`);
|
|
57
|
-
}
|
|
58
|
-
selectedRow = match;
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
// The locator schema forbids a locator with neither selector. The resolver cannot depend on a
|
|
62
|
-
// prior parse, thus it treats a row that no selector addresses as out of range.
|
|
63
|
-
return fail(reference, "locator-out-of-range", "the locator selects no row");
|
|
31
|
+
const selection = selectRow(readRows.value, locator);
|
|
32
|
+
if (selection.isErr()) {
|
|
33
|
+
return fail(reference, selection.error.reason, selection.error.detail);
|
|
64
34
|
}
|
|
65
35
|
// A real table holds an empty cell, and the snapshot parser gives it back as an absent
|
|
66
36
|
// key: `snapshotRows` drops a cell that parsed as `null` or `undefined`, so an absent key
|
|
67
37
|
// is the only absence. It is not a value that a scalar reference can bind. An empty
|
|
68
38
|
// string is a value, thus it stays valid.
|
|
69
|
-
const cell =
|
|
39
|
+
const cell = selection.value[locator.column];
|
|
70
40
|
if (cell === undefined) {
|
|
71
41
|
return fail(reference, "locator-out-of-range", `column ${locator.column} holds no value in the selected row`);
|
|
72
42
|
}
|
|
73
43
|
const valueFailure = checkValueAssertion(reference, reference.assert?.value, reference.assert?.tolerance, cell);
|
|
74
44
|
return valueFailure !== undefined ? err(valueFailure) : ok({ type: "scalar", value: cell });
|
|
75
45
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Put one own data property on a projected row.
|
|
78
|
-
*
|
|
79
|
-
* A plain assignment at the key `__proto__` reaches the prototype setter, thus the projection loses a
|
|
80
|
-
* column that the row holds. The definition makes an own data property for each key.
|
|
81
|
-
*/
|
|
82
|
-
function defineOwn(target, key, value) {
|
|
83
|
-
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
84
|
-
}
|
|
85
46
|
function resolveArtifactTable(reference, snapshot) {
|
|
86
47
|
const artifact = snapshotEntry(snapshot, reference.path);
|
|
87
48
|
if (artifact === undefined) {
|
|
@@ -94,46 +55,11 @@ function resolveArtifactTable(reference, snapshot) {
|
|
|
94
55
|
if (fileTypeHoldsNoCell(tableFileType)) {
|
|
95
56
|
return fail(reference, "unreadable-artifact", `the ${String(tableFileType)} at ${reference.path} holds no cell to read`);
|
|
96
57
|
}
|
|
97
|
-
// The bound ranks the rows as the artifact holds them, thus a bound over a column that the subset
|
|
98
|
-
// below leaves out still ranks them. An unknown bound column addresses nothing, exactly as an unknown
|
|
99
|
-
// subset column does.
|
|
100
|
-
const bound = reference.rowBound;
|
|
101
58
|
const readRows = snapshotRows(artifact);
|
|
102
59
|
if (readRows.isErr()) {
|
|
103
60
|
return fail(reference, "unreadable-artifact", `the artifact at ${reference.path} holds a malformed row: ${readRows.error}`);
|
|
104
61
|
}
|
|
105
|
-
|
|
106
|
-
if (bound !== undefined && columnsHeldByNoRow(allRows, [bound.column]).length > 0) {
|
|
107
|
-
return fail(reference, "locator-out-of-range", `the row bound names column ${bound.column}, which the table at ${reference.path} does not hold`);
|
|
108
|
-
}
|
|
109
|
-
const rows = bound === undefined ? allRows : applyRowBound(allRows, bound);
|
|
110
|
-
// The page states the shown count against the count of the artifact. This read is the one place that
|
|
111
|
-
// holds both, thus a bounded table carries the pre-bound total out with its rows.
|
|
112
|
-
const total = bound === undefined ? {} : { total: allRows.length };
|
|
113
|
-
const columns = reference.columns;
|
|
114
|
-
if (columns === undefined) {
|
|
115
|
-
return ok({ type: "table", rows, ...total });
|
|
116
|
-
}
|
|
117
|
-
// A name that no row holds addresses nothing. Without this check a projection onto an invented column
|
|
118
|
-
// gives an empty cell for each row and still resolves, which is the fabrication that grounding exists
|
|
119
|
-
// to reject.
|
|
120
|
-
const absent = columnsHeldByNoRow(rows, columns);
|
|
121
|
-
if (absent.length > 0) {
|
|
122
|
-
return fail(reference, "locator-out-of-range", `the table at ${reference.path} holds no column ${absent.join(", ")}`);
|
|
123
|
-
}
|
|
124
|
-
// Project each row onto the requested columns. A table tolerates a ragged row, thus a column that a
|
|
125
|
-
// given row lacks is left out of that row and is not a failure.
|
|
126
|
-
const projectedRows = rows.map((row) => {
|
|
127
|
-
const projected = {};
|
|
128
|
-
for (const column of columns) {
|
|
129
|
-
const cell = row[column];
|
|
130
|
-
if (cell !== undefined) {
|
|
131
|
-
defineOwn(projected, column, cell);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return projected;
|
|
135
|
-
});
|
|
136
|
-
return ok({ type: "table", rows: projectedRows, columns, ...total });
|
|
62
|
+
return readArtifactTable(reference, readRows.value);
|
|
137
63
|
}
|
|
138
64
|
function resolveArtifactFile(reference, snapshot) {
|
|
139
65
|
const artifact = snapshotEntry(snapshot, reference.path);
|
|
@@ -145,58 +71,6 @@ function resolveArtifactFile(reference, snapshot) {
|
|
|
145
71
|
}
|
|
146
72
|
return ok({ type: "file", path: reference.path, hash: artifact.hash });
|
|
147
73
|
}
|
|
148
|
-
async function resolveDerivation(reference, snapshot) {
|
|
149
|
-
const numbers = [];
|
|
150
|
-
for (const input of reference.inputs) {
|
|
151
|
-
const inputResult = await resolve(input, snapshot);
|
|
152
|
-
if (inputResult.isErr()) {
|
|
153
|
-
// Keep the inner reason so a reviewer sees the real cause, for example a missing artifact under
|
|
154
|
-
// the derivation. The derivation itself did not fail on its own terms.
|
|
155
|
-
return fail(reference, inputResult.error.reason, inputResult.error.detail);
|
|
156
|
-
}
|
|
157
|
-
const resolved = inputResult.value;
|
|
158
|
-
const numeric = resolved.type === "scalar" ? asFiniteNumber(resolved.value) : undefined;
|
|
159
|
-
if (numeric === undefined) {
|
|
160
|
-
// The arithmetic needs a finite number. A cell that holds text does not address a usable
|
|
161
|
-
// scalar, thus the closest reason is that the coordinate is out of range.
|
|
162
|
-
return fail(reference, "locator-out-of-range", `input ${describeReference(input)} did not resolve to a finite number`);
|
|
163
|
-
}
|
|
164
|
-
numbers.push(numeric);
|
|
165
|
-
}
|
|
166
|
-
const a = numbers[0];
|
|
167
|
-
const b = numbers[1];
|
|
168
|
-
if (a === undefined || b === undefined) {
|
|
169
|
-
return fail(reference, "locator-out-of-range", `operation ${reference.op} needs two inputs, and the derivation holds ${numbers.length}`);
|
|
170
|
-
}
|
|
171
|
-
let result;
|
|
172
|
-
switch (reference.op) {
|
|
173
|
-
case "ratio":
|
|
174
|
-
result = a / b;
|
|
175
|
-
break;
|
|
176
|
-
case "delta":
|
|
177
|
-
result = a - b;
|
|
178
|
-
break;
|
|
179
|
-
case "pctChange":
|
|
180
|
-
// A fraction, not a percent. A change of one half gives 0.5, thus an author asserts 0.5 and a
|
|
181
|
-
// `unit` of `%` only tells a renderer how to show it.
|
|
182
|
-
result = (a - b) / b;
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
if (!Number.isFinite(result)) {
|
|
186
|
-
return fail(reference, "locator-out-of-range", `operation ${reference.op} does not yield a finite value, because the divisor is zero`);
|
|
187
|
-
}
|
|
188
|
-
const valueFailure = checkValueAssertion(reference, reference.assert?.value, reference.assert?.tolerance, result);
|
|
189
|
-
return valueFailure !== undefined ? err(valueFailure) : ok({ type: "scalar", value: result });
|
|
190
|
-
}
|
|
191
|
-
function resolveCitation(reference, snapshot) {
|
|
192
|
-
const key = `${reference.idKind}:${reference.id}`;
|
|
193
|
-
if (snapshot.citations === undefined || !snapshot.citations.includes(key)) {
|
|
194
|
-
return fail(reference, "artifact-missing", `the citation ${key} is not in the pinned evidence`);
|
|
195
|
-
}
|
|
196
|
-
const value = { type: "citation", id: key };
|
|
197
|
-
const citationFailure = checkCitationAssertion(reference, reference.assert?.value, key);
|
|
198
|
-
return citationFailure !== undefined ? err(citationFailure) : ok(value);
|
|
199
|
-
}
|
|
200
74
|
async function resolve(reference, snapshot) {
|
|
201
75
|
switch (reference.kind) {
|
|
202
76
|
case "artifact-value":
|
|
@@ -206,7 +80,7 @@ async function resolve(reference, snapshot) {
|
|
|
206
80
|
case "artifact-file":
|
|
207
81
|
return resolveArtifactFile(reference, snapshot);
|
|
208
82
|
case "derivation":
|
|
209
|
-
return resolveDerivation(reference, snapshot);
|
|
83
|
+
return resolveDerivation(reference, snapshot, resolve);
|
|
210
84
|
case "citation":
|
|
211
85
|
return resolveCitation(reference, snapshot);
|
|
212
86
|
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* holds the streamed hash and the parsed rows against a stat signature. Two tool calls of one analysis
|
|
14
14
|
* share one cache.
|
|
15
15
|
*
|
|
16
|
-
* The realization calls the shared assert functions in `assert-rules.ts
|
|
17
|
-
*
|
|
18
|
-
* executable specification of the value tier. The two realizations are substitutable behind the seam.
|
|
16
|
+
* The realization calls the shared assert functions in `assert-rules.ts` and the shared reference reads in
|
|
17
|
+
* `reference-read.ts`, as the fixture realization does. Thus one semantics exists, and the fixture stays
|
|
18
|
+
* the executable specification of the value tier. The two realizations are substitutable behind the seam.
|
|
19
19
|
*/
|
|
20
20
|
import { type ReferenceResolver } from "./reference-resolver.js";
|
|
21
21
|
/** One artifact row, as plain cells. A text-backed format holds a numeric column as a string. */
|