@inflexa-ai/harness 0.35.2 → 0.35.4
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/citations/types.d.ts +2 -2
- package/dist/config/environment-stores.d.ts +10 -0
- package/dist/contracts/schemas/chat-parts.d.ts +11 -11
- package/dist/execution/run-canceler.js +20 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/lib/metrics.d.ts +63 -5
- package/dist/lib/metrics.js +104 -5
- package/dist/lib/otel-spans.d.ts +77 -0
- package/dist/lib/otel-spans.js +123 -0
- package/dist/lib/otel.d.ts +12 -3
- package/dist/lib/otel.js +15 -4
- package/dist/loop/run-agent.js +13 -2
- package/dist/prompts/planner.js +6 -0
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +8 -3
- package/dist/runtime/dbos.d.ts +8 -7
- package/dist/sandbox/await-exec.js +16 -9
- package/dist/sandbox/notification-sweep.js +5 -2
- package/dist/sandbox/package-identity.d.ts +208 -0
- package/dist/sandbox/package-identity.js +244 -0
- package/dist/sandbox/reaper.js +5 -2
- package/dist/sandbox/submit-exec.js +6 -3
- package/dist/sandbox/types.d.ts +20 -25
- package/dist/sandbox/watchdog.js +8 -3
- package/dist/schemas/plan-schemas.d.ts +3 -3
- package/dist/schemas/validate-plan.js +32 -15
- package/dist/schemas/workflow-state.js +3 -1
- package/dist/state/data-profile.d.ts +20 -6
- package/dist/state/data-profile.js +16 -8
- package/dist/state/index.d.ts +1 -1
- package/dist/state/schema.d.ts +2 -2
- package/dist/tasks/data-profile.js +26 -12
- package/dist/tools/execute-analysis.js +37 -20
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/sandbox/link-packages.d.ts +6 -6
- package/dist/tools/sandbox/link-packages.js +45 -14
- package/dist/tools/sandbox/list-available-packages.d.ts +23 -1
- package/dist/tools/sandbox/list-available-packages.js +185 -43
- package/dist/workflows/execute-analysis.d.ts +4 -1
- package/dist/workflows/execute-analysis.js +40 -4
- package/dist/workflows/execute-target-assessment.js +68 -31
- package/dist/workflows/sandbox-step.js +7 -1
- package/package.json +1 -1
package/dist/state/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type { ActiveSandboxRow } from "./active-sandboxes.js";
|
|
|
21
21
|
export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
|
|
22
22
|
export type { InsertPlanInput, UpsertPlanInput } from "./plans.js";
|
|
23
23
|
export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
|
|
24
|
-
export type { DataProfileStatus, DataProfileResult, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileDimension, DataProfileGroup, DataProfileGroupSlot, DataProfileFile, DataProfileMemberAnnotation, DataProfileObservation, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
|
|
24
|
+
export type { DataProfileStatus, DataProfileTerminalWrite, DataProfileResult, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileDimension, DataProfileGroup, DataProfileGroupSlot, DataProfileFile, DataProfileMemberAnnotation, DataProfileObservation, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
|
|
25
25
|
export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
|
|
26
26
|
export { createAnalysisPurge } from "./purge-analysis.js";
|
|
27
27
|
export type { AnalysisPurge, AnalysisPurgeDeps, AnalysisPurgeOutcome } from "./purge-analysis.js";
|
package/dist/state/schema.d.ts
CHANGED
|
@@ -46,10 +46,10 @@ export declare const ArtifactRowSchema: z.ZodObject<{
|
|
|
46
46
|
}, z.core.$strip>;
|
|
47
47
|
export type ArtifactRow = z.infer<typeof ArtifactRowSchema>;
|
|
48
48
|
export declare const RunStatus: z.ZodEnum<{
|
|
49
|
-
partial: "partial";
|
|
50
49
|
running: "running";
|
|
51
50
|
completed: "completed";
|
|
52
51
|
failed: "failed";
|
|
52
|
+
partial: "partial";
|
|
53
53
|
canceled: "canceled";
|
|
54
54
|
suspended_insufficient_funds: "suspended_insufficient_funds";
|
|
55
55
|
}>;
|
|
@@ -77,10 +77,10 @@ export declare const CortexRunRowSchema: z.ZodObject<{
|
|
|
77
77
|
threadId: z.ZodNullable<z.ZodString>;
|
|
78
78
|
workflowName: z.ZodString;
|
|
79
79
|
status: z.ZodEnum<{
|
|
80
|
-
partial: "partial";
|
|
81
80
|
running: "running";
|
|
82
81
|
completed: "completed";
|
|
83
82
|
failed: "failed";
|
|
83
|
+
partial: "partial";
|
|
84
84
|
canceled: "canceled";
|
|
85
85
|
suspended_insufficient_funds: "suspended_insufficient_funds";
|
|
86
86
|
}>;
|
|
@@ -45,6 +45,7 @@ import { PROFILE_INDEX_TYPES, buildProfileIndexEntries } from "./data-profile-in
|
|
|
45
45
|
import { absorbRecipe, renderAbsorbDelta } from "./data-profile-absorb.js";
|
|
46
46
|
import { formatResolutionErrors, resolveProfileSubmission } from "./data-profile-resolve.js";
|
|
47
47
|
import { completeDataProfile, completeEmptyDataProfile, failDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, tryRerunDataProfile, tryStartDataProfile, upsertArtifacts, } from "../state/index.js";
|
|
48
|
+
import { elapsedSinceIso, recordRunCompleted } from "../lib/metrics.js";
|
|
48
49
|
import { createProfileActivityEmitter } from "./data-profile-activity.js";
|
|
49
50
|
import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.js";
|
|
50
51
|
// The run literal is declared in `contracts/` — a consumer reads it back off recorded usage, and this
|
|
@@ -316,8 +317,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
316
317
|
// catalogue's fit is measured, and a completion that emitted none would make the
|
|
317
318
|
// empty-manifest path invisible to the same monitoring that watches every other.
|
|
318
319
|
logProfileMonitoring(logger, { dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
|
|
319
|
-
|
|
320
|
-
logTerminalNoop(logger, analysisId, "completion");
|
|
320
|
+
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)), "completed");
|
|
321
321
|
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
322
322
|
// This is a terminal completion like any other, so it reports one — a consumer watching
|
|
323
323
|
// an empty-manifest profile sees it settle rather than seeing the stream simply stop.
|
|
@@ -370,8 +370,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
370
370
|
await activity.indexing();
|
|
371
371
|
await indexProfile({ deps, analysisId, session: runSession, record, scan, logger });
|
|
372
372
|
logProfileMonitoring(logger, { resolution, dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
|
|
373
|
-
|
|
374
|
-
logTerminalNoop(logger, analysisId, "completion");
|
|
373
|
+
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, record)), "completed");
|
|
375
374
|
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
376
375
|
await activity.complete();
|
|
377
376
|
return;
|
|
@@ -397,8 +396,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
397
396
|
repairRounds: 0,
|
|
398
397
|
absorb: absorb.kind,
|
|
399
398
|
});
|
|
400
|
-
|
|
401
|
-
logTerminalNoop(logger, analysisId, "completion");
|
|
399
|
+
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, absorbed)), "completed");
|
|
402
400
|
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
403
401
|
await activity.complete();
|
|
404
402
|
return;
|
|
@@ -575,9 +573,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
575
573
|
});
|
|
576
574
|
// 6. Complete — store the FULL profiler finding plus the input signature for
|
|
577
575
|
// staleness detection. The scratch tree is gone; this row is all that survives.
|
|
578
|
-
|
|
579
|
-
logTerminalNoop(logger, analysisId, "completion");
|
|
580
|
-
}
|
|
576
|
+
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, profileRecord)), "completed");
|
|
581
577
|
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
582
578
|
// LAST statement of the success path, and that placement is the guarantee that exactly
|
|
583
579
|
// one terminal activity is ever emitted. Everything that could still throw — the ledger
|
|
@@ -599,8 +595,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
599
595
|
catch (err) {
|
|
600
596
|
logger.error("profile failed", logger.errorFields(err));
|
|
601
597
|
const reason = profileFailureReason(err);
|
|
602
|
-
|
|
603
|
-
logTerminalNoop(logger, analysisId, "failure");
|
|
598
|
+
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, reason)), "failed");
|
|
604
599
|
await deps.runAuthorizer.revoke(authorization, "data-profile-failed");
|
|
605
600
|
// The same bounded, user-safe line the ledger receives — never the raw error, whose paths and
|
|
606
601
|
// stack frames stay in the log record above. This is the only other terminal emission, so
|
|
@@ -617,6 +612,21 @@ export async function runDataProfileBody(input, deps) {
|
|
|
617
612
|
function logTerminalNoop(logger, analysisId, write) {
|
|
618
613
|
logger.warn("terminal write skipped: ledger row not running (cleared or expired concurrently)", { analysisId, write });
|
|
619
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Settle one terminal write of the profile ledger. A refused stamp is logged
|
|
617
|
+
* (see `logTerminalNoop`). An accepted stamp records the profile's run outcome:
|
|
618
|
+
* the CAS admits one write per `running` claim, so this is the exactly-once
|
|
619
|
+
* point of the transition — a recovery replay of the body finds the row no
|
|
620
|
+
* longer `running`, and records nothing again. The duration runs from the
|
|
621
|
+
* `data_profile_started_at` the claim stamped, never from a body-local clock.
|
|
622
|
+
*/
|
|
623
|
+
function settleTerminalWrite(logger, analysisId, write, status) {
|
|
624
|
+
if (!write.stamped) {
|
|
625
|
+
logTerminalNoop(logger, analysisId, status === "completed" ? "completion" : "failure");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
recordRunCompleted({ workflow: "data_profile", status, durationMs: elapsedSinceIso(write.startedAt) });
|
|
629
|
+
}
|
|
620
630
|
/**
|
|
621
631
|
* Resolve the `RunSession` the workflow runs under. Whether the authorization
|
|
622
632
|
* is freshly minted (Cortex-owned) or reused (caller-owned) is decided by the
|
|
@@ -762,13 +772,17 @@ async function compensateStartFailure(deps, analysisId, phase, err) {
|
|
|
762
772
|
if (failed.isErr()) {
|
|
763
773
|
logger.error("failed to mark failed after a start error", { phase, err: failed.error });
|
|
764
774
|
}
|
|
765
|
-
else if (!failed.value) {
|
|
775
|
+
else if (!failed.value.stamped) {
|
|
766
776
|
// The row this compensation was written to fail is no longer `running` — a
|
|
767
777
|
// concurrent clear/expire already moved it on, so the running-CAS refused the
|
|
768
778
|
// stamp. Nothing wedged at `running`, which is the outcome compensation exists
|
|
769
779
|
// to guarantee; just record the no-op.
|
|
770
780
|
logTerminalNoop(logger, analysisId, "compensation");
|
|
771
781
|
}
|
|
782
|
+
else {
|
|
783
|
+
// A profile that failed at start is a failed profile: its claim opened a run the ledger now closes.
|
|
784
|
+
recordRunCompleted({ workflow: "data_profile", status: "failed", durationMs: elapsedSinceIso(failed.value.startedAt) });
|
|
785
|
+
}
|
|
772
786
|
}
|
|
773
787
|
/**
|
|
774
788
|
* Start the data-profile workflow for an already-claimed analysis (the retry
|
|
@@ -10,6 +10,7 @@ import { DEFAULT_SANDBOX_MAX_STEPS } from "../config/resource-limits.js";
|
|
|
10
10
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
11
11
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
12
12
|
import { buildRunCardData } from "../memory/card-builders.js";
|
|
13
|
+
import { formatQuery, parseQuery } from "../sandbox/package-identity.js";
|
|
13
14
|
import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
|
|
14
15
|
import { validatePlan } from "../schemas/validate-plan.js";
|
|
15
16
|
import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, reserveRunById, updateRunStatus, upsertPlan } from "../state/index.js";
|
|
@@ -80,39 +81,41 @@ export class PlanPackagesMissingError extends Error {
|
|
|
80
81
|
this.name = "PlanPackagesMissingError";
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
|
-
/** One plan package entry, in requirement form: a bare name, or a name with one exact version. */
|
|
84
|
-
function parseRequirement(entry) {
|
|
85
|
-
const split = entry.indexOf("==");
|
|
86
|
-
if (split < 0)
|
|
87
|
-
return { name: entry.trim() };
|
|
88
|
-
return { name: entry.slice(0, split).trim(), version: entry.slice(split + 2).trim() };
|
|
89
|
-
}
|
|
90
84
|
/**
|
|
91
85
|
* Link the packages of the plan into the farm of the analysis, before the
|
|
92
|
-
* run reserves anything. The linked set is the union of the
|
|
93
|
-
* step. A pool miss and a collision refuse the launch with the
|
|
94
|
-
* a bound seam, the pass returns at once.
|
|
86
|
+
* run reserves anything. The linked set is the union of the queries of each
|
|
87
|
+
* step. A pool miss and a collision refuse the launch with the spellings.
|
|
88
|
+
* Without a bound seam, the pass returns at once.
|
|
95
89
|
*/
|
|
96
90
|
async function linkPlanPackages(extendAnalysisFarm, analysisId, plan) {
|
|
97
91
|
if (!extendAnalysisFarm)
|
|
98
92
|
return;
|
|
93
|
+
// The union dedupes EQUAL queries only: two entries are one ask when their
|
|
94
|
+
// spelling, their track, and their version are equal. A bare entry beside a
|
|
95
|
+
// qualified entry of one spelling stays two asks, because the bare one
|
|
96
|
+
// carries its own refusal — an absorption would drop the pin of one entry
|
|
97
|
+
// and silence a wrong track.
|
|
99
98
|
const union = new Map();
|
|
100
99
|
for (const step of plan.steps) {
|
|
101
100
|
for (const entry of step.packages ?? []) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
const parsed = parseQuery(entry);
|
|
102
|
+
if (parsed.isErr()) {
|
|
103
|
+
// `validatePlan` parsed every entry of this plan already, thus a
|
|
104
|
+
// refusal here means that the two readers disagree.
|
|
105
|
+
throw new Error(`Step "${step.id}" carries the package entry "${entry}", which the plan validation should have refused`);
|
|
106
|
+
}
|
|
107
|
+
const query = parsed.value;
|
|
108
|
+
const key = formatQuery(query);
|
|
107
109
|
if (!union.has(key))
|
|
108
|
-
union.set(key,
|
|
110
|
+
union.set(key, query);
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
|
-
|
|
113
|
+
const queries = [...union.values()];
|
|
114
|
+
if (queries.length === 0)
|
|
112
115
|
return;
|
|
113
116
|
let outcomes;
|
|
114
117
|
try {
|
|
115
|
-
outcomes = await extendAnalysisFarm(analysisId,
|
|
118
|
+
outcomes = await extendAnalysisFarm(analysisId, queries);
|
|
116
119
|
}
|
|
117
120
|
catch (cause) {
|
|
118
121
|
// A realization throw is an `unavailable` answer, not a driver error.
|
|
@@ -132,8 +135,22 @@ async function linkPlanPackages(extendAnalysisFarm, analysisId, plan) {
|
|
|
132
135
|
// so the agent replans from the true state instead of re-asking. A
|
|
133
136
|
// collision's detail names the two pins and what needs each side — the
|
|
134
137
|
// remedy is to drop or re-pin a DEPENDENT, so the refusal must name it.
|
|
135
|
-
const missing = outcomes.filter((o) => o.kind === "absent").map((o) => (o.detail === undefined ? o.
|
|
136
|
-
|
|
138
|
+
const missing = outcomes.filter((o) => o.kind === "absent").map((o) => (o.detail === undefined ? o.spelling : `${o.spelling} — ${o.detail}`));
|
|
139
|
+
// A collision names the two store directories AND the two prefixed forms.
|
|
140
|
+
// One spelling that both tracks hold is the common cause, and the prefix is
|
|
141
|
+
// the remedy that a plan can write. `formatQuery` writes the two forms,
|
|
142
|
+
// thus the refusal quotes the grammar that the plan reads.
|
|
143
|
+
//
|
|
144
|
+
// The prefix carries its CONDITION, because the outcome shape cannot tell
|
|
145
|
+
// the two collisions apart: two versions of one distribution collide too,
|
|
146
|
+
// and a prefix there resolves nothing. An unconditional instruction would
|
|
147
|
+
// send the agent through a second launch that refuses identically.
|
|
148
|
+
const collisions = outcomes
|
|
149
|
+
.filter((o) => o.kind === "collision")
|
|
150
|
+
.map((o) => `${o.spelling} — two store directories claim it, ${o.storeDirs[0]} and ${o.storeDirs[1]}` +
|
|
151
|
+
`${o.detail === undefined ? "" : ` (${o.detail})`}` +
|
|
152
|
+
`; when the two directories are one spelling in two tracks, name the track in the plan, as ` +
|
|
153
|
+
`${formatQuery({ spelling: o.spelling, track: "python" })} or ${formatQuery({ spelling: o.spelling, track: "r" })}`);
|
|
137
154
|
if (missing.length > 0 || collisions.length > 0) {
|
|
138
155
|
throw new PlanPackagesMissingError(missing, collisions);
|
|
139
156
|
}
|
|
@@ -53,7 +53,7 @@ declare function formatRun(r: CortexRunRow, verbose: boolean, exposePaths?: bool
|
|
|
53
53
|
synthesisReason?: string | undefined;
|
|
54
54
|
runId: string;
|
|
55
55
|
workflowName: string;
|
|
56
|
-
status: "
|
|
56
|
+
status: "running" | "completed" | "failed" | "partial" | "canceled" | "suspended_insufficient_funds";
|
|
57
57
|
startedAt: string;
|
|
58
58
|
completedAt: string | null;
|
|
59
59
|
planId: string | null;
|
|
@@ -8,20 +8,20 @@
|
|
|
8
8
|
* staged — the tool never installs, downloads, or acquires anything. A link
|
|
9
9
|
* is live in a container that already runs, because the farm rides a bind
|
|
10
10
|
* mount.
|
|
11
|
+
*
|
|
12
|
+
* Each entry is a query in the one grammar of the `package-identity`
|
|
13
|
+
* capability, thus the agent learns one grammar for the plan, the tool, and
|
|
14
|
+
* the prompt layer.
|
|
11
15
|
*/
|
|
12
16
|
import type { ExtendAnalysisFarm, PackageRequestOutcome } from "../../sandbox/types.js";
|
|
13
17
|
export interface LinkPackagesDeps {
|
|
14
18
|
readonly extendAnalysisFarm: ExtendAnalysisFarm;
|
|
15
19
|
readonly analysisId: string;
|
|
16
20
|
}
|
|
17
|
-
/** The tool result: one outcome per requested
|
|
21
|
+
/** The tool result: one outcome per requested query, index-aligned. */
|
|
18
22
|
export interface LinkPackagesResult {
|
|
19
23
|
readonly outcomes: readonly PackageRequestOutcome[];
|
|
20
24
|
}
|
|
21
25
|
export declare function createLinkPackagesTool(deps: LinkPackagesDeps): import("../define-tool.js").Tool<{
|
|
22
|
-
packages:
|
|
23
|
-
name: string;
|
|
24
|
-
version?: string | undefined;
|
|
25
|
-
ecosystem?: "r" | "python" | undefined;
|
|
26
|
-
}[];
|
|
26
|
+
packages: string[];
|
|
27
27
|
}, LinkPackagesResult>;
|
|
@@ -8,44 +8,75 @@
|
|
|
8
8
|
* staged — the tool never installs, downloads, or acquires anything. A link
|
|
9
9
|
* is live in a container that already runs, because the farm rides a bind
|
|
10
10
|
* mount.
|
|
11
|
+
*
|
|
12
|
+
* Each entry is a query in the one grammar of the `package-identity`
|
|
13
|
+
* capability, thus the agent learns one grammar for the plan, the tool, and
|
|
14
|
+
* the prompt layer.
|
|
11
15
|
*/
|
|
12
|
-
import { ok } from "neverthrow";
|
|
16
|
+
import { err, ok } from "neverthrow";
|
|
13
17
|
import { z } from "zod";
|
|
14
18
|
import { defineTool } from "../define-tool.js";
|
|
19
|
+
import { parseQuery } from "../../sandbox/package-identity.js";
|
|
20
|
+
/**
|
|
21
|
+
* The refusal of one entry that is not a query. The text names the entry and
|
|
22
|
+
* the issue, thus the agent corrects the entry it wrote instead of guessing
|
|
23
|
+
* which of its packages the tool refused.
|
|
24
|
+
*/
|
|
25
|
+
function describeParseError(entry, error) {
|
|
26
|
+
switch (error.type) {
|
|
27
|
+
case "empty":
|
|
28
|
+
return `"${entry}" names no package — pass a bare name, or name==version`;
|
|
29
|
+
case "location":
|
|
30
|
+
return `"${entry}" is a location — pass a package name, never a path, a URL, or a store directory`;
|
|
31
|
+
case "unknown_prefix":
|
|
32
|
+
return `"${entry}" carries the prefix "${error.prefix}:", which this tool cannot read — the permitted prefixes are "python:" and "r:", and a bare name searches both tracks`;
|
|
33
|
+
case "unsupported_specifier":
|
|
34
|
+
return `"${entry}" pins with the specifier "${error.specifier}", which this tool cannot honor — pass a bare name, or name==version with one exact version`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
15
37
|
export function createLinkPackagesTool(deps) {
|
|
16
38
|
return defineTool({
|
|
17
39
|
id: "link_packages",
|
|
18
40
|
description: "Link host-staged packages into the farm of this analysis, so an import that failed works without a restart. " +
|
|
19
41
|
"This tool links what the host already staged — it NEVER installs, downloads, or acquires anything. " +
|
|
20
|
-
"Pass each module name verbatim, exactly as the import names it
|
|
42
|
+
"Pass each module name verbatim, exactly as the import names it, optionally behind the track prefix `python:` or `r:` and optionally pinned as `name==version`. " +
|
|
21
43
|
"One outcome comes back per request: `linked` (the pool held it, and it is importable now), " +
|
|
22
44
|
"`present` (the farm held it already), " +
|
|
23
45
|
"`absent` (the pool does not hold it — a real answer, and `acquisitionPossible` states whether the host can acquire that ecosystem; report the package as missing, do not retry), " +
|
|
24
|
-
"`collision` (the request resolves to two store directories
|
|
25
|
-
"or `unavailable` (the link pass itself could not answer — the `reason` says why; it says nothing about the package's presence, so report the reason and do not re-request packages)."
|
|
46
|
+
"`collision` (the request resolves to two store directories; the `detail` names the two pins and the packages that need each), " +
|
|
47
|
+
"or `unavailable` (the link pass itself could not answer — the `reason` says why; it says nothing about the package's presence, so report the reason and do not re-request packages). " +
|
|
48
|
+
"After a `collision` of one name that BOTH tracks hold, call this tool again for that package with the prefixed form, `python:<name>` or `r:<name>`. " +
|
|
49
|
+
"A collision is terminal only after that second call also refuses, or when it names two versions of one distribution — then report it and continue without the package.",
|
|
26
50
|
inputSchema: z.object({
|
|
27
51
|
packages: z
|
|
28
|
-
.array(z.
|
|
29
|
-
name: z.string().min(1).describe("The module or package name, verbatim."),
|
|
30
|
-
version: z.string().optional().describe("One exact version, when the requirement pins one."),
|
|
31
|
-
ecosystem: z.enum(["python", "r"]).optional().describe("The ecosystem, when the bare name is ambiguous."),
|
|
32
|
-
}))
|
|
52
|
+
.array(z.string().min(1))
|
|
33
53
|
.min(1)
|
|
34
54
|
.max(50)
|
|
35
|
-
.describe(
|
|
55
|
+
.describe('The packages to link into the farm, each as `[python:|r:]<name>[==<version>]`, for example ["scanpy", "r:Seurat", "numpy==1.26.4"]. A bare name searches both tracks.'),
|
|
36
56
|
}),
|
|
37
|
-
describeCall: ({ packages }) => packages.
|
|
57
|
+
describeCall: ({ packages }) => packages.join(", "),
|
|
38
58
|
execute: async (input) => {
|
|
39
|
-
//
|
|
59
|
+
// Every entry parses before any link lands. A partial link over a
|
|
60
|
+
// batch whose tail is malformed leaves the farm in a state that the
|
|
61
|
+
// agent did not ask for and cannot see.
|
|
62
|
+
const queries = [];
|
|
63
|
+
for (const entry of input.packages) {
|
|
64
|
+
const parsed = parseQuery(entry);
|
|
65
|
+
if (parsed.isErr()) {
|
|
66
|
+
return err({ error: `link_packages refused the call: ${describeParseError(entry, parsed.error)}`, retryable: true });
|
|
67
|
+
}
|
|
68
|
+
queries.push(parsed.value);
|
|
69
|
+
}
|
|
70
|
+
// A realization throw reads as `unavailable` per query. The loop
|
|
40
71
|
// would render the throw as a raw tool error, and a raw driver
|
|
41
72
|
// message teaches the agent nothing that `reason` does not.
|
|
42
73
|
try {
|
|
43
|
-
const outcomes = await deps.extendAnalysisFarm(deps.analysisId,
|
|
74
|
+
const outcomes = await deps.extendAnalysisFarm(deps.analysisId, queries);
|
|
44
75
|
return ok({ outcomes });
|
|
45
76
|
}
|
|
46
77
|
catch (cause) {
|
|
47
78
|
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
48
|
-
return ok({ outcomes:
|
|
79
|
+
return ok({ outcomes: queries.map((query) => ({ kind: "unavailable", spelling: query.spelling, reason })) });
|
|
49
80
|
}
|
|
50
81
|
},
|
|
51
82
|
});
|
|
@@ -20,8 +20,19 @@
|
|
|
20
20
|
* hashes — a thousand rows of sha256 would bury the signal.
|
|
21
21
|
*/
|
|
22
22
|
import type { EnvironmentStorePaths, PoolInventoryPackage, PoolInventorySection } from "../../config/environment-stores.js";
|
|
23
|
+
import { type Track } from "../../sandbox/package-identity.js";
|
|
23
24
|
import { type FarmLock } from "../../sandbox/farm.js";
|
|
24
25
|
import { type ImagePackages } from "../../sandbox/image-packages.js";
|
|
26
|
+
/**
|
|
27
|
+
* The ecosystem of one lock subtree. `python` gives the Python track, and
|
|
28
|
+
* `cran`, `bioconductor`, and `github` give the R track — three subtrees of
|
|
29
|
+
* one ecosystem, because a repository is a source and not an identity rule.
|
|
30
|
+
*
|
|
31
|
+
* A subtree that names neither, such as `node`, carries no track: its packages
|
|
32
|
+
* are not in the pool of the two ecosystems, and a `names` lookup matches them
|
|
33
|
+
* by their rendered name.
|
|
34
|
+
*/
|
|
35
|
+
export declare function trackOfLockSubtree(subtree: string): Track | undefined;
|
|
25
36
|
/**
|
|
26
37
|
* Group the lock packages into sections, one per track, in a stable order:
|
|
27
38
|
* the known tracks first, then an unknown track at its first occurrence.
|
|
@@ -43,7 +54,12 @@ export declare function imageSections(record: ImagePackages): Section[];
|
|
|
43
54
|
export type SectionPackage = PoolInventoryPackage;
|
|
44
55
|
/** One language track of the store, in the section order of its source. The seam shape and the render shape are one. */
|
|
45
56
|
export type Section = PoolInventorySection;
|
|
46
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* One hit of a `names` lookup: present + exact spelling + track + store
|
|
59
|
+
* identity, or absent. A name that two sections hold answers with one entry
|
|
60
|
+
* for each of them, thus the array is longer than the `names` array and it is
|
|
61
|
+
* not aligned with it.
|
|
62
|
+
*/
|
|
47
63
|
export type CheckedPackage = {
|
|
48
64
|
readonly requested: string;
|
|
49
65
|
readonly present: true;
|
|
@@ -55,6 +71,12 @@ export type CheckedPackage = {
|
|
|
55
71
|
} | {
|
|
56
72
|
readonly requested: string;
|
|
57
73
|
readonly present: false;
|
|
74
|
+
/**
|
|
75
|
+
* The spelling that the pool does hold, when the resolution finds
|
|
76
|
+
* one. `seurat` misses and `Seurat` is present, thus the answer
|
|
77
|
+
* carries the spelling that a plan and a link both accept.
|
|
78
|
+
*/
|
|
79
|
+
readonly suggestion?: string;
|
|
58
80
|
};
|
|
59
81
|
/**
|
|
60
82
|
* Three shapes, one per call path: the store is unmounted; a `names` presence
|