@inflexa-ai/harness 0.46.2 → 0.47.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 (271) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/conversation-agent.d.ts +3 -8
  3. package/dist/agents/conversation-agent.js +24 -4
  4. package/dist/agents/report-session-agent.d.ts +11 -14
  5. package/dist/agents/report-session-agent.js +30 -17
  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 +2 -5
  25. package/dist/index.js +3 -6
  26. package/dist/input-scan/enrich.d.ts +5 -10
  27. package/dist/input-scan/enrich.js +12 -8
  28. package/dist/lib/markdown-pdf.d.ts +27 -0
  29. package/dist/lib/markdown-pdf.js +236 -0
  30. package/dist/lib/metrics.d.ts +31 -0
  31. package/dist/lib/metrics.js +64 -0
  32. package/dist/lib/pdf-text.d.ts +23 -0
  33. package/dist/lib/pdf-text.js +34 -0
  34. package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
  35. package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
  36. package/dist/loop/run-loop.d.ts +5 -0
  37. package/dist/loop/run-loop.js +30 -2
  38. package/dist/loop/tool-name-repair.d.ts +11 -0
  39. package/dist/loop/tool-name-repair.js +23 -0
  40. package/dist/loop/types.d.ts +14 -0
  41. package/dist/memory/card-builders.d.ts +1 -1
  42. package/dist/prompts/SOUL.d.ts +6 -6
  43. package/dist/prompts/SOUL.js +22 -121
  44. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  45. package/dist/prompts/analogical-reasoner.js +35 -101
  46. package/dist/prompts/briefing.d.ts +20 -11
  47. package/dist/prompts/briefing.js +36 -19
  48. package/dist/prompts/conversation.d.ts +1 -1
  49. package/dist/prompts/conversation.js +103 -383
  50. package/dist/prompts/planner.d.ts +1 -5
  51. package/dist/prompts/planner.js +45 -128
  52. package/dist/prompts/report-session.d.ts +1 -1
  53. package/dist/prompts/report-session.js +26 -57
  54. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  55. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
  56. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
  58. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  59. package/dist/prompts/sandbox/chromatin-agent.js +4 -5
  60. package/dist/prompts/sandbox/data-profiler.js +11 -34
  61. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  62. package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
  63. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  64. package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
  65. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  66. package/dist/prompts/sandbox/enrichment-agent.js +3 -1
  67. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
  69. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
  71. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  72. package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
  73. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  74. package/dist/prompts/sandbox/microbiome-agent.js +3 -1
  75. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  76. package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
  77. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  78. package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
  79. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  80. package/dist/prompts/sandbox/network-agent.js +3 -1
  81. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  82. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
  83. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  84. package/dist/prompts/sandbox/proteomics-agent.js +3 -1
  85. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  86. package/dist/prompts/sandbox/scientific-executor.js +0 -12
  87. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  88. package/dist/prompts/sandbox/single-cell-agent.js +3 -1
  89. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
  91. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
  93. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
  95. package/dist/prompts/sandbox-standards.d.ts +15 -18
  96. package/dist/prompts/sandbox-standards.js +96 -305
  97. package/dist/prompts/synthesis-agent.d.ts +1 -1
  98. package/dist/prompts/synthesis-agent.js +24 -90
  99. package/dist/provenance/collector.d.ts +2 -5
  100. package/dist/provenance/collector.js +2 -5
  101. package/dist/provenance/seam.d.ts +36 -1
  102. package/dist/provenance/workspace-reads.d.ts +36 -0
  103. package/dist/provenance/workspace-reads.js +33 -0
  104. package/dist/report-model/production-resolver.d.ts +22 -52
  105. package/dist/report-model/production-resolver.js +43 -112
  106. package/dist/report-render/chart-export.d.ts +9 -0
  107. package/dist/report-render/chart-export.js +1 -1
  108. package/dist/report-render/views/chart-view.d.ts +1 -0
  109. package/dist/report-render/views/chart-view.js +1 -0
  110. package/dist/report-render/views/lineage.d.ts +1 -0
  111. package/dist/report-render/views/page-view.d.ts +1 -0
  112. package/dist/report-render/views/page-view.js +1 -0
  113. package/dist/report-render/views/prose.d.ts +1 -0
  114. package/dist/report-render/views/prose.js +1 -0
  115. package/dist/report-render/views/references-view.d.ts +1 -0
  116. package/dist/report-render/views/values.d.ts +1 -0
  117. package/dist/report-render/views/values.js +1 -0
  118. package/dist/runtime/assemble.d.ts +20 -9
  119. package/dist/runtime/assemble.js +47 -38
  120. package/dist/sandbox/client.d.ts +5 -3
  121. package/dist/sandbox/create-sandbox.d.ts +7 -12
  122. package/dist/sandbox/create-sandbox.js +15 -37
  123. package/dist/sandbox/docker-client.d.ts +1 -3
  124. package/dist/sandbox/docker-client.js +3 -5
  125. package/dist/sandbox/exec-outcome.d.ts +11 -11
  126. package/dist/sandbox/exec-outcome.js +22 -22
  127. package/dist/sandbox/exec.d.ts +5 -0
  128. package/dist/sandbox/exec.js +9 -1
  129. package/dist/sandbox/identity.d.ts +4 -16
  130. package/dist/sandbox/identity.js +5 -21
  131. package/dist/sandbox/k8s-client.d.ts +0 -1
  132. package/dist/sandbox/k8s-client.js +8 -13
  133. package/dist/sandbox/lease-names.d.ts +6 -0
  134. package/dist/sandbox/lease-names.js +12 -0
  135. package/dist/sandbox/leases.d.ts +105 -0
  136. package/dist/sandbox/leases.js +519 -0
  137. package/dist/sandbox/liveness.d.ts +5 -2
  138. package/dist/sandbox/mount-plan.d.ts +10 -1
  139. package/dist/sandbox/mount-plan.js +7 -1
  140. package/dist/sandbox/reconciler.d.ts +58 -0
  141. package/dist/sandbox/reconciler.js +153 -0
  142. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  143. package/dist/sandbox/types.d.ts +25 -18
  144. package/dist/sandbox/types.js +10 -4
  145. package/dist/schemas/plan-schemas.d.ts +0 -21
  146. package/dist/schemas/plan-schemas.js +2 -3
  147. package/dist/schemas/run-synthesis.d.ts +2 -2
  148. package/dist/schemas/run-synthesis.js +3 -3
  149. package/dist/schemas/validate-plan.d.ts +1 -14
  150. package/dist/schemas/validate-plan.js +5 -24
  151. package/dist/schemas/workflow-state.d.ts +3 -32
  152. package/dist/schemas/workflow-state.js +4 -13
  153. package/dist/state/index.d.ts +0 -1
  154. package/dist/state/index.js +0 -1
  155. package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
  156. package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
  157. package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
  158. package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
  159. package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
  160. package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
  161. package/dist/state/migrations/index.js +6 -0
  162. package/dist/state/purge-analysis.d.ts +4 -2
  163. package/dist/state/purge-analysis.js +22 -11
  164. package/dist/state/sandbox-leases.d.ts +81 -0
  165. package/dist/state/sandbox-leases.js +247 -0
  166. package/dist/state/schema.d.ts +1 -13
  167. package/dist/state/schema.js +1 -6
  168. package/dist/state/step-executions.d.ts +0 -2
  169. package/dist/state/step-executions.js +1 -4
  170. package/dist/tasks/data-profile.d.ts +40 -2
  171. package/dist/tasks/data-profile.js +89 -87
  172. package/dist/tools/ad-hoc-router.d.ts +1 -6
  173. package/dist/tools/ad-hoc-router.js +5 -48
  174. package/dist/tools/bio/comptox.js +1 -1
  175. package/dist/tools/bio/drug-gene-interactions.js +1 -1
  176. package/dist/tools/bio/gene-disease-evidence.js +1 -1
  177. package/dist/tools/bio/keys.d.ts +1 -1
  178. package/dist/tools/bio/pubmed.js +31 -0
  179. package/dist/tools/bio/search-gene.js +17 -0
  180. package/dist/tools/bio/search-geo-datasets.js +13 -0
  181. package/dist/tools/bio/search-regulatory-approvals.js +1 -1
  182. package/dist/tools/define-tool.d.ts +25 -2
  183. package/dist/tools/define-tool.js +11 -1
  184. package/dist/tools/display/show-user.js +1 -1
  185. package/dist/tools/execute-analysis.d.ts +1 -3
  186. package/dist/tools/execute-analysis.js +5 -33
  187. package/dist/tools/report-session/index.d.ts +0 -1
  188. package/dist/tools/report-session/index.js +0 -1
  189. package/dist/tools/report-session/script-tools.d.ts +30 -0
  190. package/dist/tools/report-session/script-tools.js +177 -0
  191. package/dist/tools/research/generate-analogy-report.d.ts +16 -9
  192. package/dist/tools/research/generate-analogy-report.js +111 -49
  193. package/dist/tools/research/generate-plan.d.ts +102 -7
  194. package/dist/tools/research/generate-plan.js +74 -43
  195. package/dist/tools/research/index.d.ts +4 -3
  196. package/dist/tools/research/index.js +4 -3
  197. package/dist/tools/research/inspect-run.d.ts +0 -1
  198. package/dist/tools/research/inspect-run.js +5 -10
  199. package/dist/tools/research/search-arxiv.js +15 -0
  200. package/dist/tools/research/search-github-repos.js +13 -0
  201. package/dist/tools/research/search-semantic-scholar.js +16 -0
  202. package/dist/tools/research/synthesis-lookups.d.ts +21 -0
  203. package/dist/tools/research/synthesis-lookups.js +29 -0
  204. package/dist/tools/research/web-fetch.d.ts +1 -1
  205. package/dist/tools/research/web-fetch.js +14 -12
  206. package/dist/tools/sandbox/lease-tools.d.ts +32 -0
  207. package/dist/tools/sandbox/lease-tools.js +60 -0
  208. package/dist/tools/script/codemode-worker.d.ts +5 -0
  209. package/dist/tools/script/codemode-worker.js +5 -0
  210. package/dist/tools/script/file-tools.d.ts +100 -0
  211. package/dist/tools/script/file-tools.js +377 -0
  212. package/dist/tools/script/metrics.d.ts +21 -0
  213. package/dist/tools/script/metrics.js +38 -0
  214. package/dist/tools/script/quickjs.wasm +0 -0
  215. package/dist/tools/script/run-script.d.ts +76 -0
  216. package/dist/tools/script/run-script.js +326 -0
  217. package/dist/tools/script/scratch.d.ts +81 -0
  218. package/dist/tools/script/scratch.js +142 -0
  219. package/dist/tools/start-report-session.js +1 -1
  220. package/dist/tools/workspace/edit-file.js +2 -1
  221. package/dist/tools/workspace/execute-command.d.ts +9 -13
  222. package/dist/tools/workspace/execute-command.js +23 -19
  223. package/dist/tools/workspace/file-stat.js +1 -1
  224. package/dist/tools/workspace/grep.d.ts +5 -2
  225. package/dist/tools/workspace/grep.js +19 -10
  226. package/dist/tools/workspace/mutator.d.ts +48 -4
  227. package/dist/tools/workspace/mutator.js +43 -12
  228. package/dist/tools/workspace/read-file.d.ts +5 -2
  229. package/dist/tools/workspace/read-file.js +15 -11
  230. package/dist/tools/workspace/read-surface.d.ts +17 -0
  231. package/dist/tools/workspace/read-surface.js +17 -0
  232. package/dist/tools/workspace/result-bounds.d.ts +1 -0
  233. package/dist/tools/workspace/run-exec.d.ts +12 -13
  234. package/dist/tools/workspace/run-exec.js +13 -13
  235. package/dist/tools/workspace/scan-inputs.d.ts +2 -5
  236. package/dist/tools/workspace/scan-inputs.js +2 -3
  237. package/dist/tools/workspace/show-file.js +1 -1
  238. package/dist/tools/workspace/show-plan.js +4 -3
  239. package/dist/tools/workspace/write-file.js +2 -1
  240. package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
  241. package/dist/workflows/execute-analysis-scheduler.js +12 -32
  242. package/dist/workflows/execute-analysis.d.ts +17 -35
  243. package/dist/workflows/execute-analysis.js +13 -107
  244. package/dist/workflows/pause.js +29 -22
  245. package/dist/workflows/sandbox-step.d.ts +16 -31
  246. package/dist/workflows/sandbox-step.js +36 -71
  247. package/dist/workflows/test/sql-writes.d.ts +0 -1
  248. package/dist/workflows/test/sql-writes.js +0 -2
  249. package/dist/workspace/filesystem.d.ts +2 -2
  250. package/dist/workspace/filesystem.js +10 -5
  251. package/dist/workspace/paths.d.ts +19 -1
  252. package/dist/workspace/paths.js +22 -1
  253. package/package.json +6 -3
  254. package/dist/prompts/literature-reviewer.d.ts +0 -1
  255. package/dist/prompts/literature-reviewer.js +0 -114
  256. package/dist/sandbox/reaper.d.ts +0 -81
  257. package/dist/sandbox/reaper.js +0 -161
  258. package/dist/sandbox/step-sandbox.d.ts +0 -47
  259. package/dist/sandbox/step-sandbox.js +0 -53
  260. package/dist/state/active-sandboxes.d.ts +0 -33
  261. package/dist/state/active-sandboxes.js +0 -65
  262. package/dist/tasks/extract-values-script.d.ts +0 -64
  263. package/dist/tasks/extract-values-script.js +0 -180
  264. package/dist/tasks/extract-values.d.ts +0 -77
  265. package/dist/tasks/extract-values.js +0 -146
  266. package/dist/tools/report-session/derive-table.d.ts +0 -164
  267. package/dist/tools/report-session/derive-table.js +0 -419
  268. package/dist/tools/research/literature-reviewer.d.ts +0 -42
  269. package/dist/tools/research/literature-reviewer.js +0 -107
  270. package/dist/workflows/metrics.d.ts +0 -23
  271. package/dist/workflows/metrics.js +0 -38
@@ -7,10 +7,12 @@
7
7
  * submission is the synthesis.
8
8
  * - `report_blocker` — the run produced nothing synthesizable. An
9
9
  * accepted synthesis wins over a blocker.
10
- * - `literature_reviewer` — research sub-agent exposed as a tool. The
11
- * runtime registers it one time and gives it here;
12
- * the sub-agent loop's brief is the only context
13
- * handed across the boundary (forSubAgent semantics).
10
+ * - the lookups — the gene, protein, annotation, interaction,
11
+ * PubMed, citation, drug-gene, and preclinical
12
+ * tools (`createSynthesisLookupTools`). Each lookup
13
+ * records the PMIDs of its result, and
14
+ * `submit_synthesis` refuses a PMID that no lookup
15
+ * of the run returned.
14
16
  *
15
17
  * Two deliverables on a happy path: `synthesis.json` written to the run dir
16
18
  * and a `data-run-synthesis` chat data part emitted via the `emit` callback
@@ -27,6 +29,7 @@ import { ok } from "neverthrow";
27
29
  import { z } from "zod";
28
30
  import { writeFileWithinRoot } from "../lib/fs-helpers.js";
29
31
  import { isAllDigits } from "../lib/text.js";
32
+ import { parseUnknownRecord } from "../lib/unknown-containers.js";
30
33
  import { tryFs } from "../lib/fs-result.js";
31
34
  import { safeParseJsonAs } from "../lib/parse-json.js";
32
35
  import { RunSynthesisSchema, SynthesisDraftSchema, } from "../schemas/run-synthesis.js";
@@ -35,8 +38,10 @@ import { composeSystemPrompt } from "../agents/system-prompt.js";
35
38
  import { isSafeId, runDir } from "../workspace/paths.js";
36
39
  import { runWithSalvage } from "../loop/run-loop.js";
37
40
  import { recordedInputOf } from "../loop/tool-adapter.js";
38
- import { defineTool } from "../tools/define-tool.js";
41
+ import { defineTool, readToolResultImages, withToolCallRecord, withToolResultImages } from "../tools/define-tool.js";
39
42
  import { createReadToolOutputTool } from "../tools/read-tool-output.js";
43
+ import { createReadScriptTools } from "../tools/script/file-tools.js";
44
+ import { createReadSurfaceTools } from "../tools/workspace/read-surface.js";
40
45
  import { blockerOf, createReportBlockerToolFor, REPORT_BLOCKER_TOOL_ID } from "../tools/sandbox/report-blocker.js";
41
46
  // ── Agent identity / budgets ────────────────────────────────────────
42
47
  /**
@@ -47,17 +52,75 @@ import { blockerOf, createReportBlockerToolFor, REPORT_BLOCKER_TOOL_ID } from ".
47
52
  */
48
53
  export const SYNTHESIS_AGENT_ID = "run-synthesizer";
49
54
  /**
50
- * Budget for the synthesizer loop: read summaries + 1–3 reviewer delegations
51
- * + draft + 1–2 submit/fix cycles + headroom.
55
+ * Budget for the synthesizer loop: the rounds of lookups, each one a batch of
56
+ * parallel calls, then the draft, 1–2 submit/fix cycles, and headroom.
52
57
  */
53
- const SYNTHESIZER_MAX_ITERATIONS = 25;
54
- /**
55
- * Cap on `literature_reviewer` calls per run. Not named in the prompt: a
56
- * model treats a stated call-count limit as a target to hit, not a maximum.
57
- */
58
- const LITERATURE_REVIEWER_BUDGET = 3;
58
+ const SYNTHESIZER_MAX_ITERATIONS = 40;
59
59
  const SubmitSynthesisOutputSchema = z.object({ accepted: z.boolean() });
60
60
  const SUBMIT_SYNTHESIS_TOOL_ID = "submit_synthesis";
61
+ // ── The PMIDs of the lookups ────────────────────────────────────────
62
+ /**
63
+ * The keys under which a lookup gives back its request: `resolve_citation`
64
+ * returns the citation that it was given, normalized. A PMID there came from
65
+ * the model, not from a source.
66
+ */
67
+ const ECHO_KEYS = new Set(["input", "normalized", "supplied", "query"]);
68
+ /** Each PMID that a lookup result holds under a `pmid` or `pmids` key, outside the echo of the request. */
69
+ export function pmidsIn(value, found = new Set()) {
70
+ if (Array.isArray(value)) {
71
+ for (const item of value)
72
+ pmidsIn(item, found);
73
+ return found;
74
+ }
75
+ const record = parseUnknownRecord(value);
76
+ if (record === undefined)
77
+ return found;
78
+ for (const [key, child] of Object.entries(record)) {
79
+ if (ECHO_KEYS.has(key))
80
+ continue;
81
+ if (key === "pmid" && (typeof child === "string" || typeof child === "number")) {
82
+ if (isAllDigits(String(child)))
83
+ found.add(String(child));
84
+ }
85
+ else if (key === "pmids" && Array.isArray(child)) {
86
+ for (const pmid of child)
87
+ if (typeof pmid === "string" && isAllDigits(pmid))
88
+ found.add(pmid);
89
+ }
90
+ else {
91
+ pmidsIn(child, found);
92
+ }
93
+ }
94
+ return found;
95
+ }
96
+ const PmidRecordSchema = z.object({ pmids: z.array(z.string()) });
97
+ /**
98
+ * A lookup whose ok result records the PMIDs that it holds. The loop folds the
99
+ * record into `seen` after each round, on the first run and on each replay, thus
100
+ * the check of `submit_synthesis` reads the same set in both.
101
+ */
102
+ export function recordingPmids(tool, seen) {
103
+ if (tool.foldCallRecord !== undefined)
104
+ throw new Error(`the lookup ${tool.id} keeps its own call record, which the PMID record would replace`);
105
+ return {
106
+ ...tool,
107
+ execute: async (input, ctx) => (await tool.execute(input, ctx)).map((value) => {
108
+ const record = parseUnknownRecord(value);
109
+ if (record === undefined)
110
+ return value;
111
+ // The parsed record is a copy without the symbol keys of the value, thus the pictures move over by hand.
112
+ const images = readToolResultImages(value);
113
+ const carried = images.length === 0 ? record : withToolResultImages(record, images);
114
+ return withToolCallRecord(carried, { pmids: [...pmidsIn(record)] });
115
+ }),
116
+ foldCallRecord: (record) => {
117
+ const parsed = PmidRecordSchema.safeParse(record);
118
+ if (parsed.success)
119
+ for (const pmid of parsed.data.pmids)
120
+ seen.add(pmid);
121
+ },
122
+ };
123
+ }
61
124
  // ── Validation ──────────────────────────────────────────────────────
62
125
  // ── Semantic checks ─────────────────────────────────────────────────
63
126
  //
@@ -138,6 +201,20 @@ function checkKeyReferencePmidFormat(keyReferences) {
138
201
  }
139
202
  return issues;
140
203
  }
204
+ function checkPmidsLookedUp(synthesis, lookedUp) {
205
+ const cited = [
206
+ ...synthesis.findings.flatMap((f, fi) => f.references.map((r, ri) => ({ pmid: r.pmid, path: `synthesis.findings[${fi}].references[${ri}].pmid` }))),
207
+ ...synthesis.keyReferences.map((r, i) => ({ pmid: r.pmid, path: `synthesis.keyReferences[${i}].pmid` })),
208
+ ];
209
+ return cited
210
+ .filter(({ pmid }) => pmid.length > 0 && isAllDigits(pmid) && !lookedUp.has(pmid))
211
+ .map(({ pmid, path }) => ({
212
+ path,
213
+ code: "semantic",
214
+ message: `pmid "${pmid}" is in no lookup result of this run`,
215
+ hint: "Cite only a PMID that a lookup of this run returned. Look it up, or remove the reference.",
216
+ }));
217
+ }
141
218
  function semanticCheck(synthesis, ctx) {
142
219
  return [
143
220
  ...checkFindingStepIds(synthesis.findings, ctx),
@@ -145,6 +222,7 @@ function semanticCheck(synthesis, ctx) {
145
222
  ...checkKeyReferencesCited(synthesis.keyReferences, synthesis.findings),
146
223
  ...checkFindingPmidFormat(synthesis.findings),
147
224
  ...checkKeyReferencePmidFormat(synthesis.keyReferences),
225
+ ...checkPmidsLookedUp(synthesis, ctx.lookedUpPmids),
148
226
  ];
149
227
  }
150
228
  // ── Inner tools ─────────────────────────────────────────────────────
@@ -153,7 +231,7 @@ function buildSubmitTool(ctx) {
153
231
  id: SUBMIT_SYNTHESIS_TOOL_ID,
154
232
  description: "Submit the synthesis. Checks the payload against the semantic checks " +
155
233
  "the arg schema cannot express (stepId refs, theme→finding refs, " +
156
- "keyReferences PMIDs cited, numeric PMIDs). On success returns " +
234
+ "keyReferences PMIDs cited, numeric PMIDs, each PMID returned by a lookup of this run). On success returns " +
157
235
  "{accepted: true} — STOP after this. On rejection returns " +
158
236
  "{accepted: false, issues} — fix the specific fields at each issue " +
159
237
  "path and call again, or switch to report_blocker if the synthesis " +
@@ -196,9 +274,26 @@ function buildBlockerTool() {
196
274
  * the checks without driving the whole agent loop.
197
275
  */
198
276
  export function __buildInnerToolsForTest(args) {
199
- const ctx = { knownStepIds: args.knownStepIds };
277
+ const ctx = { knownStepIds: args.knownStepIds, lookedUpPmids: args.lookedUpPmids ?? new Set() };
200
278
  return { submit: buildSubmitTool(ctx), blocker: buildBlockerTool() };
201
279
  }
280
+ /** The synthesizer agent of one run. Its submit tool checks the step ids of the summaries and the PMIDs of its lookups. */
281
+ export function synthesizerAgentOf(input) {
282
+ const knownStepIds = new Set(input.summaries.map((s) => s.stepId));
283
+ const lookedUpPmids = new Set();
284
+ const innerCtx = { knownStepIds, lookedUpPmids };
285
+ const readTools = input.toolOutputStore ? [createReadToolOutputTool(input.toolOutputStore)] : [];
286
+ // The lookups run in `run_script`, which gives the PMID record of each nested call to its lookup.
287
+ return {
288
+ id: SYNTHESIS_AGENT_ID,
289
+ systemPrompt: composeSystemPrompt(synthesisAgentPrompt),
290
+ model: input.model,
291
+ tools: [...createReadSurfaceTools(input.workspace), buildSubmitTool(innerCtx), buildBlockerTool(), ...readTools],
292
+ scriptTools: [...input.synthesisLookups.map((tool) => recordingPmids(tool, lookedUpPmids)), ...createReadScriptTools(input.workspace)],
293
+ scriptRuntime: input.scriptRuntime,
294
+ maxIterations: SYNTHESIZER_MAX_ITERATIONS,
295
+ };
296
+ }
202
297
  /**
203
298
  * Drive the synthesizer agent loop to an outcome — the last accepted
204
299
  * synthesis (`submit_synthesis`), else a blocker (`report_blocker`).
@@ -208,21 +303,8 @@ export async function generateRunSynthesis(input) {
208
303
  if (input.summaries.length === 0) {
209
304
  throw new Error("Run synthesis: no step summaries to synthesize");
210
305
  }
211
- const knownStepIds = new Set(input.summaries.map((s) => s.stepId));
212
- const innerCtx = { knownStepIds };
213
- const reviewer = input.literatureReviewer;
214
- const submitTool = buildSubmitTool(innerCtx);
215
- const blockerTool = buildBlockerTool();
216
- const readTools = input.toolOutputStore ? [createReadToolOutputTool(input.toolOutputStore)] : [];
217
- const tools = [submitTool, blockerTool, reviewer, ...readTools];
218
- const agent = {
219
- id: SYNTHESIS_AGENT_ID,
220
- systemPrompt: composeSystemPrompt(synthesisAgentPrompt),
221
- model: input.model,
222
- tools,
223
- maxIterations: SYNTHESIZER_MAX_ITERATIONS,
224
- };
225
- const prompt = buildSynthesizerPrompt(input.planNarrative, input.summaries);
306
+ const agent = synthesizerAgentOf(input);
307
+ const prompt = synthesizerSeedOf(input.planNarrative, input.summaries);
226
308
  const signal = input.signal ?? new AbortController().signal;
227
309
  const emit = input.emit ?? (() => { });
228
310
  const run = await runWithSalvage(agent, [{ role: "user", content: prompt }], input.session, {
@@ -231,7 +313,6 @@ export async function generateRunSynthesis(input) {
231
313
  emit,
232
314
  usageRecorder: input.usageRecorder,
233
315
  toolOutputStore: input.toolOutputStore,
234
- policy: { budget: { [reviewer.id]: LITERATURE_REVIEWER_BUDGET } },
235
316
  pause: input.pause,
236
317
  }, {
237
318
  tools: [SUBMIT_SYNTHESIS_TOOL_ID, REPORT_BLOCKER_TOOL_ID],
@@ -385,7 +466,8 @@ export function formatSynthesisEmbeddingText(synthesis) {
385
466
  ].join("\n");
386
467
  }
387
468
  // ── Prompt builder ──────────────────────────────────────────────────
388
- function buildSynthesizerPrompt(planNarrative, summaries) {
469
+ /** The first message of the synthesizer: the narrative of the plan and the summary of each step. */
470
+ export function synthesizerSeedOf(planNarrative, summaries) {
389
471
  const summaryBlock = summaries.map((s) => `### ${s.stepId} (${s.agentId})\n\n${s.markdown}`).join("\n\n---\n\n");
390
472
  return ["## Analytical Narrative", planNarrative, "", "## Step Summaries", summaryBlock].join("\n");
391
473
  }
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * the agent with its own seam realizations.
6
6
  */
7
7
  export { assembleCoreRuntime } from "./runtime/assemble.js";
8
- export type { CoreRuntime, CoreRuntimeDeps, CoreWorkflowDeps, RegisteredWorkflows, ConversationAssemblyDeps, SandboxStepCallable, MakeReportReferenceResolver, ReportResolverScope, } from "./runtime/assemble.js";
8
+ export type { CoreRuntime, CoreRuntimeDeps, CoreSandboxDeps, CoreWorkflowDeps, RegisteredWorkflows, ConversationAssemblyDeps, SandboxStepCallable, MakeReportReferenceResolver, ReportResolverScope, } from "./runtime/assemble.js";
9
9
  export type { ReferenceResolver, ReportSnapshot, ResolvedValue } from "./report-model/reference-resolver.js";
10
10
  export type { ResolvePageAsset } from "./tools/report-session/index.js";
11
11
  export { PAGE_ASSETS } from "./report-render/assets.js";
@@ -43,7 +43,6 @@ export type { MakeSessionPagePublisher, SessionPageGrant, SessionPageMintFailure
43
43
  export type { ResolvePageUrl, SessionPageAccess } from "./tools/report-session/index.js";
44
44
  export { bindReadExport, bindSessionEmit } from "./provenance/seam.js";
45
45
  export type { ProvenanceExport, ProvenanceSeam, RunProvenanceEvent, SessionProvenanceEvent } from "./provenance/seam.js";
46
- export type { DerivationSource } from "./state/report-session-state.js";
47
46
  export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
48
47
  export type { RunLauncher, LaunchOptions } from "./execution/run-launcher.js";
49
48
  export { createDbosWorkflowPurger } from "./execution/dbos-workflow-purger.js";
@@ -158,6 +157,7 @@ export type { ResolveWorkspaceRoot } from "./workspace/paths.js";
158
157
  export { createSandboxClient } from "./sandbox/create-sandbox.js";
159
158
  export type { CreateSandboxClientConfig, SandboxBackendConfig } from "./sandbox/create-sandbox.js";
160
159
  export type { SandboxClient } from "./sandbox/client.js";
160
+ export type { AcquireOutcome, BoundLease, LeaseBinding, NoLease, SandboxLeases } from "./sandbox/leases.js";
161
161
  export type { ResolveSandboxLabels, SandboxLabels, SandboxSpec } from "./sandbox/types.js";
162
162
  export type { SpawnSession } from "./auth/types.js";
163
163
  export { describeSandboxError, keepSuspendingRefusal, SandboxFailure } from "./sandbox/sandbox-error.js";
@@ -188,7 +188,6 @@ export type { PlanStep } from "./workflows/execute-analysis-scheduler.js";
188
188
  export { planGraphOf } from "./workflows/plan/graph.js";
189
189
  export { planPartOf } from "./memory/card-builders.js";
190
190
  export { validatePlan } from "./schemas/validate-plan.js";
191
- export type { ValidatePlanOptions } from "./schemas/validate-plan.js";
192
191
  export type { ValidationResult } from "./schemas/validate-plan.js";
193
192
  export { upsertPlan, loadPlan, loadPlanRecord } from "./state/plans.js";
194
193
  export type { UpsertPlanInput, PlanRecord } from "./state/plans.js";
@@ -206,7 +205,5 @@ export { reportSessionDir } from "./workspace/paths.js";
206
205
  export type { LogFields, LogLevel, Logger } from "./lib/logger.js";
207
206
  export { defaultErrorFields } from "./lib/logger.js";
208
207
  export { createConsoleLogger, createNoopLogger } from "./lib/console-logger.js";
209
- export { registerSandboxReaper } from "./sandbox/reaper.js";
210
- export type { RegisterReaperDeps } from "./sandbox/reaper.js";
211
208
  export { registerNotificationSweep } from "./sandbox/notification-sweep.js";
212
209
  export type { RegisterNotificationSweepDeps } from "./sandbox/notification-sweep.js";
package/dist/index.js CHANGED
@@ -55,8 +55,7 @@ export { UnavailableSessionPagePublisher } from "./tools/report-session/session-
55
55
  // back for the report page. Thus the seam and its vocabulary belong on the front door: an embedder
56
56
  // cannot type its realization without them. The two binders ride beside the types, because an
57
57
  // embedder that calls `prepareChatTurn` or one tool factory directly binds the same total forms
58
- // that the assembly binds. `DerivationSource` rides beside the event union, because the derivation
59
- // event carries the chain of the derived table.
58
+ // that the assembly binds.
60
59
  export { bindReadExport, bindSessionEmit } from "./provenance/seam.js";
61
60
  // Run launching.
62
61
  export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
@@ -314,8 +313,6 @@ export { reportSessionDir } from "./workspace/paths.js";
314
313
  // `defaultErrorFields` is the shipped mapping for one with no opinion.
315
314
  export { defaultErrorFields } from "./lib/logger.js";
316
315
  export { createConsoleLogger, createNoopLogger } from "./lib/console-logger.js";
317
- // Sandbox-hygiene scheduled workflows. Reaper reclaims a dead host's orphaned
318
- // containers; the sweep prunes stale notifications. Each is a
319
- // `@DBOS.scheduled` no-op on an idle system.
320
- export { registerSandboxReaper } from "./sandbox/reaper.js";
316
+ // The scheduled sweep that prunes stale notifications. It is a `@DBOS.scheduled`
317
+ // no-op on an idle system. The sandbox reconciler registers with the runtime.
321
318
  export { registerNotificationSweep } from "./sandbox/notification-sweep.js";
@@ -17,8 +17,7 @@
17
17
  */
18
18
  import type { AgentSession } from "../auth/types.js";
19
19
  import type { EmitFn } from "../tools/define-tool.js";
20
- import type { SandboxClient } from "../sandbox/client.js";
21
- import type { SandboxRef } from "../sandbox/types.js";
20
+ import type { BoundLease } from "../sandbox/leases.js";
22
21
  import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
23
22
  import type { FileShape, HeaderReadout } from "./types.js";
24
23
  /** Members decoded per shape. One is enough to characterise a set whose names already match. */
@@ -45,24 +44,20 @@ export type ReadHeadersArgs = {
45
44
  readonly targets: readonly ReadoutTargetSpec[];
46
45
  readonly session: AgentSession;
47
46
  readonly fs: WorkspaceFilesystem;
48
- /** Omit the sandbox to skip container readouts; every other format still reads. */
49
- readonly sandboxClient?: SandboxClient;
50
- readonly sandbox?: SandboxRef;
47
+ /** Omit the lease to skip container readouts; every other format still reads. */
48
+ readonly lease?: BoundLease;
51
49
  /** Absolute in-sandbox path of the analysis root (`/{analysisId}`). */
52
50
  readonly mountRoot: string;
53
- readonly deadlineMs: number;
54
51
  readonly emit: EmitFn;
55
52
  };
56
53
  export type EnrichShapesArgs = {
57
54
  readonly shapes: readonly FileShape[];
58
55
  readonly session: AgentSession;
59
56
  readonly fs: WorkspaceFilesystem;
60
- /** Omit the sandbox to skip container readouts; every other format still reads. */
61
- readonly sandboxClient?: SandboxClient;
62
- readonly sandbox?: SandboxRef;
57
+ /** Omit the lease to skip container readouts; every other format still reads. */
58
+ readonly lease?: BoundLease;
63
59
  /** Absolute in-sandbox path of the analysis root (`/{analysisId}`). */
64
60
  readonly mountRoot: string;
65
- readonly deadlineMs: number;
66
61
  readonly emit: EmitFn;
67
62
  };
68
63
  /**
@@ -65,9 +65,9 @@ export async function readHeaders(args) {
65
65
  readouts.set(target.path, await readFromPrefix(args, target));
66
66
  }
67
67
  }));
68
- const { sandboxClient, sandbox } = args;
69
- if (containers.length > 0 && sandboxClient && sandbox) {
70
- for (const [path, readout] of await decodeContainers(args, { sandboxClient, sandbox }, containers))
68
+ const { lease } = args;
69
+ if (containers.length > 0 && lease) {
70
+ for (const [path, readout] of await decodeContainers(args, lease, containers))
71
71
  readouts.set(path, readout);
72
72
  }
73
73
  return readouts;
@@ -118,7 +118,7 @@ const ReadoutLineSchema = z.object({
118
118
  fields: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
119
119
  unavailable: z.string().optional(),
120
120
  });
121
- async function decodeContainers(args, machine, containers) {
121
+ async function decodeContainers(args, lease, containers) {
122
122
  const readouts = new Map();
123
123
  const source = await readFile(CONTAINER_DECODER, "utf8").catch(() => undefined);
124
124
  if (source === undefined) {
@@ -129,14 +129,18 @@ async function decodeContainers(args, machine, containers) {
129
129
  const byAbsolute = new Map();
130
130
  for (const target of containers)
131
131
  byAbsolute.set(`${args.mountRoot}/${target.path}`, target);
132
- const result = await runSandboxExec({
133
- sandboxClient: machine.sandboxClient,
134
- sandbox: machine.sandbox,
132
+ const ran = await runSandboxExec({
133
+ lease,
135
134
  command: ["python3", "-c", source, ...byAbsolute.keys()],
136
135
  timeoutSeconds: ENRICH_TIMEOUT_SECONDS,
137
- deadlineMs: args.deadlineMs,
138
136
  emit: args.emit,
139
137
  });
138
+ if (ran.isErr()) {
139
+ for (const { path } of containers)
140
+ readouts.set(path, { path, fields: {}, unavailable: ran.error.error });
141
+ return readouts;
142
+ }
143
+ const result = ran.value;
140
144
  for (const line of result.stdout.split("\n")) {
141
145
  const trimmed = line.trim();
142
146
  if (!trimmed.startsWith("{"))
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A PDF from markdown, on `marked` and `pdfmake`, with no browser.
3
+ *
4
+ * `marked` gives the token tree, and each token maps to a `pdfmake` node: a heading, a paragraph, a list, a GFM table,
5
+ * a code block, a quote, a rule, and an image. An SVG image, for example a chart of `render_chart`, lands as vector
6
+ * graphics. The text font is the Roboto that `pdfmake` ships, which holds the Greek letters and the common math signs.
7
+ *
8
+ * `pdfmake` resolves a font by a file path, also inside an SVG, thus the fonts land one time in a private temporary
9
+ * directory. Each URL is refused, and a local path is refused unless it is one of those font files, thus a document
10
+ * reads nothing that the caller did not give it.
11
+ */
12
+ /** An image that a document embeds. The caller reads the bytes, thus the document reaches no file and no URL. */
13
+ export type PdfImage = {
14
+ readonly kind: "svg";
15
+ readonly svg: string;
16
+ } | {
17
+ readonly kind: "png" | "jpeg";
18
+ readonly bytes: Uint8Array;
19
+ };
20
+ /** The source of each image of a markdown text, in the order of the text. */
21
+ export declare function markdownImageSources(markdown: string): string[];
22
+ /** The PDF of a markdown text. `images` holds each image that the text names, by its source. */
23
+ export declare function markdownToPdf(args: {
24
+ readonly markdown: string;
25
+ readonly title?: string;
26
+ readonly images: ReadonlyMap<string, PdfImage>;
27
+ }): Promise<Uint8Array>;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * A PDF from markdown, on `marked` and `pdfmake`, with no browser.
3
+ *
4
+ * `marked` gives the token tree, and each token maps to a `pdfmake` node: a heading, a paragraph, a list, a GFM table,
5
+ * a code block, a quote, a rule, and an image. An SVG image, for example a chart of `render_chart`, lands as vector
6
+ * graphics. The text font is the Roboto that `pdfmake` ships, which holds the Greek letters and the common math signs.
7
+ *
8
+ * `pdfmake` resolves a font by a file path, also inside an SVG, thus the fonts land one time in a private temporary
9
+ * directory. Each URL is refused, and a local path is refused unless it is one of those font files, thus a document
10
+ * reads nothing that the caller did not give it.
11
+ */
12
+ import { mkdtemp, writeFile } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { lexer } from "marked";
16
+ import { z } from "zod";
17
+ /** The token kinds of the lexer of `marked`. */
18
+ const MARKED_KINDS = new Set([
19
+ "blockquote",
20
+ "br",
21
+ "checkbox",
22
+ "code",
23
+ "codespan",
24
+ "def",
25
+ "del",
26
+ "em",
27
+ "escape",
28
+ "heading",
29
+ "hr",
30
+ "html",
31
+ "image",
32
+ "link",
33
+ "list",
34
+ "list_item",
35
+ "paragraph",
36
+ "space",
37
+ "strong",
38
+ "table",
39
+ "tag",
40
+ "text",
41
+ ]);
42
+ /**
43
+ * A token of one of the kinds of the lexer. No extension of `marked` is registered, thus a token with one of these
44
+ * kinds has the shape of that kind.
45
+ */
46
+ function isMarkedToken(token) {
47
+ return MARKED_KINDS.has(token.type);
48
+ }
49
+ /** The tokens of the lexer as its own kinds. */
50
+ function tokensOf(tokens) {
51
+ return (tokens ?? []).filter(isMarkedToken);
52
+ }
53
+ const FontFilesSchema = z.record(z.string(), z.string());
54
+ /** The width of the text block of an A4 page with the margins of {@link documentOf}, in points. */
55
+ const TEXT_WIDTH = 515;
56
+ const FONT_FILES = {
57
+ normal: "Roboto-Regular.ttf",
58
+ bold: "Roboto-Medium.ttf",
59
+ italics: "Roboto-Italic.ttf",
60
+ bolditalics: "Roboto-MediumItalic.ttf",
61
+ };
62
+ let ready;
63
+ /** The `pdfmake` instance with the fonts on disk and the access policies set. It is set up one time for each process. */
64
+ function pdfmakeOf() {
65
+ ready ??= (async () => {
66
+ const { default: pdfmake } = await import("pdfmake");
67
+ const vfsModule = await import("pdfmake/build/vfs_fonts.js");
68
+ const vfs = FontFilesSchema.parse(Reflect.get(Object(vfsModule), "default"));
69
+ const dir = await mkdtemp(join(tmpdir(), "pdf-fonts-"));
70
+ const paths = {};
71
+ for (const [style, file] of Object.entries(FONT_FILES)) {
72
+ const data = vfs[file];
73
+ if (data === undefined)
74
+ throw new Error(`pdfmake ships no font file ${file}`);
75
+ paths[style] = join(dir, file);
76
+ await writeFile(join(dir, file), Buffer.from(data, "base64"));
77
+ }
78
+ const fontPaths = new Set(Object.values(paths));
79
+ pdfmake.setFonts({ Roboto: paths });
80
+ pdfmake.setUrlAccessPolicy(() => false);
81
+ pdfmake.setLocalAccessPolicy((path) => fontPaths.has(path));
82
+ return pdfmake;
83
+ })();
84
+ return ready;
85
+ }
86
+ /** The text runs of inline tokens, with the styles of the marks that hold them. */
87
+ function inline(tokens, images) {
88
+ const out = [];
89
+ for (const token of tokensOf(tokens)) {
90
+ switch (token.type) {
91
+ case "strong":
92
+ out.push({ text: inline(token.tokens, images), bold: true });
93
+ break;
94
+ case "em":
95
+ out.push({ text: inline(token.tokens, images), italics: true });
96
+ break;
97
+ case "del":
98
+ out.push({ text: inline(token.tokens, images), decoration: "lineThrough" });
99
+ break;
100
+ case "codespan":
101
+ out.push({ text: token.text, background: "#f2f2f2" });
102
+ break;
103
+ case "link":
104
+ out.push({ text: inline(token.tokens, images), link: token.href, color: "#1a5fb4", decoration: "underline" });
105
+ break;
106
+ case "br":
107
+ out.push({ text: "\n" });
108
+ break;
109
+ case "image":
110
+ out.push({ text: token.text, italics: true });
111
+ break;
112
+ case "text":
113
+ out.push(token.tokens === undefined ? { text: token.text } : { text: inline(token.tokens, images) });
114
+ break;
115
+ case "escape":
116
+ out.push({ text: token.text });
117
+ break;
118
+ default:
119
+ out.push({ text: token.raw });
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ /** An image of a paragraph that holds one image alone, with its alt text as the caption. */
125
+ function imageNode(token, images) {
126
+ const image = images.get(token.href);
127
+ const caption = token.text.length > 0 ? [{ text: token.text, style: "caption" }] : [];
128
+ if (image === undefined)
129
+ return [{ text: `[${token.text || token.href}]`, italics: true }];
130
+ if (image.kind === "svg")
131
+ return [{ svg: image.svg, width: TEXT_WIDTH, margin: [0, 6, 0, 2] }, ...caption];
132
+ const data = `data:image/${image.kind};base64,${Buffer.from(image.bytes).toString("base64")}`;
133
+ return [{ image: data, fit: [TEXT_WIDTH, 600], margin: [0, 6, 0, 2] }, ...caption];
134
+ }
135
+ function blocks(tokens, images) {
136
+ const out = [];
137
+ for (const token of tokensOf(tokens)) {
138
+ switch (token.type) {
139
+ case "heading":
140
+ out.push({ text: inline(token.tokens, images), style: `h${String(Math.min(token.depth, 4))}` });
141
+ break;
142
+ case "paragraph": {
143
+ const parts = tokensOf(token.tokens);
144
+ const only = parts.length === 1 ? parts[0] : undefined;
145
+ if (only?.type === "image")
146
+ out.push(...imageNode(only, images));
147
+ else
148
+ out.push({ text: inline(token.tokens, images), style: "paragraph" });
149
+ break;
150
+ }
151
+ case "list":
152
+ out.push({
153
+ [token.ordered ? "ol" : "ul"]: token.items.map((item) => ({ stack: blocks(item.tokens, images) })),
154
+ style: "list",
155
+ });
156
+ break;
157
+ case "table":
158
+ out.push({
159
+ table: {
160
+ headerRows: 1,
161
+ widths: token.header.map(() => "*"),
162
+ body: [
163
+ token.header.map((cell) => ({ text: inline(cell.tokens, images), bold: true })),
164
+ ...token.rows.map((row) => row.map((cell) => ({ text: inline(cell.tokens, images) }))),
165
+ ],
166
+ },
167
+ layout: "lightHorizontalLines",
168
+ style: "table",
169
+ });
170
+ break;
171
+ case "code":
172
+ out.push({ text: token.text, style: "code", preserveLeadingSpaces: true });
173
+ break;
174
+ case "blockquote":
175
+ out.push({ stack: blocks(token.tokens, images), style: "quote" });
176
+ break;
177
+ case "hr":
178
+ out.push({ canvas: [{ type: "line", x1: 0, y1: 0, x2: TEXT_WIDTH, y2: 0, lineWidth: 0.5, lineColor: "#999999" }], margin: [0, 8, 0, 8] });
179
+ break;
180
+ case "text":
181
+ out.push({ text: inline(token.tokens, images), style: "paragraph" });
182
+ break;
183
+ default:
184
+ // A space, an HTML block, or a link definition prints nothing.
185
+ break;
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+ /** The source of each image of a markdown text, in the order of the text. */
191
+ export function markdownImageSources(markdown) {
192
+ const sources = [];
193
+ const walk = (tokens) => {
194
+ for (const token of tokensOf(tokens)) {
195
+ if (token.type === "image")
196
+ sources.push(token.href);
197
+ if (token.type === "list")
198
+ for (const item of token.items)
199
+ walk(item.tokens);
200
+ else if (token.type === "table")
201
+ for (const cell of [...token.header, ...token.rows.flat()])
202
+ walk(cell.tokens);
203
+ else if ("tokens" in token)
204
+ walk(token.tokens);
205
+ }
206
+ };
207
+ walk(lexer(markdown));
208
+ return [...new Set(sources)];
209
+ }
210
+ function documentOf(markdown, title, images) {
211
+ return {
212
+ info: title === undefined ? {} : { title },
213
+ pageSize: "A4",
214
+ pageMargins: [40, 48, 40, 48],
215
+ content: [...(title === undefined ? [] : [{ text: title, style: "title" }]), ...blocks(lexer(markdown), images)],
216
+ defaultStyle: { font: "Roboto", fontSize: 10, lineHeight: 1.25 },
217
+ styles: {
218
+ title: { fontSize: 20, bold: true, margin: [0, 0, 0, 12] },
219
+ h1: { fontSize: 16, bold: true, margin: [0, 12, 0, 6] },
220
+ h2: { fontSize: 13, bold: true, margin: [0, 10, 0, 4] },
221
+ h3: { fontSize: 11, bold: true, margin: [0, 8, 0, 4] },
222
+ h4: { fontSize: 10, bold: true, margin: [0, 6, 0, 2] },
223
+ paragraph: { margin: [0, 0, 0, 6] },
224
+ list: { margin: [0, 0, 0, 6] },
225
+ table: { fontSize: 9, margin: [0, 4, 0, 8] },
226
+ code: { fontSize: 8.5, background: "#f2f2f2", margin: [0, 2, 0, 8] },
227
+ quote: { color: "#555555", italics: true, margin: [12, 0, 0, 6] },
228
+ caption: { fontSize: 8.5, italics: true, color: "#555555", alignment: "center", margin: [0, 0, 0, 8] },
229
+ },
230
+ };
231
+ }
232
+ /** The PDF of a markdown text. `images` holds each image that the text names, by its source. */
233
+ export async function markdownToPdf(args) {
234
+ const pdfmake = await pdfmakeOf();
235
+ return pdfmake.createPdf(documentOf(args.markdown, args.title, args.images)).getBuffer();
236
+ }