@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
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
import { err, ok } from "neverthrow";
|
|
20
20
|
import { basename } from "node:path";
|
|
21
21
|
import { z } from "zod";
|
|
22
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
22
23
|
import { capCodePoints, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
|
|
24
|
+
import { bindSessionEmit } from "../../provenance/seam.js";
|
|
23
25
|
import { isSafeId } from "../../workspace/paths.js";
|
|
24
26
|
import { addBlock, changeBlock, locate, moveBlock, removeBlock, setTitle, } from "../../report-model/draft-operations.js";
|
|
25
27
|
import { buildOutline, childOutline, readBlock } from "../../report-model/draft-read.js";
|
|
@@ -299,6 +301,23 @@ function addedSubject(kind, block) {
|
|
|
299
301
|
function holderIdOf(document, blockId) {
|
|
300
302
|
return locate(document, blockId)?.parent?.id;
|
|
301
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* The event of one block act, or `undefined` when the document holds no block of that id.
|
|
306
|
+
*
|
|
307
|
+
* The kind rides beside the id, thus a reader of the record sees what the act touched and it needs no
|
|
308
|
+
* document of its own. The kind comes out of the document that holds the block after the act: the next
|
|
309
|
+
* document for an add, a change, and a move, and the previous one for a remove.
|
|
310
|
+
*
|
|
311
|
+
* A lookup of a landed operation always finds its block. The absent arm keeps the read total, and it drops
|
|
312
|
+
* the event rather than state a kind that no block carries.
|
|
313
|
+
*/
|
|
314
|
+
function blockActOf(type, scope, document, blockId) {
|
|
315
|
+
const kind = locate(document, blockId)?.block.kind;
|
|
316
|
+
if (kind === undefined) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
return { type, analysisId: scope.analysisId, threadId: scope.threadId, blockId, blockKind: kind };
|
|
320
|
+
}
|
|
302
321
|
/** Read the child order of one container out of a document. */
|
|
303
322
|
function containerIn(document, parentId) {
|
|
304
323
|
if (parentId === undefined) {
|
|
@@ -326,8 +345,12 @@ const AUTHORING_EXECUTION_MODE = "inline";
|
|
|
326
345
|
* Each tool reads the thread id from the scope of the call, and it loads the state of that thread through
|
|
327
346
|
* the gateway. Thus one factory serves every thread, and two threads never share one draft. A mutation
|
|
328
347
|
* persists the new document before it reports `applied: true`, thus a reported landing is never lost.
|
|
348
|
+
*
|
|
349
|
+
* A landed mutation emits one observation. The emit sits after the persist, thus a refused operation and a
|
|
350
|
+
* failed persist each emit nothing.
|
|
329
351
|
*/
|
|
330
|
-
export function createReportAuthoringTools(gateway) {
|
|
352
|
+
export function createReportAuthoringTools(gateway, deps = {}) {
|
|
353
|
+
const observe = bindSessionEmit(deps.provenance, (deps.logger ?? createNoopLogger()).named("report-authoring"));
|
|
331
354
|
/**
|
|
332
355
|
* Resolve the thread of the call, and load its state.
|
|
333
356
|
*
|
|
@@ -346,8 +369,13 @@ export function createReportAuthoringTools(gateway) {
|
|
|
346
369
|
*
|
|
347
370
|
* The persist is a compare-and-swap against the token that the load read. A concurrent turn that landed
|
|
348
371
|
* first turns the persist into a conflict, and the tool refuses with `stale-state`.
|
|
372
|
+
*
|
|
373
|
+
* `event` is the observation of the operation, and it emits after the persist landed. Thus one site owns
|
|
374
|
+
* the rule, and a refusal, a conflict, and a persist fault each emit nothing. It reads the two documents
|
|
375
|
+
* for the same reason that `holders` does: a removed block sits in the first alone, and an added block
|
|
376
|
+
* sits in the second alone. A caller that cannot name the block of its own operation gives no event.
|
|
349
377
|
*/
|
|
350
|
-
const land = async (threadId, token, previous, result, holders) => {
|
|
378
|
+
const land = async (threadId, token, previous, result, holders, event) => {
|
|
351
379
|
if (result.isErr()) {
|
|
352
380
|
return { applied: false, refusal: result.error };
|
|
353
381
|
}
|
|
@@ -362,6 +390,10 @@ export function createReportAuthoringTools(gateway) {
|
|
|
362
390
|
if (persisted.outcome === "failed") {
|
|
363
391
|
return { applied: false, refusal: { reason: "state-unavailable", detail: persisted.detail } };
|
|
364
392
|
}
|
|
393
|
+
const observed = event(previous, next);
|
|
394
|
+
if (observed !== undefined) {
|
|
395
|
+
observe(observed);
|
|
396
|
+
}
|
|
365
397
|
// A move inside one container names the same holder two times.
|
|
366
398
|
const unique = [...new Set(holders(previous, next))];
|
|
367
399
|
return { applied: true, changed: unique.map((parentId) => containerIn(next, parentId)) };
|
|
@@ -390,14 +422,17 @@ export function createReportAuthoringTools(gateway) {
|
|
|
390
422
|
if (opened.isErr()) {
|
|
391
423
|
return ok(refuse(opened.error));
|
|
392
424
|
}
|
|
393
|
-
const { threadId, state, token } = opened.value;
|
|
425
|
+
const { threadId, analysisId, state, token } = opened.value;
|
|
394
426
|
const destination = toDestination(input);
|
|
395
427
|
if (destination.isErr()) {
|
|
396
428
|
return ok(refuse(destination.error));
|
|
397
429
|
}
|
|
398
430
|
const block = decodeBlockPayload(input.block);
|
|
399
431
|
const addedId = readId(block);
|
|
400
|
-
return ok(await land(threadId, token, state.document, addBlock(state.document, { block, destination: destination.value }, state.snapshot), (_previous, next) => [addedId === undefined ? undefined : holderIdOf(next, addedId)]
|
|
432
|
+
return ok(await land(threadId, token, state.document, addBlock(state.document, { block, destination: destination.value }, state.snapshot), (_previous, next) => [addedId === undefined ? undefined : holderIdOf(next, addedId)],
|
|
433
|
+
// The grammar requires an id on the payload, thus a landed add always names one. A
|
|
434
|
+
// payload with no id reaches the core, which refuses it, and no event is due.
|
|
435
|
+
(_previous, next) => (addedId === undefined ? undefined : blockActOf("add-block", { analysisId, threadId }, next, addedId))));
|
|
401
436
|
},
|
|
402
437
|
});
|
|
403
438
|
const change_block = defineTool({
|
|
@@ -413,14 +448,15 @@ export function createReportAuthoringTools(gateway) {
|
|
|
413
448
|
if (opened.isErr()) {
|
|
414
449
|
return ok(refuse(opened.error));
|
|
415
450
|
}
|
|
416
|
-
const { threadId, state, token } = opened.value;
|
|
451
|
+
const { threadId, analysisId, state, token } = opened.value;
|
|
417
452
|
const operation = toChangeOperation(input);
|
|
418
453
|
if (operation.isErr()) {
|
|
419
454
|
return ok(refuse(operation.error));
|
|
420
455
|
}
|
|
421
|
-
return ok(await land(threadId, token, state.document, changeBlock(state.document, operation.value, state.snapshot), (_previous, next) => [
|
|
422
|
-
|
|
423
|
-
|
|
456
|
+
return ok(await land(threadId, token, state.document, changeBlock(state.document, operation.value, state.snapshot), (_previous, next) => [holderIdOf(next, input.targetId)],
|
|
457
|
+
// A change is permitted to change the kind, thus the event states the kind that the
|
|
458
|
+
// document holds after the act.
|
|
459
|
+
(_previous, next) => blockActOf("change-block", { analysisId, threadId }, next, input.targetId)));
|
|
424
460
|
},
|
|
425
461
|
});
|
|
426
462
|
const remove_block = defineTool({
|
|
@@ -434,10 +470,11 @@ export function createReportAuthoringTools(gateway) {
|
|
|
434
470
|
if (opened.isErr()) {
|
|
435
471
|
return ok(refuse(opened.error));
|
|
436
472
|
}
|
|
437
|
-
const { threadId, state, token } = opened.value;
|
|
438
|
-
return ok(await land(threadId, token, state.document, removeBlock(state.document, { targetId: input.targetId }, state.snapshot), (previous) => [
|
|
439
|
-
|
|
440
|
-
|
|
473
|
+
const { threadId, analysisId, state, token } = opened.value;
|
|
474
|
+
return ok(await land(threadId, token, state.document, removeBlock(state.document, { targetId: input.targetId }, state.snapshot), (previous) => [holderIdOf(previous, input.targetId)],
|
|
475
|
+
// The next document holds no removed block, thus the event states the kind that the
|
|
476
|
+
// previous one carried.
|
|
477
|
+
(previous) => blockActOf("remove-block", { analysisId, threadId }, previous, input.targetId)));
|
|
441
478
|
},
|
|
442
479
|
});
|
|
443
480
|
const move_block = defineTool({
|
|
@@ -452,12 +489,12 @@ export function createReportAuthoringTools(gateway) {
|
|
|
452
489
|
if (opened.isErr()) {
|
|
453
490
|
return ok(refuse(opened.error));
|
|
454
491
|
}
|
|
455
|
-
const { threadId, state, token } = opened.value;
|
|
492
|
+
const { threadId, analysisId, state, token } = opened.value;
|
|
456
493
|
const destination = toDestination(input);
|
|
457
494
|
if (destination.isErr()) {
|
|
458
495
|
return ok(refuse(destination.error));
|
|
459
496
|
}
|
|
460
|
-
return ok(await land(threadId, token, state.document, moveBlock(state.document, { targetId: input.targetId, destination: destination.value }, state.snapshot), (previous, next) => [holderIdOf(previous, input.targetId), holderIdOf(next, input.targetId)]));
|
|
497
|
+
return ok(await land(threadId, token, state.document, moveBlock(state.document, { targetId: input.targetId, destination: destination.value }, state.snapshot), (previous, next) => [holderIdOf(previous, input.targetId), holderIdOf(next, input.targetId)], (_previous, next) => blockActOf("move-block", { analysisId, threadId }, next, input.targetId)));
|
|
461
498
|
},
|
|
462
499
|
});
|
|
463
500
|
const set_title = defineTool({
|
|
@@ -473,8 +510,13 @@ export function createReportAuthoringTools(gateway) {
|
|
|
473
510
|
if (opened.isErr()) {
|
|
474
511
|
return ok(refuse(opened.error));
|
|
475
512
|
}
|
|
476
|
-
const { threadId, state, token } = opened.value;
|
|
477
|
-
return ok(await land(threadId, token, state.document, ok(setTitle(state.document, input.title)), () => [])
|
|
513
|
+
const { threadId, analysisId, state, token } = opened.value;
|
|
514
|
+
return ok(await land(threadId, token, state.document, ok(setTitle(state.document, input.title)), () => [], () => ({
|
|
515
|
+
type: "set-title",
|
|
516
|
+
analysisId,
|
|
517
|
+
threadId,
|
|
518
|
+
title: input.title,
|
|
519
|
+
})));
|
|
478
520
|
},
|
|
479
521
|
});
|
|
480
522
|
const read_outline = defineTool({
|
|
@@ -42,6 +42,7 @@ import type { DerivationSource, ReportSessionStateStore } from "../../state/repo
|
|
|
42
42
|
import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
43
43
|
import { type Tool } from "../define-tool.js";
|
|
44
44
|
import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
|
|
45
|
+
import { type ProvenanceSeam } from "../../provenance/seam.js";
|
|
45
46
|
/** The input of one derivation: the script, the declared pinned inputs, and the name of the output file. */
|
|
46
47
|
declare const deriveTableInput: z.ZodObject<{
|
|
47
48
|
script: z.ZodString;
|
|
@@ -114,6 +115,8 @@ export interface DeriveTableToolDeps {
|
|
|
114
115
|
readonly derivations: Pick<ReportSessionStateStore, "appendDerivation">;
|
|
115
116
|
readonly runDerivation?: DeriveTableRunner;
|
|
116
117
|
readonly runAuthorizer?: RunAuthorizer;
|
|
118
|
+
/** The provenance seam; an unbound session emit emits nothing and the derivation runs the same. */
|
|
119
|
+
readonly provenance?: ProvenanceSeam;
|
|
117
120
|
readonly logger?: Logger;
|
|
118
121
|
}
|
|
119
122
|
/** The synthetic run id and step id of one derivation. Both are constants, and neither names a run row. */
|
|
@@ -45,6 +45,7 @@ import { generateExecutionId } from "../../sandbox/execution-id.js";
|
|
|
45
45
|
import { isSafeId, reportSessionDerivedDir, toSandboxPath } from "../../workspace/paths.js";
|
|
46
46
|
import { defineTool } from "../define-tool.js";
|
|
47
47
|
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
48
|
+
import { bindSessionEmit } from "../../provenance/seam.js";
|
|
48
49
|
import { readHeaderColumns } from "./list-artifacts.js";
|
|
49
50
|
/** The input of one derivation: the script, the declared pinned inputs, and the name of the output file. */
|
|
50
51
|
const deriveTableInput = z.object({
|
|
@@ -128,6 +129,7 @@ export function describeExecFailure(result) {
|
|
|
128
129
|
*/
|
|
129
130
|
export function createDeriveTableTool(deps) {
|
|
130
131
|
const logger = (deps.logger ?? createNoopLogger()).named("derive-table");
|
|
132
|
+
const observe = bindSessionEmit(deps.provenance, logger);
|
|
131
133
|
const { runDerivation, runAuthorizer } = deps;
|
|
132
134
|
return defineTool({
|
|
133
135
|
id: "derive_table",
|
|
@@ -252,6 +254,19 @@ export function createDeriveTableTool(deps) {
|
|
|
252
254
|
outputName: input.output,
|
|
253
255
|
logger,
|
|
254
256
|
});
|
|
257
|
+
// The record of the derivation lands before this point, thus the event states a table that
|
|
258
|
+
// the session already holds as evidence. Each other arm derived nothing.
|
|
259
|
+
if (result.outcome === "derived") {
|
|
260
|
+
observe({
|
|
261
|
+
type: "run-derivation",
|
|
262
|
+
analysisId,
|
|
263
|
+
threadId,
|
|
264
|
+
outputPath: result.path,
|
|
265
|
+
outputHash: result.hash,
|
|
266
|
+
scriptHash: result.scriptHash,
|
|
267
|
+
sources: result.sources,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
255
270
|
return ok(result);
|
|
256
271
|
}
|
|
257
272
|
finally {
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* and renders the page.
|
|
8
8
|
*
|
|
9
9
|
* The renderer writes no file. It gives the page and the data asset of each table, and this tool stages
|
|
10
|
-
* them beside the figures.
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* them beside the figures. The tool asks the optional provenance source for the frozen document of the
|
|
11
|
+
* analysis first, and the renderer turns the opaque text into data assets of the same kind. It also writes
|
|
12
|
+
* the script of each derivation that the document references, from the text of the durable record. The
|
|
13
|
+
* shipped libraries and fonts stage under `assets/deps/`, and each report-side file at the root of
|
|
14
|
+
* `assets/`. The stage is authoritative over that root: what this preview wrote stays, and each other file
|
|
15
|
+
* goes.
|
|
14
16
|
*
|
|
15
17
|
* The page and its staged assets land in `report-sessions/{threadId}/` under the workspace root. That
|
|
16
18
|
* namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
|
|
@@ -45,6 +47,7 @@ import { type BridgeMismatch } from "../../report-render/value-bridge.js";
|
|
|
45
47
|
import type { RenderProblem } from "../../report-render/types.js";
|
|
46
48
|
import { type Tool } from "../define-tool.js";
|
|
47
49
|
import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
|
|
50
|
+
import { type ProvenanceSeam } from "../../provenance/seam.js";
|
|
48
51
|
import { type MakeSessionPagePublisher } from "./session-page-publisher.js";
|
|
49
52
|
/** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
|
|
50
53
|
declare const previewReportInput: z.ZodObject<{}, z.core.$strip>;
|
|
@@ -135,6 +138,12 @@ export interface PreviewReportToolDeps {
|
|
|
135
138
|
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
136
139
|
readonly resolvePageAsset?: ResolvePageAsset;
|
|
137
140
|
readonly makeSessionPages?: MakeSessionPagePublisher;
|
|
141
|
+
/**
|
|
142
|
+
* The provenance seam. An unbound session emit emits nothing and the render runs the same. An unbound
|
|
143
|
+
* document read, and a read that holds no document for the analysis, both give absence, and the page
|
|
144
|
+
* then carries no provenance asset.
|
|
145
|
+
*/
|
|
146
|
+
readonly provenance?: ProvenanceSeam;
|
|
138
147
|
readonly logger?: Logger;
|
|
139
148
|
}
|
|
140
149
|
/**
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* and renders the page.
|
|
8
8
|
*
|
|
9
9
|
* The renderer writes no file. It gives the page and the data asset of each table, and this tool stages
|
|
10
|
-
* them beside the figures.
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* them beside the figures. The tool asks the optional provenance source for the frozen document of the
|
|
11
|
+
* analysis first, and the renderer turns the opaque text into data assets of the same kind. It also writes
|
|
12
|
+
* the script of each derivation that the document references, from the text of the durable record. The
|
|
13
|
+
* shipped libraries and fonts stage under `assets/deps/`, and each report-side file at the root of
|
|
14
|
+
* `assets/`. The stage is authoritative over that root: what this preview wrote stays, and each other file
|
|
15
|
+
* goes.
|
|
14
16
|
*
|
|
15
17
|
* The page and its staged assets land in `report-sessions/{threadId}/` under the workspace root. That
|
|
16
18
|
* namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
|
|
@@ -55,6 +57,7 @@ import { ASSETS_DIR, DEPS_DIR, derivationScriptName, PAGE_ASSETS, stagedSource,
|
|
|
55
57
|
import { renderReportPage } from "../../report-render/render.js";
|
|
56
58
|
import { defineTool } from "../define-tool.js";
|
|
57
59
|
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
60
|
+
import { bindReadExport, bindSessionEmit } from "../../provenance/seam.js";
|
|
58
61
|
import { describeSessionPageMintFailure } from "./session-page-publisher.js";
|
|
59
62
|
/** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
|
|
60
63
|
const previewReportInput = z.object({});
|
|
@@ -383,6 +386,8 @@ async function mintAccess(makeSessionPages, analysisId, threadId, auth, logger)
|
|
|
383
386
|
*/
|
|
384
387
|
export function createPreviewReportTool(deps) {
|
|
385
388
|
const logger = (deps.logger ?? createNoopLogger()).named("preview-report");
|
|
389
|
+
const observe = bindSessionEmit(deps.provenance, logger);
|
|
390
|
+
const readProvenance = bindReadExport(deps.provenance, logger);
|
|
386
391
|
const resolvePageAsset = deps.resolvePageAsset ?? resolvePageAssetFromInstallation;
|
|
387
392
|
return defineTool({
|
|
388
393
|
id: "preview_report",
|
|
@@ -438,7 +443,15 @@ export function createPreviewReportTool(deps) {
|
|
|
438
443
|
// relative links of its entry, thus the renderer states them and it stages nothing.
|
|
439
444
|
const used = referencedDerivations(document, derivations);
|
|
440
445
|
const sessionDir = reportSessionDir(threadId);
|
|
441
|
-
|
|
446
|
+
// The provenance is the frozen document of the analysis. It rides the render as opaque text, thus
|
|
447
|
+
// the renderer moves it into a data asset and no part of this path reads a field of it. Absence
|
|
448
|
+
// gives a page with no provenance asset, and each other part of the render is unchanged.
|
|
449
|
+
const provenance = await readProvenance(analysisId);
|
|
450
|
+
const rendered = renderReportPage(document, bridged.value, {
|
|
451
|
+
records: snapshot.citationRecords,
|
|
452
|
+
derivations: used.map((record) => chainOf(record, sessionDir)),
|
|
453
|
+
provenance,
|
|
454
|
+
});
|
|
442
455
|
if (rendered.isErr()) {
|
|
443
456
|
return ok({ outcome: "render-problems", problems: rendered.error });
|
|
444
457
|
}
|
|
@@ -471,7 +484,8 @@ export function createPreviewReportTool(deps) {
|
|
|
471
484
|
// The page shows the draft that the finish read. The stamp records that draft, thus the eyes
|
|
472
485
|
// copy this hash and the record compares against it. A failed stamp is a transient store fault,
|
|
473
486
|
// and the page stays on disk. The agent runs the preview again to stamp the marker.
|
|
474
|
-
const
|
|
487
|
+
const renderedHash = computeDraftHash(draft);
|
|
488
|
+
const stamped = await deps.gateway.stampRendered(threadId, renderedHash);
|
|
475
489
|
if (stamped.outcome !== "stamped") {
|
|
476
490
|
const detail = stamped.outcome === "failed" ? stamped.detail : "the session state row is absent";
|
|
477
491
|
logger.warn("the rendered hash did not stamp", { threadId, analysisId, detail });
|
|
@@ -483,6 +497,9 @@ export function createPreviewReportTool(deps) {
|
|
|
483
497
|
type: "data-report-rendered",
|
|
484
498
|
data: { id: randomUUID(), renderedAt: new Date().toISOString(), title: document.title },
|
|
485
499
|
});
|
|
500
|
+
// The event rides the rendered arm alone, the same as the part above. The hash is the hash of
|
|
501
|
+
// the draft that the page shows, thus a consumer ties the page to the document that made it.
|
|
502
|
+
observe({ type: "preview", analysisId, threadId, pagePath: written.value, documentHash: renderedHash });
|
|
486
503
|
const access = await mintAccess(deps.makeSessionPages, analysisId, threadId, ctx.session.auth, logger);
|
|
487
504
|
return ok({ outcome: "rendered", pagePath: written.value, ...(access !== undefined ? { access } : {}) });
|
|
488
505
|
},
|
|
@@ -39,6 +39,7 @@ import type { ReportVersionStore } from "../../state/report-versions.js";
|
|
|
39
39
|
import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
40
40
|
import { type Tool } from "../define-tool.js";
|
|
41
41
|
import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
|
|
42
|
+
import { type ProvenanceSeam } from "../../provenance/seam.js";
|
|
42
43
|
/** The empty input. The tool records the current draft of the thread, thus it needs no field. */
|
|
43
44
|
declare const recordVersionInput: z.ZodObject<{}, z.core.$strip>;
|
|
44
45
|
export type RecordVersionInput = z.infer<typeof recordVersionInput>;
|
|
@@ -101,6 +102,8 @@ export interface RecordVersionToolDeps {
|
|
|
101
102
|
analysisId: string;
|
|
102
103
|
auth: AuthContext;
|
|
103
104
|
}) => ReferenceResolver;
|
|
105
|
+
/** The provenance seam; an unbound session emit emits nothing and the record runs the same. */
|
|
106
|
+
readonly provenance?: ProvenanceSeam;
|
|
104
107
|
readonly logger?: Logger;
|
|
105
108
|
}
|
|
106
109
|
/**
|
|
@@ -42,6 +42,7 @@ import { validateReport } from "../../report-model/validate.js";
|
|
|
42
42
|
import { reportSessionDerivedDir, resolveWorkspacePath } from "../../workspace/paths.js";
|
|
43
43
|
import { defineTool } from "../define-tool.js";
|
|
44
44
|
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
45
|
+
import { bindSessionEmit } from "../../provenance/seam.js";
|
|
45
46
|
/** The empty input. The tool records the current draft of the thread, thus it needs no field. */
|
|
46
47
|
const recordVersionInput = z.object({});
|
|
47
48
|
/** A short account of a record refusal, for the detail of a transient failure. */
|
|
@@ -122,6 +123,7 @@ async function pruneUnusedDerivations(args) {
|
|
|
122
123
|
*/
|
|
123
124
|
export function createRecordVersionTool(deps) {
|
|
124
125
|
const logger = (deps.logger ?? createNoopLogger()).named("record-report-version");
|
|
126
|
+
const observe = bindSessionEmit(deps.provenance, logger);
|
|
125
127
|
return defineTool({
|
|
126
128
|
id: "record_report_version",
|
|
127
129
|
description: "Record the current draft as one report version. The tool runs the whole gate first: it finishes the draft, " +
|
|
@@ -203,9 +205,11 @@ export function createRecordVersionTool(deps) {
|
|
|
203
205
|
parentSeq: thread.value.parentSeq,
|
|
204
206
|
});
|
|
205
207
|
if (recorded.isOk()) {
|
|
206
|
-
// The version stands from here
|
|
207
|
-
|
|
208
|
-
//
|
|
208
|
+
// The version stands from here, thus the event states a version that a reader can open.
|
|
209
|
+
observe({ type: "record-version", analysisId, threadId, versionId: recorded.value.versionId, replaced: recorded.value.outcome === "replaced" });
|
|
210
|
+
// The prune reclaims the bytes of each derivation that the recorded document ignores, and it
|
|
211
|
+
// decides nothing about the outcome. It runs on each record, thus an output that an amend
|
|
212
|
+
// unbound goes at the next record.
|
|
209
213
|
await pruneUnusedDerivations({
|
|
210
214
|
resolveWorkspaceRoot: deps.resolveWorkspaceRoot,
|
|
211
215
|
analysisId,
|
|
@@ -677,7 +677,13 @@ function buildPlannerSearchTools(deps) {
|
|
|
677
677
|
// collection of the reference store, or one package name that a step is
|
|
678
678
|
// about to import.
|
|
679
679
|
createListAvailableRefsTool(deps.refStorePath === undefined ? {} : { refStorePath: deps.refStorePath }),
|
|
680
|
-
|
|
680
|
+
// The planner is a conversation surface, thus the pool-scope reader binds
|
|
681
|
+
// when the embedder gives one, and the answer names what the store HOLDS.
|
|
682
|
+
createListAvailablePackagesTool({
|
|
683
|
+
...(deps.farmLockFile === undefined ? {} : { farmLockFile: deps.farmLockFile }),
|
|
684
|
+
...(deps.imagePackagesFile === undefined ? {} : { imagePackagesFile: deps.imagePackagesFile }),
|
|
685
|
+
...(deps.readPoolInventory === undefined ? {} : { readPoolInventory: deps.readPoolInventory }),
|
|
686
|
+
}),
|
|
681
687
|
];
|
|
682
688
|
}
|
|
683
689
|
/** Build the `generate_plan` tool bound to its provider and pool. */
|
|
@@ -804,7 +810,11 @@ export function createGeneratePlanTool(deps) {
|
|
|
804
810
|
// not optional planner lookups. Read both inventories host-side before
|
|
805
811
|
// the loop and put their rendered content in the planner's seed.
|
|
806
812
|
const listAvailableRefs = createListAvailableRefsTool(deps.refStorePath === undefined ? {} : { refStorePath: deps.refStorePath });
|
|
807
|
-
const listAvailablePackages = createListAvailablePackagesTool(
|
|
813
|
+
const listAvailablePackages = createListAvailablePackagesTool({
|
|
814
|
+
...(deps.farmLockFile === undefined ? {} : { farmLockFile: deps.farmLockFile }),
|
|
815
|
+
...(deps.imagePackagesFile === undefined ? {} : { imagePackagesFile: deps.imagePackagesFile }),
|
|
816
|
+
...(deps.readPoolInventory === undefined ? {} : { readPoolInventory: deps.readPoolInventory }),
|
|
817
|
+
});
|
|
808
818
|
// A path-separator query matches every reference path and selects the
|
|
809
819
|
// tool's recursive leaf scan, so the seed contains usable file paths,
|
|
810
820
|
// not only top-level directory summaries.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Sandbox barrel — sandbox-environment introspection (available lib-store
|
|
3
3
|
* packages and reference datasets). Consumed by sandbox agents.
|
|
4
4
|
*/
|
|
5
|
+
export * from "./link-packages.js";
|
|
5
6
|
export * from "./list-available-packages.js";
|
|
6
7
|
export * from "./list-available-refs.js";
|
|
7
8
|
export * from "./report-blocker.js";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Sandbox barrel — sandbox-environment introspection (available lib-store
|
|
3
3
|
* packages and reference datasets). Consumed by sandbox agents.
|
|
4
4
|
*/
|
|
5
|
+
export * from "./link-packages.js";
|
|
5
6
|
export * from "./list-available-packages.js";
|
|
6
7
|
export * from "./list-available-refs.js";
|
|
7
8
|
export * from "./report-blocker.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* link_packages — extend the farm of the analysis with host-staged packages.
|
|
3
|
+
*
|
|
4
|
+
* The tool is the agent face of the `ExtendAnalysisFarm` seam. It exists only
|
|
5
|
+
* when the embedder binds the seam (the harness-sandbox-agents spec): the
|
|
6
|
+
* composition adds it to the always-on substrate, and no `meta.tools`
|
|
7
|
+
* allowlist names it. The realization of the embedder links what the host
|
|
8
|
+
* staged — the tool never installs, downloads, or acquires anything. A link
|
|
9
|
+
* is live in a container that already runs, because the farm rides a bind
|
|
10
|
+
* mount.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtendAnalysisFarm, PackageRequestOutcome } from "../../sandbox/types.js";
|
|
13
|
+
export interface LinkPackagesDeps {
|
|
14
|
+
readonly extendAnalysisFarm: ExtendAnalysisFarm;
|
|
15
|
+
readonly analysisId: string;
|
|
16
|
+
}
|
|
17
|
+
/** The tool result: one outcome per requested package, index-aligned. */
|
|
18
|
+
export interface LinkPackagesResult {
|
|
19
|
+
readonly outcomes: readonly PackageRequestOutcome[];
|
|
20
|
+
}
|
|
21
|
+
export declare function createLinkPackagesTool(deps: LinkPackagesDeps): import("../define-tool.js").Tool<{
|
|
22
|
+
packages: {
|
|
23
|
+
name: string;
|
|
24
|
+
version?: string | undefined;
|
|
25
|
+
ecosystem?: "r" | "python" | undefined;
|
|
26
|
+
}[];
|
|
27
|
+
}, LinkPackagesResult>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* link_packages — extend the farm of the analysis with host-staged packages.
|
|
3
|
+
*
|
|
4
|
+
* The tool is the agent face of the `ExtendAnalysisFarm` seam. It exists only
|
|
5
|
+
* when the embedder binds the seam (the harness-sandbox-agents spec): the
|
|
6
|
+
* composition adds it to the always-on substrate, and no `meta.tools`
|
|
7
|
+
* allowlist names it. The realization of the embedder links what the host
|
|
8
|
+
* staged — the tool never installs, downloads, or acquires anything. A link
|
|
9
|
+
* is live in a container that already runs, because the farm rides a bind
|
|
10
|
+
* mount.
|
|
11
|
+
*/
|
|
12
|
+
import { ok } from "neverthrow";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { defineTool } from "../define-tool.js";
|
|
15
|
+
export function createLinkPackagesTool(deps) {
|
|
16
|
+
return defineTool({
|
|
17
|
+
id: "link_packages",
|
|
18
|
+
description: "Link host-staged packages into the farm of this analysis, so an import that failed works without a restart. " +
|
|
19
|
+
"This tool links what the host already staged — it NEVER installs, downloads, or acquires anything. " +
|
|
20
|
+
"Pass each module name verbatim, exactly as the import names it. " +
|
|
21
|
+
"One outcome comes back per request: `linked` (the pool held it, and it is importable now), " +
|
|
22
|
+
"`present` (the farm held it already), " +
|
|
23
|
+
"`absent` (the pool does not hold it — a real answer, and `acquisitionPossible` states whether the host can acquire that ecosystem; report the package as missing, do not retry), " +
|
|
24
|
+
"`collision` (the request resolves to two store directories — terminal for that package; the `detail` names the two pins and the packages that need each; report it and continue without it), " +
|
|
25
|
+
"or `unavailable` (the link pass itself could not answer — the `reason` says why; it says nothing about the package's presence, so report the reason and do not re-request packages).",
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
packages: z
|
|
28
|
+
.array(z.object({
|
|
29
|
+
name: z.string().min(1).describe("The module or package name, verbatim."),
|
|
30
|
+
version: z.string().optional().describe("One exact version, when the requirement pins one."),
|
|
31
|
+
ecosystem: z.enum(["python", "r"]).optional().describe("The ecosystem, when the bare name is ambiguous."),
|
|
32
|
+
}))
|
|
33
|
+
.min(1)
|
|
34
|
+
.max(50)
|
|
35
|
+
.describe("The packages to link into the farm."),
|
|
36
|
+
}),
|
|
37
|
+
describeCall: ({ packages }) => packages.map((p) => p.name).join(", "),
|
|
38
|
+
execute: async (input) => {
|
|
39
|
+
// A realization throw reads as `unavailable` per request. The loop
|
|
40
|
+
// would render the throw as a raw tool error, and a raw driver
|
|
41
|
+
// message teaches the agent nothing that `reason` does not.
|
|
42
|
+
try {
|
|
43
|
+
const outcomes = await deps.extendAnalysisFarm(deps.analysisId, input.packages);
|
|
44
|
+
return ok({ outcomes });
|
|
45
|
+
}
|
|
46
|
+
catch (cause) {
|
|
47
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
48
|
+
return ok({ outcomes: input.packages.map((request) => ({ kind: "unavailable", name: request.name, reason })) });
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -1,40 +1,48 @@
|
|
|
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
|
-
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
import type { EnvironmentStorePaths, PoolInventoryPackage, PoolInventorySection } from "../../config/environment-stores.js";
|
|
25
|
+
import { type FarmLock } from "../../sandbox/farm.js";
|
|
26
|
+
/**
|
|
27
|
+
* Group the lock packages into sections, one per track, in a stable order:
|
|
28
|
+
* the known tracks first, then an unknown track at its first occurrence.
|
|
29
|
+
* Each entry keeps the lock's version, store directory, and hash, so the
|
|
30
|
+
* targeted `names` path can report the exact store identity.
|
|
31
|
+
*/
|
|
32
|
+
export declare function lockSections(lock: FarmLock): Section[];
|
|
33
|
+
/** One package entry of a section, with the store identity where the source records it. */
|
|
34
|
+
export type SectionPackage = PoolInventoryPackage;
|
|
35
|
+
/** One language track of the store, in `packages.txt` section order. The seam shape and the render shape are one. */
|
|
36
|
+
export type Section = PoolInventorySection;
|
|
37
|
+
/** One `names` lookup: present + canonical spelling + track + store identity, or absent. */
|
|
33
38
|
export type CheckedPackage = {
|
|
34
39
|
readonly requested: string;
|
|
35
40
|
readonly present: true;
|
|
36
41
|
readonly name: string;
|
|
37
42
|
readonly section: string;
|
|
43
|
+
readonly version?: string;
|
|
44
|
+
readonly storeDir?: string;
|
|
45
|
+
readonly hash?: string;
|
|
38
46
|
} | {
|
|
39
47
|
readonly requested: string;
|
|
40
48
|
readonly present: false;
|
|
@@ -76,11 +84,23 @@ export interface PackagesQuery {
|
|
|
76
84
|
* is only the file read plus this call.
|
|
77
85
|
*/
|
|
78
86
|
export declare function queryPackages(sections: readonly Section[], { names, query, language, limit }: PackagesQuery): PackagesResult;
|
|
79
|
-
export type ListAvailablePackagesDeps = Pick<EnvironmentStorePaths, "
|
|
80
|
-
/**
|
|
87
|
+
export type ListAvailablePackagesDeps = Pick<EnvironmentStorePaths, "farmLockFile" | "imagePackagesFile" | "readPoolInventory"> & {
|
|
88
|
+
/**
|
|
89
|
+
* True when the composition also attaches the `link_packages` tool. The
|
|
90
|
+
* framing sentence must not close the world on the farm then: a package
|
|
91
|
+
* absent from the farm lock can still link in from the pool.
|
|
92
|
+
*/
|
|
93
|
+
readonly linkToolPresent?: boolean;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Create the package inventory over the bound source: the pool-scope reader
|
|
97
|
+
* when the embedder binds one (a conversation or planning surface), the
|
|
98
|
+
* host-readable farm `inflexa.lock` otherwise (a sandbox agent). The image
|
|
99
|
+
* fragment merges into either report.
|
|
100
|
+
*/
|
|
81
101
|
export declare function createListAvailablePackagesTool(deps?: ListAvailablePackagesDeps): import("../define-tool.js").Tool<{
|
|
82
102
|
names?: string[] | undefined;
|
|
83
103
|
query?: string | undefined;
|
|
84
|
-
language?: "r" | "python" | "
|
|
104
|
+
language?: "r" | "python" | "node" | "cli" | undefined;
|
|
85
105
|
limit?: number | undefined;
|
|
86
106
|
}, PackagesResult>;
|