@inflexa-ai/harness 0.35.2 → 0.35.4
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/citations/types.d.ts +2 -2
- package/dist/config/environment-stores.d.ts +10 -0
- package/dist/contracts/schemas/chat-parts.d.ts +11 -11
- package/dist/execution/run-canceler.js +20 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/lib/metrics.d.ts +63 -5
- package/dist/lib/metrics.js +104 -5
- package/dist/lib/otel-spans.d.ts +77 -0
- package/dist/lib/otel-spans.js +123 -0
- package/dist/lib/otel.d.ts +12 -3
- package/dist/lib/otel.js +15 -4
- package/dist/loop/run-agent.js +13 -2
- package/dist/prompts/planner.js +6 -0
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +8 -3
- package/dist/runtime/dbos.d.ts +8 -7
- package/dist/sandbox/await-exec.js +16 -9
- package/dist/sandbox/notification-sweep.js +5 -2
- package/dist/sandbox/package-identity.d.ts +208 -0
- package/dist/sandbox/package-identity.js +244 -0
- package/dist/sandbox/reaper.js +5 -2
- package/dist/sandbox/submit-exec.js +6 -3
- package/dist/sandbox/types.d.ts +20 -25
- package/dist/sandbox/watchdog.js +8 -3
- package/dist/schemas/plan-schemas.d.ts +3 -3
- package/dist/schemas/validate-plan.js +32 -15
- package/dist/schemas/workflow-state.js +3 -1
- package/dist/state/data-profile.d.ts +20 -6
- package/dist/state/data-profile.js +16 -8
- package/dist/state/index.d.ts +1 -1
- package/dist/state/schema.d.ts +2 -2
- package/dist/tasks/data-profile.js +26 -12
- package/dist/tools/execute-analysis.js +37 -20
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/sandbox/link-packages.d.ts +6 -6
- package/dist/tools/sandbox/link-packages.js +45 -14
- package/dist/tools/sandbox/list-available-packages.d.ts +23 -1
- package/dist/tools/sandbox/list-available-packages.js +185 -43
- package/dist/workflows/execute-analysis.d.ts +4 -1
- package/dist/workflows/execute-analysis.js +40 -4
- package/dist/workflows/execute-target-assessment.js +68 -31
- package/dist/workflows/sandbox-step.js +7 -1
- package/package.json +1 -1
package/dist/lib/otel.d.ts
CHANGED
|
@@ -5,12 +5,21 @@
|
|
|
5
5
|
* so the bundler cannot tree-shake it (side-effect-only imports get dropped).
|
|
6
6
|
*
|
|
7
7
|
* Traces: NodeTracerProvider exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT
|
|
8
|
-
* is set.
|
|
8
|
+
* is set. The provider carries the span policy of otel-spans.ts: the
|
|
9
|
+
* root span of a workflow declared with `untracedWorkflow` is not
|
|
10
|
+
* recorded (and its children with it), a replayed `cached=true` span
|
|
11
|
+
* is dropped before export, and a step body that calls `stableSpan`
|
|
12
|
+
* is exported under a stable name with its id in an `inflexa.*`
|
|
13
|
+
* attribute.
|
|
9
14
|
*
|
|
10
15
|
* Metrics: MeterProvider exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT is
|
|
11
16
|
* set and OTEL_METRICS_EXPORTER is not `none`. The export interval is
|
|
12
|
-
* OTEL_METRIC_EXPORT_INTERVAL (ms), default 60 s.
|
|
13
|
-
*
|
|
17
|
+
* OTEL_METRIC_EXPORT_INTERVAL (ms), default 60 s. The run and step
|
|
18
|
+
* outcome instruments (`cortex.run.*`, `cortex.step.*`) and the
|
|
19
|
+
* reconcile counters are defined in metrics.ts; the agent loop, the
|
|
20
|
+
* thread memory, and the workflows hold their own instruments next
|
|
21
|
+
* to their record sites. Every instrument binds lazily to the
|
|
22
|
+
* provider registered here.
|
|
14
23
|
*
|
|
15
24
|
* Resource: the host's `serviceName` / `serviceVersion` merged with the SDK
|
|
16
25
|
* env detector (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`);
|
package/dist/lib/otel.js
CHANGED
|
@@ -5,12 +5,21 @@
|
|
|
5
5
|
* so the bundler cannot tree-shake it (side-effect-only imports get dropped).
|
|
6
6
|
*
|
|
7
7
|
* Traces: NodeTracerProvider exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT
|
|
8
|
-
* is set.
|
|
8
|
+
* is set. The provider carries the span policy of otel-spans.ts: the
|
|
9
|
+
* root span of a workflow declared with `untracedWorkflow` is not
|
|
10
|
+
* recorded (and its children with it), a replayed `cached=true` span
|
|
11
|
+
* is dropped before export, and a step body that calls `stableSpan`
|
|
12
|
+
* is exported under a stable name with its id in an `inflexa.*`
|
|
13
|
+
* attribute.
|
|
9
14
|
*
|
|
10
15
|
* Metrics: MeterProvider exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT is
|
|
11
16
|
* set and OTEL_METRICS_EXPORTER is not `none`. The export interval is
|
|
12
|
-
* OTEL_METRIC_EXPORT_INTERVAL (ms), default 60 s.
|
|
13
|
-
*
|
|
17
|
+
* OTEL_METRIC_EXPORT_INTERVAL (ms), default 60 s. The run and step
|
|
18
|
+
* outcome instruments (`cortex.run.*`, `cortex.step.*`) and the
|
|
19
|
+
* reconcile counters are defined in metrics.ts; the agent loop, the
|
|
20
|
+
* thread memory, and the workflows hold their own instruments next
|
|
21
|
+
* to their record sites. Every instrument binds lazily to the
|
|
22
|
+
* provider registered here.
|
|
14
23
|
*
|
|
15
24
|
* Resource: the host's `serviceName` / `serviceVersion` merged with the SDK
|
|
16
25
|
* env detector (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`);
|
|
@@ -25,6 +34,7 @@
|
|
|
25
34
|
*/
|
|
26
35
|
import { context, propagation, metrics, trace } from "@opentelemetry/api";
|
|
27
36
|
import { createNoopLogger } from "./console-logger.js";
|
|
37
|
+
import { createHarnessSampler, DbosSpanProcessor } from "./otel-spans.js";
|
|
28
38
|
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
29
39
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
30
40
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
@@ -92,10 +102,11 @@ export function initOtel(options = {}) {
|
|
|
92
102
|
const spanProcessors = [];
|
|
93
103
|
if (endpoint) {
|
|
94
104
|
const base = endpoint.replace(/\/+$/, "");
|
|
95
|
-
spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({ url: `${base}/v1/traces` })));
|
|
105
|
+
spanProcessors.push(new DbosSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({ url: `${base}/v1/traces` }))));
|
|
96
106
|
}
|
|
97
107
|
const tracerProvider = new NodeTracerProvider({
|
|
98
108
|
resource,
|
|
109
|
+
sampler: createHarnessSampler(),
|
|
99
110
|
spanProcessors,
|
|
100
111
|
});
|
|
101
112
|
tracerProvider.register();
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsonSchema, tool as aiTool, } from "ai";
|
|
|
3
3
|
import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
|
|
4
4
|
import { stripNulCharacters } from "../input-sanitization.js";
|
|
5
5
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
6
|
+
import { ATTR_INFLEXA_TOOL_USE_ID, stableSpan } from "../lib/otel-spans.js";
|
|
6
7
|
import { hintForZodIssue, repairToolInput } from "../lib/zod-issues.js";
|
|
7
8
|
import { markInterruptedMessage, syntheticUserMessage } from "../memory/ai-sdk-message-storage.js";
|
|
8
9
|
import { stripUnansweredToolCalls } from "../memory/tool-call-integrity.js";
|
|
@@ -70,7 +71,13 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
70
71
|
session,
|
|
71
72
|
signal,
|
|
72
73
|
emit,
|
|
73
|
-
runStep: (name, fn) =>
|
|
74
|
+
runStep: (name, fn) => {
|
|
75
|
+
const stepName = `${formatStepName.tool(tu.toolName, tu.toolCallId)}:${name}`;
|
|
76
|
+
return runStep(stepName, () => {
|
|
77
|
+
stableSpan(stepName, `tool:${tu.toolName}:${name}`, { [ATTR_INFLEXA_TOOL_USE_ID]: tu.toolCallId });
|
|
78
|
+
return fn();
|
|
79
|
+
});
|
|
80
|
+
},
|
|
74
81
|
ask,
|
|
75
82
|
turnUsage,
|
|
76
83
|
});
|
|
@@ -540,7 +547,11 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
|
|
|
540
547
|
}
|
|
541
548
|
await Promise.all(stepTools.map(({ tu, idx }) => {
|
|
542
549
|
const startedAt = performance.now();
|
|
543
|
-
|
|
550
|
+
const stepName = toolStepName(tu.toolName, tu.toolCallId);
|
|
551
|
+
return runStep(stepName, () => {
|
|
552
|
+
stableSpan(stepName, `tool:${tu.toolName}`, { [ATTR_INFLEXA_TOOL_USE_ID]: tu.toolCallId });
|
|
553
|
+
return dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
|
|
554
|
+
}).then((settled) => {
|
|
544
555
|
durations[idx] = elapsedMs(startedAt);
|
|
545
556
|
const dispatched = readDispatchedStep(settled);
|
|
546
557
|
results[idx] = dispatched.result;
|
package/dist/prompts/planner.js
CHANGED
|
@@ -167,6 +167,12 @@ Every step carries a \`packages\` array — the packages its scripts import.
|
|
|
167
167
|
one exact version (\`"numpy==1.26.4"\`). Never a path, a URL, or a store
|
|
168
168
|
directory — a location is an installer detail, and validation refuses it.
|
|
169
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.
|
|
170
176
|
- The set is not a promise of completeness. The execution agent can still link a
|
|
171
177
|
missing package mid-step. Name what you know the step imports; do not pad the
|
|
172
178
|
list defensively.
|
|
@@ -42,5 +42,5 @@ export declare function sandboxOrientCorePromptFor(toolchainSource: ToolchainSou
|
|
|
42
42
|
* constant, thus the prompt stays byte-identical across the steps of one
|
|
43
43
|
* composition.
|
|
44
44
|
*/
|
|
45
|
-
export declare const sandboxPackageLinkPrompt = "# Linking Packages\n\nThe host keeps a pool of staged packages beside the farm of this analysis \u2014 the\npool is part of what the host staged before this container started. The\n`link_packages` tool links a staged package into your farm \u2014 live, with no\nrestart. A link adds no new bytes and downloads nothing; it connects your farm\nto a directory that is already on the mount. So a package your farm lacks is\nnot missing until the pool has answered:\n\n- After a failed import, and when `list_available_packages` reports a package\n absent, call `link_packages` with the module name verbatim.\n- `linked` and `present` mean the import works now \u2014 run it again.\n- `absent` is a real answer: the pool does not hold the package. When\n `acquisitionPossible` is true, report the package as missing so the host can\n acquire it. Do not retry the link, and do not attempt an install.\n- `collision` is
|
|
45
|
+
export declare const sandboxPackageLinkPrompt = "# Linking Packages\n\nThe host keeps a pool of staged packages beside the farm of this analysis \u2014 the\npool is part of what the host staged before this container started. The\n`link_packages` tool links a staged package into your farm \u2014 live, with no\nrestart. A link adds no new bytes and downloads nothing; it connects your farm\nto a directory that is already on the mount. So a package your farm lacks is\nnot missing until the pool has answered:\n\n- After a failed import, and when `list_available_packages` reports a package\n absent, call `link_packages` with the module name verbatim. Each entry is\n `[python:|r:]<name>[==<version>]`, and a bare name searches both tracks.\n- `linked` and `present` mean the import works now \u2014 run it again.\n- `absent` is a real answer: the pool does not hold the package. When\n `acquisitionPossible` is true, report the package as missing so the host can\n acquire it. Do not retry the link, and do not attempt an install.\n- `collision` means the request resolved to two store directories. When one\n name is held by both tracks, call `link_packages` again for that package\n with the prefixed form, `python:<name>` or `r:<name>`.\n- Drop the package only after that second call also refuses. A collision of two\n versions of one distribution is terminal at once: report it and continue\n without the package.\n- Report a package as missing only after `link_packages` answered `absent` or\n `unavailable` for it.\n";
|
|
46
46
|
export declare const sandboxAnalysisStepStandardsPrompt = "# Sandbox Analysis-Step Conventions\n\nThese apply to plannable analysis steps \u2014 agents that produce reproducible\nscript + output + figure artifacts. They do NOT apply to data profiling or\nreport building (those agents opt out via\n`appendAnalysisStepStandards: false`).\n\n## Language Policy\n\nPython is the DEFAULT language. Use R when:\n(a) No adequate Python equivalent (e.g. minfi, ANCOM-BC2, ChAMP).\n(b) The R implementation is significantly more mature.\n(c) The task explicitly requires an R-only package.\n\nChoose native R or rpy2 based on scope:\n- **Isolated R calls** in a Python pipeline \u2192 rpy2 bridge.\n Example: DESeq2 for one DE step, fgsea for enrichment.\n- **R-dominant pipeline** where most steps use R packages \u2192 write\n **native R scripts**. Do not wrap an entire R pipeline in rpy2.\n Examples: microbiome (DADA2 \u2192 phyloseq \u2192 vegan \u2192 ANCOM-BC2),\n DNA methylation (minfi \u2192 ChAMP \u2192 DMRcate), untargeted metabolomics\n (XCMS \u2192 CAMERA).\n\nNative R scripts follow the same standards as Python: structured logging\n(`message()`), named constants, documented functions. Save final outputs\nas CSV (and optionally AnnData) for cross-agent consumption.\n\n## Data Format Standards\n\n- **AnnData (.h5ad)** is the UNIVERSAL container for sample-by-feature\n data across ALL modalities \u2014 bulk RNA-seq, proteomics, metabolomics,\n microarray, methylation, single-cell. Samples in `.obs`, features in\n `.var`, values in `.X`, layers in `.layers`, embeddings in `.obsm`,\n unstructured results in `.uns`.\n- **MuData (.h5mu)** for multi-modal data (CITE-seq, Multiome,\n multi-omics integration).\n- Store sample metadata in `.obs`, feature annotations in `.var`. Do\n NOT write separate metadata CSV files unless explicitly needed.\n- **Intermediate analysis objects**: save as `.h5ad` or `.h5mu`. Do\n NOT save as `.rds` or `.pkl`. When R packages produce R objects\n (DESeqDataSet, phyloseq, SingleCellExperiment), convert key results\n back to AnnData or pandas.\n- **Tabular results**: CSV with human-readable column names\n (`log2_fold_change`, `adjusted_pvalue`, not `lfc`/`padj`).\n- **Genomic data**: VCF/BED/BAM are acceptable primary containers.\n Derived summary statistics go in AnnData or CSV.\n- **Dual-format output**: when producing modality-specific results,\n also output a generic CSV for downstream cross-cutting agents.\n\n## Figure Standards\n\n- **PNG at 300 DPI** for preview AND **PDF (vector)** for publication,\n same base filename with different extensions.\n- **Colorblind-safe palettes** \u2014 viridis family as default. Never use\n red-green as the sole distinguishing feature.\n- **Clean minimal themes**: matplotlib `seaborn-v0_8-whitegrid` or\n equivalent; ggplot2 `theme_classic()` or `theme_minimal()`.\n- **Complete labels**: title, axis labels with units, legend.\n- **Statistical annotations** where applicable (p-values, significance\n brackets, effect sizes).\n- No default matplotlib/ggplot styling, no cut-off labels, no\n overlapping text.\n\n## Directory Structure\n\nYour working directory has five subdirectories \u2014 write each kind of artifact\nto its own (paths are relative to your working directory):\n\n```\nscripts/ Analysis scripts (.py, .R)\noutput/ Results: CSV, AnnData (.h5ad), MuData (.h5mu), JSON\nfigures/ Visualizations: PNG (300 DPI) + PDF (vector), same base name\nlogs/ Execution logs from long-running commands\nnotebooks/ Reserved (currently unused)\n```\n\nPut data results in `output/`, not at the working-directory root. For\nlong-running commands, redirect stdout/stderr to a file in `logs/`:\n```python\nsubprocess.run(cmd, stdout=open(\"logs/deseq2_run.log\", \"w\"), stderr=subprocess.STDOUT)\n```\n\n## Code Quality\n\n**Functions** for any non-trivial logic. Each logical operation\n(load, QC, fit, plot) gets its own function with a docstring. Top-level\nscript flow should read like an outline \u2014 call functions, not inline\nlogic.\n\n```python\n# %% [markdown]\n# ## Differential Expression\n\n# %%\ndef run_deseq2(counts: pd.DataFrame, metadata: pd.DataFrame, design: str) -> pd.DataFrame:\n \"\"\"Run DESeq2 via rpy2 and return results as a DataFrame.\"\"\"\n ...\n\nde_results = run_deseq2(counts, metadata, \"~ condition + batch\")\n```\n\n**Structured logging**, not `print()`:\n\n- Python \u2014 `logging.getLogger()`:\n `logger.info(\"Loaded %d samples, %d genes\", n_samples, n_genes)`\n- R \u2014 `message()` / `warning()`:\n `message(sprintf(\"Loaded %d samples\", n_samples))`\n\nReserve `print()` / `cat()` for intentional data display in notebook\ncells only.\n\n**Validate at function boundaries**. Fail fast with clear messages.\nUse `try/except` only when recovery is meaningful.\n\n**Type hints** on Python function signatures. Add them \u2014 they document\nintent and catch mistakes early.\n\n**Named constants** for thresholds, cutoffs, parameters in the first\ncode cell:\n```python\n# %% Parameters\nRANDOM_SEED = 42\nFDR_THRESHOLD = 0.05\nMIN_LOG2FC = 1.0\n```\n\n**R specifics**: load packages at the top (never inside functions).\nUse `<-` for assignment. Use `message()` / `warning()` not `cat()` /\n`print()`.\n\n## Scripts\n\n- Start with a header comment (title + description).\n- Parameters go in the first code block.\n- Set random seeds for reproducibility.\n- Scripts must be independently re-runnable.\n- **Lint Python scripts with `ruff check <script.py>`** via\n `execute_command` before executing. `ruff` is pre-installed. This\n catches import errors, undefined names, and common bugs early.\n\n## Cleanup\n\nWhen a script fails, delete the failed script from `scripts/` and any\npartial output files before writing a corrected version. Only final\nworking scripts should remain. Use `list_files` to verify.\n\n## Interpretation & Literature Grounding\n\nGround findings in the research literature as you work \u2014 not as a\nfinal step:\n\n1. When you identify a significant finding (DE genes, enriched\n pathways, hub genes, quality issues), search PubMed with\n `pubmed({action:\"search\"})`.\n2. For relevant hits, call `pubmed({action:\"details\"})` for abstracts and\n PMIDs. Note short citations (e.g. \"Smith et al., Nature 2023\").\n3. Assess novelty per finding: **novel** (searched, nothing found),\n **confirmed** (directly supported), **partially confirmed**,\n **contradicted**, **expected** (domain knowledge, no citation\n needed).\n4. Document the search even when no references are found. \"Novel\"\n means you looked and found nothing \u2014 not that you didn't look.\n\nYour structured output captures this interpretation. It is your\nscientific assessment of what the results mean.\n\n## Analysis-Step Anti-Patterns\n\n- **Ignoring script errors.** Read the error, diagnose the root\n cause, fix the script. Do not move on.\n- **Placeholder or dummy output.** If an analysis cannot produce\n meaningful results, report the reason clearly instead of faking\n results.\n- **Monolithic scripts.** Keep scripts under 300 lines. Split into\n logical sections with markdown annotations.\n- **`print()` for logging.** Use `logging` (Python) or `message()`\n (R). `print()` is for intentional data display only.\n- **Inline logic without functions.** Wrap non-trivial operations in\n functions with docstrings and typed parameters.\n- **Magic numbers.** Define thresholds and cutoffs as named constants.\n";
|
|
@@ -312,13 +312,18 @@ to a directory that is already on the mount. So a package your farm lacks is
|
|
|
312
312
|
not missing until the pool has answered:
|
|
313
313
|
|
|
314
314
|
- After a failed import, and when \`list_available_packages\` reports a package
|
|
315
|
-
absent, call \`link_packages\` with the module name verbatim.
|
|
315
|
+
absent, call \`link_packages\` with the module name verbatim. Each entry is
|
|
316
|
+
\`[python:|r:]<name>[==<version>]\`, and a bare name searches both tracks.
|
|
316
317
|
- \`linked\` and \`present\` mean the import works now — run it again.
|
|
317
318
|
- \`absent\` is a real answer: the pool does not hold the package. When
|
|
318
319
|
\`acquisitionPossible\` is true, report the package as missing so the host can
|
|
319
320
|
acquire it. Do not retry the link, and do not attempt an install.
|
|
320
|
-
- \`collision\`
|
|
321
|
-
|
|
321
|
+
- \`collision\` means the request resolved to two store directories. When one
|
|
322
|
+
name is held by both tracks, call \`link_packages\` again for that package
|
|
323
|
+
with the prefixed form, \`python:<name>\` or \`r:<name>\`.
|
|
324
|
+
- Drop the package only after that second call also refuses. A collision of two
|
|
325
|
+
versions of one distribution is terminal at once: report it and continue
|
|
326
|
+
without the package.
|
|
322
327
|
- Report a package as missing only after \`link_packages\` answered \`absent\` or
|
|
323
328
|
\`unavailable\` for it.
|
|
324
329
|
`;
|
package/dist/runtime/dbos.d.ts
CHANGED
|
@@ -55,13 +55,14 @@ export interface DbosConfig {
|
|
|
55
55
|
* Attribute names follow the `dbos.*` semantic-convention layout
|
|
56
56
|
* (`otelAttributeFormat: "semconv"`).
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
58
|
+
* The harness TracerProvider shapes what the SDK emits (`lib/otel-spans.ts`):
|
|
59
|
+
* a workflow declared with `untracedWorkflow` is not recorded, a replayed
|
|
60
|
+
* `cached=true` span is dropped before export, and a step body that calls
|
|
61
|
+
* `stableSpan` is exported under a stable name with its id in an
|
|
62
|
+
* `inflexa.*` attribute. The step name DBOS records is unchanged.
|
|
63
|
+
*
|
|
64
|
+
* Known SDK behavior a host must plan for: a recovered workflow starts a
|
|
65
|
+
* new root trace, with no link to the trace of the original execution.
|
|
65
66
|
*/
|
|
66
67
|
readonly tracingEnabled?: boolean;
|
|
67
68
|
}
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
* varied between replays would desynchronise the recorded function-ID sequence.
|
|
56
56
|
*/
|
|
57
57
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
58
|
+
import { ATTR_INFLEXA_ATTEMPT, ATTR_INFLEXA_EXEC_ID, stableSpan } from "../lib/otel-spans.js";
|
|
58
59
|
import { digestBody } from "./digest.js";
|
|
59
60
|
import { signCallback, verifyCallback } from "./hmac.js";
|
|
60
61
|
import { createEscalationPolicy, probeLiveness, syntheticFailureReason, syntheticFailureResult } from "./liveness.js";
|
|
@@ -182,9 +183,11 @@ export async function awaitExecCallback(ref, execId, emit, deadlineMs, options =
|
|
|
182
183
|
/** Ask the sandbox directly. Returns null when the exec has not finished. */
|
|
183
184
|
const pull = async () => {
|
|
184
185
|
pullAttempt += 1;
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
const pullStepName = `sandbox.pull-exec-result.${execId}.${pullAttempt}`;
|
|
187
|
+
const pulled = await runStep(() => {
|
|
188
|
+
stableSpan(pullStepName, "sandbox.pull-exec-result", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: pullAttempt });
|
|
189
|
+
return pullExecResult(fetchImpl, ref, execId);
|
|
190
|
+
}, { name: pullStepName });
|
|
188
191
|
if (pulled.kind !== "completed")
|
|
189
192
|
return null;
|
|
190
193
|
// Identical treatment to a pushed completion: the bytes were signed by
|
|
@@ -341,9 +344,11 @@ export async function awaitExecPoll(ref, execId, emit, deadlineMs, options = {})
|
|
|
341
344
|
let probeAttempt = 0;
|
|
342
345
|
while (true) {
|
|
343
346
|
pollAttempt += 1;
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
const pollStepName = `sandbox.poll-exec-result.${execId}.${pollAttempt}`;
|
|
348
|
+
const polled = await runStep(() => {
|
|
349
|
+
stableSpan(pollStepName, "sandbox.poll-exec-result", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: pollAttempt });
|
|
350
|
+
return pollExecOnce(fetchImpl, ref, execId, cursor);
|
|
351
|
+
}, { name: pollStepName });
|
|
347
352
|
if (polled.kind === "ok") {
|
|
348
353
|
// Identical treatment to a pushed completion: the bytes were signed by
|
|
349
354
|
// the same secret, so a forgery is as fatal here as it is there.
|
|
@@ -385,9 +390,11 @@ export async function awaitExecPoll(ref, execId, emit, deadlineMs, options = {})
|
|
|
385
390
|
}
|
|
386
391
|
if (isAlive && escalation.onPoll(polled.kind === "ok" ? "ok" : "unavailable")) {
|
|
387
392
|
probeAttempt += 1;
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
393
|
+
const probeStepName = `sandbox.probe-liveness.${execId}.${probeAttempt}`;
|
|
394
|
+
const verdict = await runStep(() => {
|
|
395
|
+
stableSpan(probeStepName, "sandbox.probe-liveness", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: probeAttempt });
|
|
396
|
+
return probeLiveness(isAlive, ref);
|
|
397
|
+
}, { name: probeStepName });
|
|
391
398
|
// Only an observably dead machine fails the exec — the result lives
|
|
392
399
|
// in that machine and nowhere else, so it is unrecoverable and the
|
|
393
400
|
// synthetic failure races nothing. `alive` (live-but-slow exec,
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
20
20
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
21
|
+
import { untracedWorkflow } from "../lib/otel-spans.js";
|
|
21
22
|
const SWEEP_CRON = "0 */5 * * * *";
|
|
23
|
+
const SWEEP_WORKFLOW = "dbos-notifications-sweep";
|
|
22
24
|
const DELETE_BATCH_LIMIT = 10_000;
|
|
23
25
|
export async function sweepStaleNotifications(deps) {
|
|
24
26
|
const logger = (deps.logger ?? createNoopLogger()).named("notification-sweep");
|
|
@@ -68,9 +70,10 @@ export function registerNotificationSweep(deps) {
|
|
|
68
70
|
// a workflow registration. Register, then schedule the same callable.
|
|
69
71
|
const sweep = DBOS.registerWorkflow(async () => {
|
|
70
72
|
await sweepStaleNotifications({ deleteStale, logger: deps.logger });
|
|
71
|
-
}, { name:
|
|
73
|
+
}, { name: SWEEP_WORKFLOW });
|
|
74
|
+
untracedWorkflow(SWEEP_WORKFLOW);
|
|
72
75
|
DBOS.registerScheduled(sweep, {
|
|
73
|
-
name:
|
|
76
|
+
name: SWEEP_WORKFLOW,
|
|
74
77
|
crontab: SWEEP_CRON,
|
|
75
78
|
});
|
|
76
79
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity of a package, the query that asks for one, and the resolution
|
|
3
|
+
* between the two.
|
|
4
|
+
*
|
|
5
|
+
* A package name crosses the harness, the embedder, and the provisioner. As a
|
|
6
|
+
* bare `string` each site applies the rule that it believes, and two sites then
|
|
7
|
+
* disagree on whether `decoupleR` and `decoupler` are one package. This module
|
|
8
|
+
* is the one place that knows the rule.
|
|
9
|
+
*
|
|
10
|
+
* Two types carry the two ideas. A {@link PackageQuery} is what a person or an
|
|
11
|
+
* agent asked: a spelling, an optional track, an optional version. A
|
|
12
|
+
* {@link PackageIdentity} is the name that an ecosystem recognizes. The
|
|
13
|
+
* identity is opaque, thus a caller cannot invent one, and every identity comes
|
|
14
|
+
* from {@link pythonIdentity}, {@link rIdentity}, or {@link identityOf}.
|
|
15
|
+
*
|
|
16
|
+
* The PEP 503 fold lives in exactly one function of this file. The provisioner
|
|
17
|
+
* holds the Python twin, `images/sandbox-provisioner/package_identity.py`, and
|
|
18
|
+
* one fixture binds the two.
|
|
19
|
+
*/
|
|
20
|
+
import { type Result } from "neverthrow";
|
|
21
|
+
/** The two ecosystems that the pool holds. */
|
|
22
|
+
export type Track = "python" | "r";
|
|
23
|
+
/**
|
|
24
|
+
* What a person or an agent asked for. `spelling` is the name verbatim, as the
|
|
25
|
+
* caller wrote it — a remedy quotes it, and an R name is case-sensitive at
|
|
26
|
+
* `library()`. `track` names one ecosystem, and its absence searches both.
|
|
27
|
+
* `version` pins one exact version.
|
|
28
|
+
*
|
|
29
|
+
* A query is never a key. Two queries of one fold are two asks, because a
|
|
30
|
+
* caller that wrote `decoupleR` did not ask for `decoupler`.
|
|
31
|
+
*/
|
|
32
|
+
export type PackageQuery = {
|
|
33
|
+
readonly spelling: string;
|
|
34
|
+
readonly track?: Track;
|
|
35
|
+
readonly version?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The brand of an identity name. It has no runtime value: it exists so that a
|
|
39
|
+
* literal `{ track, name }` fails the typecheck outside this module, and thus
|
|
40
|
+
* every identity comes from a constructor that applied the rule of its track.
|
|
41
|
+
*/
|
|
42
|
+
declare const identityBrand: unique symbol;
|
|
43
|
+
/**
|
|
44
|
+
* The name that one ecosystem recognizes, under the track that holds it.
|
|
45
|
+
*
|
|
46
|
+
* The name of a Python identity is the PEP 503 fold of the spelling, because
|
|
47
|
+
* PEP 503 defines the equivalence of a distribution name. The name of an R
|
|
48
|
+
* identity is the DESCRIPTION spelling, verbatim, because `library()` is
|
|
49
|
+
* case-sensitive. Two identities are equal when their keys are equal.
|
|
50
|
+
*/
|
|
51
|
+
export type PackageIdentity = {
|
|
52
|
+
readonly track: Track;
|
|
53
|
+
readonly name: string & {
|
|
54
|
+
readonly [identityBrand]: true;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The Python identity of a spelling. The name folds, because PEP 503 makes
|
|
59
|
+
* `PyYAML`, `pyyaml`, and `py_yaml` one distribution.
|
|
60
|
+
*
|
|
61
|
+
* @param spelling The distribution name, as the caller spells it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pythonIdentity(spelling: string): PackageIdentity;
|
|
64
|
+
/**
|
|
65
|
+
* The R identity of a name. The name keeps its spelling, because `library()`
|
|
66
|
+
* is case-sensitive and `GO.db` loads under that exact name.
|
|
67
|
+
*
|
|
68
|
+
* @param name The DESCRIPTION `Package` value.
|
|
69
|
+
*/
|
|
70
|
+
export declare function rIdentity(name: string): PackageIdentity;
|
|
71
|
+
/**
|
|
72
|
+
* The identity of a name that an emitter already minted — a graph node, a lock
|
|
73
|
+
* row. The dispatch is safe over such a name, because the fold is idempotent.
|
|
74
|
+
*
|
|
75
|
+
* @param track The ecosystem that holds the name.
|
|
76
|
+
* @param name The name as the emitter wrote it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function identityOf(track: Track, name: string): PackageIdentity;
|
|
79
|
+
/**
|
|
80
|
+
* The key of an identity: `<track>:<name>`. Two identities are equal when
|
|
81
|
+
* their keys are equal, thus a map of identities keys on this string.
|
|
82
|
+
*/
|
|
83
|
+
export declare function identityKey(identity: PackageIdentity): string;
|
|
84
|
+
/**
|
|
85
|
+
* The identity that a key names, or `undefined` when the string is not a key.
|
|
86
|
+
*
|
|
87
|
+
* The FIRST colon splits the two halves. A track name holds no colon, and
|
|
88
|
+
* neither a PEP 503 name nor an R name can hold one, thus `r:GO.db` gives the
|
|
89
|
+
* R identity `GO.db`, dot and all. The name rides through {@link identityOf},
|
|
90
|
+
* and the fold is idempotent, thus a key that {@link identityKey} wrote comes
|
|
91
|
+
* back as the identity that wrote it.
|
|
92
|
+
*
|
|
93
|
+
* The key format is minted here, thus it is read here: a reader that splits
|
|
94
|
+
* the string itself owns a second copy of the format.
|
|
95
|
+
*
|
|
96
|
+
* @param key A `<track>:<name>` string, as {@link identityKey} writes it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function parseIdentityKey(key: string): PackageIdentity | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* The store address of an identity: the PEP 503 fold of its name, for both
|
|
101
|
+
* tracks. A store directory is an address and not an identity, thus two
|
|
102
|
+
* identities can share one address — `r:decoupleR` and `python:decoupler`
|
|
103
|
+
* both address as `decoupler`. The pin marker inside the directory carries
|
|
104
|
+
* the identity.
|
|
105
|
+
*/
|
|
106
|
+
export declare function identityAddress(identity: PackageIdentity): string;
|
|
107
|
+
/**
|
|
108
|
+
* Why an entry is not a query.
|
|
109
|
+
*
|
|
110
|
+
* - `empty` — the entry names no package.
|
|
111
|
+
* - `location` — a path, a URL, or a store directory. A location is an
|
|
112
|
+
* installer detail, and the pool resolves names.
|
|
113
|
+
* - `unknown_prefix` — a `<word>:` prefix that is neither `python:` nor `r:`.
|
|
114
|
+
* Unrefused, such a prefix rides into the pool as part of the name.
|
|
115
|
+
* - `unsupported_specifier` — a specifier that is not `==`. Unrefused, a range
|
|
116
|
+
* such as `numpy>=1.26` becomes a package name.
|
|
117
|
+
*/
|
|
118
|
+
export type ParseQueryError = {
|
|
119
|
+
readonly type: "empty";
|
|
120
|
+
} | {
|
|
121
|
+
readonly type: "location";
|
|
122
|
+
readonly entry: string;
|
|
123
|
+
} | {
|
|
124
|
+
readonly type: "unknown_prefix";
|
|
125
|
+
readonly prefix: string;
|
|
126
|
+
readonly entry: string;
|
|
127
|
+
} | {
|
|
128
|
+
readonly type: "unsupported_specifier";
|
|
129
|
+
readonly specifier: string;
|
|
130
|
+
readonly entry: string;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Read one entry of the grammar `[python:|r:]<spelling>[==<version>]`.
|
|
134
|
+
*
|
|
135
|
+
* The entry trims once, and the parts keep what the trim left. Every reader of
|
|
136
|
+
* the grammar in the harness calls this function: a second parser would
|
|
137
|
+
* disagree with this one, and the plan validation that passed would then meet
|
|
138
|
+
* a link pass that refuses.
|
|
139
|
+
*
|
|
140
|
+
* @param entry One package entry, as a plan, a tool call, or a host wrote it.
|
|
141
|
+
*/
|
|
142
|
+
export declare function parseQuery(entry: string): Result<PackageQuery, ParseQueryError>;
|
|
143
|
+
/**
|
|
144
|
+
* Write a query in the grammar. The prefix rides only beside a track, and
|
|
145
|
+
* `==<version>` only beside a version. For every query,
|
|
146
|
+
* `parseQuery(formatQuery(query))` gives that query again.
|
|
147
|
+
*/
|
|
148
|
+
export declare function formatQuery(query: PackageQuery): string;
|
|
149
|
+
/**
|
|
150
|
+
* What a pool answers about the identities that it holds. Two reads are
|
|
151
|
+
* enough for the ladder, thus a caller builds this over a graph, a lock, or a
|
|
152
|
+
* census without exposing its own shape.
|
|
153
|
+
*/
|
|
154
|
+
export type PoolIndex = {
|
|
155
|
+
/** Whether the pool holds this exact identity. */
|
|
156
|
+
has: (identity: PackageIdentity) => boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Each R identity of the pool whose address is this fold. The suggestion
|
|
159
|
+
* of an unknown query comes from here: a person who wrote `seurat` meant
|
|
160
|
+
* the R package `Seurat`.
|
|
161
|
+
*/
|
|
162
|
+
rIdentitiesFoldingTo: (fold: string) => readonly PackageIdentity[];
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* What a query resolves to.
|
|
166
|
+
*
|
|
167
|
+
* - `resolved` — one identity, thus the caller reads the pool under it.
|
|
168
|
+
* - `ambiguous` — one spelling that both tracks hold. The caller refuses, and
|
|
169
|
+
* the remedy is a prefixed query.
|
|
170
|
+
* - `unknown` — the pool holds no identity of this query. `suggestion` names
|
|
171
|
+
* the R identity that the caller probably meant.
|
|
172
|
+
*/
|
|
173
|
+
export type QueryResolution = {
|
|
174
|
+
readonly kind: "resolved";
|
|
175
|
+
readonly identity: PackageIdentity;
|
|
176
|
+
} | {
|
|
177
|
+
readonly kind: "ambiguous";
|
|
178
|
+
readonly python: PackageIdentity;
|
|
179
|
+
readonly r: PackageIdentity;
|
|
180
|
+
} | {
|
|
181
|
+
readonly kind: "unknown";
|
|
182
|
+
readonly suggestion?: PackageIdentity;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Resolve one query against a pool.
|
|
186
|
+
*
|
|
187
|
+
* The version takes no part, because the caller holds the versions: the graph
|
|
188
|
+
* orders them, and this function answers which package the query names.
|
|
189
|
+
*
|
|
190
|
+
* The ladder, in order:
|
|
191
|
+
*
|
|
192
|
+
* 1. The query names a track: the identity of that track, when the pool holds
|
|
193
|
+
* it. Otherwise unknown, with the R suggestion when the track is `r`.
|
|
194
|
+
* 2. The pool holds the R identity of the spelling and the Python identity of
|
|
195
|
+
* its fold: the R identity when the spelling differs from its fold, because
|
|
196
|
+
* an uppercase letter or a dot is evidence of an R spelling. Otherwise
|
|
197
|
+
* ambiguous.
|
|
198
|
+
* 3. The pool holds the R identity: the R identity.
|
|
199
|
+
* 4. The pool holds the Python identity: the Python identity.
|
|
200
|
+
* 5. Exactly one R identity of the pool folds to the fold of the spelling:
|
|
201
|
+
* unknown, with that identity as the suggestion.
|
|
202
|
+
* 6. Otherwise: unknown.
|
|
203
|
+
*
|
|
204
|
+
* A silent Python-first pick is a fault: it sends `decoupleR` to the Python
|
|
205
|
+
* `decoupler`, and the R package never links.
|
|
206
|
+
*/
|
|
207
|
+
export declare function resolveQuery(query: PackageQuery, pool: PoolIndex): QueryResolution;
|
|
208
|
+
export {};
|