@inflexa-ai/harness 0.39.1 → 0.40.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 (191) hide show
  1. package/README.md +2 -3
  2. package/dist/agents/conversation-agent.d.ts +3 -0
  3. package/dist/agents/conversation-agent.js +13 -2
  4. package/dist/agents/report-session-agent.d.ts +3 -0
  5. package/dist/agents/report-session-agent.js +2 -0
  6. package/dist/agents/sandbox/cheminformatics-agent.js +0 -3
  7. package/dist/agents/sandbox/shared.d.ts +11 -2
  8. package/dist/agents/sandbox/shared.js +4 -0
  9. package/dist/app/chat-turn.d.ts +75 -12
  10. package/dist/app/chat-turn.js +149 -21
  11. package/dist/app/message-assembly.d.ts +16 -52
  12. package/dist/app/message-assembly.js +41 -68
  13. package/dist/app/run-activity.d.ts +4 -6
  14. package/dist/app/run-activity.js +7 -27
  15. package/dist/app/spawn-report-session.d.ts +6 -11
  16. package/dist/app/spawn-report-session.js +5 -6
  17. package/dist/app/synthesize-run.d.ts +3 -0
  18. package/dist/app/synthesize-run.js +1 -0
  19. package/dist/billing/usage-recorder.d.ts +3 -4
  20. package/dist/billing/usage-recorder.js +3 -4
  21. package/dist/contracts/chat-parts.d.ts +11 -1
  22. package/dist/contracts/chat-parts.js +1 -0
  23. package/dist/contracts/index.d.ts +1 -1
  24. package/dist/contracts/part-registry.js +1 -0
  25. package/dist/contracts/report-blocks.d.ts +8 -8
  26. package/dist/contracts/schemas/chat-parts.d.ts +32 -9
  27. package/dist/contracts/schemas/chat-parts.js +10 -0
  28. package/dist/execution/artifact-metadata.d.ts +26 -33
  29. package/dist/execution/artifact-metadata.js +44 -106
  30. package/dist/execution/artifact-registration.d.ts +12 -2
  31. package/dist/execution/artifact-registration.js +9 -4
  32. package/dist/execution/post-step-pipeline.d.ts +26 -22
  33. package/dist/execution/post-step-pipeline.js +30 -24
  34. package/dist/execution/run-synthesis.d.ts +3 -0
  35. package/dist/execution/run-synthesis.js +11 -1
  36. package/dist/execution/step-summary.d.ts +21 -30
  37. package/dist/execution/step-summary.js +30 -57
  38. package/dist/index.d.ts +11 -10
  39. package/dist/index.js +16 -35
  40. package/dist/lib/zod-issues.js +2 -4
  41. package/dist/loop/compaction.d.ts +20 -0
  42. package/dist/loop/compaction.js +2 -0
  43. package/dist/loop/continue-agent.d.ts +40 -0
  44. package/dist/loop/continue-agent.js +38 -0
  45. package/dist/loop/metrics.d.ts +30 -22
  46. package/dist/loop/metrics.js +51 -22
  47. package/dist/loop/run-agent.d.ts +111 -13
  48. package/dist/loop/run-agent.js +467 -220
  49. package/dist/loop/run-to-terminal.d.ts +16 -17
  50. package/dist/loop/run-to-terminal.js +24 -35
  51. package/dist/loop/tool-mask.d.ts +23 -0
  52. package/dist/loop/tool-mask.js +46 -0
  53. package/dist/loop/tool-output.d.ts +48 -0
  54. package/dist/loop/tool-output.js +79 -0
  55. package/dist/loop/types.d.ts +6 -3
  56. package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
  57. package/dist/memory/ai-sdk-message-storage.js +78 -2
  58. package/dist/memory/conversation-display-recorder.d.ts +6 -0
  59. package/dist/memory/conversation-display-recorder.js +64 -22
  60. package/dist/memory/conversation-display-replay.d.ts +5 -14
  61. package/dist/memory/conversation-display-replay.js +66 -32
  62. package/dist/memory/conversation-display-storage.d.ts +2 -1
  63. package/dist/memory/conversation-display-storage.js +2 -1
  64. package/dist/memory/conversation-view.d.ts +53 -0
  65. package/dist/memory/conversation-view.js +130 -0
  66. package/dist/memory/count-tokens.d.ts +5 -7
  67. package/dist/memory/count-tokens.js +7 -9
  68. package/dist/memory/thread-history.d.ts +74 -102
  69. package/dist/memory/thread-history.js +181 -246
  70. package/dist/memory/thread-store.d.ts +3 -3
  71. package/dist/memory/thread-store.js +7 -2
  72. package/dist/memory/tool-call-integrity.d.ts +15 -21
  73. package/dist/memory/tool-call-integrity.js +31 -38
  74. package/dist/memory/working-memory.d.ts +2 -5
  75. package/dist/memory/working-memory.js +2 -5
  76. package/dist/prompts/SOUL.d.ts +2 -2
  77. package/dist/prompts/SOUL.js +2 -7
  78. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  79. package/dist/prompts/analogical-reasoner.js +16 -32
  80. package/dist/prompts/briefing.js +1 -1
  81. package/dist/prompts/compaction.d.ts +4 -0
  82. package/dist/prompts/compaction.js +24 -0
  83. package/dist/prompts/conversation.d.ts +1 -1
  84. package/dist/prompts/conversation.js +26 -35
  85. package/dist/prompts/execute-analysis/step-summary.js +7 -5
  86. package/dist/prompts/literature-reviewer.d.ts +1 -1
  87. package/dist/prompts/literature-reviewer.js +29 -34
  88. package/dist/prompts/planner.js +15 -29
  89. package/dist/prompts/report-session.d.ts +1 -1
  90. package/dist/prompts/report-session.js +2 -33
  91. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
  93. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
  95. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  96. package/dist/prompts/sandbox/chromatin-agent.js +1 -2
  97. package/dist/prompts/sandbox/data-profiler.js +6 -3
  98. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  99. package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
  100. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  101. package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
  102. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  103. package/dist/prompts/sandbox/enrichment-agent.js +3 -2
  104. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  105. package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
  106. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  107. package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
  108. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  109. package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
  110. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  111. package/dist/prompts/sandbox/microbiome-agent.js +14 -21
  112. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  113. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
  114. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  115. package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
  116. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  117. package/dist/prompts/sandbox/network-agent.js +7 -8
  118. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  119. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
  120. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  121. package/dist/prompts/sandbox/proteomics-agent.js +3 -4
  122. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  123. package/dist/prompts/sandbox/scientific-executor.js +7 -10
  124. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  125. package/dist/prompts/sandbox/single-cell-agent.js +2 -4
  126. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  127. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
  128. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  129. package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
  130. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  131. package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
  132. package/dist/prompts/sandbox-standards.d.ts +7 -7
  133. package/dist/prompts/sandbox-standards.js +74 -76
  134. package/dist/prompts/synthesis-agent.d.ts +1 -1
  135. package/dist/prompts/synthesis-agent.js +16 -42
  136. package/dist/providers/ai-sdk.d.ts +39 -6
  137. package/dist/providers/ai-sdk.js +117 -18
  138. package/dist/providers/errors.d.ts +2 -0
  139. package/dist/providers/errors.js +5 -0
  140. package/dist/providers/prompt-cache.d.ts +43 -41
  141. package/dist/providers/prompt-cache.js +47 -41
  142. package/dist/providers/reasoning.d.ts +10 -21
  143. package/dist/providers/reasoning.js +10 -21
  144. package/dist/providers/types.d.ts +21 -5
  145. package/dist/report-model/authoring-grammar.d.ts +2 -2
  146. package/dist/report-model/pin-snapshot.js +4 -10
  147. package/dist/runtime/assemble.d.ts +9 -7
  148. package/dist/runtime/assemble.js +7 -3
  149. package/dist/sandbox/create-sandbox.d.ts +2 -2
  150. package/dist/sandbox/submit-exec.d.ts +1 -1
  151. package/dist/schemas/artifact-manifest.d.ts +4 -4
  152. package/dist/state/artifacts.d.ts +11 -9
  153. package/dist/state/artifacts.js +112 -93
  154. package/dist/state/data-profile.d.ts +2 -2
  155. package/dist/state/data-profile.js +2 -2
  156. package/dist/state/init.js +39 -4
  157. package/dist/state/purge-analysis.js +4 -0
  158. package/dist/state/tool-outputs.d.ts +4 -0
  159. package/dist/state/tool-outputs.js +30 -0
  160. package/dist/tasks/data-profile.d.ts +31 -10
  161. package/dist/tasks/data-profile.js +116 -95
  162. package/dist/tools/ad-hoc-router.d.ts +8 -0
  163. package/dist/tools/ad-hoc-router.js +21 -2
  164. package/dist/tools/display/show-user.d.ts +1 -1
  165. package/dist/tools/execute-analysis.d.ts +3 -0
  166. package/dist/tools/execute-analysis.js +9 -1
  167. package/dist/tools/read-tool-output.d.ts +7 -0
  168. package/dist/tools/read-tool-output.js +130 -0
  169. package/dist/tools/research/generate-analogy-report.d.ts +10 -26
  170. package/dist/tools/research/generate-analogy-report.js +76 -163
  171. package/dist/tools/research/generate-plan.d.ts +3 -0
  172. package/dist/tools/research/generate-plan.js +4 -1
  173. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  174. package/dist/tools/research/literature-reviewer.js +3 -0
  175. package/dist/tools/sandbox/report-blocker.d.ts +18 -5
  176. package/dist/tools/sandbox/report-blocker.js +54 -6
  177. package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
  178. package/dist/tools/sandbox/submit-file-metadata.js +67 -0
  179. package/dist/tools/start-report-session.d.ts +1 -3
  180. package/dist/tools/start-report-session.js +0 -2
  181. package/dist/tools/workspace/execute-command.js +7 -4
  182. package/dist/tools/workspace/result-bounds.d.ts +4 -4
  183. package/dist/tools/workspace/result-bounds.js +6 -5
  184. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  185. package/dist/workflows/execute-analysis.d.ts +3 -0
  186. package/dist/workflows/execute-analysis.js +6 -3
  187. package/dist/workflows/sandbox-step.d.ts +30 -9
  188. package/dist/workflows/sandbox-step.js +37 -8
  189. package/package.json +9 -9
  190. package/dist/providers/anthropic.d.ts +0 -25
  191. package/dist/providers/anthropic.js +0 -28
@@ -1,34 +1,23 @@
1
1
  /**
2
- * The reasoning depth of a run.
2
+ * The reasoning depth of a call.
3
3
  *
4
- * The harness states the depth one time, on `ChatRequest.reasoning`, with the
5
- * neutral names of the AI SDK. It names no vendor key. Each provider package
6
- * holds the table of what its own models accept, and it maps the neutral name
7
- * onto the wire. The Anthropic package also selects adaptive thinking there.
4
+ * The effort resolves in order: `ChatRequest.reasoning` (set only through
5
+ * `RunAgentOptions.reasoning`), the provider configuration's `reasoning`, then
6
+ * {@link DEFAULT_REASONING}. Anthropic discards its message cache when the
7
+ * top-level effort changes between calls.
8
8
  *
9
- * ## Why the harness names no vendor key
10
- *
11
- * The harness wrote `providerOptions.anthropic.effort` before. A value on that
12
- * key turns the per-model table of the Anthropic package off, thus the raw name
13
- * reached the wire. A model that accepts `high` but not `xhigh` answered 400.
14
- * The neutral field has no such hazard, because the package resolves the name
15
- * for the model that it is bound to.
16
- *
17
- * ## Where the policy belongs
18
- *
19
- * The policy rides on the run, not on the provider, for the same reason that the
20
- * cache policy does. A one-shot LLM call elsewhere in the harness has its own
21
- * depth needs, and it must not inherit the depth of an agent loop.
9
+ * Do not set `providerOptions.anthropic.effort` directly. It bypasses the
10
+ * per-model table, and a model that rejects the raw name answers 400.
22
11
  */
23
12
  /**
24
- * The default policy: the deepest name of the neutral ladder.
13
+ * The last source of the effort: the deepest rung of the neutral ladder.
25
14
  *
26
15
  * Each agent of the harness drives tools over many iterations. A shallow turn
27
16
  * there wastes more calls than a deeper turn costs in tokens. The Anthropic
28
17
  * package sends `xhigh` to a model that accepts it, and `max` to a model that
29
18
  * does not. The OpenAI-compatible package sends the name as it is.
30
19
  *
31
- * A host that wants a cheaper loop passes a lower name. A host on a model with
32
- * no reasoning support passes `"provider-default"`.
20
+ * A host that wants a cheaper role sets a lower name on that role's provider
21
+ * configuration. With no reasoning support, a host sets `"provider-default"`.
33
22
  */
34
23
  export const DEFAULT_REASONING = "xhigh";
@@ -5,7 +5,7 @@
5
5
  * vendor-neutral interfaces. Every call carries an `AgentSession`; billing
6
6
  * attribution is resolved lazily at the provider wire boundary.
7
7
  */
8
- import type { FinishReason, LanguageModel, ModelMessage, ToolSet } from "ai";
8
+ import type { FinishReason, LanguageModel, ModelMessage, SystemModelMessage, ToolSet } from "ai";
9
9
  import type { LanguageModelV4CallOptions } from "@ai-sdk/provider";
10
10
  import type { ProviderOptions } from "@ai-sdk/provider-utils";
11
11
  import type { ResultAsync } from "neverthrow";
@@ -32,19 +32,30 @@ export interface ProviderCapabilities {
32
32
  */
33
33
  readonly imageUserMessages?: boolean;
34
34
  }
35
+ /**
36
+ * The request carries no free-form provider options. That would let a caller
37
+ * set a vendor key that bypasses the provider's per-model table.
38
+ */
35
39
  export interface ChatRequest {
36
- readonly system: string;
40
+ /**
41
+ * The system prompt: a plain string, or a system message that carries the
42
+ * cache marker at its end (`withSystemPromptBreakpoint`).
43
+ */
44
+ readonly system: string | SystemModelMessage;
37
45
  readonly messages: readonly ModelMessage[];
38
46
  readonly tools: ToolSet;
39
47
  /**
40
48
  * A forced choice (`required`, or one named tool) is absent on purpose: some
41
49
  * Claude models reject it with a 400, and the model is chosen at run time.
50
+ *
51
+ * CAUTION: `@ai-sdk/anthropic` strips the tools when this is `"none"`, and
52
+ * Anthropic drops its cache on any `tool_choice` change — taking a signed
53
+ * thinking block with it. Use a tool mask instead of sending `"none"`.
42
54
  */
43
55
  readonly toolChoice?: "auto" | "none";
44
- readonly providerOptions?: ProviderOptions;
45
56
  /**
46
- * How deep the model reasons on this call. Absent sends no directive, thus
47
- * the model applies its own default. Refer to `ReasoningPolicy`.
57
+ * How deep the model reasons. Absent, the effort falls back to the provider
58
+ * configuration's `reasoning`, then `DEFAULT_REASONING`.
48
59
  */
49
60
  readonly reasoning?: ReasoningPolicy;
50
61
  }
@@ -139,6 +150,11 @@ export interface ChatResponse {
139
150
  * diagnostics, never as an invariant.
140
151
  */
141
152
  readonly servedModelId?: string;
153
+ /**
154
+ * The AI SDK's `LanguageModel.provider` id, for example `anthropic.messages`.
155
+ * Absent for a bare model-id string, which names no provider.
156
+ */
157
+ readonly provider?: string;
142
158
  }
143
159
  export type ChatStreamEvent = {
144
160
  readonly type: "text-delta";
@@ -398,15 +398,15 @@ declare const AUTHORING_ATOM_SCHEMAS: readonly [z.ZodObject<{
398
398
  x: z.ZodOptional<z.ZodObject<{
399
399
  title: z.ZodOptional<z.ZodString>;
400
400
  scale: z.ZodOptional<z.ZodEnum<{
401
- linear: "linear";
402
401
  log: "log";
402
+ linear: "linear";
403
403
  }>>;
404
404
  }, z.core.$strict>>;
405
405
  y: z.ZodOptional<z.ZodObject<{
406
406
  title: z.ZodOptional<z.ZodString>;
407
407
  scale: z.ZodOptional<z.ZodEnum<{
408
- linear: "linear";
409
408
  log: "log";
409
+ linear: "linear";
410
410
  }>>;
411
411
  }, z.core.$strict>>;
412
412
  }, z.core.$strict>>;
@@ -210,19 +210,13 @@ async function collectCitations(pool, resolveWorkspaceRoot, analysisId) {
210
210
  * tier, which runs one time for each report version.
211
211
  */
212
212
  export async function pinReportSnapshot(pool, analysisId, options = {}) {
213
- let ledgerRows;
214
- try {
215
- ledgerRows = await queryAnalysisArtifacts(pool, analysisId);
216
- }
217
- catch (cause) {
218
- // The query speaks the throw protocol of the `pg` driver. This is the thin wrapper that turns
219
- // that throw into a value, thus each caller above reads a failure as data.
220
- return err({ kind: "ledger-read-failed", cause });
221
- }
213
+ const ledgerRows = await queryAnalysisArtifacts(pool, analysisId);
214
+ if (ledgerRows.isErr())
215
+ return err({ kind: "ledger-read-failed", cause: ledgerRows.error.cause });
222
216
  // The ledger accepts any path. A null-prototype map keeps a key such as `__proto__` an ordinary
223
217
  // entry, thus no path collides with a prototype slot.
224
218
  const artifacts = Object.create(null);
225
- for (const row of ledgerRows) {
219
+ for (const row of ledgerRows.value) {
226
220
  artifacts[row.path] = { hash: row.hash, fileType: row.fileType };
227
221
  }
228
222
  // A citation reference resolves against this list: the resolver refuses a key that the list does not
@@ -49,12 +49,13 @@ export type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxSt
49
49
  * here would let an embedder wire a recorder the workflows use and the
50
50
  * conversation agent does not — a half-wired ledger that reads as a complete
51
51
  * one. The protection has to hold on both sides of the assembly, not just the
52
- * conversation side.
52
+ * conversation side. Each bag omits `toolOutputStore` for the same reason: each
53
+ * loop must keep its texts in the one store that each read tool reads.
53
54
  */
54
55
  export interface CoreWorkflowDeps {
55
- readonly sandboxStep: Omit<SandboxStepDeps, "usageRecorder" | "citationResolver">;
56
- readonly buildExecuteAnalysis: (sandboxStep: SandboxStepCallable) => Omit<ExecuteAnalysisDeps, "usageRecorder" | "citationResolver">;
57
- readonly dataProfile: Omit<DataProfileDeps, "usageRecorder">;
56
+ readonly sandboxStep: Omit<SandboxStepDeps, "usageRecorder" | "citationResolver" | "toolOutputStore">;
57
+ readonly buildExecuteAnalysis: (sandboxStep: SandboxStepCallable) => Omit<ExecuteAnalysisDeps, "usageRecorder" | "citationResolver" | "toolOutputStore">;
58
+ readonly dataProfile: Omit<DataProfileDeps, "usageRecorder" | "toolOutputStore">;
58
59
  }
59
60
  /** The registered, callable workflow handles. */
60
61
  export interface RegisteredWorkflows {
@@ -66,21 +67,22 @@ export interface RegisteredWorkflows {
66
67
  }
67
68
  /**
68
69
  * Conversation-agent deps minus the workflow callable, the resource policy, the
69
- * usage recorder, the report-session anchor, and the eyes.
70
+ * usage recorder, the report-session anchor, the eyes, and the tool output store.
70
71
  * `assembleCoreRuntime` supplies each of those itself. Thus a caller cannot wire
71
- * one of these five values:
72
+ * one of these six values:
72
73
  *
73
74
  * - a stale callable
74
75
  * - a policy that diverges from the one the workflows see
75
76
  * - a recorder that only half the agent tree reports to
76
77
  * - an anchor over a different session runtime
77
78
  * - a seam that only half the agent tree looks through
79
+ * - a store that only half the agent tree keeps its texts in
78
80
  *
79
81
  * The assembly resolves the eyes one time, from `CoreRuntimeDeps.eyes` and the
80
82
  * chrome config. A caller that bound the eyes here would give the conversation
81
83
  * agent a seam that the report agent never sees.
82
84
  */
83
- export type ConversationAssemblyDeps = Omit<ConversationAgentDeps, "executeAnalysisWorkflow" | "resourcePolicy" | "usageRecorder" | "citationResolver" | "anchorReportSession" | "eyes">;
85
+ export type ConversationAssemblyDeps = Omit<ConversationAgentDeps, "executeAnalysisWorkflow" | "resourcePolicy" | "usageRecorder" | "citationResolver" | "anchorReportSession" | "eyes" | "toolOutputStore">;
84
86
  /**
85
87
  * What binds one reference resolver: the analysis whose pinned artifacts it reads, and the auth of the tool
86
88
  * call that reads them. A resolver serves one analysis, thus the report tools build one for each call.
@@ -30,6 +30,7 @@ import { createCitationResolver } from "../citations/resolve.js";
30
30
  import { createThreadStore } from "../memory/thread-store.js";
31
31
  import { createArtifactReadStore, createProductionResolver } from "../report-model/production-resolver.js";
32
32
  import { createReportVersionStore } from "../state/report-versions.js";
33
+ import { createToolOutputStore } from "../state/tool-outputs.js";
33
34
  const _assertDomainError = true;
34
35
  /**
35
36
  * Wrap a type→agent registry as the resolution surface. Held apart from
@@ -83,9 +84,10 @@ export function assembleCoreRuntime(deps) {
83
84
  ? {}
84
85
  : { semanticScholarApiKey: conversation.bioKeys.semanticScholar }),
85
86
  });
86
- const sandboxStep = registerSandboxStep({ ...wf.sandboxStep, citationResolver, usageRecorder });
87
- const executeAnalysis = registerExecuteAnalysis({ ...wf.buildExecuteAnalysis(sandboxStep), citationResolver, usageRecorder });
88
- const dataProfile = registerDataProfileWorkflow({ ...wf.dataProfile, usageRecorder });
87
+ const toolOutputStore = createToolOutputStore(conversation.pool);
88
+ const sandboxStep = registerSandboxStep({ ...wf.sandboxStep, citationResolver, usageRecorder, toolOutputStore });
89
+ const executeAnalysis = registerExecuteAnalysis({ ...wf.buildExecuteAnalysis(sandboxStep), citationResolver, usageRecorder, toolOutputStore });
90
+ const dataProfile = registerDataProfileWorkflow({ ...wf.dataProfile, usageRecorder, toolOutputStore });
89
91
  // The extraction workflow shares the profile's sandbox and authorization rails, thus it draws the same
90
92
  // three seams from the profile deps. The report resolver factory binds the extraction arm over this
91
93
  // callable, thus a fall-through report reference reads its file out of process on the profile rails.
@@ -120,6 +122,7 @@ export function assembleCoreRuntime(deps) {
120
122
  anchorReportSession: reportSession.ensureSessionState,
121
123
  resourcePolicy,
122
124
  usageRecorder,
125
+ toolOutputStore,
123
126
  citationResolver,
124
127
  ...(eyes ? { eyes } : {}),
125
128
  // The start tool passes it to the spawn, which emits the creation of a
@@ -176,6 +179,7 @@ export function assembleCoreRuntime(deps) {
176
179
  ...(deps.resolveReportPageUrl ? { resolvePageUrl: deps.resolveReportPageUrl } : {}),
177
180
  ...(deps.provenance ? { provenance: deps.provenance } : {}),
178
181
  ...(conversation.logger ? { logger: conversation.logger } : {}),
182
+ toolOutputStore,
179
183
  });
180
184
  // The single registration point for every typed agent. A future thread type's
181
185
  // agent plugs in here at assembly with no embedder change. `conversation` is
@@ -49,8 +49,8 @@ export interface CreateSandboxClientConfig {
49
49
  /**
50
50
  * Per-stream retention budget sent with every exec, so the sandbox drops
51
51
  * output past it instead of shipping it. Defaults to `EXEC_STREAM_BYTE_CAP`,
52
- * the same value the host truncates to on receipt — sending more than the
53
- * host will keep buys nothing.
52
+ * the maximum of a kept tool output and the same value the host truncates to
53
+ * on receipt — sending more than the host will keep buys nothing.
54
54
  */
55
55
  execStreamByteCap?: number;
56
56
  /** Cluster resource ceilings; every sandbox request is clamped to these. */
@@ -26,7 +26,7 @@ export interface SubmitExecDeps {
26
26
  }) => Promise<T>;
27
27
  /**
28
28
  * Per-stream retention budget to send when the body does not carry one.
29
- * Defaults to `EXEC_STREAM_BYTE_CAP`.
29
+ * Defaults to `EXEC_STREAM_BYTE_CAP`, the maximum of a kept tool output.
30
30
  */
31
31
  execStreamByteCap?: number;
32
32
  }
@@ -8,9 +8,9 @@ import { z } from "zod";
8
8
  /** Artifact type inferred from the step subdirectory. */
9
9
  export declare const ArtifactTypeSchema: z.ZodEnum<{
10
10
  output: "output";
11
- log: "log";
12
- figure: "figure";
13
11
  script: "script";
12
+ figure: "figure";
13
+ log: "log";
14
14
  notebook: "notebook";
15
15
  }>;
16
16
  export type ArtifactType = z.infer<typeof ArtifactTypeSchema>;
@@ -22,9 +22,9 @@ export declare const ArtifactManifestEntrySchema: z.ZodObject<{
22
22
  size: z.ZodNumber;
23
23
  type: z.ZodEnum<{
24
24
  output: "output";
25
- log: "log";
26
- figure: "figure";
27
25
  script: "script";
26
+ figure: "figure";
27
+ log: "log";
28
28
  notebook: "notebook";
29
29
  }>;
30
30
  hash: z.ZodOptional<z.ZodString>;
@@ -2,6 +2,8 @@
2
2
  * `cortex_artifacts` operations — artifact upsert, lookup, and file-id
3
3
  * reconciliation.
4
4
  */
5
+ import type { ResultAsync } from "neverthrow";
6
+ import { type DbError } from "../lib/db-result.js";
5
7
  import type { Querier } from "./db.js";
6
8
  import type { ArtifactRow } from "./schema.js";
7
9
  export interface RegisterArtifactInput {
@@ -20,7 +22,7 @@ export interface RegisterArtifactInput {
20
22
  */
21
23
  fileId?: string | null;
22
24
  }
23
- export declare function upsertArtifact(pool: Querier, entry: RegisterArtifactInput): Promise<void>;
25
+ export declare function upsertArtifact(pool: Querier, entry: RegisterArtifactInput): ResultAsync<void, DbError>;
24
26
  /**
25
27
  * Batched upsert — writes entries in multi-row `INSERT ... VALUES` statements
26
28
  * of at most {@link ROWS_PER_STATEMENT} rows. Callers range from per-step
@@ -30,7 +32,7 @@ export declare function upsertArtifact(pool: Querier, entry: RegisterArtifactInp
30
32
  * idempotent per row (`ON CONFLICT ... DO UPDATE`), so a failure between
31
33
  * chunks is healed by the caller's retry re-upserting the same manifest.
32
34
  */
33
- export declare function upsertArtifacts(pool: Querier, entries: RegisterArtifactInput[]): Promise<void>;
35
+ export declare function upsertArtifacts(pool: Querier, entries: RegisterArtifactInput[]): ResultAsync<void, DbError>;
34
36
  /** Result type for input artifact metadata lookups. */
35
37
  export interface InputArtifactMeta {
36
38
  path: string;
@@ -42,8 +44,8 @@ export interface InputArtifactMeta {
42
44
  * Query cortex_artifacts for input-role rows matching the given paths.
43
45
  * Used at the registration boundary to resolve metadata for data-source inputs.
44
46
  */
45
- export declare function queryInputArtifacts(pool: Querier, analysisId: string, paths: string[]): Promise<InputArtifactMeta[]>;
46
- export declare function queryUnsyncedStepArtifacts(pool: Querier, resourceId: string, runId: string, stepId: string): Promise<ArtifactRow[]>;
47
+ export declare function queryInputArtifacts(pool: Querier, analysisId: string, paths: string[]): ResultAsync<InputArtifactMeta[], DbError>;
48
+ export declare function queryUnsyncedStepArtifacts(pool: Querier, resourceId: string, runId: string, stepId: string): ResultAsync<ArtifactRow[], DbError>;
47
49
  /** One registered step output, as a downstream consumer needs to name it. */
48
50
  export interface StepArtifactRef {
49
51
  /** Workspace-root-relative path (`runs/{runId}/{stepId}/output/de.csv`). */
@@ -56,7 +58,7 @@ export interface StepArtifactRef {
56
58
  * path, so a `limit` truncates the tail of the list rather than an arbitrary
57
59
  * slice of it — and so the same call replays to the same rows.
58
60
  */
59
- export declare function queryStepArtifactPaths(pool: Querier, resourceId: string, runId: string, stepId: string, limit: number): Promise<StepArtifactRef[]>;
61
+ export declare function queryStepArtifactPaths(pool: Querier, resourceId: string, runId: string, stepId: string, limit: number): ResultAsync<StepArtifactRef[], DbError>;
60
62
  /** One registered artifact of an analysis, as a reader of the full artifact set names it. */
61
63
  export interface AnalysisArtifactRef {
62
64
  /** Workspace-root-relative path (`runs/{runId}/{stepId}/output/de.csv`). */
@@ -72,11 +74,11 @@ export interface AnalysisArtifactRef {
72
74
  * query drops such a row, a later refusal says that the artifact never existed, and that reason is
73
75
  * false.
74
76
  */
75
- export declare function queryAnalysisArtifacts(pool: Querier, analysisId: string): Promise<AnalysisArtifactRef[]>;
77
+ export declare function queryAnalysisArtifacts(pool: Querier, analysisId: string): ResultAsync<AnalysisArtifactRef[], DbError>;
76
78
  /** Count step-output artifacts produced by a run — for the run-completed card. */
77
- export declare function countArtifactsForRun(pool: Querier, analysisId: string, runId: string): Promise<number>;
78
- export declare function updateArtifactId(pool: Querier, resourceId: string, path: string, artifactId: string, fileType?: string | null): Promise<void>;
79
+ export declare function countArtifactsForRun(pool: Querier, analysisId: string, runId: string): ResultAsync<number, DbError>;
80
+ export declare function updateArtifactId(pool: Querier, resourceId: string, path: string, artifactId: string, fileType?: string | null): ResultAsync<void, DbError>;
79
81
  export declare function updateFileIds(pool: Querier, pairs: Array<{
80
82
  artifactId: string;
81
83
  fileId: string;
82
- }>): Promise<void>;
84
+ }>): ResultAsync<void, DbError>;
@@ -2,10 +2,12 @@
2
2
  * `cortex_artifacts` operations — artifact upsert, lookup, and file-id
3
3
  * reconciliation.
4
4
  */
5
- export async function upsertArtifact(pool, entry) {
5
+ import { tryMutation, tryQuery } from "../lib/db-result.js";
6
+ export function upsertArtifact(pool, entry) {
6
7
  const now = new Date().toISOString();
7
- await pool.query({
8
- text: `INSERT INTO cortex_artifacts
8
+ return tryMutation("artifacts.upsertArtifact", async () => {
9
+ await pool.query({
10
+ text: `INSERT INTO cortex_artifacts
9
11
  (analysis_id, path, hash, size, role, source_step, source_run, file_type, file_id, created_at)
10
12
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
11
13
  ON CONFLICT (analysis_id, path) DO UPDATE SET
@@ -16,18 +18,19 @@ export async function upsertArtifact(pool, entry) {
16
18
  source_run = EXCLUDED.source_run,
17
19
  file_type = COALESCE(EXCLUDED.file_type, cortex_artifacts.file_type),
18
20
  file_id = COALESCE(EXCLUDED.file_id, cortex_artifacts.file_id)`,
19
- values: [
20
- entry.resourceId,
21
- entry.path,
22
- entry.hash,
23
- entry.size,
24
- entry.role,
25
- entry.sourceStep ?? null,
26
- entry.sourceRun ?? null,
27
- entry.fileType ?? null,
28
- entry.fileId ?? null,
29
- now,
30
- ],
21
+ values: [
22
+ entry.resourceId,
23
+ entry.path,
24
+ entry.hash,
25
+ entry.size,
26
+ entry.role,
27
+ entry.sourceStep ?? null,
28
+ entry.sourceRun ?? null,
29
+ entry.fileType ?? null,
30
+ entry.fileId ?? null,
31
+ now,
32
+ ],
33
+ });
31
34
  });
32
35
  }
33
36
  const COLS_PER_ROW = 10;
@@ -49,31 +52,32 @@ const ROWS_PER_STATEMENT = 1_000;
49
52
  * idempotent per row (`ON CONFLICT ... DO UPDATE`), so a failure between
50
53
  * chunks is healed by the caller's retry re-upserting the same manifest.
51
54
  */
52
- export async function upsertArtifacts(pool, entries) {
55
+ export function upsertArtifacts(pool, entries) {
53
56
  const now = new Date().toISOString();
54
- for (let start = 0; start < entries.length; start += ROWS_PER_STATEMENT) {
55
- const chunk = entries.slice(start, start + ROWS_PER_STATEMENT);
56
- const placeholders = chunk
57
- .map((_, i) => {
58
- const base = i * COLS_PER_ROW;
59
- return (`($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, ` +
60
- `$${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10})`);
61
- })
62
- .join(", ");
63
- const values = chunk.flatMap((e) => [
64
- e.resourceId,
65
- e.path,
66
- e.hash,
67
- e.size,
68
- e.role,
69
- e.sourceStep ?? null,
70
- e.sourceRun ?? null,
71
- e.fileType ?? null,
72
- e.fileId ?? null,
73
- now,
74
- ]);
75
- await pool.query({
76
- text: `INSERT INTO cortex_artifacts
57
+ return tryMutation("artifacts.upsertArtifacts", async () => {
58
+ for (let start = 0; start < entries.length; start += ROWS_PER_STATEMENT) {
59
+ const chunk = entries.slice(start, start + ROWS_PER_STATEMENT);
60
+ const placeholders = chunk
61
+ .map((_, i) => {
62
+ const base = i * COLS_PER_ROW;
63
+ return (`($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, ` +
64
+ `$${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10})`);
65
+ })
66
+ .join(", ");
67
+ const values = chunk.flatMap((e) => [
68
+ e.resourceId,
69
+ e.path,
70
+ e.hash,
71
+ e.size,
72
+ e.role,
73
+ e.sourceStep ?? null,
74
+ e.sourceRun ?? null,
75
+ e.fileType ?? null,
76
+ e.fileId ?? null,
77
+ now,
78
+ ]);
79
+ await pool.query({
80
+ text: `INSERT INTO cortex_artifacts
77
81
  (analysis_id, path, hash, size, role, source_step, source_run, file_type, file_id, created_at)
78
82
  VALUES ${placeholders}
79
83
  ON CONFLICT (analysis_id, path) DO UPDATE SET
@@ -84,42 +88,47 @@ export async function upsertArtifacts(pool, entries) {
84
88
  source_run = EXCLUDED.source_run,
85
89
  file_type = COALESCE(EXCLUDED.file_type, cortex_artifacts.file_type),
86
90
  file_id = COALESCE(EXCLUDED.file_id, cortex_artifacts.file_id)`,
87
- values,
88
- });
89
- }
91
+ values,
92
+ });
93
+ }
94
+ });
90
95
  }
91
96
  /**
92
97
  * Query cortex_artifacts for input-role rows matching the given paths.
93
98
  * Used at the registration boundary to resolve metadata for data-source inputs.
94
99
  */
95
- export async function queryInputArtifacts(pool, analysisId, paths) {
96
- if (paths.length === 0)
97
- return [];
98
- const result = await pool.query({
99
- text: `SELECT path, hash, size, file_id
100
+ export function queryInputArtifacts(pool, analysisId, paths) {
101
+ return tryQuery("artifacts.queryInputArtifacts", async () => {
102
+ if (paths.length === 0)
103
+ return [];
104
+ const result = await pool.query({
105
+ text: `SELECT path, hash, size, file_id
100
106
  FROM cortex_artifacts
101
107
  WHERE analysis_id = $1 AND role = 'input' AND path = ANY($2::text[])`,
102
- values: [analysisId, paths],
108
+ values: [analysisId, paths],
109
+ });
110
+ return result.rows.map((r) => ({
111
+ path: r.path,
112
+ hash: r.hash,
113
+ size: Number(r.size),
114
+ fileId: r.file_id ?? null,
115
+ }));
103
116
  });
104
- return result.rows.map((r) => ({
105
- path: r.path,
106
- hash: r.hash,
107
- size: Number(r.size),
108
- fileId: r.file_id ?? null,
109
- }));
110
117
  }
111
- export async function queryUnsyncedStepArtifacts(pool, resourceId, runId, stepId) {
112
- const result = await pool.query({
113
- text: `SELECT analysis_id, path, artifact_id, file_id, hash, size, role,
118
+ export function queryUnsyncedStepArtifacts(pool, resourceId, runId, stepId) {
119
+ return tryQuery("artifacts.queryUnsyncedStepArtifacts", async () => {
120
+ const result = await pool.query({
121
+ text: `SELECT analysis_id, path, artifact_id, file_id, hash, size, role,
114
122
  source_step, source_run, created_at, file_type
115
123
  FROM cortex_artifacts
116
124
  WHERE analysis_id = $1 AND source_run = $2 AND source_step = $3
117
125
  AND artifact_id IS NOT NULL AND file_id IS NULL
118
126
  AND role = 'step_output'
119
127
  ORDER BY created_at`,
120
- values: [resourceId, runId, stepId],
128
+ values: [resourceId, runId, stepId],
129
+ });
130
+ return result.rows.map(mapArtifactRow);
121
131
  });
122
- return result.rows.map(mapArtifactRow);
123
132
  }
124
133
  /**
125
134
  * The artifacts a completed step registered, most consequential first. Ordered
@@ -127,11 +136,12 @@ export async function queryUnsyncedStepArtifacts(pool, resourceId, runId, stepId
127
136
  * path, so a `limit` truncates the tail of the list rather than an arbitrary
128
137
  * slice of it — and so the same call replays to the same rows.
129
138
  */
130
- export async function queryStepArtifactPaths(pool, resourceId, runId, stepId, limit) {
131
- if (limit <= 0)
132
- return [];
133
- const result = await pool.query({
134
- text: `SELECT path, file_type
139
+ export function queryStepArtifactPaths(pool, resourceId, runId, stepId, limit) {
140
+ return tryQuery("artifacts.queryStepArtifactPaths", async () => {
141
+ if (limit <= 0)
142
+ return [];
143
+ const result = await pool.query({
144
+ text: `SELECT path, file_type
135
145
  FROM cortex_artifacts
136
146
  WHERE analysis_id = $1 AND source_run = $2 AND source_step = $3
137
147
  AND role = 'step_output'
@@ -144,9 +154,10 @@ export async function queryStepArtifactPaths(pool, resourceId, runId, stepId, li
144
154
  END,
145
155
  path
146
156
  LIMIT $4`,
147
- values: [resourceId, runId, stepId, limit],
157
+ values: [resourceId, runId, stepId, limit],
158
+ });
159
+ return result.rows.map((r) => ({ path: r.path, fileType: r.file_type ?? null }));
148
160
  });
149
- return result.rows.map((r) => ({ path: r.path, fileType: r.file_type ?? null }));
150
161
  }
151
162
  /**
152
163
  * Every artifact of one analysis, ordered by path so the same call gives the same rows.
@@ -156,50 +167,58 @@ export async function queryStepArtifactPaths(pool, resourceId, runId, stepId, li
156
167
  * query drops such a row, a later refusal says that the artifact never existed, and that reason is
157
168
  * false.
158
169
  */
159
- export async function queryAnalysisArtifacts(pool, analysisId) {
160
- const result = await pool.query({
161
- text: `SELECT path, hash, file_type
170
+ export function queryAnalysisArtifacts(pool, analysisId) {
171
+ return tryQuery("artifacts.queryAnalysisArtifacts", async () => {
172
+ const result = await pool.query({
173
+ text: `SELECT path, hash, file_type
162
174
  FROM cortex_artifacts
163
175
  WHERE analysis_id = $1
164
176
  ORDER BY path`,
165
- values: [analysisId],
177
+ values: [analysisId],
178
+ });
179
+ return result.rows.map((r) => ({ path: r.path, hash: r.hash, fileType: r.file_type ?? null }));
166
180
  });
167
- return result.rows.map((r) => ({ path: r.path, hash: r.hash, fileType: r.file_type ?? null }));
168
181
  }
169
182
  /** Count step-output artifacts produced by a run — for the run-completed card. */
170
- export async function countArtifactsForRun(pool, analysisId, runId) {
171
- const result = await pool.query({
172
- text: `SELECT COUNT(*)::text AS n
183
+ export function countArtifactsForRun(pool, analysisId, runId) {
184
+ return tryQuery("artifacts.countArtifactsForRun", async () => {
185
+ const result = await pool.query({
186
+ text: `SELECT COUNT(*)::text AS n
173
187
  FROM cortex_artifacts
174
188
  WHERE analysis_id = $1 AND source_run = $2 AND role = 'step_output'`,
175
- values: [analysisId, runId],
189
+ values: [analysisId, runId],
190
+ });
191
+ return Number(result.rows[0]?.n ?? 0);
176
192
  });
177
- return Number(result.rows[0]?.n ?? 0);
178
193
  }
179
- export async function updateArtifactId(pool, resourceId, path, artifactId, fileType) {
180
- await pool.query({
181
- text: `UPDATE cortex_artifacts SET artifact_id = $1,
194
+ export function updateArtifactId(pool, resourceId, path, artifactId, fileType) {
195
+ return tryMutation("artifacts.updateArtifactId", async () => {
196
+ await pool.query({
197
+ text: `UPDATE cortex_artifacts SET artifact_id = $1,
182
198
  file_type = COALESCE($4, file_type)
183
199
  WHERE analysis_id = $2 AND path = $3`,
184
- values: [artifactId, resourceId, path, fileType ?? null],
200
+ values: [artifactId, resourceId, path, fileType ?? null],
201
+ });
185
202
  });
186
203
  }
187
- export async function updateFileIds(pool, pairs) {
188
- if (pairs.length === 0)
189
- return;
190
- const values = [];
191
- const tuples = [];
192
- for (let i = 0; i < pairs.length; i++) {
193
- const base = i * 2;
194
- tuples.push(`($${base + 1}::text, $${base + 2}::text)`);
195
- values.push(pairs[i].artifactId, pairs[i].fileId);
196
- }
197
- await pool.query({
198
- text: `UPDATE cortex_artifacts AS a
204
+ export function updateFileIds(pool, pairs) {
205
+ return tryMutation("artifacts.updateFileIds", async () => {
206
+ if (pairs.length === 0)
207
+ return;
208
+ const values = [];
209
+ const tuples = [];
210
+ for (let i = 0; i < pairs.length; i++) {
211
+ const base = i * 2;
212
+ tuples.push(`($${base + 1}::text, $${base + 2}::text)`);
213
+ values.push(pairs[i].artifactId, pairs[i].fileId);
214
+ }
215
+ await pool.query({
216
+ text: `UPDATE cortex_artifacts AS a
199
217
  SET file_id = c.file_id
200
218
  FROM (VALUES ${tuples.join(", ")}) AS c(artifact_id, file_id)
201
219
  WHERE a.artifact_id = c.artifact_id`,
202
- values,
220
+ values,
221
+ });
203
222
  });
204
223
  }
205
224
  function mapArtifactRow(row) {
@@ -146,9 +146,9 @@ export declare function expireStaleDataProfile(pool: Querier, analysisId: string
146
146
  * Reset a ledger row wedged at `running` with no workflow behind it back to
147
147
  * `failed`, so the normal retry path can re-profile it.
148
148
  *
149
- * A start that rejects after the CAS already flipped the row to `running`
149
+ * A start that fails after the CAS already flipped the row to `running`
150
150
  * compensates itself (`triggerDataProfile`/`runDataProfile` fail the ledger in
151
- * their catch). This covers the residual case that compensation cannot: a host
151
+ * their dispatch). This covers the residual case that compensation cannot: a host
152
152
  * that dies in the window between the CAS and the `DBOS.startWorkflow` insert
153
153
  * leaves a `running` row with no workflow for recovery to resume — nothing
154
154
  * would ever move it off `running`, and every later trigger reports