@inflexa-ai/harness 0.35.5 → 0.35.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/conversation-agent.js +10 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +17 -0
- package/dist/lib/metrics.d.ts +55 -4
- package/dist/lib/metrics.js +101 -4
- package/dist/prompts/briefing.js +6 -3
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +16 -8
- package/dist/prompts/planner.d.ts +4 -0
- package/dist/prompts/planner.js +22 -16
- package/dist/sandbox/create-sandbox.js +23 -1
- package/dist/sandbox/exec-outcome.d.ts +45 -0
- package/dist/sandbox/exec-outcome.js +65 -0
- package/dist/sandbox/k8s-client.d.ts +10 -0
- package/dist/sandbox/k8s-client.js +40 -5
- package/dist/sandbox/liveness.d.ts +3 -1
- package/dist/sandbox/types.d.ts +24 -0
- package/dist/sandbox/types.js +24 -0
- package/dist/tools/ad-hoc-router.d.ts +23 -0
- package/dist/tools/ad-hoc-router.js +93 -5
- package/dist/tools/bio/alphafold-prediction.d.ts +19 -0
- package/dist/tools/bio/alphafold-prediction.js +40 -0
- package/dist/tools/bio/index.d.ts +2 -0
- package/dist/tools/bio/index.js +3 -0
- package/dist/tools/bio/search-protein.d.ts +20 -0
- package/dist/tools/bio/search-protein.js +61 -0
- package/dist/tools/execute-analysis.d.ts +2 -1
- package/dist/tools/execute-analysis.js +66 -5
- package/dist/tools/lib/alphafold-client.d.ts +55 -0
- package/dist/tools/lib/alphafold-client.js +72 -0
- package/dist/tools/lib/api-utils.d.ts +25 -0
- package/dist/tools/lib/api-utils.js +45 -3
- package/dist/tools/lib/uniprot-client.d.ts +131 -0
- package/dist/tools/lib/uniprot-client.js +190 -0
- package/dist/tools/research/literature-reviewer.js +2 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +25 -0
- package/dist/tools/sandbox/list-available-packages.js +51 -29
- package/dist/workflows/sandbox-step.js +10 -1
- package/dist/workflows/target-assessment/progress-stream.d.ts +75 -0
- package/dist/workflows/target-assessment/progress-stream.js +110 -0
- 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
|
}),
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { bootHarness } from "./runtime/boot.js";
|
|
|
21
21
|
export type { BootHarnessDeps, BootedHarness } from "./runtime/boot.js";
|
|
22
22
|
export { createConversationAgent } from "./agents/conversation-agent.js";
|
|
23
23
|
export type { ConversationAgentDeps } from "./agents/conversation-agent.js";
|
|
24
|
+
export type { BioToolKeys } from "./tools/bio/keys.js";
|
|
24
25
|
export { createLocalRunAuthorizer } from "./auth/local-run-authorizer.js";
|
|
25
26
|
export type { RunAuthorizer, AuthorizeRunInput, RunAuthorization, RevokeByJtiRef } from "./execution/run-authorizer.js";
|
|
26
27
|
export { createNoopBillingResolver } from "./billing/noop-resolver.js";
|
|
@@ -157,6 +158,11 @@ export { registerSandboxStep } from "./workflows/sandbox-step.js";
|
|
|
157
158
|
export type { SandboxStepDeps, SandboxStepInput, SandboxStepResult, SandboxAgentBuildContext } from "./workflows/sandbox-step.js";
|
|
158
159
|
export { DEFAULT_CLAIM_INVESTIGATION_CONFIG } from "./workflows/target-assessment/investigation/index.js";
|
|
159
160
|
export type { ClaimInvestigationConfig } from "./workflows/target-assessment/investigation/index.js";
|
|
161
|
+
export { insertAssessment, getAssessment, updateProgress, listAssessmentsByOrg } from "./state/target-assessments.js";
|
|
162
|
+
export type { InsertAssessmentInput, ListAssessmentsOptions } from "./state/target-assessments.js";
|
|
163
|
+
export type { TargetAssessmentRow, TargetAssessmentListRow, TargetAssessmentStatus, TargetAssessmentError } from "./state/target-assessments.js";
|
|
164
|
+
export { createTargetAssessmentProgressStream } from "./workflows/target-assessment/progress-stream.js";
|
|
165
|
+
export type { TargetAssessmentProgressStream, TargetAssessmentProgressStreamDeps, TargetAssessmentProgressHandler, TargetAssessmentProgressSubscribeOptions, } from "./workflows/target-assessment/progress-stream.js";
|
|
160
166
|
export { createRunEventStream } from "./execution/run-event-stream.js";
|
|
161
167
|
export type { RunEventStream, RunEventStreamDeps, RunEventSubscribeOptions, RunEventPartHandler } from "./execution/run-event-stream.js";
|
|
162
168
|
export { createSandboxAgents, SANDBOX_AGENT_META } from "./agents/sandbox/index.js";
|
package/dist/index.js
CHANGED
|
@@ -275,6 +275,23 @@ export { registerSandboxStep } from "./workflows/sandbox-step.js";
|
|
|
275
275
|
// embedder wiring `executeTargetAssessment` sets these at its composition root;
|
|
276
276
|
// the dossier reports whichever values were in force.
|
|
277
277
|
export { DEFAULT_CLAIM_INVESTIGATION_CONFIG } from "./workflows/target-assessment/investigation/index.js";
|
|
278
|
+
// The row surface of `cortex_target_assessments`: the intake write an embedder's
|
|
279
|
+
// trigger route makes, the two reads its detail and list routes make, and the
|
|
280
|
+
// progress write. The row types ride with them — `getAssessment` and
|
|
281
|
+
// `listAssessmentsByOrg` hand back values an embedder cannot hold without naming,
|
|
282
|
+
// and `TargetAssessmentStatus` is what a host switches on to render one. The
|
|
283
|
+
// terminal writers (`setDossier`, `markFailed`, `markAssessmentSuspended`,
|
|
284
|
+
// `markAssessmentRunning`, `softDeleteAssessment`) stay off the front door: the
|
|
285
|
+
// workflow's own terminal path owns them, and an embedder calling one races the
|
|
286
|
+
// state the workflow is about to write.
|
|
287
|
+
export { insertAssessment, getAssessment, updateProgress, listAssessmentsByOrg } from "./state/target-assessments.js";
|
|
288
|
+
// The read side of a target assessment's progress stream. `emitProgress` writes
|
|
289
|
+
// one part per phase and the workflow owns that write; this is the only way to
|
|
290
|
+
// read it back without reaching for the durability engine, which is exactly what
|
|
291
|
+
// an embedder rendering a progress bar would otherwise have to do. One stream per
|
|
292
|
+
// assessment (`workflowID === assessmentId`), so it needs no pool — unlike the
|
|
293
|
+
// run-event seam below, which fans in a parent and its children.
|
|
294
|
+
export { createTargetAssessmentProgressStream } from "./workflows/target-assessment/progress-stream.js";
|
|
278
295
|
// Run-event read seam — the fine-grained durable channel beside `observeRun`.
|
|
279
296
|
// The two are a deliberate pair, not a duplication: `observeRun` is the coarse,
|
|
280
297
|
// in-process run/step snapshot a workflow hands its host synchronously, and it
|
package/dist/lib/metrics.d.ts
CHANGED
|
@@ -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
|
|
14
|
-
* carries a run, step, exec, user, or organization id — a
|
|
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;
|
package/dist/lib/metrics.js
CHANGED
|
@@ -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
|
|
14
|
-
* carries a run, step, exec, user, or organization id — a
|
|
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
|
}
|
package/dist/prompts/briefing.js
CHANGED
|
@@ -62,9 +62,12 @@ export const STEP_NON_TASK_FIELDS = [
|
|
|
62
62
|
"summary",
|
|
63
63
|
"artifactIds",
|
|
64
64
|
"error",
|
|
65
|
-
// Withheld on purpose:
|
|
66
|
-
//
|
|
67
|
-
//
|
|
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
|
|
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
|
|
40
|
-
|
|
41
|
-
2. **
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
5. **Protein interactions** — use \`search_interactions\` to find
|
|
47
55
|
interaction partners, especially those that also appear in the brief.
|
|
48
|
-
|
|
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
|
-
|
|
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 —
|
|
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;
|
package/dist/prompts/planner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|