@inflexa-ai/harness 0.17.0 → 0.18.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 (221) hide show
  1. package/dist/agents/report-builder.d.ts +2 -0
  2. package/dist/agents/report-builder.js +2 -0
  3. package/dist/app/spawn-report-session.d.ts +85 -0
  4. package/dist/app/spawn-report-session.js +83 -0
  5. package/dist/contracts/chat-events.d.ts +9 -0
  6. package/dist/contracts/index.d.ts +6 -2
  7. package/dist/contracts/index.js +3 -1
  8. package/dist/contracts/organ-system.d.ts +49 -0
  9. package/dist/contracts/organ-system.js +67 -0
  10. package/dist/contracts/report-blocks.d.ts +564 -0
  11. package/dist/contracts/report-blocks.js +117 -0
  12. package/dist/contracts/report-reference.d.ts +459 -0
  13. package/dist/contracts/report-reference.js +256 -0
  14. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  15. package/dist/contracts/schemas/chat-events.js +2 -0
  16. package/dist/contracts/severity.d.ts +15 -0
  17. package/dist/contracts/severity.js +10 -0
  18. package/dist/contracts/target-dossier.d.ts +8675 -12972
  19. package/dist/contracts/target-dossier.js +534 -591
  20. package/dist/data/safety-panel-schema.d.ts +18 -5
  21. package/dist/data/safety-panel-schema.js +4 -4
  22. package/dist/execution/artifact-metadata.js +1 -0
  23. package/dist/execution/report-runner.js +1 -3
  24. package/dist/execution/run-synthesis.js +2 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +19 -0
  27. package/dist/lib/async-utils.d.ts +16 -4
  28. package/dist/lib/async-utils.js +20 -15
  29. package/dist/lib/host-concurrency.js +1 -0
  30. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  31. package/dist/loop/run-agent.js +58 -10
  32. package/dist/loop/tool-detail.d.ts +40 -0
  33. package/dist/loop/tool-detail.js +35 -3
  34. package/dist/loop/types.d.ts +13 -0
  35. package/dist/memory/thread-history.d.ts +10 -0
  36. package/dist/memory/thread-history.js +12 -1
  37. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  38. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  39. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  40. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  41. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  42. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  43. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  44. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  45. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  46. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  47. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  48. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  49. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  50. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  51. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  52. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  53. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  54. package/dist/providers/__fixtures__/session.d.ts +5 -4
  55. package/dist/providers/__fixtures__/session.js +4 -4
  56. package/dist/report-model/block-walk.d.ts +56 -0
  57. package/dist/report-model/block-walk.js +89 -0
  58. package/dist/report-model/draft-finish.d.ts +60 -0
  59. package/dist/report-model/draft-finish.js +56 -0
  60. package/dist/report-model/draft-operations.d.ts +125 -0
  61. package/dist/report-model/draft-operations.js +341 -0
  62. package/dist/report-model/draft-read.d.ts +59 -0
  63. package/dist/report-model/draft-read.js +109 -0
  64. package/dist/report-model/draft.d.ts +51 -0
  65. package/dist/report-model/draft.js +32 -0
  66. package/dist/report-model/fixture-resolver.d.ts +8 -0
  67. package/dist/report-model/fixture-resolver.js +252 -0
  68. package/dist/report-model/mint-snapshot.d.ts +30 -0
  69. package/dist/report-model/mint-snapshot.js +40 -0
  70. package/dist/report-model/reference-resolver.d.ts +113 -0
  71. package/dist/report-model/reference-resolver.js +60 -0
  72. package/dist/report-model/structural-validation.d.ts +22 -0
  73. package/dist/report-model/structural-validation.js +68 -0
  74. package/dist/report-model/validate.d.ts +40 -0
  75. package/dist/report-model/validate.js +80 -0
  76. package/dist/report-render/chart.d.ts +33 -0
  77. package/dist/report-render/chart.js +538 -0
  78. package/dist/report-render/page.d.ts +127 -0
  79. package/dist/report-render/page.js +307 -0
  80. package/dist/report-render/references.d.ts +32 -0
  81. package/dist/report-render/references.js +44 -0
  82. package/dist/report-render/render.d.ts +25 -0
  83. package/dist/report-render/render.js +125 -0
  84. package/dist/report-render/script-json.d.ts +13 -0
  85. package/dist/report-render/script-json.js +15 -0
  86. package/dist/report-render/types.d.ts +39 -0
  87. package/dist/report-render/types.js +10 -0
  88. package/dist/report-render/views/chart-view.d.ts +11 -0
  89. package/dist/report-render/views/chart-view.js +23 -0
  90. package/dist/report-render/views/page-view.d.ts +20 -0
  91. package/dist/report-render/views/page-view.js +49 -0
  92. package/dist/report-render/views/prose.d.ts +28 -0
  93. package/dist/report-render/views/prose.js +75 -0
  94. package/dist/report-render/views/references-view.d.ts +20 -0
  95. package/dist/report-render/views/references-view.js +64 -0
  96. package/dist/report-render/views/values.d.ts +42 -0
  97. package/dist/report-render/views/values.js +43 -0
  98. package/dist/runtime/boot.d.ts +3 -3
  99. package/dist/runtime/boot.js +15 -4
  100. package/dist/runtime/dbos.d.ts +4 -4
  101. package/dist/state/artifacts.d.ts +16 -0
  102. package/dist/state/artifacts.js +18 -0
  103. package/dist/state/init.js +31 -0
  104. package/dist/state/purge-analysis.js +1 -0
  105. package/dist/state/report-versions.d.ts +132 -0
  106. package/dist/state/report-versions.js +194 -0
  107. package/dist/tasks/data-profile.js +1 -0
  108. package/dist/tools/bio/chembl.js +1 -0
  109. package/dist/tools/bio/comptox.js +1 -0
  110. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  111. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  112. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  113. package/dist/tools/bio/keys.d.ts +3 -2
  114. package/dist/tools/bio/keys.js +3 -2
  115. package/dist/tools/bio/lookup-annotation.js +1 -0
  116. package/dist/tools/bio/opentargets.d.ts +3 -3
  117. package/dist/tools/bio/opentargets.js +4 -3
  118. package/dist/tools/bio/pubchem.js +1 -0
  119. package/dist/tools/bio/search-clinical-trials.js +1 -0
  120. package/dist/tools/bio/search-faers.js +1 -0
  121. package/dist/tools/bio/search-geo-datasets.js +1 -0
  122. package/dist/tools/bio/search-interactions.js +1 -0
  123. package/dist/tools/bio/target-safety.d.ts +4 -6
  124. package/dist/tools/bio/target-safety.js +7 -6
  125. package/dist/tools/define-tool.d.ts +19 -12
  126. package/dist/tools/define-tool.js +4 -2
  127. package/dist/tools/display/show-user.js +1 -0
  128. package/dist/tools/iterate-report.js +2 -0
  129. package/dist/tools/lib/chembl-client.d.ts +2 -0
  130. package/dist/tools/lib/chembl-client.js +1 -0
  131. package/dist/tools/lib/monarch-client.d.ts +42 -0
  132. package/dist/tools/lib/monarch-client.js +100 -0
  133. package/dist/tools/report/build-report.js +1 -0
  134. package/dist/tools/report/mint-preview-url.js +1 -0
  135. package/dist/tools/report/preview-snapshot.js +1 -0
  136. package/dist/tools/report/submit-report.js +1 -0
  137. package/dist/tools/report/version-fs.js +4 -0
  138. package/dist/tools/report-authoring/authoring-tools.d.ts +134 -0
  139. package/dist/tools/report-authoring/authoring-tools.js +352 -0
  140. package/dist/tools/research/context7-docs.js +2 -0
  141. package/dist/tools/research/generate-analogy-report.js +1 -0
  142. package/dist/tools/research/generate-plan.js +3 -0
  143. package/dist/tools/research/inspect-data-profile.js +4 -0
  144. package/dist/tools/research/literature-reviewer.js +1 -0
  145. package/dist/tools/research/search-arxiv.js +1 -0
  146. package/dist/tools/research/search-github-repos.js +1 -0
  147. package/dist/tools/research/search-semantic-scholar.js +1 -0
  148. package/dist/tools/sandbox/list-available-packages.js +23 -0
  149. package/dist/tools/sandbox/list-available-refs.js +36 -0
  150. package/dist/tools/sandbox/report-blocker.js +1 -0
  151. package/dist/tools/sandbox/skills.js +2 -0
  152. package/dist/tools/workspace/file-stat.js +1 -0
  153. package/dist/tools/workspace/show-file.js +2 -0
  154. package/dist/tools/workspace/show-plan.js +1 -0
  155. package/dist/workflows/execute-analysis.d.ts +5 -1
  156. package/dist/workflows/execute-analysis.js +5 -5
  157. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  158. package/dist/workflows/execute-target-assessment.js +78 -16
  159. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  160. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  161. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  162. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  163. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  164. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  165. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  166. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  167. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  168. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  169. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  170. package/dist/workflows/target-assessment/coverage.js +33 -29
  171. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  172. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  173. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  174. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  175. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  176. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  177. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  178. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  179. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  180. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  181. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  182. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  183. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  184. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  185. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  186. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  187. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  188. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  189. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  190. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  191. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  192. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  193. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  194. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  195. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  196. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  197. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  198. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  199. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  200. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  201. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  202. package/dist/workflows/target-assessment/metrics.js +1 -13
  203. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
  204. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  205. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  206. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  207. package/dist/workflows/target-assessment/schemas.d.ts +46 -1
  208. package/dist/workflows/target-assessment/schemas.js +19 -0
  209. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  210. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  211. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
  212. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  213. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
  214. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  215. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  216. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  217. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  218. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  219. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  220. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  221. package/package.json +4 -1
@@ -0,0 +1,352 @@
1
+ /**
2
+ * The authoring tool surface over the draft operations.
3
+ *
4
+ * The factory closes over a private draft holder and the pinned snapshot. It gives eight tools and a
5
+ * read of the current draft. The tools read no ambient state, thus two factories hold two independent
6
+ * drafts.
7
+ *
8
+ * Each tool maps its flat input onto a core operation, and it applies the pure operation. The holder
9
+ * swaps only on a landed operation. A refused operation returns typed data in the ok channel, and the
10
+ * error channel stays for an unexpected failure.
11
+ *
12
+ * The primitive admits a flat object input only. Thus a destination is four optional fields, and not a
13
+ * nested union. A conflict between the flat fields refuses before the core runs.
14
+ */
15
+ import { err, ok } from "neverthrow";
16
+ import { z } from "zod";
17
+ import { addBlock, changeBlock, locate, moveBlock, removeBlock, setTitle, } from "../../report-model/draft-operations.js";
18
+ import { buildOutline, childOutline, readBlock } from "../../report-model/draft-read.js";
19
+ import { finishDraft } from "../../report-model/draft-finish.js";
20
+ import { DraftBlockSchema } from "../../report-model/draft.js";
21
+ import { defineTool } from "../define-tool.js";
22
+ /**
23
+ * The block payload.
24
+ *
25
+ * The published JSON Schema carries the whole draft grammar, because a tool is self-describing at attach
26
+ * time and the schema is the only place where the model learns the shape of the eight block kinds. A bare
27
+ * `z.unknown()` emits the empty schema, which tells the model nothing and leaves it to discover each
28
+ * required field through one refusal at a time.
29
+ *
30
+ * The `z.unknown()` member keeps the runtime parse permissive. Thus a malformed payload still reaches the
31
+ * core, which parses it with the same grammar and refuses `malformed-block` as typed data in the ok
32
+ * channel. A strict input schema would turn that designed refusal into a hard input-validation error from
33
+ * the loop.
34
+ */
35
+ const blockPayload = z.union([DraftBlockSchema, z.unknown()]);
36
+ /**
37
+ * A flat field that the model can leave out.
38
+ *
39
+ * Nullish, and not optional. A model that must choose one of four destination fields routinely sends
40
+ * `null` for the three it does not use, and strict function calling requires every declared key to be
41
+ * present. Under `.optional()` each of those nulls fails the input parse, and the block never lands.
42
+ */
43
+ const absentable = (schema) => schema.nullish();
44
+ /** The flat destination fields. An anchor is `before` or `after`, and a place is `start` or `end`. */
45
+ const addBlockInput = z.object({
46
+ block: blockPayload,
47
+ parentId: absentable(z.string()),
48
+ place: absentable(z.enum(["start", "end"])),
49
+ before: absentable(z.string()),
50
+ after: absentable(z.string()),
51
+ });
52
+ /**
53
+ * The change payload is a title for a section target, or a block for an atom target, and exactly one of
54
+ * the two. Both fields are absentable, because a section retitle names no block. Zod 4 treats a bare
55
+ * `z.unknown()` as a required key, thus an optional marker is what makes the retitle call representable.
56
+ */
57
+ const changeBlockInput = z.object({
58
+ targetId: z.string(),
59
+ title: absentable(z.string()),
60
+ block: blockPayload.optional(),
61
+ });
62
+ const removeBlockInput = z.object({
63
+ targetId: z.string(),
64
+ });
65
+ const moveBlockInput = z.object({
66
+ targetId: z.string(),
67
+ parentId: absentable(z.string()),
68
+ place: absentable(z.enum(["start", "end"])),
69
+ before: absentable(z.string()),
70
+ after: absentable(z.string()),
71
+ });
72
+ const readOutlineInput = z.object({});
73
+ /** The read names its target with the same field name as each mutation, thus one id key serves them all. */
74
+ const readBlockInput = z.object({
75
+ targetId: z.string(),
76
+ });
77
+ const setTitleInput = z.object({
78
+ title: z.string().min(1),
79
+ });
80
+ const finishDraftInput = z.object({});
81
+ /** Read an absentable field. An explicit `null` and an omitted key both mean that the model gave no value. */
82
+ function given(value) {
83
+ return value ?? undefined;
84
+ }
85
+ /**
86
+ * Map the flat destination fields onto the core destination.
87
+ *
88
+ * Two anchors, or an anchor beside a place, name two places at one time. Thus the map refuses with
89
+ * `conflicting-destination`, and the detail names the conflict. An anchor implies its own parent, thus a
90
+ * place beside an anchor is a contradiction and not a refinement.
91
+ *
92
+ * The reason is not `unknown-target`, which means that no block holds a named id. A conflict is a fault in
93
+ * the shape of the call, and the repair is to drop one field. An agent that reads `unknown-target` instead
94
+ * goes back to the outline for a better id, finds the ids were already correct, and sends the same
95
+ * conflicting pair again.
96
+ */
97
+ function toDestination(input) {
98
+ const parentId = given(input.parentId);
99
+ const place = given(input.place);
100
+ const before = given(input.before);
101
+ const after = given(input.after);
102
+ if (before !== undefined && after !== undefined) {
103
+ return err({ reason: "conflicting-destination", detail: "a destination names `before` or `after`, not both" });
104
+ }
105
+ const hasAnchor = before !== undefined || after !== undefined;
106
+ if (hasAnchor && place !== undefined) {
107
+ return err({ reason: "conflicting-destination", detail: "a destination names an anchor or a `place`, not both" });
108
+ }
109
+ let resolvedPlace;
110
+ if (before !== undefined) {
111
+ resolvedPlace = { before };
112
+ }
113
+ else if (after !== undefined) {
114
+ resolvedPlace = { after };
115
+ }
116
+ else {
117
+ resolvedPlace = place;
118
+ }
119
+ return ok({ parentId, place: resolvedPlace });
120
+ }
121
+ /**
122
+ * Map the flat change fields onto the core change operation.
123
+ *
124
+ * A change names a title or a block, and exactly one of the two. Neither field and both fields each name
125
+ * an ambiguous change. Thus the map refuses with `payload-kind-mismatch`.
126
+ */
127
+ function toChangeOperation(input) {
128
+ const title = given(input.title);
129
+ const block = given(input.block);
130
+ if (title !== undefined && block !== undefined) {
131
+ return err({ reason: "payload-kind-mismatch", detail: "a change names a `title` or a `block`, not both" });
132
+ }
133
+ if (title !== undefined) {
134
+ return ok({ targetId: input.targetId, title });
135
+ }
136
+ if (block !== undefined) {
137
+ return ok({ targetId: input.targetId, block: decodeBlockPayload(block) });
138
+ }
139
+ return err({ reason: "payload-kind-mismatch", detail: "a change names a `title` or a `block`" });
140
+ }
141
+ /**
142
+ * Give the block payload as the core expects it, and decode a double-encoded one.
143
+ *
144
+ * A model routinely sends a nested object as a `JSON.stringify` string. The loop repairs that at its own
145
+ * input boundary, but only for a schema that rejects the string, and this payload accepts any value so
146
+ * that a malformed block can come back as a typed refusal. Thus the decode happens here. A string that is
147
+ * not JSON passes through as it is, and the core refuses it as a malformed block.
148
+ */
149
+ function decodeBlockPayload(block) {
150
+ if (typeof block !== "string") {
151
+ return block;
152
+ }
153
+ try {
154
+ return JSON.parse(block);
155
+ }
156
+ catch {
157
+ return block;
158
+ }
159
+ }
160
+ /** Read the `kind` of an untyped block payload, or `undefined` when the payload names none. */
161
+ function readKind(block) {
162
+ return readStringField(block, "kind");
163
+ }
164
+ /** Read the `id` of an untyped block payload, or `undefined` when the payload names none. */
165
+ function readId(block) {
166
+ return readStringField(block, "id");
167
+ }
168
+ function readStringField(block, field) {
169
+ if (block !== null && typeof block === "object") {
170
+ const value = block[field];
171
+ if (typeof value === "string") {
172
+ return value;
173
+ }
174
+ }
175
+ return undefined;
176
+ }
177
+ /**
178
+ * The id of the section that holds a block, or `undefined` when the block sits at the root.
179
+ *
180
+ * The two cases are not distinguishable from the value alone, thus a caller that needs to tell "the root"
181
+ * from "no such block" tests the block itself first.
182
+ */
183
+ function holderIdOf(document, blockId) {
184
+ return locate(document, blockId)?.parent?.id;
185
+ }
186
+ /** Read the child order of one container out of a document. */
187
+ function containerIn(document, parentId) {
188
+ if (parentId === undefined) {
189
+ return { children: childOutline(document.sections, 0) };
190
+ }
191
+ const located = locate(document, parentId);
192
+ // A parent id reaches here only from a landed operation, which already resolved it to a section. The
193
+ // empty fall-back keeps the read total rather than asserting that.
194
+ const children = located !== undefined && located.block.kind === "section" ? located.block.blocks : [];
195
+ return { parentId, children: childOutline(children, located?.path.length ?? 0) };
196
+ }
197
+ /**
198
+ * Every tool of this surface runs inline.
199
+ *
200
+ * The draft is closure memory with no durable backing. A step-mode tool goes through `DBOS.runStep`, which
201
+ * caches the result of a step and replays it without running the body. A replay after a host restart would
202
+ * hand the agent the cached `applied: true` outline of each landed operation while the rebuilt closure
203
+ * holds an empty draft, and the finish would then report an empty document for a report that the
204
+ * transcript shows as complete. Inline mode keeps the tool result and the draft in one lifetime.
205
+ */
206
+ const AUTHORING_EXECUTION_MODE = "inline";
207
+ /**
208
+ * Make the eight authoring tools over one draft.
209
+ *
210
+ * The holder is a private `let`. Each tool reads the holder at call time, thus a landing through one tool
211
+ * shows in the next call of every tool. The holder swaps only inside `land`, thus a refused operation
212
+ * leaves the draft as it was.
213
+ */
214
+ export function createReportAuthoringTools(state) {
215
+ const snapshot = state.snapshot;
216
+ let draft = state.initialDraft ?? { title: "", sections: [] };
217
+ /**
218
+ * Swap the holder on a landing, and report the containers that the operation changed.
219
+ *
220
+ * `holders` names them by id, and it reads the draft as it was and the draft as it becomes, because a
221
+ * removed block has a holder only in the first and an added block has one only in the second. The
222
+ * child order comes out of `next` in either case.
223
+ */
224
+ const land = (result, holders) => result.match((next) => {
225
+ const previous = draft;
226
+ draft = next;
227
+ // A move inside one container names the same holder two times.
228
+ const unique = [...new Set(holders(previous, next))];
229
+ return { applied: true, changed: unique.map((parentId) => containerIn(next, parentId)) };
230
+ }, (refusal) => ({ applied: false, refusal }));
231
+ const refuse = (refusal) => ({ applied: false, refusal });
232
+ const add_block = defineTool({
233
+ id: "add_block",
234
+ description: "Add one block to the draft. The `block` schema gives the eight kinds and the fields of each one. " +
235
+ "You choose the `id` of the block, and it must be unique in the draft. " +
236
+ "Name the destination with `parentId` and one of `place`, `before`, or `after`. " +
237
+ "The root admits a section only, and an atom needs a section as its parent.",
238
+ inputSchema: addBlockInput,
239
+ executionMode: AUTHORING_EXECUTION_MODE,
240
+ describeCall: (input) => {
241
+ const kind = readKind(decodeBlockPayload(input.block));
242
+ return kind !== undefined ? `add ${kind}` : "add a block";
243
+ },
244
+ execute: async (input) => {
245
+ const destination = toDestination(input);
246
+ if (destination.isErr()) {
247
+ return ok(refuse(destination.error));
248
+ }
249
+ const block = decodeBlockPayload(input.block);
250
+ const addedId = readId(block);
251
+ return ok(land(addBlock(draft, { block, destination: destination.value }, snapshot), (_previous, next) => [
252
+ addedId === undefined ? undefined : holderIdOf(next, addedId),
253
+ ]));
254
+ },
255
+ });
256
+ const change_block = defineTool({
257
+ id: "change_block",
258
+ description: "Change one block by its id. Give a `title` to retitle a section, or a `block` to replace an atom, and never both. " +
259
+ "The change keeps the id of the target, thus the `id` of the payload does not matter. A kind change is permitted.",
260
+ inputSchema: changeBlockInput,
261
+ executionMode: AUTHORING_EXECUTION_MODE,
262
+ describeCall: (input) => `change ${input.targetId}`,
263
+ execute: async (input) => {
264
+ const operation = toChangeOperation(input);
265
+ if (operation.isErr()) {
266
+ return ok(refuse(operation.error));
267
+ }
268
+ return ok(land(changeBlock(draft, operation.value, snapshot), (_previous, next) => [holderIdOf(next, input.targetId)]));
269
+ },
270
+ });
271
+ const remove_block = defineTool({
272
+ id: "remove_block",
273
+ description: "Remove one block by its id. A removed section takes its whole subtree with it, and the id is free for a later add.",
274
+ inputSchema: removeBlockInput,
275
+ executionMode: AUTHORING_EXECUTION_MODE,
276
+ describeCall: (input) => `remove ${input.targetId}`,
277
+ execute: async (input) => ok(land(removeBlock(draft, { targetId: input.targetId }, snapshot), (previous) => [holderIdOf(previous, input.targetId)])),
278
+ });
279
+ const move_block = defineTool({
280
+ id: "move_block",
281
+ description: "Move one block by its id to a new destination. Name the destination with `parentId` and one of `place`, `before`, or `after`. " +
282
+ "A section cannot move into its own subtree.",
283
+ inputSchema: moveBlockInput,
284
+ executionMode: AUTHORING_EXECUTION_MODE,
285
+ describeCall: (input) => `move ${input.targetId}`,
286
+ execute: async (input) => {
287
+ const destination = toDestination(input);
288
+ if (destination.isErr()) {
289
+ return ok(refuse(destination.error));
290
+ }
291
+ return ok(land(moveBlock(draft, { targetId: input.targetId, destination: destination.value }, snapshot), (previous, next) => [
292
+ holderIdOf(previous, input.targetId),
293
+ holderIdOf(next, input.targetId),
294
+ ]));
295
+ },
296
+ });
297
+ const set_title = defineTool({
298
+ id: "set_title",
299
+ description: "Set the title of the report document. A draft starts with no title, and a document needs one. " +
300
+ "The title names the whole report, and it is not the title of a section.",
301
+ inputSchema: setTitleInput,
302
+ executionMode: AUTHORING_EXECUTION_MODE,
303
+ describeCall: (input) => `title the report ${input.title}`,
304
+ // The title sits on the document, thus no container changes its child order.
305
+ execute: async (input) => ok(land(ok(setTitle(draft, input.title)), () => [])),
306
+ });
307
+ const read_outline = defineTool({
308
+ id: "read_outline",
309
+ description: "Read the outline of the draft. Each entry gives the id, the kind, the nesting depth, and a short label. " +
310
+ "A label that ends with the character … is clipped. Use the outline as the primary view of the draft, " +
311
+ "and read one block when the label is not enough.",
312
+ inputSchema: readOutlineInput,
313
+ executionMode: AUTHORING_EXECUTION_MODE,
314
+ // The input carries no field, thus a hook can only restate the name of the tool.
315
+ describeCall: "none",
316
+ execute: async () => ok({ outline: buildOutline(draft) }),
317
+ });
318
+ const read_block = defineTool({
319
+ id: "read_block",
320
+ description: "Read one block by its id. An atom gives the full block with its bindings. A section gives its title and the id of each child, " +
321
+ "because the outline already names every block under it. Use it when the outline label is not enough.",
322
+ inputSchema: readBlockInput,
323
+ executionMode: AUTHORING_EXECUTION_MODE,
324
+ describeCall: (input) => `read ${input.targetId}`,
325
+ execute: async (input) => {
326
+ const block = readBlock(draft, input.targetId);
327
+ const result = block !== undefined ? { found: true, block } : { found: false };
328
+ return ok(result);
329
+ },
330
+ });
331
+ const finish_draft = defineTool({
332
+ id: "finish_draft",
333
+ description: "Finish the draft. The result gives each completeness gap, or the valid report document, and each advisory warning. " +
334
+ "Use it to make sure that the draft passes the whole document schema, the id rule, and the structural tier.",
335
+ inputSchema: finishDraftInput,
336
+ executionMode: AUTHORING_EXECUTION_MODE,
337
+ // The input carries no field, thus a hook can only restate the name of the tool.
338
+ describeCall: "none",
339
+ execute: async () => ok(finishDraft(draft, snapshot)),
340
+ });
341
+ return {
342
+ add_block,
343
+ change_block,
344
+ remove_block,
345
+ move_block,
346
+ set_title,
347
+ read_outline,
348
+ read_block,
349
+ finish_draft,
350
+ currentDraft: () => draft,
351
+ };
352
+ }
@@ -25,6 +25,7 @@ export const resolveLibraryIdTool = defineTool({
25
25
  libraryName: z.string().describe("Package or library name to search for (e.g., 'scanpy', 'pysam', 'lifelines')."),
26
26
  query: z.string().describe("What you need help with — used to rank results by relevance (e.g., 'how to run differential expression')."),
27
27
  }),
28
+ describeCall: "none",
28
29
  execute: async ({ libraryName, query }) => {
29
30
  const result = await apiFetchValidated(`${CONTEXT7_BASE}/search?query=${encodeURIComponent(libraryName)}&topic=${encodeURIComponent(query)}`, Context7SearchResponseSchema, { maxRetries: 1 });
30
31
  if (result.isErr())
@@ -48,6 +49,7 @@ export const queryDocsTool = defineTool({
48
49
  libraryId: z.string().describe("Context7 library ID from resolve-library-id (e.g., '/scverse/scanpy')."),
49
50
  query: z.string().describe("Specific question about the library (e.g., 'rank_genes_groups function parameters and usage')."),
50
51
  }),
52
+ describeCall: "none",
51
53
  execute: async ({ libraryId, query }) => {
52
54
  const result = await apiFetchValidated(`${CONTEXT7_BASE}/docs?libraryId=${encodeURIComponent(libraryId)}&query=${encodeURIComponent(query)}`, Context7DocsResponseSchema, { maxRetries: 1 });
53
55
  if (result.isErr())
@@ -193,6 +193,7 @@ export function createGenerateAnalogyReportTool(deps) {
193
193
  "latency on the same failure). Surface the message and ask the user to " +
194
194
  "narrow the problem.",
195
195
  inputSchema: generateAnalogyReportInputSchema,
196
+ describeCall: "none",
196
197
  execute: async (input, ctx) => {
197
198
  const childSession = forSubAgent(ctx.session, AGENT_ID);
198
199
  // Phase 1+2: drive the research agent over the cross-domain toolset.
@@ -344,6 +344,7 @@ function buildInnerTools(holder, trace, persistCtx, pool, resourcePolicy, logger
344
344
  // structured issue reporting.
345
345
  plan: z.union([PlannerPlanSchema, z.unknown()]).describe("Candidate plan; malformed values are reported in issues."),
346
346
  }),
347
+ describeCall: "none",
347
348
  execute: async (input) => {
348
349
  if (holder.outcome !== null) {
349
350
  trace.duplicateTerminalCalls++;
@@ -416,6 +417,7 @@ function buildInnerTools(holder, trace, persistCtx, pool, resourcePolicy, logger
416
417
  question: z.string().min(1),
417
418
  questionContext: z.string().optional(),
418
419
  }),
420
+ describeCall: "none",
419
421
  execute: async (input) => {
420
422
  if (holder.outcome !== null) {
421
423
  trace.duplicateTerminalCalls++;
@@ -670,6 +672,7 @@ export function createGeneratePlanTool(deps) {
670
672
  .optional()
671
673
  .describe("The planId of the prior plan being iterated on. Set only when " + "the user is asking for modifications to an existing plan."),
672
674
  }),
675
+ describeCall: ({ researchQuestion }) => researchQuestion,
673
676
  execute: async (input, ctx) => {
674
677
  const analysisId = scopeResource(ctx.session.scope).resourceId;
675
678
  // Bound per invocation: `analysisId` comes from the call-time session, not from
@@ -98,6 +98,10 @@ export function createInspectDataProfileTool(pool) {
98
98
  .optional()
99
99
  .describe(`Per-file records per page. Only used when scope is 'files'. Default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE}.`),
100
100
  }),
101
+ // `execute` defaults both fields internally, and the hook applies the same
102
+ // defaults. Without them the ordinary call, which names no field at all,
103
+ // gives no detail. `page` acts only on the `files` scope, as in `execute`.
104
+ describeCall: ({ scope, page }) => ((scope ?? "overview") === "files" ? `files (page ${page ?? 1})` : "overview"),
101
105
  execute: async (input, ctx) => {
102
106
  const resourceId = scopeResource(ctx.session.scope).resourceId;
103
107
  const status = unwrapOrThrow(await loadDataProfileStatus(pool, resourceId));
@@ -68,6 +68,7 @@ export function createLiteratureReviewerTool(deps) {
68
68
  "biological features to investigate and the experimental " +
69
69
  "context. The reviewer sees only this — include everything it needs."),
70
70
  }),
71
+ describeCall: "none",
71
72
  execute: async ({ brief }, ctx) => {
72
73
  const { messages: transcript } = await runAgent(agent, [{ role: "user", content: brief }], forSubAgent(ctx.session, AGENT_ID), {
73
74
  provider: deps.provider,
@@ -18,6 +18,7 @@ export const searchArxivTool = defineTool({
18
18
  .describe('Optional arXiv category filters (e.g., ["cs.LG", "math.OC"]). ' + "Combined with the query using AND."),
19
19
  limit: z.number().int().min(1).max(20).default(10).describe("Maximum results (1–20, default 10)."),
20
20
  }),
21
+ describeCall: "none",
21
22
  execute: async ({ query, categories, limit }, context) => {
22
23
  const result = await source.search({ query, ...(categories === undefined ? {} : { categories }), limit }, context.signal);
23
24
  if (result.status !== "ok")
@@ -61,6 +61,7 @@ export function createSearchGithubReposTool(deps) {
61
61
  language: z.string().optional().describe('Optional language filter (e.g., "python").'),
62
62
  limit: z.number().int().min(1).max(20).default(10).describe("Maximum results (1–20, default 10)."),
63
63
  }),
64
+ describeCall: "none",
64
65
  execute: async ({ query, language, limit }) => {
65
66
  const q = language ? `${query} language:${language}` : query;
66
67
  const params = new URLSearchParams({
@@ -24,6 +24,7 @@ export function createSearchSemanticScholarTool(options = {}) {
24
24
  '"reinforcement learning for control" or "feedback stabilisation in oscillators".'),
25
25
  limit: z.number().int().min(1).max(20).default(10).describe("Maximum number of results to return (1–20, default 10)."),
26
26
  }),
27
+ describeCall: "none",
27
28
  execute: async ({ query, limit }, context) => {
28
29
  const result = await source.search(query, limit, context.signal);
29
30
  if (result.status !== "ok")
@@ -26,6 +26,7 @@ import { readFile } from "node:fs/promises";
26
26
  import { ok } from "neverthrow";
27
27
  import { z } from "zod";
28
28
  import { defineTool } from "../define-tool.js";
29
+ import { capCodePoints, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
29
30
  /**
30
31
  * Where the list lives when the host mounts the library store at the same path
31
32
  * the sandbox does. Hosts that do not — the store is baked into the image and
@@ -187,6 +188,28 @@ export function createListAvailablePackagesTool(deps = {}) {
187
188
  .optional()
188
189
  .describe(`Cap the packages listed. Omit to get the whole set — the store fits well within the ${DEFAULT_LIMIT} ceiling. Ignored when \`names\` is given.`),
189
190
  }),
191
+ // `queryPackages` returns on `names` before it reads another field, thus
192
+ // the presence check comes first. An empty array is not a presence check.
193
+ // `names` are exact identifiers a caller checks for, thus they name the
194
+ // call as they are. A blank query counts as absent, because `queryPackages`
195
+ // trims the needle and applies no filter when nothing is left.
196
+ //
197
+ // A needle rides behind `matching "…"`, because a bare needle reads as a
198
+ // package name. The needle takes its own bound, thus the emit-site cap
199
+ // cannot cut inside the mark and leave the quote open. `language` stays a
200
+ // trailing qualifier, exactly as `execute` treats it.
201
+ describeCall: ({ names, query, language }) => {
202
+ if (names !== undefined && names.length > 0)
203
+ return names.join(", ");
204
+ const needle = query?.trim();
205
+ if (needle !== undefined && needle !== "") {
206
+ const marked = `matching "${capCodePoints(needle, DETAIL_NEEDLE_MAX_LENGTH)}"`;
207
+ return language === undefined ? marked : `${marked} (${language})`;
208
+ }
209
+ if (language !== undefined)
210
+ return `${language} packages`;
211
+ return "full package list";
212
+ },
190
213
  execute: async (input) => {
191
214
  // An unreadable inventory is an expected environment state — model it as an
192
215
  // `available: false` data variant telling the caller the set is UNKNOWN.
@@ -27,6 +27,7 @@ import { lstat, readFile, readdir } from "node:fs/promises";
27
27
  import { join as hostJoin, posix as posixPath } from "node:path";
28
28
  import { ok } from "neverthrow";
29
29
  import { z } from "zod";
30
+ import { capCodePoints, DETAIL_MAX_LENGTH, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
30
31
  import { REFERENCE_DATA_CATALOG } from "../../reference-data/catalog.js";
31
32
  import { parseReferenceInstallReceipt } from "../../reference-data/receipt.js";
32
33
  import { defineTool } from "../define-tool.js";
@@ -510,6 +511,41 @@ export function createListAvailableRefsTool(deps) {
510
511
  "The store is provisioned by the host and is NOT frozen: datasets can be added through the host's own provisioning path, and whatever is added shows up on a later call. " +
511
512
  "So before you report a gap as permanent, check whether any tool you hold reaches that provisioning path — offer that route if one exists, and call the gap permanent only when none does.",
512
513
  inputSchema: ListAvailableRefsInputSchema,
514
+ // `execute` resolves `path ?? category` for the subtree it inspects, and
515
+ // it ignores `category` when `path` is present. `query` is an ADDITIVE
516
+ // filter over that subtree, thus the target keeps naming the call and the
517
+ // filter rides beside it.
518
+ //
519
+ // A needle rides behind `matching "…"`: a bare needle reads as a path, and
520
+ // a filtered subtree must name both halves, not the target alone. The two
521
+ // halves join with a space and a word, as `grep` joins its own two halves.
522
+ // A separator glyph belongs to a host, which owns its line and its own
523
+ // vocabulary; the harness renders nothing and thus borrows nothing.
524
+ //
525
+ // Each half carries its own bound. The emit-site cap cuts the TAIL, and
526
+ // `path` admits 4096 bytes, thus one long target would swallow the very
527
+ // filter this detail exists to state. The needle takes a fixed budget, and
528
+ // the target takes what the bounded filter leaves. Then both halves reach
529
+ // a reader, and each one marks its own cut.
530
+ //
531
+ // A blank value counts as absent, because `execute` treats it that way: it
532
+ // browses the store root for a blank target, and it applies no filter for a
533
+ // blank needle. The precedence stays `??` and never `||`, thus a blank
534
+ // `path` still suppresses `category` exactly as `execute` does.
535
+ describeCall: ({ path, category, query }) => {
536
+ const target = path ?? category;
537
+ const named = target !== undefined && target.trim() !== "" ? target : undefined;
538
+ const needle = query?.trim();
539
+ const filter = needle === undefined || needle === "" ? undefined : `matching "${capCodePoints(needle, DETAIL_NEEDLE_MAX_LENGTH)}"`;
540
+ if (named === undefined)
541
+ return filter ?? "full reference store";
542
+ if (filter === undefined)
543
+ return named;
544
+ // The budget comes off the rendered filter, never off a count of its
545
+ // parts, thus it cannot drift from the template above. One code point
546
+ // pays for the joining space.
547
+ return `${capCodePoints(named, DETAIL_MAX_LENGTH - [...filter].length - 1)} ${filter}`;
548
+ },
513
549
  execute: async ({ path, category, query, limit }) => {
514
550
  // `category` is shorthand for a top-level path; an explicit `path` wins.
515
551
  const requestedInput = path ?? category;
@@ -33,6 +33,7 @@ export function createReportBlockerToolFor(deps) {
33
33
  id: "report_blocker",
34
34
  description: `Terminal. ${deps.blockedWhen} Pass a clear reason. Stop immediately after calling — take no further actions.`,
35
35
  inputSchema: ReportBlockerInputSchema,
36
+ describeCall: "none",
36
37
  execute: async (input) => {
37
38
  deps.record({ kind: "blocker", reason: input.reason });
38
39
  return ok({
@@ -54,6 +54,7 @@ export function createSkillTools(deps) {
54
54
  query: z.string().min(1).describe("Keywords to match (e.g. 'PyDESeq2 contrast syntax')."),
55
55
  topK: z.number().int().min(1).max(MAX_TOP_K).optional().describe(`Max results to return. Defaults to ${DEFAULT_TOP_K}.`),
56
56
  }),
57
+ describeCall: "none",
57
58
  execute: async ({ query, topK }) => {
58
59
  if (allowed.size === 0) {
59
60
  return ok({ status: "no_skills" });
@@ -114,6 +115,7 @@ export function createSkillTools(deps) {
114
115
  skill: z.string().min(1).describe("Skill name (one of your declared skills)."),
115
116
  path: z.string().min(1).describe("File path within the skill, e.g. 'SKILL.md' or 'references/pydeseq2-api.md'."),
116
117
  }),
118
+ describeCall: "none",
117
119
  execute: async ({ skill, path }) => {
118
120
  if (!allowed.has(skill)) {
119
121
  return ok({ status: "skill_not_declared", skill });
@@ -22,6 +22,7 @@ export function createFileStatTool(fs, workingDir) {
22
22
  "in bytes. Use before reading to size a file. Missing and out-of-scope " +
23
23
  "paths return a data variant, not an error.",
24
24
  inputSchema: FileStatInputSchema,
25
+ describeCall: "none",
25
26
  execute: async ({ path }, ctx) => {
26
27
  const result = unwrapOrThrow(await fs.stat({ session: ctx.session, path, workingDir }));
27
28
  switch (result.kind) {
@@ -38,6 +38,8 @@ export const showFileTool = defineTool({
38
38
  "Cards render in call order, so to interleave figures with prose alternate `show_user(markdown)` and `show_file` calls " +
39
39
  "(markdown image syntax pointing at a workspace file does NOT render — this tool is the only way to show one).",
40
40
  inputSchema: ShowFileInputSchema,
41
+ // `.min(1)` on `files` makes index 0 total after the parse.
42
+ describeCall: ({ files }) => (files.length === 1 ? files[0].path : `${files.length} files`),
41
43
  execute: async (input, ctx) => {
42
44
  const card = buildFileReferenceCardData(input);
43
45
  // A malformed/traversal path yields no card — an expected outcome the model can self-correct
@@ -32,6 +32,7 @@ export function createShowPlanTool(pool) {
32
32
  .describe("Plan ID (`pln-<8hex>`) from `generate_plan` (newly created) or `inspect_run` (each run carries the planId it ran)"),
33
33
  title: z.string().optional().describe("Optional card heading; defaults to the plan's own title"),
34
34
  }),
35
+ describeCall: ({ planId }) => planId,
35
36
  execute: async ({ planId, title }, ctx) => {
36
37
  const resourceId = scopeResource(ctx.session.scope).resourceId;
37
38
  // "Plan not found" / "plan no longer parses" are expected outcomes —
@@ -298,8 +298,12 @@ export interface ExecuteAnalysisDeps {
298
298
  * point. Idempotency is the consumer's job, achieved via the deterministic
299
299
  * identifiers the events carry, not via step caching. Every call site is
300
300
  * guarded so a throwing observer never fails the run.
301
+ *
302
+ * Events arrive with the run's `RunSession` from durable workflow input — a
303
+ * persisting realization needs its credential, scope, and identity.
304
+ * Implementations may ignore the parameter.
301
305
  */
302
- readonly emitProvenance?: (event: RunProvenanceEvent) => void;
306
+ readonly emitProvenance?: (event: RunProvenanceEvent, session: RunSession) => void;
303
307
  /**
304
308
  * Optional, fire-and-forget observation of the run's live shape, for a host driving a UI.
305
309
  *
@@ -87,12 +87,12 @@ function emitStreamPart(part) {
87
87
  * swallowed, never corrupt run state — integrity enforcement lives at the
88
88
  * host's own ledger, not here.
89
89
  */
90
- function emitProvenanceGuarded(deps, event) {
90
+ function emitProvenanceGuarded(deps, event, session) {
91
91
  const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis");
92
92
  if (!deps.emitProvenance)
93
93
  return;
94
94
  try {
95
- deps.emitProvenance(event);
95
+ deps.emitProvenance(event, session);
96
96
  }
97
97
  catch (err) {
98
98
  logger.error("emitProvenance threw", { runId: event.runId, event: event.type, ...logger.errorFields(err) });
@@ -437,7 +437,7 @@ export async function runExecuteAnalysisBody(input, deps) {
437
437
  planSummary: input.planSummary,
438
438
  stepCount: input.steps.length,
439
439
  atMs: startedAtMs,
440
- });
440
+ }, input.runSession);
441
441
  // Opening snapshot: every step pending. The scheduler's own map does not exist yet, and an
442
442
  // empty one yields exactly that — so a host has the run's full shape before the first dispatch
443
443
  // rather than learning the step list from whichever transition happens to land first.
@@ -930,7 +930,7 @@ async function runSchedulerLoop(args) {
930
930
  status: stepStatus,
931
931
  ...(stepDurationMs !== undefined ? { durationMs: stepDurationMs } : {}),
932
932
  atMs: settledAtMs,
933
- });
933
+ }, input.runSession);
934
934
  }
935
935
  await drainBudgetExceededNotifications(budgetExceededChildIds);
936
936
  return {
@@ -1000,7 +1000,7 @@ async function collectAndComplete(args) {
1000
1000
  status,
1001
1001
  atMs: terminalAtMs,
1002
1002
  durationMs: terminalAtMs - startedAtMs,
1003
- });
1003
+ }, input.runSession);
1004
1004
  try {
1005
1005
  await DBOS.runStep(async () => {
1006
1006
  unwrapOrThrow(await updateRunStatus(deps.pool, runId, status, failureReason ?? (status === "canceled" && !budgetExceeded ? "external_cancel" : null)));