@inflexa-ai/harness 0.39.2 → 0.41.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 +104 -12
  10. package/dist/app/chat-turn.js +180 -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,38 +1,10 @@
1
1
  /**
2
- * Chat-turn message assembly.
2
+ * Chat-turn message assembly: the view of the latest compaction marker, the user message, and then the
3
+ * context records. A kind gets a new record only when the view holds none of that kind, or when the hash
4
+ * of its text differs. The root loop compacts the view past its budget.
3
5
  *
4
- * Builds the `messages` array `runAgent` receives for one conversation turn:
5
- *
6
- * [ ...loadRecent(threadId, budget) ← stable, cacheable prefix
7
- * {user: cortex_analysis_state.context}, ← tail
8
- * {user: runActivityContext}, ← tail
9
- * {user: render(workingMemory)}, ← tail, `conversation` thread only
10
- * {user: normalizeUnicode(redactSecrets(input))} ] ← tail
11
- *
12
- * `system + tools + history` is the cacheable prefix — it only extends
13
- * turn-to-turn. Run activity, working memory, and analysis context go in the
14
- * **tail** as `user` messages: they change every turn, so a system-message
15
- * placement would bust the Anthropic cache prefix.
16
- *
17
- * A `report` thread does not get the working-memory tail. The spawn copies the
18
- * working-memory render into the child transcript at the anchor. A live render
19
- * sees state past that anchor, and that breaks the knowledge cap of a report
20
- * session. The analysis context and the run activity stay on both thread types.
21
- *
22
- * The history window of a `report` thread also keeps the first turn. That turn
23
- * is the seed, and it is the one record of the brief and of the copied render.
24
- * The window evicts the oldest turn first, thus a long session would drop its
25
- * own charter and keep only its tools. A `conversation` thread evicts as before,
26
- * because its live tail carries the working memory on each turn.
27
- *
28
- * Sanitization (`redactSecrets`, `normalizeUnicode`) is applied **once**, to
29
- * the new user input only — never to history, assistant messages, tool
30
- * results, the analysis context, or the rendered working memory.
31
- *
32
- * The returned `userMessage` is the sanitized user input on its own — the
33
- * route persists `[userMessage, ...loop output]` via `appendTurn`, so the
34
- * tail injections (ephemeral, re-derived each turn) are never written to the
35
- * thread store.
6
+ * The view of a `report` thread keeps the first turn, because that turn is the seed: the one record of
7
+ * the brief and of the copied working memory.
36
8
  */
37
9
  import type { ModelMessage } from "ai";
38
10
  import type { Logger } from "../lib/logger.js";
@@ -40,16 +12,10 @@ import type { LoopMessage } from "../loop/types.js";
40
12
  import type { ThreadHistory } from "../memory/thread-history.js";
41
13
  import type { ThreadType } from "../memory/thread-store.js";
42
14
  import type { WorkingMemoryStore } from "../memory/working-memory.js";
43
- /**
44
- * Token budget for the `loadRecent` history window. Sized to leave headroom
45
- * under the model context window for the system prompt, tool definitions,
46
- * the three tail messages, and the output budget.
47
- */
48
- export declare const DEFAULT_HISTORY_TOKEN_BUDGET = 120000;
49
15
  export interface AssembleMessagesArgs {
50
16
  /** The conversation thread — a UI-generated UUID, never the analysisId. */
51
17
  readonly threadId: string;
52
- /** The type of the thread. A `report` thread drops the working-memory tail, and its window keeps the seed. */
18
+ /** The type of the thread. A `report` thread gets no working-memory record, and its view keeps the seed. */
53
19
  readonly threadType: ThreadType;
54
20
  /** The analysis scope — keys working memory and (separately) the context. */
55
21
  readonly analysisId: string;
@@ -57,29 +23,27 @@ export interface AssembleMessagesArgs {
57
23
  readonly userInput: string;
58
24
  /** `cortex_analysis_state.context`, already read by the route. `null` when absent. */
59
25
  readonly analysisContext: string | null;
60
- /** Fresh, non-persisted analysis-wide run activity rendered by chat-turn preparation. */
26
+ /** The analysis-wide run activity that chat-turn preparation rendered for this turn. */
61
27
  readonly runActivityContext: string;
62
- /** The conversation message store — supplies the history window. */
28
+ /** The conversation message store — supplies the view of the thread. */
63
29
  readonly history: ThreadHistory;
64
- /** The working-memory store — rendered into the tail. */
30
+ /** The working-memory store — rendered into a context record. */
65
31
  readonly workingMemory: WorkingMemoryStore;
66
- /** History-window token budget. Defaults to {@link DEFAULT_HISTORY_TOKEN_BUDGET}. */
67
- readonly tokenBudget?: number;
68
32
  /** Diagnostic seam for the history repair record; omitted falls back to no-op. */
69
33
  readonly logger?: Logger;
70
34
  }
71
35
  export interface AssembledMessages {
72
36
  /** The full message array for `runAgent`. */
73
37
  readonly messages: LoopMessage[];
74
- /**
75
- * The sanitized user input as its own message — the genuine turn start.
76
- * The route persists this plus the loop's output; the tail injections are
77
- * not persisted.
78
- */
38
+ /** The sanitized user input as its own message — the genuine turn start. */
79
39
  readonly userMessage: ModelMessage;
40
+ /** The context records after the user message. The turn stores them with the user message. */
41
+ readonly contextRecords: readonly ModelMessage[];
80
42
  }
81
43
  /**
82
- * Assemble the message array for one chat turn. Async — it reads the history
83
- * window and renders working memory.
44
+ * Assemble the message array for one chat turn. Async — it reads the view of
45
+ * the thread and renders working memory.
84
46
  */
85
47
  export declare function assembleMessages(args: AssembleMessagesArgs): Promise<AssembledMessages>;
48
+ /** The context records of one turn, in the order analysis context, run activity, working memory. */
49
+ export declare function contextRecordsFor(args: Pick<AssembleMessagesArgs, "threadType" | "analysisId" | "analysisContext" | "runActivityContext" | "workingMemory">, history: readonly ModelMessage[]): Promise<ModelMessage[]>;
@@ -1,66 +1,33 @@
1
1
  /**
2
- * Chat-turn message assembly.
2
+ * Chat-turn message assembly: the view of the latest compaction marker, the user message, and then the
3
+ * context records. A kind gets a new record only when the view holds none of that kind, or when the hash
4
+ * of its text differs. The root loop compacts the view past its budget.
3
5
  *
4
- * Builds the `messages` array `runAgent` receives for one conversation turn:
5
- *
6
- * [ ...loadRecent(threadId, budget) ← stable, cacheable prefix
7
- * {user: cortex_analysis_state.context}, ← tail
8
- * {user: runActivityContext}, ← tail
9
- * {user: render(workingMemory)}, ← tail, `conversation` thread only
10
- * {user: normalizeUnicode(redactSecrets(input))} ] ← tail
11
- *
12
- * `system + tools + history` is the cacheable prefix — it only extends
13
- * turn-to-turn. Run activity, working memory, and analysis context go in the
14
- * **tail** as `user` messages: they change every turn, so a system-message
15
- * placement would bust the Anthropic cache prefix.
16
- *
17
- * A `report` thread does not get the working-memory tail. The spawn copies the
18
- * working-memory render into the child transcript at the anchor. A live render
19
- * sees state past that anchor, and that breaks the knowledge cap of a report
20
- * session. The analysis context and the run activity stay on both thread types.
21
- *
22
- * The history window of a `report` thread also keeps the first turn. That turn
23
- * is the seed, and it is the one record of the brief and of the copied render.
24
- * The window evicts the oldest turn first, thus a long session would drop its
25
- * own charter and keep only its tools. A `conversation` thread evicts as before,
26
- * because its live tail carries the working memory on each turn.
27
- *
28
- * Sanitization (`redactSecrets`, `normalizeUnicode`) is applied **once**, to
29
- * the new user input only — never to history, assistant messages, tool
30
- * results, the analysis context, or the rendered working memory.
31
- *
32
- * The returned `userMessage` is the sanitized user input on its own — the
33
- * route persists `[userMessage, ...loop output]` via `appendTurn`, so the
34
- * tail injections (ephemeral, re-derived each turn) are never written to the
35
- * thread store.
6
+ * The view of a `report` thread keeps the first turn, because that turn is the seed: the one record of
7
+ * the brief and of the copied working memory.
36
8
  */
37
9
  import { createNoopLogger } from "../lib/console-logger.js";
38
10
  import { unwrapOrThrow } from "../lib/result.js";
39
- import { stripUnansweredToolCalls } from "../memory/tool-call-integrity.js";
11
+ import { contextRecordMessage, contextRecordOf } from "../memory/ai-sdk-message-storage.js";
12
+ import { answerUnansweredToolCalls } from "../memory/tool-call-integrity.js";
40
13
  import { normalizeUnicode, redactSecrets } from "../input-sanitization.js";
41
14
  /**
42
- * Token budget for the `loadRecent` history window. Sized to leave headroom
43
- * under the model context window for the system prompt, tool definitions,
44
- * the three tail messages, and the output budget.
45
- */
46
- export const DEFAULT_HISTORY_TOKEN_BUDGET = 120_000;
47
- /**
48
- * Assemble the message array for one chat turn. Async — it reads the history
49
- * window and renders working memory.
15
+ * Assemble the message array for one chat turn. Async — it reads the view of
16
+ * the thread and renders working memory.
50
17
  */
51
18
  export async function assembleMessages(args) {
52
- const budget = args.tokenBudget ?? DEFAULT_HISTORY_TOKEN_BUDGET;
53
- const history = unwrapOrThrow(await args.history.loadRecent(args.threadId, budget, { keepFirstTurn: args.threadType === "report" }));
19
+ const history = unwrapOrThrow(await args.history.loadRecent(args.threadId, { keepFirstTurn: args.threadType === "report" }));
54
20
  // Read-side safety net for the wire contract: every tool call carries a
55
21
  // result. The loop upholds it at every exit, but the store outlives any one
56
22
  // writer — a row an older build wrote, or a hand-edited database, can carry
57
23
  // an unanswered call, and the provider boundary then refuses the WHOLE
58
- // transcript on every turn: the thread is wedged forever. Stripping the
59
- // orphan here degrades one message instead. The warning is the only account
60
- // a reader gets that the stored thread and the assembled one differ.
61
- const repaired = stripUnansweredToolCalls(history);
24
+ // transcript on every turn: the thread is wedged forever. The fix inserts the
25
+ // same not-run result the loop gives, in place, so no stored message changes
26
+ // and the prefix stays identical across turns. The warning is the only
27
+ // account a reader gets that the stored thread and the assembled one differ.
28
+ const repaired = answerUnansweredToolCalls(history);
62
29
  if (repaired.length > 0) {
63
- (args.logger ?? createNoopLogger()).named("assembly").warn("unanswered tool calls stripped from thread history", {
30
+ (args.logger ?? createNoopLogger()).named("assembly").warn("unanswered tool calls answered in thread history", {
64
31
  threadId: args.threadId,
65
32
  toolCallIds: repaired.map((d) => d.toolCallId),
66
33
  tools: repaired.map((d) => d.toolName),
@@ -71,27 +38,33 @@ export async function assembleMessages(args) {
71
38
  role: "user",
72
39
  content: normalizeUnicode(redactSecrets(args.userInput)),
73
40
  };
74
- const tail = [];
75
- // Analysis context — platform-supplied, trusted. Injected only when present.
41
+ const contextRecords = await contextRecordsFor(args, history);
42
+ return { messages: [...history, userMessage, ...contextRecords], userMessage, contextRecords };
43
+ }
44
+ /** The context records of one turn, in the order analysis context, run activity, working memory. */
45
+ export async function contextRecordsFor(args, history) {
46
+ const texts = [];
76
47
  if (args.analysisContext && args.analysisContext.trim().length > 0) {
77
- tail.push({
78
- role: "user",
79
- content: `[Analysis Context]\n${args.analysisContext}`,
80
- });
48
+ texts.push(["analysis-context", `[Analysis Context]\n${args.analysisContext}`]);
81
49
  }
82
- tail.push({
83
- role: "user",
84
- content: args.runActivityContext,
85
- });
86
- // Working memory — agent-authored and trusted. A `conversation` thread
87
- // always gets it, because the render names each section even when it is
88
- // empty. A `report` thread reads the frozen copy in its seed message.
50
+ texts.push(["run-activity", args.runActivityContext]);
51
+ // A `report` thread reads the frozen copy in its seed, because a live render sees state past its anchor.
89
52
  if (args.threadType !== "report") {
90
- tail.push({
91
- role: "user",
92
- content: unwrapOrThrow(await args.workingMemory.render(args.analysisId)),
93
- });
53
+ const render = unwrapOrThrow(await args.workingMemory.render(args.analysisId));
54
+ // An emptied memory needs a record of its own, or the last full copy would read as the current state.
55
+ texts.push(["working-memory", `[Working Memory]\n${render.length > 0 ? render : "The working memory is empty."}`]);
56
+ }
57
+ const latestHash = new Map();
58
+ for (const message of history) {
59
+ const record = contextRecordOf(message);
60
+ if (record !== undefined)
61
+ latestHash.set(record.kind, record.hash);
62
+ }
63
+ const records = [];
64
+ for (const [kind, text] of texts) {
65
+ const record = contextRecordMessage(kind, text);
66
+ if (contextRecordOf(record)?.hash !== latestHash.get(kind))
67
+ records.push(record);
94
68
  }
95
- tail.push(userMessage);
96
- return { messages: [...history, ...tail], userMessage };
69
+ return records;
97
70
  }
@@ -1,13 +1,11 @@
1
1
  /**
2
- * Ephemeral conversation-tail projection of non-terminal analysis runs.
3
- *
4
- * This is derived operational state, not memory: callers render it afresh for
5
- * each turn and never persist it to the thread.
2
+ * The Run Activity record of a chat turn. The render holds no time of the call, thus a set of runs
3
+ * that does not change gives byte-identical text, and the turn stores no new record.
6
4
  */
7
5
  import type { RunPage } from "../state/runs.js";
8
6
  /** Maximum detailed run rows paid into every conversation turn. */
9
7
  export declare const RUN_ACTIVITY_DETAIL_LIMIT = 20;
10
- /** Render the bounded run-activity snapshot injected into a conversation tail. */
11
- export declare function renderRunActivity(activity: RunPage, nowMs?: number): string;
8
+ /** Render the bounded run-activity snapshot of a conversation turn. */
9
+ export declare function renderRunActivity(activity: RunPage): string;
12
10
  /** Render an honest degraded state when the activity projection cannot be read. */
13
11
  export declare function renderRunActivityUnavailable(): string;
@@ -1,36 +1,16 @@
1
1
  /**
2
- * Ephemeral conversation-tail projection of non-terminal analysis runs.
3
- *
4
- * This is derived operational state, not memory: callers render it afresh for
5
- * each turn and never persist it to the thread.
2
+ * The Run Activity record of a chat turn. The render holds no time of the call, thus a set of runs
3
+ * that does not change gives byte-identical text, and the turn stores no new record.
6
4
  */
7
5
  /** Maximum detailed run rows paid into every conversation turn. */
8
6
  export const RUN_ACTIVITY_DETAIL_LIMIT = 20;
9
- function compactAge(startedAt, nowMs) {
10
- const startedMs = Date.parse(startedAt);
11
- if (!Number.isFinite(startedMs))
12
- return "unknown age";
13
- const elapsedSeconds = Math.max(0, Math.floor((nowMs - startedMs) / 1_000));
14
- if (elapsedSeconds < 60)
15
- return `${elapsedSeconds}s ago`;
16
- const elapsedMinutes = Math.floor(elapsedSeconds / 60);
17
- if (elapsedMinutes < 60)
18
- return `${elapsedMinutes}m ago`;
19
- const elapsedHours = Math.floor(elapsedMinutes / 60);
20
- if (elapsedHours < 24)
21
- return `${elapsedHours}h ago`;
22
- return `${Math.floor(elapsedHours / 24)}d ago`;
23
- }
24
- function renderRows(label, rows, nowMs) {
7
+ function renderRows(label, rows) {
25
8
  if (rows.length === 0)
26
9
  return [];
27
- return [
28
- `${label}:`,
29
- ...rows.map((run) => `- runId: ${run.runId} | planId: ${run.planId ?? "none"} | startedAt: ${run.startedAt} | started: ${compactAge(run.startedAt, nowMs)}`),
30
- ];
10
+ return [`${label}:`, ...rows.map((run) => `- runId: ${run.runId} | planId: ${run.planId ?? "none"} | startedAt: ${run.startedAt}`)];
31
11
  }
32
- /** Render the bounded run-activity snapshot injected into a conversation tail. */
33
- export function renderRunActivity(activity, nowMs = Date.now()) {
12
+ /** Render the bounded run-activity snapshot of a conversation turn. */
13
+ export function renderRunActivity(activity) {
34
14
  const lines = ["[Run Activity]"];
35
15
  if (activity.total === 0) {
36
16
  lines.push("No runs are currently running or suspended.");
@@ -38,7 +18,7 @@ export function renderRunActivity(activity, nowMs = Date.now()) {
38
18
  }
39
19
  const running = activity.runs.filter((run) => run.status === "running");
40
20
  const suspended = activity.runs.filter((run) => run.status === "suspended_insufficient_funds");
41
- lines.push(...renderRows("Running", running, nowMs), ...renderRows("Suspended", suspended, nowMs));
21
+ lines.push(...renderRows("Running", running), ...renderRows("Suspended", suspended));
42
22
  const omitted = activity.total - activity.runs.length;
43
23
  if (omitted > 0) {
44
24
  lines.push(`Showing ${activity.runs.length} of ${activity.total} non-terminal runs; ${omitted} omitted. Use inspect_run to page the full list.`);
@@ -77,11 +77,10 @@ import type { EnsureSessionStateResult } from "./report-session-runtime.js";
77
77
  * The set is closed. `parent_not_found` covers an absent parent and an archived
78
78
  * one alike, because `getThread` filters the tombstone and a spawn into hidden
79
79
  * state is not permitted. `parent_not_a_conversation` keeps the tree flat: a
80
- * report session cannot spawn another. `empty_parent_transcript` refuses a
81
- * report on a parent that holds no messages, because such a report reports
82
- * nothing. `no_browser` refuses every spawn under a composition that gives no
83
- * route to a look: no eyes seam, no capture seam, and no configured browser
84
- * endpoint. It carries the line that explains the absent capability.
80
+ * report session cannot spawn another. `no_browser` refuses every spawn under
81
+ * a composition that gives no route to a look: no eyes seam, no capture seam,
82
+ * and no configured browser endpoint. It carries the line that explains the
83
+ * absent capability.
85
84
  */
86
85
  export type SpawnRefusal = {
87
86
  readonly type: "no_browser";
@@ -97,10 +96,6 @@ export type SpawnRefusal = {
97
96
  readonly op: string;
98
97
  readonly parentThreadId: string;
99
98
  readonly threadType: ThreadType;
100
- } | {
101
- readonly type: "empty_parent_transcript";
102
- readonly op: string;
103
- readonly parentThreadId: string;
104
99
  };
105
100
  /** The paging inputs `listReportSessions` forwards to the thread listing. */
106
101
  export interface ReportSessionPaging {
@@ -237,8 +232,8 @@ export interface ReportSessionSpawn {
237
232
  * result on the ok channel, thus the outcome vocabulary holds no pin arm.
238
233
  *
239
234
  * Refused with a `SpawnRefusal`, no row written: a composition with no
240
- * eyes, an absent or archived parent, a parent that is not a conversation,
241
- * and a parent with no messages. A store refusal (`DbError`,
235
+ * eyes, an absent or archived parent, and a parent that is not a
236
+ * conversation. A store refusal (`DbError`,
242
237
  * `ThreadInputError`) passes through unchanged. A failed seed write purges
243
238
  * the child and returns the fault, thus no context-less thread survives.
244
239
  */
@@ -264,12 +264,11 @@ export function createReportSessionSpawn(deps) {
264
264
  if (parent.threadType !== "conversation") {
265
265
  return errAsync({ type: "parent_not_a_conversation", op: OP, parentThreadId, threadType: parent.threadType });
266
266
  }
267
- return history.latestSeq(parentThreadId).andThen((anchor) => {
268
- // `null` is a parent with no messages. A report on an empty transcript
269
- // reports nothing, so the refusal is the correct answer.
270
- if (anchor === null) {
271
- return errAsync({ type: "empty_parent_transcript", op: OP, parentThreadId });
272
- }
267
+ return history.latestSeq(parentThreadId).andThen((latest) => {
268
+ // A turn appends after its loop, thus the first ask of a chat finds
269
+ // no row. The seed and the tail carry the context, not the parent
270
+ // transcript. `-1` is the point before seq 0.
271
+ const anchor = latest ?? -1;
273
272
  // `total`, not the page length: N counts every existing report child,
274
273
  // not the count on one page. Two concurrent spawns can compose one N.
275
274
  // The result is two titles a user renames, and no identifier collides.
@@ -17,6 +17,7 @@
17
17
  import type { Pool } from "pg";
18
18
  import type { SynthesisPhase } from "@inflexa-ai/harness/contracts/chat-parts.js";
19
19
  import type { RunSession } from "../auth/types.js";
20
+ import type { ToolOutputStore } from "../loop/tool-output.js";
20
21
  import type { EmitFn } from "../loop/types.js";
21
22
  import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
22
23
  import type { BioToolKeys } from "../tools/bio/keys.js";
@@ -43,6 +44,8 @@ export interface SynthesizeRunDeps {
43
44
  readonly citationResolver: CitationResolver;
44
45
  /** LLM usage-accounting seam for the synthesizer loop; omitted falls back to the no-op recorder. */
45
46
  readonly usageRecorder?: UsageRecorder;
47
+ /** The store of the synthesizer and reviewer loops and of their read tools. */
48
+ readonly toolOutputStore?: ToolOutputStore;
46
49
  }
47
50
  /** Optional fields for a progress update, mirroring the wire part. */
48
51
  export interface SynthesisProgressExtra {
@@ -49,6 +49,7 @@ export async function synthesizeRun(deps, params) {
49
49
  bioKeys,
50
50
  citationResolver,
51
51
  usageRecorder,
52
+ ...(deps.toolOutputStore ? { toolOutputStore: deps.toolOutputStore } : {}),
52
53
  summaries,
53
54
  planNarrative,
54
55
  runId,
@@ -6,10 +6,9 @@
6
6
  * telemetry every completed LLM call produces. The harness names no storage or
7
7
  * display technology — it emits records; the embedder decides where they land.
8
8
  *
9
- * The loop is the single delivery site (it sees every reply in both execution
10
- * modes and already holds the session), so a realization is wired once at the
11
- * composition root and reaches every `runAgent` invocation through the deps
12
- * bags. OSS default: `createNoopUsageRecorder` (`./noop-usage-recorder.ts`).
9
+ * Delivery sites: the loop's `accountForChatCall`, and the two direct
10
+ * `provider.chat` callers outside a loop — the ad hoc router and the analogy
11
+ * conversion. OSS default: `createNoopUsageRecorder` (`./noop-usage-recorder.ts`).
13
12
  */
14
13
  import type { ResultAsync } from "neverthrow";
15
14
  import type { Scope } from "../auth/types.js";
@@ -6,9 +6,8 @@
6
6
  * telemetry every completed LLM call produces. The harness names no storage or
7
7
  * display technology — it emits records; the embedder decides where they land.
8
8
  *
9
- * The loop is the single delivery site (it sees every reply in both execution
10
- * modes and already holds the session), so a realization is wired once at the
11
- * composition root and reaches every `runAgent` invocation through the deps
12
- * bags. OSS default: `createNoopUsageRecorder` (`./noop-usage-recorder.ts`).
9
+ * Delivery sites: the loop's `accountForChatCall`, and the two direct
10
+ * `provider.chat` callers outside a loop — the ad hoc router and the analogy
11
+ * conversion. OSS default: `createNoopUsageRecorder` (`./noop-usage-recorder.ts`).
13
12
  */
14
13
  export {};
@@ -6,6 +6,7 @@
6
6
  * Presentation: data-presentation (agent-synthesized content),
7
7
  * data-plan (plan by id), data-file-reference (files by path)
8
8
  * Approval: data-ask (a tool's pending user-approval prompt)
9
+ * Compaction: data-compaction (a compaction of the conversation thread)
9
10
  * Run lifecycle: run-started, dag-state, run-completed, run-failed, run-synthesis,
10
11
  * synthesis-progress
11
12
  * Per-step live: step-activity, step-file-tree
@@ -382,4 +383,13 @@ export interface ReportRenderedPart {
382
383
  /** The title of the rendered document. */
383
384
  title?: string;
384
385
  }
385
- export type CortexChatPart = PresentationPart | PlanPart | RunCardPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart;
386
+ /** One compaction of the thread: `running`, then `done` or `failed` under the same id. A reload shows it as a divider. */
387
+ export interface CompactionPart {
388
+ type: "data-compaction";
389
+ id: string;
390
+ status: "running" | "done" | "failed";
391
+ tokensBefore: number;
392
+ tokensAfter?: number;
393
+ durationMs?: number;
394
+ }
395
+ export type CortexChatPart = PresentationPart | PlanPart | RunCardPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart | CompactionPart;
@@ -6,6 +6,7 @@
6
6
  * Presentation: data-presentation (agent-synthesized content),
7
7
  * data-plan (plan by id), data-file-reference (files by path)
8
8
  * Approval: data-ask (a tool's pending user-approval prompt)
9
+ * Compaction: data-compaction (a compaction of the conversation thread)
9
10
  * Run lifecycle: run-started, dag-state, run-completed, run-failed, run-synthesis,
10
11
  * synthesis-progress
11
12
  * Per-step live: step-activity, step-file-tree
@@ -2,7 +2,7 @@ export { ORGAN_SYSTEMS, OrganSystemSchema, ORGAN_SYSTEM_LABELS, organSystemLabel
2
2
  export type { OrganSystem } from "./organ-system.js";
3
3
  export { SEVERITIES, SeveritySchema } from "./severity.js";
4
4
  export type { Severity } from "./severity.js";
5
- export type { PlanStep, PresentationContent, PresentationPart, PlanPart, RunCardPart, FileReferenceEntry, FileReferencePart, AskPart, RunStartedPart, StepStatus, DagStepState, DagStatePart, StepPhase, StepActivityPart, FileTreeEntry, StepFileTreePart, StepOutputFile, StepOutputPart, StepSummaryPart, StepUsagePart, StepBlockedPart, SynthesizedFinding, BiologicalTheme, RunSynthesisPart, SynthesisPhase, SynthesisProgressPart, RunCompletedFinding, RunCompletedPart, RunFailedPart, ChildSessionStartedPart, ReportRenderedPart, CortexChatPart, } from "./chat-parts.js";
5
+ export type { PlanStep, PresentationContent, PresentationPart, PlanPart, RunCardPart, FileReferenceEntry, FileReferencePart, AskPart, RunStartedPart, StepStatus, DagStepState, DagStatePart, StepPhase, StepActivityPart, FileTreeEntry, StepFileTreePart, StepOutputFile, StepOutputPart, StepSummaryPart, StepUsagePart, StepBlockedPart, SynthesizedFinding, BiologicalTheme, RunSynthesisPart, SynthesisPhase, SynthesisProgressPart, RunCompletedFinding, RunCompletedPart, RunFailedPart, ChildSessionStartedPart, ReportRenderedPart, CompactionPart, CortexChatPart, } from "./chat-parts.js";
6
6
  export type { EventSource, TextDeltaEvent, ToolStartedEvent, ToolFinishedEvent, FinishEvent, ChatErrorEvent, CortexChatEvent } from "./chat-events.js";
7
7
  export type { TokenUsageRollup } from "./usage.js";
8
8
  export { DATA_PROFILE_RUN_LITERAL } from "./data-profile.js";
@@ -18,6 +18,7 @@ export const PART_REGISTRY = {
18
18
  "data-run-failed": { emitter: "workflow", consumer: "sidebar", transient: false, reconciling: false },
19
19
  "data-child-session-started": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
20
20
  "data-report-rendered": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
21
+ "data-compaction": { emitter: "conversation", consumer: "conversation", transient: true, reconciling: true },
21
22
  };
22
23
  export function isTransient(type) {
23
24
  return PART_REGISTRY[type].transient;
@@ -200,15 +200,15 @@ export declare const ChartAxesSchema: z.ZodObject<{
200
200
  x: z.ZodOptional<z.ZodObject<{
201
201
  title: z.ZodOptional<z.ZodString>;
202
202
  scale: z.ZodOptional<z.ZodEnum<{
203
- linear: "linear";
204
203
  log: "log";
204
+ linear: "linear";
205
205
  }>>;
206
206
  }, z.core.$strict>>;
207
207
  y: z.ZodOptional<z.ZodObject<{
208
208
  title: z.ZodOptional<z.ZodString>;
209
209
  scale: z.ZodOptional<z.ZodEnum<{
210
- linear: "linear";
211
210
  log: "log";
211
+ linear: "linear";
212
212
  }>>;
213
213
  }, z.core.$strict>>;
214
214
  }, z.core.$strict>;
@@ -297,15 +297,15 @@ export declare const ChartCompositionSchema: z.ZodObject<{
297
297
  x: z.ZodOptional<z.ZodObject<{
298
298
  title: z.ZodOptional<z.ZodString>;
299
299
  scale: z.ZodOptional<z.ZodEnum<{
300
- linear: "linear";
301
300
  log: "log";
301
+ linear: "linear";
302
302
  }>>;
303
303
  }, z.core.$strict>>;
304
304
  y: z.ZodOptional<z.ZodObject<{
305
305
  title: z.ZodOptional<z.ZodString>;
306
306
  scale: z.ZodOptional<z.ZodEnum<{
307
- linear: "linear";
308
307
  log: "log";
308
+ linear: "linear";
309
309
  }>>;
310
310
  }, z.core.$strict>>;
311
311
  }, z.core.$strict>>;
@@ -724,15 +724,15 @@ export declare const ChartBlockSchema: z.ZodObject<{
724
724
  x: z.ZodOptional<z.ZodObject<{
725
725
  title: z.ZodOptional<z.ZodString>;
726
726
  scale: z.ZodOptional<z.ZodEnum<{
727
- linear: "linear";
728
727
  log: "log";
728
+ linear: "linear";
729
729
  }>>;
730
730
  }, z.core.$strict>>;
731
731
  y: z.ZodOptional<z.ZodObject<{
732
732
  title: z.ZodOptional<z.ZodString>;
733
733
  scale: z.ZodOptional<z.ZodEnum<{
734
- linear: "linear";
735
734
  log: "log";
735
+ linear: "linear";
736
736
  }>>;
737
737
  }, z.core.$strict>>;
738
738
  }, z.core.$strict>>;
@@ -1168,15 +1168,15 @@ export declare const ATOM_BLOCK_SCHEMAS: readonly [z.ZodObject<{
1168
1168
  x: z.ZodOptional<z.ZodObject<{
1169
1169
  title: z.ZodOptional<z.ZodString>;
1170
1170
  scale: z.ZodOptional<z.ZodEnum<{
1171
- linear: "linear";
1172
1171
  log: "log";
1172
+ linear: "linear";
1173
1173
  }>>;
1174
1174
  }, z.core.$strict>>;
1175
1175
  y: z.ZodOptional<z.ZodObject<{
1176
1176
  title: z.ZodOptional<z.ZodString>;
1177
1177
  scale: z.ZodOptional<z.ZodEnum<{
1178
- linear: "linear";
1179
1178
  log: "log";
1179
+ linear: "linear";
1180
1180
  }>>;
1181
1181
  }, z.core.$strict>>;
1182
1182
  }, z.core.$strict>>;