@inflexa-ai/harness 0.35.5 → 0.35.6

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 (37) hide show
  1. package/dist/agents/conversation-agent.js +10 -2
  2. package/dist/lib/metrics.d.ts +55 -4
  3. package/dist/lib/metrics.js +101 -4
  4. package/dist/prompts/briefing.js +6 -3
  5. package/dist/prompts/literature-reviewer.d.ts +1 -1
  6. package/dist/prompts/literature-reviewer.js +16 -8
  7. package/dist/prompts/planner.d.ts +4 -0
  8. package/dist/prompts/planner.js +22 -16
  9. package/dist/sandbox/create-sandbox.js +23 -1
  10. package/dist/sandbox/exec-outcome.d.ts +45 -0
  11. package/dist/sandbox/exec-outcome.js +65 -0
  12. package/dist/sandbox/k8s-client.d.ts +10 -0
  13. package/dist/sandbox/k8s-client.js +40 -5
  14. package/dist/sandbox/liveness.d.ts +3 -1
  15. package/dist/sandbox/types.d.ts +24 -0
  16. package/dist/sandbox/types.js +24 -0
  17. package/dist/tools/ad-hoc-router.d.ts +23 -0
  18. package/dist/tools/ad-hoc-router.js +93 -5
  19. package/dist/tools/bio/alphafold-prediction.d.ts +19 -0
  20. package/dist/tools/bio/alphafold-prediction.js +40 -0
  21. package/dist/tools/bio/index.d.ts +2 -0
  22. package/dist/tools/bio/index.js +3 -0
  23. package/dist/tools/bio/search-protein.d.ts +20 -0
  24. package/dist/tools/bio/search-protein.js +61 -0
  25. package/dist/tools/execute-analysis.d.ts +2 -1
  26. package/dist/tools/execute-analysis.js +66 -5
  27. package/dist/tools/lib/alphafold-client.d.ts +55 -0
  28. package/dist/tools/lib/alphafold-client.js +72 -0
  29. package/dist/tools/lib/api-utils.d.ts +25 -0
  30. package/dist/tools/lib/api-utils.js +45 -3
  31. package/dist/tools/lib/uniprot-client.d.ts +131 -0
  32. package/dist/tools/lib/uniprot-client.js +190 -0
  33. package/dist/tools/research/literature-reviewer.js +2 -0
  34. package/dist/tools/sandbox/list-available-packages.d.ts +25 -0
  35. package/dist/tools/sandbox/list-available-packages.js +51 -29
  36. package/dist/workflows/sandbox-step.js +10 -1
  37. package/package.json +1 -1
@@ -27,7 +27,7 @@ import { createWorkingMemory } from "../memory/working-memory.js";
27
27
  import { conversationPrompt } from "../prompts/conversation.js";
28
28
  import { composeSystemPrompt } from "./system-prompt.js";
29
29
  // Bio-lookup leaf tools (pure — no dependencies).
30
- import { searchGeneTool, lookupAnnotationTool, searchInteractionsTool, chemblTool, pubchemTool, openTargetsTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, genePreclinicalProfileTool, targetSafetyTool, } from "../tools/bio/index.js";
30
+ import { searchGeneTool, searchProteinTool, lookupAnnotationTool, searchInteractionsTool, alphafoldPredictionTool, chemblTool, pubchemTool, openTargetsTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, genePreclinicalProfileTool, targetSafetyTool, } from "../tools/bio/index.js";
31
31
  import { createNcbiTools, createChemDbTools } from "../tools/bio/keys.js";
32
32
  // Dependency-bearing tool factories.
33
33
  import { createGeneratePlanTool, createInspectRunTool, createInspectDataProfileTool, createGenerateAnalogyReportTool, createSearchSemanticScholarTool, searchArxivTool, } from "../tools/research/index.js";
@@ -58,12 +58,17 @@ export function createConversationAgent(deps) {
58
58
  ...(deps.logger ? { logger: deps.logger } : {}),
59
59
  });
60
60
  const tools = [
61
- // Identifier resolution — the ENSG most other bio tools key off.
61
+ // Identifier resolution — the ENSG most other bio tools key off, and the
62
+ // UniProt accession that the structure tools take.
62
63
  searchGeneTool,
64
+ searchProteinTool,
63
65
  // Functional annotation (GO / KEGG / Reactome behind one vocabulary) and
64
66
  // STRING networks + gene-set enrichment.
65
67
  lookupAnnotationTool,
66
68
  searchInteractionsTool,
69
+ // Structural biology — the whole-chain confidence of a predicted 3-D
70
+ // model, and the URLs of its coordinate and per-residue files.
71
+ alphafoldPredictionTool,
67
72
  // Literature (search / details / fulltext behind one action).
68
73
  ncbi.pubmed,
69
74
  // Verification of one caller-supplied citation (distinct from discovery).
@@ -131,6 +136,9 @@ export function createConversationAgent(deps) {
131
136
  resourcePolicy,
132
137
  utilityProvider,
133
138
  utilityModel,
139
+ ...(farmLockFile ? { farmLockFile } : {}),
140
+ ...(imagePackagesFile ? { imagePackagesFile } : {}),
141
+ ...(readPoolInventory ? { readPoolInventory } : {}),
134
142
  ...(deps.extendAnalysisFarm ? { extendAnalysisFarm: deps.extendAnalysisFarm } : {}),
135
143
  ...(deps.logger ? { logger: deps.logger } : {}),
136
144
  }),
@@ -6,18 +6,25 @@
6
6
  * - cortex.run.duration{status, workflow} — histogram, start to terminal status
7
7
  * - cortex.step.completed{status, agent_id} — counter, one per executed step that settled
8
8
  * - cortex.step.duration{status, agent_id} — histogram, step start to its terminal ledger write
9
+ * - cortex.sandbox.execs{outcome} — counter, one per sandbox exec that returned a terminal result
10
+ * - cortex.sandbox.exec.duration{outcome} — histogram, the runtime the sandbox reported for that exec
11
+ * - cortex.sandbox.exec.peak_memory_bytes{outcome} — histogram, the peak resident set of that exec
12
+ * - cortex.sandbox.exec.cpu_seconds{outcome} — histogram, the CPU time of that exec
9
13
  * - cortex.artifact.reconcile.dropped{agent_id} — counter for missing manifest entries
10
14
  * - cortex.artifact.reconcile.input_dropped{agent_id, reason} — counter for lineage input drops
11
15
  *
12
16
  * Every label is a bounded vocabulary: `status` is the terminal enum, `workflow`
13
- * names the workflow family, `agent_id` has about 15 values. No instrument
14
- * carries a run, step, exec, user, or organization id — a label with unbounded
15
- * values multiplies the series count.
17
+ * names the workflow family, `agent_id` has about 15 values, `outcome` has
18
+ * five. No instrument carries a run, step, exec, user, or organization id — a
19
+ * label with unbounded values multiplies the series count, and the collector
20
+ * drops a datapoint that carries one.
16
21
  *
17
22
  * A run or step outcome is recorded INSIDE the DBOS step that persists the same
18
23
  * outcome to the ledger (or behind the ledger's own compare-and-set where the
19
24
  * body has no such step). DBOS caches a completed step and does not re-run its
20
- * body on recovery, so a replayed workflow records nothing twice.
25
+ * body on recovery, so a replayed workflow records nothing twice. An exec has
26
+ * no such step, so `recordSandboxExec` carries its own idempotence — see the
27
+ * note on `countedExecs`.
21
28
  *
22
29
  * Instruments are lazy so the record site binds to whichever `MeterProvider`
23
30
  * is globally registered at first use. The metrics API has no late-binding
@@ -31,12 +38,39 @@ export type RunOutcome = "completed" | "partial" | "failed" | "canceled";
31
38
  export type RunWorkflow = "analysis" | "target_assessment" | "data_profile";
32
39
  /** Terminal status of an executed step. A blocker is a failure to deliver, so `blocked` maps here to `failed`. */
33
40
  export type StepOutcome = "completed" | "failed" | "canceled";
41
+ /**
42
+ * Terminal outcome of one sandbox exec. `nonzero` is the command's own
43
+ * verdict; `timeout` is the deadline; the two synthetic values name a machine
44
+ * that died under the command, which is an outcome of the platform and not of
45
+ * the analysis. Five values, thus five series for each instrument.
46
+ */
47
+ export type SandboxExecOutcome = "ok" | "nonzero" | "timeout" | "synthetic-dead" | "synthetic-oom";
34
48
  /**
35
49
  * Bucket bounds for durations that run from seconds to hours: 30 s, then
36
50
  * 1, 2, 5, 10, 30, 60, 120, 240 min. Ten buckets with the overflow, so one
37
51
  * histogram costs about ten series per label set.
38
52
  */
39
53
  export declare const RUN_DURATION_BOUNDS_MS: readonly number[];
54
+ /**
55
+ * Bucket bounds for one exec, which runs from a sub-second probe to an
56
+ * hours-long analysis: 1, 5, 15, 30 s, then 1, 5, 15, 30, 60 min. The run
57
+ * bounds start at 30 s and would put almost every exec in the first bucket.
58
+ */
59
+ export declare const EXEC_DURATION_BOUNDS_MS: readonly number[];
60
+ /**
61
+ * Bucket bounds for the peak resident set of one exec, in bytes: 128 MiB up to
62
+ * 128 GiB by doubling. The top bound is the largest memory a step can request
63
+ * (`SANDBOX_MAX_MEMORY_GB`), thus the overflow bucket holds only a sandbox that
64
+ * outgrew its own ceiling.
65
+ */
66
+ export declare const SANDBOX_PEAK_MEMORY_BOUNDS_BYTES: readonly number[];
67
+ /**
68
+ * Bucket bounds for the CPU time of one exec, in seconds: sub-second up to four
69
+ * hours. Read against `cortex.sandbox.exec.duration` it gives the mean core
70
+ * count the command held, which is what says whether its CPU request was worth
71
+ * granting.
72
+ */
73
+ export declare const SANDBOX_CPU_SECONDS_BOUNDS: readonly number[];
40
74
  /**
41
75
  * Record one run that reached a terminal status. Call it inside the DBOS step
42
76
  * that persists that status, or behind the ledger compare-and-set that accepted
@@ -71,6 +105,23 @@ export declare function stepOutcomeOf(status: StepExecutionStatus): StepOutcome
71
105
  * without a duration instead of recording a bogus one.
72
106
  */
73
107
  export declare function elapsedSinceIso(startedAt: string | null | undefined, nowMs?: number): number | undefined;
108
+ /**
109
+ * Record one sandbox exec that returned a terminal result. Call it at the one
110
+ * seam where an `ExecResult` crosses from the wire into the process. A result
111
+ * that carries no runtime (a synthetic failure) is counted, not timed.
112
+ *
113
+ * `peakMemoryBytes` and `cpuMillis` are the kernel accounting the sandbox
114
+ * reports for the command. Each one is independent: a sandbox image that
115
+ * predates the accounting, and an exec whose command never spawned, report
116
+ * neither, thus a missing member records nothing rather than a zero.
117
+ */
118
+ export declare function recordSandboxExec(args: {
119
+ readonly execId: string;
120
+ readonly outcome: SandboxExecOutcome;
121
+ readonly durationMs?: number | null;
122
+ readonly peakMemoryBytes?: number | undefined;
123
+ readonly cpuMillis?: number | undefined;
124
+ }): void;
74
125
  /** Record one manifest entry dropped at reconcile. */
75
126
  export declare function recordArtifactReconcileDropped(args: {
76
127
  readonly agentId: string;
@@ -6,18 +6,25 @@
6
6
  * - cortex.run.duration{status, workflow} — histogram, start to terminal status
7
7
  * - cortex.step.completed{status, agent_id} — counter, one per executed step that settled
8
8
  * - cortex.step.duration{status, agent_id} — histogram, step start to its terminal ledger write
9
+ * - cortex.sandbox.execs{outcome} — counter, one per sandbox exec that returned a terminal result
10
+ * - cortex.sandbox.exec.duration{outcome} — histogram, the runtime the sandbox reported for that exec
11
+ * - cortex.sandbox.exec.peak_memory_bytes{outcome} — histogram, the peak resident set of that exec
12
+ * - cortex.sandbox.exec.cpu_seconds{outcome} — histogram, the CPU time of that exec
9
13
  * - cortex.artifact.reconcile.dropped{agent_id} — counter for missing manifest entries
10
14
  * - cortex.artifact.reconcile.input_dropped{agent_id, reason} — counter for lineage input drops
11
15
  *
12
16
  * Every label is a bounded vocabulary: `status` is the terminal enum, `workflow`
13
- * names the workflow family, `agent_id` has about 15 values. No instrument
14
- * carries a run, step, exec, user, or organization id — a label with unbounded
15
- * values multiplies the series count.
17
+ * names the workflow family, `agent_id` has about 15 values, `outcome` has
18
+ * five. No instrument carries a run, step, exec, user, or organization id — a
19
+ * label with unbounded values multiplies the series count, and the collector
20
+ * drops a datapoint that carries one.
16
21
  *
17
22
  * A run or step outcome is recorded INSIDE the DBOS step that persists the same
18
23
  * outcome to the ledger (or behind the ledger's own compare-and-set where the
19
24
  * body has no such step). DBOS caches a completed step and does not re-run its
20
- * body on recovery, so a replayed workflow records nothing twice.
25
+ * body on recovery, so a replayed workflow records nothing twice. An exec has
26
+ * no such step, so `recordSandboxExec` carries its own idempotence — see the
27
+ * note on `countedExecs`.
21
28
  *
22
29
  * Instruments are lazy so the record site binds to whichever `MeterProvider`
23
30
  * is globally registered at first use. The metrics API has no late-binding
@@ -31,6 +38,28 @@ import { metrics } from "@opentelemetry/api";
31
38
  * histogram costs about ten series per label set.
32
39
  */
33
40
  export const RUN_DURATION_BOUNDS_MS = [30_000, 60_000, 120_000, 300_000, 600_000, 1_800_000, 3_600_000, 7_200_000, 14_400_000];
41
+ /**
42
+ * Bucket bounds for one exec, which runs from a sub-second probe to an
43
+ * hours-long analysis: 1, 5, 15, 30 s, then 1, 5, 15, 30, 60 min. The run
44
+ * bounds start at 30 s and would put almost every exec in the first bucket.
45
+ */
46
+ export const EXEC_DURATION_BOUNDS_MS = [1_000, 5_000, 15_000, 30_000, 60_000, 300_000, 900_000, 1_800_000, 3_600_000];
47
+ /**
48
+ * Bucket bounds for the peak resident set of one exec, in bytes: 128 MiB up to
49
+ * 128 GiB by doubling. The top bound is the largest memory a step can request
50
+ * (`SANDBOX_MAX_MEMORY_GB`), thus the overflow bucket holds only a sandbox that
51
+ * outgrew its own ceiling.
52
+ */
53
+ export const SANDBOX_PEAK_MEMORY_BOUNDS_BYTES = [
54
+ 134_217_728, 536_870_912, 1_073_741_824, 2_147_483_648, 4_294_967_296, 8_589_934_592, 17_179_869_184, 34_359_738_368, 68_719_476_736, 137_438_953_472,
55
+ ];
56
+ /**
57
+ * Bucket bounds for the CPU time of one exec, in seconds: sub-second up to four
58
+ * hours. Read against `cortex.sandbox.exec.duration` it gives the mean core
59
+ * count the command held, which is what says whether its CPU request was worth
60
+ * granting.
61
+ */
62
+ export const SANDBOX_CPU_SECONDS_BOUNDS = [0.5, 1, 5, 15, 60, 300, 900, 1_800, 3_600, 14_400];
34
63
  let instruments;
35
64
  function getInstruments() {
36
65
  if (instruments === undefined) {
@@ -55,6 +84,25 @@ function getInstruments() {
55
84
  unit: "ms",
56
85
  advice,
57
86
  }),
87
+ sandboxExecs: meter.createCounter("cortex.sandbox.execs", {
88
+ description: "Sandbox execs that returned a terminal result. Tagged by outcome.",
89
+ unit: "{exec}",
90
+ }),
91
+ sandboxExecDuration: meter.createHistogram("cortex.sandbox.exec.duration", {
92
+ description: "Runtime the sandbox reported for one exec. Tagged by outcome.",
93
+ unit: "ms",
94
+ advice: { explicitBucketBoundaries: [...EXEC_DURATION_BOUNDS_MS] },
95
+ }),
96
+ sandboxExecPeakMemory: meter.createHistogram("cortex.sandbox.exec.peak_memory_bytes", {
97
+ description: "Peak resident set of one sandbox exec and of every descendant it waited for. Tagged by outcome.",
98
+ unit: "By",
99
+ advice: { explicitBucketBoundaries: [...SANDBOX_PEAK_MEMORY_BOUNDS_BYTES] },
100
+ }),
101
+ sandboxExecCpuSeconds: meter.createHistogram("cortex.sandbox.exec.cpu_seconds", {
102
+ description: "User plus system CPU time of one sandbox exec. Tagged by outcome.",
103
+ unit: "s",
104
+ advice: { explicitBucketBoundaries: [...SANDBOX_CPU_SECONDS_BOUNDS] },
105
+ }),
58
106
  artifactReconcileDropped: meter.createCounter("cortex.artifact.reconcile.dropped", {
59
107
  description: "Manifest entries dropped because their on-disk file was missing at " +
60
108
  "registration time (writes-then-deletes, renames). Tagged by agent_id.",
@@ -132,6 +180,54 @@ export function elapsedSinceIso(startedAt, nowMs = Date.now()) {
132
180
  return undefined;
133
181
  return Math.max(0, nowMs - startedMs);
134
182
  }
183
+ /**
184
+ * Exec ids already counted, so a replay counts one exec one time.
185
+ *
186
+ * The run and step counters sit inside the DBOS step that persists the same
187
+ * outcome, and DBOS never runs a cached step body a second time. An exec has
188
+ * no such step: `awaitExec` returns into the workflow body, and a new step
189
+ * around the record would shift the function-id sequence of every workflow
190
+ * that is already in flight. An exec id is `{workflowId}:{stepId}:{functionId}`
191
+ * and it is stable across a replay, thus it is the key that makes the record
192
+ * idempotent. The set is process-local: a recovery in a fresh process holds no
193
+ * memory of the first count and can count that exec again.
194
+ */
195
+ const countedExecs = new Set();
196
+ /**
197
+ * How many exec ids the guard remembers. A host runs far fewer concurrent
198
+ * execs than this, and a replay follows its original within one run, thus the
199
+ * cap only drops an id that no live workflow can present again.
200
+ */
201
+ const MAX_COUNTED_EXECS = 4096;
202
+ /**
203
+ * Record one sandbox exec that returned a terminal result. Call it at the one
204
+ * seam where an `ExecResult` crosses from the wire into the process. A result
205
+ * that carries no runtime (a synthetic failure) is counted, not timed.
206
+ *
207
+ * `peakMemoryBytes` and `cpuMillis` are the kernel accounting the sandbox
208
+ * reports for the command. Each one is independent: a sandbox image that
209
+ * predates the accounting, and an exec whose command never spawned, report
210
+ * neither, thus a missing member records nothing rather than a zero.
211
+ */
212
+ export function recordSandboxExec(args) {
213
+ if (countedExecs.has(args.execId))
214
+ return;
215
+ if (countedExecs.size >= MAX_COUNTED_EXECS) {
216
+ const oldest = countedExecs.values().next();
217
+ if (!oldest.done)
218
+ countedExecs.delete(oldest.value);
219
+ }
220
+ countedExecs.add(args.execId);
221
+ const attributes = { outcome: args.outcome };
222
+ const { sandboxExecs, sandboxExecDuration, sandboxExecPeakMemory, sandboxExecCpuSeconds } = getInstruments();
223
+ sandboxExecs.add(1, attributes);
224
+ if (args.durationMs !== undefined && args.durationMs !== null)
225
+ sandboxExecDuration.record(Math.max(0, args.durationMs), attributes);
226
+ if (args.peakMemoryBytes !== undefined)
227
+ sandboxExecPeakMemory.record(Math.max(0, args.peakMemoryBytes), attributes);
228
+ if (args.cpuMillis !== undefined)
229
+ sandboxExecCpuSeconds.record(Math.max(0, args.cpuMillis) / 1000, attributes);
230
+ }
135
231
  /** Record one manifest entry dropped at reconcile. */
136
232
  export function recordArtifactReconcileDropped(args) {
137
233
  getInstruments().artifactReconcileDropped.add(1, { agent_id: args.agentId });
@@ -143,4 +239,5 @@ export function recordLineageInputDropped(args) {
143
239
  /** Test hook — drop memoised instruments to rebind a fresh MeterProvider. */
144
240
  export function __resetMetricsForTest() {
145
241
  instruments = undefined;
242
+ countedExecs.clear();
146
243
  }
@@ -62,9 +62,12 @@ export const STEP_NON_TASK_FIELDS = [
62
62
  "summary",
63
63
  "artifactIds",
64
64
  "error",
65
- // Withheld on purpose: the pre-launch link pass consumes the packages of a
66
- // step, and a step agent must not re-litigate them (the
67
- // planning-enhancements spec).
65
+ // Withheld on purpose: every step reaches an agent through a launch, and
66
+ // the pre-launch link pass consumes the packages of every step it carries —
67
+ // an ad hoc step as much as a planned one. Thus the set is settled before
68
+ // the agent reads its task, and a step agent must not re-litigate it. What
69
+ // the agent does need to know about them — a dropped entry, or a step that
70
+ // declares none — rides in `caveats`.
68
71
  "packages",
69
72
  ];
70
73
  // ── Formatting primitives ─────────────────────────────────────────────
@@ -1 +1 @@
1
- export declare const literatureReviewerPrompt = "# Literature Reviewer\n\nYou are a systematic biology investigator. You receive a research brief\ncontaining genes, pathways, or biological features to investigate, along\nwith the experimental context. Your job is to use your tools to build a\ncomprehensive evidence profile and return a structured report.\n\nYou are task-oriented \u2014 you do NOT interact with the user. You receive\na brief, investigate, and return results.\n\n## Tool usage notes (read before calling any tool)\n\nSeveral tools have strict input shapes. Mis-shaping your call wastes a\nturn and forces a retry.\n\n- `pubmed({action:\"search\"})` \u2014 pass a focused query string. Shape: `{ action: \"search\", query: \"BRCA1 AND breast cancer\", maxResults: 10 }`.\n- `pubmed({action:\"details\"})` \u2014 MUST be called with `pmids`, a non-empty array of\n PMID strings. Shape: `{ action: \"details\", pmids: [\"12345678\", \"87654321\"] }`.\n- `search_interactions` \u2014 accepts up to **100 identifiers per call**. If you\n need more, **batch across multiple calls** rather than truncating your\n list. Shape: `{ identifiers: [\"TP53\", \"BRCA1\", ...], limit: 50 }`.\n- `search_gene` \u2014 batches up to 200 symbols; `lookup_annotation` takes one\n term or accession per call.\n- `gene_preclinical_profile` \u2014 single human gene symbol per call; returns\n baseline expression and mouse-KO phenotype together. Empty/null fields are\n valid \"no data\" outcomes (Bgee may have no calls for dog or macaque; many\n human genes are not yet IMPC-phenotyped). Do NOT retry on empty output.\n- `pubmed({action:\"fulltext\"})` \u2014 single `pmcId` per call (from a `details` result). Use sparingly.\n- `resolve_citation` \u2014 verify one citation already supplied in the brief or returned by another source. Use it for identity/metadata verification, not topical discovery. Preserve its verdict and coverage exactly: `inconclusive` is not `not_found`, and an unavailable authority is not proof of fabrication.\n\nBatching beats truncation: if you have 300 identifiers to look up in\n`search_interactions`, make three calls of 100 rather than one call of 100\nthat silently drops the other 200.\n\n## Investigation Process\n\nFor each gene, pathway, or feature in the brief:\n\n1. **Gene/protein lookup** \u2014 use `search_gene` to get function, aliases,\n associated diseases, and expression patterns.\n2. **Pathway context** \u2014 use `lookup_annotation({vocabulary:\"pathways\"})` to\n find pathways involving the gene. Note which pathways connect multiple\n genes from the brief.\n3. **GO terms** \u2014 use `lookup_annotation({vocabulary:\"go\"})` for functional\n annotations when the gene's role is unclear from the gene search alone.\n4. **Protein interactions** \u2014 use `search_interactions` to find\n interaction partners, especially those that also appear in the brief.\n5. **Literature evidence** \u2014 use `pubmed({action:\"search\"})` with targeted queries\n combining the gene name with the disease/condition from the brief.\n Use `pubmed({action:\"details\"})` for the most relevant hits. Use\n `pubmed({action:\"fulltext\"})` only for highly relevant papers that need\n deeper reading.\n6. **Preclinical grounding** \u2014 when the brief asks about tissue expression,\n model-organism suitability, or KO consequences, call\n `gene_preclinical_profile`, which returns cross-species baseline\n expression and mouse-KO phenotype + viability together. It takes a single\n human gene symbol and tolerates \"no data\" cleanly.\n\n## Depth Guidelines\n\n- **Top priority targets** (top DE genes, hub genes, user-specified):\n Full investigation \u2014 all 5 steps above.\n- **Supporting targets** (enriched pathways, interaction partners):\n Gene lookup + pathway + literature. Skip GO terms and interactions\n unless results are ambiguous.\n- **Limit PubMed searches**: max 2-3 queries per gene. Combine terms\n effectively (e.g., \"BRCA1 AND breast cancer AND RNA-seq\") rather\n than running many narrow queries.\n- **Limit article deep-reads**: use `pubmed({action:\"fulltext\"})` on at most\n 3-5 articles total, only for papers directly relevant to the\n experimental context.\n\n## Output Format\n\nReturn a structured report in this format:\n\n### Evidence Summary\n\nFor each investigated target:\n\n**[Gene/Pathway Name]**\n- **Function**: Brief description of known function\n- **Relevance to context**: How it connects to the experimental\n conditions (e.g., \"known role in AD pathology\")\n- **Literature support**: Key findings from PubMed (cite PMIDs)\n- **Interaction network**: Notable interaction partners found,\n especially those also in the data\n- **Novelty assessment**: Is this finding well-established, recently\n emerging, or potentially novel?\n\n### Cross-Target Patterns\n- Genes that share pathways or interact directly\n- Convergent biological themes (e.g., multiple genes in immune signaling)\n- Contradictions or unexpected associations\n\n### Key References\n- List the most important PMIDs with one-line descriptions\n\n## Do NOT\n\n- **Fabricate results.** If a search returns no results, say so.\n- **Turn resolver uncertainty into certainty.** Never describe a weak candidate, partial coverage, or an unavailable source as proof that a citation is fabricated.\n- **Skip tool calls.** Do not claim knowledge about a gene without\n looking it up. Your value is in systematic, verified investigation.\n- **Over-read articles.** Use `pubmed({action:\"fulltext\"})` sparingly \u2014 abstracts\n from `pubmed({action:\"details\"})` are sufficient for most assessments.\n- **Investigate targets not in the brief.** Stay focused on what was\n requested. Note interesting leads for follow-up but do not chase them.\n- **Produce conversational text.** You are returning a structured report,\n not chatting with a user.\n";
1
+ export declare const literatureReviewerPrompt = "# Literature Reviewer\n\nYou are a systematic biology investigator. You receive a research brief\ncontaining genes, pathways, or biological features to investigate, along\nwith the experimental context. Your job is to use your tools to build a\ncomprehensive evidence profile and return a structured report.\n\nYou are task-oriented \u2014 you do NOT interact with the user. You receive\na brief, investigate, and return results.\n\n## Tool usage notes (read before calling any tool)\n\nSeveral tools have strict input shapes. Mis-shaping your call wastes a\nturn and forces a retry.\n\n- `pubmed({action:\"search\"})` \u2014 pass a focused query string. Shape: `{ action: \"search\", query: \"BRCA1 AND breast cancer\", maxResults: 10 }`.\n- `pubmed({action:\"details\"})` \u2014 MUST be called with `pmids`, a non-empty array of\n PMID strings. Shape: `{ action: \"details\", pmids: [\"12345678\", \"87654321\"] }`.\n- `search_interactions` \u2014 accepts up to **100 identifiers per call**. If you\n need more, **batch across multiple calls** rather than truncating your\n list. Shape: `{ identifiers: [\"TP53\", \"BRCA1\", ...], limit: 50 }`.\n- `search_gene` \u2014 batches up to 200 symbols; `lookup_annotation` takes one\n term or accession per call.\n- `search_protein` \u2014 one gene symbol or UniProt accession per call. It answers\n with the protein: the accession, the recommended name, the sequence length,\n the curated function, and the subcellular locations. An empty `proteins`\n array is a valid \"no data\" outcome; do NOT retry the same call.\n- `gene_preclinical_profile` \u2014 single human gene symbol per call; returns\n baseline expression and mouse-KO phenotype together. Empty/null fields are\n valid \"no data\" outcomes (Bgee may have no calls for dog or macaque; many\n human genes are not yet IMPC-phenotyped). Do NOT retry on empty output.\n- `pubmed({action:\"fulltext\"})` \u2014 single `pmcId` per call (from a `details` result). Use sparingly.\n- `resolve_citation` \u2014 verify one citation already supplied in the brief or returned by another source. Use it for identity/metadata verification, not topical discovery. Preserve its verdict and coverage exactly: `inconclusive` is not `not_found`, and an unavailable authority is not proof of fabrication.\n\nBatching beats truncation: if you have 300 identifiers to look up in\n`search_interactions`, make three calls of 100 rather than one call of 100\nthat silently drops the other 200.\n\n## Investigation Process\n\nFor each gene, pathway, or feature in the brief:\n\n1. **Gene lookup** \u2014 use `search_gene` to get the gene: its Ensembl ID,\n coordinates, biotype, and aliases.\n2. **Protein lookup** \u2014 use `search_protein` when the claim is about the\n protein rather than the locus: what it does, how long it is, and where in\n the cell it acts. It also returns the UniProt accession, which is the key\n the structure tools take.\n3. **Pathway context** \u2014 use `lookup_annotation({vocabulary:\"pathways\"})` to\n find pathways involving the gene. Note which pathways connect multiple\n genes from the brief.\n4. **GO terms** \u2014 use `lookup_annotation({vocabulary:\"go\"})` for functional\n annotations when the gene's role is unclear from the gene search alone.\n5. **Protein interactions** \u2014 use `search_interactions` to find\n interaction partners, especially those that also appear in the brief.\n6. **Literature evidence** \u2014 use `pubmed({action:\"search\"})` with targeted queries\n combining the gene name with the disease/condition from the brief.\n Use `pubmed({action:\"details\"})` for the most relevant hits. Use\n `pubmed({action:\"fulltext\"})` only for highly relevant papers that need\n deeper reading.\n7. **Preclinical grounding** \u2014 when the brief asks about tissue expression,\n model-organism suitability, or KO consequences, call\n `gene_preclinical_profile`, which returns cross-species baseline\n expression and mouse-KO phenotype + viability together. It takes a single\n human gene symbol and tolerates \"no data\" cleanly.\n\n## Depth Guidelines\n\n- **Top priority targets** (top DE genes, hub genes, user-specified):\n Full investigation \u2014 every step above.\n- **Supporting targets** (enriched pathways, interaction partners):\n Gene lookup + pathway + literature. Skip GO terms and interactions\n unless results are ambiguous.\n- **Limit PubMed searches**: max 2-3 queries per gene. Combine terms\n effectively (e.g., \"BRCA1 AND breast cancer AND RNA-seq\") rather\n than running many narrow queries.\n- **Limit article deep-reads**: use `pubmed({action:\"fulltext\"})` on at most\n 3-5 articles total, only for papers directly relevant to the\n experimental context.\n\n## Output Format\n\nReturn a structured report in this format:\n\n### Evidence Summary\n\nFor each investigated target:\n\n**[Gene/Pathway Name]**\n- **Function**: Brief description of known function\n- **Relevance to context**: How it connects to the experimental\n conditions (e.g., \"known role in AD pathology\")\n- **Literature support**: Key findings from PubMed (cite PMIDs)\n- **Interaction network**: Notable interaction partners found,\n especially those also in the data\n- **Novelty assessment**: Is this finding well-established, recently\n emerging, or potentially novel?\n\n### Cross-Target Patterns\n- Genes that share pathways or interact directly\n- Convergent biological themes (e.g., multiple genes in immune signaling)\n- Contradictions or unexpected associations\n\n### Key References\n- List the most important PMIDs with one-line descriptions\n\n## Do NOT\n\n- **Fabricate results.** If a search returns no results, say so.\n- **Turn resolver uncertainty into certainty.** Never describe a weak candidate, partial coverage, or an unavailable source as proof that a citation is fabricated.\n- **Skip tool calls.** Do not claim knowledge about a gene without\n looking it up. Your value is in systematic, verified investigation.\n- **Over-read articles.** Use `pubmed({action:\"fulltext\"})` sparingly \u2014 abstracts\n from `pubmed({action:\"details\"})` are sufficient for most assessments.\n- **Investigate targets not in the brief.** Stay focused on what was\n requested. Note interesting leads for follow-up but do not chase them.\n- **Produce conversational text.** You are returning a structured report,\n not chatting with a user.\n";
@@ -21,6 +21,10 @@ turn and forces a retry.
21
21
  list. Shape: \`{ identifiers: ["TP53", "BRCA1", ...], limit: 50 }\`.
22
22
  - \`search_gene\` — batches up to 200 symbols; \`lookup_annotation\` takes one
23
23
  term or accession per call.
24
+ - \`search_protein\` — one gene symbol or UniProt accession per call. It answers
25
+ with the protein: the accession, the recommended name, the sequence length,
26
+ the curated function, and the subcellular locations. An empty \`proteins\`
27
+ array is a valid "no data" outcome; do NOT retry the same call.
24
28
  - \`gene_preclinical_profile\` — single human gene symbol per call; returns
25
29
  baseline expression and mouse-KO phenotype together. Empty/null fields are
26
30
  valid "no data" outcomes (Bgee may have no calls for dog or macaque; many
@@ -36,21 +40,25 @@ that silently drops the other 200.
36
40
 
37
41
  For each gene, pathway, or feature in the brief:
38
42
 
39
- 1. **Gene/protein lookup** — use \`search_gene\` to get function, aliases,
40
- associated diseases, and expression patterns.
41
- 2. **Pathway context** — use \`lookup_annotation({vocabulary:"pathways"})\` to
43
+ 1. **Gene lookup** — use \`search_gene\` to get the gene: its Ensembl ID,
44
+ coordinates, biotype, and aliases.
45
+ 2. **Protein lookup** — use \`search_protein\` when the claim is about the
46
+ protein rather than the locus: what it does, how long it is, and where in
47
+ the cell it acts. It also returns the UniProt accession, which is the key
48
+ the structure tools take.
49
+ 3. **Pathway context** — use \`lookup_annotation({vocabulary:"pathways"})\` to
42
50
  find pathways involving the gene. Note which pathways connect multiple
43
51
  genes from the brief.
44
- 3. **GO terms** — use \`lookup_annotation({vocabulary:"go"})\` for functional
52
+ 4. **GO terms** — use \`lookup_annotation({vocabulary:"go"})\` for functional
45
53
  annotations when the gene's role is unclear from the gene search alone.
46
- 4. **Protein interactions** — use \`search_interactions\` to find
54
+ 5. **Protein interactions** — use \`search_interactions\` to find
47
55
  interaction partners, especially those that also appear in the brief.
48
- 5. **Literature evidence** — use \`pubmed({action:"search"})\` with targeted queries
56
+ 6. **Literature evidence** — use \`pubmed({action:"search"})\` with targeted queries
49
57
  combining the gene name with the disease/condition from the brief.
50
58
  Use \`pubmed({action:"details"})\` for the most relevant hits. Use
51
59
  \`pubmed({action:"fulltext"})\` only for highly relevant papers that need
52
60
  deeper reading.
53
- 6. **Preclinical grounding** — when the brief asks about tissue expression,
61
+ 7. **Preclinical grounding** — when the brief asks about tissue expression,
54
62
  model-organism suitability, or KO consequences, call
55
63
  \`gene_preclinical_profile\`, which returns cross-species baseline
56
64
  expression and mouse-KO phenotype + viability together. It takes a single
@@ -59,7 +67,7 @@ For each gene, pathway, or feature in the brief:
59
67
  ## Depth Guidelines
60
68
 
61
69
  - **Top priority targets** (top DE genes, hub genes, user-specified):
62
- Full investigation — all 5 steps above.
70
+ Full investigation — every step above.
63
71
  - **Supporting targets** (enriched pathways, interaction partners):
64
72
  Gene lookup + pathway + literature. Skip GO terms and interactions
65
73
  unless results are ambiguous.
@@ -2,4 +2,8 @@ import type { ResourcePolicy } from "../config/resource-limits.js";
2
2
  /** The Resource Estimation planning rules — concrete host limits when a
3
3
  * policy is supplied, the historical default guidance otherwise. */
4
4
  export declare function resourceEstimationSection(policy?: ResourcePolicy): string;
5
+ /** The one grammar of a `packages` entry. Every surface that asks a model for
6
+ * that array teaches it from here: two texts of one grammar drift, and a
7
+ * spelling rule that only one surface states is a launch refusal on the other. */
8
+ export declare function packagesSection(): string;
5
9
  export declare function plannerPrompt(agentCatalog: string, resourcePolicy?: ResourcePolicy): string;
@@ -25,6 +25,27 @@ need 18 GB of RAM.`;
25
25
  restructuring or downsizing yields a viable plan — call \`report_blocker\`
26
26
  naming the resource shortfall as the reason.`;
27
27
  }
28
+ /** The one grammar of a `packages` entry. Every surface that asks a model for
29
+ * that array teaches it from here: two texts of one grammar drift, and a
30
+ * spelling rule that only one surface states is a launch refusal on the other. */
31
+ export function packagesSection() {
32
+ return `### The Packages of Each Step
33
+ Every step carries a \`packages\` array — the packages its scripts import.
34
+
35
+ - Name each package as a requirement: a bare name (\`"scanpy"\`), or a name with
36
+ one exact version (\`"numpy==1.26.4"\`). Never a path, a URL, or a store
37
+ directory — a location is an installer detail, and validation refuses it.
38
+ - An empty array is valid: the step needs nothing beyond the baked toolchain.
39
+ - When the package census shows one name under a Python section AND under an R
40
+ section, write the prefixed form the census shows: \`"python:igraph"\` or
41
+ \`"r:igraph"\`. A bare name that both tracks hold refuses the launch, because
42
+ the pool cannot tell which package you mean.
43
+ - Write a bare name for every other package. The prefix is for a both-track
44
+ name only.
45
+ - The set is not a promise of completeness. The execution agent can still link a
46
+ missing package mid-step. Name what you know the step imports; do not pad the
47
+ list defensively.`;
48
+ }
28
49
  export function plannerPrompt(agentCatalog, resourcePolicy) {
29
50
  return `# Analysis Planner
30
51
 
@@ -160,22 +181,7 @@ only once the run reaches it, which is the most expensive moment to learn it.
160
181
  absent reference: \`request_clarification\`, rather than plan a step that can
161
182
  only report failure.
162
183
 
163
- ### The Packages of Each Step
164
- Every step carries a \`packages\` array — the packages its scripts import.
165
-
166
- - Name each package as a requirement: a bare name (\`"scanpy"\`), or a name with
167
- one exact version (\`"numpy==1.26.4"\`). Never a path, a URL, or a store
168
- directory — a location is an installer detail, and validation refuses it.
169
- - An empty array is valid: the step needs nothing beyond the baked toolchain.
170
- - When the package census shows one name under a Python section AND under an R
171
- section, write the prefixed form the census shows: \`"python:igraph"\` or
172
- \`"r:igraph"\`. A bare name that both tracks hold refuses the launch, because
173
- the pool cannot tell which package you mean.
174
- - Write a bare name for every other package. The prefix is for a both-track
175
- name only.
176
- - The set is not a promise of completeness. The execution agent can still link a
177
- missing package mid-step. Name what you know the step imports; do not pad the
178
- list defensively.
184
+ ${packagesSection()}
179
185
 
180
186
  ### Resource Estimation
181
187
  ${resourceEstimationSection(resourcePolicy)}
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { chmod, mkdir } from "node:fs/promises";
15
15
  import { join } from "node:path";
16
+ import { recordSandboxExec } from "../lib/metrics.js";
16
17
  import { clampResources } from "../config/resource-limits.js";
17
18
  import { tailWritePrefix } from "../workspace/paths.js";
18
19
  import { tryMutation } from "../lib/db-result.js";
@@ -21,6 +22,7 @@ import { clearSandboxRef, setSandboxRef } from "../state/index.js";
21
22
  import { capExecStreams, EXEC_STREAM_BYTE_CAP } from "../tools/workspace/result-bounds.js";
22
23
  import { awaitExec } from "./await-exec.js";
23
24
  import { createDockerSandboxOps } from "./docker-client.js";
25
+ import { noteExecOutcome, sandboxExecOutcomeOf, summarizeExec } from "./exec-outcome.js";
24
26
  import { createK8sSandboxOps } from "./k8s-client.js";
25
27
  import { sandboxWriteTail } from "./mount-plan.js";
26
28
  import { SandboxFailure } from "./sandbox-error.js";
@@ -198,7 +200,27 @@ export function createSandboxClient(config) {
198
200
  // tool results, workflow return values, durable step outputs — is bounded
199
201
  // by construction, including against a sandbox image that predates the
200
202
  // retention budget and still returns whole streams.
201
- awaitExec: async (ref, execId, emit, deadline) => capExecStreams(await awaitExec(ref, execId, emit, deadline, composeAwaitOptions(config.awaitOptions, transport, isAlive)), config.execStreamByteCap ?? EXEC_STREAM_BYTE_CAP),
203
+ //
204
+ // The same seam is where an exec becomes observable. It sees every exec
205
+ // of every caller — the step agent, the data profile, the derivations —
206
+ // and it sees the outcome the caller then folds into a tool result, so
207
+ // one record here counts what a consumer would each have to count for
208
+ // itself. `recordSandboxExec` is idempotent over the exec id, because a
209
+ // replayed body reaches this line again. `capExecStreams` cuts the
210
+ // streams and keeps every other field, thus the kernel accounting the
211
+ // sandbox reported reaches the record intact.
212
+ awaitExec: async (ref, execId, emit, deadline) => {
213
+ const result = capExecStreams(await awaitExec(ref, execId, emit, deadline, composeAwaitOptions(config.awaitOptions, transport, isAlive)), config.execStreamByteCap ?? EXEC_STREAM_BYTE_CAP);
214
+ recordSandboxExec({
215
+ execId,
216
+ outcome: sandboxExecOutcomeOf(result),
217
+ durationMs: result.durationMs,
218
+ peakMemoryBytes: result.usage?.peakMemoryBytes,
219
+ cpuMillis: result.usage?.cpuMillis,
220
+ });
221
+ noteExecOutcome(ref.sandboxId, summarizeExec(result));
222
+ return result;
223
+ },
202
224
  isAlive,
203
225
  isAliveById: async (sandboxId) => unwrapOrThrow((await ops.isAliveById(sandboxId)).mapErr(failing)),
204
226
  teardown,
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What one settled exec is worth to an operator: the bounded outcome that
3
+ * `cortex.sandbox.execs` counts, and the summary that the `step failure`
4
+ * record of `workflows/sandbox-step.ts` carries.
5
+ *
6
+ * No stream text crosses this module. Sandbox output is generated code and the
7
+ * output of that code, thus it is untrusted, and the sandbox-exec-logging spec
8
+ * keeps that text on the sandbox side of the boundary. A summary gives the
9
+ * SIZE of stderr and never a byte of it.
10
+ *
11
+ * The summary of the last exec of a sandbox lives in one process-local cell,
12
+ * keyed by sandbox id. A sandbox belongs to one step (see the teardown of
13
+ * `create-sandbox.ts`), thus the cell is step-scoped in effect and the step
14
+ * body reads it without a new dependency through the agent factory. A replay
15
+ * re-runs the await loop and writes the cell again, thus a recovered body
16
+ * reads the summary of the same exec. The map is capped, so a sandbox whose
17
+ * step never reaches the read site cannot hold memory.
18
+ */
19
+ import type { SandboxExecOutcome } from "../lib/metrics.js";
20
+ import type { ExecResult } from "./types.js";
21
+ /**
22
+ * The outcome one result carries. A synthetic failure names a dead machine and
23
+ * not a command, thus it wins over every other field. A timeout wins over the
24
+ * exit code that the kill produced. `ok` is exit code 0 and nothing else, thus
25
+ * a result with no code counts as `nonzero`.
26
+ */
27
+ export declare function sandboxExecOutcomeOf(result: ExecResult): SandboxExecOutcome;
28
+ /** The account of one exec that a failure record can hold. Text-free by construction. */
29
+ export interface ExecOutcomeSummary {
30
+ readonly exitCode: number | null;
31
+ readonly timedOut: boolean;
32
+ /**
33
+ * What the command wrote to stderr, in bytes. The sandbox reports the total
34
+ * when it dropped output past its retention budget; without that total the
35
+ * retained slice IS the whole stream, so its length is the total.
36
+ */
37
+ readonly stderrTotalBytes: number;
38
+ readonly syntheticFailureReason?: string;
39
+ }
40
+ /** Reduce a result to its summary. */
41
+ export declare function summarizeExec(result: ExecResult): ExecOutcomeSummary;
42
+ /** Keep `summary` as the last known exec of `sandboxId`. */
43
+ export declare function noteExecOutcome(sandboxId: string, summary: ExecOutcomeSummary): void;
44
+ /** The last known exec of `sandboxId`, or `undefined` when it ran none. */
45
+ export declare function lastExecOutcome(sandboxId: string): ExecOutcomeSummary | undefined;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * What one settled exec is worth to an operator: the bounded outcome that
3
+ * `cortex.sandbox.execs` counts, and the summary that the `step failure`
4
+ * record of `workflows/sandbox-step.ts` carries.
5
+ *
6
+ * No stream text crosses this module. Sandbox output is generated code and the
7
+ * output of that code, thus it is untrusted, and the sandbox-exec-logging spec
8
+ * keeps that text on the sandbox side of the boundary. A summary gives the
9
+ * SIZE of stderr and never a byte of it.
10
+ *
11
+ * The summary of the last exec of a sandbox lives in one process-local cell,
12
+ * keyed by sandbox id. A sandbox belongs to one step (see the teardown of
13
+ * `create-sandbox.ts`), thus the cell is step-scoped in effect and the step
14
+ * body reads it without a new dependency through the agent factory. A replay
15
+ * re-runs the await loop and writes the cell again, thus a recovered body
16
+ * reads the summary of the same exec. The map is capped, so a sandbox whose
17
+ * step never reaches the read site cannot hold memory.
18
+ */
19
+ /** The reason that names a machine the kernel killed for its memory limit. */
20
+ const OOM_KILLED = "sandbox-oom-killed";
21
+ /**
22
+ * The outcome one result carries. A synthetic failure names a dead machine and
23
+ * not a command, thus it wins over every other field. A timeout wins over the
24
+ * exit code that the kill produced. `ok` is exit code 0 and nothing else, thus
25
+ * a result with no code counts as `nonzero`.
26
+ */
27
+ export function sandboxExecOutcomeOf(result) {
28
+ if (result.syntheticFailure)
29
+ return result.syntheticFailure.reason === OOM_KILLED ? "synthetic-oom" : "synthetic-dead";
30
+ if (result.timedOut)
31
+ return "timeout";
32
+ return result.exitCode === 0 ? "ok" : "nonzero";
33
+ }
34
+ /** Reduce a result to its summary. */
35
+ export function summarizeExec(result) {
36
+ return {
37
+ exitCode: result.exitCode,
38
+ timedOut: result.timedOut,
39
+ stderrTotalBytes: result.stderrTotalBytes ?? Buffer.byteLength(result.stderr, "utf8"),
40
+ ...(result.syntheticFailure ? { syntheticFailureReason: result.syntheticFailure.reason } : {}),
41
+ };
42
+ }
43
+ /**
44
+ * How many sandboxes keep a summary. A host runs far fewer sandboxes at once
45
+ * than this, thus the cap only drops the summary of a step that ended long ago.
46
+ */
47
+ const MAX_TRACKED_SANDBOXES = 256;
48
+ const lastExecBySandbox = new Map();
49
+ /** Keep `summary` as the last known exec of `sandboxId`. */
50
+ export function noteExecOutcome(sandboxId, summary) {
51
+ // Delete before set so the key moves to the end of the insertion order and
52
+ // the eviction below drops the least recently written sandbox.
53
+ lastExecBySandbox.delete(sandboxId);
54
+ lastExecBySandbox.set(sandboxId, summary);
55
+ while (lastExecBySandbox.size > MAX_TRACKED_SANDBOXES) {
56
+ const oldest = lastExecBySandbox.keys().next();
57
+ if (oldest.done)
58
+ break;
59
+ lastExecBySandbox.delete(oldest.value);
60
+ }
61
+ }
62
+ /** The last known exec of `sandboxId`, or `undefined` when it ran none. */
63
+ export function lastExecOutcome(sandboxId) {
64
+ return lastExecBySandbox.get(sandboxId);
65
+ }
@@ -95,6 +95,16 @@ export interface K8sClientConfig {
95
95
  coreApi?: CoreV1Api;
96
96
  registerSandbox: (meta: CreateSandboxMeta, ref: SandboxRef) => Promise<void>;
97
97
  }
98
+ /** Clock and sleep seams, so a test can drive the slow-pod warning without waiting a minute. */
99
+ export interface WaitForPodReadyDeps {
100
+ readonly logger?: Logger;
101
+ readonly now?: () => number;
102
+ readonly sleep?: (ms: number) => Promise<void>;
103
+ }
104
+ export declare function waitForPodReady(coreApi: CoreV1Api, namespace: string, sandboxId: string, deps?: WaitForPodReadyDeps): ResultAsync<{
105
+ podIP: string;
106
+ podName: string;
107
+ }, SandboxError>;
98
108
  export declare function createK8sSandboxOps(config: K8sClientConfig): {
99
109
  createSandbox(meta: CreateSandboxMeta, identity: SandboxIdentity): ResultAsync<SandboxRef, SandboxError>;
100
110
  teardown(ref: SandboxRef): ResultAsync<void, SandboxError>;