@inflexa-ai/harness 0.12.0 → 0.14.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 +9 -15
- package/dist/agents/conversation-agent.js +32 -31
- package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
- package/dist/agents/sandbox/chromatin-agent.js +1 -1
- package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
- package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
- package/dist/agents/sandbox/enrichment-agent.js +1 -1
- package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
- package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
- package/dist/agents/sandbox/index.js +0 -3
- package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
- package/dist/agents/sandbox/network-agent.js +1 -1
- package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
- package/dist/agents/sandbox/scientific-executor.js +1 -1
- package/dist/agents/sandbox/shared.d.ts +2 -2
- package/dist/agents/sandbox/shared.js +8 -14
- package/dist/agents/sandbox/translational-safety-agent.js +4 -7
- package/dist/agents/sandbox/types.d.ts +5 -5
- package/dist/agents/sandbox/types.js +4 -4
- package/dist/app/chat-turn.js +4 -1
- package/dist/app/message-assembly.d.ts +5 -2
- package/dist/app/message-assembly.js +7 -2
- package/dist/app/run-activity.d.ts +13 -0
- package/dist/app/run-activity.js +51 -0
- package/dist/config/resource-limits.d.ts +0 -7
- package/dist/config/resource-limits.js +0 -1
- package/dist/contracts/target-dossier.d.ts +10 -10
- package/dist/execution/dbos-run-launcher.js +1 -26
- package/dist/execution/report-runner.js +1 -1
- package/dist/execution/run-authorizer.d.ts +1 -1
- package/dist/execution/run-authorizer.js +1 -1
- package/dist/execution/run-event-parts.d.ts +44 -0
- package/dist/execution/run-event-parts.js +0 -0
- package/dist/execution/run-event-stream.d.ts +91 -0
- package/dist/execution/run-event-stream.js +212 -0
- package/dist/execution/run-launcher.d.ts +2 -18
- package/dist/execution/run-launcher.js +2 -4
- package/dist/execution/run-synthesis.js +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +23 -6
- package/dist/input-sanitization.d.ts +14 -4
- package/dist/input-sanitization.js +27 -4
- package/dist/lib/storage.js +4 -1
- package/dist/loop/budget-exceeded.d.ts +18 -2
- package/dist/loop/budget-exceeded.js +49 -2
- package/dist/loop/run-agent.d.ts +22 -1
- package/dist/loop/run-agent.js +51 -5
- package/dist/loop/run-to-terminal.d.ts +0 -2
- package/dist/loop/run-to-terminal.js +8 -1
- package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
- package/dist/memory/ai-sdk-message-storage.js +45 -4
- package/dist/memory/content-to-cortex.js +19 -3
- package/dist/memory/message-backfill.js +1 -1
- package/dist/memory/reconstruct-cards.d.ts +1 -1
- package/dist/memory/reconstruct-cards.js +7 -5
- package/dist/memory/thread-history.js +72 -8
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +41 -15
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +16 -14
- package/dist/prompts/planner.d.ts +3 -0
- package/dist/prompts/planner.js +16 -21
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
- package/dist/prompts/sandbox-standards.d.ts +4 -4
- package/dist/prompts/sandbox-standards.js +5 -5
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +7 -7
- package/dist/providers/errors.d.ts +7 -0
- package/dist/providers/errors.js +88 -3
- package/dist/runtime/assemble.d.ts +5 -10
- package/dist/runtime/assemble.js +0 -4
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +1 -1
- package/dist/runtime/dbos.d.ts +4 -5
- package/dist/runtime/dbos.js +6 -7
- package/dist/runtime/pools.d.ts +15 -1
- package/dist/runtime/pools.js +15 -1
- package/dist/sandbox/mount-plan.d.ts +1 -1
- package/dist/sandbox/sandbox-error.d.ts +1 -1
- package/dist/sandbox/sandbox-error.js +1 -1
- package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
- package/dist/sandbox/sandbox-step-translate.js +17 -2
- package/dist/sandbox/types.d.ts +1 -1
- package/dist/schemas/validate-plan.d.ts +1 -1
- package/dist/schemas/workflow-state.d.ts +2 -2
- package/dist/schemas/workflow-state.js +2 -2
- package/dist/state/data-profile.d.ts +53 -1
- package/dist/state/data-profile.js +66 -6
- package/dist/state/index.d.ts +6 -6
- package/dist/state/index.js +4 -4
- package/dist/state/init.js +37 -2
- package/dist/state/plans.js +1 -1
- package/dist/state/runs.d.ts +49 -0
- package/dist/state/runs.js +150 -0
- package/dist/tasks/data-profile-activity.d.ts +87 -0
- package/dist/tasks/data-profile-activity.js +56 -0
- package/dist/tasks/data-profile.js +66 -4
- package/dist/tools/__fixtures__/tool-context.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +31 -0
- package/dist/tools/ad-hoc-router.js +132 -0
- package/dist/tools/analysis-invocation.d.ts +4 -0
- package/dist/tools/analysis-invocation.js +17 -0
- package/dist/tools/bio/chembl.d.ts +1 -1
- package/dist/tools/bio/chembl.js +51 -54
- package/dist/tools/bio/comptox.js +35 -33
- package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
- package/dist/tools/bio/drug-gene-interactions.js +175 -0
- package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
- package/dist/tools/bio/gene-disease-evidence.js +187 -0
- package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
- package/dist/tools/bio/gene-preclinical-profile.js +125 -0
- package/dist/tools/bio/index.d.ts +23 -16
- package/dist/tools/bio/index.js +29 -22
- package/dist/tools/bio/keys.d.ts +16 -9
- package/dist/tools/bio/keys.js +22 -13
- package/dist/tools/bio/lookup-annotation.d.ts +48 -0
- package/dist/tools/bio/lookup-annotation.js +173 -0
- package/dist/tools/bio/opentargets.d.ts +7 -11
- package/dist/tools/bio/opentargets.js +24 -35
- package/dist/tools/bio/pubchem.js +6 -4
- package/dist/tools/bio/pubmed.d.ts +15 -7
- package/dist/tools/bio/pubmed.js +79 -20
- package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
- package/dist/tools/bio/search-clinical-trials.js +26 -5
- package/dist/tools/bio/search-faers.js +9 -3
- package/dist/tools/bio/search-gene.d.ts +0 -1
- package/dist/tools/bio/search-gene.js +4 -5
- package/dist/tools/bio/search-geo-datasets.js +10 -4
- package/dist/tools/bio/search-interactions.d.ts +8 -4
- package/dist/tools/bio/search-interactions.js +28 -12
- package/dist/tools/bio/target-safety.d.ts +59 -0
- package/dist/tools/bio/target-safety.js +191 -0
- package/dist/tools/define-tool.d.ts +6 -2
- package/dist/tools/define-tool.js +1 -1
- package/dist/tools/execute-analysis.d.ts +44 -0
- package/dist/tools/execute-analysis.js +252 -0
- package/dist/tools/iterate-report.js +7 -6
- package/dist/tools/lib/dgidb-client.d.ts +49 -0
- package/dist/tools/lib/dgidb-client.js +168 -0
- package/dist/tools/lib/disgenet-client.d.ts +56 -0
- package/dist/tools/lib/disgenet-client.js +59 -0
- package/dist/tools/lib/drugbank-client.d.ts +83 -0
- package/dist/tools/lib/drugbank-client.js +95 -0
- package/dist/tools/lib/ensembl-client.d.ts +4 -1
- package/dist/tools/lib/ensembl-client.js +5 -3
- package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
- package/dist/tools/lib/gwas-catalog-client.js +136 -0
- package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
- package/dist/tools/lib/pharmgkb-client.js +32 -0
- package/dist/tools/lib/pubmed-client.d.ts +57 -4
- package/dist/tools/lib/pubmed-client.js +57 -3
- package/dist/tools/lib/string-client.d.ts +25 -4
- package/dist/tools/lib/string-client.js +22 -5
- package/dist/tools/research/generate-plan.d.ts +16 -9
- package/dist/tools/research/generate-plan.js +116 -70
- package/dist/tools/research/inspect-data-profile.d.ts +31 -6
- package/dist/tools/research/inspect-data-profile.js +23 -7
- package/dist/tools/research/inspect-run.d.ts +33 -15
- package/dist/tools/research/inspect-run.js +155 -66
- package/dist/tools/research/literature-reviewer.js +7 -11
- package/dist/workflows/execute-analysis.d.ts +68 -5
- package/dist/workflows/execute-analysis.js +76 -7
- package/dist/workflows/sandbox-step.js +1 -11
- package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
- package/dist/agents/sandbox/ephemeral-executor.js +0 -20
- package/dist/execution/ephemeral-runner.d.ts +0 -98
- package/dist/execution/ephemeral-runner.js +0 -189
- package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
- package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
- package/dist/tools/bio/check-safety-panel.d.ts +0 -35
- package/dist/tools/bio/check-safety-panel.js +0 -76
- package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
- package/dist/tools/bio/get-impc-ko-profile.js +0 -23
- package/dist/tools/bio/lookup-go-term.d.ts +0 -31
- package/dist/tools/bio/lookup-go-term.js +0 -116
- package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
- package/dist/tools/bio/search-bgee-expression.js +0 -30
- package/dist/tools/bio/search-clinvar.d.ts +0 -13
- package/dist/tools/bio/search-clinvar.js +0 -28
- package/dist/tools/bio/search-dgidb.d.ts +0 -53
- package/dist/tools/bio/search-dgidb.js +0 -213
- package/dist/tools/bio/search-disgenet.d.ts +0 -30
- package/dist/tools/bio/search-disgenet.js +0 -84
- package/dist/tools/bio/search-drugbank.d.ts +0 -38
- package/dist/tools/bio/search-drugbank.js +0 -109
- package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
- package/dist/tools/bio/search-gwas-catalog.js +0 -172
- package/dist/tools/bio/search-pathway.d.ts +0 -12
- package/dist/tools/bio/search-pathway.js +0 -32
- package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
- package/dist/tools/bio/search-pharmgkb.js +0 -52
- package/dist/tools/execute-plan.d.ts +0 -64
- package/dist/tools/execute-plan.js +0 -211
- package/dist/tools/run-ephemeral.d.ts +0 -33
- package/dist/tools/run-ephemeral.js +0 -65
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
* database already holds, and it would lose fidelity at every hop.
|
|
12
12
|
*
|
|
13
13
|
* The tool drives an internal "planner" agent that communicates outcomes
|
|
14
|
-
* EXCLUSIVELY via
|
|
14
|
+
* EXCLUSIVELY via three terminal tool calls:
|
|
15
15
|
*
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
* - submit_plan(plan) → terminal success: re-validates + persists
|
|
16
|
+
* - submit_plan(plan) → terminal success: re-validates + persists;
|
|
17
|
+
* rejected candidates return structured issues
|
|
19
18
|
* - request_clarification(…) → terminal: planner needs more context
|
|
20
19
|
* - report_blocker(reason) → terminal: no viable plan
|
|
21
20
|
*
|
|
@@ -42,6 +41,7 @@ import { plannerPrompt } from "../../prompts/planner.js";
|
|
|
42
41
|
import { hydratePlanSteps, PlannerPlanSchema } from "../../schemas/plan-schemas.js";
|
|
43
42
|
import { validatePlan } from "../../schemas/validate-plan.js";
|
|
44
43
|
import { AnalysisPlanSchema } from "../../schemas/workflow-state.js";
|
|
44
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
45
45
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
46
46
|
import { hintForZodIssue } from "../../lib/zod-issues.js";
|
|
47
47
|
import { insertPlan, loadDataProfileStatus, loadPlan } from "../../state/index.js";
|
|
@@ -49,8 +49,8 @@ import { insertPlan, loadDataProfileStatus, loadPlan } from "../../state/index.j
|
|
|
49
49
|
/** Sub-agent identity for the planner — provenance only. */
|
|
50
50
|
const PLANNER_AGENT_ID = "planner";
|
|
51
51
|
/**
|
|
52
|
-
* Budget for the planner's internal loop:
|
|
53
|
-
*
|
|
52
|
+
* Budget for the planner's internal loop: one draft/submit attempt plus
|
|
53
|
+
* correction retries and headroom.
|
|
54
54
|
*/
|
|
55
55
|
const PLANNER_MAX_ITERATIONS = 13;
|
|
56
56
|
/** Wall-clock guard for a single plan-generation invocation. */
|
|
@@ -251,33 +251,7 @@ function fullyValidate(candidate, resourcePolicy) {
|
|
|
251
251
|
* shared `holder` so the outer `execute` reads the terminal outcome after the
|
|
252
252
|
* loop finishes.
|
|
253
253
|
*/
|
|
254
|
-
function buildInnerTools(holder, persistCtx, pool, resourcePolicy,
|
|
255
|
-
// The two environment stores travel together as one bag rather than as a pair
|
|
256
|
-
// of adjacent optional strings, which would be silently swappable at the call site.
|
|
257
|
-
stores) {
|
|
258
|
-
const validatePlanTool = defineTool({
|
|
259
|
-
id: "validate_plan",
|
|
260
|
-
description: "Dry-run a candidate plan and get back everything that is wrong with it. " +
|
|
261
|
-
"Takes the plan in ANY shape: a malformed, partial, or wrong-typed " +
|
|
262
|
-
"candidate is REPORTED, not rejected. Returns {valid, issues[]} covering " +
|
|
263
|
-
"both schema problems (missing / wrong-typed fields) and the semantic " +
|
|
264
|
-
"checks a schema cannot express (unknown agent IDs, DAG cycles, dangling " +
|
|
265
|
-
"depends_on references, duplicate step IDs, resource ceilings). The " +
|
|
266
|
-
"authoritative field-by-field plan schema is the arg schema of " +
|
|
267
|
-
"submit_plan — this tool deliberately does not restate it. Non-terminal: " +
|
|
268
|
-
"call as often as needed to iterate toward a clean plan, then submit_plan.",
|
|
269
|
-
// Deliberately permissive: a structurally-invalid candidate must reach
|
|
270
|
-
// `execute` so the model gets a structured {valid:false, issues} result —
|
|
271
|
-
// including semantic issues — instead of a bare Zod rejection at the loop's
|
|
272
|
-
// input boundary. `execute` re-parses against PlannerPlanSchema itself.
|
|
273
|
-
inputSchema: z.object({
|
|
274
|
-
plan: z.unknown().describe("The candidate plan, in any shape. Field-by-field schema: see submit_plan."),
|
|
275
|
-
}),
|
|
276
|
-
execute: async (input) => {
|
|
277
|
-
const result = fullyValidate(input.plan, resourcePolicy);
|
|
278
|
-
return ok(result.valid ? { valid: true, issues: [] } : { valid: false, issues: result.issues });
|
|
279
|
-
},
|
|
280
|
-
});
|
|
254
|
+
function buildInnerTools(holder, persistCtx, pool, resourcePolicy, logger) {
|
|
281
255
|
const submitPlanTool = defineTool({
|
|
282
256
|
id: "submit_plan",
|
|
283
257
|
description: "Submit the final plan for persistence. Re-validates the plan; on " +
|
|
@@ -285,9 +259,16 @@ stores) {
|
|
|
285
259
|
"rejection returns {accepted: false, issues} — fix and call again, " +
|
|
286
260
|
"or switch to report_blocker if the plan cannot be made valid. This " +
|
|
287
261
|
"arg schema is the authoritative plan contract.",
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
|
|
262
|
+
// Permissive on purpose: every candidate reaches `execute`, where the
|
|
263
|
+
// model gets structured schema and semantic issues in one response.
|
|
264
|
+
// The field-by-field contract remains visible in this tool's description
|
|
265
|
+
// and the validation below is authoritative.
|
|
266
|
+
inputSchema: z.object({
|
|
267
|
+
// Keep the authoritative shape visible to the model while the
|
|
268
|
+
// permissive branch lets malformed candidates reach execute for
|
|
269
|
+
// structured issue reporting.
|
|
270
|
+
plan: z.union([PlannerPlanSchema, z.unknown()]).describe("Candidate plan; malformed values are reported in issues."),
|
|
271
|
+
}),
|
|
291
272
|
execute: async (input) => {
|
|
292
273
|
if (holder.outcome !== null) {
|
|
293
274
|
return ok({
|
|
@@ -303,6 +284,7 @@ stores) {
|
|
|
303
284
|
}
|
|
304
285
|
const result = fullyValidate(input.plan, resourcePolicy);
|
|
305
286
|
if (!result.valid) {
|
|
287
|
+
logger.debug("submit_plan rejected a plan", { issueCount: result.issues.length, issues: result.issues });
|
|
306
288
|
return ok({ accepted: false, issues: result.issues });
|
|
307
289
|
}
|
|
308
290
|
const persisted = await persistPlan(result.plan, persistCtx, pool);
|
|
@@ -358,60 +340,95 @@ stores) {
|
|
|
358
340
|
"incompatible with every available agent, etc.) — not for a plan you " +
|
|
359
341
|
"could fix and submit.",
|
|
360
342
|
});
|
|
361
|
-
// Environment discovery is non-terminal and read-only: it lets the planner check what
|
|
362
|
-
// reference data this environment holds, and what a step will actually be able to
|
|
363
|
-
// import, before committing a step to needing either. Both read host-side, so no
|
|
364
|
-
// sandbox exists or is needed at plan time — which is the whole point, since a plan
|
|
365
|
-
// that assumes an absent package fails only once the run reaches that step.
|
|
366
|
-
const listAvailableRefs = createListAvailableRefsTool(stores.refStorePath === undefined ? {} : { refStorePath: stores.refStorePath });
|
|
367
|
-
const listAvailablePackages = createListAvailablePackagesTool(stores.packagesFile === undefined ? {} : { packagesFile: stores.packagesFile });
|
|
368
343
|
const terminal = [submitPlanTool, requestClarificationTool, reportBlockerTool];
|
|
369
|
-
return {
|
|
344
|
+
return { terminal };
|
|
345
|
+
}
|
|
346
|
+
function inventoryContent(label, result) {
|
|
347
|
+
if (result.isErr())
|
|
348
|
+
return `## ${label}\n\nInventory lookup failed: ${result.error.error}`;
|
|
349
|
+
const value = result.value;
|
|
350
|
+
if (typeof value === "object" && value !== null && "content" in value && typeof value.content === "string") {
|
|
351
|
+
return `## ${label}\n\n${value.content}`;
|
|
352
|
+
}
|
|
353
|
+
return `## ${label}\n\n${JSON.stringify(value)}`;
|
|
370
354
|
}
|
|
371
355
|
/**
|
|
372
356
|
* Translate the captured `PlannerOutcome` (plus any loop error) into the
|
|
373
|
-
* `PlanningAgentOutput` contract the conversation agent consumes
|
|
357
|
+
* `PlanningAgentOutput` contract the conversation agent consumes, alongside the
|
|
358
|
+
* kind the diagnostic record carries.
|
|
359
|
+
*
|
|
360
|
+
* Both come out of one branch deliberately: deriving the kind separately would
|
|
361
|
+
* be a second copy of this precedence order, free to drift from the answer the
|
|
362
|
+
* caller actually returned.
|
|
374
363
|
*/
|
|
375
364
|
function shapeOutcome(args) {
|
|
376
365
|
const { holder, runError, timedOut, outerAborted } = args;
|
|
377
366
|
const outcome = holder.outcome;
|
|
378
367
|
if (outcome?.kind === "plan_submitted") {
|
|
379
|
-
return { event: "plan_complete", planId: outcome.planId, plan: outcome.plan };
|
|
368
|
+
return { output: { event: "plan_complete", planId: outcome.planId, plan: outcome.plan }, kind: "plan_submitted" };
|
|
380
369
|
}
|
|
381
370
|
if (outcome?.kind === "clarification") {
|
|
382
371
|
return {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
372
|
+
output: {
|
|
373
|
+
event: "clarification_needed",
|
|
374
|
+
question: outcome.question,
|
|
375
|
+
...(outcome.questionContext ? { questionContext: outcome.questionContext } : {}),
|
|
376
|
+
},
|
|
377
|
+
kind: "clarification",
|
|
386
378
|
};
|
|
387
379
|
}
|
|
388
380
|
if (outcome?.kind === "blocker") {
|
|
389
|
-
return { event: "error", error: outcome.reason };
|
|
381
|
+
return { output: { event: "error", error: outcome.reason }, kind: "blocker" };
|
|
390
382
|
}
|
|
391
383
|
if (outcome?.kind === "persist_error") {
|
|
392
|
-
return { event: "error", error: `Failed to save plan: ${outcome.message}` };
|
|
384
|
+
return { output: { event: "error", error: `Failed to save plan: ${outcome.message}` }, kind: "persist_error" };
|
|
393
385
|
}
|
|
394
386
|
// No terminal outcome — something went wrong in the loop.
|
|
395
387
|
if (timedOut) {
|
|
396
388
|
return {
|
|
397
|
-
|
|
398
|
-
|
|
389
|
+
output: {
|
|
390
|
+
event: "error",
|
|
391
|
+
error: "Plan generation timed out — the model may be overloaded.",
|
|
392
|
+
},
|
|
393
|
+
kind: "timeout",
|
|
399
394
|
};
|
|
400
395
|
}
|
|
401
396
|
if (outerAborted) {
|
|
402
|
-
return { event: "error", error: "Plan generation was cancelled." };
|
|
397
|
+
return { output: { event: "error", error: "Plan generation was cancelled." }, kind: "cancelled" };
|
|
403
398
|
}
|
|
404
399
|
if (runError) {
|
|
405
400
|
const msg = runError instanceof Error ? runError.message : String(runError);
|
|
406
|
-
return { event: "error", error: `Plan generation failed: ${msg}` };
|
|
401
|
+
return { output: { event: "error", error: `Plan generation failed: ${msg}` }, kind: "loop_error" };
|
|
407
402
|
}
|
|
408
403
|
return {
|
|
409
|
-
|
|
410
|
-
|
|
404
|
+
output: {
|
|
405
|
+
event: "error",
|
|
406
|
+
error: "Plan generation completed without a terminal outcome — the planner " + "did not call submit_plan, request_clarification, or report_blocker.",
|
|
407
|
+
},
|
|
408
|
+
kind: "no_outcome",
|
|
411
409
|
};
|
|
412
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Severity follows the ending, not the return type — which is uniformly `ok(...)`,
|
|
413
|
+
* so it distinguishes nothing. A submitted plan and a clarification request are
|
|
414
|
+
* both this tool working as designed; a blocker is a real answer that cost the
|
|
415
|
+
* user their plan; the rest are failures.
|
|
416
|
+
*/
|
|
417
|
+
const OUTCOME_LEVEL = {
|
|
418
|
+
plan_submitted: "info",
|
|
419
|
+
clarification: "info",
|
|
420
|
+
blocker: "warn",
|
|
421
|
+
persist_error: "error",
|
|
422
|
+
timeout: "error",
|
|
423
|
+
cancelled: "error",
|
|
424
|
+
loop_error: "error",
|
|
425
|
+
no_outcome: "error",
|
|
426
|
+
invalid_parent_plan: "warn",
|
|
427
|
+
parent_plan_load_failed: "error",
|
|
428
|
+
};
|
|
413
429
|
/** Build the `generate_plan` tool bound to its provider and pool. */
|
|
414
430
|
export function createGeneratePlanTool(deps) {
|
|
431
|
+
const baseLogger = (deps.logger ?? createNoopLogger()).named("generate-plan");
|
|
415
432
|
return defineTool({
|
|
416
433
|
id: "generate_plan",
|
|
417
434
|
description: "Generate an analysis plan (DAG of steps) for this analysis's data and the user's research question. " +
|
|
@@ -420,7 +437,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
420
437
|
"to the planner directly. Do NOT summarize or re-type them into this call; you cannot restate that " +
|
|
421
438
|
"record more faithfully than the record itself. Pass only what the profile cannot hold: the research " +
|
|
422
439
|
"question, facts the user told you (analystNotes), prior run results, and their constraints. " +
|
|
423
|
-
"Returns a structured plan ready for show_plan and
|
|
440
|
+
"Returns a structured plan ready for show_plan and execute_analysis plan mode, or a clarification question if " +
|
|
424
441
|
"the planner is missing something it cannot infer.",
|
|
425
442
|
inputSchema: z.object({
|
|
426
443
|
researchQuestion: z.string().describe("What the user wants to analyze — their goal and specific questions."),
|
|
@@ -450,6 +467,18 @@ export function createGeneratePlanTool(deps) {
|
|
|
450
467
|
}),
|
|
451
468
|
execute: async (input, ctx) => {
|
|
452
469
|
const analysisId = scopeResource(ctx.session.scope).resourceId;
|
|
470
|
+
// Bound per invocation: `analysisId` comes from the call-time session, not from
|
|
471
|
+
// the construction-time deps the factory closes over.
|
|
472
|
+
const logger = baseLogger.with({ analysisId });
|
|
473
|
+
const startedAt = Date.now();
|
|
474
|
+
// Every exit routes through here, so "recorded exactly once per invocation" holds
|
|
475
|
+
// by construction rather than by remembering. `elapsedMs` is what separates a
|
|
476
|
+
// planner that gave up early from one still working when the guard cut it — the
|
|
477
|
+
// fixed budget above makes the number readable without any other context.
|
|
478
|
+
const finish = (shaped) => {
|
|
479
|
+
logger[OUTCOME_LEVEL[shaped.kind]]("plan generation finished", { outcome: shaped.kind, elapsedMs: Date.now() - startedAt });
|
|
480
|
+
return ok(shaped.output);
|
|
481
|
+
};
|
|
453
482
|
// If iterating, load the parent plan so the planner sees what it is
|
|
454
483
|
// revising. Fails fast with the sanitized message submit_plan would
|
|
455
484
|
// have surfaced — saves a wasted planner run.
|
|
@@ -460,17 +489,17 @@ export function createGeneratePlanTool(deps) {
|
|
|
460
489
|
analysisId,
|
|
461
490
|
}));
|
|
462
491
|
if (!priorPlan) {
|
|
463
|
-
return
|
|
464
|
-
event: "error",
|
|
465
|
-
|
|
492
|
+
return finish({
|
|
493
|
+
output: { event: "error", error: "parentPlanId is not a valid plan for this analysis" },
|
|
494
|
+
kind: "invalid_parent_plan",
|
|
466
495
|
});
|
|
467
496
|
}
|
|
468
497
|
priorPlanBlock = formatPriorPlan(input.parentPlanId, priorPlan);
|
|
469
498
|
}
|
|
470
499
|
catch {
|
|
471
|
-
return
|
|
472
|
-
event: "error",
|
|
473
|
-
|
|
500
|
+
return finish({
|
|
501
|
+
output: { event: "error", error: "Plan iteration failed — parent plan could not be loaded." },
|
|
502
|
+
kind: "parent_plan_load_failed",
|
|
474
503
|
});
|
|
475
504
|
}
|
|
476
505
|
}
|
|
@@ -484,9 +513,21 @@ export function createGeneratePlanTool(deps) {
|
|
|
484
513
|
// the user their plan.
|
|
485
514
|
const profileStatus = await loadDataProfileStatus(deps.pool, analysisId).unwrapOr(null);
|
|
486
515
|
const dataContextBlock = renderDataContext(classifyGrounding(profileStatus));
|
|
516
|
+
// Reference data and package availability are mandatory grounding,
|
|
517
|
+
// not optional planner lookups. Read both inventories host-side before
|
|
518
|
+
// the loop and put their rendered content in the planner's seed.
|
|
519
|
+
const listAvailableRefs = createListAvailableRefsTool(deps.refStorePath === undefined ? {} : { refStorePath: deps.refStorePath });
|
|
520
|
+
const listAvailablePackages = createListAvailablePackagesTool(deps.packagesFile === undefined ? {} : { packagesFile: deps.packagesFile });
|
|
521
|
+
// A path-separator query matches every reference path and selects the
|
|
522
|
+
// tool's recursive leaf scan, so the seed contains usable file paths,
|
|
523
|
+
// not only top-level directory summaries.
|
|
524
|
+
const [refsResult, packagesResult] = await Promise.all([listAvailableRefs.execute({ query: "/" }, ctx), listAvailablePackages.execute({}, ctx)]);
|
|
525
|
+
const groundingBlock = [inventoryContent("Available Reference Data", refsResult), inventoryContent("Available Packages", packagesResult)].join("\n\n");
|
|
487
526
|
const prompt = [
|
|
488
527
|
...(priorPlanBlock ? [priorPlanBlock, ""] : []),
|
|
489
528
|
...(dataContextBlock ? [dataContextBlock, ""] : []),
|
|
529
|
+
groundingBlock,
|
|
530
|
+
"",
|
|
490
531
|
"## Research Question",
|
|
491
532
|
input.researchQuestion,
|
|
492
533
|
...(input.analystNotes ? ["", "## Analyst Notes (from the user — facts about the data the profile does not record)", input.analystNotes] : []),
|
|
@@ -498,12 +539,12 @@ export function createGeneratePlanTool(deps) {
|
|
|
498
539
|
analysisId,
|
|
499
540
|
parentPlanId: input.parentPlanId ?? null,
|
|
500
541
|
};
|
|
501
|
-
const innerTools = buildInnerTools(holder, persistCtx, deps.pool, deps.resourcePolicy,
|
|
542
|
+
const innerTools = buildInnerTools(holder, persistCtx, deps.pool, deps.resourcePolicy, logger);
|
|
502
543
|
const planner = {
|
|
503
544
|
id: PLANNER_AGENT_ID,
|
|
504
545
|
systemPrompt: composeSystemPrompt(plannerInstructions(deps.resourcePolicy)),
|
|
505
|
-
model: deps.model,
|
|
506
|
-
tools: innerTools.
|
|
546
|
+
model: deps.conversation.model,
|
|
547
|
+
tools: innerTools.terminal,
|
|
507
548
|
maxIterations: PLANNER_MAX_ITERATIONS,
|
|
508
549
|
};
|
|
509
550
|
// Merge the outer abort signal with a wall-clock timeout — either
|
|
@@ -512,12 +553,17 @@ export function createGeneratePlanTool(deps) {
|
|
|
512
553
|
let runError = null;
|
|
513
554
|
try {
|
|
514
555
|
await runToTerminal(planner, [{ role: "user", content: prompt }], forSubAgent(ctx.session, PLANNER_AGENT_ID), {
|
|
515
|
-
provider: deps.provider,
|
|
556
|
+
provider: deps.conversation.provider,
|
|
516
557
|
signal,
|
|
517
558
|
emit: ctx.emit,
|
|
518
559
|
runStep: passthroughStep,
|
|
519
|
-
}, {
|
|
520
560
|
resolved: () => holder.outcome !== null,
|
|
561
|
+
// Planner prose is unusable: every meaningful outcome is
|
|
562
|
+
// a tool call, and the terminal predicate stops the loop
|
|
563
|
+
// as soon as one is recorded.
|
|
564
|
+
toolChoice: "required",
|
|
565
|
+
logger,
|
|
566
|
+
}, {
|
|
521
567
|
tools: innerTools.terminal,
|
|
522
568
|
nudge: "You ended without a terminal outcome. Call submit_plan with " +
|
|
523
569
|
"your final plan now, or request_clarification / report_blocker " +
|
|
@@ -527,7 +573,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
527
573
|
catch (err) {
|
|
528
574
|
runError = err;
|
|
529
575
|
}
|
|
530
|
-
return
|
|
576
|
+
return finish(shapeOutcome({
|
|
531
577
|
holder,
|
|
532
578
|
runError,
|
|
533
579
|
timedOut: signal.aborted && !ctx.signal.aborted,
|
|
@@ -12,11 +12,9 @@
|
|
|
12
12
|
* and it is why the record is served in full rather than summarized: an agent that
|
|
13
13
|
* cannot pull a fact here has no fallback but to re-derive it from the raw bytes.
|
|
14
14
|
*
|
|
15
|
-
* Bounded by construction. `inspect_run
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* reports `total` and `hasMore`: an elided tail is a fact the model can see and act on,
|
|
19
|
-
* never a silent truncation.
|
|
15
|
+
* Bounded by construction. Like `inspect_run`, the per-file scope is explicitly
|
|
16
|
+
* paged and always reports `total` and `hasMore`: an elided tail is a fact the
|
|
17
|
+
* model can see and act on, never a silent truncation.
|
|
20
18
|
*
|
|
21
19
|
* Lifecycle states are data variants, not errors (the `defineTool` contract): a missing,
|
|
22
20
|
* in-flight, failed, or stale profile is an ordinary, expected outcome the model must be
|
|
@@ -61,7 +59,17 @@ interface FilesOutput extends ProfileEnvelope {
|
|
|
61
59
|
readonly hasMore: boolean;
|
|
62
60
|
readonly files: DataProfileFile[];
|
|
63
61
|
}
|
|
64
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* No profile to serve — the honest lifecycle states, in the ok channel.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately carries no staleness verdict. None of this variant's three
|
|
66
|
+
* producers retains a comparand: an analysis that was never profiled and one
|
|
67
|
+
* with no input files never had one, and `clearDataProfile` nulls
|
|
68
|
+
* `seed_input_file_ids` along with every other profile column, so a cleared row
|
|
69
|
+
* keeps no record of what it once covered. Such a field could only ever be
|
|
70
|
+
* constant here, and a constant field carries no information while implying
|
|
71
|
+
* that it does. `FailedOutput` omits one for the same reason — see its note.
|
|
72
|
+
*/
|
|
65
73
|
interface AbsentOutput {
|
|
66
74
|
readonly state: "absent";
|
|
67
75
|
readonly message: string;
|
|
@@ -71,9 +79,26 @@ interface PendingOutput {
|
|
|
71
79
|
readonly status: "pending" | "running";
|
|
72
80
|
readonly message: string;
|
|
73
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* A recorded failure with no profile to fall back on.
|
|
84
|
+
*
|
|
85
|
+
* Carries no staleness verdict, deliberately. Whether the failed attempt covered
|
|
86
|
+
* the files the analysis holds NOW is not derivable here: this tool reads one
|
|
87
|
+
* `cortex_analysis_state` row, the current input set is the embedder's knowledge,
|
|
88
|
+
* and `seed_input_file_ids` is the most recently seeded set rather than a
|
|
89
|
+
* per-attempt snapshot (`upsertAnalysis` overwrites it). A field reporting that
|
|
90
|
+
* verdict could only ever hold one value, which carries no information while
|
|
91
|
+
* implying that it does — and the tool description, being the whole of what an
|
|
92
|
+
* agent knows about this tool, must not advertise a distinction it cannot make.
|
|
93
|
+
*
|
|
94
|
+
* `failedAt` is reported instead: a fact the row actually holds, and one an agent
|
|
95
|
+
* that knows when the input set last changed can act on.
|
|
96
|
+
*/
|
|
74
97
|
interface FailedOutput {
|
|
75
98
|
readonly state: "failed";
|
|
76
99
|
readonly error: string | null;
|
|
100
|
+
/** When the failure was recorded; `null` when the row holds no time. */
|
|
101
|
+
readonly failedAt: string | null;
|
|
77
102
|
readonly message: string;
|
|
78
103
|
}
|
|
79
104
|
export type InspectDataProfileOutput = OverviewOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
|
|
@@ -12,11 +12,9 @@
|
|
|
12
12
|
* and it is why the record is served in full rather than summarized: an agent that
|
|
13
13
|
* cannot pull a fact here has no fallback but to re-derive it from the raw bytes.
|
|
14
14
|
*
|
|
15
|
-
* Bounded by construction. `inspect_run
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* reports `total` and `hasMore`: an elided tail is a fact the model can see and act on,
|
|
19
|
-
* never a silent truncation.
|
|
15
|
+
* Bounded by construction. Like `inspect_run`, the per-file scope is explicitly
|
|
16
|
+
* paged and always reports `total` and `hasMore`: an elided tail is a fact the
|
|
17
|
+
* model can see and act on, never a silent truncation.
|
|
20
18
|
*
|
|
21
19
|
* Lifecycle states are data variants, not errors (the `defineTool` contract): a missing,
|
|
22
20
|
* in-flight, failed, or stale profile is an ordinary, expected outcome the model must be
|
|
@@ -57,6 +55,15 @@ function stalenessReasons(status, result) {
|
|
|
57
55
|
}
|
|
58
56
|
return reasons;
|
|
59
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The qualification the `failed` message must carry. A bare `failed: <error>`
|
|
60
|
+
* reads as a verdict on whatever files the agent is currently holding, and
|
|
61
|
+
* `message` is what an agent quotes back to the user — so the limit of what this
|
|
62
|
+
* row can establish belongs in the prose channel, not only in the field set.
|
|
63
|
+
*/
|
|
64
|
+
const FAILURE_SCOPE_NOTE = "This records an earlier attempt; this row cannot establish whether it covered the input files the analysis " +
|
|
65
|
+
"holds now, so do not read it as a verdict on the current data. Compare failedAt against when the input set " +
|
|
66
|
+
"last changed, and re-profile if they have moved apart.";
|
|
60
67
|
export function createInspectDataProfileTool(pool) {
|
|
61
68
|
return defineTool({
|
|
62
69
|
id: "inspect_data_profile",
|
|
@@ -72,7 +79,11 @@ export function createInspectDataProfileTool(pool) {
|
|
|
72
79
|
"always with the true total and hasMore, so you can see exactly what you have not read yet. " +
|
|
73
80
|
"The state field says what you got: 'ready'; 'stale' (a profile is returned but may not describe the current " +
|
|
74
81
|
"inputs — staleReason says why); 'pending' (profiling is still running); 'failed'; or 'absent' (never profiled, " +
|
|
75
|
-
"or the analysis has no input files)."
|
|
82
|
+
"or the analysis has no input files). " +
|
|
83
|
+
"A 'failed' state reports a PAST attempt, with failedAt naming when it was recorded — it is not a verdict on the " +
|
|
84
|
+
"input files you are holding now, and this tool cannot tell you whether that attempt covered them. Compare failedAt " +
|
|
85
|
+
"against when the input set last changed before you conclude anything: if the data moved since, say the profile " +
|
|
86
|
+
"needs re-running rather than diagnosing the data itself.",
|
|
76
87
|
inputSchema: z.object({
|
|
77
88
|
scope: z
|
|
78
89
|
.enum(["overview", "files"])
|
|
@@ -101,10 +112,15 @@ export function createInspectDataProfileTool(pool) {
|
|
|
101
112
|
const result = status.result;
|
|
102
113
|
if (!result) {
|
|
103
114
|
if (status.status === "failed") {
|
|
115
|
+
// A failure that DID leave a prior profile on the row never reaches
|
|
116
|
+
// here — it is served as `stale`, where `stalenessReasons` names the
|
|
117
|
+
// failed re-profile and compares the input sets through the single
|
|
118
|
+
// shared predicate. This branch is the case with nothing to compare.
|
|
104
119
|
return ok({
|
|
105
120
|
state: "failed",
|
|
106
121
|
error: status.error,
|
|
107
|
-
|
|
122
|
+
failedAt: status.completedAt,
|
|
123
|
+
message: `Data profiling failed and no earlier profile exists: ${status.error ?? "no reason recorded"}. ${FAILURE_SCOPE_NOTE}`,
|
|
108
124
|
});
|
|
109
125
|
}
|
|
110
126
|
if (status.status === "completed") {
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* inspectRun — inspect workflow runs for the current analysis.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Results are pull-only via this tool — the workflow does NOT write to the
|
|
9
|
-
* conversation thread on completion. The retired `run-result-writer` push
|
|
10
|
-
* payload (run status, step summaries, run synthesis, artifact paths) is
|
|
11
|
-
* surfaced here on demand: the run row + step `summaryPath` references +
|
|
12
|
-
* the `synthesisPath` for runs whose synthesis was produced let the
|
|
13
|
-
* conversation agent `read_file` the artefacts it needs.
|
|
4
|
+
* Results are pull-only: workflows never append completion messages to a
|
|
5
|
+
* conversation. List mode is active-first and paged. Targeted mode makes
|
|
6
|
+
* readiness explicit and can perform one bounded, abort-aware wait.
|
|
14
7
|
*/
|
|
15
8
|
import type { Pool } from "pg";
|
|
16
9
|
import type { CortexRunRow } from "../../state/schema.js";
|
|
@@ -20,7 +13,6 @@ interface FormattedStep {
|
|
|
20
13
|
agentId: string;
|
|
21
14
|
wave: number;
|
|
22
15
|
status: string;
|
|
23
|
-
/** Absent for steps that never ran (`pending`/`skipped`) — no output tree exists to point at. */
|
|
24
16
|
summaryPath?: string;
|
|
25
17
|
durationMs?: number | null;
|
|
26
18
|
error?: string | null;
|
|
@@ -29,15 +21,34 @@ interface FormattedStep {
|
|
|
29
21
|
finishReason?: string | null;
|
|
30
22
|
hitMaxSteps?: boolean | null;
|
|
31
23
|
}
|
|
24
|
+
interface WaitMetadata {
|
|
25
|
+
readonly requestedSeconds: number;
|
|
26
|
+
readonly cutoffReached: boolean;
|
|
27
|
+
}
|
|
32
28
|
type InspectRunOutput = {
|
|
33
|
-
message: string;
|
|
34
|
-
} | {
|
|
35
29
|
runs: FormattedRun[];
|
|
30
|
+
total: number;
|
|
31
|
+
page: number;
|
|
32
|
+
pageSize: number;
|
|
33
|
+
hasMore: boolean;
|
|
34
|
+
} | {
|
|
35
|
+
inspectionState: "not_found";
|
|
36
|
+
message: string;
|
|
37
|
+
wait?: WaitMetadata;
|
|
36
38
|
} | {
|
|
39
|
+
inspectionState: "in_progress" | "suspended" | "terminal";
|
|
40
|
+
message: string;
|
|
37
41
|
run: FormattedRun;
|
|
38
42
|
steps: FormattedStep[];
|
|
43
|
+
elapsedMs?: number;
|
|
44
|
+
wait?: WaitMetadata;
|
|
45
|
+
selfWaitPrevented?: boolean;
|
|
39
46
|
};
|
|
40
|
-
|
|
47
|
+
interface InspectRunClock {
|
|
48
|
+
now(): number;
|
|
49
|
+
wait(ms: number, signal: AbortSignal): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
declare function formatRun(r: CortexRunRow, verbose: boolean, exposePaths?: boolean): {
|
|
41
52
|
error?: string | null | undefined;
|
|
42
53
|
synthesisReason?: string | undefined;
|
|
43
54
|
runId: string;
|
|
@@ -49,8 +60,15 @@ declare function formatRun(r: CortexRunRow, verbose: boolean): {
|
|
|
49
60
|
synthesisPath: string | null;
|
|
50
61
|
synthesisStatus: "failed" | "produced" | "skipped_no_summaries" | "skipped_blocker" | null;
|
|
51
62
|
};
|
|
52
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Build the run-inspection tool. The optional clock is an internal testing
|
|
65
|
+
* seam for deterministic cutoff and transition coverage.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createInspectRunTool(pool: Pool, clock?: InspectRunClock): import("../define-tool.js").Tool<{
|
|
53
68
|
runId?: string | undefined;
|
|
69
|
+
page?: number | undefined;
|
|
70
|
+
pageSize?: number | undefined;
|
|
71
|
+
waitForTerminalSeconds?: number | undefined;
|
|
54
72
|
includeDiagnostics?: boolean | undefined;
|
|
55
73
|
}, InspectRunOutput>;
|
|
56
74
|
export {};
|