@inflexa-ai/harness 0.39.2 → 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 (184) 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/post-step-pipeline.d.ts +23 -16
  31. package/dist/execution/post-step-pipeline.js +19 -21
  32. package/dist/execution/run-synthesis.d.ts +3 -0
  33. package/dist/execution/run-synthesis.js +11 -1
  34. package/dist/execution/step-summary.d.ts +21 -30
  35. package/dist/execution/step-summary.js +30 -57
  36. package/dist/index.d.ts +9 -8
  37. package/dist/index.js +15 -34
  38. package/dist/lib/zod-issues.js +2 -4
  39. package/dist/loop/compaction.d.ts +20 -0
  40. package/dist/loop/compaction.js +2 -0
  41. package/dist/loop/continue-agent.d.ts +40 -0
  42. package/dist/loop/continue-agent.js +38 -0
  43. package/dist/loop/metrics.d.ts +30 -22
  44. package/dist/loop/metrics.js +51 -22
  45. package/dist/loop/run-agent.d.ts +111 -13
  46. package/dist/loop/run-agent.js +467 -220
  47. package/dist/loop/run-to-terminal.d.ts +16 -17
  48. package/dist/loop/run-to-terminal.js +24 -35
  49. package/dist/loop/tool-mask.d.ts +23 -0
  50. package/dist/loop/tool-mask.js +46 -0
  51. package/dist/loop/tool-output.d.ts +48 -0
  52. package/dist/loop/tool-output.js +79 -0
  53. package/dist/loop/types.d.ts +6 -3
  54. package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
  55. package/dist/memory/ai-sdk-message-storage.js +78 -2
  56. package/dist/memory/conversation-display-recorder.d.ts +6 -0
  57. package/dist/memory/conversation-display-recorder.js +64 -22
  58. package/dist/memory/conversation-display-replay.d.ts +5 -14
  59. package/dist/memory/conversation-display-replay.js +66 -32
  60. package/dist/memory/conversation-display-storage.d.ts +2 -1
  61. package/dist/memory/conversation-display-storage.js +2 -1
  62. package/dist/memory/conversation-view.d.ts +53 -0
  63. package/dist/memory/conversation-view.js +130 -0
  64. package/dist/memory/count-tokens.d.ts +5 -7
  65. package/dist/memory/count-tokens.js +7 -9
  66. package/dist/memory/thread-history.d.ts +74 -102
  67. package/dist/memory/thread-history.js +181 -246
  68. package/dist/memory/thread-store.d.ts +3 -3
  69. package/dist/memory/thread-store.js +7 -2
  70. package/dist/memory/tool-call-integrity.d.ts +15 -21
  71. package/dist/memory/tool-call-integrity.js +31 -38
  72. package/dist/memory/working-memory.d.ts +2 -5
  73. package/dist/memory/working-memory.js +2 -5
  74. package/dist/prompts/SOUL.d.ts +2 -2
  75. package/dist/prompts/SOUL.js +2 -7
  76. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  77. package/dist/prompts/analogical-reasoner.js +16 -32
  78. package/dist/prompts/briefing.js +1 -1
  79. package/dist/prompts/compaction.d.ts +4 -0
  80. package/dist/prompts/compaction.js +24 -0
  81. package/dist/prompts/conversation.d.ts +1 -1
  82. package/dist/prompts/conversation.js +26 -35
  83. package/dist/prompts/execute-analysis/step-summary.js +7 -5
  84. package/dist/prompts/literature-reviewer.d.ts +1 -1
  85. package/dist/prompts/literature-reviewer.js +29 -34
  86. package/dist/prompts/planner.js +15 -29
  87. package/dist/prompts/report-session.d.ts +1 -1
  88. package/dist/prompts/report-session.js +2 -33
  89. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
  91. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
  93. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/chromatin-agent.js +1 -2
  95. package/dist/prompts/sandbox/data-profiler.js +6 -3
  96. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  97. package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
  98. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  99. package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
  100. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  101. package/dist/prompts/sandbox/enrichment-agent.js +3 -2
  102. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  103. package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
  104. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  105. package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
  106. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  107. package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
  108. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  109. package/dist/prompts/sandbox/microbiome-agent.js +14 -21
  110. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  111. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
  112. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  113. package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
  114. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  115. package/dist/prompts/sandbox/network-agent.js +7 -8
  116. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  117. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
  118. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  119. package/dist/prompts/sandbox/proteomics-agent.js +3 -4
  120. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  121. package/dist/prompts/sandbox/scientific-executor.js +7 -10
  122. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  123. package/dist/prompts/sandbox/single-cell-agent.js +2 -4
  124. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  125. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
  126. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  127. package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
  128. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  129. package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
  130. package/dist/prompts/sandbox-standards.d.ts +7 -7
  131. package/dist/prompts/sandbox-standards.js +74 -76
  132. package/dist/prompts/synthesis-agent.d.ts +1 -1
  133. package/dist/prompts/synthesis-agent.js +16 -42
  134. package/dist/providers/ai-sdk.d.ts +39 -6
  135. package/dist/providers/ai-sdk.js +117 -18
  136. package/dist/providers/errors.d.ts +2 -0
  137. package/dist/providers/errors.js +5 -0
  138. package/dist/providers/prompt-cache.d.ts +43 -41
  139. package/dist/providers/prompt-cache.js +47 -41
  140. package/dist/providers/reasoning.d.ts +10 -21
  141. package/dist/providers/reasoning.js +10 -21
  142. package/dist/providers/types.d.ts +21 -5
  143. package/dist/report-model/authoring-grammar.d.ts +2 -2
  144. package/dist/runtime/assemble.d.ts +9 -7
  145. package/dist/runtime/assemble.js +7 -3
  146. package/dist/sandbox/create-sandbox.d.ts +2 -2
  147. package/dist/sandbox/submit-exec.d.ts +1 -1
  148. package/dist/schemas/artifact-manifest.d.ts +4 -4
  149. package/dist/state/init.js +39 -4
  150. package/dist/state/purge-analysis.js +4 -0
  151. package/dist/state/tool-outputs.d.ts +4 -0
  152. package/dist/state/tool-outputs.js +30 -0
  153. package/dist/tasks/data-profile.d.ts +3 -0
  154. package/dist/tasks/data-profile.js +2 -0
  155. package/dist/tools/ad-hoc-router.d.ts +8 -0
  156. package/dist/tools/ad-hoc-router.js +21 -2
  157. package/dist/tools/display/show-user.d.ts +1 -1
  158. package/dist/tools/execute-analysis.d.ts +3 -0
  159. package/dist/tools/execute-analysis.js +9 -1
  160. package/dist/tools/read-tool-output.d.ts +7 -0
  161. package/dist/tools/read-tool-output.js +130 -0
  162. package/dist/tools/research/generate-analogy-report.d.ts +10 -26
  163. package/dist/tools/research/generate-analogy-report.js +76 -163
  164. package/dist/tools/research/generate-plan.d.ts +3 -0
  165. package/dist/tools/research/generate-plan.js +4 -1
  166. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  167. package/dist/tools/research/literature-reviewer.js +3 -0
  168. package/dist/tools/sandbox/report-blocker.d.ts +18 -5
  169. package/dist/tools/sandbox/report-blocker.js +54 -6
  170. package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
  171. package/dist/tools/sandbox/submit-file-metadata.js +67 -0
  172. package/dist/tools/start-report-session.d.ts +1 -3
  173. package/dist/tools/start-report-session.js +0 -2
  174. package/dist/tools/workspace/execute-command.js +7 -4
  175. package/dist/tools/workspace/result-bounds.d.ts +4 -4
  176. package/dist/tools/workspace/result-bounds.js +6 -5
  177. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  178. package/dist/workflows/execute-analysis.d.ts +3 -0
  179. package/dist/workflows/execute-analysis.js +1 -0
  180. package/dist/workflows/sandbox-step.d.ts +30 -9
  181. package/dist/workflows/sandbox-step.js +37 -8
  182. package/package.json +9 -9
  183. package/dist/providers/anthropic.d.ts +0 -25
  184. 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>>;
@@ -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>;
@@ -242,8 +242,8 @@ CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_metadata
242
242
  -- the JSON operators retractLastTurn uses work the same on either type.
243
243
  --
244
244
  -- reported_usage holds a whole turn's TokenUsageRollup — what providers reported
245
- -- for the turn this row completed — and is written only on the LAST assistant row
246
- -- of a turn, so a reloaded transcript can show the figure the live turn showed.
245
+ -- for the turn this row completed — on the LAST assistant row of an older turn
246
+ -- only. cortex_thread_turns holds the figures of each later turn.
247
247
  -- It is NOT the tokens column beside it, and the two must never be read for
248
248
  -- each other: tokens is an offline js-tiktoken estimate stamped on EVERY row so
249
249
  -- the read path can window by budget without a provider round trip, and it exists
@@ -283,8 +283,8 @@ CREATE TABLE IF NOT EXISTS messages (
283
283
  role TEXT,
284
284
  content_jsonb JSONB,
285
285
  message_envelope JSON,
286
- -- Versioned AI SDK UIMessage projection for the whole turn. Present only
287
- -- on the genuine-user-start row. Model-history reads never select it.
286
+ -- Versioned AI SDK UIMessage projection of one written group, on the first
287
+ -- row of that group. Model-history reads never select it.
288
288
  display_envelope JSONB,
289
289
  tokens INTEGER NOT NULL,
290
290
  reported_usage JSONB,
@@ -294,6 +294,24 @@ CREATE TABLE IF NOT EXISTS messages (
294
294
  PRIMARY KEY (thread_id, seq)
295
295
  );
296
296
 
297
+ -- The state of one chat turn, and no message: a close never changes a row of
298
+ -- messages. messages.reported_usage and messages.turn_duration_ms hold the
299
+ -- figures of the older turns, which have no record here.
300
+ CREATE TABLE IF NOT EXISTS cortex_thread_turns (
301
+ thread_id TEXT NOT NULL,
302
+ -- The seq of the user row that opens the turn.
303
+ start_seq BIGINT NOT NULL,
304
+ status TEXT NOT NULL CHECK (status IN ('open', 'done', 'aborted', 'failed')),
305
+ -- A short text from the class of the error, never its message, because each later turn sends the note.
306
+ reason TEXT,
307
+ -- Absent when no provider reported a quantity, the same as messages.reported_usage.
308
+ reported_usage JSONB,
309
+ turn_duration_ms BIGINT,
310
+ opened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
311
+ closed_at TIMESTAMPTZ,
312
+ PRIMARY KEY (thread_id, start_seq)
313
+ );
314
+
297
315
  -- Conversation thread metadata — the harness ThreadStore module's table,
298
316
  -- one row per conversation thread. Holds the analysis-scope, title, and
299
317
  -- soft-delete tombstone the harness messages table deliberately lacks.
@@ -413,6 +431,23 @@ CREATE TABLE IF NOT EXISTS cortex_ask_grants (
413
431
  created_at TEXT NOT NULL,
414
432
  PRIMARY KEY (analysis_id, user_id, grant_key)
415
433
  );
434
+
435
+ -- The kept text of each cut tool result. thread_id is null for a text of a run, thus purgeThread removes
436
+ -- only the texts of chat turns, and purgeAnalysis removes each row by analysis_id.
437
+ CREATE TABLE IF NOT EXISTS cortex_tool_outputs (
438
+ analysis_id TEXT NOT NULL,
439
+ ref TEXT NOT NULL,
440
+ tool_name TEXT NOT NULL,
441
+ tool_call_id TEXT NOT NULL,
442
+ thread_id TEXT,
443
+ content TEXT NOT NULL,
444
+ total_length INTEGER NOT NULL,
445
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
446
+ PRIMARY KEY (analysis_id, ref)
447
+ );
448
+
449
+ CREATE INDEX IF NOT EXISTS cortex_tool_outputs_thread_idx
450
+ ON cortex_tool_outputs (thread_id) WHERE thread_id IS NOT NULL;
416
451
  `;
417
452
  /**
418
453
  * Initialize Cortex state tables. Safe to call on every startup and across
@@ -78,6 +78,8 @@ const ANALYSIS_KEYED_DELETES = [
78
78
  { op: "purgeAnalysis.workingMemory", sql: "DELETE FROM cortex_working_memory WHERE analysis_id = $1" },
79
79
  { op: "purgeAnalysis.asks", sql: "DELETE FROM cortex_asks WHERE analysis_id = $1" },
80
80
  { op: "purgeAnalysis.askGrants", sql: "DELETE FROM cortex_ask_grants WHERE analysis_id = $1" },
81
+ // By its own analysis id, not through the thread rows: a kept text of a run names no thread.
82
+ { op: "purgeAnalysis.toolOutputs", sql: "DELETE FROM cortex_tool_outputs WHERE analysis_id = $1" },
81
83
  { op: "purgeAnalysis.plans", sql: "DELETE FROM cortex_plans WHERE analysis_id = $1" },
82
84
  { op: "purgeAnalysis.reportVersions", sql: "DELETE FROM cortex_report_versions WHERE analysis_id = $1" },
83
85
  { op: "purgeAnalysis.reportSessionState", sql: "DELETE FROM cortex_report_session_state WHERE analysis_id = $1" },
@@ -133,6 +135,8 @@ export function createAnalysisPurge({ pool, workflows, logger: injected }) {
133
135
  // thread is still the analysis's, and a purge takes it too.
134
136
  deleteRows(client, "purgeAnalysis.messages", `DELETE FROM messages
135
137
  WHERE thread_id IN (SELECT thread_id FROM cortex_analysis_threads WHERE analysis_id = $1)`, analysisId)
138
+ .andThen((messages) => deleteRows(client, "purgeAnalysis.turnRecords", `DELETE FROM cortex_thread_turns
139
+ WHERE thread_id IN (SELECT thread_id FROM cortex_analysis_threads WHERE analysis_id = $1)`, analysisId).map(() => messages))
136
140
  .andThen((messages) => deleteRows(client, "purgeAnalysis.threads", "DELETE FROM cortex_analysis_threads WHERE analysis_id = $1", analysisId).map((threads) => ({
137
141
  threads,
138
142
  messages,
@@ -0,0 +1,4 @@
1
+ /** The Postgres realization of the tool output store of the loop, over `cortex_tool_outputs`. */
2
+ import type { Pool } from "pg";
3
+ import type { ToolOutputStore } from "../loop/tool-output.js";
4
+ export declare function createToolOutputStore(pool: Pool): ToolOutputStore;
@@ -0,0 +1,30 @@
1
+ /** The Postgres realization of the tool output store of the loop, over `cortex_tool_outputs`. */
2
+ import { tryMutation, tryQuery } from "../lib/db-result.js";
3
+ function toKeptToolOutput(row) {
4
+ return {
5
+ analysisId: row.analysis_id,
6
+ ref: row.ref,
7
+ toolName: row.tool_name,
8
+ toolCallId: row.tool_call_id,
9
+ ...(row.thread_id === null ? {} : { threadId: row.thread_id }),
10
+ content: row.content,
11
+ totalLength: row.total_length,
12
+ };
13
+ }
14
+ export function createToolOutputStore(pool) {
15
+ return {
16
+ put: (output) => tryMutation("toolOutputs.put", () => pool.query(
17
+ // The update sets no `created_at`, thus the row keeps the time of its first put.
18
+ `INSERT INTO cortex_tool_outputs (analysis_id, ref, tool_name, tool_call_id, thread_id, content, total_length)
19
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
20
+ ON CONFLICT (analysis_id, ref) DO UPDATE SET
21
+ tool_name = EXCLUDED.tool_name,
22
+ tool_call_id = EXCLUDED.tool_call_id,
23
+ thread_id = EXCLUDED.thread_id,
24
+ content = EXCLUDED.content,
25
+ total_length = EXCLUDED.total_length`, [output.analysisId, output.ref, output.toolName, output.toolCallId, output.threadId ?? null, output.content, output.totalLength])).map(() => undefined),
26
+ get: (analysisId, ref) => tryQuery("toolOutputs.get", () => pool.query(`SELECT analysis_id, ref, tool_name, tool_call_id, thread_id, content, total_length
27
+ FROM cortex_tool_outputs
28
+ WHERE analysis_id = $1 AND ref = $2`, [analysisId, ref])).map(({ rows }) => (rows[0] === undefined ? null : toKeptToolOutput(rows[0]))),
29
+ };
30
+ }
@@ -26,6 +26,7 @@ import type { RunAuthorization, RunAuthorizer } from "../execution/run-authorize
26
26
  import type { StagedInput } from "../execution/staged-input.js";
27
27
  import type { SandboxAgentDeps } from "../agents/sandbox/shared.js";
28
28
  import type { BioToolKeys } from "../tools/bio/keys.js";
29
+ import type { ToolOutputStore } from "../loop/tool-output.js";
29
30
  import type { DbError } from "../lib/db-result.js";
30
31
  import { type GateRefusal } from "../lib/hooks.js";
31
32
  import type { Logger } from "../lib/logger.js";
@@ -63,6 +64,8 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
63
64
  readonly skillsDir: string;
64
65
  /** LLM usage-accounting seam for the profiler agent loop; omitted falls back to the no-op recorder. */
65
66
  readonly usageRecorder?: UsageRecorder;
67
+ /** The store of the profiler loop and of its read tool. */
68
+ readonly toolOutputStore?: ToolOutputStore;
66
69
  /**
67
70
  * The farm-extension seam of the embedder. Bound, it rides into the
68
71
  * profiler agent deps, and the substrate attaches `link_packages`. Thus
@@ -271,6 +271,7 @@ export function profilerSandboxAgentDeps(deps, step) {
271
271
  ...(deps.imagePackagesFile ? { imagePackagesFile: deps.imagePackagesFile } : {}),
272
272
  ...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}),
273
273
  ...(deps.extendAnalysisFarm ? { extendAnalysisFarm: deps.extendAnalysisFarm } : {}),
274
+ ...(deps.toolOutputStore ? { toolOutputStore: deps.toolOutputStore } : {}),
274
275
  bioKeys: deps.bioKeys,
275
276
  step,
276
277
  };
@@ -548,6 +549,7 @@ export async function runDataProfileBody(input, deps) {
548
549
  runStep: durableStep,
549
550
  resolved: () => accepted !== null,
550
551
  usageRecorder: deps.usageRecorder,
552
+ toolOutputStore: deps.toolOutputStore,
551
553
  isFatalLoopError: (err) => err instanceof DBOSErrors.DBOSWorkflowCancelledError,
552
554
  }, {
553
555
  tools: [submitProfileTool],
@@ -1,7 +1,9 @@
1
1
  import type { Pool } from "pg";
2
2
  import { type AgentSession } from "../auth/types.js";
3
+ import type { UsageRecorder } from "../billing/usage-recorder.js";
3
4
  import type { ResourcePolicy, ResourceSpec } from "../config/resource-limits.js";
4
5
  import type { Logger } from "../lib/logger.js";
6
+ import { type AgentRunUsage } from "../loop/metrics.js";
5
7
  import { type ChatProvider } from "../providers/types.js";
6
8
  import type { CheckedPackage } from "./sandbox/list-available-packages.js";
7
9
  export declare const AD_HOC_ROUTER_AGENT_ID = "adhoc-router";
@@ -48,6 +50,8 @@ export interface AdHocRouterDeps {
48
50
  * the entries reach the link pass as the model wrote them.
49
51
  */
50
52
  readonly resolvePackages?: (names: readonly string[]) => Promise<readonly CheckedPackage[] | null>;
53
+ /** Falls back to the no-op recorder when omitted. */
54
+ readonly usageRecorder?: UsageRecorder;
51
55
  }
52
56
  export declare function defaultAdHocResources(policy?: ResourcePolicy): ResourceSpec;
53
57
  export declare function validAdHocResources(value: unknown, policy?: ResourcePolicy): ResourceSpec | null;
@@ -56,4 +60,8 @@ export declare function routeAdHocRequest(deps: AdHocRouterDeps, input: {
56
60
  request: string;
57
61
  session: AgentSession;
58
62
  signal: AbortSignal;
63
+ /** The router call folds its usage into this, so the turn total includes it. */
64
+ readonly turnUsage?: AgentRunUsage;
65
+ /** Carried into the record key of the router usage entry. */
66
+ readonly invocationId?: string;
59
67
  }): Promise<AdHocRoute>;
@@ -3,8 +3,11 @@ import { z } from "zod";
3
3
  import { PLANNABLE_AGENT_CATALOG } from "../agents/sandbox-catalog.js";
4
4
  import { forSubAgent } from "../auth/types.js";
5
5
  import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../app/data-profile-orientation.js";
6
+ import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
6
7
  import { createNoopLogger } from "../lib/console-logger.js";
7
8
  import { unwrapOrThrow } from "../lib/result.js";
9
+ import { countChatTokens } from "../loop/metrics.js";
10
+ import { accountForChatCall } from "../loop/run-agent.js";
8
11
  import { packagesSection, resourceEstimationSection } from "../prompts/planner.js";
9
12
  import { effectiveDeadlineMs } from "../providers/types.js";
10
13
  import { formatQuery, parseQuery } from "../sandbox/package-identity.js";
@@ -12,6 +15,8 @@ import { loadDataProfileStatus } from "../state/index.js";
12
15
  export const AD_HOC_ROUTER_AGENT_ID = "adhoc-router";
13
16
  export const AD_HOC_ROUTER_TIMEOUT_MS = 10_000;
14
17
  export const AD_HOC_FALLBACK_AGENT_ID = "scientific-executor";
18
+ /** Must differ from a loop step name such as `llm-0`, since both share the usage-record key space. */
19
+ const AD_HOC_ROUTE_CALL_NAME = "adhoc-route";
15
20
  const resourcesSchema = z.object({
16
21
  cpu: z.number().positive(),
17
22
  memoryGb: z.number().positive(),
@@ -152,6 +157,7 @@ function routeTool() {
152
157
  }
153
158
  export async function routeAdHocRequest(deps, input) {
154
159
  const logger = (deps.logger ?? createNoopLogger()).named("adhoc-router").with({ analysisId: input.analysisId, model: deps.model });
160
+ const routerSession = forSubAgent(input.session, AD_HOC_ROUTER_AGENT_ID);
155
161
  const catalog = PLANNABLE_AGENT_CATALOG.map((agent) => `- ${agent.id}: capabilities [${agent.capabilities.join(", ")}]; suitable for [${agent.suitableFor.join(", ")}]`).join("\n");
156
162
  const orientation = await profileOrientation(deps.pool, input.analysisId, logger);
157
163
  const controller = new AbortController();
@@ -160,7 +166,8 @@ export async function routeAdHocRequest(deps, input) {
160
166
  let raw;
161
167
  let failure;
162
168
  try {
163
- const response = unwrapOrThrow(await deps.provider.chat({
169
+ const response = unwrapOrThrow(await deps.provider
170
+ .chat({
164
171
  system: [
165
172
  "Select exactly one specialist for a targeted one-step analysis, estimate its sandbox resources, and name the packages that the step imports.",
166
173
  "Choose only from the supplied catalog. Do not create a plan, reject the request, or select scientific-executor.",
@@ -179,7 +186,19 @@ export async function routeAdHocRequest(deps, input) {
179
186
  // No forced tool choice: some Claude models reject it with a
180
187
  // 400. A reply without the call falls back below as `malformed`.
181
188
  tools: routeTool(),
182
- }, forSubAgent(input.session, AD_HOC_ROUTER_AGENT_ID), signal));
189
+ }, routerSession, signal)
190
+ .map(countChatTokens(AD_HOC_ROUTER_AGENT_ID)));
191
+ // A failed call throws at unwrapOrThrow above, so no usage is recorded.
192
+ accountForChatCall(response, {
193
+ session: routerSession,
194
+ agentId: AD_HOC_ROUTER_AGENT_ID,
195
+ callPath: routerSession.provenance.callPath,
196
+ stepName: AD_HOC_ROUTE_CALL_NAME,
197
+ ...(input.invocationId === undefined ? {} : { invocationId: input.invocationId }),
198
+ usageRecorder: deps.usageRecorder ?? createNoopUsageRecorder(),
199
+ logger,
200
+ rollups: input.turnUsage === undefined ? [] : [input.turnUsage],
201
+ });
183
202
  const call = Array.isArray(response.message.content)
184
203
  ? response.message.content.find((part) => part.type === "tool-call" && part.toolName === "submit_route")
185
204
  : undefined;
@@ -26,7 +26,7 @@ type ShowUserOutput = {
26
26
  id: string;
27
27
  };
28
28
  export declare const showUserTool: import("../define-tool.js").Tool<{
29
- kind: "code" | "markdown" | "table" | "echart" | "svg" | "structure";
29
+ kind: "code" | "markdown" | "echart" | "svg" | "table" | "structure";
30
30
  title?: string | undefined;
31
31
  spec?: Record<string, unknown> | undefined;
32
32
  dataPath?: string | undefined;
@@ -3,6 +3,7 @@
3
3
  * hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
4
4
  */
5
5
  import type { Pool } from "pg";
6
+ import type { UsageRecorder } from "../billing/usage-recorder.js";
6
7
  import type { EnvironmentStorePaths } from "../config/environment-stores.js";
7
8
  import type { ResourcePolicy } from "../config/resource-limits.js";
8
9
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
@@ -29,6 +30,8 @@ export interface ExecuteAnalysisToolDeps extends Pick<EnvironmentStorePaths, "fa
29
30
  * run reserves anything. Without it, the link pass returns at once.
30
31
  */
31
32
  readonly extendAnalysisFarm?: ExtendAnalysisFarm;
33
+ /** Falls back to the no-op recorder when omitted. */
34
+ readonly usageRecorder?: UsageRecorder;
32
35
  }
33
36
  export declare class PlanNotFoundError extends Error {
34
37
  constructor(planId: string);
@@ -296,7 +296,15 @@ async function persistedAdHocPlan(deps, args) {
296
296
  resourcePolicy: deps.resourcePolicy,
297
297
  logger: deps.logger,
298
298
  ...(resolvePackages ? { resolvePackages } : {}),
299
- }, { analysisId: args.analysisId, request: args.request, session: args.ctx.session, signal: args.ctx.signal });
299
+ ...(deps.usageRecorder ? { usageRecorder: deps.usageRecorder } : {}),
300
+ }, {
301
+ analysisId: args.analysisId,
302
+ request: args.request,
303
+ session: args.ctx.session,
304
+ signal: args.ctx.signal,
305
+ ...(args.ctx.turnUsage ? { turnUsage: args.ctx.turnUsage } : {}),
306
+ invocationId: args.ctx.invocationId,
307
+ });
300
308
  const candidate = buildAdHocPlan(args.request, route);
301
309
  unwrapOrThrow(await upsertPlan(deps.pool, { planId: args.planId, analysisId: args.analysisId, plan: candidate }));
302
310
  const stored = unwrapOrThrow(await loadPlan(deps.pool, args.planId, { analysisId: args.analysisId }));
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `read_tool_output` — reads the kept text of a tool result that the loop cut, by a window or by a pattern.
3
+ * Each result stays under the cap of the loop, thus the loop never cuts a result of this tool.
4
+ */
5
+ import { type ToolOutputStore } from "../loop/tool-output.js";
6
+ import { type Tool } from "./define-tool.js";
7
+ export declare function createReadToolOutputTool(store: ToolOutputStore): Tool;