@inflexa-ai/harness 0.31.0 → 0.32.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 (62) hide show
  1. package/dist/agents/conversation-agent.d.ts +7 -0
  2. package/dist/agents/conversation-agent.js +1 -0
  3. package/dist/agents/report-session-agent.d.ts +9 -0
  4. package/dist/agents/report-session-agent.js +10 -2
  5. package/dist/app/chat-turn.d.ts +11 -0
  6. package/dist/app/chat-turn.js +17 -1
  7. package/dist/app/spawn-report-session.d.ts +12 -0
  8. package/dist/app/spawn-report-session.js +21 -1
  9. package/dist/contracts/report-blocks.d.ts +8 -8
  10. package/dist/contracts/schemas/chat-parts.d.ts +6 -6
  11. package/dist/index.d.ts +4 -1
  12. package/dist/index.js +8 -0
  13. package/dist/prompts/briefing.d.ts +12 -1
  14. package/dist/prompts/briefing.js +27 -1
  15. package/dist/prompts/sandbox-standards.d.ts +1 -1
  16. package/dist/prompts/sandbox-standards.js +27 -0
  17. package/dist/provenance/seam.d.ts +203 -0
  18. package/dist/provenance/seam.js +71 -0
  19. package/dist/report-model/authoring-grammar.d.ts +2 -2
  20. package/dist/report-render/assets.d.ts +10 -0
  21. package/dist/report-render/assets.js +14 -0
  22. package/dist/report-render/design.js +256 -0
  23. package/dist/report-render/page.d.ts +75 -0
  24. package/dist/report-render/page.js +793 -1
  25. package/dist/report-render/provenance-data.d.ts +42 -0
  26. package/dist/report-render/provenance-data.js +62 -0
  27. package/dist/report-render/render.d.ts +20 -1
  28. package/dist/report-render/render.js +23 -10
  29. package/dist/report-render/types.d.ts +3 -0
  30. package/dist/report-render/views/chart-view.d.ts +5 -1
  31. package/dist/report-render/views/chart-view.js +6 -2
  32. package/dist/report-render/views/lineage.d.ts +93 -0
  33. package/dist/report-render/views/lineage.js +86 -0
  34. package/dist/report-render/views/page-view.d.ts +22 -1
  35. package/dist/report-render/views/page-view.js +12 -3
  36. package/dist/report-render/views/prose.d.ts +7 -1
  37. package/dist/report-render/views/prose.js +10 -6
  38. package/dist/report-render/views/references-view.d.ts +17 -1
  39. package/dist/report-render/views/references-view.js +12 -4
  40. package/dist/report-render/views/values.d.ts +17 -4
  41. package/dist/report-render/views/values.js +21 -8
  42. package/dist/runtime/assemble.d.ts +22 -0
  43. package/dist/runtime/assemble.js +4 -0
  44. package/dist/sandbox/k8s-client.d.ts +0 -5
  45. package/dist/sandbox/k8s-client.js +9 -115
  46. package/dist/sandbox/thread-env.d.ts +18 -12
  47. package/dist/sandbox/thread-env.js +36 -25
  48. package/dist/schemas/artifact-manifest.d.ts +4 -4
  49. package/dist/tools/display/show-user.d.ts +1 -1
  50. package/dist/tools/report-authoring/authoring-tools.d.ts +16 -1
  51. package/dist/tools/report-authoring/authoring-tools.js +58 -16
  52. package/dist/tools/report-session/derive-table.d.ts +3 -0
  53. package/dist/tools/report-session/derive-table.js +15 -0
  54. package/dist/tools/report-session/preview-report.d.ts +13 -4
  55. package/dist/tools/report-session/preview-report.js +23 -6
  56. package/dist/tools/report-session/record-version.d.ts +3 -0
  57. package/dist/tools/report-session/record-version.js +7 -3
  58. package/dist/tools/start-report-session.d.ts +7 -0
  59. package/dist/tools/start-report-session.js +1 -0
  60. package/dist/workflows/execute-analysis.d.ts +15 -69
  61. package/dist/workflows/execute-analysis.js +5 -4
  62. package/package.json +2 -1
@@ -35,6 +35,7 @@ import { type BioToolKeys } from "../tools/bio/keys.js";
35
35
  import type { EnvironmentStorePaths } from "../config/environment-stores.js";
36
36
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
37
37
  import type { RunLauncher } from "../execution/run-launcher.js";
38
+ import type { ProvenanceSeam } from "../provenance/seam.js";
38
39
  import type { Logger } from "../lib/logger.js";
39
40
  import type { UsageRecorder } from "../billing/usage-recorder.js";
40
41
  import type { CitationResolver } from "../citations/types.js";
@@ -111,6 +112,12 @@ export interface ConversationAgentDeps extends EnvironmentStorePaths {
111
112
  * at a page reads that same answer.
112
113
  */
113
114
  readonly eyes?: AcquireEyes;
115
+ /**
116
+ * The provenance seam. `start_report_session` hands it to its spawn, which
117
+ * emits the creation of the session at the moment that the child lands. No
118
+ * other tool of this roster reads it.
119
+ */
120
+ readonly provenance?: ProvenanceSeam;
114
121
  /**
115
122
  * Host resource policy — per-step ceilings + machine budget. `generate_plan`
116
123
  * states the ceilings to the planner and validates against them;
@@ -122,6 +122,7 @@ export function createConversationAgent(deps) {
122
122
  chrome,
123
123
  anchorSession: deps.anchorReportSession,
124
124
  ...(deps.eyes ? { eyes: deps.eyes } : {}),
125
+ ...(deps.provenance ? { provenance: deps.provenance } : {}),
125
126
  ...(deps.logger ? { logger: deps.logger } : {}),
126
127
  }),
127
128
  // Display.
@@ -38,6 +38,7 @@ import type { ThreadStore } from "../memory/thread-store.js";
38
38
  import type { ReferenceResolver } from "../report-model/reference-resolver.js";
39
39
  import type { ReportVersionStore } from "../state/report-versions.js";
40
40
  import type { ReportSessionStateGateway } from "../tools/report-authoring/authoring-tools.js";
41
+ import type { ProvenanceSeam } from "../provenance/seam.js";
41
42
  import { type MakeSessionPagePublisher, type ResolvePageAsset, type ResolvePageUrl } from "../tools/report-session/index.js";
42
43
  /** Canonical agent id -- distinct from the id of the old templating agent. */
43
44
  export declare const REPORT_SESSION_AGENT_ID: "report-session";
@@ -114,6 +115,14 @@ export interface ReportSessionAgentDeps {
114
115
  * tool navigates through a `file://` URL of the page path.
115
116
  */
116
117
  readonly resolvePageUrl?: ResolvePageUrl;
118
+ /**
119
+ * The provenance seam -- the acts of the session as typed events, and the frozen document of the
120
+ * analysis as opaque bytes. The tools that change the draft, derive a table, preview the page, and
121
+ * record a version each emit through the session member, thus one bound member carries the whole
122
+ * session. The preview asks the read member before each render, and the page then carries the
123
+ * document. An unbound member emits nothing, or gives absence, and every tool acts the same.
124
+ */
125
+ readonly provenance?: ProvenanceSeam;
117
126
  /** Operational logging seam; omitted falls back to no-op. */
118
127
  readonly logger?: Logger;
119
128
  }
@@ -39,8 +39,11 @@ const REPORT_SESSION_MAX_ITERATIONS = 200;
39
39
  /** Build the report `AgentDefinition` with every tool bound to its deps. */
40
40
  export function createReportSessionAgent(deps) {
41
41
  const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, store, threads, chrome, eyes, makeResolver, resolvePageAsset, logger } = deps;
42
- const { derivations, runDerivation, runAuthorizer, makeSessionPages, resolvePageUrl } = deps;
43
- const authoring = createReportAuthoringTools(gateway);
42
+ const { derivations, runDerivation, runAuthorizer, makeSessionPages, resolvePageUrl, provenance } = deps;
43
+ // The seam rides into each tool that lands an act of the session, thus the composition binds it one
44
+ // time and no tool reads a second source.
45
+ const observation = provenance ? { provenance } : {};
46
+ const authoring = createReportAuthoringTools(gateway, { ...observation, ...(logger ? { logger } : {}) });
44
47
  const tools = [
45
48
  // The read surface toward the analysis. It roams the tree read-only.
46
49
  createReadFileTool(workspaceFs),
@@ -75,6 +78,7 @@ export function createReportSessionAgent(deps) {
75
78
  derivations,
76
79
  ...(runDerivation ? { runDerivation } : {}),
77
80
  ...(runAuthorizer ? { runAuthorizer } : {}),
81
+ ...observation,
78
82
  ...(logger ? { logger } : {}),
79
83
  }),
80
84
  // The render-and-preview tool. It resolves the page root per call.
@@ -84,6 +88,9 @@ export function createReportSessionAgent(deps) {
84
88
  ...(makeResolver ? { makeResolver } : {}),
85
89
  ...(resolvePageAsset ? { resolvePageAsset } : {}),
86
90
  ...(makeSessionPages ? { makeSessionPages } : {}),
91
+ // The preview is the one site that carries the document into the page, thus the read member
92
+ // reaches this tool alone.
93
+ ...observation,
87
94
  ...(logger ? { logger } : {}),
88
95
  }),
89
96
  // The eyes tool. It opens the rendered page in headless Chrome.
@@ -103,6 +110,7 @@ export function createReportSessionAgent(deps) {
103
110
  threads,
104
111
  resolveWorkspaceRoot,
105
112
  ...(makeResolver ? { makeResolver } : {}),
113
+ ...observation,
106
114
  ...(logger ? { logger } : {}),
107
115
  }),
108
116
  ];
@@ -16,10 +16,21 @@ import type { Pool } from "pg";
16
16
  import { type ThreadType } from "../memory/thread-store.js";
17
17
  import { type AssembledMessages } from "./message-assembly.js";
18
18
  import type { Logger } from "../lib/logger.js";
19
+ import { type ProvenanceSeam } from "../provenance/seam.js";
19
20
  export interface PrepareChatTurnDeps {
20
21
  /** Operational logging seam; omitted falls back to no-op. */
21
22
  readonly logger?: Logger;
22
23
  readonly pool: Pool;
24
+ /**
25
+ * The provenance seam. The turn reads its session emit member, and no other
26
+ * member. The turn holds the one site that writes the conversation thread of
27
+ * an analysis, thus it is the one site that can tell the embedder the true
28
+ * moment of that creation.
29
+ *
30
+ * The member is optional and fire-and-forget, the same as it is at each site
31
+ * of a report session.
32
+ */
33
+ readonly provenance?: ProvenanceSeam;
23
34
  }
24
35
  export interface PrepareChatTurnParams {
25
36
  readonly analysisId: string;
@@ -21,6 +21,7 @@ import { loadAnalysisStatus, queryNonTerminalRunsByAnalysis } from "../state/ind
21
21
  import { assembleMessages } from "./message-assembly.js";
22
22
  import { renderRunActivity, renderRunActivityUnavailable, RUN_ACTIVITY_DETAIL_LIMIT } from "./run-activity.js";
23
23
  import { createNoopLogger } from "../lib/console-logger.js";
24
+ import { bindSessionEmit } from "../provenance/seam.js";
24
25
  /**
25
26
  * Prepare one chat turn: resolve thread ownership, seed the title, load
26
27
  * analysis status, and assemble the message array. Input sanitization stays
@@ -29,6 +30,10 @@ import { createNoopLogger } from "../lib/console-logger.js";
29
30
  export async function prepareChatTurn(deps, params) {
30
31
  const { pool } = deps;
31
32
  const { analysisId, threadId, userInput } = params;
33
+ const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
34
+ // An unbound member gives a call that does nothing, thus the emit below needs
35
+ // no test of its own.
36
+ const observe = bindSessionEmit(deps.provenance, logger);
32
37
  // Ownership check before any read/write of the thread — a `threadId`
33
38
  // owned by a different analysis is indistinguishable from a missing one.
34
39
  const store = createThreadStore(pool);
@@ -54,13 +59,24 @@ export async function prepareChatTurn(deps, params) {
54
59
  title: deriveThreadTitle(userInput),
55
60
  }));
56
61
  threadType = created.threadType;
62
+ // The row that comes back is not proof that this call wrote it. The read
63
+ // above hides an archived thread, and the idempotent insert then reads
64
+ // that same row back. Thus a turn on an archived thread reaches here with
65
+ // a row that a different site created, and an emit would record a start
66
+ // that never happened.
67
+ //
68
+ // This site writes a live conversation thread and nothing else. An
69
+ // archived row, or a row of another kind, is therefore one that already
70
+ // existed. A root session has no parent, thus the event carries none.
71
+ if (created.deletedAt === null && created.threadType === "conversation") {
72
+ observe({ type: "create-session", analysisId, threadId, sessionKind: created.threadType });
73
+ }
57
74
  }
58
75
  else if (!existing.title || existing.title.length === 0) {
59
76
  unwrapOrThrow(await store.updateTitle(threadId, deriveThreadTitle(userInput)));
60
77
  }
61
78
  }
62
79
  catch (err) {
63
- const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
64
80
  logger.warn("title-seed failed (non-fatal)", logger.errorFields(err));
65
81
  }
66
82
  const analysisState = await loadAnalysisStatus(pool, analysisId).unwrapOr(null);
@@ -66,6 +66,7 @@ import type { AcquireEyes } from "../lib/eyes.js";
66
66
  import type { Logger } from "../lib/logger.js";
67
67
  import type { CapturePage } from "../lib/page-capture.js";
68
68
  import { type Thread, type ThreadInputError, type ThreadPage, type ThreadType } from "../memory/thread-store.js";
69
+ import { type ProvenanceSeam } from "../provenance/seam.js";
69
70
  import type { EnsureSessionStateResult } from "./report-session-runtime.js";
70
71
  /**
71
72
  * A spawn the operation refuses before it writes a row. Distinct from
@@ -192,6 +193,17 @@ export interface ReportSessionSpawnDeps {
192
193
  * is idempotent.
193
194
  */
194
195
  readonly anchorSession?: (threadId: string) => Promise<EnsureSessionStateResult>;
196
+ /**
197
+ * The provenance seam. The spawn reads its session emit member, and no other
198
+ * member. The spawn is the birth of the session, thus it is the one site that
199
+ * can tell the embedder the true moment of the ask. A consumer that waits for
200
+ * the first authoring act reads a later moment, and it reads nothing at all
201
+ * for a session that stays empty.
202
+ *
203
+ * The member is optional and fire-and-forget, the same as it is at each
204
+ * authoring site.
205
+ */
206
+ readonly provenance?: ProvenanceSeam;
195
207
  /** Operational logging seam. An omitted logger falls back to the no-op. */
196
208
  readonly logger?: Logger;
197
209
  }
@@ -65,6 +65,7 @@ import { createNoopLogger } from "../lib/console-logger.js";
65
65
  import { conversationRecordTurn, createThreadHistory } from "../memory/thread-history.js";
66
66
  import { createThreadStore } from "../memory/thread-store.js";
67
67
  import { createWorkingMemory } from "../memory/working-memory.js";
68
+ import { bindSessionEmit } from "../provenance/seam.js";
68
69
  const _assertDomainError = true;
69
70
  /**
70
71
  * Whether the composition gives a route to a look at the rendered page. The
@@ -165,6 +166,9 @@ export function createReportSessionSpawn(deps) {
165
166
  // The routes are fixed at construction, thus the gate reads one boolean and
166
167
  // never a live probe of the sidecar.
167
168
  const eyesAvailable = compositionHasEyes(deps);
169
+ // An unbound member gives a call that does nothing, thus the spawn emits at
170
+ // one site with no test of its own.
171
+ const observe = bindSessionEmit(deps.provenance, log);
168
172
  /**
169
173
  * Write the one seed message of the child and give the child back. The
170
174
  * message rides as a record, not as user input: nobody typed it in the child
@@ -250,7 +254,7 @@ export function createReportSessionSpawn(deps) {
250
254
  if (!eyesAvailable) {
251
255
  return errAsync({ type: "no_browser", op: OP, parentThreadId, detail: NO_BROWSER_DETAIL });
252
256
  }
253
- return store.getThread(parentThreadId).andThen((parent) => {
257
+ const spawned = store.getThread(parentThreadId).andThen((parent) => {
254
258
  // An absent parent and an archived one arrive the same way — `getThread`
255
259
  // filters the tombstone — and both refuse as `parent_not_found`.
256
260
  if (parent === null) {
@@ -288,6 +292,22 @@ export function createReportSessionSpawn(deps) {
288
292
  .andThen((child) => ResultAsync.fromSafePromise(pinSessionSnapshot(child))));
289
293
  });
290
294
  });
295
+ // The emit rides the ok channel alone, thus a refusal and a fault tell the
296
+ // embedder nothing. The child row is on disk at this point, and no
297
+ // authoring act of the session ran yet.
298
+ //
299
+ // The kind comes from the row and not from the input, because the insert
300
+ // is idempotent and the store tells a caller to read the type back.
301
+ return spawned.map((child) => {
302
+ observe({
303
+ type: "create-session",
304
+ analysisId: child.analysisId,
305
+ threadId: child.threadId,
306
+ sessionKind: child.threadType,
307
+ parentThreadId,
308
+ });
309
+ return child;
310
+ });
291
311
  }
292
312
  function listReportSessions(analysisId, paging = {}) {
293
313
  return store.listThreads({ analysisId, type: "report", ...paging });
@@ -200,15 +200,15 @@ export declare const ChartAxesSchema: z.ZodObject<{
200
200
  x: z.ZodOptional<z.ZodObject<{
201
201
  title: z.ZodOptional<z.ZodString>;
202
202
  scale: z.ZodOptional<z.ZodEnum<{
203
- log: "log";
204
203
  linear: "linear";
204
+ log: "log";
205
205
  }>>;
206
206
  }, z.core.$strict>>;
207
207
  y: z.ZodOptional<z.ZodObject<{
208
208
  title: z.ZodOptional<z.ZodString>;
209
209
  scale: z.ZodOptional<z.ZodEnum<{
210
- log: "log";
211
210
  linear: "linear";
211
+ log: "log";
212
212
  }>>;
213
213
  }, z.core.$strict>>;
214
214
  }, z.core.$strict>;
@@ -297,15 +297,15 @@ export declare const ChartCompositionSchema: z.ZodObject<{
297
297
  x: z.ZodOptional<z.ZodObject<{
298
298
  title: z.ZodOptional<z.ZodString>;
299
299
  scale: z.ZodOptional<z.ZodEnum<{
300
- log: "log";
301
300
  linear: "linear";
301
+ log: "log";
302
302
  }>>;
303
303
  }, z.core.$strict>>;
304
304
  y: z.ZodOptional<z.ZodObject<{
305
305
  title: z.ZodOptional<z.ZodString>;
306
306
  scale: z.ZodOptional<z.ZodEnum<{
307
- log: "log";
308
307
  linear: "linear";
308
+ log: "log";
309
309
  }>>;
310
310
  }, z.core.$strict>>;
311
311
  }, z.core.$strict>>;
@@ -724,15 +724,15 @@ export declare const ChartBlockSchema: z.ZodObject<{
724
724
  x: z.ZodOptional<z.ZodObject<{
725
725
  title: z.ZodOptional<z.ZodString>;
726
726
  scale: z.ZodOptional<z.ZodEnum<{
727
- log: "log";
728
727
  linear: "linear";
728
+ log: "log";
729
729
  }>>;
730
730
  }, z.core.$strict>>;
731
731
  y: z.ZodOptional<z.ZodObject<{
732
732
  title: z.ZodOptional<z.ZodString>;
733
733
  scale: z.ZodOptional<z.ZodEnum<{
734
- log: "log";
735
734
  linear: "linear";
735
+ log: "log";
736
736
  }>>;
737
737
  }, z.core.$strict>>;
738
738
  }, z.core.$strict>>;
@@ -1168,15 +1168,15 @@ export declare const ATOM_BLOCK_SCHEMAS: readonly [z.ZodObject<{
1168
1168
  x: z.ZodOptional<z.ZodObject<{
1169
1169
  title: z.ZodOptional<z.ZodString>;
1170
1170
  scale: z.ZodOptional<z.ZodEnum<{
1171
- log: "log";
1172
1171
  linear: "linear";
1172
+ log: "log";
1173
1173
  }>>;
1174
1174
  }, z.core.$strict>>;
1175
1175
  y: z.ZodOptional<z.ZodObject<{
1176
1176
  title: z.ZodOptional<z.ZodString>;
1177
1177
  scale: z.ZodOptional<z.ZodEnum<{
1178
- log: "log";
1179
1178
  linear: "linear";
1179
+ log: "log";
1180
1180
  }>>;
1181
1181
  }, z.core.$strict>>;
1182
1182
  }, z.core.$strict>>;
@@ -240,9 +240,9 @@ export declare const StepOutputFileSchema: z.ZodObject<{
240
240
  fileType: z.ZodEnum<{
241
241
  output: "output";
242
242
  summary: "summary";
243
- script: "script";
244
- figure: "figure";
245
243
  log: "log";
244
+ figure: "figure";
245
+ script: "script";
246
246
  notebook: "notebook";
247
247
  }>;
248
248
  description: z.ZodString;
@@ -259,9 +259,9 @@ export declare const StepOutputPartSchema: z.ZodObject<{
259
259
  fileType: z.ZodEnum<{
260
260
  output: "output";
261
261
  summary: "summary";
262
- script: "script";
263
- figure: "figure";
264
262
  log: "log";
263
+ figure: "figure";
264
+ script: "script";
265
265
  notebook: "notebook";
266
266
  }>;
267
267
  description: z.ZodString;
@@ -597,9 +597,9 @@ export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
597
597
  fileType: z.ZodEnum<{
598
598
  output: "output";
599
599
  summary: "summary";
600
- script: "script";
601
- figure: "figure";
602
600
  log: "log";
601
+ figure: "figure";
602
+ script: "script";
603
603
  notebook: "notebook";
604
604
  }>;
605
605
  description: z.ZodString;
package/dist/index.d.ts CHANGED
@@ -36,6 +36,9 @@ export type { ProvenanceCollector } from "./provenance/collector.js";
36
36
  export { UnavailableSessionPagePublisher } from "./tools/report-session/session-page-publisher.js";
37
37
  export type { MakeSessionPagePublisher, SessionPagePublisher, SessionPageMintResult } from "./tools/report-session/session-page-publisher.js";
38
38
  export type { ResolvePageUrl, SessionPageAccess } from "./tools/report-session/index.js";
39
+ export { bindReadExport, bindSessionEmit } from "./provenance/seam.js";
40
+ export type { ProvenanceExport, ProvenanceSeam, RunProvenanceEvent, SessionProvenanceEvent } from "./provenance/seam.js";
41
+ export type { DerivationSource } from "./state/report-session-state.js";
39
42
  export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
40
43
  export type { RunLauncher, LaunchOptions } from "./execution/run-launcher.js";
41
44
  export { createDbosWorkflowPurger } from "./execution/dbos-workflow-purger.js";
@@ -140,7 +143,7 @@ export { deliverExecEvent, execEventTopic } from "./sandbox/deliver-exec-event.j
140
143
  export { ExecEventMessageSchema } from "./sandbox/types.js";
141
144
  export type { ExecEventMessage, DoneMarker } from "./sandbox/types.js";
142
145
  export { registerExecuteAnalysis } from "./workflows/execute-analysis.js";
143
- export type { ExecuteAnalysisDeps, ExecuteAnalysisInput, ExecuteAnalysisResult, RunObservation, RunProvenanceEvent, RunStepObservation, StepRuntimeStatus, } from "./workflows/execute-analysis.js";
146
+ export type { ExecuteAnalysisDeps, ExecuteAnalysisInput, ExecuteAnalysisResult, RunObservation, RunStepObservation, StepRuntimeStatus, } from "./workflows/execute-analysis.js";
144
147
  export { registerSandboxStep } from "./workflows/sandbox-step.js";
145
148
  export type { SandboxStepDeps, SandboxStepInput, SandboxStepResult, SandboxAgentBuildContext } from "./workflows/sandbox-step.js";
146
149
  export { DEFAULT_CLAIM_INVESTIGATION_CONFIG } from "./workflows/target-assessment/investigation/index.js";
package/dist/index.js CHANGED
@@ -47,6 +47,14 @@ export { createNoopArtifactRegistry } from "./execution/noop-artifact-registry.j
47
47
  // state. The URL seam of the eyes rides beside the publisher factory: a host whose
48
48
  // browser cannot reach the workspace tree binds a served URL for the look.
49
49
  export { UnavailableSessionPagePublisher } from "./tools/report-session/session-page-publisher.js";
50
+ // Seam: the one provenance surface. The embedder records the facts of a run and the acts of a
51
+ // session in the document of the analysis that it owns, and it gives the bytes of that document
52
+ // back for the report page. Thus the seam and its vocabulary belong on the front door: an embedder
53
+ // cannot type its realization without them. The two binders ride beside the types, because an
54
+ // embedder that calls `prepareChatTurn` or one tool factory directly binds the same total forms
55
+ // that the assembly binds. `DerivationSource` rides beside the event union, because the derivation
56
+ // event carries the chain of the derived table.
57
+ export { bindReadExport, bindSessionEmit } from "./provenance/seam.js";
50
58
  // Run launching.
51
59
  export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
52
60
  // Seam: workflow-ledger reclamation — cancels and deletes an analysis's rows in
@@ -36,7 +36,9 @@ export declare const STEP_TASK_FIELDS: readonly ["name", "question", "descriptio
36
36
  * threaded separately through the workflow input, execution knobs, and result
37
37
  * fields populated after the step runs. `depends_on` is not rendered here
38
38
  * because the dependencies appear as {@link renderUpstream} blocks — with their
39
- * results — rather than as bare ids.
39
+ * results — rather than as bare ids. `resources` renders as its own
40
+ * {@link renderResources} section, because the budget is an execution bound,
41
+ * not a task instruction.
40
42
  */
41
43
  export declare const STEP_NON_TASK_FIELDS: readonly ["id", "track", "step_type", "depends_on", "status", "resources", "agent", "timeout", "maxSteps", "summary", "artifactIds", "error"];
42
44
  /**
@@ -66,6 +68,15 @@ export interface WorkspaceFrame {
66
68
  /** The in-sandbox analysis root of a frame — the read-only mount of the whole tree. */
67
69
  export declare function analysisRootOf(frame: WorkspaceFrame): string;
68
70
  export declare function renderWorkspace(frame: WorkspaceFrame): string;
71
+ /**
72
+ * The cpu and memory budget of the step, with the one rule that keeps a
73
+ * parallel workload inside it. The mounted cpu files make `detectCores()` and
74
+ * `os.cpu_count()` report the quota, thus the fork count is right by default.
75
+ * The thread pools default to one thread (see `sandbox/thread-env.ts`), and
76
+ * only the agent knows when a step is thread-parallel. Thus the briefing names
77
+ * the budget and the rule, and the agent raises a pool per command.
78
+ */
79
+ export declare function renderResources(resources: AnalysisStep["resources"]): string;
69
80
  /**
70
81
  * What dataset the analysis is holding, projected from the persisted profile.
71
82
  * Absent or not-yet-profiled → `""`, and the agent falls back to its always-on
@@ -45,7 +45,9 @@ export const STEP_TASK_FIELDS = [
45
45
  * threaded separately through the workflow input, execution knobs, and result
46
46
  * fields populated after the step runs. `depends_on` is not rendered here
47
47
  * because the dependencies appear as {@link renderUpstream} blocks — with their
48
- * results — rather than as bare ids.
48
+ * results — rather than as bare ids. `resources` renders as its own
49
+ * {@link renderResources} section, because the budget is an execution bound,
50
+ * not a task instruction.
49
51
  */
50
52
  export const STEP_NON_TASK_FIELDS = [
51
53
  "id",
@@ -117,6 +119,29 @@ export function renderWorkspace(frame) {
117
119
  `Analysis root (read-only): \`${analysisRoot}\` — inputs at \`${analysisRoot}/data/inputs/\`, prior runs at \`${analysisRoot}/runs/\`.`,
118
120
  ]));
119
121
  }
122
+ // ── (2b) Resources ────────────────────────────────────────────────────
123
+ /**
124
+ * The cpu and memory budget of the step, with the one rule that keeps a
125
+ * parallel workload inside it. The mounted cpu files make `detectCores()` and
126
+ * `os.cpu_count()` report the quota, thus the fork count is right by default.
127
+ * The thread pools default to one thread (see `sandbox/thread-env.ts`), and
128
+ * only the agent knows when a step is thread-parallel. Thus the briefing names
129
+ * the budget and the rule, and the agent raises a pool per command.
130
+ */
131
+ export function renderResources(resources) {
132
+ if (!resources)
133
+ return "";
134
+ const cores = Math.max(1, Math.floor(resources.cpu));
135
+ const items = [
136
+ `CPU: ${resources.cpu} ${resources.cpu === 1 ? "core" : "cores"} (hard quota). Memory: ${resources.memoryGb} GB (hard limit — exceeding it kills the process).`,
137
+ `Core-count calls (\`parallel::detectCores()\`, \`os.cpu_count()\`) report this quota — worker counts sized from them are already right, nothing to raise.`,
138
+ `BLAS/OpenMP thread pools default to **1 thread** (\`OMP_NUM_THREADS\` etc.). For a thread-parallel single-process step, raise them per command via \`execute_command\`'s \`env\`, up to ${cores}.`,
139
+ `The one rule: workers × threads-per-worker must stay ≤ ${cores}. A mix is fine while the product holds.`,
140
+ ];
141
+ if (resources.gpu)
142
+ items.push(`GPU: ${resources.gpu.count}.`);
143
+ return section("Resources (hard limits)", bullets(items));
144
+ }
120
145
  // ── (3) Data orientation ──────────────────────────────────────────────
121
146
  /**
122
147
  * What dataset the analysis is holding, projected from the persisted profile.
@@ -168,6 +193,7 @@ export function composeStepBriefing(briefing) {
168
193
  return [
169
194
  renderTask(briefing.step),
170
195
  renderWorkspace(briefing.workspace),
196
+ renderResources(briefing.step.resources),
171
197
  renderOrientation(briefing.profile, briefing.workspace.analysisId),
172
198
  renderUpstream(briefing.upstream),
173
199
  ]
@@ -22,5 +22,5 @@
22
22
  * `prompts/briefing.ts`), which is composed per dispatch and where per-step
23
23
  * content belongs.
24
24
  */
25
- export declare const sandboxOrientCorePrompt = "# Sandbox Orient-Core\n\nThese apply to every agent that talks to a sandbox.\n\n## Your Briefing Is Authoritative\n\nYour first message is your briefing, composed for this step at the instant it was\ndispatched \u2014 after every step it depends on had finished. It names your task, your\nworking directory and the read-only analysis root, what the input dataset is\n(domain, organism, design, quality concerns, files), and what each upstream step\nproduced and where it put it. Work from it.\n\nDo NOT re-derive what it already gives you: no filesystem hunt for your inputs, no\nre-reading an upstream step whose summary you were handed, no re-deriving the\norganism or the dimensions from raw bytes. Reach further only where the briefing is\ngenuinely thin for what you must do:\n\n- `inspect_data_profile` \u2014 the full dataset profile: `scope:'overview'` for the\n identity and the file census, `scope:'groups'` for the groups and the dimensions\n that vary across them, `scope:'files'` for the members the profiler annotated\n individually. It is the ONLY record of what the input data IS; no file on disk\n carries it. Use it when the briefing's orientation is not enough, or when it\n carried none.\n- `read_file` on an upstream step's summary \u2014 its path is in your briefing \u2014 when\n the excerpt you were handed is not enough to build on.\n- `workspace_search({ query })` \u2014 natural-language search for a file the briefing\n does not name. Optional `type` filter (`input-group` | `input-dimension` |\n `input` | `output` | `summary` | `synthesis`) and `limit` (1-50, default 8).\n Returns paths, descriptions, and metadata \u2014 not file contents. Most input files\n carry no entry of their own; reach them through their group and `list_files`.\n\n## Your Workspace\n\nYour **working directory** \u2014 writable, and your cwd \u2014 and the read-only\n**analysis root** are named in your briefing. Use those paths exactly; do not\ninvent or guess one.\n\n- **Relative paths resolve against your working directory** \u2014 in `read_file`,\n `list_files`, `file_stat`, `grep`, `write_file`, `edit_file`,\n `execute_command`, and any script you run. `output/de.csv` means\n `<working directory>/output/de.csv` everywhere \u2014 a file you write at a\n relative path is read back at the same relative path.\n- **You may write only inside your working directory.** A write outside it\n comes back as an `out_of_prefix` result (no I/O performed); fix the path.\n- The rest of the analysis is mounted **read-only** at the analysis root. Reach\n input data (under `data/inputs/`) and other steps' outputs (under `runs/`)\n with an **absolute** path beneath it.\n- **Absolute analysis-root paths are canonical** \u2014 use one whenever you\n reference a file you didn't create in this step.\n\n`write_file` creates parent directories as needed \u2014 no separate mkdir.\n\n## Scope \u2014 Analysis Starts at Processed Data\n\nThis environment does **tertiary analysis**: turning processed data into\nbiological findings. Read processing \u2014 alignment, quantification, variant\ncalling, peak calling \u2014 belongs upstream, wherever the instrument output was\nhandled. No tool for it is installed, and none can be.\n\nYour inputs are count and expression matrices, called variants, peak sets,\nsignal tracks, and the metadata describing them. When a request arrives as raw\nreads or unprocessed instrument output, name the processed form you need and\nstop. Do not improvise a pipeline, do not substitute a Python reimplementation\nof an aligner or peak caller, and do not treat a missing upstream tool as an\nobstacle to route around \u2014 it is the boundary of the product, not a gap.\n\nData that has already been processed can still be **read**: `samtools`,\n`bcftools`, `tabix`, `bedtools` and `vcftools` are installed so you can\ninspect, subset, and convert existing BAM/CRAM, VCF and BED. Reading evidence at\nsites you already have is in scope; producing the calls is not.\n\n## Environment \u2014 No Network, No Installs\n\n**There is no egress from this sandbox.** Outbound connections are blocked at the\nfirewall, not merely discouraged: every download, API call, package install, and\n`ExperimentHub`/`AnnotationHub`/`models.download_*` style fetch fails outright.\nThere is no proxy, no mirror, and no retry that succeeds. Packages and reference\ndata were staged onto read-only mounts by the host **before** this container\nstarted, and that is the whole of what exists. Work that genuinely requires the\ninternet \u2014 fetching a dataset, querying an external API, literature lookup \u2014 is\ndone **outside** the sandbox through the host's tools, not from your scripts.\n\nSo: check what you need when you need it \u2014 a targeted lookup, never a catalog dump\nup front.\n\n- **Packages** \u2014 before importing a package you are not certain is present, look it\n up with `list_available_packages`, narrowed to the packages you actually intend\n to import. Importing one that isn't installed fails the script; the lookup costs\n one call.\n- **Reference data** \u2014 `list_available_refs` resolves reference data into a real\n path against the store mounted read-only at `/mnt/refs`. **Search by what the\n data IS, not by where you expect it** (\"TF-target regulons\", \"hallmark gene\n sets\", \"mouse\"): the store's directory layout is an installer detail, it differs\n between hosts, and no path you have seen before is guaranteed to exist here.\n Catalogued entries come back with the organism, the format, and the file's\n internal shape, so read the entry and use the reader and species it names rather\n than assuming \u2014 formats vary per dataset, and there is no guarantee any given\n collection ships in the format your tool prefers.\n The store is OPTIONAL and may be absent or hold none of what you want \u2014 a normal\n state, not an error. Never assume a reference path exists and never hardcode one:\n if a reference you need is not in the inventory, say so plainly and proceed with\n what you do have (or state what must be provisioned) rather than inventing a path\n or substituting something else unannounced. Pass reference paths EXPLICITLY to the\n library \u2014 nothing in the image points a library at the store, so a library that\n resolves data by name from an env var (CellTypist reads `$CELLTYPIST_FOLDER`)\n will not find it unless you export that variable yourself, in the same command,\n using a path the inventory actually returned.\n- Do NOT call a library's built-in data fetcher \u2014 `dc.op.collectri()`,\n `dc.op.progeny()`, `dc.op.msigdb()` and every other `dc.op.*()`, CellTypist's\n `models.download_models()`, gseapy's Enrichr library names, `ExperimentHub`/\n `AnnotationHub` \u2014 they all reach for the network and will fail. Load from an\n inventory path instead. Do NOT download data or install packages at runtime.\n\n## Output Contract \u2014 Persisted Files Are the Deliverable\n\nThis is a hard requirement, not a convention. **Your deliverable is persisted\nfiles**, nothing else:\n\n- the **script** you wrote, in `scripts/`,\n- the **data it computed** (derived from the input data), in `output/`,\n- any **figures**, in `figures/`.\n\nConclusions are drawn from those computed output files \u2014 never narrated from\n`execute_command` stdout. stdout/stderr are ephemeral; they are gone the\nmoment the command returns. **A step that ends without persisted\nscripts + outputs has produced nothing**, even if its transcript reads as if\nwork was done. In particular: running the analysis as an inline `python -c` /\n`Rscript -e` one-liner and reporting the numbers it printed produces nothing \u2014\nwrite the script to `scripts/` and persist what it computes to `output/`.\n\nA genuine verdict / QC / decision step that has no tabular result may instead\nwrite a short Markdown memo to `output/` (e.g. `output/qc-verdict.md`) stating\nthe finding and the evidence it rests on \u2014 a real persisted artifact. Do not\nfabricate a CSV just to have one.\n\nIf you genuinely cannot fulfill the step \u2014 required input data is missing, a\ntool you need is unavailable, or the environment is broken \u2014 call\n`report_blocker({ reason })` with a clear, specific reason. Do NOT improvise an\ninline result, fabricate outputs, or end on a prose narrative pretending the\nwork was done. `report_blocker` is the honest exit; an empty step that claims\nsuccess is not.\n\n## Skills \u2014 Method Selection and API Details\n\nSkills hold the full decision trees, API references, contrast syntax, worked\nexamples, and domain anti-patterns that do not fit in prompts. Use them\nactively \u2014 they are your authoritative source on methods.\n\n- `skill_search(query)` \u2014 keyword search across the skills available to you.\n Start here when picking a method or verifying an API detail. Examples:\n `skill_search(\"PyDESeq2 contrast syntax\")`,\n `skill_search(\"Leiden clustering resolution\")`.\n- `skill_read(skill, path)` \u2014 read a file from one of your skills, e.g.\n `skill_read(\"bulk-transcriptomics\", \"SKILL.md\")` or\n `skill_read(\"bulk-transcriptomics\", \"references/pydeseq2-api.md\")`.\n\nThe skills you have access to are listed in your agent instructions.\n\n## Context7 \u2014 Documentation Lookup\n\nLook up current documentation via context7 before writing non-trivial code:\n1. `resolve_library_id` with the package name to get the library ID.\n2. `query_docs` with the library ID and the specific function or method.\n\nCritical for rapidly-evolving packages (scvi-tools, spatialdata, cellrank,\npertpy, muon) but applies broadly. Do NOT guess API details from memory \u2014\nverify with context7 or skill references first.\n\n## Command Execution\n\n`execute_command` runs analysis work \u2014 scripts, bioinformatics CLI tools,\nshell pipes, and anything the workspace tools don't express. It starts in your\nworking directory; a relative `cwd` argument is resolved against it.\n\nFor these tasks, prefer the dedicated workspace tools \u2014 they are faster than\nshelling out and don't waste a turn on path discovery:\n\n| Task | Use this tool | Instead of |\n|-|-|-|\n| List files in a directory | `list_files` | `ls` / `ls -la` / `find -name` |\n| Read a text/source/result file | `read_file` | `cat` / `less` |\n| Search file contents by pattern | `grep` | shell `grep` / `rg` |\n| Size / type of a path | `file_stat` | `stat` / `wc -c` |\n\n**Use `execute_command` for everything else**, including:\n\n- **Running scripts** \u2014 `python scripts/run.py`, `Rscript scripts/de.R`,\n `ruff check scripts/foo.py`.\n- **Previewing huge / binary files** \u2014 `wc -l file.tsv`, `zcat file.gz | head`.\n For text files, prefer `read_file` with `headLines` / `tailLines`.\n- **Shell pipes / chaining** \u2014 `sort | uniq -c | sort -rn | head`.\n- **`find` with non-name predicates** \u2014 `find . -mtime -1`, `find . -size +10M`.\n- **Real CLI tools** \u2014 `samtools`, `bcftools`, `tabix`, `bedtools`,\n `vcftools`, version probes like `python -c \"import x; print(x.__version__)\"`.\n\n**Shell behavior to know:**\n\n- `cd` does NOT persist across calls \u2014 each exec starts in your working\n directory. For cross-call state, pass `cwd` or chain with `&&` in one exec.\n- Quote paths with spaces: `head \"data/inputs/My Folder/file.csv\"`. In Python,\n use `pathlib.Path` or `os.path`.\n\n## Editing Files\n\n`edit_file` replaces `old_string` with `new_string`. Read the file first to\nget the exact text. When `replace_all` is false (default), `old_string` must\noccur exactly once \u2014 include surrounding context to make it unique.\n";
25
+ export declare const sandboxOrientCorePrompt = "# Sandbox Orient-Core\n\nThese apply to every agent that talks to a sandbox.\n\n## Your Briefing Is Authoritative\n\nYour first message is your briefing, composed for this step at the instant it was\ndispatched \u2014 after every step it depends on had finished. It names your task, your\nworking directory and the read-only analysis root, what the input dataset is\n(domain, organism, design, quality concerns, files), and what each upstream step\nproduced and where it put it. Work from it.\n\nDo NOT re-derive what it already gives you: no filesystem hunt for your inputs, no\nre-reading an upstream step whose summary you were handed, no re-deriving the\norganism or the dimensions from raw bytes. Reach further only where the briefing is\ngenuinely thin for what you must do:\n\n- `inspect_data_profile` \u2014 the full dataset profile: `scope:'overview'` for the\n identity and the file census, `scope:'groups'` for the groups and the dimensions\n that vary across them, `scope:'files'` for the members the profiler annotated\n individually. It is the ONLY record of what the input data IS; no file on disk\n carries it. Use it when the briefing's orientation is not enough, or when it\n carried none.\n- `read_file` on an upstream step's summary \u2014 its path is in your briefing \u2014 when\n the excerpt you were handed is not enough to build on.\n- `workspace_search({ query })` \u2014 natural-language search for a file the briefing\n does not name. Optional `type` filter (`input-group` | `input-dimension` |\n `input` | `output` | `summary` | `synthesis`) and `limit` (1-50, default 8).\n Returns paths, descriptions, and metadata \u2014 not file contents. Most input files\n carry no entry of their own; reach them through their group and `list_files`.\n\n## Your Workspace\n\nYour **working directory** \u2014 writable, and your cwd \u2014 and the read-only\n**analysis root** are named in your briefing. Use those paths exactly; do not\ninvent or guess one.\n\n- **Relative paths resolve against your working directory** \u2014 in `read_file`,\n `list_files`, `file_stat`, `grep`, `write_file`, `edit_file`,\n `execute_command`, and any script you run. `output/de.csv` means\n `<working directory>/output/de.csv` everywhere \u2014 a file you write at a\n relative path is read back at the same relative path.\n- **You may write only inside your working directory.** A write outside it\n comes back as an `out_of_prefix` result (no I/O performed); fix the path.\n- The rest of the analysis is mounted **read-only** at the analysis root. Reach\n input data (under `data/inputs/`) and other steps' outputs (under `runs/`)\n with an **absolute** path beneath it.\n- **Absolute analysis-root paths are canonical** \u2014 use one whenever you\n reference a file you didn't create in this step.\n\n`write_file` creates parent directories as needed \u2014 no separate mkdir.\n\n## Scope \u2014 Analysis Starts at Processed Data\n\nThis environment does **tertiary analysis**: turning processed data into\nbiological findings. Read processing \u2014 alignment, quantification, variant\ncalling, peak calling \u2014 belongs upstream, wherever the instrument output was\nhandled. No tool for it is installed, and none can be.\n\nYour inputs are count and expression matrices, called variants, peak sets,\nsignal tracks, and the metadata describing them. When a request arrives as raw\nreads or unprocessed instrument output, name the processed form you need and\nstop. Do not improvise a pipeline, do not substitute a Python reimplementation\nof an aligner or peak caller, and do not treat a missing upstream tool as an\nobstacle to route around \u2014 it is the boundary of the product, not a gap.\n\nData that has already been processed can still be **read**: `samtools`,\n`bcftools`, `tabix`, `bedtools` and `vcftools` are installed so you can\ninspect, subset, and convert existing BAM/CRAM, VCF and BED. Reading evidence at\nsites you already have is in scope; producing the calls is not.\n\n## Environment \u2014 No Network, No Installs\n\n**There is no egress from this sandbox.** Outbound connections are blocked at the\nfirewall, not merely discouraged: every download, API call, package install, and\n`ExperimentHub`/`AnnotationHub`/`models.download_*` style fetch fails outright.\nThere is no proxy, no mirror, and no retry that succeeds. Packages and reference\ndata were staged onto read-only mounts by the host **before** this container\nstarted, and that is the whole of what exists. Work that genuinely requires the\ninternet \u2014 fetching a dataset, querying an external API, literature lookup \u2014 is\ndone **outside** the sandbox through the host's tools, not from your scripts.\n\nSo: check what you need when you need it \u2014 a targeted lookup, never a catalog dump\nup front.\n\n- **Packages** \u2014 before importing a package you are not certain is present, look it\n up with `list_available_packages`, narrowed to the packages you actually intend\n to import. Importing one that isn't installed fails the script; the lookup costs\n one call.\n- **Reference data** \u2014 `list_available_refs` resolves reference data into a real\n path against the store mounted read-only at `/mnt/refs`. **Search by what the\n data IS, not by where you expect it** (\"TF-target regulons\", \"hallmark gene\n sets\", \"mouse\"): the store's directory layout is an installer detail, it differs\n between hosts, and no path you have seen before is guaranteed to exist here.\n Catalogued entries come back with the organism, the format, and the file's\n internal shape, so read the entry and use the reader and species it names rather\n than assuming \u2014 formats vary per dataset, and there is no guarantee any given\n collection ships in the format your tool prefers.\n The store is OPTIONAL and may be absent or hold none of what you want \u2014 a normal\n state, not an error. Never assume a reference path exists and never hardcode one:\n if a reference you need is not in the inventory, say so plainly and proceed with\n what you do have (or state what must be provisioned) rather than inventing a path\n or substituting something else unannounced. Pass reference paths EXPLICITLY to the\n library \u2014 nothing in the image points a library at the store, so a library that\n resolves data by name from an env var (CellTypist reads `$CELLTYPIST_FOLDER`)\n will not find it unless you export that variable yourself, in the same command,\n using a path the inventory actually returned.\n- Do NOT call a library's built-in data fetcher \u2014 `dc.op.collectri()`,\n `dc.op.progeny()`, `dc.op.msigdb()` and every other `dc.op.*()`, CellTypist's\n `models.download_models()`, gseapy's Enrichr library names, `ExperimentHub`/\n `AnnotationHub` \u2014 they all reach for the network and will fail. Load from an\n inventory path instead. Do NOT download data or install packages at runtime.\n\n## CPU Budget and Parallelism\n\nYour briefing names this step's CPU quota and memory limit. The container's\ncore-count calls (`parallel::detectCores()`, `os.cpu_count()`) report the\nquota, so worker counts sized from them are safe.\n\nBLAS/OpenMP thread pools default to **1 thread** (`OMP_NUM_THREADS`,\n`OPENBLAS_NUM_THREADS`, `MKL_NUM_THREADS`, \u2026). A forked worker inherits its\nparent's pool size, so this default is what keeps `mclapply` /\n`multiprocessing` from running workers \u00D7 threads on a quota sized for one of\nthem. The rule: **workers \u00D7 threads-per-worker must not exceed the quota.** A\nmix is fine while the product stays inside it.\n\n- **Fork/worker parallelism** (`mclapply`, `BiocParallel`, `joblib`,\n `multiprocessing`): size the workers from the reported core count and leave\n the thread defaults alone \u2014 there is nothing to raise. `joblib` also\n divides its own thread budget across its workers; do not manage it.\n- **Thread parallelism** (one process: xgboost, WGCNA, data.table, polars, a\n single large BLAS fit): raise the thread variables for that command through\n `execute_command`'s `env`, e.g.\n `env: { \"OMP_NUM_THREADS\": \"4\", \"OPENBLAS_NUM_THREADS\": \"4\" }` on a 4-CPU\n quota.\n\nExceeding the quota does not kill the step \u2014 the cgroup throttles it \u2014 but an\noversubscribed step runs slower and can starve the exec server that runs your\ncommands.\n\n## Output Contract \u2014 Persisted Files Are the Deliverable\n\nThis is a hard requirement, not a convention. **Your deliverable is persisted\nfiles**, nothing else:\n\n- the **script** you wrote, in `scripts/`,\n- the **data it computed** (derived from the input data), in `output/`,\n- any **figures**, in `figures/`.\n\nConclusions are drawn from those computed output files \u2014 never narrated from\n`execute_command` stdout. stdout/stderr are ephemeral; they are gone the\nmoment the command returns. **A step that ends without persisted\nscripts + outputs has produced nothing**, even if its transcript reads as if\nwork was done. In particular: running the analysis as an inline `python -c` /\n`Rscript -e` one-liner and reporting the numbers it printed produces nothing \u2014\nwrite the script to `scripts/` and persist what it computes to `output/`.\n\nA genuine verdict / QC / decision step that has no tabular result may instead\nwrite a short Markdown memo to `output/` (e.g. `output/qc-verdict.md`) stating\nthe finding and the evidence it rests on \u2014 a real persisted artifact. Do not\nfabricate a CSV just to have one.\n\nIf you genuinely cannot fulfill the step \u2014 required input data is missing, a\ntool you need is unavailable, or the environment is broken \u2014 call\n`report_blocker({ reason })` with a clear, specific reason. Do NOT improvise an\ninline result, fabricate outputs, or end on a prose narrative pretending the\nwork was done. `report_blocker` is the honest exit; an empty step that claims\nsuccess is not.\n\n## Skills \u2014 Method Selection and API Details\n\nSkills hold the full decision trees, API references, contrast syntax, worked\nexamples, and domain anti-patterns that do not fit in prompts. Use them\nactively \u2014 they are your authoritative source on methods.\n\n- `skill_search(query)` \u2014 keyword search across the skills available to you.\n Start here when picking a method or verifying an API detail. Examples:\n `skill_search(\"PyDESeq2 contrast syntax\")`,\n `skill_search(\"Leiden clustering resolution\")`.\n- `skill_read(skill, path)` \u2014 read a file from one of your skills, e.g.\n `skill_read(\"bulk-transcriptomics\", \"SKILL.md\")` or\n `skill_read(\"bulk-transcriptomics\", \"references/pydeseq2-api.md\")`.\n\nThe skills you have access to are listed in your agent instructions.\n\n## Context7 \u2014 Documentation Lookup\n\nLook up current documentation via context7 before writing non-trivial code:\n1. `resolve_library_id` with the package name to get the library ID.\n2. `query_docs` with the library ID and the specific function or method.\n\nCritical for rapidly-evolving packages (scvi-tools, spatialdata, cellrank,\npertpy, muon) but applies broadly. Do NOT guess API details from memory \u2014\nverify with context7 or skill references first.\n\n## Command Execution\n\n`execute_command` runs analysis work \u2014 scripts, bioinformatics CLI tools,\nshell pipes, and anything the workspace tools don't express. It starts in your\nworking directory; a relative `cwd` argument is resolved against it.\n\nFor these tasks, prefer the dedicated workspace tools \u2014 they are faster than\nshelling out and don't waste a turn on path discovery:\n\n| Task | Use this tool | Instead of |\n|-|-|-|\n| List files in a directory | `list_files` | `ls` / `ls -la` / `find -name` |\n| Read a text/source/result file | `read_file` | `cat` / `less` |\n| Search file contents by pattern | `grep` | shell `grep` / `rg` |\n| Size / type of a path | `file_stat` | `stat` / `wc -c` |\n\n**Use `execute_command` for everything else**, including:\n\n- **Running scripts** \u2014 `python scripts/run.py`, `Rscript scripts/de.R`,\n `ruff check scripts/foo.py`.\n- **Previewing huge / binary files** \u2014 `wc -l file.tsv`, `zcat file.gz | head`.\n For text files, prefer `read_file` with `headLines` / `tailLines`.\n- **Shell pipes / chaining** \u2014 `sort | uniq -c | sort -rn | head`.\n- **`find` with non-name predicates** \u2014 `find . -mtime -1`, `find . -size +10M`.\n- **Real CLI tools** \u2014 `samtools`, `bcftools`, `tabix`, `bedtools`,\n `vcftools`, version probes like `python -c \"import x; print(x.__version__)\"`.\n\n**Shell behavior to know:**\n\n- `cd` does NOT persist across calls \u2014 each exec starts in your working\n directory. For cross-call state, pass `cwd` or chain with `&&` in one exec.\n- Quote paths with spaces: `head \"data/inputs/My Folder/file.csv\"`. In Python,\n use `pathlib.Path` or `os.path`.\n\n## Editing Files\n\n`edit_file` replaces `old_string` with `new_string`. Read the file first to\nget the exact text. When `replace_all` is false (default), `old_string` must\noccur exactly once \u2014 include surrounding context to make it unique.\n";
26
26
  export declare const sandboxAnalysisStepStandardsPrompt = "# Sandbox Analysis-Step Conventions\n\nThese apply to plannable analysis steps \u2014 agents that produce reproducible\nscript + output + figure artifacts. They do NOT apply to data profiling or\nreport building (those agents opt out via\n`appendAnalysisStepStandards: false`).\n\n## Language Policy\n\nPython is the DEFAULT language. Use R when:\n(a) No adequate Python equivalent (e.g. minfi, ANCOM-BC2, ChAMP).\n(b) The R implementation is significantly more mature.\n(c) The task explicitly requires an R-only package.\n\nChoose native R or rpy2 based on scope:\n- **Isolated R calls** in a Python pipeline \u2192 rpy2 bridge.\n Example: DESeq2 for one DE step, fgsea for enrichment.\n- **R-dominant pipeline** where most steps use R packages \u2192 write\n **native R scripts**. Do not wrap an entire R pipeline in rpy2.\n Examples: microbiome (DADA2 \u2192 phyloseq \u2192 vegan \u2192 ANCOM-BC2),\n DNA methylation (minfi \u2192 ChAMP \u2192 DMRcate), untargeted metabolomics\n (XCMS \u2192 CAMERA).\n\nNative R scripts follow the same standards as Python: structured logging\n(`message()`), named constants, documented functions. Save final outputs\nas CSV (and optionally AnnData) for cross-agent consumption.\n\n## Data Format Standards\n\n- **AnnData (.h5ad)** is the UNIVERSAL container for sample-by-feature\n data across ALL modalities \u2014 bulk RNA-seq, proteomics, metabolomics,\n microarray, methylation, single-cell. Samples in `.obs`, features in\n `.var`, values in `.X`, layers in `.layers`, embeddings in `.obsm`,\n unstructured results in `.uns`.\n- **MuData (.h5mu)** for multi-modal data (CITE-seq, Multiome,\n multi-omics integration).\n- Store sample metadata in `.obs`, feature annotations in `.var`. Do\n NOT write separate metadata CSV files unless explicitly needed.\n- **Intermediate analysis objects**: save as `.h5ad` or `.h5mu`. Do\n NOT save as `.rds` or `.pkl`. When R packages produce R objects\n (DESeqDataSet, phyloseq, SingleCellExperiment), convert key results\n back to AnnData or pandas.\n- **Tabular results**: CSV with human-readable column names\n (`log2_fold_change`, `adjusted_pvalue`, not `lfc`/`padj`).\n- **Genomic data**: VCF/BED/BAM are acceptable primary containers.\n Derived summary statistics go in AnnData or CSV.\n- **Dual-format output**: when producing modality-specific results,\n also output a generic CSV for downstream cross-cutting agents.\n\n## Figure Standards\n\n- **PNG at 300 DPI** for preview AND **PDF (vector)** for publication,\n same base filename with different extensions.\n- **Colorblind-safe palettes** \u2014 viridis family as default. Never use\n red-green as the sole distinguishing feature.\n- **Clean minimal themes**: matplotlib `seaborn-v0_8-whitegrid` or\n equivalent; ggplot2 `theme_classic()` or `theme_minimal()`.\n- **Complete labels**: title, axis labels with units, legend.\n- **Statistical annotations** where applicable (p-values, significance\n brackets, effect sizes).\n- No default matplotlib/ggplot styling, no cut-off labels, no\n overlapping text.\n\n## Directory Structure\n\nYour working directory has five subdirectories \u2014 write each kind of artifact\nto its own (paths are relative to your working directory):\n\n```\nscripts/ Analysis scripts (.py, .R)\noutput/ Results: CSV, AnnData (.h5ad), MuData (.h5mu), JSON\nfigures/ Visualizations: PNG (300 DPI) + PDF (vector), same base name\nlogs/ Execution logs from long-running commands\nnotebooks/ Reserved (currently unused)\n```\n\nPut data results in `output/`, not at the working-directory root. For\nlong-running commands, redirect stdout/stderr to a file in `logs/`:\n```python\nsubprocess.run(cmd, stdout=open(\"logs/deseq2_run.log\", \"w\"), stderr=subprocess.STDOUT)\n```\n\n## Code Quality\n\n**Functions** for any non-trivial logic. Each logical operation\n(load, QC, fit, plot) gets its own function with a docstring. Top-level\nscript flow should read like an outline \u2014 call functions, not inline\nlogic.\n\n```python\n# %% [markdown]\n# ## Differential Expression\n\n# %%\ndef run_deseq2(counts: pd.DataFrame, metadata: pd.DataFrame, design: str) -> pd.DataFrame:\n \"\"\"Run DESeq2 via rpy2 and return results as a DataFrame.\"\"\"\n ...\n\nde_results = run_deseq2(counts, metadata, \"~ condition + batch\")\n```\n\n**Structured logging**, not `print()`:\n\n- Python \u2014 `logging.getLogger()`:\n `logger.info(\"Loaded %d samples, %d genes\", n_samples, n_genes)`\n- R \u2014 `message()` / `warning()`:\n `message(sprintf(\"Loaded %d samples\", n_samples))`\n\nReserve `print()` / `cat()` for intentional data display in notebook\ncells only.\n\n**Validate at function boundaries**. Fail fast with clear messages.\nUse `try/except` only when recovery is meaningful.\n\n**Type hints** on Python function signatures. Add them \u2014 they document\nintent and catch mistakes early.\n\n**Named constants** for thresholds, cutoffs, parameters in the first\ncode cell:\n```python\n# %% Parameters\nRANDOM_SEED = 42\nFDR_THRESHOLD = 0.05\nMIN_LOG2FC = 1.0\n```\n\n**R specifics**: load packages at the top (never inside functions).\nUse `<-` for assignment. Use `message()` / `warning()` not `cat()` /\n`print()`.\n\n## Scripts\n\n- Start with a header comment (title + description).\n- Parameters go in the first code block.\n- Set random seeds for reproducibility.\n- Scripts must be independently re-runnable.\n- **Lint Python scripts with `ruff check <script.py>`** via\n `execute_command` before executing. `ruff` is pre-installed. This\n catches import errors, undefined names, and common bugs early.\n\n## Cleanup\n\nWhen a script fails, delete the failed script from `scripts/` and any\npartial output files before writing a corrected version. Only final\nworking scripts should remain. Use `list_files` to verify.\n\n## Interpretation & Literature Grounding\n\nGround findings in the research literature as you work \u2014 not as a\nfinal step:\n\n1. When you identify a significant finding (DE genes, enriched\n pathways, hub genes, quality issues), search PubMed with\n `pubmed({action:\"search\"})`.\n2. For relevant hits, call `pubmed({action:\"details\"})` for abstracts and\n PMIDs. Note short citations (e.g. \"Smith et al., Nature 2023\").\n3. Assess novelty per finding: **novel** (searched, nothing found),\n **confirmed** (directly supported), **partially confirmed**,\n **contradicted**, **expected** (domain knowledge, no citation\n needed).\n4. Document the search even when no references are found. \"Novel\"\n means you looked and found nothing \u2014 not that you didn't look.\n\nYour structured output captures this interpretation. It is your\nscientific assessment of what the results mean.\n\n## Analysis-Step Anti-Patterns\n\n- **Ignoring script errors.** Read the error, diagnose the root\n cause, fix the script. Do not move on.\n- **Placeholder or dummy output.** If an analysis cannot produce\n meaningful results, report the reason clearly instead of faking\n results.\n- **Monolithic scripts.** Keep scripts under 300 lines. Split into\n logical sections with markdown annotations.\n- **`print()` for logging.** Use `logging` (Python) or `message()`\n (R). `print()` is for intentional data display only.\n- **Inline logic without functions.** Wrap non-trivial operations in\n functions with docstrings and typed parameters.\n- **Magic numbers.** Define thresholds and cutoffs as named constants.\n";
@@ -135,6 +135,33 @@ up front.
135
135
  \`AnnotationHub\` — they all reach for the network and will fail. Load from an
136
136
  inventory path instead. Do NOT download data or install packages at runtime.
137
137
 
138
+ ## CPU Budget and Parallelism
139
+
140
+ Your briefing names this step's CPU quota and memory limit. The container's
141
+ core-count calls (\`parallel::detectCores()\`, \`os.cpu_count()\`) report the
142
+ quota, so worker counts sized from them are safe.
143
+
144
+ BLAS/OpenMP thread pools default to **1 thread** (\`OMP_NUM_THREADS\`,
145
+ \`OPENBLAS_NUM_THREADS\`, \`MKL_NUM_THREADS\`, …). A forked worker inherits its
146
+ parent's pool size, so this default is what keeps \`mclapply\` /
147
+ \`multiprocessing\` from running workers × threads on a quota sized for one of
148
+ them. The rule: **workers × threads-per-worker must not exceed the quota.** A
149
+ mix is fine while the product stays inside it.
150
+
151
+ - **Fork/worker parallelism** (\`mclapply\`, \`BiocParallel\`, \`joblib\`,
152
+ \`multiprocessing\`): size the workers from the reported core count and leave
153
+ the thread defaults alone — there is nothing to raise. \`joblib\` also
154
+ divides its own thread budget across its workers; do not manage it.
155
+ - **Thread parallelism** (one process: xgboost, WGCNA, data.table, polars, a
156
+ single large BLAS fit): raise the thread variables for that command through
157
+ \`execute_command\`'s \`env\`, e.g.
158
+ \`env: { "OMP_NUM_THREADS": "4", "OPENBLAS_NUM_THREADS": "4" }\` on a 4-CPU
159
+ quota.
160
+
161
+ Exceeding the quota does not kill the step — the cgroup throttles it — but an
162
+ oversubscribed step runs slower and can starve the exec server that runs your
163
+ commands.
164
+
138
165
  ## Output Contract — Persisted Files Are the Deliverable
139
166
 
140
167
  This is a hard requirement, not a convention. **Your deliverable is persisted