@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.
Files changed (96) hide show
  1. package/dist/agents/conversation-agent.d.ts +15 -0
  2. package/dist/agents/conversation-agent.js +16 -6
  3. package/dist/agents/report-session-agent.d.ts +9 -0
  4. package/dist/agents/report-session-agent.js +10 -2
  5. package/dist/agents/sandbox/shared.d.ts +13 -1
  6. package/dist/agents/sandbox/shared.js +22 -5
  7. package/dist/app/chat-turn.d.ts +11 -0
  8. package/dist/app/chat-turn.js +17 -1
  9. package/dist/app/spawn-report-session.d.ts +12 -0
  10. package/dist/app/spawn-report-session.js +21 -1
  11. package/dist/config/environment-stores.d.ts +53 -5
  12. package/dist/contracts/report-blocks.d.ts +8 -8
  13. package/dist/contracts/schemas/chat-parts.d.ts +6 -6
  14. package/dist/index.d.ts +8 -1
  15. package/dist/index.js +11 -0
  16. package/dist/prompts/briefing.d.ts +1 -1
  17. package/dist/prompts/briefing.js +4 -0
  18. package/dist/prompts/planner.js +13 -0
  19. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  20. package/dist/prompts/sandbox/multi-omics-integration-agent.js +5 -5
  21. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  22. package/dist/prompts/sandbox/network-agent.js +4 -3
  23. package/dist/prompts/sandbox-standards.d.ts +20 -0
  24. package/dist/prompts/sandbox-standards.js +65 -0
  25. package/dist/provenance/seam.d.ts +203 -0
  26. package/dist/provenance/seam.js +71 -0
  27. package/dist/reference-data/catalog.js +11 -9
  28. package/dist/report-model/authoring-grammar.d.ts +2 -2
  29. package/dist/report-render/assets.d.ts +10 -0
  30. package/dist/report-render/assets.js +14 -0
  31. package/dist/report-render/design.js +256 -0
  32. package/dist/report-render/page.d.ts +75 -0
  33. package/dist/report-render/page.js +793 -1
  34. package/dist/report-render/provenance-data.d.ts +42 -0
  35. package/dist/report-render/provenance-data.js +62 -0
  36. package/dist/report-render/render.d.ts +20 -1
  37. package/dist/report-render/render.js +23 -10
  38. package/dist/report-render/types.d.ts +3 -0
  39. package/dist/report-render/views/chart-view.d.ts +5 -1
  40. package/dist/report-render/views/chart-view.js +6 -2
  41. package/dist/report-render/views/lineage.d.ts +93 -0
  42. package/dist/report-render/views/lineage.js +86 -0
  43. package/dist/report-render/views/page-view.d.ts +22 -1
  44. package/dist/report-render/views/page-view.js +12 -3
  45. package/dist/report-render/views/prose.d.ts +7 -1
  46. package/dist/report-render/views/prose.js +10 -6
  47. package/dist/report-render/views/references-view.d.ts +17 -1
  48. package/dist/report-render/views/references-view.js +12 -4
  49. package/dist/report-render/views/values.d.ts +17 -4
  50. package/dist/report-render/views/values.js +21 -8
  51. package/dist/runtime/assemble.d.ts +22 -0
  52. package/dist/runtime/assemble.js +4 -0
  53. package/dist/sandbox/create-sandbox.d.ts +20 -4
  54. package/dist/sandbox/create-sandbox.js +4 -0
  55. package/dist/sandbox/docker-client.d.ts +16 -6
  56. package/dist/sandbox/docker-client.js +47 -31
  57. package/dist/sandbox/farm.d.ts +95 -0
  58. package/dist/sandbox/farm.js +127 -0
  59. package/dist/sandbox/k8s-client.d.ts +24 -10
  60. package/dist/sandbox/k8s-client.js +85 -122
  61. package/dist/sandbox/mount-plan.d.ts +23 -1
  62. package/dist/sandbox/mount-plan.js +44 -7
  63. package/dist/sandbox/sandbox-error.d.ts +12 -0
  64. package/dist/sandbox/sandbox-error.js +2 -0
  65. package/dist/sandbox/types.d.ts +118 -0
  66. package/dist/schemas/artifact-manifest.d.ts +4 -4
  67. package/dist/schemas/plan-schemas.d.ts +3 -0
  68. package/dist/schemas/plan-schemas.js +1 -1
  69. package/dist/schemas/validate-plan.js +20 -1
  70. package/dist/schemas/workflow-state.d.ts +4 -0
  71. package/dist/schemas/workflow-state.js +10 -0
  72. package/dist/tasks/data-profile.d.ts +15 -0
  73. package/dist/tasks/data-profile.js +36 -26
  74. package/dist/tools/display/show-user.d.ts +1 -1
  75. package/dist/tools/execute-analysis.d.ts +18 -0
  76. package/dist/tools/execute-analysis.js +87 -0
  77. package/dist/tools/report-authoring/authoring-tools.d.ts +16 -1
  78. package/dist/tools/report-authoring/authoring-tools.js +58 -16
  79. package/dist/tools/report-session/derive-table.d.ts +3 -0
  80. package/dist/tools/report-session/derive-table.js +15 -0
  81. package/dist/tools/report-session/preview-report.d.ts +13 -4
  82. package/dist/tools/report-session/preview-report.js +23 -6
  83. package/dist/tools/report-session/record-version.d.ts +3 -0
  84. package/dist/tools/report-session/record-version.js +7 -3
  85. package/dist/tools/research/generate-plan.js +12 -2
  86. package/dist/tools/sandbox/index.d.ts +1 -0
  87. package/dist/tools/sandbox/index.js +1 -0
  88. package/dist/tools/sandbox/link-packages.d.ts +27 -0
  89. package/dist/tools/sandbox/link-packages.js +52 -0
  90. package/dist/tools/sandbox/list-available-packages.d.ts +50 -30
  91. package/dist/tools/sandbox/list-available-packages.js +131 -40
  92. package/dist/tools/start-report-session.d.ts +7 -0
  93. package/dist/tools/start-report-session.js +1 -0
  94. package/dist/workflows/execute-analysis.d.ts +15 -69
  95. package/dist/workflows/execute-analysis.js +5 -4
  96. 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
- holderIdOf(next, input.targetId),
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
- holderIdOf(previous, input.targetId),
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. It also writes the script of each derivation that the document references, from
11
- * the text of the durable record. The shipped libraries and fonts stage under `assets/deps/`, and each
12
- * report-side file at the root of `assets/`. The stage is authoritative over that root: what this preview
13
- * wrote stays, and each other file goes.
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. It also writes the script of each derivation that the document references, from
11
- * the text of the durable record. The shipped libraries and fonts stage under `assets/deps/`, and each
12
- * report-side file at the root of `assets/`. The stage is authoritative over that root: what this preview
13
- * wrote stays, and each other file goes.
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
- const rendered = renderReportPage(document, bridged.value, snapshot.citationRecords, used.map((record) => chainOf(record, sessionDir)));
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 stamped = await deps.gateway.stampRendered(threadId, computeDraftHash(draft));
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. The prune reclaims the bytes of each derivation that the
207
- // recorded document ignores, and it decides nothing about the outcome. It runs on each
208
- // record, thus an output that an amend unbound goes at the next record.
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
- createListAvailablePackagesTool(deps.packagesFile === undefined ? {} : { packagesFile: deps.packagesFile }),
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(deps.packagesFile === undefined ? {} : { packagesFile: deps.packagesFile });
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 available in the
3
- * sandbox.
2
+ * listAvailablePackages — query the R/Python/CLI/Node packages of the
3
+ * environment, from the inventory source the embedder binds.
4
4
  *
5
- * The data source is the store's `packages.txt`, assembled by the library-store
6
- * build (see the lib-store-build spec) and read from wherever the HOST can see
7
- * it — the container path when the host mounts the same store, an injected path
8
- * when it does not. Its shape is fixed by the
9
- * producers (`scripts/lib-store-common.sh`, `images/sandbox-python/inflexa-libs-refresh`):
10
- * two `#` advisory lines, then one `## <Section>` heading per language track
11
- * followed by that track's packages as a single comma-separated line.
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
- * # Available packages in the sandbox environment.
14
- * # Do NOT attempt to install packages — ...
16
+ * ## System tools (CLI)
17
+ * samtools, bcftools, ...
15
18
  *
16
- * ## R (CRAN)
17
- * Seurat, dplyr, ggplot2, ...
18
- *
19
- * ## Python (pip)
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
- /** One language track of the store, in `packages.txt` section order. */
27
- export interface Section {
28
- /** The section heading verbatim, e.g. `R (CRAN)`, `Python (pip)`. */
29
- readonly title: string;
30
- readonly packages: readonly string[];
31
- }
32
- /** One `names` lookup: present + canonical spelling + track, or absent. */
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, "packagesFile">;
80
- /** Create the package inventory over a host-readable `packages.txt`. */
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" | "cli" | "node" | undefined;
104
+ language?: "r" | "python" | "node" | "cli" | undefined;
85
105
  limit?: number | undefined;
86
106
  }, PackagesResult>;