@inflexa-ai/harness 0.5.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 +15 -0
- package/dist/agents/conversation-agent.js +12 -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/state/init.js +31 -5
- package/dist/tasks/data-profile.d.ts +12 -0
- package/dist/tasks/data-profile.js +2 -0
- package/dist/tools/approval/contract.d.ts +8 -0
- package/dist/tools/approval/gateway.js +5 -3
- package/dist/tools/approval/queries.d.ts +4 -2
- package/dist/tools/approval/queries.js +29 -14
- 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
|
@@ -52,6 +52,18 @@ export interface DataProfileDeps {
|
|
|
52
52
|
readonly embedding: EmbeddingProvider;
|
|
53
53
|
/** Absolute path to the skills tree (one subdirectory per skill). */
|
|
54
54
|
readonly skillsDir: string;
|
|
55
|
+
/**
|
|
56
|
+
* Host path of the library store's `packages.txt`. Omit when the host mounts the
|
|
57
|
+
* store at the sandbox's own path; a host whose store is baked into the image must
|
|
58
|
+
* inject its extracted copy, or the profiler reads the inventory as unknown.
|
|
59
|
+
*/
|
|
60
|
+
readonly packagesFile?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Host path of the reference store. Omit only when none is provisioned: the
|
|
63
|
+
* profiler carries reference discovery, and without this it reports an installed
|
|
64
|
+
* store as absent.
|
|
65
|
+
*/
|
|
66
|
+
readonly refStorePath?: string;
|
|
55
67
|
}
|
|
56
68
|
/**
|
|
57
69
|
* Workflow input. JSON-serialisable — DBOS persists it as the workflow's
|
|
@@ -216,6 +216,8 @@ export async function runDataProfileBody(input, deps) {
|
|
|
216
216
|
embedding: deps.embedding,
|
|
217
217
|
model: deps.model,
|
|
218
218
|
skillsDir: deps.skillsDir,
|
|
219
|
+
...(deps.packagesFile ? { packagesFile: deps.packagesFile } : {}),
|
|
220
|
+
...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}),
|
|
219
221
|
bioKeys: deps.bioKeys,
|
|
220
222
|
step: {
|
|
221
223
|
sandbox,
|
|
@@ -21,6 +21,14 @@ export interface AskRequest {
|
|
|
21
21
|
readonly title: string;
|
|
22
22
|
readonly command: string;
|
|
23
23
|
readonly detail?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Keys the standing grant an `always` records when the class being blessed is
|
|
26
|
+
* broader than the displayed `command` — a tool that grants a whole family of
|
|
27
|
+
* operations passes the family's key here while still showing one concrete
|
|
28
|
+
* command. It is a generic, opaque key string, not a tool- or domain-specific
|
|
29
|
+
* field, and the surface never renders it. Absent, the grant keys on `command`.
|
|
30
|
+
*/
|
|
31
|
+
readonly grantKey?: string;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* A user's decision on an approval request — three distinct outcomes, never a
|
|
@@ -27,11 +27,13 @@ export function createAskGateway(deps) {
|
|
|
27
27
|
title: request.title,
|
|
28
28
|
command: request.command,
|
|
29
29
|
detail: request.detail ?? null,
|
|
30
|
+
grantKey: request.grantKey ?? null,
|
|
30
31
|
createdAt: now,
|
|
31
32
|
};
|
|
32
|
-
// A standing grant for this
|
|
33
|
-
//
|
|
34
|
-
|
|
33
|
+
// A standing grant for this ask's grant key — the command unless the tool
|
|
34
|
+
// supplied a broader key — short-circuits the prompt: record the invocation as
|
|
35
|
+
// `resolved` for the audit trail and return without pausing.
|
|
36
|
+
if (unwrapOrThrow(await selectGrant(pool, ctx.analysisId, request.grantKey ?? request.command))) {
|
|
35
37
|
unwrapOrThrow(await insertGrantedAsk(pool, row, now));
|
|
36
38
|
return { kind: "always" };
|
|
37
39
|
}
|
|
@@ -40,6 +40,8 @@ export interface AskRow {
|
|
|
40
40
|
readonly title: string;
|
|
41
41
|
readonly command: string;
|
|
42
42
|
readonly detail: string | null;
|
|
43
|
+
/** Grant key an `always` keys its standing grant under; `null` falls back to `command`. */
|
|
44
|
+
readonly grantKey: string | null;
|
|
43
45
|
readonly createdAt: string;
|
|
44
46
|
}
|
|
45
47
|
/** Insert a fresh `pending` ask before its decision is awaited. */
|
|
@@ -50,8 +52,8 @@ export declare function insertPendingAsk(querier: Querier, row: AskRow): ResultA
|
|
|
50
52
|
* every approval-gated action. The recorded reply is `{ kind: "always" }`.
|
|
51
53
|
*/
|
|
52
54
|
export declare function insertGrantedAsk(querier: Querier, row: AskRow, resolvedAt: string): ResultAsync<void, DbError>;
|
|
53
|
-
/** Does a standing grant cover `(analysisId,
|
|
54
|
-
export declare function selectGrant(querier: Querier, analysisId: string,
|
|
55
|
+
/** Does a standing grant cover `(analysisId, grantKey)` in this analysis? */
|
|
56
|
+
export declare function selectGrant(querier: Querier, analysisId: string, grantKey: string): ResultAsync<boolean, DbError>;
|
|
55
57
|
/** Read a row's current status + recorded reply. `null` when no such row exists. */
|
|
56
58
|
export declare function selectAskResolution(querier: Querier, id: string): ResultAsync<AskResolution | null, DbError>;
|
|
57
59
|
/**
|
|
@@ -15,9 +15,9 @@ export function insertPendingAsk(querier, row) {
|
|
|
15
15
|
return tryMutation("asks.insertPending", async () => {
|
|
16
16
|
await querier.query({
|
|
17
17
|
text: `INSERT INTO cortex_asks
|
|
18
|
-
(id, analysis_id, thread_id, title, command, detail, status, created_at)
|
|
19
|
-
VALUES ($1, $2, $3, $4, $5, $6, 'pending', $
|
|
20
|
-
values: [row.id, row.analysisId, row.threadId, row.title, row.command, row.detail, row.createdAt],
|
|
18
|
+
(id, analysis_id, thread_id, title, command, detail, grant_key, status, created_at)
|
|
19
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8)`,
|
|
20
|
+
values: [row.id, row.analysisId, row.threadId, row.title, row.command, row.detail, row.grantKey ?? row.command, row.createdAt],
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -30,18 +30,29 @@ export function insertGrantedAsk(querier, row, resolvedAt) {
|
|
|
30
30
|
return tryMutation("asks.insertGranted", async () => {
|
|
31
31
|
await querier.query({
|
|
32
32
|
text: `INSERT INTO cortex_asks
|
|
33
|
-
(id, analysis_id, thread_id, title, command, detail, status, reply, created_at, resolved_at)
|
|
34
|
-
VALUES ($1, $2, $3, $4, $5, $6, 'resolved', $
|
|
35
|
-
values: [
|
|
33
|
+
(id, analysis_id, thread_id, title, command, detail, grant_key, status, reply, created_at, resolved_at)
|
|
34
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, 'resolved', $8::jsonb, $9, $10)`,
|
|
35
|
+
values: [
|
|
36
|
+
row.id,
|
|
37
|
+
row.analysisId,
|
|
38
|
+
row.threadId,
|
|
39
|
+
row.title,
|
|
40
|
+
row.command,
|
|
41
|
+
row.detail,
|
|
42
|
+
row.grantKey ?? row.command,
|
|
43
|
+
JSON.stringify({ kind: "always" }),
|
|
44
|
+
row.createdAt,
|
|
45
|
+
resolvedAt,
|
|
46
|
+
],
|
|
36
47
|
});
|
|
37
48
|
});
|
|
38
49
|
}
|
|
39
|
-
/** Does a standing grant cover `(analysisId,
|
|
40
|
-
export function selectGrant(querier, analysisId,
|
|
50
|
+
/** Does a standing grant cover `(analysisId, grantKey)` in this analysis? */
|
|
51
|
+
export function selectGrant(querier, analysisId, grantKey) {
|
|
41
52
|
return tryQuery("asks.selectGrant", async () => {
|
|
42
53
|
const result = await querier.query({
|
|
43
|
-
text: `SELECT 1 FROM cortex_ask_grants WHERE analysis_id = $1 AND
|
|
44
|
-
values: [analysisId,
|
|
54
|
+
text: `SELECT 1 FROM cortex_ask_grants WHERE analysis_id = $1 AND grant_key = $2`,
|
|
55
|
+
values: [analysisId, grantKey],
|
|
45
56
|
});
|
|
46
57
|
return (result.rowCount ?? 0) > 0;
|
|
47
58
|
});
|
|
@@ -114,19 +125,23 @@ export function answerAsk(pool, id, reply, now) {
|
|
|
114
125
|
return withTransaction(pool, "asks.answer", (client) => tryMutation("asks.answer:update", () => client.query({
|
|
115
126
|
text: `UPDATE cortex_asks SET status = $1, reply = $2::jsonb, resolved_at = $3
|
|
116
127
|
WHERE id = $4 AND status = 'pending'
|
|
117
|
-
RETURNING analysis_id, command`,
|
|
128
|
+
RETURNING analysis_id, COALESCE(grant_key, command) AS grant_key`,
|
|
118
129
|
values: [status, JSON.stringify(reply), now, id],
|
|
119
130
|
})).andThen((updated) => {
|
|
120
131
|
if ((updated.rowCount ?? 0) === 0)
|
|
121
132
|
return discriminateMiss(client, id);
|
|
122
133
|
if (reply.kind !== "always")
|
|
123
134
|
return okAsync("applied");
|
|
135
|
+
// The cast's non-null `grant_key` is sound without any boot-order
|
|
136
|
+
// caveat: every insert writes grant_key (command when the tool supplied
|
|
137
|
+
// none), and the COALESCE above covers any pending row that predates
|
|
138
|
+
// the column — `command` is NOT NULL, so the key can never be null.
|
|
124
139
|
const granted = updated.rows[0];
|
|
125
140
|
return tryMutation("asks.answer:grant", async () => {
|
|
126
141
|
await client.query({
|
|
127
|
-
text: `INSERT INTO cortex_ask_grants (analysis_id,
|
|
128
|
-
VALUES ($1, $2, $3) ON CONFLICT (analysis_id,
|
|
129
|
-
values: [granted.analysis_id, granted.
|
|
142
|
+
text: `INSERT INTO cortex_ask_grants (analysis_id, grant_key, created_at)
|
|
143
|
+
VALUES ($1, $2, $3) ON CONFLICT (analysis_id, grant_key) DO NOTHING`,
|
|
144
|
+
values: [granted.analysis_id, granted.grant_key, now],
|
|
130
145
|
});
|
|
131
146
|
}).map(() => "applied");
|
|
132
147
|
}));
|
|
@@ -41,6 +41,13 @@ export interface GeneratePlanDeps {
|
|
|
41
41
|
* default guidance and validation skips the ceiling check.
|
|
42
42
|
*/
|
|
43
43
|
readonly resourcePolicy?: ResourcePolicy;
|
|
44
|
+
/**
|
|
45
|
+
* Host path of the reference store, giving the planner reference discovery.
|
|
46
|
+
* Absent, the tool reports the store as unavailable and the planner keeps
|
|
47
|
+
* planning as though nothing is guaranteed present — which is what the prompt
|
|
48
|
+
* already tells it to do.
|
|
49
|
+
*/
|
|
50
|
+
readonly refStorePath?: string;
|
|
44
51
|
}
|
|
45
52
|
/** Build the `generate_plan` tool bound to its provider and pool. */
|
|
46
53
|
export declare function createGeneratePlanTool(deps: GeneratePlanDeps): Tool;
|
|
@@ -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 {};
|