@inflexa-ai/harness 0.46.2 → 0.48.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 (279) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/conversation-agent.d.ts +7 -6
  3. package/dist/agents/conversation-agent.js +26 -4
  4. package/dist/agents/report-session-agent.d.ts +14 -14
  5. package/dist/agents/report-session-agent.js +40 -26
  6. package/dist/agents/sandbox/shared.d.ts +18 -13
  7. package/dist/agents/sandbox/shared.js +42 -27
  8. package/dist/agents/sandbox/types.d.ts +1 -1
  9. package/dist/agents/sandbox-catalog.d.ts +2 -0
  10. package/dist/agents/sandbox-catalog.js +30 -1
  11. package/dist/app/synthesize-run.d.ts +10 -6
  12. package/dist/app/synthesize-run.js +4 -2
  13. package/dist/contracts/analogy-report.d.ts +46 -2
  14. package/dist/contracts/analogy-report.js +31 -25
  15. package/dist/contracts/chat-parts.d.ts +3 -14
  16. package/dist/contracts/report-reference.d.ts +2 -1
  17. package/dist/contracts/report-reference.js +2 -1
  18. package/dist/contracts/schemas/chat-parts.d.ts +0 -27
  19. package/dist/contracts/schemas/chat-parts.js +1 -3
  20. package/dist/execution/run-canceler.d.ts +2 -0
  21. package/dist/execution/run-canceler.js +9 -0
  22. package/dist/execution/run-synthesis.d.ts +36 -8
  23. package/dist/execution/run-synthesis.js +114 -32
  24. package/dist/index.d.ts +5 -6
  25. package/dist/index.js +5 -7
  26. package/dist/input-scan/enrich.d.ts +5 -10
  27. package/dist/input-scan/enrich.js +12 -8
  28. package/dist/lib/hooks.d.ts +1 -1
  29. package/dist/lib/markdown-pdf.d.ts +27 -0
  30. package/dist/lib/markdown-pdf.js +236 -0
  31. package/dist/lib/metrics.d.ts +31 -0
  32. package/dist/lib/metrics.js +64 -0
  33. package/dist/lib/pdf-text.d.ts +23 -0
  34. package/dist/lib/pdf-text.js +34 -0
  35. package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
  36. package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
  37. package/dist/loop/chat-stream.js +2 -1
  38. package/dist/loop/run-loop.d.ts +5 -0
  39. package/dist/loop/run-loop.js +30 -2
  40. package/dist/loop/tool-name-repair.d.ts +11 -0
  41. package/dist/loop/tool-name-repair.js +23 -0
  42. package/dist/loop/types.d.ts +19 -0
  43. package/dist/memory/card-builders.d.ts +1 -1
  44. package/dist/prompts/SOUL.d.ts +6 -6
  45. package/dist/prompts/SOUL.js +22 -121
  46. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  47. package/dist/prompts/analogical-reasoner.js +35 -101
  48. package/dist/prompts/briefing.d.ts +20 -11
  49. package/dist/prompts/briefing.js +36 -19
  50. package/dist/prompts/conversation.d.ts +1 -1
  51. package/dist/prompts/conversation.js +103 -383
  52. package/dist/prompts/planner.d.ts +1 -5
  53. package/dist/prompts/planner.js +45 -128
  54. package/dist/prompts/report-session.d.ts +1 -1
  55. package/dist/prompts/report-session.js +29 -58
  56. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
  58. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  59. package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
  60. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  61. package/dist/prompts/sandbox/chromatin-agent.js +4 -5
  62. package/dist/prompts/sandbox/data-profiler.js +11 -34
  63. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  64. package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
  65. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  66. package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
  67. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/enrichment-agent.js +3 -1
  69. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
  71. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  72. package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
  73. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  74. package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
  75. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  76. package/dist/prompts/sandbox/microbiome-agent.js +3 -1
  77. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  78. package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
  79. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  80. package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
  81. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  82. package/dist/prompts/sandbox/network-agent.js +3 -1
  83. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  84. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
  85. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  86. package/dist/prompts/sandbox/proteomics-agent.js +3 -1
  87. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  88. package/dist/prompts/sandbox/scientific-executor.js +0 -12
  89. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/single-cell-agent.js +3 -1
  91. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
  93. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
  95. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  96. package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
  97. package/dist/prompts/sandbox-standards.d.ts +15 -18
  98. package/dist/prompts/sandbox-standards.js +96 -305
  99. package/dist/prompts/synthesis-agent.d.ts +1 -1
  100. package/dist/prompts/synthesis-agent.js +24 -90
  101. package/dist/provenance/collector.d.ts +2 -5
  102. package/dist/provenance/collector.js +2 -5
  103. package/dist/provenance/seam.d.ts +36 -1
  104. package/dist/provenance/workspace-reads.d.ts +36 -0
  105. package/dist/provenance/workspace-reads.js +33 -0
  106. package/dist/report-model/production-resolver.d.ts +22 -52
  107. package/dist/report-model/production-resolver.js +43 -112
  108. package/dist/report-render/chart-export.d.ts +9 -0
  109. package/dist/report-render/chart-export.js +1 -1
  110. package/dist/report-render/views/chart-view.d.ts +1 -0
  111. package/dist/report-render/views/chart-view.js +1 -0
  112. package/dist/report-render/views/lineage.d.ts +1 -0
  113. package/dist/report-render/views/page-view.d.ts +1 -0
  114. package/dist/report-render/views/page-view.js +1 -0
  115. package/dist/report-render/views/prose.d.ts +1 -0
  116. package/dist/report-render/views/prose.js +1 -0
  117. package/dist/report-render/views/references-view.d.ts +1 -0
  118. package/dist/report-render/views/values.d.ts +1 -0
  119. package/dist/report-render/views/values.js +1 -0
  120. package/dist/runtime/assemble.d.ts +32 -12
  121. package/dist/runtime/assemble.js +49 -38
  122. package/dist/sandbox/client.d.ts +5 -3
  123. package/dist/sandbox/create-sandbox.d.ts +7 -12
  124. package/dist/sandbox/create-sandbox.js +15 -37
  125. package/dist/sandbox/docker-client.d.ts +1 -3
  126. package/dist/sandbox/docker-client.js +3 -5
  127. package/dist/sandbox/exec-outcome.d.ts +11 -11
  128. package/dist/sandbox/exec-outcome.js +22 -22
  129. package/dist/sandbox/exec.d.ts +5 -0
  130. package/dist/sandbox/exec.js +9 -1
  131. package/dist/sandbox/identity.d.ts +4 -16
  132. package/dist/sandbox/identity.js +5 -21
  133. package/dist/sandbox/k8s-client.d.ts +0 -1
  134. package/dist/sandbox/k8s-client.js +8 -13
  135. package/dist/sandbox/lease-names.d.ts +6 -0
  136. package/dist/sandbox/lease-names.js +12 -0
  137. package/dist/sandbox/leases.d.ts +105 -0
  138. package/dist/sandbox/leases.js +519 -0
  139. package/dist/sandbox/liveness.d.ts +5 -2
  140. package/dist/sandbox/mount-plan.d.ts +10 -1
  141. package/dist/sandbox/mount-plan.js +7 -1
  142. package/dist/sandbox/reconciler.d.ts +58 -0
  143. package/dist/sandbox/reconciler.js +153 -0
  144. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  145. package/dist/sandbox/types.d.ts +25 -18
  146. package/dist/sandbox/types.js +10 -4
  147. package/dist/schemas/plan-schemas.d.ts +0 -21
  148. package/dist/schemas/plan-schemas.js +2 -3
  149. package/dist/schemas/run-synthesis.d.ts +2 -2
  150. package/dist/schemas/run-synthesis.js +3 -3
  151. package/dist/schemas/validate-plan.d.ts +1 -14
  152. package/dist/schemas/validate-plan.js +5 -24
  153. package/dist/schemas/workflow-state.d.ts +3 -32
  154. package/dist/schemas/workflow-state.js +4 -13
  155. package/dist/state/index.d.ts +2 -3
  156. package/dist/state/index.js +1 -2
  157. package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
  158. package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
  159. package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
  160. package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
  161. package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
  162. package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
  163. package/dist/state/migrations/index.js +6 -0
  164. package/dist/state/purge-analysis.d.ts +4 -2
  165. package/dist/state/purge-analysis.js +22 -11
  166. package/dist/state/sandbox-leases.d.ts +81 -0
  167. package/dist/state/sandbox-leases.js +247 -0
  168. package/dist/state/schema.d.ts +1 -13
  169. package/dist/state/schema.js +1 -6
  170. package/dist/state/step-executions.d.ts +13 -2
  171. package/dist/state/step-executions.js +23 -4
  172. package/dist/tasks/data-profile.d.ts +40 -2
  173. package/dist/tasks/data-profile.js +89 -87
  174. package/dist/tools/ad-hoc-router.d.ts +1 -6
  175. package/dist/tools/ad-hoc-router.js +5 -48
  176. package/dist/tools/bio/comptox.js +1 -1
  177. package/dist/tools/bio/drug-gene-interactions.js +1 -1
  178. package/dist/tools/bio/gene-disease-evidence.js +1 -1
  179. package/dist/tools/bio/keys.d.ts +1 -1
  180. package/dist/tools/bio/pubmed.js +31 -0
  181. package/dist/tools/bio/search-gene.js +17 -0
  182. package/dist/tools/bio/search-geo-datasets.js +13 -0
  183. package/dist/tools/bio/search-regulatory-approvals.js +1 -1
  184. package/dist/tools/define-tool.d.ts +25 -2
  185. package/dist/tools/define-tool.js +11 -1
  186. package/dist/tools/display/show-user.js +1 -1
  187. package/dist/tools/execute-analysis.d.ts +1 -3
  188. package/dist/tools/execute-analysis.js +5 -33
  189. package/dist/tools/report-authoring/authoring-tools.d.ts +2 -2
  190. package/dist/tools/report-authoring/authoring-tools.js +31 -6
  191. package/dist/tools/report-session/index.d.ts +0 -1
  192. package/dist/tools/report-session/index.js +0 -1
  193. package/dist/tools/report-session/script-tools.d.ts +33 -0
  194. package/dist/tools/report-session/script-tools.js +186 -0
  195. package/dist/tools/research/generate-analogy-report.d.ts +16 -9
  196. package/dist/tools/research/generate-analogy-report.js +111 -49
  197. package/dist/tools/research/generate-plan.d.ts +102 -7
  198. package/dist/tools/research/generate-plan.js +74 -43
  199. package/dist/tools/research/index.d.ts +4 -3
  200. package/dist/tools/research/index.js +4 -3
  201. package/dist/tools/research/inspect-run.d.ts +0 -1
  202. package/dist/tools/research/inspect-run.js +5 -10
  203. package/dist/tools/research/search-arxiv.js +15 -0
  204. package/dist/tools/research/search-github-repos.js +13 -0
  205. package/dist/tools/research/search-semantic-scholar.js +16 -0
  206. package/dist/tools/research/synthesis-lookups.d.ts +21 -0
  207. package/dist/tools/research/synthesis-lookups.js +29 -0
  208. package/dist/tools/research/web-fetch.d.ts +1 -1
  209. package/dist/tools/research/web-fetch.js +14 -12
  210. package/dist/tools/sandbox/lease-tools.d.ts +32 -0
  211. package/dist/tools/sandbox/lease-tools.js +60 -0
  212. package/dist/tools/script/archive.d.ts +34 -0
  213. package/dist/tools/script/archive.js +182 -0
  214. package/dist/tools/script/codemode-worker.d.ts +5 -0
  215. package/dist/tools/script/codemode-worker.js +5 -0
  216. package/dist/tools/script/file-tools.d.ts +106 -0
  217. package/dist/tools/script/file-tools.js +491 -0
  218. package/dist/tools/script/metrics.d.ts +30 -0
  219. package/dist/tools/script/metrics.js +53 -0
  220. package/dist/tools/script/quickjs.wasm +0 -0
  221. package/dist/tools/script/run-script.d.ts +76 -0
  222. package/dist/tools/script/run-script.js +413 -0
  223. package/dist/tools/script/scratch.d.ts +84 -0
  224. package/dist/tools/script/scratch.js +145 -0
  225. package/dist/tools/start-report-session.js +1 -1
  226. package/dist/tools/workspace/edit-file.js +2 -1
  227. package/dist/tools/workspace/execute-command.d.ts +9 -13
  228. package/dist/tools/workspace/execute-command.js +23 -19
  229. package/dist/tools/workspace/file-stat.js +1 -1
  230. package/dist/tools/workspace/grep.d.ts +5 -2
  231. package/dist/tools/workspace/grep.js +19 -10
  232. package/dist/tools/workspace/mutator.d.ts +51 -4
  233. package/dist/tools/workspace/mutator.js +54 -13
  234. package/dist/tools/workspace/read-file.d.ts +5 -2
  235. package/dist/tools/workspace/read-file.js +15 -11
  236. package/dist/tools/workspace/read-surface.d.ts +17 -0
  237. package/dist/tools/workspace/read-surface.js +17 -0
  238. package/dist/tools/workspace/result-bounds.d.ts +1 -0
  239. package/dist/tools/workspace/run-exec.d.ts +12 -13
  240. package/dist/tools/workspace/run-exec.js +13 -13
  241. package/dist/tools/workspace/scan-inputs.d.ts +2 -5
  242. package/dist/tools/workspace/scan-inputs.js +2 -3
  243. package/dist/tools/workspace/show-file.js +1 -1
  244. package/dist/tools/workspace/show-plan.js +4 -3
  245. package/dist/tools/workspace/write-file.js +2 -1
  246. package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
  247. package/dist/workflows/execute-analysis-scheduler.js +12 -32
  248. package/dist/workflows/execute-analysis.d.ts +17 -35
  249. package/dist/workflows/execute-analysis.js +13 -107
  250. package/dist/workflows/pause.js +29 -22
  251. package/dist/workflows/sandbox-step.d.ts +16 -31
  252. package/dist/workflows/sandbox-step.js +36 -71
  253. package/dist/workflows/test/sql-writes.d.ts +0 -1
  254. package/dist/workflows/test/sql-writes.js +0 -2
  255. package/dist/workspace/filesystem.d.ts +2 -2
  256. package/dist/workspace/filesystem.js +10 -5
  257. package/dist/workspace/paths.d.ts +19 -1
  258. package/dist/workspace/paths.js +22 -1
  259. package/dist/workspace/session-files.d.ts +35 -0
  260. package/dist/workspace/session-files.js +16 -0
  261. package/package.json +7 -3
  262. package/dist/prompts/literature-reviewer.d.ts +0 -1
  263. package/dist/prompts/literature-reviewer.js +0 -114
  264. package/dist/sandbox/reaper.d.ts +0 -81
  265. package/dist/sandbox/reaper.js +0 -161
  266. package/dist/sandbox/step-sandbox.d.ts +0 -47
  267. package/dist/sandbox/step-sandbox.js +0 -53
  268. package/dist/state/active-sandboxes.d.ts +0 -33
  269. package/dist/state/active-sandboxes.js +0 -65
  270. package/dist/tasks/extract-values-script.d.ts +0 -64
  271. package/dist/tasks/extract-values-script.js +0 -180
  272. package/dist/tasks/extract-values.d.ts +0 -77
  273. package/dist/tasks/extract-values.js +0 -146
  274. package/dist/tools/report-session/derive-table.d.ts +0 -164
  275. package/dist/tools/report-session/derive-table.js +0 -419
  276. package/dist/tools/research/literature-reviewer.d.ts +0 -42
  277. package/dist/tools/research/literature-reviewer.js +0 -107
  278. package/dist/workflows/metrics.d.ts +0 -23
  279. package/dist/workflows/metrics.js +0 -38
@@ -22,6 +22,8 @@
22
22
  * last accepted plan, else the last clarification, else the first blocker —
23
23
  * and persists an accepted plan under an id that a rerun of the call gives again.
24
24
  */
25
+ import { createReadScriptTools } from "../script/file-tools.js";
26
+ import { createReadSurfaceTools } from "../workspace/read-surface.js";
25
27
  import { errorMessageOf } from "../../lib/thrown.js";
26
28
  import { textOf } from "../../lib/unknown-text.js";
27
29
  import { createHash } from "node:crypto";
@@ -51,6 +53,7 @@ import { createSearchSemanticScholarTool } from "./search-semantic-scholar.js";
51
53
  import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../../app/data-profile-orientation.js";
52
54
  import { DEFAULT_SANDBOX_MAX_STEPS } from "../../config/resource-limits.js";
53
55
  import { plannerPrompt } from "../../prompts/planner.js";
56
+ import { planCardOf } from "../../memory/card-builders.js";
54
57
  import { hydratePlanSteps, PlannerPlanSchema } from "../../schemas/plan-schemas.js";
55
58
  import { validatePlan } from "../../schemas/validate-plan.js";
56
59
  import { AnalysisPlanSchema } from "../../schemas/workflow-state.js";
@@ -169,10 +172,9 @@ function traceOf(steps) {
169
172
  return { submitAttempts, rejectedAttempts, rejections };
170
173
  }
171
174
  // ── Prompt / catalog ────────────────────────────────────────────────
172
- /** The planner system prompt is deterministic per policy — build it once per
173
- * factory (the policy is a construction-time dep, fixed for the process). */
174
- function plannerInstructions(resourcePolicy) {
175
- return plannerPrompt(formatAgentCatalog(), resourcePolicy);
175
+ /** The planner system prompt is deterministic for the process: the catalog is fixed at load. */
176
+ export function plannerInstructions() {
177
+ return plannerPrompt(formatAgentCatalog());
176
178
  }
177
179
  const ValidationIssueSchema = z.object({ path: z.string(), code: z.enum(["schema", "semantic"]), message: z.string(), hint: z.string().optional() });
178
180
  const SubmitPlanOutputSchema = z.discriminatedUnion("accepted", [
@@ -226,7 +228,7 @@ function selectedOutcomeOf(steps) {
226
228
  * Format a loaded prior plan as a markdown block the planner can read —
227
229
  * narrative + one line per step. Used only when `parentPlanId` is set.
228
230
  */
229
- function formatPriorPlan(parentPlanId, plan) {
231
+ export function formatPriorPlan(parentPlanId, plan) {
230
232
  const parsed = AnalysisPlanSchema.safeParse(plan);
231
233
  if (!parsed.success)
232
234
  return null;
@@ -243,10 +245,9 @@ function formatPriorPlan(parentPlanId, plan) {
243
245
  "**Steps:**",
244
246
  ...stepLines,
245
247
  "",
246
- "The user is iterating on this plan. Preserve steps and IDs that are " +
247
- "not being changed; modify only what `userConstraints` describes. " +
248
- "Reuse step IDs when a step's purpose is unchanged so downstream " +
249
- "references survive.",
248
+ "The user is revising this plan, not asking for a new one. The changes are in `## User Constraints`. " +
249
+ "Each step keeps its id, its agent, and its order, also a step that the user asked to change: apply the change to that step. " +
250
+ "Thus `depends_on` and the stored artifacts survive. Give a new id to new work only.",
250
251
  ].join("\n");
251
252
  }
252
253
  /**
@@ -305,7 +306,7 @@ const NO_FACTS_GUIDANCE = "Plan from the research question alone, do not invent
305
306
  *
306
307
  * Pure — the same grounding renders the same string.
307
308
  */
308
- function renderDataContext(grounding, analysisId) {
309
+ export function renderDataContext(grounding, analysisId) {
309
310
  switch (grounding.kind) {
310
311
  case "absent":
311
312
  return "";
@@ -356,7 +357,7 @@ async function persistPlan(plan, ctx, pool, logger) {
356
357
  unwrapOrThrow(await upsertPlan(pool, {
357
358
  planId,
358
359
  analysisId: ctx.analysisId,
359
- plan: hydratePlanSteps(plan),
360
+ plan: { ...hydratePlanSteps(plan), request: ctx.request },
360
361
  parentPlanId: ctx.parentPlanId,
361
362
  }));
362
363
  return { ok: true, planId };
@@ -523,7 +524,7 @@ function restoreSkeletonGrounding(candidate, memory) {
523
524
  * Full validation: Zod schema + semantic checks. The plan is valid only if
524
525
  * BOTH pass.
525
526
  */
526
- function fullyValidate(candidate, resourcePolicy) {
527
+ function fullyValidate(candidate) {
527
528
  // The permissive arg schema of the planner tools accepts a string, thus the
528
529
  // loop-boundary repair never sees a plan that a model sent as a JSON-encoded
529
530
  // string. A small model does this on a large nested schema. Decode it here,
@@ -546,7 +547,7 @@ function fullyValidate(candidate, resourcePolicy) {
546
547
  created_at: parsed.data.created_at,
547
548
  omicsType: parsed.data.omicsType,
548
549
  omicsSubtype: parsed.data.omicsSubtype,
549
- }, { perStepCeiling: resourcePolicy?.perStep });
550
+ });
550
551
  if (!semantic.valid) {
551
552
  return {
552
553
  valid: false,
@@ -559,11 +560,30 @@ function fullyValidate(candidate, resourcePolicy) {
559
560
  }
560
561
  return { valid: true, plan: parsed.data };
561
562
  }
563
+ /** The planner agent over its search tools and its terminal tools. */
564
+ export function plannerAgentOf(deps, searchTools, terminal) {
565
+ const readTools = deps.toolOutputStore ? [createReadToolOutputTool(deps.toolOutputStore)] : [];
566
+ return {
567
+ id: PLANNER_AGENT_ID,
568
+ systemPrompt: composeSystemPrompt(plannerInstructions()),
569
+ model: deps.conversation.model,
570
+ // The search tools come first, and the terminal tools come last. The order is the order of the prompt, and it
571
+ // is stable across every invocation, thus the cached request prefix holds.
572
+ tools: [...searchTools, ...createReadSurfaceTools(deps.workspace), ...readTools, ...terminal],
573
+ scriptTools: createReadScriptTools(deps.workspace),
574
+ scriptRuntime: deps.scriptRuntime,
575
+ maxIterations: PLANNER_MAX_ITERATIONS,
576
+ };
577
+ }
578
+ /** The terminal tools of a planner with no knowledge answer, for a consumer that runs the planner outside this tool. */
579
+ export function plannerTerminalTools(logger) {
580
+ return buildInnerTools(logger, { steps: new Map(), claims: new Set() }).terminal;
581
+ }
562
582
  /**
563
583
  * Build the planner's inner tools for one invocation. `memory` holds the last
564
584
  * `knowledge_recommend` answer of the run, which the loop folds after each round.
565
585
  */
566
- function buildInnerTools(resourcePolicy, logger, memory) {
586
+ function buildInnerTools(logger, memory) {
567
587
  const submitPlanTool = defineTool({
568
588
  id: SUBMIT_PLAN_TOOL_ID,
569
589
  description: "Submit the plan. Checks the plan; on success returns {accepted: true, plan} " +
@@ -592,7 +612,7 @@ function buildInnerTools(resourcePolicy, logger, memory) {
592
612
  logger.info("submit_plan restored the template of skeleton steps", { steps: restored });
593
613
  if (repaired.length > 0)
594
614
  logger.info("submit_plan repaired the claim ids of plan steps", { claims: repaired });
595
- const result = fullyValidate(candidate, resourcePolicy);
615
+ const result = fullyValidate(candidate);
596
616
  // An unknown claim rejects a plan that the schema accepts, beside the
597
617
  // schema issues when the schema rejects it, thus one attempt shows both.
598
618
  if (!result.valid || unknown.length > 0) {
@@ -644,7 +664,7 @@ function buildInnerTools(resourcePolicy, logger, memory) {
644
664
  * no outcome distinguishes. The record is what makes "the planner produced a plan
645
665
  * naming packages this install does not have" traceable to its cause.
646
666
  */
647
- function inventoryContent(label, result, logger) {
667
+ export function inventoryContent(label, result, logger) {
648
668
  if (result.isErr()) {
649
669
  logger.warn("planner grounding inventory lookup failed", { inventory: label, error: result.error.error });
650
670
  return `## ${label}\n\nInventory lookup failed: ${result.error.error}`;
@@ -855,8 +875,28 @@ export function buildPlannerSearchTools(deps, hooks = {}) {
855
875
  }),
856
876
  ];
857
877
  }
878
+ /**
879
+ * The seed of the planner: the prior plan when the call revises one, the data context, the two censuses, and then
880
+ * what the caller gave. Each block is optional where the call holds none.
881
+ */
882
+ export function composePlannerSeed(parts) {
883
+ const { priorPlanBlock, dataContextBlock, groundingBlock, input } = parts;
884
+ return [
885
+ ...(priorPlanBlock ? [priorPlanBlock, ""] : []),
886
+ ...(dataContextBlock ? [dataContextBlock, ""] : []),
887
+ groundingBlock,
888
+ "",
889
+ "## Research Question",
890
+ input.researchQuestion,
891
+ ...(input.analystNotes ? ["", "## Analyst Notes (from the user — facts about the data the profile does not record)", input.analystNotes] : []),
892
+ ...(input.priorRuns ? ["", "## Prior Run Results", input.priorRuns] : []),
893
+ ...(input.userConstraints ? ["", "## User Constraints", input.userConstraints] : []),
894
+ ].join("\n");
895
+ }
858
896
  const generatePlanInputSchema = z.object({
859
- researchQuestion: z.string().describe("What the user wants to analyze — their goal and specific questions."),
897
+ researchQuestion: z
898
+ .string()
899
+ .describe("The request of the user, in the words of the user: the goal, the questions, and each deliverable that the user named, with its format. The plan stores it, and each step of the run reads it."),
860
900
  analystNotes: z
861
901
  .string()
862
902
  .optional()
@@ -983,17 +1023,7 @@ export function createGeneratePlanTool(deps, registerSubAgent) {
983
1023
  const refsBlock = inventoryContent("Available Reference Data", refsResult, logger);
984
1024
  const packagesBlock = inventoryContent("Available Packages", packagesResult, logger);
985
1025
  const groundingBlock = [refsBlock, packagesBlock].join("\n\n");
986
- const prompt = [
987
- ...(priorPlanBlock ? [priorPlanBlock, ""] : []),
988
- ...(dataContextBlock ? [dataContextBlock, ""] : []),
989
- groundingBlock,
990
- "",
991
- "## Research Question",
992
- input.researchQuestion,
993
- ...(input.analystNotes ? ["", "## Analyst Notes (from the user — facts about the data the profile does not record)", input.analystNotes] : []),
994
- ...(input.priorRuns ? ["", "## Prior Run Results", input.priorRuns] : []),
995
- ...(input.userConstraints ? ["", "## User Constraints", input.userConstraints] : []),
996
- ].join("\n");
1026
+ const prompt = composePlannerSeed({ priorPlanBlock, dataContextBlock, groundingBlock, input });
997
1027
  return {
998
1028
  prompt,
999
1029
  grounding: grounding.kind,
@@ -1037,13 +1067,14 @@ export function createGeneratePlanTool(deps, registerSubAgent) {
1037
1067
  analysisId,
1038
1068
  parentPlanId: input.parentPlanId ?? null,
1039
1069
  invocationId: ctx.invocationId,
1070
+ request: input.researchQuestion,
1040
1071
  };
1041
1072
  // The last recommend answer of this invocation, thus `submit_plan`
1042
1073
  // stamps the snapshot digest, restores a template the model dropped,
1043
1074
  // and repairs or refuses a claim id the answer did not return. The loop
1044
1075
  // folds each answer after its round, on the first run and on a replay.
1045
1076
  const memory = { steps: new Map(), claims: new Set() };
1046
- const innerTools = buildInnerTools(deps.resourcePolicy, logger, memory);
1077
+ const innerTools = buildInnerTools(logger, memory);
1047
1078
  // Built here rather than at construction: a `describeCall` hook reads no
1048
1079
  // dep, thus the tool must stay constructible from an empty bag. The tool
1049
1080
  // definitions are identical across invocations, thus the request prefix
@@ -1051,17 +1082,7 @@ export function createGeneratePlanTool(deps, registerSubAgent) {
1051
1082
  const searchTools = buildPlannerSearchTools(deps, {
1052
1083
  onRecommend: (answer) => rememberAnswer(memory, answer),
1053
1084
  });
1054
- const readTools = deps.toolOutputStore ? [createReadToolOutputTool(deps.toolOutputStore)] : [];
1055
- const planner = {
1056
- id: PLANNER_AGENT_ID,
1057
- systemPrompt: composeSystemPrompt(plannerInstructions(deps.resourcePolicy)),
1058
- model: deps.conversation.model,
1059
- // The search tools come first, and the terminal tools come last.
1060
- // The order is the order of the prompt, and it is stable across
1061
- // every invocation, thus the cached request prefix holds.
1062
- tools: [...searchTools, ...readTools, ...innerTools.terminal],
1063
- maxIterations: PLANNER_MAX_ITERATIONS,
1064
- };
1085
+ const planner = plannerAgentOf(deps, searchTools, innerTools.terminal);
1065
1086
  // Merge the outer abort signal with a wall-clock timeout — either
1066
1087
  // cancels the planner promptly.
1067
1088
  const signal = AbortSignal.any([ctx.signal, AbortSignal.timeout(planTimeoutMs)]);
@@ -1155,13 +1176,23 @@ export function createGeneratePlanTool(deps, registerSubAgent) {
1155
1176
  "record more faithfully than the record itself. Pass only what the profile cannot hold: the research " +
1156
1177
  "question, facts the user told you (analystNotes), prior run results, and their constraints. " +
1157
1178
  "The planner is a multi-step sub-agent, thus a call can take many minutes. " +
1158
- "The result carries an `event`: `plan_complete` with {planId, plan} — the plan is saved and ready for show_plan and " +
1159
- "execute_analysis plan mode, and this call neither shows nor runs it; `clarification_needed` with {question, questionContext?} " +
1179
+ "The result carries an `event`: `plan_complete` with {planId, plan} — the plan is saved, the call shows its plan card to the user, " +
1180
+ "and it does not run it (execute_analysis plan mode does, after the approval); `clarification_needed` with {question, questionContext?} " +
1160
1181
  "when the planner is missing a fact it cannot infer; or `error` with {error} when no plan was saved (the planner reported " +
1161
1182
  "a blocker, the save failed, the call timed out or was cancelled, or parentPlanId is not a plan of this analysis).",
1162
1183
  inputSchema: generatePlanInputSchema,
1163
1184
  describeCall: ({ researchQuestion }) => researchQuestion,
1164
1185
  startsWorkflow: true,
1165
- execute: async (input, ctx) => ok(await plan(input, ctx)),
1186
+ execute: async (input, ctx) => {
1187
+ const output = await plan(input, ctx);
1188
+ // The card comes from the result of the planner, with no read of the store, thus a replay of the body
1189
+ // gives the same card under the same id, and a reader keeps one card.
1190
+ if (output.event === "plan_complete" && output.planId !== undefined && output.plan !== undefined) {
1191
+ const card = planCardOf({ planId: output.planId, plan: hydratePlanSteps(output.plan), parentPlanId: input.parentPlanId ?? null });
1192
+ if (card !== null)
1193
+ await ctx.emit({ type: "data-plan", source: ctx.session.provenance, data: card });
1194
+ }
1195
+ return ok(output);
1196
+ },
1166
1197
  });
1167
1198
  }
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Research barrel — dependency-bearing planning, run inspection, the
3
- * literature-reviewer and analogical-reasoner sub-agents, developer-docs
4
- * lookup, and the cross-domain search tools the analogical-reasoner uses.
3
+ * analogical-reasoner sub-agent, the lookups of the run synthesizer,
4
+ * developer-docs lookup, and the cross-domain search tools the
5
+ * analogical-reasoner uses.
5
6
  * Dep-bearing tools are factory closures (`createXTool(deps)`); pure tools
6
7
  * are module-scope `defineTool` values.
7
8
  */
8
9
  export * from "./generate-plan.js";
9
10
  export * from "./inspect-run.js";
10
11
  export * from "./inspect-data-profile.js";
11
- export * from "./literature-reviewer.js";
12
+ export * from "./synthesis-lookups.js";
12
13
  export * from "./generate-analogy-report.js";
13
14
  export * from "./context7-docs.js";
14
15
  export * from "./search-semantic-scholar.js";
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Research barrel — dependency-bearing planning, run inspection, the
3
- * literature-reviewer and analogical-reasoner sub-agents, developer-docs
4
- * lookup, and the cross-domain search tools the analogical-reasoner uses.
3
+ * analogical-reasoner sub-agent, the lookups of the run synthesizer,
4
+ * developer-docs lookup, and the cross-domain search tools the
5
+ * analogical-reasoner uses.
5
6
  * Dep-bearing tools are factory closures (`createXTool(deps)`); pure tools
6
7
  * are module-scope `defineTool` values.
7
8
  */
8
9
  export * from "./generate-plan.js";
9
10
  export * from "./inspect-run.js";
10
11
  export * from "./inspect-data-profile.js";
11
- export * from "./literature-reviewer.js";
12
+ export * from "./synthesis-lookups.js";
12
13
  export * from "./generate-analogy-report.js";
13
14
  export * from "./context7-docs.js";
14
15
  export * from "./search-semantic-scholar.js";
@@ -69,7 +69,6 @@ type InspectRunOutput = {
69
69
  steps: FormattedStep[];
70
70
  elapsedMs?: number;
71
71
  wait?: WaitMetadata;
72
- selfWaitPrevented?: boolean;
73
72
  };
74
73
  type InspectRunClock = {
75
74
  now(): number;
@@ -106,8 +106,7 @@ export function createInspectRunTool(pool, clock = systemClock) {
106
106
  "With a runId, returns the run, its steps, and an inspectionState: not_found; in_progress; paused (a step waits until the user resumes or stops the run, and pauses gives each cause); " +
107
107
  "or terminal (run.status completed, partial, failed, or canceled). " +
108
108
  "Output paths appear only on a terminal run: run.synthesisPath when synthesisStatus is produced, and the summaryPath of each step that ran. " +
109
- "For a user-directed check, waitForTerminalSeconds can wait once for 1-30 seconds; if the cutoff returns in_progress, stop polling for that turn. " +
110
- "A wait on the run that the caller itself runs inside returns at once with selfWaitPrevented. " +
109
+ "For a user-directed check, waitForTerminalSeconds can wait once for 1-30 seconds; if the cutoff returns in_progress, stop polling for that turn and tell the user that the run still runs. " +
111
110
  "Default response is lightweight — pass includeDiagnostics:true to add error/duration/retry telemetry.",
112
111
  inputSchema,
113
112
  // The two calls do different work: one inspects a named run, the other
@@ -139,12 +138,11 @@ export function createInspectRunTool(pool, clock = systemClock) {
139
138
  wait: waitSeconds === undefined ? undefined : { requestedSeconds: waitSeconds, cutoffReached: false },
140
139
  });
141
140
  }
142
- const selfWait = waitSeconds !== undefined && ctx.session.runFrame?.runId === input.runId;
143
141
  const runId = input.runId;
144
142
  const pausesOf = async (status) => status === "running" ? unwrapOrThrow(await queryPausesOfWork(pool, resourceId, runId)) : [];
145
143
  let pauses = await pausesOf(run.status);
146
144
  let cutoffReached = false;
147
- if (waitSeconds !== undefined && run.status === "running" && pauses.length === 0 && !selfWait) {
145
+ if (waitSeconds !== undefined && run.status === "running" && pauses.length === 0) {
148
146
  const deadline = clock.now() + waitSeconds * 1_000;
149
147
  while (run.status === "running" && pauses.length === 0) {
150
148
  const remaining = deadline - clock.now();
@@ -180,11 +178,9 @@ export function createInspectRunTool(pool, clock = systemClock) {
180
178
  });
181
179
  }
182
180
  if (run.status === "running") {
183
- const message = selfWait
184
- ? "This run is still in progress. Results are not ready, and the enclosing workflow cannot finish while its current step waits for itself."
185
- : cutoffReached
186
- ? "This run is still in progress after the requested wait cutoff. Results are not ready; stop polling for this turn."
187
- : "This run is in progress. Results are not ready.";
181
+ const message = cutoffReached
182
+ ? "This run is still in progress after the requested wait cutoff. Results are not ready; stop polling for this turn."
183
+ : "This run is in progress. Results are not ready.";
188
184
  return ok({
189
185
  inspectionState: "in_progress",
190
186
  message,
@@ -192,7 +188,6 @@ export function createInspectRunTool(pool, clock = systemClock) {
192
188
  steps,
193
189
  elapsedMs: Math.max(0, clock.now() - Date.parse(run.startedAt)),
194
190
  ...wait,
195
- selfWaitPrevented: selfWait ? true : undefined,
196
191
  });
197
192
  }
198
193
  return ok({
@@ -4,6 +4,20 @@ import { z } from "zod";
4
4
  import { createArxivSource } from "../../literature/sources/arxiv.js";
5
5
  import { defineTool } from "../define-tool.js";
6
6
  export { parseArxivAtom } from "../../literature/sources/arxiv.js";
7
+ const SearchArxivOutputSchema = z.object({
8
+ success: z.boolean(),
9
+ error: z.string().optional().describe("Present when success is false: why the search failed."),
10
+ papers: z.array(z.object({
11
+ id: z.string(),
12
+ title: z.string(),
13
+ abstract: z.string(),
14
+ authors: z.array(z.string()),
15
+ published: z.string(),
16
+ categories: z.array(z.string()),
17
+ url: z.string(),
18
+ pdfUrl: z.string().optional(),
19
+ })),
20
+ });
7
21
  const source = createArxivSource({ maxRetries: 3, retryDelayMs: 1_000, timeoutMs: 90_000 });
8
22
  export const searchArxivTool = defineTool({
9
23
  id: "search_arxiv",
@@ -15,6 +29,7 @@ export const searchArxivTool = defineTool({
15
29
  "('2301.00001') also matches as free text.\n" +
16
30
  "ABSENCE IS NORMAL: an empty papers array means arXiv holds nothing for the query, and `success: false` with an `error` string means the service " +
17
31
  "could not be reached. Report either one and continue, do not retry unchanged.",
32
+ outputSchema: SearchArxivOutputSchema,
18
33
  inputSchema: z.object({
19
34
  query: z.string().describe('Free-text query. Example: "adaptive control feedback stabilization".'),
20
35
  categories: z
@@ -16,6 +16,18 @@ import { apiFetchValidated, describeApiError } from "../lib/api-utils.js";
16
16
  import { parseUnknownRecord } from "../../lib/unknown-containers.js";
17
17
  import { isUnknownArray } from "../../lib/unknown-array.js";
18
18
  const BASE_URL = "https://api.github.com/search/repositories";
19
+ const SearchGithubReposOutputSchema = z.object({
20
+ success: z.boolean(),
21
+ error: z.string().optional().describe("Present when success is false: why the search failed."),
22
+ repos: z.array(z.object({
23
+ url: z.string(),
24
+ fullName: z.string(),
25
+ description: z.string().optional(),
26
+ stars: z.number(),
27
+ language: z.string().optional(),
28
+ lastUpdated: z.string().optional(),
29
+ })),
30
+ });
19
31
  // Raw GitHub search wire shape, validated at the fetch boundary. Every field is
20
32
  // optional — `parseGithubReposResponse` already filters out items missing the
21
33
  // fields it needs, so an over-strict schema would drop otherwise-usable pages.
@@ -73,6 +85,7 @@ export function createSearchGithubReposTool(deps) {
73
85
  "and `success: false` with an `error` string means GitHub could not be " +
74
86
  "reached or refused the request (the low unauthenticated rate limit " +
75
87
  "lands here). Report either one and continue, do not retry unchanged.",
88
+ outputSchema: SearchGithubReposOutputSchema,
76
89
  inputSchema: SearchGithubReposInputSchema,
77
90
  describeCall: "none",
78
91
  execute: async ({ query, language, limit }) => {
@@ -4,6 +4,21 @@ import { z } from "zod";
4
4
  import { createSemanticScholarSource } from "../../literature/sources/semantic-scholar.js";
5
5
  import { defineTool } from "../define-tool.js";
6
6
  export { parseSemanticScholarResponse } from "../../literature/sources/semantic-scholar.js";
7
+ const SearchSemanticScholarOutputSchema = z.object({
8
+ success: z.boolean(),
9
+ error: z.string().optional().describe("Present when success is false: why the search failed."),
10
+ papers: z.array(z.object({
11
+ id: z.string(),
12
+ title: z.string(),
13
+ abstract: z.string().optional(),
14
+ year: z.number().optional(),
15
+ venue: z.string().optional(),
16
+ citationCount: z.number().optional(),
17
+ url: z.string().optional(),
18
+ authors: z.array(z.string()),
19
+ externalIds: z.record(z.string(), z.string()).optional().describe("For example DOI, ArXiv, PubMed, CorpusId."),
20
+ })),
21
+ });
7
22
  export function createSearchSemanticScholarTool(options = {}) {
8
23
  const source = createSemanticScholarSource({
9
24
  maxRetries: 3,
@@ -23,6 +38,7 @@ export function createSearchSemanticScholarTool(options = {}) {
23
38
  "back for a citation.\n" +
24
39
  "ABSENCE IS NORMAL: an empty papers array means the corpus holds nothing for the query, and `success: false` with an `error` string means the " +
25
40
  "service could not be reached (its unkeyed rate limit lands here). Report either one and continue, do not retry unchanged.",
41
+ outputSchema: SearchSemanticScholarOutputSchema,
26
42
  inputSchema: z.object({
27
43
  query: z
28
44
  .string()
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The lookups of the run synthesizer: the gene, protein, annotation,
3
+ * interaction, PubMed, citation, drug-gene, and preclinical-profile tools.
4
+ *
5
+ * The synthesizer calls them directly, thus each lookup result, and each PMID
6
+ * that the synthesis cites, is in the transcript of the synthesizer, where
7
+ * `submit_synthesis` can check it.
8
+ */
9
+ import type { CitationResolver } from "../../citations/types.js";
10
+ import type { Logger } from "../../lib/logger.js";
11
+ import { type BioToolKeys } from "../bio/keys.js";
12
+ import type { Tool } from "../define-tool.js";
13
+ export type SynthesisLookupDeps = {
14
+ /** API keys for the bio/chem data sources of the lookups. */
15
+ readonly bioKeys: BioToolKeys;
16
+ /** Shared bibliographic verification service; all agent surfaces share its pacing/cache. */
17
+ readonly citationResolver: CitationResolver;
18
+ /** Operational logging; omitted falls back to no-op. */
19
+ readonly logger?: Logger;
20
+ };
21
+ export declare function createSynthesisLookupTools(deps: SynthesisLookupDeps): readonly Tool[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The lookups of the run synthesizer: the gene, protein, annotation,
3
+ * interaction, PubMed, citation, drug-gene, and preclinical-profile tools.
4
+ *
5
+ * The synthesizer calls them directly, thus each lookup result, and each PMID
6
+ * that the synthesis cites, is in the transcript of the synthesizer, where
7
+ * `submit_synthesis` can check it.
8
+ */
9
+ import { genePreclinicalProfileTool } from "../bio/gene-preclinical-profile.js";
10
+ import { createChemDbTools, createNcbiTools } from "../bio/keys.js";
11
+ import { lookupAnnotationTool } from "../bio/lookup-annotation.js";
12
+ import { searchGeneTool } from "../bio/search-gene.js";
13
+ import { searchInteractionsTool } from "../bio/search-interactions.js";
14
+ import { searchProteinTool } from "../bio/search-protein.js";
15
+ import { createResolveCitationTool } from "./resolve-citation.js";
16
+ export function createSynthesisLookupTools(deps) {
17
+ const ncbi = createNcbiTools(deps.bioKeys);
18
+ const chemDb = createChemDbTools(deps.bioKeys, { logger: deps.logger });
19
+ return [
20
+ searchGeneTool,
21
+ searchProteinTool,
22
+ lookupAnnotationTool,
23
+ searchInteractionsTool,
24
+ ncbi.pubmed,
25
+ createResolveCitationTool(deps.citationResolver),
26
+ chemDb.drugGeneInteractions,
27
+ genePreclinicalProfileTool,
28
+ ];
29
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * One URL in, extracted text out: HTML becomes Markdown (Readability over a
5
5
  * `linkedom` DOM, then Turndown, with the body text as the fallback), a PDF
6
- * becomes the text of its first 100 pages through `pdf-parse`, and a text or
6
+ * becomes the text of its first 100 pages through `unpdf`, and a text or
7
7
  * JSON body passes through. A bad scheme, an internal address, an HTTP error, an
8
8
  * oversized body, a redirect chain past the limit, and a body the tool cannot
9
9
  * extract are all normal results on the ok channel — never a failure of the
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * One URL in, extracted text out: HTML becomes Markdown (Readability over a
5
5
  * `linkedom` DOM, then Turndown, with the body text as the fallback), a PDF
6
- * becomes the text of its first 100 pages through `pdf-parse`, and a text or
6
+ * becomes the text of its first 100 pages through `unpdf`, and a text or
7
7
  * JSON body passes through. A bad scheme, an internal address, an HTTP error, an
8
8
  * oversized body, a redirect chain past the limit, and a body the tool cannot
9
9
  * extract are all normal results on the ok channel — never a failure of the
@@ -420,27 +420,29 @@ function htmlExtractionOf(body) {
420
420
  }
421
421
  }
422
422
  }
423
- let pdfParseModule;
423
+ let pdfTextModule;
424
424
  async function extractBody(contentType, body) {
425
425
  if (contentType === "text/html" || contentType === "application/xhtml+xml") {
426
426
  return htmlExtractionOf(body);
427
427
  }
428
428
  if (contentType === "application/pdf") {
429
- // `pdf-parse` makes `os.availableParallelism()` return `undefined` process-wide under Bun when its module loads,
430
- // so the public barrel must never import it statically (inflexa-ai/inflexa#701). The first PDF loads it once.
431
- pdfParseModule ??= await import("pdf-parse");
432
- const parser = new pdfParseModule.PDFParse({ data: body });
429
+ // PDF.js is a large module, thus the first PDF loads it, and a host that fetches no PDF never pays for it.
430
+ pdfTextModule ??= await import("../../lib/pdf-text.js");
433
431
  try {
434
- const result = await parser.getText({ first: PDF_PAGE_CAP });
435
- const pdfCut = result.total > result.pages.length ? { totalPages: result.total, pagesRead: result.pages.length } : undefined;
436
- return { kind: "text", text: result.text.trim(), pdfCut };
432
+ const result = await pdfTextModule.readPdfText(body, { first: 1, last: PDF_PAGE_CAP });
433
+ const pdfCut = result.totalPages > result.pages.length ? { totalPages: result.totalPages, pagesRead: result.pages.length } : undefined;
434
+ return {
435
+ kind: "text",
436
+ text: result.pages
437
+ .map((page) => page.text)
438
+ .join("\n\n")
439
+ .trim(),
440
+ pdfCut,
441
+ };
437
442
  }
438
443
  catch (error) {
439
444
  return { kind: "unextractable", detail: `the PDF parser failed: ${errorMessageOf(error) ?? textOf(error)}` };
440
445
  }
441
- finally {
442
- await parser.destroy();
443
- }
444
446
  }
445
447
  if (contentType.startsWith("text/") || contentType === "application/json" || contentType.endsWith("+json")) {
446
448
  return { kind: "text", text: body.toString("utf8") };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `acquire_sandbox` and `release_sandbox` (sandbox-leases spec): the agent holds its sandbox through a lease.
3
+ *
4
+ * Each tool gives the size, and nothing else. The bound lease carries the key (the id of the workflow of the agent),
5
+ * the session, and the spawn settings of the step, and the sandbox module owns the admission, the spawn, and the
6
+ * teardown. `acquire_sandbox` blocks until the machine is ready, also while the lease waits in the queue.
7
+ */
8
+ import { z } from "zod";
9
+ import type { BoundLease } from "../../sandbox/leases.js";
10
+ import { type Tool } from "../define-tool.js";
11
+ export declare const ACQUIRE_SANDBOX_TOOL_ID = "acquire_sandbox";
12
+ export declare const RELEASE_SANDBOX_TOOL_ID = "release_sandbox";
13
+ declare const AcquireSandboxInputSchema: z.ZodObject<{
14
+ cpu: z.ZodNumber;
15
+ memoryGb: z.ZodNumber;
16
+ gpuCount: z.ZodOptional<z.ZodNumber>;
17
+ }, z.core.$strip>;
18
+ type AcquireSandboxResult = {
19
+ readonly status: "granted";
20
+ readonly cpu: number;
21
+ readonly memoryGb: number;
22
+ readonly gpuCount: number;
23
+ readonly message: string;
24
+ };
25
+ declare const ReleaseSandboxInputSchema: z.ZodObject<{}, z.core.$strip>;
26
+ type ReleaseSandboxResult = {
27
+ readonly status: "released";
28
+ readonly message: string;
29
+ };
30
+ export declare function createAcquireSandboxTool(lease: BoundLease): Tool<z.infer<typeof AcquireSandboxInputSchema>, AcquireSandboxResult>;
31
+ export declare function createReleaseSandboxTool(lease: BoundLease): Tool<z.infer<typeof ReleaseSandboxInputSchema>, ReleaseSandboxResult>;
32
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `acquire_sandbox` and `release_sandbox` (sandbox-leases spec): the agent holds its sandbox through a lease.
3
+ *
4
+ * Each tool gives the size, and nothing else. The bound lease carries the key (the id of the workflow of the agent),
5
+ * the session, and the spawn settings of the step, and the sandbox module owns the admission, the spawn, and the
6
+ * teardown. `acquire_sandbox` blocks until the machine is ready, also while the lease waits in the queue.
7
+ */
8
+ import { err, ok } from "neverthrow";
9
+ import { z } from "zod";
10
+ import { defineTool } from "../define-tool.js";
11
+ export const ACQUIRE_SANDBOX_TOOL_ID = "acquire_sandbox";
12
+ export const RELEASE_SANDBOX_TOOL_ID = "release_sandbox";
13
+ const AcquireSandboxInputSchema = z.object({
14
+ cpu: z.number().positive().describe("CPU cores of the sandbox. A fraction is permitted, and the grant can be smaller than the request."),
15
+ memoryGb: z.number().positive().describe("Memory of the sandbox in GB. A command that uses more memory gets an out-of-memory kill."),
16
+ gpuCount: z.number().int().nonnegative().optional().describe("GPUs of the sandbox. Omit for none."),
17
+ });
18
+ const ReleaseSandboxInputSchema = z.object({});
19
+ export function createAcquireSandboxTool(lease) {
20
+ return defineTool({
21
+ id: ACQUIRE_SANDBOX_TOOL_ID,
22
+ description: "Get a sandbox of the given size for `execute_command`, and wait until it is ready. The call can wait for free " +
23
+ "capacity. The result gives the granted size, which can be smaller than the request. A call with a different " +
24
+ "size replaces your sandbox: the files of your working directory stay, and the next command runs on the new " +
25
+ "size. Your files and your outputs live in the workspace, not in the sandbox.",
26
+ inputSchema: AcquireSandboxInputSchema,
27
+ describeCall: ({ cpu, memoryGb }) => `Waiting for a sandbox (${cpu} CPU, ${memoryGb} GB)`,
28
+ execute: async ({ cpu, memoryGb, gpuCount }) => {
29
+ const size = gpuCount === undefined || gpuCount === 0 ? { cpu, memoryGb } : { cpu, memoryGb, gpu: { count: gpuCount } };
30
+ const outcome = await lease.acquire(size);
31
+ switch (outcome.kind) {
32
+ case "granted":
33
+ return ok({
34
+ status: "granted",
35
+ cpu: outcome.size.cpu,
36
+ memoryGb: outcome.size.memoryGb,
37
+ gpuCount: outcome.size.gpu?.count ?? 0,
38
+ message: "The sandbox is ready.",
39
+ });
40
+ case "refused":
41
+ return err({ error: `The host refused the sandbox: ${outcome.reason}.`, retryable: false });
42
+ case "failed":
43
+ return err({ error: `The sandbox did not start: ${outcome.detail}`, retryable: true });
44
+ }
45
+ },
46
+ });
47
+ }
48
+ export function createReleaseSandboxTool(lease) {
49
+ return defineTool({
50
+ id: RELEASE_SANDBOX_TOOL_ID,
51
+ description: "Release your sandbox when your commands are done and you work only with files or research. The files stay. " +
52
+ "A later `execute_command` needs `acquire_sandbox` again.",
53
+ inputSchema: ReleaseSandboxInputSchema,
54
+ describeCall: () => "Releasing the sandbox",
55
+ execute: async () => {
56
+ await lease.release();
57
+ return ok({ status: "released", message: "The sandbox is released." });
58
+ },
59
+ });
60
+ }