@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.
Files changed (207) hide show
  1. package/dist/agents/conversation-agent.d.ts +9 -15
  2. package/dist/agents/conversation-agent.js +32 -31
  3. package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
  4. package/dist/agents/sandbox/chromatin-agent.js +1 -1
  5. package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
  6. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
  7. package/dist/agents/sandbox/enrichment-agent.js +1 -1
  8. package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
  9. package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
  10. package/dist/agents/sandbox/index.js +0 -3
  11. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
  12. package/dist/agents/sandbox/network-agent.js +1 -1
  13. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
  14. package/dist/agents/sandbox/scientific-executor.js +1 -1
  15. package/dist/agents/sandbox/shared.d.ts +2 -2
  16. package/dist/agents/sandbox/shared.js +8 -14
  17. package/dist/agents/sandbox/translational-safety-agent.js +4 -7
  18. package/dist/agents/sandbox/types.d.ts +5 -5
  19. package/dist/agents/sandbox/types.js +4 -4
  20. package/dist/app/chat-turn.js +4 -1
  21. package/dist/app/message-assembly.d.ts +5 -2
  22. package/dist/app/message-assembly.js +7 -2
  23. package/dist/app/run-activity.d.ts +13 -0
  24. package/dist/app/run-activity.js +51 -0
  25. package/dist/config/resource-limits.d.ts +0 -7
  26. package/dist/config/resource-limits.js +0 -1
  27. package/dist/contracts/target-dossier.d.ts +10 -10
  28. package/dist/execution/dbos-run-launcher.js +1 -26
  29. package/dist/execution/report-runner.js +1 -1
  30. package/dist/execution/run-authorizer.d.ts +1 -1
  31. package/dist/execution/run-authorizer.js +1 -1
  32. package/dist/execution/run-event-parts.d.ts +44 -0
  33. package/dist/execution/run-event-parts.js +0 -0
  34. package/dist/execution/run-event-stream.d.ts +91 -0
  35. package/dist/execution/run-event-stream.js +212 -0
  36. package/dist/execution/run-launcher.d.ts +2 -18
  37. package/dist/execution/run-launcher.js +2 -4
  38. package/dist/execution/run-synthesis.js +1 -1
  39. package/dist/index.d.ts +7 -4
  40. package/dist/index.js +23 -6
  41. package/dist/input-sanitization.d.ts +14 -4
  42. package/dist/input-sanitization.js +27 -4
  43. package/dist/lib/storage.js +4 -1
  44. package/dist/loop/budget-exceeded.d.ts +18 -2
  45. package/dist/loop/budget-exceeded.js +49 -2
  46. package/dist/loop/run-agent.d.ts +22 -1
  47. package/dist/loop/run-agent.js +51 -5
  48. package/dist/loop/run-to-terminal.d.ts +0 -2
  49. package/dist/loop/run-to-terminal.js +8 -1
  50. package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
  51. package/dist/memory/ai-sdk-message-storage.js +45 -4
  52. package/dist/memory/content-to-cortex.js +19 -3
  53. package/dist/memory/message-backfill.js +1 -1
  54. package/dist/memory/reconstruct-cards.d.ts +1 -1
  55. package/dist/memory/reconstruct-cards.js +7 -5
  56. package/dist/memory/thread-history.js +72 -8
  57. package/dist/prompts/conversation.d.ts +1 -1
  58. package/dist/prompts/conversation.js +41 -15
  59. package/dist/prompts/literature-reviewer.d.ts +1 -1
  60. package/dist/prompts/literature-reviewer.js +16 -14
  61. package/dist/prompts/planner.d.ts +3 -0
  62. package/dist/prompts/planner.js +16 -21
  63. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  64. package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
  65. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  66. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
  67. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
  69. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
  71. package/dist/prompts/sandbox-standards.d.ts +4 -4
  72. package/dist/prompts/sandbox-standards.js +5 -5
  73. package/dist/prompts/synthesis-agent.d.ts +1 -1
  74. package/dist/prompts/synthesis-agent.js +7 -7
  75. package/dist/providers/errors.d.ts +7 -0
  76. package/dist/providers/errors.js +88 -3
  77. package/dist/runtime/assemble.d.ts +5 -10
  78. package/dist/runtime/assemble.js +0 -4
  79. package/dist/runtime/boot.d.ts +2 -2
  80. package/dist/runtime/boot.js +1 -1
  81. package/dist/runtime/dbos.d.ts +4 -5
  82. package/dist/runtime/dbos.js +6 -7
  83. package/dist/runtime/pools.d.ts +15 -1
  84. package/dist/runtime/pools.js +15 -1
  85. package/dist/sandbox/mount-plan.d.ts +1 -1
  86. package/dist/sandbox/sandbox-error.d.ts +1 -1
  87. package/dist/sandbox/sandbox-error.js +1 -1
  88. package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
  89. package/dist/sandbox/sandbox-step-translate.js +17 -2
  90. package/dist/sandbox/types.d.ts +1 -1
  91. package/dist/schemas/validate-plan.d.ts +1 -1
  92. package/dist/schemas/workflow-state.d.ts +2 -2
  93. package/dist/schemas/workflow-state.js +2 -2
  94. package/dist/state/data-profile.d.ts +53 -1
  95. package/dist/state/data-profile.js +66 -6
  96. package/dist/state/index.d.ts +6 -6
  97. package/dist/state/index.js +4 -4
  98. package/dist/state/init.js +37 -2
  99. package/dist/state/plans.js +1 -1
  100. package/dist/state/runs.d.ts +49 -0
  101. package/dist/state/runs.js +150 -0
  102. package/dist/tasks/data-profile-activity.d.ts +87 -0
  103. package/dist/tasks/data-profile-activity.js +56 -0
  104. package/dist/tasks/data-profile.js +66 -4
  105. package/dist/tools/__fixtures__/tool-context.js +1 -0
  106. package/dist/tools/ad-hoc-router.d.ts +31 -0
  107. package/dist/tools/ad-hoc-router.js +132 -0
  108. package/dist/tools/analysis-invocation.d.ts +4 -0
  109. package/dist/tools/analysis-invocation.js +17 -0
  110. package/dist/tools/bio/chembl.d.ts +1 -1
  111. package/dist/tools/bio/chembl.js +51 -54
  112. package/dist/tools/bio/comptox.js +35 -33
  113. package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
  114. package/dist/tools/bio/drug-gene-interactions.js +175 -0
  115. package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
  116. package/dist/tools/bio/gene-disease-evidence.js +187 -0
  117. package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
  118. package/dist/tools/bio/gene-preclinical-profile.js +125 -0
  119. package/dist/tools/bio/index.d.ts +23 -16
  120. package/dist/tools/bio/index.js +29 -22
  121. package/dist/tools/bio/keys.d.ts +16 -9
  122. package/dist/tools/bio/keys.js +22 -13
  123. package/dist/tools/bio/lookup-annotation.d.ts +48 -0
  124. package/dist/tools/bio/lookup-annotation.js +173 -0
  125. package/dist/tools/bio/opentargets.d.ts +7 -11
  126. package/dist/tools/bio/opentargets.js +24 -35
  127. package/dist/tools/bio/pubchem.js +6 -4
  128. package/dist/tools/bio/pubmed.d.ts +15 -7
  129. package/dist/tools/bio/pubmed.js +79 -20
  130. package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
  131. package/dist/tools/bio/search-clinical-trials.js +26 -5
  132. package/dist/tools/bio/search-faers.js +9 -3
  133. package/dist/tools/bio/search-gene.d.ts +0 -1
  134. package/dist/tools/bio/search-gene.js +4 -5
  135. package/dist/tools/bio/search-geo-datasets.js +10 -4
  136. package/dist/tools/bio/search-interactions.d.ts +8 -4
  137. package/dist/tools/bio/search-interactions.js +28 -12
  138. package/dist/tools/bio/target-safety.d.ts +59 -0
  139. package/dist/tools/bio/target-safety.js +191 -0
  140. package/dist/tools/define-tool.d.ts +6 -2
  141. package/dist/tools/define-tool.js +1 -1
  142. package/dist/tools/execute-analysis.d.ts +44 -0
  143. package/dist/tools/execute-analysis.js +252 -0
  144. package/dist/tools/iterate-report.js +7 -6
  145. package/dist/tools/lib/dgidb-client.d.ts +49 -0
  146. package/dist/tools/lib/dgidb-client.js +168 -0
  147. package/dist/tools/lib/disgenet-client.d.ts +56 -0
  148. package/dist/tools/lib/disgenet-client.js +59 -0
  149. package/dist/tools/lib/drugbank-client.d.ts +83 -0
  150. package/dist/tools/lib/drugbank-client.js +95 -0
  151. package/dist/tools/lib/ensembl-client.d.ts +4 -1
  152. package/dist/tools/lib/ensembl-client.js +5 -3
  153. package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
  154. package/dist/tools/lib/gwas-catalog-client.js +136 -0
  155. package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
  156. package/dist/tools/lib/pharmgkb-client.js +32 -0
  157. package/dist/tools/lib/pubmed-client.d.ts +57 -4
  158. package/dist/tools/lib/pubmed-client.js +57 -3
  159. package/dist/tools/lib/string-client.d.ts +25 -4
  160. package/dist/tools/lib/string-client.js +22 -5
  161. package/dist/tools/research/generate-plan.d.ts +16 -9
  162. package/dist/tools/research/generate-plan.js +116 -70
  163. package/dist/tools/research/inspect-data-profile.d.ts +31 -6
  164. package/dist/tools/research/inspect-data-profile.js +23 -7
  165. package/dist/tools/research/inspect-run.d.ts +33 -15
  166. package/dist/tools/research/inspect-run.js +155 -66
  167. package/dist/tools/research/literature-reviewer.js +7 -11
  168. package/dist/workflows/execute-analysis.d.ts +68 -5
  169. package/dist/workflows/execute-analysis.js +76 -7
  170. package/dist/workflows/sandbox-step.js +1 -11
  171. package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
  172. package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
  173. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
  174. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  175. package/package.json +1 -1
  176. package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
  177. package/dist/agents/sandbox/ephemeral-executor.js +0 -20
  178. package/dist/execution/ephemeral-runner.d.ts +0 -98
  179. package/dist/execution/ephemeral-runner.js +0 -189
  180. package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
  181. package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
  182. package/dist/tools/bio/check-safety-panel.d.ts +0 -35
  183. package/dist/tools/bio/check-safety-panel.js +0 -76
  184. package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
  185. package/dist/tools/bio/get-impc-ko-profile.js +0 -23
  186. package/dist/tools/bio/lookup-go-term.d.ts +0 -31
  187. package/dist/tools/bio/lookup-go-term.js +0 -116
  188. package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
  189. package/dist/tools/bio/search-bgee-expression.js +0 -30
  190. package/dist/tools/bio/search-clinvar.d.ts +0 -13
  191. package/dist/tools/bio/search-clinvar.js +0 -28
  192. package/dist/tools/bio/search-dgidb.d.ts +0 -53
  193. package/dist/tools/bio/search-dgidb.js +0 -213
  194. package/dist/tools/bio/search-disgenet.d.ts +0 -30
  195. package/dist/tools/bio/search-disgenet.js +0 -84
  196. package/dist/tools/bio/search-drugbank.d.ts +0 -38
  197. package/dist/tools/bio/search-drugbank.js +0 -109
  198. package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
  199. package/dist/tools/bio/search-gwas-catalog.js +0 -172
  200. package/dist/tools/bio/search-pathway.d.ts +0 -12
  201. package/dist/tools/bio/search-pathway.js +0 -32
  202. package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
  203. package/dist/tools/bio/search-pharmgkb.js +0 -52
  204. package/dist/tools/execute-plan.d.ts +0 -64
  205. package/dist/tools/execute-plan.js +0 -211
  206. package/dist/tools/run-ephemeral.d.ts +0 -33
  207. 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 four terminal tool calls:
14
+ * EXCLUSIVELY via three terminal tool calls:
15
15
  *
16
- * - validate_plan(plan) → non-terminal dry-run: an any-shape candidate
17
- * in, {valid, issues} out (schema + semantic)
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: 1 draft + ~3 validate/fix
53
- * cycles + 1 submit + headroom.
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
- // Strict: a malformed plan must not reach submission. `execute` still
289
- // re-validates (schema + semantic) — it does not lean on this boundary.
290
- inputSchema: z.object({ plan: PlannerPlanSchema }),
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 { all: [validatePlanTool, listAvailableRefs, listAvailablePackages, ...terminal], terminal };
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
- event: "clarification_needed",
384
- question: outcome.question,
385
- ...(outcome.questionContext ? { questionContext: outcome.questionContext } : {}),
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
- event: "error",
398
- error: "Plan generation timed out — the model may be overloaded.",
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
- event: "error",
410
- error: "Plan generation completed without a terminal outcome — the planner " + "did not call submit_plan, request_clarification, or report_blocker.",
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 execute_plan, or a clarification question if " +
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 ok({
464
- event: "error",
465
- error: "parentPlanId is not a valid plan for this analysis",
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 ok({
472
- event: "error",
473
- error: "Plan iteration failed — parent plan could not be loaded.",
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, deps);
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.all,
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 ok(shapeOutcome({
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` is the cautionary counter-example — it caps
16
- * its query at 50 rows with no `limit` parameter and no signal to the caller, so a
17
- * 51st run is simply invisible. Here the per-file scope is explicitly paged and always
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
- /** No profile to serve — the honest lifecycle states, in the ok channel. */
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` is the cautionary counter-example — it caps
16
- * its query at 50 rows with no `limit` parameter and no signal to the caller, so a
17
- * 51st run is simply invisible. Here the per-file scope is explicitly paged and always
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
- message: `Data profiling failed and no earlier profile exists: ${status.error ?? "no reason recorded"}.`,
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
- * Dependency-bearing: the database `Pool` is captured by the factory
5
- * (see the harness-durable-runtime spec). The analysis id is read from the request-scoped `Session`,
6
- * not an ambient request context.
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
- declare function formatRun(r: CortexRunRow, verbose: boolean): {
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
- export declare function createInspectRunTool(pool: Pool): import("../define-tool.js").Tool<{
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 {};