@inflexa-ai/harness 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/sandbox/__fixtures__/deps.js +6 -0
- package/dist/agents/sandbox/data-profiler.js +5 -4
- package/dist/agents/sandbox/shared.js +11 -0
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/data-profile-orientation.js +30 -1
- package/dist/app/data-profile-policy.d.ts +33 -7
- package/dist/app/data-profile-policy.js +31 -9
- package/dist/contracts/data-profile.d.ts +166 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/execution/input-signature.d.ts +33 -0
- package/dist/execution/input-signature.js +0 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -0
- package/dist/input-scan/coverage.d.ts +22 -0
- package/dist/input-scan/coverage.js +85 -0
- package/dist/input-scan/enrich.d.ts +41 -0
- package/dist/input-scan/enrich.js +227 -0
- package/dist/input-scan/formats.d.ts +41 -0
- package/dist/input-scan/formats.js +279 -0
- package/dist/input-scan/index.d.ts +12 -0
- package/dist/input-scan/index.js +11 -0
- package/dist/input-scan/scan.d.ts +50 -0
- package/dist/input-scan/scan.js +182 -0
- package/dist/input-scan/shapes.d.ts +46 -0
- package/dist/input-scan/shapes.js +0 -0
- package/dist/input-scan/types.d.ts +172 -0
- package/dist/input-scan/types.js +12 -0
- package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
- package/dist/prompts/sandbox/data-profiler.js +159 -275
- package/dist/provenance/exec-frame.js +9 -3
- package/dist/sandbox/client.d.ts +7 -0
- package/dist/sandbox/create-sandbox.js +1 -0
- package/dist/sandbox/docker-client.d.ts +1 -0
- package/dist/sandbox/docker-client.js +20 -15
- package/dist/sandbox/k8s-client.d.ts +6 -5
- package/dist/sandbox/k8s-client.js +55 -39
- package/dist/sandbox/reaper.d.ts +24 -13
- package/dist/sandbox/reaper.js +47 -14
- package/dist/sandbox/types.d.ts +6 -5
- package/dist/schemas/data-profile-schemas.d.ts +62 -0
- package/dist/schemas/data-profile-schemas.js +68 -2
- package/dist/state/data-profile.d.ts +7 -88
- package/dist/state/index.d.ts +1 -1
- package/dist/tasks/data-profile-activity.d.ts +8 -0
- package/dist/tasks/data-profile-activity.js +1 -0
- package/dist/tasks/data-profile-index.d.ts +45 -0
- package/dist/tasks/data-profile-index.js +134 -0
- package/dist/tasks/data-profile.d.ts +8 -23
- package/dist/tasks/data-profile.js +150 -117
- package/dist/tools/research/generate-plan.js +1 -1
- package/dist/tools/research/inspect-data-profile.d.ts +30 -4
- package/dist/tools/research/inspect-data-profile.js +66 -10
- package/dist/tools/workspace/scan-inputs.d.ts +38 -0
- package/dist/tools/workspace/scan-inputs.js +61 -0
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/tools/workspace/workspace-search.js +12 -9
- package/dist/workspace/filesystem.d.ts +27 -0
- package/dist/workspace/filesystem.js +27 -0
- package/package.json +1 -1
|
@@ -30,6 +30,9 @@ export function makeFakeSandboxClient() {
|
|
|
30
30
|
async isAlive() {
|
|
31
31
|
return { alive: true, oomKilled: false };
|
|
32
32
|
},
|
|
33
|
+
async isAliveById() {
|
|
34
|
+
return { alive: true, oomKilled: false };
|
|
35
|
+
},
|
|
33
36
|
async teardown() { },
|
|
34
37
|
async teardownById() { },
|
|
35
38
|
async listManagedSandboxes() {
|
|
@@ -42,6 +45,9 @@ export function makeFakeWorkspaceFs() {
|
|
|
42
45
|
readFile() {
|
|
43
46
|
return okAsync({ kind: "ok", content: Buffer.alloc(0), truncated: false });
|
|
44
47
|
},
|
|
48
|
+
readBytes() {
|
|
49
|
+
return okAsync({ kind: "ok", bytes: Buffer.alloc(0) });
|
|
50
|
+
},
|
|
45
51
|
list() {
|
|
46
52
|
return okAsync({ kind: "ok", entries: [] });
|
|
47
53
|
},
|
|
@@ -10,10 +10,11 @@ export const meta = {
|
|
|
10
10
|
capabilities: ["data profiling", "literature review", "methodology research", "analysis planning", "experimental design analysis"],
|
|
11
11
|
suitableFor: ["bulk-rna-seq", "single-cell", "proteomics", "metabolomics", "genomics", "transcriptomics", "chemical-structures", "compound-screening"],
|
|
12
12
|
skills: [],
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
13
|
+
// `scanInputs` is the profiler's own instrument: the workflow injects one scan into
|
|
14
|
+
// its briefing, and the tool is how it re-scans a subtree it wants to group
|
|
15
|
+
// differently. Declared rather than bolted on, so the resolved roster stays
|
|
16
|
+
// `meta.tools` plus the always-on set.
|
|
17
|
+
tools: [...BASE_SANDBOX_TOOLS, "scanInputs"],
|
|
17
18
|
defaultMaxSteps: 85,
|
|
18
19
|
plannable: false,
|
|
19
20
|
};
|
|
@@ -30,6 +30,7 @@ import { chemblTool, genePreclinicalProfileTool, lookupAnnotationTool, openTarge
|
|
|
30
30
|
import { createNcbiTools, createChemDbTools } from "../../tools/bio/keys.js";
|
|
31
31
|
// Workspace read surface.
|
|
32
32
|
import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createWorkspaceSearchTool } from "../../tools/workspace/index.js";
|
|
33
|
+
import { createScanInputsTool } from "../../tools/workspace/scan-inputs.js";
|
|
33
34
|
// Workspace mutate surface.
|
|
34
35
|
import { createEditFileTool, createExecuteCommandTool, createWorkspaceMutator, createWriteFileTool } from "../../tools/workspace/index.js";
|
|
35
36
|
// Skills (declared per agent via meta.skills).
|
|
@@ -60,6 +61,16 @@ function resolveSandboxTools(deps, tools) {
|
|
|
60
61
|
const registry = {
|
|
61
62
|
listAvailablePackages: createListAvailablePackagesTool({ ...(deps.packagesFile ? { packagesFile: deps.packagesFile } : {}) }),
|
|
62
63
|
listAvailableRefs: createListAvailableRefsTool({ ...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}) }),
|
|
64
|
+
scanInputs: createScanInputsTool({
|
|
65
|
+
workspaceFs: deps.workspaceFs,
|
|
66
|
+
analysisId: deps.step.analysisId,
|
|
67
|
+
sandboxClient: deps.sandboxClient,
|
|
68
|
+
sandbox: deps.step.sandbox,
|
|
69
|
+
workflowId: deps.step.workflowId,
|
|
70
|
+
stepId: deps.step.stepId,
|
|
71
|
+
nextFunctionId: deps.step.nextFunctionId,
|
|
72
|
+
deadlineMs: deps.step.deadlineMs,
|
|
73
|
+
}),
|
|
63
74
|
resolveLibraryId: resolveLibraryIdTool,
|
|
64
75
|
queryDocs: queryDocsTool,
|
|
65
76
|
inspectRun: createInspectRunTool(deps.pool),
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* in the tool's `perSource` report; the rest of the call still answers.
|
|
13
13
|
*/
|
|
14
14
|
/** Closed allowlist of tools any sandbox agent may declare. */
|
|
15
|
-
export type SandboxToolName = "listAvailablePackages" | "listAvailableRefs" | "resolveLibraryId" | "queryDocs" | "inspectRun" | "pubmed" | "resolve_citation" | "searchGene" | "lookupAnnotation" | "searchInteractions" | "chembl" | "pubchem" | "opentargets" | "geneDiseaseEvidence" | "drugGeneInteractions" | "genePreclinicalProfile" | "searchFaers" | "searchClinicalTrials" | "searchGeoDatasets" | "targetSafety" | "comptox";
|
|
15
|
+
export type SandboxToolName = "listAvailablePackages" | "listAvailableRefs" | "scanInputs" | "resolveLibraryId" | "queryDocs" | "inspectRun" | "pubmed" | "resolve_citation" | "searchGene" | "lookupAnnotation" | "searchInteractions" | "chembl" | "pubchem" | "opentargets" | "geneDiseaseEvidence" | "drugGeneInteractions" | "genePreclinicalProfile" | "searchFaers" | "searchClinicalTrials" | "searchGeoDatasets" | "targetSafety" | "comptox";
|
|
16
16
|
/** Planner-facing metadata + tool allowlist for one sandbox agent. */
|
|
17
17
|
export interface AgentMeta {
|
|
18
18
|
/** Stable agent id — also the key in the catalog and the `AgentDefinition.id`. */
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
export const DATA_PROFILE_ORIENTATION_MAX_CHARS = 1200;
|
|
19
19
|
/** Files listed before the tail is elided (the count still reports the total). */
|
|
20
20
|
const MAX_FILES = 8;
|
|
21
|
+
/** Kinds listed before the tail is elided. A dataset with more is described by its first few. */
|
|
22
|
+
const MAX_KINDS = 6;
|
|
23
|
+
const MAX_KIND_DESCRIPTION_CHARS = 90;
|
|
21
24
|
/** Quality concerns listed — the profiler orders them, so these are the top ones. */
|
|
22
25
|
const MAX_CONCERNS = 3;
|
|
23
26
|
const MAX_DESIGN_CHARS = 200;
|
|
@@ -105,9 +108,35 @@ export function buildDataProfileOrientation(result, maxChars = DATA_PROFILE_ORIE
|
|
|
105
108
|
const more = concerns.length > shown.length ? ` (+${concerns.length - shown.length} more)` : "";
|
|
106
109
|
lines.push(`Concerns: ${shown.join("; ")}${more}`);
|
|
107
110
|
}
|
|
111
|
+
// Kinds come first because they are what the dataset IS at this budget: four lines
|
|
112
|
+
// describing 3513 files, where the per-file list could only show the first eight of
|
|
113
|
+
// them and imply the rest away.
|
|
114
|
+
const kinds = result.kinds ?? [];
|
|
115
|
+
if (kinds.length > 0) {
|
|
116
|
+
const shown = kinds.slice(0, MAX_KINDS);
|
|
117
|
+
const datasetFiles = kinds.reduce((sum, kind) => sum + kind.count, 0);
|
|
118
|
+
const header = shown.length < kinds.length
|
|
119
|
+
? `Kinds (${shown.length} of ${kinds.length}, ${datasetFiles} files):`
|
|
120
|
+
: `Kinds (${kinds.length}, ${datasetFiles} files):`;
|
|
121
|
+
lines.push(header);
|
|
122
|
+
for (const kind of shown) {
|
|
123
|
+
const facts = [`${kind.count}x`, present(kind.format)].filter((f) => f !== undefined).join(", ");
|
|
124
|
+
lines.push(`- ${kind.name} (${facts}) — ${clip(kind.memberRepresents, MAX_KIND_DESCRIPTION_CHARS)}`);
|
|
125
|
+
}
|
|
126
|
+
const axes = result.axes ?? [];
|
|
127
|
+
if (axes.length > 0) {
|
|
128
|
+
lines.push(`Axes: ${axes.map((axis) => `${axis.label} (${axis.cardinality})`).join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
if (result.coverage && result.coverage.unmatched > 0) {
|
|
131
|
+
lines.push(`Coverage: ${result.coverage.matched} of ${result.coverage.total} files match a kind`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
108
134
|
if (result.files.length > 0) {
|
|
109
135
|
const shown = result.files.slice(0, MAX_FILES);
|
|
110
|
-
|
|
136
|
+
// With kinds present these are the individually notable files — the metadata
|
|
137
|
+
// sheet, the README, the outlier — not a sample of the dataset.
|
|
138
|
+
const label = kinds.length > 0 ? "Notable files" : "Files";
|
|
139
|
+
const header = shown.length < result.files.length ? `${label} (${shown.length} of ${result.files.length}):` : `${label} (${result.files.length}):`;
|
|
111
140
|
lines.push(header);
|
|
112
141
|
for (const file of shown) {
|
|
113
142
|
lines.push(`- ${file.path} — ${clip(file.description, MAX_FILE_DESCRIPTION_CHARS)}${formatFileFacts(file)}`);
|
|
@@ -5,13 +5,37 @@
|
|
|
5
5
|
* — staging stays embedder-side per the data-profile-init spec, and the atomic running→expired
|
|
6
6
|
* flip stays an SQL guard (`expireStaleDataProfile`), not part of this decision.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
import type { DataProfileInputSignature, DataProfileLifecycleStatus } from "../contracts/data-profile.js";
|
|
9
|
+
export type { DataProfileLifecycleStatus };
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The analysis's inputs as they are NOW. A caller holding sizes and mtimes supplies
|
|
12
|
+
* the signature; one holding only the ledger's seeded ids supplies just those, and the
|
|
13
|
+
* comparison degrades honestly to what those ids can settle.
|
|
13
14
|
*/
|
|
14
|
-
export
|
|
15
|
+
export interface CurrentInputSet {
|
|
16
|
+
readonly fileIds: readonly string[];
|
|
17
|
+
readonly signature?: DataProfileInputSignature;
|
|
18
|
+
}
|
|
19
|
+
/** The comparand a profile snapshot carries. Which one is present depends on its era. */
|
|
20
|
+
export interface ProfiledInputSnapshot {
|
|
21
|
+
readonly inputSignature?: DataProfileInputSignature;
|
|
22
|
+
readonly inputFileIds?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A completed profile is stale when the analysis's current input set no longer matches
|
|
26
|
+
* the set the profile actually covered.
|
|
27
|
+
*
|
|
28
|
+
* The signature is preferred where both sides carry one: it covers size and mtime as
|
|
29
|
+
* well as identity, so an in-place edit is drift too. A snapshot predating the
|
|
30
|
+
* signature falls back to its identity list rather than being treated as drift, since
|
|
31
|
+
* that list answers the same question for the set it covered. A snapshot carrying
|
|
32
|
+
* NEITHER is drift: nothing establishes what it covered, and one re-profile heals it —
|
|
33
|
+
* exactly how a wholly absent `result` is already treated.
|
|
34
|
+
*
|
|
35
|
+
* An empty current set is never stale: there is nothing to compare, and an analysis
|
|
36
|
+
* whose inputs were removed is handled by clearing the profile, not by re-profiling it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isDataProfileStale(current: CurrentInputSet, profiled: ProfiledInputSnapshot | null | undefined): boolean;
|
|
15
39
|
export type DataProfileAction = {
|
|
16
40
|
readonly kind: "none";
|
|
17
41
|
} | {
|
|
@@ -21,8 +45,10 @@ export type DataProfileAction = {
|
|
|
21
45
|
};
|
|
22
46
|
export interface DataProfileDecisionInput {
|
|
23
47
|
readonly status: DataProfileLifecycleStatus;
|
|
24
|
-
|
|
25
|
-
readonly
|
|
48
|
+
/** The analysis's inputs as they are now — the seeded ids, plus a signature where the caller has one. */
|
|
49
|
+
readonly current: CurrentInputSet;
|
|
50
|
+
/** The comparand the stored profile carries, or null when it holds none. */
|
|
51
|
+
readonly profiled: ProfiledInputSnapshot | null;
|
|
26
52
|
}
|
|
27
53
|
/**
|
|
28
54
|
* Decide the next profiling action from a (post-expire) status:
|
|
@@ -6,17 +6,39 @@
|
|
|
6
6
|
* flip stays an SQL guard (`expireStaleDataProfile`), not part of this decision.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
* A completed profile is stale when the analysis's
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* A completed profile is stale when the analysis's current input set no longer matches
|
|
10
|
+
* the set the profile actually covered.
|
|
11
|
+
*
|
|
12
|
+
* The signature is preferred where both sides carry one: it covers size and mtime as
|
|
13
|
+
* well as identity, so an in-place edit is drift too. A snapshot predating the
|
|
14
|
+
* signature falls back to its identity list rather than being treated as drift, since
|
|
15
|
+
* that list answers the same question for the set it covered. A snapshot carrying
|
|
16
|
+
* NEITHER is drift: nothing establishes what it covered, and one re-profile heals it —
|
|
17
|
+
* exactly how a wholly absent `result` is already treated.
|
|
18
|
+
*
|
|
19
|
+
* An empty current set is never stale: there is nothing to compare, and an analysis
|
|
20
|
+
* whose inputs were removed is handled by clearing the profile, not by re-profiling it.
|
|
12
21
|
*/
|
|
13
|
-
export function isDataProfileStale(
|
|
14
|
-
if (
|
|
22
|
+
export function isDataProfileStale(current, profiled) {
|
|
23
|
+
if (current.fileIds.length === 0 && current.signature === undefined)
|
|
15
24
|
return false;
|
|
16
|
-
if (
|
|
25
|
+
if (!profiled)
|
|
17
26
|
return true;
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
if (profiled.inputSignature) {
|
|
28
|
+
// A caller with no signature of its own can still compare counts, which catches
|
|
29
|
+
// an added or removed file. It cannot catch a same-count swap, and claiming
|
|
30
|
+
// otherwise would be a verdict the comparand does not support.
|
|
31
|
+
if (!current.signature)
|
|
32
|
+
return profiled.inputSignature.count !== current.fileIds.length;
|
|
33
|
+
return profiled.inputSignature.digest !== current.signature.digest || profiled.inputSignature.count !== current.signature.count;
|
|
34
|
+
}
|
|
35
|
+
if (profiled.inputFileIds) {
|
|
36
|
+
if (current.fileIds.length !== profiled.inputFileIds.length)
|
|
37
|
+
return true;
|
|
38
|
+
const covered = new Set(profiled.inputFileIds);
|
|
39
|
+
return current.fileIds.some((id) => !covered.has(id));
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
20
42
|
}
|
|
21
43
|
/**
|
|
22
44
|
* Decide the next profiling action from a (post-expire) status:
|
|
@@ -30,7 +52,7 @@ export function isDataProfileStale(seedInputFileIds, profiledInputFileIds) {
|
|
|
30
52
|
export function decideDataProfileAction(input) {
|
|
31
53
|
if (input.status === "pending")
|
|
32
54
|
return { kind: "trigger" };
|
|
33
|
-
if (input.status === "completed" && isDataProfileStale(input.
|
|
55
|
+
if (input.status === "completed" && isDataProfileStale(input.current, input.profiled)) {
|
|
34
56
|
return { kind: "retrigger" };
|
|
35
57
|
}
|
|
36
58
|
return { kind: "none" };
|
|
@@ -24,3 +24,169 @@
|
|
|
24
24
|
* reporting the profile as an unnamed run — a failure with no red test anywhere.
|
|
25
25
|
*/
|
|
26
26
|
export declare const DATA_PROFILE_RUN_LITERAL: "data-profile";
|
|
27
|
+
/**
|
|
28
|
+
* Where an extracted subject fact came from, so a consumer can judge how much to trust
|
|
29
|
+
* it. Ordered most-direct first in the profiler's own guidance.
|
|
30
|
+
*/
|
|
31
|
+
export type DataProfileSubjectSource = "metadata" | "document" | "filename" | "user-context" | "inferred";
|
|
32
|
+
/** The subject organism, with its provenance and the profiler's confidence in it. */
|
|
33
|
+
export interface DataProfileOrganism {
|
|
34
|
+
scientificName: string;
|
|
35
|
+
/** NCBI Taxonomy ID as a string, e.g. `"9606"`. */
|
|
36
|
+
taxonId: string;
|
|
37
|
+
source: DataProfileSubjectSource;
|
|
38
|
+
confidence: "high" | "medium" | "low";
|
|
39
|
+
notes?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One profiled file's persisted record.
|
|
43
|
+
*
|
|
44
|
+
* `path` + `description` are the original pair every snapshot carries; everything below
|
|
45
|
+
* them is optional on read, because a snapshot written before the record was widened
|
|
46
|
+
* carries only the pair.
|
|
47
|
+
*/
|
|
48
|
+
export interface DataProfileFile {
|
|
49
|
+
path: string;
|
|
50
|
+
description: string;
|
|
51
|
+
/** Semantic data type (count-matrix, variants, clinical-metadata, document, …). */
|
|
52
|
+
dataType?: string;
|
|
53
|
+
/** File format (CSV, h5ad, VCF, BAM, …). */
|
|
54
|
+
format?: string;
|
|
55
|
+
rows?: number | null;
|
|
56
|
+
cols?: number | null;
|
|
57
|
+
/** Searchable labels the profiler attached. */
|
|
58
|
+
tags?: string[];
|
|
59
|
+
/** Quality issues specific to this file. */
|
|
60
|
+
warnings?: string[];
|
|
61
|
+
/** Flat, identity-establishing facts (sparsity, delimiter, normalizationState, …). */
|
|
62
|
+
metrics?: Record<string, string | number | boolean>;
|
|
63
|
+
}
|
|
64
|
+
/** Dataset-wide quality findings the profiler recorded. */
|
|
65
|
+
export interface DataProfileQualityAssessment {
|
|
66
|
+
concerns: string[];
|
|
67
|
+
strengths: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One repeating set of files — the dataset's structure, as opposed to its contents.
|
|
71
|
+
*
|
|
72
|
+
* A kind is a claim about MEANING, made by the profiler agent: the deterministic input
|
|
73
|
+
* scan establishes that filenames differ only at one position, and cannot establish that
|
|
74
|
+
* the files are the same sort of thing. A singleton is a kind of `count` 1, so a
|
|
75
|
+
* two-file analysis is two kinds and needs no special case.
|
|
76
|
+
*/
|
|
77
|
+
export interface DataProfileKind {
|
|
78
|
+
name: string;
|
|
79
|
+
/** What ONE member represents. The grouping decision, stated apart from the description. */
|
|
80
|
+
memberRepresents: string;
|
|
81
|
+
description: string;
|
|
82
|
+
count: number;
|
|
83
|
+
/** Glob the members match, relative to the analysis root. Coverage is computed from it. */
|
|
84
|
+
pathPattern: string;
|
|
85
|
+
format?: string;
|
|
86
|
+
/** Labels of the `axes` that vary across this set's members. */
|
|
87
|
+
axisLabels?: string[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* What varies across a kind's members — the experimental design, as far as it is
|
|
91
|
+
* observable. The scan reports that a filename position varies and which values it
|
|
92
|
+
* takes; what the variation IS is the agent's label.
|
|
93
|
+
*/
|
|
94
|
+
export interface DataProfileAxis {
|
|
95
|
+
label: string;
|
|
96
|
+
cardinality: number;
|
|
97
|
+
exampleValues?: string[];
|
|
98
|
+
description?: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The comparand identifying WHICH files a profile covered and whether the SAME BYTES
|
|
102
|
+
* were profiled.
|
|
103
|
+
*
|
|
104
|
+
* `digest` is a stable hash over the staged inputs' identities, sizes, and mtimes in a
|
|
105
|
+
* canonical order, so it depends on the set and not on enumeration order. It excludes
|
|
106
|
+
* content hashes deliberately: enumerating those would mean reading every input in full
|
|
107
|
+
* on every parity check. An edit preserving both byte length and mtime is therefore not
|
|
108
|
+
* detected — bounded, and documented.
|
|
109
|
+
*/
|
|
110
|
+
export interface DataProfileInputSignature {
|
|
111
|
+
count: number;
|
|
112
|
+
digest: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* How much of the scanned tree the profile's kinds describe.
|
|
116
|
+
*
|
|
117
|
+
* Computed deterministically by matching the submitted kind patterns against the scanned
|
|
118
|
+
* file set — never self-reported. Low coverage is a distinct failure from drift: a
|
|
119
|
+
* profile that classified a fraction of its inputs used to carry the same comparand as
|
|
120
|
+
* one that classified all of them, so it read as complete and fresh. Coverage does NOT by
|
|
121
|
+
* itself drive a re-profile, because some input sets legitimately resist classification.
|
|
122
|
+
*/
|
|
123
|
+
export interface DataProfileCoverage {
|
|
124
|
+
matched: number;
|
|
125
|
+
unmatched: number;
|
|
126
|
+
total: number;
|
|
127
|
+
unmatchedSample?: string[];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One profiled input file's legacy drift signature. `fileId` is a path identity (two
|
|
131
|
+
* files at the same path share it regardless of content), so `size` + `mtimeMs` are what
|
|
132
|
+
* let a consumer notice the bytes behind a path changed. Superseded by
|
|
133
|
+
* {@link DataProfileInputSignature}, and still readable on older rows.
|
|
134
|
+
*/
|
|
135
|
+
export interface DataProfileInputFile {
|
|
136
|
+
fileId: string;
|
|
137
|
+
size: number;
|
|
138
|
+
mtimeMs: number;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The profile snapshot stored in `cortex_analysis_state.data_profile_result`.
|
|
142
|
+
*
|
|
143
|
+
* It carries the profiler's full finding rather than a summary of it: this row is the
|
|
144
|
+
* profile's only durable home, so a field dropped on the way in is not "summarized
|
|
145
|
+
* away", it is destroyed, and the next reader can recover it only by re-reading the raw
|
|
146
|
+
* inputs.
|
|
147
|
+
*
|
|
148
|
+
* It does NOT carry a record per input file. The workspace filesystem is the
|
|
149
|
+
* authoritative file list — listing, grep, and the vector index all read the live tree —
|
|
150
|
+
* so a copy here would be a stale duplicate on a row detoasted by every reader,
|
|
151
|
+
* including the planner's, which reads a few hundred characters of it. `files` holds the
|
|
152
|
+
* individually notable inputs; `kinds` holds the dataset's structure.
|
|
153
|
+
*/
|
|
154
|
+
export interface DataProfileResult {
|
|
155
|
+
summary: string;
|
|
156
|
+
/** Individually described files — notable singletons, not the dataset's contents. */
|
|
157
|
+
files: DataProfileFile[];
|
|
158
|
+
/** The repeating sets the tree was grouped into. Absent on a pre-kinds snapshot. */
|
|
159
|
+
kinds?: DataProfileKind[];
|
|
160
|
+
/** What varies across those sets. Absent for the same reason as `kinds`. */
|
|
161
|
+
axes?: DataProfileAxis[];
|
|
162
|
+
/** The drift comparand. Preferred over `inputFileIds`, which it replaces. */
|
|
163
|
+
inputSignature?: DataProfileInputSignature;
|
|
164
|
+
/** How much of the scanned tree the kinds describe. Absent on pre-coverage snapshots. */
|
|
165
|
+
coverage?: DataProfileCoverage;
|
|
166
|
+
/** The legacy identity-list comparand, on rows written before the signature existed. */
|
|
167
|
+
inputFileIds?: string[];
|
|
168
|
+
/** The legacy per-file drift signature, on rows of the same era. */
|
|
169
|
+
inputFiles?: DataProfileInputFile[];
|
|
170
|
+
/** ISO 8601 timestamp of profile completion. */
|
|
171
|
+
profiledAt: string;
|
|
172
|
+
/** Scientific domain (transcriptomics, cheminformatics, clinical, …). */
|
|
173
|
+
domain?: string;
|
|
174
|
+
/** Specific subtype within the domain (bulk-rna-seq, single-cell, LC-MS/MS, …). */
|
|
175
|
+
subtype?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Explicit `null` is a real, distinct value — the profiler looked and no input
|
|
178
|
+
* identified an organism — as opposed to absent, which means the snapshot predates
|
|
179
|
+
* the field.
|
|
180
|
+
*/
|
|
181
|
+
organism?: DataProfileOrganism | null;
|
|
182
|
+
tissue?: string | null;
|
|
183
|
+
cellType?: string | null;
|
|
184
|
+
condition?: string | null;
|
|
185
|
+
/** Public dataset accessions found in any input (GSE/SRP/PRJNA/E-MTAB/phs/EGAS). */
|
|
186
|
+
accessions?: string[];
|
|
187
|
+
/** Conditions, groups, comparisons, replicates, pairing. */
|
|
188
|
+
experimentalDesign?: string;
|
|
189
|
+
qualityAssessment?: DataProfileQualityAssessment;
|
|
190
|
+
}
|
|
191
|
+
/** The profile's lifecycle, as the ledger records it. */
|
|
192
|
+
export type DataProfileLifecycleStatus = "pending" | "running" | "completed" | "failed";
|
|
@@ -6,6 +6,7 @@ export type { PlanStep, PresentationContent, PresentationPart, PlanPart, RunCard
|
|
|
6
6
|
export type { EventSource, TextDeltaEvent, ToolStartedEvent, ToolFinishedEvent, FinishEvent, ChatErrorEvent, CortexChatEvent } from "./chat-events.js";
|
|
7
7
|
export type { TokenUsageRollup } from "./usage.js";
|
|
8
8
|
export { DATA_PROFILE_RUN_LITERAL } from "./data-profile.js";
|
|
9
|
+
export type { DataProfileAxis, DataProfileCoverage, DataProfileFile, DataProfileInputFile, DataProfileInputSignature, DataProfileKind, DataProfileLifecycleStatus, DataProfileOrganism, DataProfileQualityAssessment, DataProfileResult, DataProfileSubjectSource, } from "./data-profile.js";
|
|
9
10
|
export type { TextPart, ToolCallPart, CortexPart, CortexMessage } from "./message.js";
|
|
10
11
|
export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./part-registry.js";
|
|
11
12
|
export type { CortexChatPartType, PartDescriptor, PartEmitter, PartConsumer } from "./part-registry.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The input signature — the fixed-width comparand a completed profile persists in
|
|
3
|
+
* place of a per-file identity list.
|
|
4
|
+
*
|
|
5
|
+
* `inputFileIds` had exactly one consumer, the staleness comparison, so a set of
|
|
6
|
+
* several thousand identifiers was persisted, detoasted on every read of the row, and
|
|
7
|
+
* compared element-wise to answer a question 40 bytes answer. The signature covers
|
|
8
|
+
* `size` and `mtimeMs` as well, so it also notices that the bytes behind a path
|
|
9
|
+
* changed — a `fileId` is a path identity and survives an in-place edit unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Order independence is a property of the digest, not of the caller: entries are
|
|
12
|
+
* sorted canonically before hashing, so two enumerations of the same set agree.
|
|
13
|
+
*/
|
|
14
|
+
import type { DataProfileInputSignature } from "../state/data-profile.js";
|
|
15
|
+
import type { StagedInput } from "./staged-input.js";
|
|
16
|
+
/** The fields a signature covers. A manifest entry satisfies it structurally. */
|
|
17
|
+
export interface SignableInput {
|
|
18
|
+
readonly fileId: string;
|
|
19
|
+
readonly size: number;
|
|
20
|
+
readonly mtimeMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Digest a staged input set.
|
|
24
|
+
*
|
|
25
|
+
* A workflow input persisted before `StagedInput.mtimeMs` existed deserializes without
|
|
26
|
+
* it despite the compile-time type; such an entry contributes `-` in the mtime field
|
|
27
|
+
* rather than omitting the signature entirely. The comparison still notices an added,
|
|
28
|
+
* removed, or resized file, and one re-profile under the current code restores the full
|
|
29
|
+
* signature.
|
|
30
|
+
*/
|
|
31
|
+
export declare function computeInputSignature(inputs: readonly SignableInput[]): DataProfileInputSignature;
|
|
32
|
+
/** Convenience over the workflow's own manifest type. */
|
|
33
|
+
export declare function signatureOfStagedInputs(inputs: readonly StagedInput[]): DataProfileInputSignature;
|
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -115,10 +115,13 @@ export { DATA_PROFILE_RUN_LITERAL } from "./contracts/data-profile.js";
|
|
|
115
115
|
export { initCortexState } from "./state/init.js";
|
|
116
116
|
export { upsertAnalysis } from "./state/analyses.js";
|
|
117
117
|
export { clearDataProfile, loadDataProfileStatus, tryRetryDataProfile, reconcileOrphanedDataProfile } from "./state/data-profile.js";
|
|
118
|
-
export type { DataProfileInputFile, DataProfileResult,
|
|
118
|
+
export type { DataProfileAxis, DataProfileCoverage, DataProfileFile, DataProfileInputFile, DataProfileInputSignature, DataProfileKind, DataProfileLifecycleStatus, DataProfileOrganism, DataProfileQualityAssessment, DataProfileResult, DataProfileSubjectSource, } from "./contracts/data-profile.js";
|
|
119
|
+
export type { DataProfileStatus } from "./state/data-profile.js";
|
|
119
120
|
export { createAnalysisPurge } from "./state/purge-analysis.js";
|
|
120
121
|
export type { AnalysisPurge, AnalysisPurgeDeps, AnalysisPurgeOutcome } from "./state/purge-analysis.js";
|
|
121
122
|
export type { StagedInput } from "./execution/staged-input.js";
|
|
123
|
+
export { computeInputSignature } from "./execution/input-signature.js";
|
|
124
|
+
export type { SignableInput } from "./execution/input-signature.js";
|
|
122
125
|
export type { ResolveWorkspaceRoot } from "./workspace/paths.js";
|
|
123
126
|
export { createSandboxClient } from "./sandbox/create-sandbox.js";
|
|
124
127
|
export type { CreateSandboxClientConfig, SandboxBackendConfig } from "./sandbox/create-sandbox.js";
|
package/dist/index.js
CHANGED
|
@@ -205,6 +205,11 @@ export { clearDataProfile, loadDataProfileStatus, tryRetryDataProfile, reconcile
|
|
|
205
205
|
// Analysis reclamation — `purgeAnalysis` reclaims an analysis's whole Postgres
|
|
206
206
|
// footprint from its id alone; workspace files stay the embedder's to dispose of.
|
|
207
207
|
export { createAnalysisPurge } from "./state/purge-analysis.js";
|
|
208
|
+
// The input signature a completed profile persists. Exported because the embedder owns
|
|
209
|
+
// the OTHER side of the comparison — it enumerates the current input set — and a second
|
|
210
|
+
// implementation of the digest would silently disagree with the one on the row, which is
|
|
211
|
+
// the failure a fixed-width comparand exists to prevent.
|
|
212
|
+
export { computeInputSignature } from "./execution/input-signature.js";
|
|
208
213
|
export { createSandboxClient } from "./sandbox/create-sandbox.js";
|
|
209
214
|
export { MachineBudgetSchema, ResourceLimitsSchema, ResourcePolicySchema, ResourceSpecSchema, parseResourcePolicy } from "./config/resource-limits.js";
|
|
210
215
|
export { createWorkspaceFilesystem } from "./workspace/filesystem.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile coverage — how much of the scanned tree the agent's kinds actually describe.
|
|
3
|
+
*
|
|
4
|
+
* Measured against the SUBMITTED kinds, never against the scan's own shapes. Computed
|
|
5
|
+
* from the shapes it would report only whether the scan grouped its own observations,
|
|
6
|
+
* which is always yes; measured against the kinds it is a real check, and it is the
|
|
7
|
+
* check that was missing when a profile covering 49 of 3513 files read as complete and
|
|
8
|
+
* fresh.
|
|
9
|
+
*
|
|
10
|
+
* Deterministic by construction: matching declared patterns against a known file set
|
|
11
|
+
* requires no judgement, and a coverage figure a model self-reported would not be a
|
|
12
|
+
* check at all.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProfileCoverage {
|
|
15
|
+
/** Files matching at least one declared kind pattern. */
|
|
16
|
+
readonly matched: number;
|
|
17
|
+
readonly unmatched: number;
|
|
18
|
+
readonly total: number;
|
|
19
|
+
readonly unmatchedSample: string[];
|
|
20
|
+
}
|
|
21
|
+
/** Coverage of a scanned file set by a set of declared kind patterns. */
|
|
22
|
+
export declare function computeCoverage(paths: readonly string[], patterns: readonly string[]): ProfileCoverage;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile coverage — how much of the scanned tree the agent's kinds actually describe.
|
|
3
|
+
*
|
|
4
|
+
* Measured against the SUBMITTED kinds, never against the scan's own shapes. Computed
|
|
5
|
+
* from the shapes it would report only whether the scan grouped its own observations,
|
|
6
|
+
* which is always yes; measured against the kinds it is a real check, and it is the
|
|
7
|
+
* check that was missing when a profile covering 49 of 3513 files read as complete and
|
|
8
|
+
* fresh.
|
|
9
|
+
*
|
|
10
|
+
* Deterministic by construction: matching declared patterns against a known file set
|
|
11
|
+
* requires no judgement, and a coverage figure a model self-reported would not be a
|
|
12
|
+
* check at all.
|
|
13
|
+
*/
|
|
14
|
+
/** Unmatched paths carried on the snapshot, so a shortfall names examples rather than a bare count. */
|
|
15
|
+
const MAX_UNMATCHED_SAMPLE = 10;
|
|
16
|
+
/**
|
|
17
|
+
* Compile a kind's path pattern to a matcher.
|
|
18
|
+
*
|
|
19
|
+
* `**` crosses directory separators, `*` and `?` do not, and `<0>`-style placeholders —
|
|
20
|
+
* the syntax the manifest renders variable positions in, which an agent may reasonably
|
|
21
|
+
* copy — match one token. Everything else is literal.
|
|
22
|
+
*/
|
|
23
|
+
function compilePattern(pattern) {
|
|
24
|
+
let out = "";
|
|
25
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
26
|
+
const char = pattern[i];
|
|
27
|
+
if (char === "*") {
|
|
28
|
+
if (pattern[i + 1] === "*") {
|
|
29
|
+
out += ".*";
|
|
30
|
+
i++;
|
|
31
|
+
// `**/` should also match zero directories, so the slash is optional.
|
|
32
|
+
if (pattern[i + 1] === "/")
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
out += "[^/]*";
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (char === "?") {
|
|
40
|
+
out += "[^/]";
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (char === "<") {
|
|
44
|
+
const close = pattern.indexOf(">", i);
|
|
45
|
+
if (close > i) {
|
|
46
|
+
out += "[^/]*";
|
|
47
|
+
i = close;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
out += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
}
|
|
53
|
+
return new RegExp(`^${out}$`);
|
|
54
|
+
}
|
|
55
|
+
function normalize(path) {
|
|
56
|
+
return path.replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+/g, "/");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether a path falls under a pattern. A pattern naming a directory covers everything
|
|
60
|
+
* beneath it — agents write `data/inputs/vcf` as often as `data/inputs/vcf/*.vcf.gz`,
|
|
61
|
+
* and reading the first as a zero-file kind would report a coverage failure that is an
|
|
62
|
+
* artefact of notation.
|
|
63
|
+
*/
|
|
64
|
+
function matches(path, pattern, compiled) {
|
|
65
|
+
if (compiled.test(path))
|
|
66
|
+
return true;
|
|
67
|
+
const bare = pattern.replace(/\/+$/, "");
|
|
68
|
+
return bare.length > 0 && !/[*?<]/.test(bare) && path.startsWith(`${bare}/`);
|
|
69
|
+
}
|
|
70
|
+
/** Coverage of a scanned file set by a set of declared kind patterns. */
|
|
71
|
+
export function computeCoverage(paths, patterns) {
|
|
72
|
+
const compiled = patterns.map((pattern) => ({ pattern, regex: compilePattern(pattern) }));
|
|
73
|
+
const unmatchedSample = [];
|
|
74
|
+
let matched = 0;
|
|
75
|
+
for (const raw of paths) {
|
|
76
|
+
const path = normalize(raw);
|
|
77
|
+
if (compiled.some(({ pattern, regex }) => matches(path, pattern, regex))) {
|
|
78
|
+
matched++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (unmatchedSample.length < MAX_UNMATCHED_SAMPLE)
|
|
82
|
+
unmatchedSample.push(path);
|
|
83
|
+
}
|
|
84
|
+
return { matched, unmatched: paths.length - matched, total: paths.length, unmatchedSample };
|
|
85
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-shape header readout — the one part of the scan that needs the sandbox.
|
|
3
|
+
*
|
|
4
|
+
* A bounded prefix read compared against a magic table runs on the host; a DECODER
|
|
5
|
+
* over user-supplied bytes does not. zlib, HDF5, and PDF parsers in the long-lived
|
|
6
|
+
* multi-tenant harness process are exactly the exposure the ephemeral sandbox exists
|
|
7
|
+
* to contain, so every decode below runs there.
|
|
8
|
+
*
|
|
9
|
+
* Cost is O(shapes), not O(files): shapes are observed from names and sizes alone, so
|
|
10
|
+
* enrichment decodes a bounded number of members per shape — roughly four files for
|
|
11
|
+
* the 3513-file tree that motivated this capability. One exec covers every shape, so
|
|
12
|
+
* even the round trip does not scale with the tree.
|
|
13
|
+
*
|
|
14
|
+
* Nothing ships in `sandbox-base` for this: the decoder is a Python program passed on
|
|
15
|
+
* the command line through the existing `runSandboxExec` path, so the capability
|
|
16
|
+
* releases on the harness's own path rather than on the image's manual one.
|
|
17
|
+
*/
|
|
18
|
+
import type { EmitFn } from "../tools/define-tool.js";
|
|
19
|
+
import type { SandboxClient } from "../sandbox/client.js";
|
|
20
|
+
import type { SandboxRef } from "../sandbox/types.js";
|
|
21
|
+
import type { FileShape } from "./types.js";
|
|
22
|
+
/** Members decoded per shape. One is enough to characterise a set whose names already match. */
|
|
23
|
+
export declare const MEMBERS_DECODED_PER_SHAPE = 1;
|
|
24
|
+
export interface EnrichShapesArgs {
|
|
25
|
+
readonly shapes: readonly FileShape[];
|
|
26
|
+
readonly sandboxClient: SandboxClient;
|
|
27
|
+
readonly sandbox: SandboxRef;
|
|
28
|
+
/** Absolute in-sandbox path of the analysis root (`/{analysisId}`). */
|
|
29
|
+
readonly mountRoot: string;
|
|
30
|
+
readonly execId: string;
|
|
31
|
+
readonly deadlineMs: number;
|
|
32
|
+
readonly emit: EmitFn;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Decode one member per shape and attach the readout.
|
|
36
|
+
*
|
|
37
|
+
* A shape whose decode failed keeps its `unavailable` note rather than losing the
|
|
38
|
+
* shape: the readout is enrichment, and a manifest without it still carries every
|
|
39
|
+
* structural observation the agent's grouping rests on.
|
|
40
|
+
*/
|
|
41
|
+
export declare function enrichShapes(args: EnrichShapesArgs): Promise<FileShape[]>;
|