@inflexa-ai/harness 0.45.0 → 0.45.1
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 +10 -0
- package/dist/agents/conversation-agent.js +19 -1
- package/dist/agents/sandbox/shared.d.ts +23 -2
- package/dist/agents/sandbox/shared.js +51 -18
- package/dist/agents/sandbox/translational-safety-agent.js +26 -0
- package/dist/agents/sandbox/types.d.ts +24 -1
- package/dist/agents/sandbox-catalog.d.ts +14 -0
- package/dist/agents/sandbox-catalog.js +19 -0
- package/dist/app/approve-plan.d.ts +5 -2
- package/dist/app/approve-plan.js +1 -1
- package/dist/app/chat-turn.js +6 -3
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +12 -2
- package/dist/contracts/chat-events.js +1 -1
- package/dist/contracts/chat-frame.js +6 -2
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +25 -0
- package/dist/contracts/schemas/chat-events.js +7 -0
- package/dist/contracts/schemas/chat-parts.d.ts +39 -0
- package/dist/contracts/schemas/chat-parts.js +17 -0
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/dbos-workflow-purger.d.ts +2 -0
- package/dist/execution/dbos-workflow-purger.js +21 -14
- package/dist/execution/run-canceler.d.ts +33 -11
- package/dist/execution/run-canceler.js +129 -27
- package/dist/execution/run-end-record.d.ts +30 -0
- package/dist/execution/run-end-record.js +27 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +11 -1
- package/dist/loop/call-guard.d.ts +50 -0
- package/dist/loop/call-guard.js +90 -0
- package/dist/loop/compaction.d.ts +5 -0
- package/dist/loop/compaction.js +17 -0
- package/dist/loop/run-agent.d.ts +9 -0
- package/dist/loop/run-agent.js +15 -1
- package/dist/loop/run-to-terminal.js +4 -1
- package/dist/loop/types.d.ts +6 -0
- package/dist/memory/conversation-display-recorder.d.ts +0 -2
- package/dist/memory/conversation-display-recorder.js +28 -3
- package/dist/memory/thread-history.d.ts +34 -7
- package/dist/memory/thread-history.js +151 -42
- package/dist/prompts/briefing.d.ts +57 -3
- package/dist/prompts/briefing.js +129 -2
- package/dist/prompts/planner.js +13 -2
- package/dist/provenance/seam.d.ts +23 -15
- package/dist/provenance/seam.js +7 -5
- package/dist/providers/ai-sdk.js +87 -7
- package/dist/providers/types.d.ts +6 -0
- package/dist/reference-data/catalog.js +141 -2
- package/dist/report-model/authoring-grammar.d.ts +26 -26
- package/dist/sandbox/image-packages.d.ts +26 -4
- package/dist/sandbox/image-packages.js +18 -4
- package/dist/schemas/data-profile-schemas.d.ts +12 -12
- package/dist/schemas/plan-schemas.d.ts +57 -3
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/workflow-state.d.ts +113 -0
- package/dist/schemas/workflow-state.js +54 -0
- package/dist/state/artifacts.d.ts +17 -1
- package/dist/state/artifacts.js +21 -3
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +1 -1
- package/dist/state/runs.d.ts +43 -9
- package/dist/state/runs.js +85 -14
- package/dist/state/step-executions.d.ts +9 -0
- package/dist/state/step-executions.js +22 -0
- package/dist/tools/ad-hoc-router.d.ts +2 -0
- package/dist/tools/ad-hoc-router.js +18 -2
- package/dist/tools/capability-data.d.ts +36 -0
- package/dist/tools/capability-data.js +73 -0
- package/dist/tools/execute-analysis.d.ts +37 -9
- package/dist/tools/execute-analysis.js +131 -21
- package/dist/tools/knowledge/check.d.ts +60 -0
- package/dist/tools/knowledge/check.js +92 -0
- package/dist/tools/knowledge/client.d.ts +382 -0
- package/dist/tools/knowledge/client.js +302 -0
- package/dist/tools/knowledge/environment.d.ts +78 -0
- package/dist/tools/knowledge/environment.js +105 -0
- package/dist/tools/knowledge/index.d.ts +49 -0
- package/dist/tools/knowledge/index.js +70 -0
- package/dist/tools/knowledge/local-slots.d.ts +62 -0
- package/dist/tools/knowledge/local-slots.js +177 -0
- package/dist/tools/knowledge/pin.d.ts +14 -0
- package/dist/tools/knowledge/pin.js +17 -0
- package/dist/tools/knowledge/recommend.d.ts +92 -0
- package/dist/tools/knowledge/recommend.js +98 -0
- package/dist/tools/knowledge/situation.d.ts +143 -0
- package/dist/tools/knowledge/situation.js +164 -0
- package/dist/tools/knowledge/skeleton.d.ts +69 -0
- package/dist/tools/knowledge/skeleton.js +202 -0
- package/dist/tools/knowledge/template.d.ts +137 -0
- package/dist/tools/knowledge/template.js +267 -0
- package/dist/tools/research/generate-plan.d.ts +23 -0
- package/dist/tools/research/generate-plan.js +203 -12
- package/dist/tools/research/guarded-address.d.ts +20 -0
- package/dist/tools/research/guarded-address.js +148 -0
- package/dist/tools/research/searxng-engine.d.ts +40 -0
- package/dist/tools/research/searxng-engine.js +120 -0
- package/dist/tools/research/web-fetch.d.ts +124 -0
- package/dist/tools/research/web-fetch.js +627 -0
- package/dist/tools/research/web-search.d.ts +75 -0
- package/dist/tools/research/web-search.js +112 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
- package/dist/tools/sandbox/list-available-packages.js +12 -4
- package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
- package/dist/tools/sandbox/list-available-refs.js +60 -3
- package/dist/tools/sandbox/report-blocker.js +5 -1
- package/dist/tools/terminal-text.d.ts +11 -0
- package/dist/tools/terminal-text.js +19 -0
- package/dist/tools/workspace/decision-record.d.ts +81 -0
- package/dist/tools/workspace/decision-record.js +117 -0
- package/dist/tools/workspace/edit-file.d.ts +3 -1
- package/dist/tools/workspace/edit-file.js +20 -2
- package/dist/tools/workspace/execute-command.d.ts +17 -0
- package/dist/tools/workspace/execute-command.js +28 -1
- package/dist/tools/workspace/index.d.ts +1 -0
- package/dist/tools/workspace/index.js +1 -0
- package/dist/tools/workspace/mutator.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +58 -18
- package/dist/workflows/execute-analysis.js +273 -58
- package/dist/workflows/sandbox-step.d.ts +12 -0
- package/package.json +5 -1
|
@@ -3,18 +3,21 @@
|
|
|
3
3
|
* hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { err, ok, ResultAsync } from "neverthrow";
|
|
6
|
+
import { err, ok, okAsync, ResultAsync } from "neverthrow";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { capabilityDataRequirement } from "../agents/sandbox-catalog.js";
|
|
8
9
|
import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
|
|
9
10
|
import { DEFAULT_SANDBOX_MAX_STEPS } from "../config/resource-limits.js";
|
|
10
11
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
12
|
+
import { readInstalledDatasetIds } from "./sandbox/list-available-refs.js";
|
|
13
|
+
import { tryQuery, withTransaction } from "../lib/db-result.js";
|
|
11
14
|
import { deliverNotice, passGate } from "../lib/hooks.js";
|
|
12
15
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
13
16
|
import { buildRunCardData } from "../memory/card-builders.js";
|
|
14
17
|
import { formatQuery, parseQuery } from "../sandbox/package-identity.js";
|
|
15
18
|
import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
|
|
16
19
|
import { validatePlan } from "../schemas/validate-plan.js";
|
|
17
|
-
import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, reserveRunById, suspendAnalysis, updateRunStatus, upsertPlan } from "../state/index.js";
|
|
20
|
+
import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, queryNewestPlanRun, reserveRunById, suspendAnalysis, updateRunStatus, upsertPlan, } from "../state/index.js";
|
|
18
21
|
import { routeAdHocRequest } from "./ad-hoc-router.js";
|
|
19
22
|
import { adHocPlanId, adHocRunId } from "./analysis-invocation.js";
|
|
20
23
|
import { defineTool } from "./define-tool.js";
|
|
@@ -92,6 +95,66 @@ export class PlanPackagesMissingError extends Error {
|
|
|
92
95
|
this.name = "PlanPackagesMissingError";
|
|
93
96
|
}
|
|
94
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* A declared dataset that the reference store does not hold, found at the pre-launch pass. The
|
|
100
|
+
* message names the datasets and no remedy command — the remedy belongs to the embedder, and its
|
|
101
|
+
* wrapper appends it.
|
|
102
|
+
*/
|
|
103
|
+
export class PlanReferencesMissingError extends Error {
|
|
104
|
+
missing;
|
|
105
|
+
unavailable;
|
|
106
|
+
constructor(missing, unavailable) {
|
|
107
|
+
const parts = [
|
|
108
|
+
// The store-level reason FIRST, for the same reason the package error leads with it:
|
|
109
|
+
// with an unanswerable store, no per-dataset claim below it is trustworthy.
|
|
110
|
+
unavailable !== undefined
|
|
111
|
+
? `the store cannot answer (${unavailable}) — this says nothing about the datasets themselves, so do not re-request them`
|
|
112
|
+
: null,
|
|
113
|
+
missing.length > 0 ? `the store does not hold: ${missing.join(", ")}` : null,
|
|
114
|
+
].filter((part) => part !== null);
|
|
115
|
+
super(`The reference data of this plan is not installed — ${parts.join(" ")}`);
|
|
116
|
+
this.missing = missing;
|
|
117
|
+
this.unavailable = unavailable;
|
|
118
|
+
this.name = "PlanReferencesMissingError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the reference data that the capabilities of a plan declare, before the run reserves
|
|
123
|
+
* anything. A step names its capability, the agent of the step holds the declaration, and the
|
|
124
|
+
* declaration names catalog ids. An absent dataset refuses the launch with the ids named.
|
|
125
|
+
*
|
|
126
|
+
* Nothing is read when no step declares a requirement: the common plan touches no store at all.
|
|
127
|
+
*
|
|
128
|
+
* @returns How many datasets reached the store. Zero says that nothing was checked — a plan that
|
|
129
|
+
* declares none, or a store the embedder bound nowhere — which a launch record must be able to tell
|
|
130
|
+
* from a checked launch.
|
|
131
|
+
*/
|
|
132
|
+
async function resolvePlanReferences(refStorePath, plan) {
|
|
133
|
+
const declared = new Set();
|
|
134
|
+
for (const step of plan.steps) {
|
|
135
|
+
if (!step.agent || !step.capability)
|
|
136
|
+
continue;
|
|
137
|
+
for (const dataset of capabilityDataRequirement(step.agent, step.capability))
|
|
138
|
+
declared.add(dataset);
|
|
139
|
+
}
|
|
140
|
+
if (declared.size === 0)
|
|
141
|
+
return ok(0);
|
|
142
|
+
// An unbound store is an embedder that provisions reference data some other way, exactly as an
|
|
143
|
+
// unbound farm seam leaves the package pass at once. Reading the container path here instead
|
|
144
|
+
// would fail on every host and refuse every launch that declares a capability.
|
|
145
|
+
if (refStorePath === undefined)
|
|
146
|
+
return ok(0);
|
|
147
|
+
const installed = await readInstalledDatasetIds({ refStorePath });
|
|
148
|
+
if (installed === undefined) {
|
|
149
|
+
// The path is named, because the reason is a fault of the environment and the reader has to
|
|
150
|
+
// know WHICH store did not answer. A bare "could not be read" sends a caller round in circles.
|
|
151
|
+
return err({ type: "references_missing", missing: [], unavailable: `the reference store at ${refStorePath} could not be read` });
|
|
152
|
+
}
|
|
153
|
+
const missing = [...declared].filter((dataset) => !installed.has(dataset)).sort();
|
|
154
|
+
if (missing.length > 0)
|
|
155
|
+
return err({ type: "references_missing", missing });
|
|
156
|
+
return ok(declared.size);
|
|
157
|
+
}
|
|
95
158
|
/**
|
|
96
159
|
* Link the packages of the plan into the farm of the analysis, before the
|
|
97
160
|
* run reserves anything. The linked set is the union of the queries of each
|
|
@@ -226,7 +289,7 @@ function parseStoredPlan(raw, planId) {
|
|
|
226
289
|
function validateStoredPlan(raw, planId) {
|
|
227
290
|
const parsed = parseStoredPlan(raw, planId);
|
|
228
291
|
if (parsed.isErr())
|
|
229
|
-
return throwLaunchRefusal(parsed.error);
|
|
292
|
+
return throwLaunchRefusal(parsed.error, planId);
|
|
230
293
|
return parsed.value;
|
|
231
294
|
}
|
|
232
295
|
function adHocTitle(request) {
|
|
@@ -286,6 +349,7 @@ export function buildAdHocPlan(request, route, createdAt = new Date().toISOStrin
|
|
|
286
349
|
status: "pending",
|
|
287
350
|
resources: route.resources,
|
|
288
351
|
agent: route.agentId,
|
|
352
|
+
...(route.capability ? { capability: route.capability } : {}),
|
|
289
353
|
maxSteps: meta?.defaultMaxSteps ?? DEFAULT_SANDBOX_MAX_STEPS,
|
|
290
354
|
},
|
|
291
355
|
],
|
|
@@ -359,6 +423,12 @@ async function launchReservedRun(deps, args) {
|
|
|
359
423
|
const resources = stepResources(planId, plan);
|
|
360
424
|
if (resources.isErr())
|
|
361
425
|
return err(resources.error);
|
|
426
|
+
// Reference data first: it is a read, so it costs nothing to learn before the farm link
|
|
427
|
+
// writes. Both passes come before the run reserves anything, thus a miss of either leaves no
|
|
428
|
+
// run row and no revoked authorization behind.
|
|
429
|
+
const references = await resolvePlanReferences(deps.refStorePath, plan);
|
|
430
|
+
if (references.isErr())
|
|
431
|
+
return err(references.error);
|
|
362
432
|
// The link pass comes before the run reserves anything, thus a pool
|
|
363
433
|
// miss leaves no run row and no revoked authorization behind.
|
|
364
434
|
const linked = await linkPlanPackages(deps.extendAnalysisFarm, analysisId, planId, plan);
|
|
@@ -413,26 +483,52 @@ async function launchReservedRun(deps, args) {
|
|
|
413
483
|
logger.info("analysis launched", { analysisId, runId, planId, mode: args.mode, packageQueries });
|
|
414
484
|
return ok({ runId, started: true });
|
|
415
485
|
}
|
|
486
|
+
function staleViewOf(newest, seen) {
|
|
487
|
+
if (newest !== null)
|
|
488
|
+
return newest === seen ? null : { type: "newer", runId: newest };
|
|
489
|
+
return seen === null ? null : { type: "missing", runId: seen };
|
|
490
|
+
}
|
|
491
|
+
/** A stale view starts nothing: it gives the newest run with `started: false`, or `run_not_found` when the plan has no run. */
|
|
492
|
+
function refuseStaleView(view) {
|
|
493
|
+
return view.type === "newer" ? ok({ runId: view.runId, started: false }) : err({ type: "run_not_found", runId: view.runId });
|
|
494
|
+
}
|
|
495
|
+
function lockPlanLaunch(client, planId) {
|
|
496
|
+
return tryQuery("execute-analysis.lockPlanLaunch", () => client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`plan-launch:${planId}`])).map(() => undefined);
|
|
497
|
+
}
|
|
416
498
|
/**
|
|
417
|
-
* Reserve a fresh run for a plan
|
|
418
|
-
*
|
|
499
|
+
* Reserve a fresh run for a plan under the lock of the plan, thus the reservations of one plan occur one
|
|
500
|
+
* after the other. A caller with a `newestRunId` reserves nothing when the newest run of the plan is a
|
|
501
|
+
* different run. The new row starts after the newest start, thus it is the newest run of the plan. A
|
|
502
|
+
* concurrent launch of the same plan can hold the active-plan slot, and its run is the answer.
|
|
419
503
|
*/
|
|
420
504
|
async function reservePlanRun(deps, args) {
|
|
421
505
|
const runId = randomUUID();
|
|
422
506
|
try {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
507
|
+
const reserved = await withTransaction(deps.pool, "execute-analysis.reservePlanRun", (client) => lockPlanLaunch(client, args.planId)
|
|
508
|
+
.andThen(() => queryNewestPlanRun(client, { analysisId: args.analysisId, planId: args.planId }))
|
|
509
|
+
.andThen((newest) => {
|
|
510
|
+
const stale = args.newestRunId === undefined ? null : staleViewOf(newest?.runId ?? null, args.newestRunId);
|
|
511
|
+
if (stale)
|
|
512
|
+
return okAsync({ type: "stale", view: stale });
|
|
513
|
+
const startedAt = newest === null ? undefined : new Date(Math.max(Date.now(), Date.parse(newest.startedAt) + 1)).toISOString();
|
|
514
|
+
return insertRun(client, {
|
|
515
|
+
runId,
|
|
516
|
+
analysisId: args.analysisId,
|
|
517
|
+
threadId: args.threadId,
|
|
518
|
+
workflowName: "executeAnalysis",
|
|
519
|
+
planId: args.planId,
|
|
520
|
+
...(startedAt === undefined ? {} : { startedAt }),
|
|
521
|
+
}).map(() => ({ type: "inserted" }));
|
|
522
|
+
}));
|
|
523
|
+
if (reserved.isErr())
|
|
524
|
+
return err({ type: "store_failed", cause: reserved.error });
|
|
525
|
+
if (reserved.value.type === "stale")
|
|
526
|
+
return refuseStaleView(reserved.value.view);
|
|
432
527
|
return ok({ runId, started: true });
|
|
433
528
|
}
|
|
434
529
|
catch (cause) {
|
|
435
|
-
// `insertRun` rejects its promise on a collision of the active-plan index, and on nothing else.
|
|
530
|
+
// `insertRun` rejects its promise on a collision of the active-plan index, and on nothing else. The
|
|
531
|
+
// transaction is rolled back and released by now, so the read goes to the pool.
|
|
436
532
|
if (!isDedupCollision(cause))
|
|
437
533
|
return err({ type: "store_failed", cause });
|
|
438
534
|
const active = await queryActiveRun(deps.pool, args.analysisId, args.planId);
|
|
@@ -467,7 +563,7 @@ async function reserveAdHocRun(deps, args) {
|
|
|
467
563
|
}
|
|
468
564
|
}
|
|
469
565
|
async function launchStoredPlan(deps, input) {
|
|
470
|
-
const { session, planId } = input;
|
|
566
|
+
const { session, planId, newestRunId } = input;
|
|
471
567
|
const analysisId = session.scope.analysisId;
|
|
472
568
|
const raw = await loadPlan(deps.pool, planId, { analysisId });
|
|
473
569
|
if (raw.isErr())
|
|
@@ -477,6 +573,14 @@ async function launchStoredPlan(deps, input) {
|
|
|
477
573
|
const plan = parseStoredPlan(raw.value, planId);
|
|
478
574
|
if (plan.isErr())
|
|
479
575
|
return err(plan.error);
|
|
576
|
+
if (newestRunId !== undefined) {
|
|
577
|
+
const newest = await queryNewestPlanRun(deps.pool, { analysisId, planId });
|
|
578
|
+
if (newest.isErr())
|
|
579
|
+
return err({ type: "store_failed", cause: newest.error });
|
|
580
|
+
const stale = staleViewOf(newest.value?.runId ?? null, newestRunId);
|
|
581
|
+
if (stale)
|
|
582
|
+
return refuseStaleView(stale);
|
|
583
|
+
}
|
|
480
584
|
const active = await queryActiveRun(deps.pool, analysisId, planId);
|
|
481
585
|
if (active.isErr())
|
|
482
586
|
return err({ type: "store_failed", cause: active.error });
|
|
@@ -487,13 +591,15 @@ async function launchStoredPlan(deps, input) {
|
|
|
487
591
|
planId,
|
|
488
592
|
plan: plan.value,
|
|
489
593
|
mode: "plan",
|
|
490
|
-
reserve: (frame) => reservePlanRun(deps, { ...frame, planId }),
|
|
594
|
+
reserve: (frame) => reservePlanRun(deps, { ...frame, planId, ...(newestRunId === undefined ? {} : { newestRunId }) }),
|
|
491
595
|
});
|
|
492
596
|
}
|
|
493
597
|
/**
|
|
494
598
|
* Start the run of a stored plan. It reads the plan under the analysis of the session, validates it,
|
|
495
|
-
*
|
|
496
|
-
*
|
|
599
|
+
* compares the newest run of the plan with `newestRunId`, links its packages, reserves a run,
|
|
600
|
+
* authorizes it, and starts the workflow. It gives the run id, or the refusal, and it emits no chat
|
|
601
|
+
* part. `started` is false when this call started nothing: a run of the plan was already active, or the
|
|
602
|
+
* newest run of the plan is not the run that the caller saw. `runId` then names that run.
|
|
497
603
|
*
|
|
498
604
|
* This is an API boundary: a host calls it outside a tool body, thus no outcome above rejects the
|
|
499
605
|
* promise.
|
|
@@ -502,14 +608,18 @@ export function launchPlanRun(deps, input) {
|
|
|
502
608
|
return new ResultAsync(launchStoredPlan(deps, input));
|
|
503
609
|
}
|
|
504
610
|
/** Throw the error that the tool body threw before the launch gave a `Result`. */
|
|
505
|
-
function throwLaunchRefusal(refusal) {
|
|
611
|
+
function throwLaunchRefusal(refusal, planId) {
|
|
506
612
|
switch (refusal.type) {
|
|
613
|
+
case "run_not_found":
|
|
614
|
+
throw new Error(`Plan ${planId} has no run ${refusal.runId}`);
|
|
507
615
|
case "plan_not_found":
|
|
508
616
|
throw new PlanNotFoundError(refusal.planId);
|
|
509
617
|
case "plan_invalid":
|
|
510
618
|
throw refusal.malformed ? new PlanMalformedError(refusal.planId) : new PlanValidationError(refusal.planId, refusal.errors);
|
|
511
619
|
case "packages_missing":
|
|
512
620
|
throw new PlanPackagesMissingError(refusal.missing, refusal.collisions, refusal.unavailable);
|
|
621
|
+
case "references_missing":
|
|
622
|
+
throw new PlanReferencesMissingError(refusal.missing, refusal.unavailable);
|
|
513
623
|
case "store_failed":
|
|
514
624
|
return unwrapOrThrow(err(refusal.cause));
|
|
515
625
|
case "launch_failed":
|
|
@@ -566,7 +676,7 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
566
676
|
return err({ error: `The analysis is suspended: ${refusal.reason}`, retryable: false });
|
|
567
677
|
if (refusal.type === "not_authorized")
|
|
568
678
|
return err({ error: `The run was not authorized: ${refusal.reason}`, retryable: false });
|
|
569
|
-
return throwLaunchRefusal(refusal);
|
|
679
|
+
return throwLaunchRefusal(refusal, planId);
|
|
570
680
|
}
|
|
571
681
|
await emitRunCard(launched.value.runId);
|
|
572
682
|
return ok({ runId: launched.value.runId, status: "in_progress" });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `knowledge_check` — the drafted steps against the rules, once, before
|
|
3
|
+
* `submit_plan`. A violation names the rule and the permitted alternatives.
|
|
4
|
+
* The planner revises once. There is no loop: the host counts the calls of
|
|
5
|
+
* one plan generation, and past `CHECK_CALL_LIMIT` the tool answers with a
|
|
6
|
+
* typed refusal and never reaches the service. A small model that cannot
|
|
7
|
+
* satisfy a finding otherwise rephrases the step until the plan times out.
|
|
8
|
+
*/
|
|
9
|
+
import type { CheckResponse, KnowledgeClient, KnowledgeRejected, KnowledgeSnapshotMismatch, KnowledgeUnavailable } from "./client.js";
|
|
10
|
+
import type { SnapshotPin } from "./pin.js";
|
|
11
|
+
export interface KnowledgeCheckDeps {
|
|
12
|
+
readonly client: KnowledgeClient;
|
|
13
|
+
/** The release the plan pinned on its first recommend answer. The check sends it, thus another release refuses the call. */
|
|
14
|
+
readonly pin?: SnapshotPin;
|
|
15
|
+
}
|
|
16
|
+
export type KnowledgeCheckOutput = CheckResponse | KnowledgeUnavailable | KnowledgeRejected | KnowledgeSnapshotMismatch;
|
|
17
|
+
/** The checks one plan generation may run. Two is a draft and one revision; the third is slack. */
|
|
18
|
+
export declare const CHECK_CALL_LIMIT = 3;
|
|
19
|
+
export declare function createKnowledgeCheckTool(deps: KnowledgeCheckDeps): import("../define-tool.js").Tool<{
|
|
20
|
+
question: "enrichment" | "differential_expression" | "qc" | "full_plan" | "tf_activity" | "deconvolution" | "coexpression" | "clustering" | "survival" | "signature_scoring";
|
|
21
|
+
modality: "bulk_rna_seq";
|
|
22
|
+
data_state: "fastq" | "counts" | "tpm_or_fpkm" | "log_normalized";
|
|
23
|
+
organism: "other" | "human" | "mouse";
|
|
24
|
+
n_groups: number;
|
|
25
|
+
n_per_group_min: number;
|
|
26
|
+
n_per_group_max: number;
|
|
27
|
+
paired: boolean;
|
|
28
|
+
batch: "none" | "known_balanced" | "known_confounded" | "known_unbalanced" | "suspected";
|
|
29
|
+
steps: {
|
|
30
|
+
step_type: "normalize" | "enrichment" | "differential_expression" | "report" | "tf_activity" | "deconvolution" | "coexpression" | "clustering" | "survival" | "signature_scoring" | "variance_partition" | "pathway_activity" | "transcript_level" | "annotation" | "cohort_assembly" | "qc_sample_structure" | "filter_low_counts" | "model_design" | "shrink_lfc" | "multiple_testing";
|
|
31
|
+
method: string;
|
|
32
|
+
method_id?: string | undefined;
|
|
33
|
+
package?: string | undefined;
|
|
34
|
+
parameters?: {
|
|
35
|
+
name: string;
|
|
36
|
+
value: string | number | boolean;
|
|
37
|
+
}[] | undefined;
|
|
38
|
+
outcome?: string | undefined;
|
|
39
|
+
}[];
|
|
40
|
+
count_source?: "unknown" | "salmon" | "kallisto" | "star_featurecounts" | "rsem" | undefined;
|
|
41
|
+
import_state?: "unknown" | "quantifications" | "estimated_counts_with_lengths" | "corrected_counts" | "integer_counts" | undefined;
|
|
42
|
+
n_samples?: number | undefined;
|
|
43
|
+
n_events?: number | undefined;
|
|
44
|
+
blocking_factor?: "other" | "individual" | "cell_line" | "litter" | "site" | "pair" | null | undefined;
|
|
45
|
+
block_structure?: "crossed" | "nested" | undefined;
|
|
46
|
+
covariates?: ("other" | "technical" | "sex" | "age" | "clinical")[] | undefined;
|
|
47
|
+
n_timepoints?: number | null | undefined;
|
|
48
|
+
continuous_predictor?: "other" | "score" | "time" | "age" | "dose" | null | undefined;
|
|
49
|
+
technical_replicates?: boolean | undefined;
|
|
50
|
+
sample_exclusions?: boolean | undefined;
|
|
51
|
+
feature_level?: "gene" | "exon" | "transcript" | undefined;
|
|
52
|
+
library_type?: "unknown" | "total" | "polyA" | "three_prime" | undefined;
|
|
53
|
+
strandedness?: "unknown" | "verified" | "declared_unverified" | undefined;
|
|
54
|
+
interaction?: boolean | undefined;
|
|
55
|
+
classifier?: boolean | undefined;
|
|
56
|
+
extra_analyses?: ("tf_activity" | "deconvolution" | "coexpression" | "clustering" | "survival" | "signature_scoring" | "variance_partition" | "pathway_activity" | "transcript_level" | "annotation")[] | undefined;
|
|
57
|
+
preferred_language?: "R" | "python" | undefined;
|
|
58
|
+
enrichment_input?: "ranked_list" | "gene_list" | "sample_scores" | undefined;
|
|
59
|
+
quality_flags?: ("low_depth_sample" | "outlier_sample" | "sample_identity_doubt" | "high_duplication" | "degraded_rna")[] | undefined;
|
|
60
|
+
}, KnowledgeCheckOutput>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `knowledge_check` — the drafted steps against the rules, once, before
|
|
3
|
+
* `submit_plan`. A violation names the rule and the permitted alternatives.
|
|
4
|
+
* The planner revises once. There is no loop: the host counts the calls of
|
|
5
|
+
* one plan generation, and past `CHECK_CALL_LIMIT` the tool answers with a
|
|
6
|
+
* typed refusal and never reaches the service. A small model that cannot
|
|
7
|
+
* satisfy a finding otherwise rephrases the step until the plan times out.
|
|
8
|
+
*/
|
|
9
|
+
import { ok } from "neverthrow";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { defineTool } from "../define-tool.js";
|
|
12
|
+
import { SituationFieldsSchema, toSituation } from "./situation.js";
|
|
13
|
+
/** The checks one plan generation may run. Two is a draft and one revision; the third is slack. */
|
|
14
|
+
export const CHECK_CALL_LIMIT = 3;
|
|
15
|
+
const DraftedStepSchema = z.object({
|
|
16
|
+
step_type: z
|
|
17
|
+
.enum([
|
|
18
|
+
"cohort_assembly",
|
|
19
|
+
"qc_sample_structure",
|
|
20
|
+
"filter_low_counts",
|
|
21
|
+
"normalize",
|
|
22
|
+
"model_design",
|
|
23
|
+
"differential_expression",
|
|
24
|
+
"shrink_lfc",
|
|
25
|
+
"multiple_testing",
|
|
26
|
+
"enrichment",
|
|
27
|
+
"variance_partition",
|
|
28
|
+
"tf_activity",
|
|
29
|
+
"pathway_activity",
|
|
30
|
+
"signature_scoring",
|
|
31
|
+
"deconvolution",
|
|
32
|
+
"coexpression",
|
|
33
|
+
"clustering",
|
|
34
|
+
"survival",
|
|
35
|
+
"transcript_level",
|
|
36
|
+
"annotation",
|
|
37
|
+
"report",
|
|
38
|
+
])
|
|
39
|
+
.describe("The kind of work the drafted step does."),
|
|
40
|
+
method: z.string().min(1).describe("The method as the step names it, for example `DESeq2 Wald test` or `edgeR quasi-likelihood`."),
|
|
41
|
+
method_id: z
|
|
42
|
+
.string()
|
|
43
|
+
.regex(/^M-\d{4}$/)
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("The `method.id` of the step in the `knowledge_recommend` answer (for example `M-0001`), copied when the step uses that method. With it the check matches the method exactly and ignores the wording."),
|
|
46
|
+
package: z.string().optional().describe("The main package of the step, for example `DESeq2`."),
|
|
47
|
+
parameters: z
|
|
48
|
+
.array(z.object({ name: z.string().min(1), value: z.union([z.string(), z.number(), z.boolean()]) }))
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("The key parameters of the step, for example `alpha`, `min_count`, `lfc_shrink`, `rank_metric`."),
|
|
51
|
+
outcome: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("The outcome the step states when a flag of `knowledge_recommend` removes inference, copied from the flag, for example `descriptive_only`."),
|
|
55
|
+
});
|
|
56
|
+
/** `ok`, or the counts of the findings, plus the steps the check did not assess when there are any. */
|
|
57
|
+
function describeFindings(result) {
|
|
58
|
+
const findings = result.ok ? "ok" : `${result.violations.length} violation(s), ${result.warnings.length} warning(s)`;
|
|
59
|
+
const notAssessed = result.not_assessed?.length ?? 0;
|
|
60
|
+
return notAssessed > 0 ? `${findings}, ${notAssessed} not assessed` : findings;
|
|
61
|
+
}
|
|
62
|
+
export function createKnowledgeCheckTool(deps) {
|
|
63
|
+
let calls = 0;
|
|
64
|
+
return defineTool({
|
|
65
|
+
id: "knowledge_check",
|
|
66
|
+
description: "Check the method steps you drafted for a bulk RNA-seq analysis against the rules of the Inflexa knowledge service, once, after the draft and before `submit_plan`. " +
|
|
67
|
+
"Send the same situation you sent to `knowledge_recommend` and the drafted steps: the step type, the method as the step names it, its package, and its key parameters. " +
|
|
68
|
+
"The answer lists `violations` (a forbidden method, or an inferential test on a design that a flag limits) and `warnings` (a method outside the set the rules name, a parameter that differs from a sourced default, or a required parameter that the step does not state), each with the rule id and the permitted alternatives. " +
|
|
69
|
+
"Revise a violated step once, then submit. A warning is advice: keep the step when the data gives a reason, and state the reason in the step. `ok: true` means no finding on the assessed steps. " +
|
|
70
|
+
"`not_assessed` lists the steps that no rule covers; the check neither passed nor failed them, so submit them as drafted. " +
|
|
71
|
+
"`match: unavailable` means the service did not answer, and `match: snapshot_mismatch` means the service moved to another release than the one your recommend answer came from; in both cases submit the draft as it is. " +
|
|
72
|
+
`The host accepts ${CHECK_CALL_LIMIT} checks per plan; after that the tool refuses and you submit with the findings you have.`,
|
|
73
|
+
inputSchema: SituationFieldsSchema.extend({
|
|
74
|
+
steps: z.array(DraftedStepSchema).min(1).describe("The drafted method steps, one entry per step type."),
|
|
75
|
+
}),
|
|
76
|
+
describeCall: ({ steps }) => `${steps.length} drafted step(s)`,
|
|
77
|
+
describeResult: (_input, result) => ("ok" in result ? describeFindings(result) : result.match),
|
|
78
|
+
execute: async (input) => {
|
|
79
|
+
calls += 1;
|
|
80
|
+
if (calls > CHECK_CALL_LIMIT) {
|
|
81
|
+
return ok({
|
|
82
|
+
match: "rejected",
|
|
83
|
+
message: `The host accepts ${CHECK_CALL_LIMIT} checks per plan, and this is call ${calls}. Submit the plan with the findings of the last check.`,
|
|
84
|
+
issues: [],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const { steps, ...fields } = input;
|
|
88
|
+
const answer = await deps.client.check(toSituation(fields), steps, deps.pin?.get());
|
|
89
|
+
return ok(answer);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|