@inflexa-ai/harness 0.6.0 → 0.7.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/conversation-agent.d.ts +7 -0
- package/dist/agents/conversation-agent.js +9 -2
- package/dist/agents/sandbox/shared.d.ts +12 -0
- package/dist/agents/sandbox/shared.js +3 -11
- package/dist/execution/ephemeral-runner.d.ts +12 -0
- package/dist/execution/ephemeral-runner.js +2 -0
- package/dist/prompts/planner.js +26 -0
- package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
- package/dist/prompts/sandbox/data-profiler.js +5 -4
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +34 -21
- package/dist/reference-data/catalog.d.ts +33 -3
- package/dist/reference-data/catalog.js +213 -8
- package/dist/tasks/data-profile.d.ts +12 -0
- package/dist/tasks/data-profile.js +2 -0
- package/dist/tools/research/generate-plan.d.ts +7 -0
- package/dist/tools/research/generate-plan.js +8 -3
- package/dist/tools/sandbox/list-available-packages.d.ts +16 -3
- package/dist/tools/sandbox/list-available-packages.js +72 -38
- package/dist/tools/sandbox/list-available-refs.d.ts +31 -75
- package/dist/tools/sandbox/list-available-refs.js +352 -443
- package/package.json +1 -1
|
@@ -32,6 +32,7 @@ import { runToTerminal } from "../../loop/run-to-terminal.js";
|
|
|
32
32
|
import { passthroughStep } from "../../loop/run-step.js";
|
|
33
33
|
import { forSubAgent, scopeResource } from "../../auth/types.js";
|
|
34
34
|
import { defineTool } from "../define-tool.js";
|
|
35
|
+
import { createListAvailableRefsTool } from "../sandbox/list-available-refs.js";
|
|
35
36
|
import { createReportBlockerToolFor } from "../sandbox/report-blocker.js";
|
|
36
37
|
import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../../app/data-profile-orientation.js";
|
|
37
38
|
import { isDataProfileStale } from "../../app/data-profile-policy.js";
|
|
@@ -249,7 +250,7 @@ function fullyValidate(candidate, resourcePolicy) {
|
|
|
249
250
|
* shared `holder` so the outer `execute` reads the terminal outcome after the
|
|
250
251
|
* loop finishes.
|
|
251
252
|
*/
|
|
252
|
-
function buildInnerTools(holder, persistCtx, pool, resourcePolicy) {
|
|
253
|
+
function buildInnerTools(holder, persistCtx, pool, resourcePolicy, refStorePath) {
|
|
253
254
|
const validatePlanTool = defineTool({
|
|
254
255
|
id: "validate_plan",
|
|
255
256
|
description: "Dry-run a candidate plan and get back everything that is wrong with it. " +
|
|
@@ -353,8 +354,12 @@ function buildInnerTools(holder, persistCtx, pool, resourcePolicy) {
|
|
|
353
354
|
"incompatible with every available agent, etc.) — not for a plan you " +
|
|
354
355
|
"could fix and submit.",
|
|
355
356
|
});
|
|
357
|
+
// Reference discovery is non-terminal and read-only: it lets the planner check what
|
|
358
|
+
// reference data this environment actually holds before committing a step to needing
|
|
359
|
+
// it. It reads the store host-side, so no sandbox exists or is needed at plan time.
|
|
360
|
+
const listAvailableRefs = createListAvailableRefsTool(refStorePath === undefined ? {} : { refStorePath });
|
|
356
361
|
const terminal = [submitPlanTool, requestClarificationTool, reportBlockerTool];
|
|
357
|
-
return { all: [validatePlanTool, ...terminal], terminal };
|
|
362
|
+
return { all: [validatePlanTool, listAvailableRefs, ...terminal], terminal };
|
|
358
363
|
}
|
|
359
364
|
/**
|
|
360
365
|
* Translate the captured `PlannerOutcome` (plus any loop error) into the
|
|
@@ -486,7 +491,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
486
491
|
analysisId,
|
|
487
492
|
parentPlanId: input.parentPlanId ?? null,
|
|
488
493
|
};
|
|
489
|
-
const innerTools = buildInnerTools(holder, persistCtx, deps.pool, deps.resourcePolicy);
|
|
494
|
+
const innerTools = buildInnerTools(holder, persistCtx, deps.pool, deps.resourcePolicy, deps.refStorePath);
|
|
490
495
|
const planner = {
|
|
491
496
|
id: PLANNER_AGENT_ID,
|
|
492
497
|
systemPrompt: composeSystemPrompt(plannerInstructions(deps.resourcePolicy)),
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* listAvailablePackages — query the R/Python/CLI/Node packages available in the
|
|
3
3
|
* sandbox.
|
|
4
4
|
*
|
|
5
|
-
* The data source is
|
|
6
|
-
*
|
|
5
|
+
* The data source is the store's `packages.txt`, assembled by the library-store
|
|
6
|
+
* build (see the lib-store-build spec) and read from wherever the HOST can see
|
|
7
|
+
* it — the container path when the host mounts the same store, an injected path
|
|
8
|
+
* when it does not. Its shape is fixed by the
|
|
7
9
|
* producers (`scripts/lib-store-common.sh`, `images/sandbox-python/inflexa-libs-refresh`):
|
|
8
10
|
* two `#` advisory lines, then one `## <Section>` heading per language track
|
|
9
11
|
* followed by that track's packages as a single comma-separated line.
|
|
@@ -73,7 +75,18 @@ export interface PackagesQuery {
|
|
|
73
75
|
* is only the file read plus this call.
|
|
74
76
|
*/
|
|
75
77
|
export declare function queryPackages(sections: readonly Section[], { names, query, language, limit }: PackagesQuery): PackagesResult;
|
|
76
|
-
export
|
|
78
|
+
export interface ListAvailablePackagesDeps {
|
|
79
|
+
/**
|
|
80
|
+
* Path to the store's `packages.txt` AS THE HOST SEES IT. Defaults to the
|
|
81
|
+
* container path, which is correct only when the host mounts the same store.
|
|
82
|
+
* A host that does not — one whose store is baked into the sandbox image and
|
|
83
|
+
* never bind-mounted — must inject the path to its own extracted copy, or the
|
|
84
|
+
* read fails and every caller is told the inventory is unknown.
|
|
85
|
+
*/
|
|
86
|
+
readonly packagesFile?: string;
|
|
87
|
+
}
|
|
88
|
+
/** Create the package inventory over a host-readable `packages.txt`. */
|
|
89
|
+
export declare function createListAvailablePackagesTool(deps?: ListAvailablePackagesDeps): import("../define-tool.js").Tool<{
|
|
77
90
|
names?: string[] | undefined;
|
|
78
91
|
query?: string | undefined;
|
|
79
92
|
language?: "r" | "python" | "cli" | "node" | undefined;
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* listAvailablePackages — query the R/Python/CLI/Node packages available in the
|
|
3
3
|
* sandbox.
|
|
4
4
|
*
|
|
5
|
-
* The data source is
|
|
6
|
-
*
|
|
5
|
+
* The data source is the store's `packages.txt`, assembled by the library-store
|
|
6
|
+
* build (see the lib-store-build spec) and read from wherever the HOST can see
|
|
7
|
+
* it — the container path when the host mounts the same store, an injected path
|
|
8
|
+
* when it does not. Its shape is fixed by the
|
|
7
9
|
* producers (`scripts/lib-store-common.sh`, `images/sandbox-python/inflexa-libs-refresh`):
|
|
8
10
|
* two `#` advisory lines, then one `## <Section>` heading per language track
|
|
9
11
|
* followed by that track's packages as a single comma-separated line.
|
|
@@ -24,10 +26,32 @@ import { readFile } from "node:fs/promises";
|
|
|
24
26
|
import { ok } from "neverthrow";
|
|
25
27
|
import { z } from "zod";
|
|
26
28
|
import { defineTool } from "../define-tool.js";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Where the list lives when the host mounts the library store at the same path
|
|
31
|
+
* the sandbox does. Hosts that do not — the store is baked into the image and
|
|
32
|
+
* never bind-mounted — inject their own path instead.
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_PACKAGES_FILE = "/mnt/libs/current/packages.txt";
|
|
35
|
+
/**
|
|
36
|
+
* Default cap on a listing — high enough that the real store is never truncated.
|
|
37
|
+
* The shipped catalog is ~270 packages (~3 KB, well under a thousand tokens), so
|
|
38
|
+
* a low default bought nothing and cost correctness: a partial listing reads as a
|
|
39
|
+
* complete one, and an agent concludes a package is absent when it was merely not
|
|
40
|
+
* rendered. The cap survives only as a backstop for a downstream `FROM` image
|
|
41
|
+
* that adds a pathological number of packages.
|
|
42
|
+
*/
|
|
43
|
+
const DEFAULT_LIMIT = 2_000;
|
|
44
|
+
/**
|
|
45
|
+
* Naming packages here would be worse than saying nothing: this state means the
|
|
46
|
+
* list could not be read, so any roll-call is a guess the agent has no way to
|
|
47
|
+
* check, and an agent told to "assume numpy is available" will import it and
|
|
48
|
+
* fail at runtime instead of probing first.
|
|
49
|
+
*/
|
|
50
|
+
const UNAVAILABLE_NOTE = "Package list not available — the library store's inventory could not be read, so what is installed is UNKNOWN. " +
|
|
51
|
+
"Do not assume any package is present, and do not infer one from the analysis you were asked to run. " +
|
|
52
|
+
"Probe each package you intend to use before relying on it (`python3 -c 'import <pkg>'`, " +
|
|
53
|
+
'R `requireNamespace("<pkg>", quietly = TRUE)`) and degrade gracefully when it is absent. ' +
|
|
54
|
+
"Nothing can be installed at runtime.";
|
|
31
55
|
/** The `language` filter values, mapped onto the concrete section headings. */
|
|
32
56
|
const LANGUAGE_MATCHERS = {
|
|
33
57
|
// The R triple: `R (CRAN)`, `R (Bioconductor)`, `R (GitHub)`.
|
|
@@ -135,35 +159,45 @@ export function queryPackages(sections, { names, query, language, limit }) {
|
|
|
135
159
|
const { content, returned } = renderListing(filtered, limit ?? DEFAULT_LIMIT);
|
|
136
160
|
return { available: true, total, returned, hasMore: returned < total, content };
|
|
137
161
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
/** Create the package inventory over a host-readable `packages.txt`. */
|
|
163
|
+
export function createListAvailablePackagesTool(deps = {}) {
|
|
164
|
+
const packagesFile = deps.packagesFile ?? DEFAULT_PACKAGES_FILE;
|
|
165
|
+
return defineTool({
|
|
166
|
+
id: "list_available_packages",
|
|
167
|
+
description: "Query the R, Python, CLI, and Node packages installed in the sandbox. No packages can be installed at runtime — only what this tool reports is importable. " +
|
|
168
|
+
"A full listing is small (a few hundred packages) and is returned whole by default, so a listing you get back is the complete set unless `hasMore` says otherwise. " +
|
|
169
|
+
'`names`: check specific packages (e.g. ["Seurat", "scanpy"]) — returns present/absent plus the language track for each, case-insensitively; this is the cheapest call and the right one for \'is X available?\'. ' +
|
|
170
|
+
"`query`: case-insensitive substring filter over package names. " +
|
|
171
|
+
"`language`: restrict to one track (r | python | cli | node). " +
|
|
172
|
+
"`limit`: cap the packages listed; the response always carries the true `total` and a `hasMore` flag, so truncation is never silent. " +
|
|
173
|
+
'The package list carries NO version numbers — this tool cannot report a package\'s version. Check a version at runtime instead (e.g. `python -c "import scanpy; print(scanpy.__version__)"`).',
|
|
174
|
+
inputSchema: z.object({
|
|
175
|
+
names: z
|
|
176
|
+
.array(z.string())
|
|
177
|
+
.max(100)
|
|
178
|
+
.optional()
|
|
179
|
+
.describe("Check these exact package names for presence (case-insensitive). Returns one entry per name: present/absent + the language track it lives in."),
|
|
180
|
+
query: z.string().optional().describe("Case-insensitive substring filter over package names."),
|
|
181
|
+
language: z.enum(["r", "python", "cli", "node"]).optional().describe("Restrict results to one language track."),
|
|
182
|
+
limit: z
|
|
183
|
+
.number()
|
|
184
|
+
.int()
|
|
185
|
+
.min(1)
|
|
186
|
+
.max(DEFAULT_LIMIT)
|
|
187
|
+
.optional()
|
|
188
|
+
.describe(`Cap the packages listed. Omit to get the whole set — the store fits well within the ${DEFAULT_LIMIT} ceiling. Ignored when \`names\` is given.`),
|
|
189
|
+
}),
|
|
190
|
+
execute: async (input) => {
|
|
191
|
+
// An unreadable inventory is an expected environment state — model it as an
|
|
192
|
+
// `available: false` data variant telling the caller the set is UNKNOWN.
|
|
193
|
+
let raw;
|
|
194
|
+
try {
|
|
195
|
+
raw = await readFile(packagesFile, "utf-8");
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return ok({ available: false, content: UNAVAILABLE_NOTE });
|
|
199
|
+
}
|
|
200
|
+
return ok(queryPackages(parsePackagesFile(raw), input));
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reference-store discovery
|
|
3
|
-
* store
|
|
2
|
+
* Reference-store discovery: `createListAvailableRefsTool`, a bounded, no-follow
|
|
3
|
+
* inventory of what is ACTUALLY provisioned in the reference store (managed
|
|
4
|
+
* installs and user-added files alike). Narrow with `path`/`category`, filter
|
|
5
|
+
* with `query`, cap with `limit`; every response carries `returned`/`total`/
|
|
6
|
+
* `hasMore` and the `categories` present, so a listing is never an unbounded
|
|
7
|
+
* dump. Symlinks are never followed; nothing can be downloaded at runtime.
|
|
4
8
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The store is read from the host filesystem at the embedder-supplied
|
|
10
|
+
* `refStorePath` and reported at `/mnt/refs`, the container path sandboxes mount
|
|
11
|
+
* it at — read where it lives, render where the caller will open it. Reading it
|
|
12
|
+
* host-side is what lets a planner ask what reference data exists before any
|
|
13
|
+
* sandbox is created; a step agent asking mid-run sees the same store, because
|
|
14
|
+
* the mount and this path are the same bytes.
|
|
15
|
+
*
|
|
16
|
+
* The scan reports paths; meaning is joined on afterwards from the install
|
|
17
|
+
* receipt and the catalog it names (organism, format, and each file's internal
|
|
18
|
+
* shape). Callers search by what the data IS, so the store's directory layout
|
|
19
|
+
* stays an installer detail rather than an interface anything encodes. Metadata
|
|
20
|
+
* only ever ENRICHES an entry — a file with no receipt and no catalog match is
|
|
21
|
+
* still returned, because a user-provided store is as valid as a managed one.
|
|
14
22
|
*/
|
|
15
|
-
import type { SandboxClient } from "../../sandbox/client.js";
|
|
16
|
-
import type { SandboxRef } from "../../sandbox/types.js";
|
|
17
23
|
export interface ListAvailableRefsDeps {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
readonly
|
|
24
|
-
readonly markExecActive?: (execId: string) => Promise<void>;
|
|
25
|
-
/** Filesystem root used by the scanner; production callers omit this. */
|
|
26
|
-
readonly scanRoot?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Host path of the reference store — the same bytes sandboxes see at
|
|
26
|
+
* `/mnt/refs`. Omitted (or absent on disk) means no store is provisioned,
|
|
27
|
+
* which is a normal state reported as `unavailable`, not an error.
|
|
28
|
+
*/
|
|
29
|
+
readonly refStorePath?: string;
|
|
27
30
|
}
|
|
28
31
|
export interface ReferenceInventoryMetadata {
|
|
29
32
|
readonly datasetId?: string;
|
|
@@ -35,6 +38,10 @@ export interface ReferenceInventoryMetadata {
|
|
|
35
38
|
readonly category?: string;
|
|
36
39
|
readonly subtype?: string;
|
|
37
40
|
readonly organism?: string;
|
|
41
|
+
/** Logical content format, independent of compression — which reader to reach for. */
|
|
42
|
+
readonly format?: string;
|
|
43
|
+
/** The file's internal shape (key columns, identifier space), so a caller can use it without opening it first. */
|
|
44
|
+
readonly contents?: string;
|
|
38
45
|
readonly rows?: number;
|
|
39
46
|
readonly endpoint?: string;
|
|
40
47
|
}
|
|
@@ -42,7 +49,7 @@ interface ReferenceInventoryEntryBase {
|
|
|
42
49
|
readonly path: string;
|
|
43
50
|
readonly metadata?: ReferenceInventoryMetadata;
|
|
44
51
|
}
|
|
45
|
-
/** A bounded, no-follow observation
|
|
52
|
+
/** A bounded, no-follow observation of the reference store. */
|
|
46
53
|
export type ReferenceInventoryEntry = (ReferenceInventoryEntryBase & {
|
|
47
54
|
readonly kind: "file";
|
|
48
55
|
readonly bytes: number;
|
|
@@ -82,62 +89,11 @@ interface RawScan {
|
|
|
82
89
|
readonly receipts?: unknown[];
|
|
83
90
|
readonly legacyEntries?: unknown[];
|
|
84
91
|
}
|
|
85
|
-
/** Create
|
|
92
|
+
/** Create reference discovery over the host-visible reference store. */
|
|
86
93
|
export declare function createListAvailableRefsTool(deps: ListAvailableRefsDeps): import("../define-tool.js").Tool<{
|
|
87
94
|
path?: string | undefined;
|
|
88
95
|
category?: string | undefined;
|
|
89
96
|
query?: string | undefined;
|
|
90
97
|
limit?: number | undefined;
|
|
91
98
|
}, ReferenceInventoryResult>;
|
|
92
|
-
export interface RegistryEntry {
|
|
93
|
-
local_path: string;
|
|
94
|
-
sha256: string;
|
|
95
|
-
bytes: number;
|
|
96
|
-
rows: number | null;
|
|
97
|
-
category: string | null;
|
|
98
|
-
subtype: string | null;
|
|
99
|
-
organism: string | null;
|
|
100
|
-
tax_id: string | null;
|
|
101
|
-
dataset: string | null;
|
|
102
|
-
endpoint: string | null;
|
|
103
|
-
}
|
|
104
|
-
export interface Registry {
|
|
105
|
-
registry_version: string;
|
|
106
|
-
build_id: string;
|
|
107
|
-
generated_at: string;
|
|
108
|
-
files: {
|
|
109
|
-
by_category: Record<string, RegistryEntry[]>;
|
|
110
|
-
};
|
|
111
|
-
summary: {
|
|
112
|
-
total_output_files: number;
|
|
113
|
-
categories: string[];
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Either the store is unmounted, or a bounded listing: `categories` always names
|
|
118
|
-
* the full valid set (the drill-in index), and `total`/`hasMore` make truncation
|
|
119
|
-
* explicit.
|
|
120
|
-
*/
|
|
121
|
-
export type RefsResult = {
|
|
122
|
-
readonly available: false;
|
|
123
|
-
readonly content: string;
|
|
124
|
-
} | {
|
|
125
|
-
readonly available: true;
|
|
126
|
-
readonly total: number;
|
|
127
|
-
readonly returned: number;
|
|
128
|
-
readonly hasMore: boolean;
|
|
129
|
-
readonly categories: readonly string[];
|
|
130
|
-
readonly content: string;
|
|
131
|
-
};
|
|
132
|
-
/** The tool's input, as parsed from `inputSchema`. */
|
|
133
|
-
export interface RefsQuery {
|
|
134
|
-
readonly category?: string;
|
|
135
|
-
readonly query?: string;
|
|
136
|
-
readonly limit?: number;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Answer a refs query against a parsed registry. Pure — the tool's `execute` is
|
|
140
|
-
* only the file read plus this call.
|
|
141
|
-
*/
|
|
142
|
-
export declare function queryRefs(registry: Registry, { category, query, limit }: RefsQuery): RefsResult;
|
|
143
99
|
export {};
|