@inflexa-ai/harness 0.31.1 → 0.33.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 +15 -0
- package/dist/agents/conversation-agent.js +16 -6
- package/dist/agents/report-session-agent.d.ts +9 -0
- package/dist/agents/report-session-agent.js +10 -2
- package/dist/agents/sandbox/shared.d.ts +13 -1
- package/dist/agents/sandbox/shared.js +22 -5
- package/dist/app/chat-turn.d.ts +11 -0
- package/dist/app/chat-turn.js +17 -1
- package/dist/app/spawn-report-session.d.ts +12 -0
- package/dist/app/spawn-report-session.js +21 -1
- package/dist/config/environment-stores.d.ts +53 -5
- package/dist/contracts/report-blocks.d.ts +8 -8
- package/dist/contracts/schemas/chat-parts.d.ts +6 -6
- package/dist/index.d.ts +8 -1
- package/dist/index.js +11 -0
- package/dist/prompts/briefing.d.ts +1 -1
- package/dist/prompts/briefing.js +4 -0
- package/dist/prompts/planner.js +13 -0
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +5 -5
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +4 -3
- package/dist/prompts/sandbox-standards.d.ts +20 -0
- package/dist/prompts/sandbox-standards.js +65 -0
- package/dist/provenance/seam.d.ts +203 -0
- package/dist/provenance/seam.js +71 -0
- package/dist/reference-data/catalog.js +11 -9
- package/dist/report-model/authoring-grammar.d.ts +2 -2
- package/dist/report-render/assets.d.ts +10 -0
- package/dist/report-render/assets.js +14 -0
- package/dist/report-render/design.js +256 -0
- package/dist/report-render/page.d.ts +75 -0
- package/dist/report-render/page.js +793 -1
- package/dist/report-render/provenance-data.d.ts +42 -0
- package/dist/report-render/provenance-data.js +62 -0
- package/dist/report-render/render.d.ts +20 -1
- package/dist/report-render/render.js +23 -10
- package/dist/report-render/types.d.ts +3 -0
- package/dist/report-render/views/chart-view.d.ts +5 -1
- package/dist/report-render/views/chart-view.js +6 -2
- package/dist/report-render/views/lineage.d.ts +93 -0
- package/dist/report-render/views/lineage.js +86 -0
- package/dist/report-render/views/page-view.d.ts +22 -1
- package/dist/report-render/views/page-view.js +12 -3
- package/dist/report-render/views/prose.d.ts +7 -1
- package/dist/report-render/views/prose.js +10 -6
- package/dist/report-render/views/references-view.d.ts +17 -1
- package/dist/report-render/views/references-view.js +12 -4
- package/dist/report-render/views/values.d.ts +17 -4
- package/dist/report-render/views/values.js +21 -8
- package/dist/runtime/assemble.d.ts +22 -0
- package/dist/runtime/assemble.js +4 -0
- package/dist/sandbox/create-sandbox.d.ts +20 -4
- package/dist/sandbox/create-sandbox.js +4 -0
- package/dist/sandbox/docker-client.d.ts +16 -6
- package/dist/sandbox/docker-client.js +47 -31
- package/dist/sandbox/farm.d.ts +95 -0
- package/dist/sandbox/farm.js +127 -0
- package/dist/sandbox/k8s-client.d.ts +24 -10
- package/dist/sandbox/k8s-client.js +85 -122
- package/dist/sandbox/mount-plan.d.ts +23 -1
- package/dist/sandbox/mount-plan.js +44 -7
- package/dist/sandbox/sandbox-error.d.ts +12 -0
- package/dist/sandbox/sandbox-error.js +2 -0
- package/dist/sandbox/types.d.ts +118 -0
- package/dist/schemas/artifact-manifest.d.ts +4 -4
- package/dist/schemas/plan-schemas.d.ts +3 -0
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/validate-plan.js +20 -1
- package/dist/schemas/workflow-state.d.ts +4 -0
- package/dist/schemas/workflow-state.js +10 -0
- package/dist/tasks/data-profile.d.ts +15 -0
- package/dist/tasks/data-profile.js +36 -26
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +18 -0
- package/dist/tools/execute-analysis.js +87 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +16 -1
- package/dist/tools/report-authoring/authoring-tools.js +58 -16
- package/dist/tools/report-session/derive-table.d.ts +3 -0
- package/dist/tools/report-session/derive-table.js +15 -0
- package/dist/tools/report-session/preview-report.d.ts +13 -4
- package/dist/tools/report-session/preview-report.js +23 -6
- package/dist/tools/report-session/record-version.d.ts +3 -0
- package/dist/tools/report-session/record-version.js +7 -3
- package/dist/tools/research/generate-plan.js +12 -2
- package/dist/tools/sandbox/index.d.ts +1 -0
- package/dist/tools/sandbox/index.js +1 -0
- package/dist/tools/sandbox/link-packages.d.ts +27 -0
- package/dist/tools/sandbox/link-packages.js +52 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +50 -30
- package/dist/tools/sandbox/list-available-packages.js +131 -40
- package/dist/tools/start-report-session.d.ts +7 -0
- package/dist/tools/start-report-session.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +15 -69
- package/dist/workflows/execute-analysis.js +5 -4
- package/package.json +2 -1
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* listAvailablePackages — query the R/Python/CLI/Node packages
|
|
3
|
-
*
|
|
2
|
+
* listAvailablePackages — query the R/Python/CLI/Node packages of the
|
|
3
|
+
* environment, from the inventory source the embedder binds.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* when it does not.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* Two sources exist, one per vantage. A SANDBOX agent reads the
|
|
6
|
+
* `inflexa.lock` of the mounted farm — what a step can import right now —
|
|
7
|
+
* from wherever the HOST can see it: the farm container path when the host
|
|
8
|
+
* mounts the same farm, an injected path when it does not. A CONVERSATION or
|
|
9
|
+
* planning surface reads the pool-scope inventory (`readPoolInventory`) —
|
|
10
|
+
* everything the store holds, whether or not a farm links it yet — because
|
|
11
|
+
* the ask flow marks the packages the POOL does not hold, and the farm of a
|
|
12
|
+
* new analysis is empty. The baked image inventory fragment
|
|
13
|
+
* (`image-packages.txt`, at `/opt/inflexa` in the image) merges into either
|
|
14
|
+
* report when the host can read one. The fragment keeps the section format:
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* ## System tools (CLI)
|
|
17
|
+
* samtools, bcftools, ...
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* anndata, scanpy, ...
|
|
21
|
-
*
|
|
22
|
-
* The file carries **names only — there are no version strings in it**, so this
|
|
23
|
-
* tool reports presence and language track and cannot report a version.
|
|
19
|
+
* Each entry renders as `name==version` where the source pins a version, and
|
|
20
|
+
* a targeted `names` lookup also carries the store directory and the full
|
|
21
|
+
* content hash where the source records them. A full listing carries no
|
|
22
|
+
* hashes — a thousand rows of sha256 would bury the signal.
|
|
24
23
|
*/
|
|
25
24
|
import { readFile } from "node:fs/promises";
|
|
26
25
|
import { ok } from "neverthrow";
|
|
@@ -28,12 +27,45 @@ import { z } from "zod";
|
|
|
28
27
|
import { defineTool } from "../define-tool.js";
|
|
29
28
|
import { capCodePoints, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
|
|
30
29
|
import { LIBS_CONTAINER_PATH } from "../../sandbox/mount-plan.js";
|
|
30
|
+
import { readFarmLockFile } from "../../sandbox/farm.js";
|
|
31
31
|
/**
|
|
32
|
-
* Where the
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Where the lock lives when the host mounts the farm at the same path the
|
|
33
|
+
* sandbox does. Both farm container paths are tried, because the path keys on
|
|
34
|
+
* the declared toolchain (`/mnt/libs/farm` under `"image"`, `/mnt/libs/current`
|
|
35
|
+
* under `"store"`), and the tool does not carry that declaration. A host that
|
|
36
|
+
* reads the farm somewhere else injects its own path instead.
|
|
35
37
|
*/
|
|
36
|
-
const
|
|
38
|
+
const DEFAULT_FARM_LOCK_FILES = [`${LIBS_CONTAINER_PATH}/farm/inflexa.lock`, `${LIBS_CONTAINER_PATH}/current/inflexa.lock`];
|
|
39
|
+
/** Where the baked image inventory fragment lives inside the image. */
|
|
40
|
+
const DEFAULT_IMAGE_PACKAGES_FILE = "/opt/inflexa/image-packages.txt";
|
|
41
|
+
/** The section title of each known lock track. An unknown track titles itself. */
|
|
42
|
+
const TRACK_TITLES = {
|
|
43
|
+
python: "Python (pip)",
|
|
44
|
+
cran: "R (CRAN)",
|
|
45
|
+
bioconductor: "R (Bioconductor)",
|
|
46
|
+
github: "R (GitHub)",
|
|
47
|
+
node: "Node (npm)",
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Group the lock packages into sections, one per track, in a stable order:
|
|
51
|
+
* the known tracks first, then an unknown track at its first occurrence.
|
|
52
|
+
* Each entry keeps the lock's version, store directory, and hash, so the
|
|
53
|
+
* targeted `names` path can report the exact store identity.
|
|
54
|
+
*/
|
|
55
|
+
export function lockSections(lock) {
|
|
56
|
+
const byTrack = new Map();
|
|
57
|
+
for (const track of Object.keys(TRACK_TITLES))
|
|
58
|
+
byTrack.set(track, []);
|
|
59
|
+
for (const pkg of lock.packages) {
|
|
60
|
+
const entry = { name: pkg.name, version: pkg.version, storeDir: pkg.store_dir, hash: pkg.hash };
|
|
61
|
+
const open = byTrack.get(pkg.track);
|
|
62
|
+
if (open)
|
|
63
|
+
open.push(entry);
|
|
64
|
+
else
|
|
65
|
+
byTrack.set(pkg.track, [entry]);
|
|
66
|
+
}
|
|
67
|
+
return [...byTrack.entries()].filter(([, packages]) => packages.length > 0).map(([track, packages]) => ({ title: TRACK_TITLES[track] ?? track, packages }));
|
|
68
|
+
}
|
|
37
69
|
/**
|
|
38
70
|
* Default cap on a listing — high enough that the real store is never truncated.
|
|
39
71
|
* The shipped catalog is ~270 packages (~3 KB, well under a thousand tokens), so
|
|
@@ -49,11 +81,18 @@ const DEFAULT_LIMIT = 2_000;
|
|
|
49
81
|
* check, and an agent told to "assume numpy is available" will import it and
|
|
50
82
|
* fail at runtime instead of probing first.
|
|
51
83
|
*/
|
|
52
|
-
const UNAVAILABLE_NOTE = "Package list not available — the
|
|
84
|
+
const UNAVAILABLE_NOTE = "Package list not available — the inventory of the farm could not be read, so what is installed is UNKNOWN. " +
|
|
53
85
|
"Do not assume any package is present, and do not infer one from the analysis you were asked to run. " +
|
|
54
86
|
"Probe each package you intend to use before relying on it (`python3 -c 'import <pkg>'`, " +
|
|
55
87
|
'R `requireNamespace("<pkg>", quietly = TRUE)`) and degrade gracefully when it is absent. ' +
|
|
56
88
|
"Nothing can be installed at runtime.";
|
|
89
|
+
/**
|
|
90
|
+
* The pool-scope twin of {@link UNAVAILABLE_NOTE}. It names no runtime probe,
|
|
91
|
+
* because a conversation surface has no runtime to probe, and an unreadable
|
|
92
|
+
* pool must read as UNKNOWN rather than as empty.
|
|
93
|
+
*/
|
|
94
|
+
const POOL_UNAVAILABLE_NOTE = "Package list not available — the package pool could not be read, so what the store holds is UNKNOWN. " +
|
|
95
|
+
"Do not assume a package is present, and do not report a package as absent.";
|
|
57
96
|
/** The `language` filter values, mapped onto the concrete section headings. */
|
|
58
97
|
const LANGUAGE_MATCHERS = {
|
|
59
98
|
// The R triple: `R (CRAN)`, `R (Bioconductor)`, `R (GitHub)`.
|
|
@@ -87,11 +126,15 @@ export function parsePackagesFile(content) {
|
|
|
87
126
|
for (const name of trimmed.split(",")) {
|
|
88
127
|
const pkg = name.trim();
|
|
89
128
|
if (pkg)
|
|
90
|
-
open.packages.push(pkg);
|
|
129
|
+
open.packages.push({ name: pkg });
|
|
91
130
|
}
|
|
92
131
|
}
|
|
93
132
|
return sections;
|
|
94
133
|
}
|
|
134
|
+
/** One entry as the listing renders it: `name==version` where the source pins a version, the bare name otherwise. */
|
|
135
|
+
function renderPackage(entry) {
|
|
136
|
+
return entry.version === undefined ? entry.name : `${entry.name}==${entry.version}`;
|
|
137
|
+
}
|
|
95
138
|
/** Render sections to the agent-facing listing, bounded at `limit` packages. */
|
|
96
139
|
function renderListing(sections, limit) {
|
|
97
140
|
const lines = [];
|
|
@@ -106,7 +149,7 @@ function renderListing(sections, limit) {
|
|
|
106
149
|
}
|
|
107
150
|
const shown = section.packages.slice(0, room);
|
|
108
151
|
returned += shown.length;
|
|
109
|
-
lines.push(`\n## ${section.title}`, ` ${shown.join(", ")}`);
|
|
152
|
+
lines.push(`\n## ${section.title}`, ` ${shown.map(renderPackage).join(", ")}`);
|
|
110
153
|
const hidden = section.packages.length - shown.length;
|
|
111
154
|
if (hidden > 0)
|
|
112
155
|
lines.push(` … and ${hidden} more in this section (raise \`limit\` or narrow with \`query\`).`);
|
|
@@ -126,15 +169,26 @@ export function queryPackages(sections, { names, query, language, limit }) {
|
|
|
126
169
|
for (const pkg of section.packages) {
|
|
127
170
|
// First writer wins: `packages.txt` sections are ordered, so a name
|
|
128
171
|
// colliding across tracks resolves to the earliest one.
|
|
129
|
-
if (!index.has(pkg.toLowerCase()))
|
|
130
|
-
index.set(pkg.toLowerCase(), {
|
|
172
|
+
if (!index.has(pkg.name.toLowerCase()))
|
|
173
|
+
index.set(pkg.name.toLowerCase(), { entry: pkg, section: section.title });
|
|
131
174
|
}
|
|
132
175
|
}
|
|
133
176
|
const checked = names.map((requested) => {
|
|
134
177
|
const hit = index.get(requested.trim().toLowerCase());
|
|
178
|
+
if (!hit)
|
|
179
|
+
return { requested, present: false };
|
|
135
180
|
// `name` echoes the catalog's canonical spelling — R package names are
|
|
136
181
|
// case-sensitive at `library()`, so the exact one is what the caller needs.
|
|
137
|
-
|
|
182
|
+
// The store identity rides only where the source records it.
|
|
183
|
+
return {
|
|
184
|
+
requested,
|
|
185
|
+
present: true,
|
|
186
|
+
name: hit.entry.name,
|
|
187
|
+
section: hit.section,
|
|
188
|
+
...(hit.entry.version === undefined ? {} : { version: hit.entry.version }),
|
|
189
|
+
...(hit.entry.storeDir === undefined ? {} : { storeDir: hit.entry.storeDir }),
|
|
190
|
+
...(hit.entry.hash === undefined ? {} : { hash: hit.entry.hash }),
|
|
191
|
+
};
|
|
138
192
|
});
|
|
139
193
|
return { available: true, checked };
|
|
140
194
|
}
|
|
@@ -144,7 +198,7 @@ export function queryPackages(sections, { names, query, language, limit }) {
|
|
|
144
198
|
.filter((s) => matchesLanguage(s.title))
|
|
145
199
|
.map((s) => ({
|
|
146
200
|
title: s.title,
|
|
147
|
-
packages: needle ? s.packages.filter((p) => p.toLowerCase().includes(needle)) : s.packages,
|
|
201
|
+
packages: needle ? s.packages.filter((p) => p.name.toLowerCase().includes(needle)) : s.packages,
|
|
148
202
|
}))
|
|
149
203
|
.filter((s) => s.packages.length > 0);
|
|
150
204
|
const total = filtered.reduce((n, s) => n + s.packages.length, 0);
|
|
@@ -161,18 +215,36 @@ export function queryPackages(sections, { names, query, language, limit }) {
|
|
|
161
215
|
const { content, returned } = renderListing(filtered, limit ?? DEFAULT_LIMIT);
|
|
162
216
|
return { available: true, total, returned, hasMore: returned < total, content };
|
|
163
217
|
}
|
|
164
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Create the package inventory over the bound source: the pool-scope reader
|
|
220
|
+
* when the embedder binds one (a conversation or planning surface), the
|
|
221
|
+
* host-readable farm `inflexa.lock` otherwise (a sandbox agent). The image
|
|
222
|
+
* fragment merges into either report.
|
|
223
|
+
*/
|
|
165
224
|
export function createListAvailablePackagesTool(deps = {}) {
|
|
166
|
-
const
|
|
225
|
+
const lockCandidates = deps.farmLockFile ? [deps.farmLockFile] : DEFAULT_FARM_LOCK_FILES;
|
|
226
|
+
const imagePackagesFile = deps.imagePackagesFile ?? DEFAULT_IMAGE_PACKAGES_FILE;
|
|
227
|
+
const readPoolInventory = deps.readPoolInventory;
|
|
228
|
+
// The scope decides the framing sentence: a sandbox agent reads what it can
|
|
229
|
+
// import NOW, and a conversation surface reads what the store HOLDS — a
|
|
230
|
+
// package absent from the pool needs an acquisition, not a shrug. A sandbox
|
|
231
|
+
// composition with the link seam gets a third framing. There the farm is
|
|
232
|
+
// not the boundary of an import, and a closed-world sentence would send the
|
|
233
|
+
// agent to report a package that one link call supplies.
|
|
234
|
+
const scopeSentence = readPoolInventory
|
|
235
|
+
? "Query the R, Python, CLI, and Node packages the package pool holds — the set a run can link into an analysis. A package reported present needs no acquisition; a package absent here is not in the store yet. "
|
|
236
|
+
: deps.linkToolPresent
|
|
237
|
+
? "Query the R, Python, CLI, and Node packages importable in the sandbox right now. Nothing installs at runtime; a package absent here can still be linkable from the host's staged pool — call `link_packages` before treating it as missing. "
|
|
238
|
+
: "Query the R, Python, CLI, and Node packages installed in the sandbox. No packages can be installed at runtime — only what this tool reports is importable. ";
|
|
167
239
|
return defineTool({
|
|
168
240
|
id: "list_available_packages",
|
|
169
|
-
description:
|
|
241
|
+
description: scopeSentence +
|
|
170
242
|
"A full listing is small (a few hundred packages) and is returned whole by default, so a listing you get back is the complete set unless `hasMore` says otherwise. " +
|
|
171
243
|
'`names`: check specific packages (e.g. ["Seurat", "scanpy"]) — returns present/absent plus the language track for each, case-insensitively; this is the cheapest call and the right one for \'is X available?\'. ' +
|
|
172
244
|
"`query`: case-insensitive substring filter over package names. " +
|
|
173
245
|
"`language`: restrict to one track (r | python | cli | node). " +
|
|
174
246
|
"`limit`: cap the packages listed; the response always carries the true `total` and a `hasMore` flag, so truncation is never silent. " +
|
|
175
|
-
|
|
247
|
+
"Each entry renders as `name==version` where the source pins a version, and a `names` check also reports the store directory and the content hash where the source records them.",
|
|
176
248
|
inputSchema: z.object({
|
|
177
249
|
names: z
|
|
178
250
|
.array(z.string())
|
|
@@ -212,16 +284,35 @@ export function createListAvailablePackagesTool(deps = {}) {
|
|
|
212
284
|
return "full package list";
|
|
213
285
|
},
|
|
214
286
|
execute: async (input) => {
|
|
287
|
+
// The baked image fragment merges in when the host can read one; a
|
|
288
|
+
// missing fragment merges nothing, and that is the normal state for
|
|
289
|
+
// a host that runs outside the image.
|
|
290
|
+
const fragmentSections = await readFile(imagePackagesFile, "utf-8")
|
|
291
|
+
.then(parsePackagesFile)
|
|
292
|
+
.catch(() => []);
|
|
215
293
|
// An unreadable inventory is an expected environment state — model it as an
|
|
216
|
-
// `available: false` data variant telling the caller the set is UNKNOWN
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
294
|
+
// `available: false` data variant telling the caller the set is UNKNOWN,
|
|
295
|
+
// WITH the reason: without it, a structural fault (a damaged dependency
|
|
296
|
+
// graph) reads as a transient flake, and the caller retries for ever.
|
|
297
|
+
if (readPoolInventory) {
|
|
298
|
+
const pool = await readPoolInventory().catch((cause) => ({
|
|
299
|
+
kind: "unavailable",
|
|
300
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
301
|
+
}));
|
|
302
|
+
if (pool.kind === "unavailable")
|
|
303
|
+
return ok({ available: false, content: `${POOL_UNAVAILABLE_NOTE} The reason: ${pool.reason}.` });
|
|
304
|
+
return ok(queryPackages([...pool.sections, ...fragmentSections], input));
|
|
305
|
+
}
|
|
306
|
+
let lock = null;
|
|
307
|
+
for (const candidate of lockCandidates) {
|
|
308
|
+
lock = readFarmLockFile(candidate).unwrapOr(null);
|
|
309
|
+
if (lock !== null)
|
|
310
|
+
break;
|
|
220
311
|
}
|
|
221
|
-
|
|
312
|
+
if (lock === null) {
|
|
222
313
|
return ok({ available: false, content: UNAVAILABLE_NOTE });
|
|
223
314
|
}
|
|
224
|
-
return ok(queryPackages(
|
|
315
|
+
return ok(queryPackages([...lockSections(lock), ...fragmentSections], input));
|
|
225
316
|
},
|
|
226
317
|
});
|
|
227
318
|
}
|
|
@@ -24,6 +24,7 @@ import type { AcquireEyes } from "../lib/eyes.js";
|
|
|
24
24
|
import type { Logger } from "../lib/logger.js";
|
|
25
25
|
import type { ThreadType } from "../memory/thread-store.js";
|
|
26
26
|
import { type Tool } from "./define-tool.js";
|
|
27
|
+
import type { ProvenanceSeam } from "../provenance/seam.js";
|
|
27
28
|
/**
|
|
28
29
|
* The brief of the ask, plus the one override of the advice.
|
|
29
30
|
*
|
|
@@ -101,6 +102,12 @@ export interface StartReportSessionToolDeps {
|
|
|
101
102
|
* carries no stale value of its own.
|
|
102
103
|
*/
|
|
103
104
|
readonly anchorSession?: (threadId: string) => Promise<EnsureSessionStateResult>;
|
|
105
|
+
/**
|
|
106
|
+
* The provenance seam of the composition. The tool hands it to the spawn,
|
|
107
|
+
* which emits the creation of the session at the moment that the child lands.
|
|
108
|
+
* The tool emits nothing of its own.
|
|
109
|
+
*/
|
|
110
|
+
readonly provenance?: ProvenanceSeam;
|
|
104
111
|
readonly logger?: Logger;
|
|
105
112
|
}
|
|
106
113
|
/**
|
|
@@ -111,6 +111,7 @@ export function createStartReportSessionTool(deps) {
|
|
|
111
111
|
chrome: deps.chrome,
|
|
112
112
|
...(deps.eyes ? { eyes: deps.eyes } : {}),
|
|
113
113
|
...(deps.anchorSession ? { anchorSession: deps.anchorSession } : {}),
|
|
114
|
+
...(deps.provenance ? { provenance: deps.provenance } : {}),
|
|
114
115
|
...(deps.logger ? { logger: deps.logger } : {}),
|
|
115
116
|
};
|
|
116
117
|
const spawn = createReportSessionSpawn(spawnDeps);
|
|
@@ -69,6 +69,7 @@ import type { SynthesisStatus, UpdateStepExecutionInput } from "../state/index.j
|
|
|
69
69
|
import type { AnalysisStep } from "../schemas/workflow-state.js";
|
|
70
70
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
71
71
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
72
|
+
import type { ProvenanceSeam } from "../provenance/seam.js";
|
|
72
73
|
import type { RunCharge } from "../billing/run-charge.js";
|
|
73
74
|
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
74
75
|
import { type PlanStep } from "./execute-analysis-scheduler.js";
|
|
@@ -154,63 +155,6 @@ export interface ExecuteAnalysisResult {
|
|
|
154
155
|
readonly failedSteps: readonly string[];
|
|
155
156
|
readonly canceledSteps: readonly string[];
|
|
156
157
|
}
|
|
157
|
-
/**
|
|
158
|
-
* A run-lifecycle provenance observation handed to an optional host observer.
|
|
159
|
-
* Harness-owned plain union — the harness stays tsprov-free and bus-free; the
|
|
160
|
-
* host maps these execution facts onto its own ledger vocabulary.
|
|
161
|
-
*
|
|
162
|
-
* Every `atMs` is epoch milliseconds read via `await DBOS.now()`, a checkpointed
|
|
163
|
-
* step: a body re-executed by DBOS recovery reads the recorded value, so a
|
|
164
|
-
* re-emitted event carries the identical timestamp and merges on the host's
|
|
165
|
-
* ledger without a value conflict. Never source these from a wall clock
|
|
166
|
-
* (`Date.now()`) — that would diverge across replays and defeat the merge.
|
|
167
|
-
*
|
|
168
|
-
* `run_completed.durationMs` is the terminal `atMs` minus the `run_started`
|
|
169
|
-
* `atMs` (both `DBOS.now()` reads) — the true workflow-observed run span.
|
|
170
|
-
*
|
|
171
|
-
* `step_completed` fires once at EVERY scheduler-loop settlement — the only site
|
|
172
|
-
* that observes every executed step (registration sees only artifact-producing
|
|
173
|
-
* steps, and a child cannot observe its own parent-driven cancel). Steps that
|
|
174
|
-
* were never dispatched (dependents of a failed sibling) emit nothing by design;
|
|
175
|
-
* the run's terminal status carries that outcome. `status` maps the settlement
|
|
176
|
-
* outcome: `complete` → `"completed"`, `canceled` → `"canceled"`,
|
|
177
|
-
* `failed`/`blocked`/child-error → `"failed"`.
|
|
178
|
-
*
|
|
179
|
-
* `run_completed` fires at BOTH terminal boundaries (success and failure); the
|
|
180
|
-
* `status` field distinguishes them.
|
|
181
|
-
*/
|
|
182
|
-
export type RunProvenanceEvent = {
|
|
183
|
-
type: "run_started";
|
|
184
|
-
analysisId: string;
|
|
185
|
-
runId: string;
|
|
186
|
-
planSummary: string;
|
|
187
|
-
stepCount: number;
|
|
188
|
-
atMs: number;
|
|
189
|
-
} | {
|
|
190
|
-
type: "step_completed";
|
|
191
|
-
analysisId: string;
|
|
192
|
-
runId: string;
|
|
193
|
-
stepId: string;
|
|
194
|
-
/** Settlement outcome mapped to a terminal step status. */
|
|
195
|
-
status: "completed" | "failed" | "canceled";
|
|
196
|
-
/** The child's durable execution duration; absent when the child settled by throwing. */
|
|
197
|
-
durationMs?: number;
|
|
198
|
-
atMs: number;
|
|
199
|
-
} | {
|
|
200
|
-
type: "run_completed";
|
|
201
|
-
analysisId: string;
|
|
202
|
-
runId: string;
|
|
203
|
-
/**
|
|
204
|
-
* The body's terminal status. Both boundary sites resolve it through `deriveFinalStatus`,
|
|
205
|
-
* which records a budget pause as `"canceled"` — so `"suspended_insufficient_funds"` (a
|
|
206
|
-
* `RunStatus` member) is never emitted here and is deliberately absent from this narrower
|
|
207
|
-
* `ExecuteAnalysisFinalStatus`-minus-`"running"` set.
|
|
208
|
-
*/
|
|
209
|
-
status: Exclude<ExecuteAnalysisFinalStatus, "running">;
|
|
210
|
-
atMs: number;
|
|
211
|
-
/** `atMs − run_started.atMs`: the workflow-observed run span in ms. */
|
|
212
|
-
durationMs: number;
|
|
213
|
-
};
|
|
214
158
|
/**
|
|
215
159
|
* A step's in-run display state. Wider than the ledger's `StepExecutionStatus`: `queued` is a
|
|
216
160
|
* scheduler-only condition (dependency-satisfied but held for budget capacity) that never reaches a
|
|
@@ -291,9 +235,11 @@ export interface ExecuteAnalysisDeps {
|
|
|
291
235
|
*/
|
|
292
236
|
readonly usageRecorder?: UsageRecorder;
|
|
293
237
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
238
|
+
* The provenance seam of the composition. The body reads its run emit member,
|
|
239
|
+
* and it reads no other member.
|
|
240
|
+
*
|
|
241
|
+
* The emit is deliberately invoked directly in the workflow body (NOT wrapped
|
|
242
|
+
* in `DBOS.runStep`) so that body re-execution on DBOS recovery re-fires it —
|
|
297
243
|
* a cached step would suppress the recovery re-emission, defeating the
|
|
298
244
|
* point. Idempotency is the consumer's job, achieved via the deterministic
|
|
299
245
|
* identifiers the events carry, not via step caching. Every call site is
|
|
@@ -303,19 +249,19 @@ export interface ExecuteAnalysisDeps {
|
|
|
303
249
|
* persisting realization needs its credential, scope, and identity.
|
|
304
250
|
* Implementations may ignore the parameter.
|
|
305
251
|
*/
|
|
306
|
-
readonly
|
|
252
|
+
readonly provenance?: ProvenanceSeam;
|
|
307
253
|
/**
|
|
308
254
|
* Optional, fire-and-forget observation of the run's live shape, for a host driving a UI.
|
|
309
255
|
*
|
|
310
|
-
* Independent of {@link
|
|
311
|
-
* the other, they share no payload, and supplying one does not imply the other.
|
|
312
|
-
* separately because provenance closes a signed, hash-chained record under a
|
|
313
|
-
* lock, while this is a lossy-tolerant display channel — coupling them would put
|
|
314
|
-
* behind the chain's write discipline and force provenance to carry step names and
|
|
315
|
-
* has no reason to record.
|
|
256
|
+
* Independent of the run emit of {@link provenance} in every direction: neither is implemented
|
|
257
|
+
* in terms of the other, they share no payload, and supplying one does not imply the other.
|
|
258
|
+
* They exist separately because provenance closes a signed, hash-chained record under a
|
|
259
|
+
* single-writer lock, while this is a lossy-tolerant display channel — coupling them would put
|
|
260
|
+
* a UI repaint behind the chain's write discipline and force provenance to carry step names and
|
|
261
|
+
* agent ids it has no reason to record.
|
|
316
262
|
*
|
|
317
|
-
* Invoked directly in the workflow body (NOT wrapped in `DBOS.runStep`) for the same reason
|
|
318
|
-
*
|
|
263
|
+
* Invoked directly in the workflow body (NOT wrapped in `DBOS.runStep`) for the same reason the
|
|
264
|
+
* run emit is: body re-execution on recovery must re-fire it, and a cached step would
|
|
319
265
|
* suppress that. Idempotency comes free for rendering because the payload is a whole-run
|
|
320
266
|
* snapshot; a host taking a DURABLE side effect on one must key it by run id and status itself.
|
|
321
267
|
*
|
|
@@ -88,14 +88,15 @@ function emitStreamPart(part) {
|
|
|
88
88
|
* host's own ledger, not here.
|
|
89
89
|
*/
|
|
90
90
|
function emitProvenanceGuarded(deps, event, session) {
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
91
|
+
const emit = deps.provenance?.emitRunEvent;
|
|
92
|
+
if (!emit)
|
|
93
93
|
return;
|
|
94
|
+
const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis");
|
|
94
95
|
try {
|
|
95
|
-
|
|
96
|
+
emit(event, session);
|
|
96
97
|
}
|
|
97
98
|
catch (err) {
|
|
98
|
-
logger.error("
|
|
99
|
+
logger.error("the run emit of the provenance seam threw", { runId: event.runId, event: event.type, ...logger.errorFields(err) });
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inflexa-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@dbos-inc/dbos-sdk": "^4.23.6",
|
|
57
57
|
"@fontsource-variable/space-grotesk": "5.1.1",
|
|
58
58
|
"@fontsource/ibm-plex-mono": "5.1.1",
|
|
59
|
+
"@inflexa-ai/tsprov": "0.6.0",
|
|
59
60
|
"@kubernetes/client-node": "^1.4.0",
|
|
60
61
|
"@opentelemetry/api": "^1.9.1",
|
|
61
62
|
"@opentelemetry/core": "^2.9.0",
|