@inflexa-ai/harness 0.42.2 → 0.43.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 (199) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/sandbox/shared.d.ts +1 -6
  3. package/dist/agents/sandbox/shared.js +0 -15
  4. package/dist/app/chat-turn.js +13 -1
  5. package/dist/config/environment-stores.d.ts +4 -2
  6. package/dist/contracts/chat-events.d.ts +9 -1
  7. package/dist/contracts/chat-frame.d.ts +3 -3
  8. package/dist/contracts/chat-frame.js +8 -3
  9. package/dist/contracts/chat-parts.d.ts +4 -1
  10. package/dist/contracts/data-profile.d.ts +4 -6
  11. package/dist/contracts/index.d.ts +1 -1
  12. package/dist/contracts/report-blocks.d.ts +1364 -75
  13. package/dist/contracts/report-blocks.js +221 -29
  14. package/dist/contracts/report-reference.js +1 -1
  15. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  16. package/dist/contracts/schemas/chat-events.js +5 -0
  17. package/dist/contracts/schemas/chat-parts.d.ts +2 -2
  18. package/dist/contracts/schemas/index.d.ts +1 -1
  19. package/dist/contracts/schemas/index.js +1 -1
  20. package/dist/execution/input-signature.d.ts +7 -13
  21. package/dist/execution/input-signature.js +0 -0
  22. package/dist/execution/run-canceler.d.ts +3 -2
  23. package/dist/execution/run-canceler.js +3 -2
  24. package/dist/execution/run-event-stream.d.ts +16 -2
  25. package/dist/execution/run-event-stream.js +54 -3
  26. package/dist/index.d.ts +1 -8
  27. package/dist/index.js +5 -16
  28. package/dist/input-scan/enrich.d.ts +0 -2
  29. package/dist/input-scan/enrich.js +0 -1
  30. package/dist/lib/metrics.js +6 -7
  31. package/dist/lib/otel-spans.d.ts +4 -9
  32. package/dist/lib/otel-spans.js +3 -8
  33. package/dist/loop/run-agent.d.ts +4 -2
  34. package/dist/loop/run-agent.js +38 -23
  35. package/dist/memory/thread-history.d.ts +1 -1
  36. package/dist/prompts/report-session.d.ts +1 -1
  37. package/dist/prompts/report-session.js +17 -0
  38. package/dist/provenance/exec-frame.d.ts +1 -1
  39. package/dist/report-model/authoring-grammar.d.ts +416 -17
  40. package/dist/report-model/authoring-grammar.js +25 -4
  41. package/dist/report-model/block-walk.d.ts +20 -4
  42. package/dist/report-model/block-walk.js +64 -7
  43. package/dist/report-model/draft-finish.d.ts +6 -2
  44. package/dist/report-model/draft-finish.js +6 -1
  45. package/dist/report-model/production-resolver.d.ts +8 -0
  46. package/dist/report-model/production-resolver.js +1 -1
  47. package/dist/report-model/validate.d.ts +20 -10
  48. package/dist/report-model/validate.js +29 -13
  49. package/dist/report-render/chart-export.d.ts +52 -0
  50. package/dist/report-render/chart-export.js +192 -0
  51. package/dist/report-render/chart-presets.d.ts +46 -47
  52. package/dist/report-render/chart-presets.js +31 -101
  53. package/dist/report-render/chart-renderers.d.ts +163 -0
  54. package/dist/report-render/chart-renderers.js +1055 -0
  55. package/dist/report-render/chart-toolbox.d.ts +124 -0
  56. package/dist/report-render/chart-toolbox.js +595 -0
  57. package/dist/report-render/chart.d.ts +60 -27
  58. package/dist/report-render/chart.js +2323 -524
  59. package/dist/report-render/design.d.ts +476 -16
  60. package/dist/report-render/design.js +390 -56
  61. package/dist/report-render/figures/common.d.ts +321 -0
  62. package/dist/report-render/figures/common.js +759 -0
  63. package/dist/report-render/figures/dendrogram.d.ts +36 -0
  64. package/dist/report-render/figures/dendrogram.js +130 -0
  65. package/dist/report-render/figures/dense.d.ts +111 -0
  66. package/dist/report-render/figures/dense.js +204 -0
  67. package/dist/report-render/figures/dotplot.d.ts +28 -0
  68. package/dist/report-render/figures/dotplot.js +234 -0
  69. package/dist/report-render/figures/embedding.d.ts +43 -0
  70. package/dist/report-render/figures/embedding.js +360 -0
  71. package/dist/report-render/figures/equal-units.d.ts +101 -0
  72. package/dist/report-render/figures/equal-units.js +215 -0
  73. package/dist/report-render/figures/forest.d.ts +20 -0
  74. package/dist/report-render/figures/forest.js +398 -0
  75. package/dist/report-render/figures/gsea.d.ts +30 -0
  76. package/dist/report-render/figures/gsea.js +391 -0
  77. package/dist/report-render/figures/heatmap.d.ts +40 -0
  78. package/dist/report-render/figures/heatmap.js +395 -0
  79. package/dist/report-render/figures/index.d.ts +135 -0
  80. package/dist/report-render/figures/index.js +100 -0
  81. package/dist/report-render/figures/km.d.ts +16 -0
  82. package/dist/report-render/figures/km.js +406 -0
  83. package/dist/report-render/figures/label-room.d.ts +130 -0
  84. package/dist/report-render/figures/label-room.js +341 -0
  85. package/dist/report-render/figures/locuszoom.d.ts +50 -0
  86. package/dist/report-render/figures/locuszoom.js +675 -0
  87. package/dist/report-render/figures/lollipop.d.ts +16 -0
  88. package/dist/report-render/figures/lollipop.js +247 -0
  89. package/dist/report-render/figures/ma.d.ts +20 -0
  90. package/dist/report-render/figures/ma.js +114 -0
  91. package/dist/report-render/figures/manhattan.d.ts +60 -0
  92. package/dist/report-render/figures/manhattan.js +343 -0
  93. package/dist/report-render/figures/oncoprint.d.ts +41 -0
  94. package/dist/report-render/figures/oncoprint.js +420 -0
  95. package/dist/report-render/figures/pca.d.ts +37 -0
  96. package/dist/report-render/figures/pca.js +241 -0
  97. package/dist/report-render/figures/qq.d.ts +30 -0
  98. package/dist/report-render/figures/qq.js +182 -0
  99. package/dist/report-render/figures/roc.d.ts +17 -0
  100. package/dist/report-render/figures/roc.js +116 -0
  101. package/dist/report-render/figures/sankey.d.ts +48 -0
  102. package/dist/report-render/figures/sankey.js +353 -0
  103. package/dist/report-render/figures/upset.d.ts +60 -0
  104. package/dist/report-render/figures/upset.js +316 -0
  105. package/dist/report-render/figures/volcano.d.ts +25 -0
  106. package/dist/report-render/figures/volcano.js +166 -0
  107. package/dist/report-render/hybrid-svg.d.ts +75 -0
  108. package/dist/report-render/hybrid-svg.js +504 -0
  109. package/dist/report-render/number-format.d.ts +19 -0
  110. package/dist/report-render/number-format.js +25 -1
  111. package/dist/report-render/page.d.ts +41 -4
  112. package/dist/report-render/page.js +247 -12
  113. package/dist/report-render/render.d.ts +13 -4
  114. package/dist/report-render/render.js +37 -6
  115. package/dist/report-render/types.d.ts +23 -2
  116. package/dist/report-render/value-bridge.d.ts +38 -3
  117. package/dist/report-render/value-bridge.js +137 -6
  118. package/dist/report-render/views/chart-view.d.ts +42 -6
  119. package/dist/report-render/views/chart-view.js +70 -9
  120. package/dist/report-render/views/page-view.js +9 -10
  121. package/dist/runtime/assemble.js +2 -2
  122. package/dist/sandbox/client.d.ts +23 -32
  123. package/dist/sandbox/client.js +3 -11
  124. package/dist/sandbox/create-sandbox.d.ts +9 -24
  125. package/dist/sandbox/create-sandbox.js +15 -30
  126. package/dist/sandbox/docker-client.d.ts +12 -22
  127. package/dist/sandbox/docker-client.js +29 -43
  128. package/dist/sandbox/exec.d.ts +58 -0
  129. package/dist/sandbox/exec.js +197 -0
  130. package/dist/sandbox/identity.d.ts +5 -5
  131. package/dist/sandbox/identity.js +7 -10
  132. package/dist/sandbox/k8s-client.d.ts +4 -10
  133. package/dist/sandbox/k8s-client.js +5 -12
  134. package/dist/sandbox/liveness.d.ts +5 -12
  135. package/dist/sandbox/liveness.js +5 -12
  136. package/dist/sandbox/notification-sweep.d.ts +4 -7
  137. package/dist/sandbox/notification-sweep.js +4 -7
  138. package/dist/sandbox/reaper.d.ts +3 -3
  139. package/dist/sandbox/reaper.js +3 -3
  140. package/dist/sandbox/sandbox-error.d.ts +10 -11
  141. package/dist/sandbox/sandbox-error.js +10 -11
  142. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  143. package/dist/sandbox/sandbox-step-translate.js +1 -1
  144. package/dist/sandbox/types.d.ts +31 -99
  145. package/dist/sandbox/types.js +17 -84
  146. package/dist/state/active-sandboxes.d.ts +7 -32
  147. package/dist/state/active-sandboxes.js +11 -46
  148. package/dist/state/index.d.ts +1 -2
  149. package/dist/state/index.js +1 -1
  150. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
  151. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
  152. package/dist/state/migrations/index.js +2 -0
  153. package/dist/state/schema.d.ts +1 -5
  154. package/dist/state/schema.js +1 -6
  155. package/dist/state/step-executions.js +1 -2
  156. package/dist/tasks/data-profile.js +7 -20
  157. package/dist/tasks/derive-table-exec.d.ts +4 -14
  158. package/dist/tasks/derive-table-exec.js +9 -24
  159. package/dist/tasks/extract-values.d.ts +2 -2
  160. package/dist/tasks/extract-values.js +4 -7
  161. package/dist/tools/define-tool.d.ts +56 -13
  162. package/dist/tools/define-tool.js +28 -1
  163. package/dist/tools/execute-analysis.js +3 -3
  164. package/dist/tools/report-session/derive-table.d.ts +8 -9
  165. package/dist/tools/report-session/derive-table.js +3 -4
  166. package/dist/tools/report-session/preview-report.js +4 -49
  167. package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
  168. package/dist/tools/sandbox/list-available-packages.js +5 -4
  169. package/dist/tools/workspace/edit-file.d.ts +5 -4
  170. package/dist/tools/workspace/edit-file.js +10 -13
  171. package/dist/tools/workspace/execute-command.d.ts +27 -42
  172. package/dist/tools/workspace/execute-command.js +42 -38
  173. package/dist/tools/workspace/index.d.ts +5 -6
  174. package/dist/tools/workspace/index.js +5 -6
  175. package/dist/tools/workspace/mutator.d.ts +43 -8
  176. package/dist/tools/workspace/mutator.js +45 -34
  177. package/dist/tools/workspace/run-exec.d.ts +3 -12
  178. package/dist/tools/workspace/run-exec.js +7 -24
  179. package/dist/tools/workspace/scan-inputs.d.ts +0 -3
  180. package/dist/tools/workspace/scan-inputs.js +0 -1
  181. package/dist/tools/workspace/write-file.d.ts +12 -3
  182. package/dist/tools/workspace/write-file.js +17 -13
  183. package/dist/workflows/sandbox-step.d.ts +7 -11
  184. package/dist/workflows/sandbox-step.js +24 -27
  185. package/package.json +3 -1
  186. package/dist/sandbox/await-exec.d.ts +0 -143
  187. package/dist/sandbox/await-exec.js +0 -417
  188. package/dist/sandbox/deliver-exec-event.d.ts +0 -25
  189. package/dist/sandbox/deliver-exec-event.js +0 -29
  190. package/dist/sandbox/digest.d.ts +0 -14
  191. package/dist/sandbox/digest.js +0 -17
  192. package/dist/sandbox/exec-id.d.ts +0 -12
  193. package/dist/sandbox/exec-id.js +0 -20
  194. package/dist/sandbox/hmac.d.ts +0 -62
  195. package/dist/sandbox/hmac.js +0 -72
  196. package/dist/sandbox/submit-exec.d.ts +0 -38
  197. package/dist/sandbox/submit-exec.js +0 -80
  198. package/dist/sandbox/watchdog.d.ts +0 -71
  199. package/dist/sandbox/watchdog.js +0 -161
@@ -1,22 +1,17 @@
1
1
  /**
2
2
  * The container of one report-session derivation, as a registered DBOS workflow.
3
3
  *
4
- * The `derive_table` tool runs inside one live chat turn, and a chat turn is not a workflow body. An await
5
- * of an exec under the callback transport calls `DBOS.recv`, which a workflow body alone can call. Thus the
4
+ * The `derive_table` tool runs inside one live chat turn, and a chat turn is not a workflow. An exec runs as
5
+ * a durable step of a workflow, and the reaper maps each container to the workflow that owns it. Thus the
6
6
  * container lives here, behind a registered workflow, and the tool starts it and awaits the handle.
7
7
  *
8
8
  * The shape is the shape of `tasks/extract-values.ts`: a body that a test drives directly, a registration
9
9
  * that closes over the construction-time deps, and a trigger that starts the workflow and gives the result
10
10
  * back. The tool holds the trigger as a plain function, thus no module under `tools/` imports DBOS.
11
11
  *
12
- * The body owns the whole machine lifetime. It creates the container, submits one exec, awaits the terminal
12
+ * The body owns the whole machine lifetime. It creates the container, runs one exec to its terminal
13
13
  * result, and tears the container down on both paths. A teardown fault reaches the log alone, because the
14
14
  * work is done by then.
15
- *
16
- * The exec id is `${workflowId}:${stepId}:${fnId}`, the one shape that `workflowIdFromExec` parses. A
17
- * callback host reads the owner workflow out of the exec id alone, thus a flat id makes the completion
18
- * callback unroutable and it lands as a `400`. The pull backstop still settles such an exec, but it pays
19
- * the quiet interval first.
20
15
  */
21
16
  import { DBOS } from "@dbos-inc/dbos-sdk";
22
17
  import { err, ok } from "neverthrow";
@@ -26,7 +21,7 @@ import { mintSandboxIdentity } from "../sandbox/identity.js";
26
21
  import { keepSuspendingRefusal } from "../sandbox/sandbox-error.js";
27
22
  import { suspensionOfSpawnRefusal } from "../workflows/suspension.js";
28
23
  import { buildDerivationExec, DERIVATION_DEADLINE_MS, DERIVATION_RESOURCES, DERIVE_RUN_LITERAL, DERIVE_STEP_LITERAL, } from "../tools/report-session/derive-table.js";
29
- /** The awaitExec callback. A derivation reports no live activity, thus the callback drops each event. */
24
+ /** The exec progress callback. A derivation reports no live activity, thus the callback drops each event. */
30
25
  const noopEmit = () => { };
31
26
  /**
32
27
  * The body, exported so a test drives it without a registered workflow.
@@ -39,9 +34,6 @@ const noopEmit = () => { };
39
34
  export async function runDeriveTableExecBody(input, deps) {
40
35
  const logger = (deps.logger ?? createNoopLogger()).named("derive-table-exec").with({ analysisId: input.analysisId });
41
36
  const workflowId = DBOS.workflowID ?? deriveTableExecWorkflowId(input.executionId);
42
- // One workflow submits one exec, thus the function id is a constant. The three segments are what a
43
- // callback host parses to find the workflow that awaits this exec.
44
- const execId = `${workflowId}:${DERIVE_STEP_LITERAL}:fn-0`;
45
37
  const spawned = keepSuspendingRefusal(await deps.sandboxClient.createSandbox(forStep(input.runSession, DERIVE_STEP_LITERAL), { childWorkflowId: workflowId, resources: DERIVATION_RESOURCES, writableTail: input.writableTail }, mintSandboxIdentity(DERIVE_RUN_LITERAL)));
46
38
  if (spawned.isErr()) {
47
39
  const suspension = suspensionOfSpawnRefusal(spawned.error);
@@ -50,17 +42,15 @@ export async function runDeriveTableExecBody(input, deps) {
50
42
  }
51
43
  const sandbox = spawned.value;
52
44
  try {
53
- // A checkpointed clock, not `Date.now()`: the await gates on this absolute deadline, and a
54
- // wall-clock deadline that grew on replay would shift which loop iteration crosses it.
45
+ // A checkpointed clock, not `Date.now()`: a wall-clock deadline would grow on each recovery.
55
46
  const deadline = (await (deps.now ?? (() => DBOS.now()))()) + DERIVATION_DEADLINE_MS;
56
- await deps.sandboxClient.submitExec(sandbox, buildDerivationExec({
47
+ const request = buildDerivationExec({
57
48
  script: input.script,
58
- execId,
59
49
  workingDir: input.workingDir,
60
50
  inputs: input.inputs,
61
51
  output: input.output,
62
- }));
63
- return ok(await deps.sandboxClient.awaitExec(sandbox, execId, noopEmit, deadline));
52
+ });
53
+ return ok(await deps.sandboxClient.exec(sandbox, request, noopEmit, deadline));
64
54
  }
65
55
  finally {
66
56
  try {
@@ -78,12 +68,7 @@ export async function runDeriveTableExecBody(input, deps) {
78
68
  export function registerDeriveTableExecWorkflow(deps) {
79
69
  return DBOS.registerWorkflow((input) => runDeriveTableExecBody(input, deps), { name: "derive-table-exec" });
80
70
  }
81
- /**
82
- * The workflow id of one derivation.
83
- *
84
- * It carries exactly one colon, because `workflowIdFromExec` recovers the id by a strip of the last two
85
- * colon-delimited segments of the exec id. The execution id is unique already, thus no nonce joins it.
86
- */
71
+ /** The workflow id of one derivation. The execution id is unique already, thus no nonce joins it. */
87
72
  export function deriveTableExecWorkflowId(executionId) {
88
73
  return `${DERIVE_RUN_LITERAL}:${executionId}`;
89
74
  }
@@ -18,7 +18,7 @@ import type { Logger } from "../lib/logger.js";
18
18
  import type { ExtractionArm, ExtractionArtifact, ExtractionRequest } from "../report-model/production-resolver.js";
19
19
  import type { SandboxClient } from "../sandbox/client.js";
20
20
  import { type Suspension } from "../workflows/suspension.js";
21
- import type { ExecResult, SubmitExecBody } from "../sandbox/types.js";
21
+ import type { ExecRequest, ExecResult } from "../sandbox/types.js";
22
22
  import { type ExtractValuesResult } from "./extract-values-script.js";
23
23
  export type { ExtractValuesResult } from "./extract-values-script.js";
24
24
  /**
@@ -60,7 +60,7 @@ export interface ExtractValuesWorkflowInput {
60
60
  *
61
61
  * The function is pure, thus a test asserts the command shape without a sandbox.
62
62
  */
63
- export declare function buildExtractionExec(analysisId: string, requests: readonly ExtractionRequest[], execId: string): SubmitExecBody;
63
+ export declare function buildExtractionExec(analysisId: string, requests: readonly ExtractionRequest[]): ExecRequest;
64
64
  /**
65
65
  * Parse the script output from the exec result. A synthetic failure, a timeout, and a non-zero exit each
66
66
  * throw, because each means the script did not emit its map. A stdout that is not the map shape throws too.
@@ -35,7 +35,7 @@ const EXTRACTION_DEADLINE_MS = 300_000;
35
35
  * carries no file size. A later change can size it from the batch.
36
36
  */
37
37
  const EXTRACTION_RESOURCES = { cpu: 2, memoryGb: 8 };
38
- /** The awaitExec callback. The extraction pass reports no live activity, thus the callback drops each event. */
38
+ /** The exec progress callback. The extraction pass reports no live activity, thus the callback drops each event. */
39
39
  const noopEmit = () => { };
40
40
  /**
41
41
  * Build the sandbox exec for one extraction pass. The command runs the fixed script through `python3 -c`.
@@ -50,11 +50,10 @@ const noopEmit = () => { };
50
50
  *
51
51
  * The function is pure, thus a test asserts the command shape without a sandbox.
52
52
  */
53
- export function buildExtractionExec(analysisId, requests, execId) {
53
+ export function buildExtractionExec(analysisId, requests) {
54
54
  const scriptRequests = requests.map((request) => ({ path: request.path, format: request.format, hash: request.hash }));
55
55
  return {
56
56
  command: ["python3", "-c", EXTRACTION_SCRIPT],
57
- execId,
58
57
  cwd: `/${analysisId}`,
59
58
  env: { [EXTRACTION_INPUT_ENV]: JSON.stringify(scriptRequests) },
60
59
  timeoutSeconds: Math.floor(EXTRACTION_DEADLINE_MS / 1000),
@@ -113,11 +112,9 @@ export async function runExtractValuesBody(input, deps) {
113
112
  }
114
113
  const sandbox = spawned.value;
115
114
  try {
116
- // A checkpointed clock, not `Date.now()`: the await gates on this absolute deadline, and a
117
- // wall-clock deadline that grew on replay would shift which loop iteration crosses it.
115
+ // A checkpointed clock, not `Date.now()`: a wall-clock deadline would grow on each recovery.
118
116
  const deadlineAbs = (await DBOS.now()) + EXTRACTION_DEADLINE_MS;
119
- await deps.sandboxClient.submitExec(sandbox, buildExtractionExec(analysisId, requests, executionId));
120
- const result = await deps.sandboxClient.awaitExec(sandbox, executionId, noopEmit, deadlineAbs);
117
+ const result = await deps.sandboxClient.exec(sandbox, buildExtractionExec(analysisId, requests), noopEmit, deadlineAbs);
121
118
  const map = parseExtractionOutput(result);
122
119
  await revoke("extract-values-completed");
123
120
  return ok(map);
@@ -8,7 +8,7 @@
8
8
  * captures its deps and calls `defineTool`.
9
9
  *
10
10
  * `ToolContext` carries only request-scoped values every tool may need
11
- * (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`, `turnUsage`) — no pool, no sandbox, no
11
+ * (`invocationId`, `session`, `signal`, `emit`, `ask`, `turnUsage`) — no pool, no sandbox, no
12
12
  * logger. The error contract: an expected
13
13
  * outcome ("not found", "no results") stays in the ok channel as a data
14
14
  * variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
@@ -19,7 +19,7 @@ import type { Result } from "neverthrow";
19
19
  import { z } from "zod";
20
20
  import type { AgentSession } from "../auth/types.js";
21
21
  import type { AgentRunUsage } from "../loop/metrics.js";
22
- import type { EmitFn, RunStep } from "../loop/types.js";
22
+ import type { EmitFn } from "../loop/types.js";
23
23
  import type { AskApproval, AskRequest } from "./approval/contract.js";
24
24
  export type { EmitFn };
25
25
  /**
@@ -35,7 +35,13 @@ export interface ToolError {
35
35
  readonly retryable: boolean;
36
36
  readonly cause?: unknown;
37
37
  }
38
- export type ToolExecutionMode = "step" | "workflow" | "inline";
38
+ /**
39
+ * How the loop runs a tool call. `step` (the default) wraps the whole call in
40
+ * one durable step, thus a replay returns the cached result and never runs the
41
+ * body again. `inline` runs the call with no step, for a tool whose effects are
42
+ * not durable work.
43
+ */
44
+ export type ToolExecutionMode = "step" | "inline";
39
45
  /** Runtime guard — does a `Result`'s error value carry the `ToolError` shape? */
40
46
  export declare function isToolError(value: unknown): value is ToolError;
41
47
  /**
@@ -78,13 +84,37 @@ export declare function withToolResultImages<T extends object>(value: T, images:
78
84
  * in the same shape.
79
85
  */
80
86
  export declare function readToolResultImages(value: unknown): ToolResultImage[];
87
+ /**
88
+ * The reserved key that carries the call record on a tool ok value.
89
+ *
90
+ * A call record is what a call leaves for process-local state, for example the
91
+ * lineage collector of a sandbox step. A replay returns the cached result of a
92
+ * step-mode call and never runs its body, thus a side effect of the body on
93
+ * such state is lost on recovery. The record goes into the cached dispatch
94
+ * instead, and the loop hands it to `foldCallRecord` after the call settles, on
95
+ * the first run and on each replay alike.
96
+ *
97
+ * It is a symbol, thus `JSON.stringify` omits it and the model never reads it.
98
+ * The record itself must be plain JSON, because the step cache serializes it.
99
+ */
100
+ export declare const toolCallRecordKey: unique symbol;
101
+ /** A tool ok value that carries a call record under the reserved key. */
102
+ export type WithToolCallRecord<T, R> = T & {
103
+ readonly [toolCallRecordKey]: R;
104
+ };
105
+ /** The type of the call record that a tool ok type carries. A variant with no record contributes nothing. */
106
+ export type ToolCallRecordOf<Output> = Output extends {
107
+ readonly [toolCallRecordKey]: infer R;
108
+ } ? R : never;
109
+ /** Attach a call record to a tool ok value. The value keeps its own fields. */
110
+ export declare function withToolCallRecord<T extends object, R>(value: T, record: R): WithToolCallRecord<T, R>;
111
+ /** Read the call record that a tool ok value carries, or `undefined` when it carries none. */
112
+ export declare function readToolCallRecord(value: unknown): unknown;
81
113
  /**
82
114
  * The request-scoped values passed to every tool's `execute`. No injected
83
115
  * dependencies (see the harness-durable-runtime spec) — invocation identity,
84
- * `session`, `signal`, `emit`, the `runStep`
85
- * durability seam (`passthroughStep` in chat, `DBOS.runStep` in workflows) a
86
- * tool uses to wrap its own durable work, the `ask` user-approval seam, and the
87
- * turn's usage accumulator a sub-agent-running tool hands to its child loop.
116
+ * `session`, `signal`, `emit`, the `ask` user-approval seam, and the turn's
117
+ * usage accumulator a sub-agent-running tool hands to its child loop.
88
118
  */
89
119
  export interface ToolContext {
90
120
  /** Stable identity of this AI SDK tool call. Redelivery preserves it; a new
@@ -93,11 +123,6 @@ export interface ToolContext {
93
123
  readonly session: AgentSession;
94
124
  readonly signal: AbortSignal;
95
125
  readonly emit: EmitFn;
96
- /**
97
- * Wrap durable work in a replay-cached step. The loop namespaces the name
98
- * under the tool's own step name, so a tool just passes a short local label.
99
- */
100
- readonly runStep: RunStep;
101
126
  /**
102
127
  * Pause for an explicit user decision on a concrete action. Resolves with the
103
128
  * approval (`once`/`always`) or throws `AskRejectedError` on denial. Resolves
@@ -117,7 +142,7 @@ export interface ToolContext {
117
142
  }
118
143
  /**
119
144
  * A packaged tool: identity, the Zod input contract, the emitted AI SDK input
120
- * schema, execution mode, the optional call-description hook, and the executor.
145
+ * schema, execution mode, the optional hooks, and the executor.
121
146
  */
122
147
  export interface Tool<Input = unknown, Output = unknown> {
123
148
  readonly id: string;
@@ -129,6 +154,11 @@ export interface Tool<Input = unknown, Output = unknown> {
129
154
  describeCall?(input: Input): string;
130
155
  /** See {@link ToolDefinition.describeResult}. Absent when the tool declares none. */
131
156
  describeResult?(input: Input, result: Output): string;
157
+ /**
158
+ * See {@link ToolDefinition.foldCallRecord}. The record arrives untyped: the
159
+ * loop passes back only what the `execute` of the same tool attached.
160
+ */
161
+ foldCallRecord?(record: unknown): void;
132
162
  execute(input: Input, ctx: ToolContext): Promise<Result<Output, ToolError>>;
133
163
  }
134
164
  export interface ToolDefinition<Schema extends z.ZodType, Output> {
@@ -199,6 +229,19 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
199
229
  * The hook never reaches the model.
200
230
  */
201
231
  readonly describeResult?: (input: z.infer<Schema>, result: Output) => string;
232
+ /**
233
+ * Fold the call record of one settled call into process-local state. The
234
+ * record is the one that `execute` attached with `withToolCallRecord`.
235
+ *
236
+ * The loop runs the hook after the round settles, in the order of the tool
237
+ * calls, on the first run and on each replay alike. Thus state that a
238
+ * replay rebuilds from scratch sees the same records in the same order. A
239
+ * call whose ok value carries no record, and a failed call, never reach it.
240
+ *
241
+ * The hook is synchronous. It must never fail a call: the call has settled,
242
+ * and its result may already be cached. The loop logs a throw and goes on.
243
+ */
244
+ readonly foldCallRecord?: (record: ToolCallRecordOf<Output>) => void;
202
245
  execute(input: z.infer<Schema>, ctx: ToolContext): Promise<Result<Output, ToolError>>;
203
246
  }
204
247
  /**
@@ -8,7 +8,7 @@
8
8
  * captures its deps and calls `defineTool`.
9
9
  *
10
10
  * `ToolContext` carries only request-scoped values every tool may need
11
- * (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`, `turnUsage`) — no pool, no sandbox, no
11
+ * (`invocationId`, `session`, `signal`, `emit`, `ask`, `turnUsage`) — no pool, no sandbox, no
12
12
  * logger. The error contract: an expected
13
13
  * outcome ("not found", "no results") stays in the ok channel as a data
14
14
  * variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
@@ -62,6 +62,30 @@ export function readToolResultImages(value) {
62
62
  const entries = Array.isArray(carried) ? carried : [carried];
63
63
  return entries.filter(isToolResultImage);
64
64
  }
65
+ /**
66
+ * The reserved key that carries the call record on a tool ok value.
67
+ *
68
+ * A call record is what a call leaves for process-local state, for example the
69
+ * lineage collector of a sandbox step. A replay returns the cached result of a
70
+ * step-mode call and never runs its body, thus a side effect of the body on
71
+ * such state is lost on recovery. The record goes into the cached dispatch
72
+ * instead, and the loop hands it to `foldCallRecord` after the call settles, on
73
+ * the first run and on each replay alike.
74
+ *
75
+ * It is a symbol, thus `JSON.stringify` omits it and the model never reads it.
76
+ * The record itself must be plain JSON, because the step cache serializes it.
77
+ */
78
+ export const toolCallRecordKey = Symbol("toolCallRecord");
79
+ /** Attach a call record to a tool ok value. The value keeps its own fields. */
80
+ export function withToolCallRecord(value, record) {
81
+ return { ...value, [toolCallRecordKey]: record };
82
+ }
83
+ /** Read the call record that a tool ok value carries, or `undefined` when it carries none. */
84
+ export function readToolCallRecord(value) {
85
+ if (typeof value !== "object" || value === null)
86
+ return undefined;
87
+ return value[toolCallRecordKey];
88
+ }
65
89
  /**
66
90
  * Package a `Tool`. The emitted JSON Schema must have a top-level
67
91
  * `"type": "object"` — model tool calling rejects anything else. A `z.discriminatedUnion`
@@ -95,6 +119,9 @@ export function defineTool(def) {
95
119
  // declares no result hook carries no key, thus "has a hook" is one
96
120
  // property check on either side.
97
121
  ...(typeof def.describeResult === "function" ? { describeResult: def.describeResult } : {}),
122
+ // The loop passes back only a record that this tool's `execute`
123
+ // attached, thus the record has the declared type.
124
+ ...(typeof def.foldCallRecord === "function" ? { foldCallRecord: def.foldCallRecord } : {}),
98
125
  execute: def.execute,
99
126
  };
100
127
  }
@@ -274,7 +274,7 @@ export function buildAdHocPlan(request, route, createdAt = new Date().toISOStrin
274
274
  * would pay a file read on every ad hoc launch to learn nothing — while the
275
275
  * router, with no resolution, already keeps every entry that parses.
276
276
  */
277
- function adHocPackageResolver(deps) {
277
+ function adHocPackageResolver(deps, analysisId) {
278
278
  if (!deps.farmLockFile && !deps.imagePackagesFile && !deps.readPoolInventory)
279
279
  return undefined;
280
280
  const inventory = {
@@ -283,7 +283,7 @@ function adHocPackageResolver(deps) {
283
283
  ...(deps.readPoolInventory === undefined ? {} : { readPoolInventory: deps.readPoolInventory }),
284
284
  };
285
285
  return async (names) => {
286
- const read = await readInventorySections(inventory);
286
+ const read = await readInventorySections(inventory, analysisId);
287
287
  // An inventory that cannot answer must not drop a name: the router
288
288
  // keeps every parsed entry, and the link pass judges at launch.
289
289
  if (read.kind === "unavailable")
@@ -296,7 +296,7 @@ async function persistedAdHocPlan(deps, args) {
296
296
  const existing = unwrapOrThrow(await loadPlan(deps.pool, args.planId, { analysisId: args.analysisId }));
297
297
  if (existing)
298
298
  return validateStoredPlan(existing, args.planId);
299
- const resolvePackages = adHocPackageResolver(deps);
299
+ const resolvePackages = adHocPackageResolver(deps, args.analysisId);
300
300
  const route = await routeAdHocRequest({
301
301
  provider: deps.utilityProvider,
302
302
  model: deps.utilityModel,
@@ -29,9 +29,9 @@
29
29
  * condition one time, up front, the same discipline as the eyes. A per-attempt failure would instead read as
30
30
  * a transient fault, and it would invite a repeat of a call that can never pass.
31
31
  *
32
- * The container runs behind a seam, and never in the turn. An await of an exec is a workflow-body call
33
- * under the callback transport, and a report turn is not a body. Thus the tool takes an injected runner,
34
- * and a registered workflow owns the container. The tool holds no sandbox client, and it imports no DBOS.
32
+ * The container runs behind a seam, and never in the turn. An exec is a durable step of a workflow, and a
33
+ * report turn is not a workflow. Thus the tool takes an injected runner, and a registered workflow owns the
34
+ * container. The tool holds no sandbox client, and it imports no DBOS.
35
35
  */
36
36
  import { type Result } from "neverthrow";
37
37
  import { z } from "zod";
@@ -40,7 +40,7 @@ import type { Suspension } from "../../workflows/suspension.js";
40
40
  import type { RunAuthorizer } from "../../execution/run-authorizer.js";
41
41
  import { type Logger } from "../../lib/logger.js";
42
42
  import type { ResourceSpec } from "../../config/resource-limits.js";
43
- import type { ExecResult, SubmitExecBody } from "../../sandbox/types.js";
43
+ import type { ExecRequest, ExecResult } from "../../sandbox/types.js";
44
44
  import type { DerivationSource, ReportSessionStateStore } from "../../state/report-session-state.js";
45
45
  import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
46
46
  import { type Tool } from "../define-tool.js";
@@ -156,11 +156,10 @@ export interface DerivationInputMount {
156
156
  */
157
157
  export declare function buildDerivationExec(args: {
158
158
  readonly script: string;
159
- readonly execId: string;
160
159
  readonly workingDir: string;
161
160
  readonly inputs: readonly DerivationInputMount[];
162
161
  readonly output: string;
163
- }): SubmitExecBody;
162
+ }): ExecRequest;
164
163
  /**
165
164
  * Give the failure of an exec result, or `undefined` when the script ran to a clean exit.
166
165
  *
@@ -190,9 +189,9 @@ export interface DeriveTableExecInput {
190
189
  /**
191
190
  * Run one derivation exec and give the terminal result back.
192
191
  *
193
- * The composition realizes it over a registered workflow, thus the container lives inside a workflow body
194
- * and the await is legal under each transport. A fault of the sandbox rejects the promise, and the tool
195
- * turns that rejection into one short detail.
192
+ * The composition realizes it over a registered workflow, thus the container and its exec live inside a
193
+ * workflow. A fault of the sandbox rejects the promise, and the tool turns that rejection into one short
194
+ * detail.
196
195
  */
197
196
  export type DeriveTableRunner = (input: DeriveTableExecInput) => Promise<Result<ExecResult, Suspension>>;
198
197
  /**
@@ -29,9 +29,9 @@
29
29
  * condition one time, up front, the same discipline as the eyes. A per-attempt failure would instead read as
30
30
  * a transient fault, and it would invite a repeat of a call that can never pass.
31
31
  *
32
- * The container runs behind a seam, and never in the turn. An await of an exec is a workflow-body call
33
- * under the callback transport, and a report turn is not a body. Thus the tool takes an injected runner,
34
- * and a registered workflow owns the container. The tool holds no sandbox client, and it imports no DBOS.
32
+ * The container runs behind a seam, and never in the turn. An exec is a durable step of a workflow, and a
33
+ * report turn is not a workflow. Thus the tool takes an injected runner, and a registered workflow owns the
34
+ * container. The tool holds no sandbox client, and it imports no DBOS.
35
35
  */
36
36
  import { ok } from "neverthrow";
37
37
  import { unlink } from "node:fs/promises";
@@ -90,7 +90,6 @@ const NO_SANDBOX_DETAIL = "the composition gives no sandbox, thus this session c
90
90
  export function buildDerivationExec(args) {
91
91
  return {
92
92
  command: ["python3", "-c", args.script],
93
- execId: args.execId,
94
93
  cwd: args.workingDir,
95
94
  env: {
96
95
  [DERIVE_INPUT_ENV]: JSON.stringify(args.inputs.map((input) => ({ path: input.path, hash: input.hash }))),
@@ -52,7 +52,7 @@ import { referencedPaths, walkBlocks } from "../../report-model/block-walk.js";
52
52
  import { computeDraftHash } from "../../report-model/draft-hash.js";
53
53
  import { finishDraft } from "../../report-model/draft-finish.js";
54
54
  import { resolveDocumentReferences } from "../../report-model/validate.js";
55
- import { bridgeValues } from "../../report-render/value-bridge.js";
55
+ import { bridgeValues, collectResolutions } from "../../report-render/value-bridge.js";
56
56
  import { resolvePageAssetFromInstallation } from "../../report-render/asset-lookup.js";
57
57
  import { ASSETS_DIR, DEPS_DIR, derivationScriptName, PAGE_ASSETS, stagedSource, tableSidecarName } from "../../report-render/assets.js";
58
58
  import { renderReportPage } from "../../report-render/render.js";
@@ -77,51 +77,6 @@ function assetFileName(file) {
77
77
  function figureSourcePolicy(file) {
78
78
  return `assets/${assetFileName(file)}`;
79
79
  }
80
- /**
81
- * Pair each block with the resolved value of its binding, in document order.
82
- *
83
- * A value-bearing kind carries the resolved value of its one reference, looked up by the block id from
84
- * the map that the resolution pass filled. A no-value kind carries none. Where a reference sits in a
85
- * block is the knowledge of `block-walk.ts`, thus this walk reads the block id alone and never a binding
86
- * field.
87
- *
88
- * The switch is exhaustive over the eight block kinds. A ninth kind reaches the end with no return, and
89
- * the declared return type fails the build. Thus the walk cannot drop a kind in silence. A value-bearing
90
- * block reaches here only when its reference resolved, because an unresolved reference short-circuits
91
- * before this walk.
92
- */
93
- function collectResolutions(blocks, resolvedByBlock) {
94
- const resolutions = [];
95
- const visit = (block) => {
96
- switch (block.kind) {
97
- case "section":
98
- resolutions.push({ blockId: block.id, kind: "section" });
99
- for (const child of block.blocks) {
100
- visit(child);
101
- }
102
- return;
103
- case "text":
104
- case "claim":
105
- case "citation":
106
- resolutions.push({ blockId: block.id, kind: block.kind });
107
- return;
108
- case "metric":
109
- case "table":
110
- case "chart":
111
- case "figure": {
112
- const resolved = resolvedByBlock.get(block.id);
113
- if (resolved !== undefined) {
114
- resolutions.push({ blockId: block.id, kind: block.kind, resolved });
115
- }
116
- return;
117
- }
118
- }
119
- };
120
- for (const block of blocks) {
121
- visit(block);
122
- }
123
- return resolutions;
124
- }
125
80
  /**
126
81
  * The sidecar of each table block of the document, in document order.
127
82
  *
@@ -201,11 +156,11 @@ function scriptAssets(records) {
201
156
  * references. On a clean pass the resolutions map onto the render model in document order.
202
157
  */
203
158
  async function resolveDocument(document, resolver, snapshot) {
204
- const { resolvedByBlock, failures } = await resolveDocumentReferences(document.sections, snapshot, resolver);
159
+ const { resolvedByBlock, resolvedBySlot, failures } = await resolveDocumentReferences(document.sections, snapshot, resolver);
205
160
  if (failures.length > 0) {
206
161
  return { resolutions: [], unresolved: failures };
207
162
  }
208
- return { resolutions: collectResolutions(document.sections, resolvedByBlock), unresolved: [] };
163
+ return { resolutions: collectResolutions(document.sections, resolvedByBlock, resolvedBySlot), unresolved: [] };
209
164
  }
210
165
  /**
211
166
  * Resolve the page root, stage each bound image and each manifest entry, and write the page.
@@ -456,7 +411,7 @@ export function createPreviewReportTool(deps) {
456
411
  // the renderer moves it into a data asset and no part of this path reads a field of it. Absence
457
412
  // gives a page with no provenance asset, and each other part of the render is unchanged.
458
413
  const provenance = await readProvenance(analysisId);
459
- const rendered = renderReportPage(document, bridged.value, {
414
+ const rendered = await renderReportPage(document, bridged.value, {
460
415
  records: snapshot.citationRecords,
461
416
  derivations: used.map((record) => chainOf(record, sessionDir)),
462
417
  provenance,
@@ -140,7 +140,7 @@ export type InventoryRead = {
140
140
  * here, thus the census a model sees and the check a launch runs never
141
141
  * disagree about what the source holds.
142
142
  */
143
- export declare function readInventorySections(deps: ListAvailablePackagesDeps): Promise<InventoryRead>;
143
+ export declare function readInventorySections(deps: ListAvailablePackagesDeps, analysisId: string): Promise<InventoryRead>;
144
144
  /**
145
145
  * Create the package inventory over the bound source: the pool-scope reader
146
146
  * when the embedder binds one (a conversation or planning surface), the
@@ -400,7 +400,7 @@ export function queryPackages(rawSections, { names, query, language, limit }) {
400
400
  * here, thus the census a model sees and the check a launch runs never
401
401
  * disagree about what the source holds.
402
402
  */
403
- export async function readInventorySections(deps) {
403
+ export async function readInventorySections(deps, analysisId) {
404
404
  // The image record merges in when the host can read a valid one. An absent
405
405
  // record and an invalid record both merge nothing, and neither is an
406
406
  // error: the record is an enrichment, and the farm or pool inventory stays
@@ -422,8 +422,9 @@ export async function readInventorySections(deps) {
422
422
  }
423
423
  // Both farm container paths are tried when the host injects none, because
424
424
  // the path keys on the declared toolchain and this read carries none.
425
+ const farmLockFile = typeof deps.farmLockFile === "function" ? deps.farmLockFile(analysisId) : deps.farmLockFile;
425
426
  let lock = null;
426
- for (const candidate of deps.farmLockFile ? [deps.farmLockFile] : DEFAULT_FARM_LOCK_FILES) {
427
+ for (const candidate of farmLockFile ? [farmLockFile] : DEFAULT_FARM_LOCK_FILES) {
427
428
  lock = readFarmLockFile(candidate).unwrapOr(null);
428
429
  if (lock !== null)
429
430
  break;
@@ -502,8 +503,8 @@ export function createListAvailablePackagesTool(deps = {}) {
502
503
  return `${language} packages`;
503
504
  return "full package list";
504
505
  },
505
- execute: async (input) => {
506
- const read = await readInventorySections(deps);
506
+ execute: async (input, ctx) => {
507
+ const read = await readInventorySections(deps, ctx.session.scope.analysisId);
507
508
  // An unreadable inventory is an expected environment state — model it as an
508
509
  // `available: false` data variant telling the caller the set is UNKNOWN,
509
510
  // WITH the reason: without it, a structural fault (a damaged dependency
@@ -11,10 +11,11 @@
11
11
  * (compiled with the `gm` flags — multiline anchors), `new_string` may use
12
12
  * capture-group references, and exactly one of `replace_all=true` or
13
13
  * `expected_matches` gates the write. Provenance hashes the post-edit
14
- * content — recorded by the mutator.
14
+ * content — the mutator records it through the fold of this tool.
15
15
  */
16
+ import { type WithToolCallRecord } from "../define-tool.js";
16
17
  import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
17
- import type { WorkspaceMutator, WriteFileResult } from "./mutator.js";
18
+ import type { WorkspaceMutator, WriteFileResult, WriteRecord } from "./mutator.js";
18
19
  /** Outcome of an `edit_file` call — expected outcomes are data variants, never throws. */
19
20
  export type EditFileResult = {
20
21
  readonly status: "file_not_found";
@@ -43,14 +44,14 @@ export type EditFileResult = {
43
44
  readonly lines: readonly number[];
44
45
  } | Exclude<WriteFileResult, {
45
46
  status: "ok";
46
- }> | {
47
+ }> | WithToolCallRecord<{
47
48
  readonly status: "ok";
48
49
  readonly path: string;
49
50
  readonly replacements: number;
50
51
  readonly bytesWritten: number;
51
52
  /** Regex mode only: 1-based line numbers where the matches started. */
52
53
  readonly lines?: readonly number[];
53
- };
54
+ }, WriteRecord>;
54
55
  export interface EditFileDeps {
55
56
  readonly mutator: WorkspaceMutator;
56
57
  /** Read seam used to fetch current file content for search/replace. */
@@ -11,11 +11,11 @@
11
11
  * (compiled with the `gm` flags — multiline anchors), `new_string` may use
12
12
  * capture-group references, and exactly one of `replace_all=true` or
13
13
  * `expected_matches` gates the write. Provenance hashes the post-edit
14
- * content — recorded by the mutator.
14
+ * content — the mutator records it through the fold of this tool.
15
15
  */
16
16
  import { ok } from "neverthrow";
17
17
  import { z } from "zod";
18
- import { defineTool } from "../define-tool.js";
18
+ import { defineTool, withToolCallRecord } from "../define-tool.js";
19
19
  import { unwrapOrThrow } from "../../lib/result.js";
20
20
  const EditFileInputSchema = z.object({
21
21
  path: z
@@ -108,9 +108,6 @@ function replaceRegex(content, pattern, replacement, expected) {
108
108
  export function createEditFileTool(deps) {
109
109
  return defineTool({
110
110
  id: "edit_file",
111
- // The mutator wraps the disk mutation in `ctx.runStep` itself, so the
112
- // body runs unwrapped in the workflow body (see the harness-tools spec).
113
- executionMode: "workflow",
114
111
  description: "Edit a file in your working directory by replacing specific text. " +
115
112
  "Read the file first to get the exact text. When replace_all is false " +
116
113
  "(default), old_string must occur exactly once. For a bulk pattern " +
@@ -166,24 +163,24 @@ export function createEditFileTool(deps) {
166
163
  lines: replaced.lines,
167
164
  });
168
165
  }
169
- const result = await deps.mutator.writeFile({
166
+ const write = await deps.mutator.writeFile({
170
167
  path,
171
168
  content: replaced.content,
172
169
  toolName: "edit_file",
173
170
  invocationId: ctx.invocationId,
174
- runStep: ctx.runStep,
175
171
  session: ctx.session,
176
172
  });
177
- if (result.status !== "ok")
178
- return ok(result);
173
+ if (write.status !== "ok")
174
+ return ok(write);
179
175
  const lines = replaced.lines;
180
- return ok({
176
+ return ok(withToolCallRecord({
181
177
  status: "ok",
182
- path: result.path,
178
+ path: write.path,
183
179
  replacements: replaced.replacements,
184
- bytesWritten: result.bytesWritten,
180
+ bytesWritten: write.bytesWritten,
185
181
  ...(lines === undefined ? {} : { lines }),
186
- });
182
+ }, write.record));
187
183
  },
184
+ foldCallRecord: (record) => deps.mutator.recordWrite(record),
188
185
  });
189
186
  }