@inflexa-ai/harness 0.23.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/README.md +1 -2
- package/dist/agents/conversation-agent.d.ts +0 -9
- 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/chat-parts.d.ts +29 -33
- package/dist/contracts/content-url.d.ts +7 -41
- package/dist/contracts/content-url.js +7 -48
- package/dist/contracts/data-profile.d.ts +166 -0
- package/dist/contracts/index.d.ts +3 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/message.d.ts +1 -1
- package/dist/contracts/message.js +1 -1
- package/dist/contracts/part-registry.js +2 -3
- package/dist/contracts/schemas/chat-parts.d.ts +16 -53
- package/dist/contracts/schemas/chat-parts.js +15 -25
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/input-signature.d.ts +33 -0
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/run-authorizer.d.ts +1 -1
- package/dist/execution/run-authorizer.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -2
- 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/lib/zod-issues.d.ts +2 -3
- package/dist/lib/zod-issues.js +2 -3
- package/dist/loop/__fixtures__/scripted-provider.js +3 -1
- package/dist/loop/run-agent.d.ts +5 -0
- package/dist/loop/run-agent.js +10 -1
- package/dist/loop/run-to-terminal.d.ts +1 -1
- package/dist/loop/run-to-terminal.js +1 -1
- package/dist/memory/card-builders.d.ts +0 -27
- package/dist/memory/card-builders.js +0 -75
- package/dist/memory/conversation-display-backfill.d.ts +2 -4
- package/dist/memory/conversation-display-backfill.js +3 -9
- package/dist/memory/conversation-display-storage.d.ts +3 -4
- package/dist/memory/conversation-display-storage.js +3 -4
- package/dist/memory/reconstruct-cards.d.ts +5 -7
- package/dist/memory/reconstruct-cards.js +6 -20
- 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/runtime/boot.js +2 -12
- 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/report-session/preview-report.d.ts +2 -1
- package/dist/tools/report-session/preview-report.js +9 -1
- 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/start-report-session.js +2 -2
- 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/dist/workspace/paths.d.ts +0 -21
- package/dist/workspace/paths.js +0 -37
- package/package.json +1 -3
- package/dist/agents/report-builder.d.ts +0 -34
- package/dist/agents/report-builder.js +0 -34
- package/dist/execution/report-render.d.ts +0 -39
- package/dist/execution/report-render.js +0 -98
- package/dist/execution/report-runner.d.ts +0 -111
- package/dist/execution/report-runner.js +0 -230
- package/dist/prompts/report-builder.d.ts +0 -1
- package/dist/prompts/report-builder.js +0 -174
- package/dist/tools/iterate-report.d.ts +0 -227
- package/dist/tools/iterate-report.js +0 -883
- package/dist/tools/lib/preview-lock.d.ts +0 -20
- package/dist/tools/lib/preview-lock.js +0 -35
- package/dist/tools/lib/report-preflight.d.ts +0 -39
- package/dist/tools/lib/report-preflight.js +0 -283
- package/dist/tools/report/build-report.d.ts +0 -13
- package/dist/tools/report/build-report.js +0 -27
- package/dist/tools/report/index.d.ts +0 -15
- package/dist/tools/report/index.js +0 -15
- package/dist/tools/report/mint-preview-url.d.ts +0 -26
- package/dist/tools/report/mint-preview-url.js +0 -47
- package/dist/tools/report/preview-publisher.d.ts +0 -53
- package/dist/tools/report/preview-publisher.js +0 -45
- package/dist/tools/report/preview-snapshot.d.ts +0 -34
- package/dist/tools/report/preview-snapshot.js +0 -98
- package/dist/tools/report/submit-report.d.ts +0 -26
- package/dist/tools/report/submit-report.js +0 -148
- package/dist/tools/report/version-fs.d.ts +0 -15
- package/dist/tools/report/version-fs.js +0 -165
|
@@ -28,12 +28,14 @@
|
|
|
28
28
|
* render problem, a figure that escapes the workspace root, a write failure, and a stamp failure. The tool
|
|
29
29
|
* never throws for one of them. When the page
|
|
30
30
|
* lands, the tool stamps the hash of the rendered draft on the session state, thus the eyes and the record
|
|
31
|
-
* know which draft the page shows.
|
|
31
|
+
* know which draft the page shows. It then emits one durable `data-report-rendered` part, thus the
|
|
32
|
+
* transcript places the render and a live client learns that a fresh page exists. The filesystem speaks the
|
|
32
33
|
* throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
|
|
33
34
|
* write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
|
|
34
35
|
* control-flow exception propagate.
|
|
35
36
|
*/
|
|
36
37
|
import { err, ok } from "neverthrow";
|
|
38
|
+
import { randomUUID } from "node:crypto";
|
|
37
39
|
import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
38
40
|
import { extname, join } from "node:path";
|
|
39
41
|
import { relative as relativePosix } from "node:path/posix";
|
|
@@ -475,6 +477,12 @@ export function createPreviewReportTool(deps) {
|
|
|
475
477
|
logger.warn("the rendered hash did not stamp", { threadId, analysisId, detail });
|
|
476
478
|
return ok({ outcome: "stamp-failed", pagePath: written.value, detail });
|
|
477
479
|
}
|
|
480
|
+
// The part rides the rendered arm only. Each degraded arm shows no
|
|
481
|
+
// fresh page, thus it emits nothing.
|
|
482
|
+
await ctx.emit({
|
|
483
|
+
type: "data-report-rendered",
|
|
484
|
+
data: { id: randomUUID(), renderedAt: new Date().toISOString(), title: document.title },
|
|
485
|
+
});
|
|
478
486
|
const access = await mintAccess(deps.makeSessionPages, analysisId, threadId, ctx.session.auth, logger);
|
|
479
487
|
return ok({ outcome: "rendered", pagePath: written.value, ...(access !== undefined ? { access } : {}) });
|
|
480
488
|
},
|
|
@@ -182,7 +182,7 @@ function classifyGrounding(status) {
|
|
|
182
182
|
// preserve `data_profile_result` on purpose, so a non-`completed` status
|
|
183
183
|
// carrying a result means what is on the row is the PREVIOUS profile.
|
|
184
184
|
const reasons = [];
|
|
185
|
-
if (isDataProfileStale(status.seedInputFileIds ?? [], result
|
|
185
|
+
if (isDataProfileStale({ fileIds: status.seedInputFileIds ?? [] }, result)) {
|
|
186
186
|
reasons.push("the analysis's input file set changed after this profile was taken");
|
|
187
187
|
}
|
|
188
188
|
if (status.status === "pending" || status.status === "running") {
|
|
@@ -46,8 +46,34 @@ interface OverviewOutput extends ProfileEnvelope {
|
|
|
46
46
|
readonly experimentalDesign?: string;
|
|
47
47
|
readonly qualityAssessment?: DataProfileResult["qualityAssessment"];
|
|
48
48
|
readonly accessions?: string[];
|
|
49
|
-
/** How many per-file records `scope:"files"` would page through. */
|
|
50
|
-
readonly
|
|
49
|
+
/** How many per-file records `scope:"files"` would page through — NOT the dataset's size. */
|
|
50
|
+
readonly describedFileCount: number;
|
|
51
|
+
/**
|
|
52
|
+
* How many files the dataset holds, or `null` when the snapshot cannot say (one
|
|
53
|
+
* written before kinds and coverage existed). Reported separately from
|
|
54
|
+
* `describedFileCount` because a profile describing eight files of several thousand
|
|
55
|
+
* is correct under this capability, and a single count would read as a dataset of
|
|
56
|
+
* eight files.
|
|
57
|
+
*/
|
|
58
|
+
readonly datasetFileCount: number | null;
|
|
59
|
+
/** How many kinds `scope:"kinds"` would return; `null` on a pre-kinds snapshot. */
|
|
60
|
+
readonly kindCount: number | null;
|
|
61
|
+
/** Present when the two counts differ — says where the rest of the dataset is described. */
|
|
62
|
+
readonly structureNote?: string;
|
|
63
|
+
readonly coverage?: DataProfileResult["coverage"];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The dataset's structure. A snapshot predating kinds reports `available: false`
|
|
67
|
+
* rather than an empty list: "this profile predates the structure" and "this dataset
|
|
68
|
+
* has no structure" are different facts, and only one of them is true.
|
|
69
|
+
*/
|
|
70
|
+
interface KindsOutput extends ProfileEnvelope {
|
|
71
|
+
readonly scope: "kinds";
|
|
72
|
+
readonly available: boolean;
|
|
73
|
+
readonly kinds?: DataProfileResult["kinds"];
|
|
74
|
+
readonly axes?: DataProfileResult["axes"];
|
|
75
|
+
readonly coverage?: DataProfileResult["coverage"];
|
|
76
|
+
readonly message?: string;
|
|
51
77
|
}
|
|
52
78
|
interface FilesOutput extends ProfileEnvelope {
|
|
53
79
|
readonly scope: "files";
|
|
@@ -101,9 +127,9 @@ interface FailedOutput {
|
|
|
101
127
|
readonly failedAt: string | null;
|
|
102
128
|
readonly message: string;
|
|
103
129
|
}
|
|
104
|
-
export type InspectDataProfileOutput = OverviewOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
|
|
130
|
+
export type InspectDataProfileOutput = OverviewOutput | KindsOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
|
|
105
131
|
export declare function createInspectDataProfileTool(pool: Pool): import("../define-tool.js").Tool<{
|
|
106
|
-
scope?: "files" | "
|
|
132
|
+
scope?: "overview" | "files" | "kinds" | undefined;
|
|
107
133
|
page?: number | undefined;
|
|
108
134
|
pageSize?: number | undefined;
|
|
109
135
|
}, InspectDataProfileOutput>;
|
|
@@ -31,6 +31,20 @@ import { defineTool } from "../define-tool.js";
|
|
|
31
31
|
const DEFAULT_PAGE_SIZE = 20;
|
|
32
32
|
/** Ceiling on `pageSize` — a page is a context-window budget, not a dump. */
|
|
33
33
|
const MAX_PAGE_SIZE = 100;
|
|
34
|
+
/**
|
|
35
|
+
* How many files the dataset holds, as far as the snapshot can establish it.
|
|
36
|
+
*
|
|
37
|
+
* Coverage counts the scanned tree, so it is the authority where present. Summed kind
|
|
38
|
+
* counts are the fallback. A snapshot with neither returns `null` rather than the
|
|
39
|
+
* described-file count, which would be a dataset size the row cannot support.
|
|
40
|
+
*/
|
|
41
|
+
function datasetFileCount(result) {
|
|
42
|
+
if (result.coverage)
|
|
43
|
+
return result.coverage.total;
|
|
44
|
+
if (result.kinds && result.kinds.length > 0)
|
|
45
|
+
return result.kinds.reduce((sum, kind) => sum + kind.count, 0);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
34
48
|
/**
|
|
35
49
|
* Name every reason the served profile may not describe the current inputs.
|
|
36
50
|
*
|
|
@@ -44,7 +58,7 @@ const MAX_PAGE_SIZE = 100;
|
|
|
44
58
|
*/
|
|
45
59
|
function stalenessReasons(status, result) {
|
|
46
60
|
const reasons = [];
|
|
47
|
-
if (isDataProfileStale(status.seedInputFileIds ?? [], result
|
|
61
|
+
if (isDataProfileStale({ fileIds: status.seedInputFileIds ?? [] }, result)) {
|
|
48
62
|
reasons.push("the analysis's input file set changed after this profile was taken");
|
|
49
63
|
}
|
|
50
64
|
if (status.status === "running" || status.status === "pending") {
|
|
@@ -69,14 +83,20 @@ export function createInspectDataProfileTool(pool) {
|
|
|
69
83
|
id: "inspect_data_profile",
|
|
70
84
|
description: "Read this analysis's data profile — the AUTHORITATIVE record of what the input dataset is: " +
|
|
71
85
|
"organism and taxon id, scientific domain and subtype, tissue, cell type, condition, experimental design, " +
|
|
72
|
-
"dataset-wide quality concerns and strengths, public accessions,
|
|
73
|
-
"row/column dimensions, tags
|
|
86
|
+
"dataset-wide quality concerns and strengths, public accessions, the KINDS of file the dataset is made of " +
|
|
87
|
+
"and what varies across them, and per-file data type, format, row/column dimensions, tags and warnings for " +
|
|
88
|
+
"the files described individually. " +
|
|
74
89
|
"There is NO data-profile file in the workspace — this tool is the only way to read it. Do not search for one, " +
|
|
75
90
|
"and do not rediscover these facts by listing or reading the raw input files. " +
|
|
76
91
|
"Call it before you reason about the data (planning, writing analysis code, interpreting results). " +
|
|
77
|
-
"scope:'overview' (the default) returns the dataset-level orientation
|
|
78
|
-
"
|
|
92
|
+
"scope:'overview' (the default) returns the dataset-level orientation, how many files are described individually, " +
|
|
93
|
+
"and how many files the dataset holds — those two differ on purpose, because a large dataset is described by its " +
|
|
94
|
+
"kinds rather than file by file. " +
|
|
95
|
+
"scope:'kinds' returns those kinds with their counts, path patterns, and axes — this is where a dataset of thousands " +
|
|
96
|
+
"of files is described. A profile taken before kinds existed reports the scope unavailable. " +
|
|
97
|
+
"scope:'files' returns the individually described files, paged: page (1-based, default 1) and pageSize (default 20, max 100), " +
|
|
79
98
|
"always with the true total and hasMore, so you can see exactly what you have not read yet. " +
|
|
99
|
+
"For the paths of files no record describes, list the workspace tree. " +
|
|
80
100
|
"The state field says what you got: 'ready'; 'stale' (a profile is returned but may not describe the current " +
|
|
81
101
|
"inputs — staleReason says why); 'pending' (profiling is still running); 'failed'; or 'absent' (never profiled, " +
|
|
82
102
|
"or the analysis has no input files). " +
|
|
@@ -86,9 +106,11 @@ export function createInspectDataProfileTool(pool) {
|
|
|
86
106
|
"needs re-running rather than diagnosing the data itself.",
|
|
87
107
|
inputSchema: z.object({
|
|
88
108
|
scope: z
|
|
89
|
-
.enum(["overview", "files"])
|
|
109
|
+
.enum(["overview", "kinds", "files"])
|
|
90
110
|
.optional()
|
|
91
|
-
.describe("'overview' (default): dataset-level facts
|
|
111
|
+
.describe("'overview' (default): dataset-level facts, described-file count, and dataset file count. " +
|
|
112
|
+
"'kinds': the sets the dataset is made of and what varies across them. " +
|
|
113
|
+
"'files': paged records for the individually described files."),
|
|
92
114
|
page: z.number().int().min(1).optional().describe("1-based page of per-file records. Only used when scope is 'files'. Default 1."),
|
|
93
115
|
pageSize: z
|
|
94
116
|
.number()
|
|
@@ -101,7 +123,7 @@ export function createInspectDataProfileTool(pool) {
|
|
|
101
123
|
// `execute` defaults both fields internally, and the hook applies the same
|
|
102
124
|
// defaults. Without them the ordinary call, which names no field at all,
|
|
103
125
|
// gives no detail. `page` acts only on the `files` scope, as in `execute`.
|
|
104
|
-
describeCall: ({ scope, page }) => ((scope ?? "overview") === "files" ? `files (page ${page ?? 1})` : "overview"),
|
|
126
|
+
describeCall: ({ scope, page }) => ((scope ?? "overview") === "files" ? `files (page ${page ?? 1})` : (scope ?? "overview")),
|
|
105
127
|
execute: async (input, ctx) => {
|
|
106
128
|
const resourceId = scopeResource(ctx.session.scope).resourceId;
|
|
107
129
|
const status = unwrapOrThrow(await loadDataProfileStatus(pool, resourceId));
|
|
@@ -145,7 +167,17 @@ export function createInspectDataProfileTool(pool) {
|
|
|
145
167
|
const envelope = reasons.length > 0
|
|
146
168
|
? { state: "stale", staleReason: reasons.join("; "), profiledAt: result.profiledAt }
|
|
147
169
|
: { state: "ready", profiledAt: result.profiledAt };
|
|
148
|
-
|
|
170
|
+
const scope = input.scope ?? "overview";
|
|
171
|
+
if (scope === "overview") {
|
|
172
|
+
const dataset = datasetFileCount(result);
|
|
173
|
+
const described = result.files.length;
|
|
174
|
+
const structureNote = dataset === null
|
|
175
|
+
? "This profile predates the dataset-structure record, so only the individually described files are known; " +
|
|
176
|
+
"list the workspace tree for the rest."
|
|
177
|
+
: dataset !== described
|
|
178
|
+
? `${described} of ${dataset} files are described individually. Call scope:'kinds' for what the rest are, ` +
|
|
179
|
+
"and list the workspace tree for their paths."
|
|
180
|
+
: undefined;
|
|
149
181
|
return ok({
|
|
150
182
|
...envelope,
|
|
151
183
|
scope: "overview",
|
|
@@ -159,7 +191,31 @@ export function createInspectDataProfileTool(pool) {
|
|
|
159
191
|
experimentalDesign: result.experimentalDesign,
|
|
160
192
|
qualityAssessment: result.qualityAssessment,
|
|
161
193
|
accessions: result.accessions,
|
|
162
|
-
|
|
194
|
+
describedFileCount: described,
|
|
195
|
+
datasetFileCount: dataset,
|
|
196
|
+
kindCount: result.kinds ? result.kinds.length : null,
|
|
197
|
+
...(structureNote ? { structureNote } : {}),
|
|
198
|
+
...(result.coverage ? { coverage: result.coverage } : {}),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (scope === "kinds") {
|
|
202
|
+
if (!result.kinds) {
|
|
203
|
+
return ok({
|
|
204
|
+
...envelope,
|
|
205
|
+
scope: "kinds",
|
|
206
|
+
available: false,
|
|
207
|
+
message: "This profile was taken before the dataset-structure record existed, so it carries no kinds. " +
|
|
208
|
+
"That is a fact about the profile, not about the dataset: use scope:'files' and the workspace " +
|
|
209
|
+
"listing tools, or re-profile the analysis.",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return ok({
|
|
213
|
+
...envelope,
|
|
214
|
+
scope: "kinds",
|
|
215
|
+
available: true,
|
|
216
|
+
kinds: result.kinds,
|
|
217
|
+
...(result.axes ? { axes: result.axes } : {}),
|
|
218
|
+
...(result.coverage ? { coverage: result.coverage } : {}),
|
|
163
219
|
});
|
|
164
220
|
}
|
|
165
221
|
const pageSize = input.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
@@ -173,8 +173,8 @@ export function createStartReportSessionTool(deps) {
|
|
|
173
173
|
// names a session that an earlier turn announced, thus a second
|
|
174
174
|
// part would put a second entry into the transcript.
|
|
175
175
|
await ctx.emit({
|
|
176
|
-
type: "data-
|
|
177
|
-
data: { threadId: child.threadId, parentThreadId },
|
|
176
|
+
type: "data-child-session-started",
|
|
177
|
+
data: { threadId: child.threadId, parentThreadId, threadType: "report" },
|
|
178
178
|
});
|
|
179
179
|
return ok({ outcome: "started", threadId: child.threadId, title: child.title });
|
|
180
180
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `scan_inputs` tool — re-run the deterministic input scan over a subtree.
|
|
3
|
+
*
|
|
4
|
+
* The data-profile body runs the scan once before the agent loop and injects the
|
|
5
|
+
* manifest into the briefing, because the scan is always needed and its result does
|
|
6
|
+
* not depend on agent judgement. This tool is the other half: an agent that disagrees
|
|
7
|
+
* with the observed shapes, or wants a directory the injected manifest summarised, can
|
|
8
|
+
* get the evidence to group differently.
|
|
9
|
+
*
|
|
10
|
+
* Dependency-bearing factory. The walk and the shape observation run in-process over
|
|
11
|
+
* the workspace read seam; only the per-shape header readout reaches into the sandbox
|
|
12
|
+
* (see `input-scan/enrich.ts`).
|
|
13
|
+
*/
|
|
14
|
+
import type { SandboxClient } from "../../sandbox/client.js";
|
|
15
|
+
import type { SandboxRef } from "../../sandbox/types.js";
|
|
16
|
+
import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
|
|
17
|
+
export interface ScanInputsDeps {
|
|
18
|
+
readonly workspaceFs: WorkspaceFilesystem;
|
|
19
|
+
readonly analysisId: string;
|
|
20
|
+
/** Omit to run the host-side scan with no header readout (no sandbox wired). */
|
|
21
|
+
readonly sandboxClient?: SandboxClient;
|
|
22
|
+
readonly sandbox?: SandboxRef;
|
|
23
|
+
readonly workflowId: string;
|
|
24
|
+
readonly stepId: string;
|
|
25
|
+
readonly nextFunctionId: () => string;
|
|
26
|
+
readonly deadlineMs: () => number;
|
|
27
|
+
}
|
|
28
|
+
type ScanInputsOutput = {
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly fileCount: number;
|
|
31
|
+
readonly shapeCount: number;
|
|
32
|
+
/** The rendered manifest — bounded whatever the subtree's size. */
|
|
33
|
+
readonly manifest: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function createScanInputsTool(deps: ScanInputsDeps): import("../define-tool.js").Tool<{
|
|
36
|
+
path: string;
|
|
37
|
+
}, ScanInputsOutput>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `scan_inputs` tool — re-run the deterministic input scan over a subtree.
|
|
3
|
+
*
|
|
4
|
+
* The data-profile body runs the scan once before the agent loop and injects the
|
|
5
|
+
* manifest into the briefing, because the scan is always needed and its result does
|
|
6
|
+
* not depend on agent judgement. This tool is the other half: an agent that disagrees
|
|
7
|
+
* with the observed shapes, or wants a directory the injected manifest summarised, can
|
|
8
|
+
* get the evidence to group differently.
|
|
9
|
+
*
|
|
10
|
+
* Dependency-bearing factory. The walk and the shape observation run in-process over
|
|
11
|
+
* the workspace read seam; only the per-shape header readout reaches into the sandbox
|
|
12
|
+
* (see `input-scan/enrich.ts`).
|
|
13
|
+
*/
|
|
14
|
+
import { ok } from "neverthrow";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { enrichShapes } from "../../input-scan/enrich.js";
|
|
17
|
+
import { renderInputScanManifest, scanInputTree } from "../../input-scan/scan.js";
|
|
18
|
+
import { defineTool } from "../define-tool.js";
|
|
19
|
+
export function createScanInputsTool(deps) {
|
|
20
|
+
return defineTool({
|
|
21
|
+
id: "scan_inputs",
|
|
22
|
+
description: "Scan a directory of input files and report what is OBSERVABLE about it: per-format counts, " +
|
|
23
|
+
"the shapes its filenames form (sets of files whose names differ only at marked positions), " +
|
|
24
|
+
"the distinct values each varying position takes, how those positions co-occur, value overlap " +
|
|
25
|
+
"between shapes, and the files that share structure with nothing else. " +
|
|
26
|
+
"Deterministic — no model, no per-file decoding — so it costs the same on three files as on three thousand. " +
|
|
27
|
+
"Your briefing already carries a scan of the whole input tree; call this to look at a subtree more closely, " +
|
|
28
|
+
"or to check a grouping you are unsure about. " +
|
|
29
|
+
"It reports observations only: which files are one KIND of data, and what a varying position MEANS, are yours to decide.",
|
|
30
|
+
inputSchema: z.object({
|
|
31
|
+
path: z.string().min(1).describe("Directory to scan, relative to the analysis root (e.g. 'data/inputs' or 'data/inputs/vcf')."),
|
|
32
|
+
}),
|
|
33
|
+
describeCall: ({ path }) => path,
|
|
34
|
+
execute: async ({ path }, ctx) => {
|
|
35
|
+
const scan = await scanInputTree({
|
|
36
|
+
session: ctx.session,
|
|
37
|
+
fs: deps.workspaceFs,
|
|
38
|
+
root: path.replace(/^\/+/, "").replace(/\/+$/, ""),
|
|
39
|
+
signal: ctx.signal,
|
|
40
|
+
});
|
|
41
|
+
const shapes = deps.sandboxClient && deps.sandbox
|
|
42
|
+
? await enrichShapes({
|
|
43
|
+
shapes: scan.manifest.shapes,
|
|
44
|
+
sandboxClient: deps.sandboxClient,
|
|
45
|
+
sandbox: deps.sandbox,
|
|
46
|
+
mountRoot: `/${deps.analysisId}`,
|
|
47
|
+
execId: `${deps.workflowId}:${deps.stepId}:${deps.nextFunctionId()}`,
|
|
48
|
+
deadlineMs: deps.deadlineMs(),
|
|
49
|
+
emit: ctx.emit,
|
|
50
|
+
})
|
|
51
|
+
: scan.manifest.shapes;
|
|
52
|
+
const manifest = { ...scan.manifest, shapes };
|
|
53
|
+
return ok({
|
|
54
|
+
path: manifest.root,
|
|
55
|
+
fileCount: manifest.fileCount,
|
|
56
|
+
shapeCount: manifest.shapes.length,
|
|
57
|
+
manifest: renderInputScanManifest(manifest),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -15,7 +15,7 @@ import type { EmbeddingProvider } from "../../providers/types.js";
|
|
|
15
15
|
export declare function createWorkspaceSearchTool(pool: Pool, embedding: EmbeddingProvider): import("../define-tool.js").Tool<{
|
|
16
16
|
query: string;
|
|
17
17
|
limit: number;
|
|
18
|
-
type?: "output" | "input" | "synthesis" | "summary" | undefined;
|
|
18
|
+
type?: "output" | "input" | "synthesis" | "summary" | "input-kind" | undefined;
|
|
19
19
|
}, {
|
|
20
20
|
results: {
|
|
21
21
|
id: string;
|
|
@@ -32,19 +32,22 @@ function indexName(analysisId) {
|
|
|
32
32
|
export function createWorkspaceSearchTool(pool, embedding) {
|
|
33
33
|
return defineTool({
|
|
34
34
|
id: "workspace_search",
|
|
35
|
-
description: "Semantic search over the analysis workspace. Returns ranked
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
"literature-grounded synthesis).
|
|
35
|
+
description: "Semantic search over the analysis workspace. Returns ranked entries with descriptions " +
|
|
36
|
+
"and metadata; read a file separately to see its contents. Indexed entries are exactly " +
|
|
37
|
+
'five types, and `type` restricts results to one of them: "input-kind" (a KIND of input ' +
|
|
38
|
+
"data — a repeating set the data profiler named, carrying its count and a pathPattern " +
|
|
39
|
+
'matching its members), "input" (one entity the dataset is about — a subject, sample, or ' +
|
|
40
|
+
'other axis value — with the kinds of file it carries), "output" (files a step produced), ' +
|
|
41
|
+
'"summary" (a step\'s summary), "synthesis" (a run\'s literature-grounded synthesis). ' +
|
|
42
|
+
"For output/summary/synthesis the entry id IS the workspace path; for the two input types " +
|
|
43
|
+
"it is not, because a kind and an entity each span many files — search input-kind for a " +
|
|
44
|
+
"set's pathPattern, then list the tree for its members.",
|
|
42
45
|
inputSchema: z.object({
|
|
43
46
|
query: z.string().min(1).describe("What to search for, in natural language"),
|
|
44
47
|
type: z
|
|
45
|
-
.enum(["input", "output", "summary", "synthesis"])
|
|
48
|
+
.enum(["input-kind", "input", "output", "summary", "synthesis"])
|
|
46
49
|
.optional()
|
|
47
|
-
.describe('Restrict results to one entry type: "input", "output", "summary", or "synthesis"'),
|
|
50
|
+
.describe('Restrict results to one entry type: "input-kind", "input", "output", "summary", or "synthesis"'),
|
|
48
51
|
limit: z.number().int().min(1).max(50).default(8).describe("Maximum number of results to return"),
|
|
49
52
|
}),
|
|
50
53
|
describeCall: ({ query, type }) => (type === undefined ? query : `${query} (${type})`),
|
|
@@ -67,6 +67,15 @@ export type StatResult = {
|
|
|
67
67
|
} | {
|
|
68
68
|
readonly kind: "out_of_scope";
|
|
69
69
|
};
|
|
70
|
+
/** A bounded prefix of a file's raw bytes. `bytes` may be shorter than requested. */
|
|
71
|
+
export type ReadBytesResult = {
|
|
72
|
+
readonly kind: "ok";
|
|
73
|
+
readonly bytes: Buffer;
|
|
74
|
+
} | {
|
|
75
|
+
readonly kind: "not_found";
|
|
76
|
+
} | {
|
|
77
|
+
readonly kind: "out_of_scope";
|
|
78
|
+
};
|
|
70
79
|
export interface WorkspaceFilesystem {
|
|
71
80
|
/**
|
|
72
81
|
* Read a workspace file. `headLines` / `tailLines` (mutually exclusive,
|
|
@@ -86,6 +95,24 @@ export interface WorkspaceFilesystem {
|
|
|
86
95
|
/** Absolute host base for relative paths. Defaults to the analysis root. */
|
|
87
96
|
readonly workingDir?: string;
|
|
88
97
|
}): ResultAsync<ReadFileResult, FsError>;
|
|
98
|
+
/**
|
|
99
|
+
* Read the first `length` bytes of a file, raw.
|
|
100
|
+
*
|
|
101
|
+
* Distinct from `readFile` because that surface is line- and text-oriented — it
|
|
102
|
+
* decodes to UTF-8 for its line windows and its byte cap is an outer bound on a
|
|
103
|
+
* whole-file read, not a prefix request. Magic-byte format detection needs an
|
|
104
|
+
* exact, small prefix of the raw bytes and nothing else.
|
|
105
|
+
*
|
|
106
|
+
* Local files only: a prefix of a presigned object would mean fetching the whole
|
|
107
|
+
* object to read its first 512 bytes, which defeats the bound. An absent local
|
|
108
|
+
* file is `not_found`.
|
|
109
|
+
*/
|
|
110
|
+
readBytes(args: {
|
|
111
|
+
readonly session: AgentSession;
|
|
112
|
+
readonly path: string;
|
|
113
|
+
readonly length: number;
|
|
114
|
+
readonly workingDir?: string;
|
|
115
|
+
}): ResultAsync<ReadBytesResult, FsError>;
|
|
89
116
|
list(args: {
|
|
90
117
|
readonly session: AgentSession;
|
|
91
118
|
readonly path: string;
|
|
@@ -117,6 +117,33 @@ export function createWorkspaceFilesystem(deps) {
|
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
119
|
},
|
|
120
|
+
readBytes({ session, path, length, workingDir }) {
|
|
121
|
+
return confinedResolve(session, path, workingDir).andThen((resolved) => {
|
|
122
|
+
if (resolved.kind === "out_of_scope")
|
|
123
|
+
return okAsync({ kind: "out_of_scope" });
|
|
124
|
+
const absolute = resolved.absolute;
|
|
125
|
+
return safeStat(absolute).andThen((localStat) => {
|
|
126
|
+
if (!localStat?.isFile())
|
|
127
|
+
return okAsync({ kind: "not_found" });
|
|
128
|
+
return openReadNoFollow(absolute).andThen((opened) => {
|
|
129
|
+
if (opened.kind === "out_of_scope")
|
|
130
|
+
return okAsync({ kind: "out_of_scope" });
|
|
131
|
+
const fh = opened.fh;
|
|
132
|
+
return tryFs("workspace.readBytes", async () => {
|
|
133
|
+
try {
|
|
134
|
+
const want = Math.max(0, Math.min(length, localStat.size));
|
|
135
|
+
const buf = Buffer.alloc(want);
|
|
136
|
+
const { bytesRead } = want > 0 ? await fh.read(buf, 0, want, 0) : { bytesRead: 0 };
|
|
137
|
+
return { kind: "ok", bytes: buf.subarray(0, bytesRead) };
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await fh.close();
|
|
141
|
+
}
|
|
142
|
+
}, { path: absolute });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
},
|
|
120
147
|
list({ session, path, workingDir }) {
|
|
121
148
|
return confinedResolve(session, path, workingDir).andThen((resolved) => {
|
|
122
149
|
if (resolved.kind === "out_of_scope") {
|
|
@@ -159,27 +159,6 @@ export declare function tailWritePrefix(args: {
|
|
|
159
159
|
* a step briefing names (see the harness-workspace-tools spec).
|
|
160
160
|
*/
|
|
161
161
|
export declare function toSandboxPath(workspaceRoot: string, resourceId: string, hostAbsPath: string): string;
|
|
162
|
-
/**
|
|
163
|
-
* Parent of every preview, workspace-root-relative. Callers that enumerate
|
|
164
|
-
* previews join this; callers that address one join {@link previewDir}. Both
|
|
165
|
-
* spell the segment through this constant so the tree has one owner.
|
|
166
|
-
*/
|
|
167
|
-
export declare const PREVIEWS_ROOT = "previews";
|
|
168
|
-
/**
|
|
169
|
-
* Versioned preview directory, workspace-root-relative. Previews live inside
|
|
170
|
-
* the analysis tree (`previews/{previewId}/v{N}`); the content-token `res`
|
|
171
|
-
* claim keeps the separate `previews/{analysisId}/{previewId}` formula in
|
|
172
|
-
* `contracts/content-url.ts` — that is URL space, not a filesystem sub-path,
|
|
173
|
-
* and hosts that serve previews map one onto the other themselves.
|
|
174
|
-
*/
|
|
175
|
-
export declare function previewVersionDir(previewId: string, version: number): string;
|
|
176
|
-
/** Preview root for a specific preview (all versions), workspace-root-relative. */
|
|
177
|
-
export declare function previewDir(previewId: string): string;
|
|
178
|
-
/**
|
|
179
|
-
* Find the highest version number from a list of version directory names.
|
|
180
|
-
* Expects names like "v1", "v2", etc. Returns 0 if no versions found.
|
|
181
|
-
*/
|
|
182
|
-
export declare function latestPreviewVersion(dirNames: string[]): number;
|
|
183
162
|
/**
|
|
184
163
|
* Parent of every report session, workspace-root-relative. Callers that
|
|
185
164
|
* enumerate the sessions join this. Callers that address one join
|
package/dist/workspace/paths.js
CHANGED
|
@@ -151,43 +151,6 @@ export function toSandboxPath(workspaceRoot, resourceId, hostAbsPath) {
|
|
|
151
151
|
}
|
|
152
152
|
return tail === "" ? `/${resourceId}` : `/${resourceId}/${tail}`;
|
|
153
153
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Parent of every preview, workspace-root-relative. Callers that enumerate
|
|
156
|
-
* previews join this; callers that address one join {@link previewDir}. Both
|
|
157
|
-
* spell the segment through this constant so the tree has one owner.
|
|
158
|
-
*/
|
|
159
|
-
export const PREVIEWS_ROOT = "previews";
|
|
160
|
-
/**
|
|
161
|
-
* Versioned preview directory, workspace-root-relative. Previews live inside
|
|
162
|
-
* the analysis tree (`previews/{previewId}/v{N}`); the content-token `res`
|
|
163
|
-
* claim keeps the separate `previews/{analysisId}/{previewId}` formula in
|
|
164
|
-
* `contracts/content-url.ts` — that is URL space, not a filesystem sub-path,
|
|
165
|
-
* and hosts that serve previews map one onto the other themselves.
|
|
166
|
-
*/
|
|
167
|
-
export function previewVersionDir(previewId, version) {
|
|
168
|
-
return `${previewDir(previewId)}/v${version}`;
|
|
169
|
-
}
|
|
170
|
-
/** Preview root for a specific preview (all versions), workspace-root-relative. */
|
|
171
|
-
export function previewDir(previewId) {
|
|
172
|
-
assertSafeId(previewId, "previewId");
|
|
173
|
-
return `${PREVIEWS_ROOT}/${previewId}`;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Find the highest version number from a list of version directory names.
|
|
177
|
-
* Expects names like "v1", "v2", etc. Returns 0 if no versions found.
|
|
178
|
-
*/
|
|
179
|
-
export function latestPreviewVersion(dirNames) {
|
|
180
|
-
let max = 0;
|
|
181
|
-
for (const name of dirNames) {
|
|
182
|
-
const match = name.match(/^v(\d+)$/);
|
|
183
|
-
if (match) {
|
|
184
|
-
const n = parseInt(match[1], 10);
|
|
185
|
-
if (n > max)
|
|
186
|
-
max = n;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return max;
|
|
190
|
-
}
|
|
191
154
|
/**
|
|
192
155
|
* Parent of every report session, workspace-root-relative. Callers that
|
|
193
156
|
* enumerate the sessions join this. Callers that address one join
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inflexa-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|
|
@@ -76,7 +76,6 @@
|
|
|
76
76
|
"mime": "^4.1.0",
|
|
77
77
|
"nanoid": "^5.1.16",
|
|
78
78
|
"neverthrow": "^8.2.0",
|
|
79
|
-
"nunjucks": "^3.2.4",
|
|
80
79
|
"openai": "^7.4.0",
|
|
81
80
|
"p-queue": "^9.3.3",
|
|
82
81
|
"pdf-parse": "^2.4.5",
|
|
@@ -89,7 +88,6 @@
|
|
|
89
88
|
"@types/bun": "latest",
|
|
90
89
|
"@types/dockerode": "^3.3.47",
|
|
91
90
|
"@types/node": "^24.13.3",
|
|
92
|
-
"@types/nunjucks": "^3.2.6",
|
|
93
91
|
"@types/pdf-parse": "^1.1.5",
|
|
94
92
|
"@types/pg": "^8.20.0",
|
|
95
93
|
"csstree-validator": "^4.0.1",
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Report Builder Agent — in-process templating agent (no sandbox).
|
|
3
|
-
*
|
|
4
|
-
* Authors HTML reports via Jinja2 templating. Runs in the Cortex Node
|
|
5
|
-
* process — file IO is in-process via the `versionFs` tool roster, rendering
|
|
6
|
-
* is Node-side via Nunjucks (`build_report` tool), and visual validation
|
|
7
|
-
* uses headless Chrome via the `preview_snapshot` tool.
|
|
8
|
-
*
|
|
9
|
-
* The agent receives a complete brief from iterate-report's pre-flight
|
|
10
|
-
* (assets staged, columns parsed, sections fully composed) and has no
|
|
11
|
-
* discovery surface — its workspace is rooted at the version directory.
|
|
12
|
-
*
|
|
13
|
-
* NOT a sandbox-agent catalog member (per design Decision #2 — not
|
|
14
|
-
* plannable). The full tool roster is constructed inside the runner so
|
|
15
|
-
* each tool shares the runner's closure-captured outcome state and
|
|
16
|
-
* preview-dir paths; this factory just packages the result into a plain
|
|
17
|
-
* `AgentDefinition`.
|
|
18
|
-
*/
|
|
19
|
-
import type { AgentDefinition } from "../loop/types.js";
|
|
20
|
-
import type { Tool } from "../tools/define-tool.js";
|
|
21
|
-
export declare const REPORT_BUILDER_AGENT_ID = "report-builder";
|
|
22
|
-
/** Skill pack the report-builder prompt directs the model to read (design-system reference). */
|
|
23
|
-
export declare const REPORT_BUILDER_SKILLS: readonly ["report-html"];
|
|
24
|
-
export interface CreateReportBuilderAgentDeps {
|
|
25
|
-
/** Model id — provenance / metric label; the provider owns the wire model. */
|
|
26
|
-
readonly model: string;
|
|
27
|
-
/**
|
|
28
|
-
* Full tool roster — the 4 custom report tools plus the in-process
|
|
29
|
-
* versionFs surface, all constructed inside the runner so they close
|
|
30
|
-
* over the iteration's outcome state and preview-dir paths.
|
|
31
|
-
*/
|
|
32
|
-
readonly tools: readonly Tool[];
|
|
33
|
-
}
|
|
34
|
-
export declare function createReportBuilderAgent(deps: CreateReportBuilderAgentDeps): AgentDefinition;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Report Builder Agent — in-process templating agent (no sandbox).
|
|
3
|
-
*
|
|
4
|
-
* Authors HTML reports via Jinja2 templating. Runs in the Cortex Node
|
|
5
|
-
* process — file IO is in-process via the `versionFs` tool roster, rendering
|
|
6
|
-
* is Node-side via Nunjucks (`build_report` tool), and visual validation
|
|
7
|
-
* uses headless Chrome via the `preview_snapshot` tool.
|
|
8
|
-
*
|
|
9
|
-
* The agent receives a complete brief from iterate-report's pre-flight
|
|
10
|
-
* (assets staged, columns parsed, sections fully composed) and has no
|
|
11
|
-
* discovery surface — its workspace is rooted at the version directory.
|
|
12
|
-
*
|
|
13
|
-
* NOT a sandbox-agent catalog member (per design Decision #2 — not
|
|
14
|
-
* plannable). The full tool roster is constructed inside the runner so
|
|
15
|
-
* each tool shares the runner's closure-captured outcome state and
|
|
16
|
-
* preview-dir paths; this factory just packages the result into a plain
|
|
17
|
-
* `AgentDefinition`.
|
|
18
|
-
*/
|
|
19
|
-
import { reportBuilderPrompt } from "../prompts/report-builder.js";
|
|
20
|
-
import { composeSystemPrompt } from "./system-prompt.js";
|
|
21
|
-
export const REPORT_BUILDER_AGENT_ID = "report-builder";
|
|
22
|
-
/** Skill pack the report-builder prompt directs the model to read (design-system reference). */
|
|
23
|
-
export const REPORT_BUILDER_SKILLS = ["report-html"];
|
|
24
|
-
/** Runaway guard — Jinja recovery may take several build/edit cycles. */
|
|
25
|
-
const REPORT_BUILDER_MAX_ITERATIONS = 75;
|
|
26
|
-
export function createReportBuilderAgent(deps) {
|
|
27
|
-
return {
|
|
28
|
-
id: REPORT_BUILDER_AGENT_ID,
|
|
29
|
-
systemPrompt: composeSystemPrompt(reportBuilderPrompt),
|
|
30
|
-
model: deps.model,
|
|
31
|
-
tools: deps.tools,
|
|
32
|
-
maxIterations: REPORT_BUILDER_MAX_ITERATIONS,
|
|
33
|
-
};
|
|
34
|
-
}
|