@inflexa-ai/harness 0.28.1 → 0.29.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/app/data-profile-orientation.d.ts +52 -27
- package/dist/app/data-profile-orientation.js +331 -92
- package/dist/app/data-profile-view.d.ts +70 -0
- package/dist/app/data-profile-view.js +123 -0
- package/dist/app/synthesize-run.js +1 -1
- package/dist/contracts/data-profile.d.ts +276 -25
- package/dist/contracts/index.d.ts +3 -1
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/profile-vocabulary.d.ts +103 -0
- package/dist/contracts/profile-vocabulary.js +449 -0
- package/dist/contracts/target-dossier.d.ts +21 -21
- package/dist/execution/input-signature.d.ts +16 -3
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/post-step-pipeline.js +1 -1
- package/dist/execution/staged-input.d.ts +4 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.js +7 -0
- package/dist/input-scan/clustering.d.ts +63 -0
- package/dist/input-scan/clustering.js +208 -0
- package/dist/input-scan/companions.d.ts +30 -0
- package/dist/input-scan/companions.js +138 -0
- package/dist/input-scan/decoder/container_readout.py +171 -0
- package/dist/input-scan/detect-sets.d.ts +24 -0
- package/dist/input-scan/detect-sets.js +279 -0
- package/dist/input-scan/enrich.d.ts +62 -19
- package/dist/input-scan/enrich.js +132 -187
- package/dist/input-scan/formats.d.ts +13 -5
- package/dist/input-scan/formats.js +13 -5
- package/dist/input-scan/index.d.ts +10 -4
- package/dist/input-scan/index.js +7 -4
- package/dist/input-scan/markers.d.ts +21 -0
- package/dist/input-scan/markers.js +89 -0
- package/dist/input-scan/menu.d.ts +127 -0
- package/dist/input-scan/menu.js +0 -0
- package/dist/input-scan/quarantine.d.ts +17 -0
- package/dist/input-scan/quarantine.js +69 -0
- package/dist/input-scan/readout-budget.d.ts +32 -0
- package/dist/input-scan/readout-budget.js +61 -0
- package/dist/input-scan/readout.d.ts +33 -0
- package/dist/input-scan/readout.js +336 -0
- package/dist/input-scan/scan.d.ts +5 -4
- package/dist/input-scan/scan.js +5 -5
- package/dist/input-scan/set-types.d.ts +209 -0
- package/dist/input-scan/set-types.js +15 -0
- package/dist/input-scan/templates.d.ts +93 -0
- package/dist/input-scan/templates.js +369 -0
- package/dist/input-scan/tokens.d.ts +94 -0
- package/dist/input-scan/tokens.js +267 -0
- package/dist/input-scan/tuning.d.ts +55 -0
- package/dist/input-scan/tuning.js +55 -0
- package/dist/input-scan/types.d.ts +2 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +8 -6
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +3 -2
- package/dist/prompts/sandbox/data-profiler.d.ts +10 -1
- package/dist/prompts/sandbox/data-profiler.js +197 -53
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +10 -7
- package/dist/schemas/data-profile-schemas.d.ts +943 -80
- package/dist/schemas/data-profile-schemas.js +300 -115
- package/dist/state/data-profile.d.ts +1 -1
- package/dist/state/index.d.ts +1 -1
- package/dist/state/vector-store.d.ts +13 -0
- package/dist/state/vector-store.js +8 -0
- package/dist/tasks/data-profile-absorb.d.ts +78 -0
- package/dist/tasks/data-profile-absorb.js +285 -0
- package/dist/tasks/data-profile-index.d.ts +34 -25
- package/dist/tasks/data-profile-index.js +160 -59
- package/dist/tasks/data-profile-resolve.d.ts +72 -0
- package/dist/tasks/data-profile-resolve.js +639 -0
- package/dist/tasks/data-profile.d.ts +16 -5
- package/dist/tasks/data-profile.js +270 -131
- package/dist/tools/research/inspect-data-profile.d.ts +48 -22
- package/dist/tools/research/inspect-data-profile.js +73 -54
- package/dist/tools/workspace/scan-inputs.d.ts +4 -4
- package/dist/tools/workspace/scan-inputs.js +13 -14
- package/dist/tools/workspace/workspace-search.d.ts +11 -2
- package/dist/tools/workspace/workspace-search.js +44 -16
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +21 -21
- package/package.json +5 -3
- package/dist/input-scan/coverage.d.ts +0 -22
- package/dist/input-scan/coverage.js +0 -85
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
* **There is no data-profile file.** The profiler's `runs/data-profile/` scratch tree
|
|
9
9
|
* is deleted when profiling completes; the profile's only durable home is the
|
|
10
10
|
* `cortex_analysis_state` row this tool reads. That makes this the single authoritative
|
|
11
|
-
* source for dataset facts
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* source for dataset facts — the identity fields, the resolved groups with their slots,
|
|
12
|
+
* the dimensions with their observations, and the partition census — and it is why the
|
|
13
|
+
* record is served in full rather than summarized: an agent that cannot pull a fact here
|
|
14
|
+
* has no fallback but to re-derive it from the raw bytes.
|
|
14
15
|
*
|
|
15
|
-
* Bounded by construction. Like `inspect_run`, the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Bounded by construction. Like `inspect_run`, the member scope is explicitly paged and
|
|
17
|
+
* always reports `total` and `hasMore`: an elided tail is a fact the model can see and
|
|
18
|
+
* act on, never a silent truncation. The annotated-member count and the dataset's file
|
|
19
|
+
* count are reported as two distinct figures, because a profile annotating eight members
|
|
20
|
+
* of several thousand files is correct and a single number would read as a tiny dataset.
|
|
18
21
|
*
|
|
19
22
|
* Lifecycle states are data variants, not errors (the `defineTool` contract): a missing,
|
|
20
23
|
* in-flight, failed, or stale profile is an ordinary, expected outcome the model must be
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
*/
|
|
29
32
|
import { ok } from "neverthrow";
|
|
30
33
|
import { z } from "zod";
|
|
34
|
+
import { profileCaveats, profileDatasetFileCount, profileFileRecords, profileGroups } from "../../app/data-profile-view.js";
|
|
31
35
|
import { scopeResource } from "../../auth/types.js";
|
|
32
36
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
33
37
|
import { loadDataProfileStatus } from "../../state/index.js";
|
|
@@ -37,31 +41,19 @@ import { defineTool } from "../define-tool.js";
|
|
|
37
41
|
const DEFAULT_PAGE_SIZE = 20;
|
|
38
42
|
/** Ceiling on `pageSize` — a page is a context-window budget, not a dump. */
|
|
39
43
|
const MAX_PAGE_SIZE = 100;
|
|
40
|
-
/**
|
|
41
|
-
* How many files the dataset holds, as far as the snapshot can establish it.
|
|
42
|
-
*
|
|
43
|
-
* Coverage counts the scanned tree, so it is the authority where present. Summed kind
|
|
44
|
-
* counts are the fallback. A snapshot with neither returns `null` rather than the
|
|
45
|
-
* described-file count, which would be a dataset size the row cannot support.
|
|
46
|
-
*/
|
|
47
|
-
function datasetFileCount(result) {
|
|
48
|
-
if (result.coverage)
|
|
49
|
-
return result.coverage.total;
|
|
50
|
-
if (result.kinds && result.kinds.length > 0)
|
|
51
|
-
return result.kinds.reduce((sum, kind) => sum + kind.count, 0);
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
44
|
/** Serve each per-file record with a path that resolves in the frame of the caller. */
|
|
55
45
|
function rootFilePaths(files, analysisId) {
|
|
56
46
|
return files.map((file) => ({ ...file, path: toAnalysisRootPath(analysisId, file.path) }));
|
|
57
47
|
}
|
|
58
48
|
/**
|
|
59
|
-
* Serve each
|
|
49
|
+
* Serve each group with a display pattern that resolves in the frame of the caller.
|
|
60
50
|
*
|
|
61
|
-
* A
|
|
62
|
-
*
|
|
63
|
-
* only re-roots the pattern.
|
|
51
|
+
* A pattern is a path with varying segments, thus it takes the same projection as a file
|
|
52
|
+
* path. Its placeholders survive it: the projection only re-roots the pattern.
|
|
64
53
|
*/
|
|
54
|
+
function rootGroupPatterns(groups, analysisId) {
|
|
55
|
+
return groups.map((group) => ({ ...group, displayPattern: toAnalysisRootPath(analysisId, group.displayPattern) }));
|
|
56
|
+
}
|
|
65
57
|
function rootKindPatterns(kinds, analysisId) {
|
|
66
58
|
return kinds.map((kind) => ({ ...kind, pathPattern: toAnalysisRootPath(analysisId, kind.pathPattern) }));
|
|
67
59
|
}
|
|
@@ -103,20 +95,21 @@ export function createInspectDataProfileTool(pool) {
|
|
|
103
95
|
id: "inspect_data_profile",
|
|
104
96
|
description: "Read this analysis's data profile — the AUTHORITATIVE record of what the input dataset is: " +
|
|
105
97
|
"organism and taxon id, scientific domain and subtype, tissue, cell type, condition, experimental design, " +
|
|
106
|
-
"dataset-wide
|
|
107
|
-
"
|
|
108
|
-
"the files described individually. " +
|
|
98
|
+
"dataset-wide caveats, public accessions, the GROUPS of file the dataset is made of, the DIMENSIONS that vary " +
|
|
99
|
+
"across it with the evidence behind each, and the file census (kept, unclassified, quarantined). " +
|
|
109
100
|
"There is NO data-profile file in the workspace — this tool is the only way to read it. Do not search for one, " +
|
|
110
101
|
"and do not rediscover these facts by listing or reading the raw input files. " +
|
|
111
102
|
"Call it before you reason about the data (planning, writing analysis code, interpreting results). " +
|
|
112
|
-
"scope:'overview' (the default) returns the dataset-level orientation
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
103
|
+
"scope:'overview' (the default) returns the dataset-level orientation plus the partition accounting — how many " +
|
|
104
|
+
"files were kept, how many landed in no group, how many were quarantined — and how many members are described " +
|
|
105
|
+
"individually. That last figure and the dataset's file count differ on purpose: a large dataset is described by " +
|
|
106
|
+
"its groups, not file by file. " +
|
|
107
|
+
"scope:'groups' returns the groups with their derived counts, display patterns, and slots, plus the dimensions " +
|
|
108
|
+
"with their observations, reconciliations, and probe outcomes — this is where a dataset of thousands of files is " +
|
|
109
|
+
"described. A profile written under the previous model returns its kinds and axes, labelled as such. " +
|
|
110
|
+
"scope:'files' returns the individually annotated members, paged: page (1-based, default 1) and pageSize (default 20, max 100), " +
|
|
118
111
|
"always with the true total and hasMore, so you can see exactly what you have not read yet. " +
|
|
119
|
-
"For the paths of files no record describes, list the workspace tree. " +
|
|
112
|
+
"For the paths of files no record describes, use scope:'groups' for the structure and list the workspace tree for the paths. " +
|
|
120
113
|
"The state field says what you got: 'ready'; 'stale' (a profile is returned but may not describe the current " +
|
|
121
114
|
"inputs — staleReason says why); 'pending' (profiling is still running); 'failed'; or 'absent' (never profiled, " +
|
|
122
115
|
"or the analysis has no input files). " +
|
|
@@ -126,19 +119,19 @@ export function createInspectDataProfileTool(pool) {
|
|
|
126
119
|
"needs re-running rather than diagnosing the data itself.",
|
|
127
120
|
inputSchema: z.object({
|
|
128
121
|
scope: z
|
|
129
|
-
.enum(["overview", "
|
|
122
|
+
.enum(["overview", "groups", "files"])
|
|
130
123
|
.optional()
|
|
131
|
-
.describe("'overview' (default): dataset-level facts,
|
|
132
|
-
"'
|
|
133
|
-
"'files': paged records for the individually
|
|
134
|
-
page: z.number().int().min(1).optional().describe("1-based page of
|
|
124
|
+
.describe("'overview' (default): dataset-level facts, the partition accounting, the annotated-member count, and the dataset file count. " +
|
|
125
|
+
"'groups': the groups the dataset is made of, with their slots, and the dimensions that vary across it. " +
|
|
126
|
+
"'files': paged records for the individually annotated members."),
|
|
127
|
+
page: z.number().int().min(1).optional().describe("1-based page of member records. Only used when scope is 'files'. Default 1."),
|
|
135
128
|
pageSize: z
|
|
136
129
|
.number()
|
|
137
130
|
.int()
|
|
138
131
|
.min(1)
|
|
139
132
|
.max(MAX_PAGE_SIZE)
|
|
140
133
|
.optional()
|
|
141
|
-
.describe(`
|
|
134
|
+
.describe(`Member records per page. Only used when scope is 'files'. Default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE}.`),
|
|
142
135
|
}),
|
|
143
136
|
// `execute` defaults both fields internally, and the hook applies the same
|
|
144
137
|
// defaults. Without them the ordinary call, which names no field at all,
|
|
@@ -160,8 +153,8 @@ export function createInspectDataProfileTool(pool) {
|
|
|
160
153
|
if (status.status === "failed") {
|
|
161
154
|
// A failure that DID leave a prior profile on the row never reaches
|
|
162
155
|
// here — it is served as `stale`, where `stalenessReasons` names the
|
|
163
|
-
// failed re-profile
|
|
164
|
-
//
|
|
156
|
+
// failed re-profile. This branch is the case with no prior profile to
|
|
157
|
+
// serve at all.
|
|
165
158
|
return ok({
|
|
166
159
|
state: "failed",
|
|
167
160
|
error: status.error,
|
|
@@ -188,14 +181,16 @@ export function createInspectDataProfileTool(pool) {
|
|
|
188
181
|
? { state: "stale", staleReason: reasons.join("; "), profiledAt: result.profiledAt }
|
|
189
182
|
: { state: "ready", profiledAt: result.profiledAt };
|
|
190
183
|
const scope = input.scope ?? "overview";
|
|
184
|
+
const describedFiles = profileFileRecords(result);
|
|
185
|
+
const groups = profileGroups(result);
|
|
191
186
|
if (scope === "overview") {
|
|
192
|
-
const dataset =
|
|
193
|
-
const described =
|
|
187
|
+
const dataset = profileDatasetFileCount(result);
|
|
188
|
+
const described = describedFiles.length;
|
|
194
189
|
const structureNote = dataset === null
|
|
195
190
|
? "This profile predates the dataset-structure record, so only the individually described files are known; " +
|
|
196
191
|
"list the workspace tree for the rest."
|
|
197
192
|
: dataset !== described
|
|
198
|
-
? `${described} of ${dataset} files are described individually. Call scope:'
|
|
193
|
+
? `${described} of ${dataset} files are described individually. Call scope:'groups' for what the rest are, ` +
|
|
199
194
|
"and list the workspace tree for their paths."
|
|
200
195
|
: undefined;
|
|
201
196
|
return ok({
|
|
@@ -209,40 +204,63 @@ export function createInspectDataProfileTool(pool) {
|
|
|
209
204
|
cellType: result.cellType,
|
|
210
205
|
condition: result.condition,
|
|
211
206
|
experimentalDesign: result.experimentalDesign,
|
|
212
|
-
|
|
207
|
+
caveats: profileCaveats(result),
|
|
213
208
|
accessions: result.accessions,
|
|
214
209
|
describedFileCount: described,
|
|
215
210
|
datasetFileCount: dataset,
|
|
216
|
-
|
|
211
|
+
// Presence, not truthiness: a completed profile over a tree the scan kept
|
|
212
|
+
// nothing of carries `groups: []`, and "zero groups" is a finding — reading
|
|
213
|
+
// it as "no structure record" would report the analysis unprofiled.
|
|
214
|
+
groupCount: result.groups !== undefined || result.kinds !== undefined ? groups.length : null,
|
|
217
215
|
...(structureNote ? { structureNote } : {}),
|
|
216
|
+
...(result.partition ? { partition: result.partition } : {}),
|
|
218
217
|
...(result.coverage ? { coverage: result.coverage } : {}),
|
|
219
218
|
});
|
|
220
219
|
}
|
|
221
|
-
if (scope === "
|
|
222
|
-
if (
|
|
220
|
+
if (scope === "groups") {
|
|
221
|
+
if (result.groups !== undefined) {
|
|
222
|
+
return ok({
|
|
223
|
+
...envelope,
|
|
224
|
+
scope: "groups",
|
|
225
|
+
available: true,
|
|
226
|
+
groups: rootGroupPatterns(result.groups, resourceId),
|
|
227
|
+
...(result.dimensions ? { dimensions: result.dimensions } : {}),
|
|
228
|
+
...(result.probes ? { probes: result.probes } : {}),
|
|
229
|
+
...(result.partition ? { partition: result.partition } : {}),
|
|
230
|
+
...(result.recipe ? { recipe: result.recipe } : {}),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (result.kinds === undefined) {
|
|
223
234
|
return ok({
|
|
224
235
|
...envelope,
|
|
225
|
-
scope: "
|
|
236
|
+
scope: "groups",
|
|
226
237
|
available: false,
|
|
227
|
-
message: "This profile was taken before the dataset-structure record existed, so it carries no
|
|
238
|
+
message: "This profile was taken before the dataset-structure record existed, so it carries no groups. " +
|
|
228
239
|
"That is a fact about the profile, not about the dataset: use scope:'files' and the workspace " +
|
|
229
240
|
"listing tools, or re-profile the analysis.",
|
|
230
241
|
});
|
|
231
242
|
}
|
|
243
|
+
// The structure exists; only its vocabulary is older. Serving it labelled beats
|
|
244
|
+
// reporting the scope unavailable, which would tell an agent the dataset has no
|
|
245
|
+
// structure when the row plainly holds some.
|
|
232
246
|
return ok({
|
|
233
247
|
...envelope,
|
|
234
|
-
scope: "
|
|
248
|
+
scope: "groups",
|
|
235
249
|
available: true,
|
|
250
|
+
legacy: true,
|
|
236
251
|
kinds: rootKindPatterns(result.kinds, resourceId),
|
|
237
252
|
...(result.axes ? { axes: result.axes } : {}),
|
|
238
253
|
...(result.coverage ? { coverage: result.coverage } : {}),
|
|
254
|
+
message: "Authored under the previous model: these are kinds and axes, not resolved groups and dimensions. " +
|
|
255
|
+
"Counts and patterns were the profiler's own claims rather than a computed partition, and there is no " +
|
|
256
|
+
"unclassified sweep or quarantine accounting on a profile of that era.",
|
|
239
257
|
});
|
|
240
258
|
}
|
|
241
259
|
const pageSize = input.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
242
260
|
const page = input.page ?? 1;
|
|
243
|
-
const total =
|
|
261
|
+
const total = describedFiles.length;
|
|
244
262
|
const start = (page - 1) * pageSize;
|
|
245
|
-
const files =
|
|
263
|
+
const files = describedFiles.slice(start, start + pageSize);
|
|
246
264
|
return ok({
|
|
247
265
|
...envelope,
|
|
248
266
|
scope: "files",
|
|
@@ -250,6 +268,7 @@ export function createInspectDataProfileTool(pool) {
|
|
|
250
268
|
pageSize,
|
|
251
269
|
total,
|
|
252
270
|
hasMore: start + files.length < total,
|
|
271
|
+
datasetFileCount: profileDatasetFileCount(result),
|
|
253
272
|
files: rootFilePaths(files, resourceId),
|
|
254
273
|
});
|
|
255
274
|
},
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* with the observed shapes, or wants a directory the injected manifest summarised, can
|
|
8
8
|
* get the evidence to group differently.
|
|
9
9
|
*
|
|
10
|
-
* Dependency-bearing factory. The walk
|
|
11
|
-
* the workspace read seam; only
|
|
12
|
-
* (see `input-scan/enrich.ts`).
|
|
10
|
+
* Dependency-bearing factory. The walk, the shape observation, and every
|
|
11
|
+
* prefix-sufficient header readout run in-process over the workspace read seam; only a
|
|
12
|
+
* footer-indexed container reaches into the sandbox (see `input-scan/enrich.ts`).
|
|
13
13
|
*/
|
|
14
14
|
import type { SandboxClient } from "../../sandbox/client.js";
|
|
15
15
|
import type { SandboxRef } from "../../sandbox/types.js";
|
|
@@ -17,7 +17,7 @@ import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
|
|
|
17
17
|
export interface ScanInputsDeps {
|
|
18
18
|
readonly workspaceFs: WorkspaceFilesystem;
|
|
19
19
|
readonly analysisId: string;
|
|
20
|
-
/** Omit to
|
|
20
|
+
/** Omit to skip the container readouts; the prefix readouts run regardless. */
|
|
21
21
|
readonly sandboxClient?: SandboxClient;
|
|
22
22
|
readonly sandbox?: SandboxRef;
|
|
23
23
|
readonly workflowId: string;
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* with the observed shapes, or wants a directory the injected manifest summarised, can
|
|
8
8
|
* get the evidence to group differently.
|
|
9
9
|
*
|
|
10
|
-
* Dependency-bearing factory. The walk
|
|
11
|
-
* the workspace read seam; only
|
|
12
|
-
* (see `input-scan/enrich.ts`).
|
|
10
|
+
* Dependency-bearing factory. The walk, the shape observation, and every
|
|
11
|
+
* prefix-sufficient header readout run in-process over the workspace read seam; only a
|
|
12
|
+
* footer-indexed container reaches into the sandbox (see `input-scan/enrich.ts`).
|
|
13
13
|
*/
|
|
14
14
|
import { ok } from "neverthrow";
|
|
15
15
|
import { z } from "zod";
|
|
@@ -38,17 +38,16 @@ export function createScanInputsTool(deps) {
|
|
|
38
38
|
root: path.replace(/^\/+/, "").replace(/\/+$/, ""),
|
|
39
39
|
signal: ctx.signal,
|
|
40
40
|
});
|
|
41
|
-
const shapes =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
: scan.manifest.shapes;
|
|
41
|
+
const shapes = await enrichShapes({
|
|
42
|
+
shapes: scan.manifest.shapes,
|
|
43
|
+
session: ctx.session,
|
|
44
|
+
fs: deps.workspaceFs,
|
|
45
|
+
...(deps.sandboxClient && deps.sandbox ? { sandboxClient: deps.sandboxClient, sandbox: deps.sandbox } : {}),
|
|
46
|
+
mountRoot: `/${deps.analysisId}`,
|
|
47
|
+
execId: `${deps.workflowId}:${deps.stepId}:${deps.nextFunctionId()}`,
|
|
48
|
+
deadlineMs: deps.deadlineMs(),
|
|
49
|
+
emit: ctx.emit,
|
|
50
|
+
});
|
|
52
51
|
const manifest = { ...scan.manifest, shapes };
|
|
53
52
|
return ok({
|
|
54
53
|
path: manifest.root,
|
|
@@ -12,14 +12,23 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Pool } from "pg";
|
|
14
14
|
import type { EmbeddingProvider } from "../../providers/types.js";
|
|
15
|
+
/**
|
|
16
|
+
* The workspace path a hit addresses, or `undefined` for a pattern entry. Resolved here,
|
|
17
|
+
* once, across every index era — group/dimension/kind entries and previous-era entity
|
|
18
|
+
* entries (`entity` in metadata) are patterns; everything else is a file whose path is
|
|
19
|
+
* stamped in metadata or, on rows older than the stamp, is the id minus the resource
|
|
20
|
+
* segment. The consumer branches on presence, nothing else.
|
|
21
|
+
*/
|
|
22
|
+
export declare function fileTargetOf(id: string, metadata: unknown): string | undefined;
|
|
15
23
|
export declare function createWorkspaceSearchTool(pool: Pool, embedding: EmbeddingProvider): import("../define-tool.js").Tool<{
|
|
16
24
|
query: string;
|
|
17
25
|
limit: number;
|
|
18
|
-
type?: "output" | "input" | "synthesis" | "summary" | "input-kind" | undefined;
|
|
26
|
+
type?: "output" | "input" | "synthesis" | "summary" | "input-group" | "input-dimension" | "input-kind" | undefined;
|
|
19
27
|
}, {
|
|
20
28
|
results: {
|
|
29
|
+
metadata: unknown;
|
|
30
|
+
path?: string | undefined;
|
|
21
31
|
id: string;
|
|
22
32
|
score: number;
|
|
23
|
-
metadata: unknown;
|
|
24
33
|
}[];
|
|
25
34
|
}>;
|
|
@@ -29,25 +29,49 @@ function indexName(analysisId) {
|
|
|
29
29
|
}
|
|
30
30
|
return name;
|
|
31
31
|
}
|
|
32
|
+
/** Entry types that address a pattern spanning many files, never a single one. */
|
|
33
|
+
const PATTERN_TYPES = new Set(["input-group", "input-dimension", "input-kind"]);
|
|
34
|
+
/**
|
|
35
|
+
* The workspace path a hit addresses, or `undefined` for a pattern entry. Resolved here,
|
|
36
|
+
* once, across every index era — group/dimension/kind entries and previous-era entity
|
|
37
|
+
* entries (`entity` in metadata) are patterns; everything else is a file whose path is
|
|
38
|
+
* stamped in metadata or, on rows older than the stamp, is the id minus the resource
|
|
39
|
+
* segment. The consumer branches on presence, nothing else.
|
|
40
|
+
*/
|
|
41
|
+
export function fileTargetOf(id, metadata) {
|
|
42
|
+
const meta = metadata && typeof metadata === "object" ? metadata : {};
|
|
43
|
+
const type = typeof meta.type === "string" ? meta.type : undefined;
|
|
44
|
+
if (type !== undefined && PATTERN_TYPES.has(type))
|
|
45
|
+
return undefined;
|
|
46
|
+
if (type === "input" && meta.entity !== undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (typeof meta.path === "string")
|
|
49
|
+
return meta.path;
|
|
50
|
+
return id.replace(/^\/[^/]+\//, "");
|
|
51
|
+
}
|
|
32
52
|
export function createWorkspaceSearchTool(pool, embedding) {
|
|
33
53
|
return defineTool({
|
|
34
54
|
id: "workspace_search",
|
|
35
55
|
description: "Semantic search over the analysis workspace. Returns ranked entries with descriptions " +
|
|
36
|
-
"and metadata; read a file separately to see its contents.
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
56
|
+
"and metadata; read a file separately to see its contents. `type` restricts results to one " +
|
|
57
|
+
'entry type: "input-group" (a GROUP of input data the data profiler declared, carrying its ' +
|
|
58
|
+
'derived count, its display pattern, and its slots), "input-dimension" (something that VARIES ' +
|
|
59
|
+
"across the dataset — subject, timepoint, batch — with the observations evidencing it), " +
|
|
60
|
+
'"input" (one input file the profiler annotated individually, or, on a profile of the ' +
|
|
61
|
+
'previous era, one entity the dataset is about), "output" (files a step produced), ' +
|
|
62
|
+
'"summary" (a step\'s summary), "synthesis" (a run\'s literature-grounded synthesis), ' +
|
|
63
|
+
'"input-kind" (a repeating set, on a profile of the previous era). ' +
|
|
64
|
+
"A hit carrying `path` is a FILE — read that path to see its contents. A hit without " +
|
|
65
|
+
"`path` is a PATTERN spanning many files (a group, a dimension, an entity): use its " +
|
|
66
|
+
"metadata, e.g. search input-group for a group's display pattern, then list the tree for " +
|
|
67
|
+
"its members. Most input files carry no entry of their own — find them through their " +
|
|
68
|
+
"group and the listing tools.",
|
|
45
69
|
inputSchema: z.object({
|
|
46
70
|
query: z.string().min(1).describe("What to search for, in natural language"),
|
|
47
71
|
type: z
|
|
48
|
-
.enum(["input-
|
|
72
|
+
.enum(["input-group", "input-dimension", "input", "input-kind", "output", "summary", "synthesis"])
|
|
49
73
|
.optional()
|
|
50
|
-
.describe('Restrict results to one entry type: "input-
|
|
74
|
+
.describe('Restrict results to one entry type: "input-group", "input-dimension", "input", "input-kind", "output", "summary", or "synthesis"'),
|
|
51
75
|
limit: z.number().int().min(1).max(50).default(8).describe("Maximum number of results to return"),
|
|
52
76
|
}),
|
|
53
77
|
describeCall: ({ query, type }) => (type === undefined ? query : `${query} (${type})`),
|
|
@@ -77,11 +101,15 @@ export function createWorkspaceSearchTool(pool, embedding) {
|
|
|
77
101
|
// exist. That is absence, not a failure: recover into ok([]).
|
|
78
102
|
isUndefinedTable(e.cause) ? okAsync([]) : err(e)));
|
|
79
103
|
return ok({
|
|
80
|
-
results: rows.map((r) =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
results: rows.map((r) => {
|
|
105
|
+
const path = fileTargetOf(r.id, r.metadata);
|
|
106
|
+
return {
|
|
107
|
+
id: r.id,
|
|
108
|
+
score: r.score,
|
|
109
|
+
...(path !== undefined ? { path } : {}),
|
|
110
|
+
metadata: r.metadata,
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
85
113
|
});
|
|
86
114
|
},
|
|
87
115
|
});
|
|
@@ -124,6 +124,27 @@ export declare const Phase4OutputSchema: z.ZodObject<{
|
|
|
124
124
|
coverage: z.ZodLiteral<"not_loaded">;
|
|
125
125
|
reason: z.ZodOptional<z.ZodString>;
|
|
126
126
|
}, z.core.$strip>], "coverage">;
|
|
127
|
+
entity: z.ZodObject<{
|
|
128
|
+
canonicalId: z.ZodString;
|
|
129
|
+
symbol: z.ZodString;
|
|
130
|
+
displayName: z.ZodString;
|
|
131
|
+
entityType: z.ZodEnum<{
|
|
132
|
+
gene: "gene";
|
|
133
|
+
rna: "rna";
|
|
134
|
+
protein: "protein";
|
|
135
|
+
complex: "complex";
|
|
136
|
+
}>;
|
|
137
|
+
ontology: z.ZodString;
|
|
138
|
+
identifiers: z.ZodObject<{
|
|
139
|
+
hgnc: z.ZodOptional<z.ZodString>;
|
|
140
|
+
ensembl: z.ZodOptional<z.ZodString>;
|
|
141
|
+
uniprot: z.ZodOptional<z.ZodString>;
|
|
142
|
+
chembl: z.ZodOptional<z.ZodString>;
|
|
143
|
+
ncbiGene: z.ZodOptional<z.ZodString>;
|
|
144
|
+
}, z.core.$strip>;
|
|
145
|
+
synonyms: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
146
|
+
proteinFamily: z.ZodOptional<z.ZodString>;
|
|
147
|
+
}, z.core.$strip>;
|
|
127
148
|
off_target_panel: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
128
149
|
coverage: z.ZodLiteral<"available">;
|
|
129
150
|
data: z.ZodObject<{
|
|
@@ -347,27 +368,6 @@ export declare const Phase4OutputSchema: z.ZodObject<{
|
|
|
347
368
|
inferred_therapeutic_area: z.ZodNullable<z.ZodString>;
|
|
348
369
|
no_liabilities_disclosure: z.ZodOptional<z.ZodString>;
|
|
349
370
|
}, z.core.$strict>;
|
|
350
|
-
entity: z.ZodObject<{
|
|
351
|
-
canonicalId: z.ZodString;
|
|
352
|
-
symbol: z.ZodString;
|
|
353
|
-
displayName: z.ZodString;
|
|
354
|
-
entityType: z.ZodEnum<{
|
|
355
|
-
gene: "gene";
|
|
356
|
-
rna: "rna";
|
|
357
|
-
protein: "protein";
|
|
358
|
-
complex: "complex";
|
|
359
|
-
}>;
|
|
360
|
-
ontology: z.ZodString;
|
|
361
|
-
identifiers: z.ZodObject<{
|
|
362
|
-
hgnc: z.ZodOptional<z.ZodString>;
|
|
363
|
-
ensembl: z.ZodOptional<z.ZodString>;
|
|
364
|
-
uniprot: z.ZodOptional<z.ZodString>;
|
|
365
|
-
chembl: z.ZodOptional<z.ZodString>;
|
|
366
|
-
ncbiGene: z.ZodOptional<z.ZodString>;
|
|
367
|
-
}, z.core.$strip>;
|
|
368
|
-
synonyms: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
369
|
-
proteinFamily: z.ZodOptional<z.ZodString>;
|
|
370
|
-
}, z.core.$strip>;
|
|
371
371
|
indications: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
372
372
|
coverage: z.ZodLiteral<"available">;
|
|
373
373
|
data: z.ZodObject<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inflexa-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -33,15 +33,17 @@
|
|
|
33
33
|
"directory": "harness"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
|
-
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
|
|
37
37
|
"design:fixture": "bun scripts/render-fixture.ts",
|
|
38
38
|
"format": "prettier --write src/",
|
|
39
39
|
"format:file": "prettier --write",
|
|
40
40
|
"lint": "eslint .",
|
|
41
|
-
"
|
|
41
|
+
"lint:python": "ruff check src/input-scan/decoder && ruff format --check src/input-scan/decoder",
|
|
42
|
+
"prepare": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
|
|
42
43
|
"prepublishOnly": "bun run build && bun run smoke",
|
|
43
44
|
"smoke": "node scripts/smoke.mjs",
|
|
44
45
|
"test:full": "bash scripts/test-full.sh",
|
|
46
|
+
"test:python": "python3 -m pytest src/input-scan/decoder -q",
|
|
45
47
|
"typecheck": "tsc --noEmit"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
@@ -1,22 +0,0 @@
|
|
|
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;
|
|
@@ -1,85 +0,0 @@
|
|
|
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
|
-
}
|