@inflexa-ai/harness 0.19.2 → 0.21.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 (139) hide show
  1. package/dist/agents/conversation-agent.d.ts +26 -14
  2. package/dist/agents/conversation-agent.js +12 -23
  3. package/dist/agents/report-session-agent.d.ts +31 -5
  4. package/dist/agents/report-session-agent.js +31 -6
  5. package/dist/app/chat-turn.js +3 -1
  6. package/dist/app/message-assembly.d.ts +15 -1
  7. package/dist/app/message-assembly.js +22 -8
  8. package/dist/app/report-session-runtime.d.ts +21 -4
  9. package/dist/app/report-session-runtime.js +53 -6
  10. package/dist/app/spawn-report-session.d.ts +174 -17
  11. package/dist/app/spawn-report-session.js +233 -21
  12. package/dist/contracts/chat-events.d.ts +7 -2
  13. package/dist/contracts/message.d.ts +10 -0
  14. package/dist/contracts/report-blocks.d.ts +743 -19
  15. package/dist/contracts/report-blocks.js +186 -11
  16. package/dist/contracts/report-reference.d.ts +88 -0
  17. package/dist/contracts/report-reference.js +48 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.js +20 -0
  20. package/dist/lib/chrome.d.ts +26 -6
  21. package/dist/lib/chrome.js +99 -32
  22. package/dist/lib/eyes.d.ts +57 -0
  23. package/dist/lib/eyes.js +45 -0
  24. package/dist/lib/page-capture.d.ts +14 -0
  25. package/dist/lib/page-capture.js +56 -2
  26. package/dist/literature/sources/pubmed.js +2 -1
  27. package/dist/loop/run-agent.js +58 -23
  28. package/dist/loop/tool-detail.d.ts +19 -2
  29. package/dist/loop/tool-detail.js +35 -2
  30. package/dist/memory/content-to-cortex.d.ts +3 -2
  31. package/dist/memory/content-to-cortex.js +21 -12
  32. package/dist/memory/conversation-display-replay.js +14 -7
  33. package/dist/memory/thread-history.d.ts +60 -3
  34. package/dist/memory/thread-history.js +55 -16
  35. package/dist/memory/thread-store.d.ts +11 -1
  36. package/dist/memory/thread-store.js +12 -2
  37. package/dist/prompts/conversation.d.ts +1 -1
  38. package/dist/prompts/conversation.js +23 -6
  39. package/dist/prompts/report-session.d.ts +1 -1
  40. package/dist/prompts/report-session.js +183 -9
  41. package/dist/report-model/authoring-grammar.d.ts +463 -0
  42. package/dist/report-model/authoring-grammar.js +77 -0
  43. package/dist/report-model/block-walk.d.ts +57 -14
  44. package/dist/report-model/block-walk.js +116 -11
  45. package/dist/report-model/draft-finish.d.ts +22 -4
  46. package/dist/report-model/draft-finish.js +39 -7
  47. package/dist/report-model/draft-operations.d.ts +3 -0
  48. package/dist/report-model/draft-operations.js +19 -5
  49. package/dist/report-model/draft-read.d.ts +8 -5
  50. package/dist/report-model/draft-read.js +21 -5
  51. package/dist/report-model/fixture-resolver.js +15 -4
  52. package/dist/report-model/pin-snapshot.d.ts +24 -6
  53. package/dist/report-model/pin-snapshot.js +204 -6
  54. package/dist/report-model/production-resolver.js +15 -4
  55. package/dist/report-model/reference-resolver.d.ts +50 -1
  56. package/dist/report-model/reference-resolver.js +94 -1
  57. package/dist/report-model/reference-stamp.d.ts +45 -0
  58. package/dist/report-model/reference-stamp.js +173 -0
  59. package/dist/report-model/structural-validation.d.ts +4 -1
  60. package/dist/report-model/structural-validation.js +48 -3
  61. package/dist/report-model/validate.d.ts +1 -1
  62. package/dist/report-model/validate.js +1 -1
  63. package/dist/report-render/asset-lookup.d.ts +19 -0
  64. package/dist/report-render/asset-lookup.js +37 -0
  65. package/dist/report-render/assets.d.ts +45 -1
  66. package/dist/report-render/assets.js +65 -6
  67. package/dist/report-render/chart-presets.d.ts +102 -0
  68. package/dist/report-render/chart-presets.js +205 -0
  69. package/dist/report-render/chart.d.ts +112 -10
  70. package/dist/report-render/chart.js +1164 -41
  71. package/dist/report-render/design.d.ts +99 -0
  72. package/dist/report-render/design.js +289 -107
  73. package/dist/report-render/number-format.d.ts +117 -0
  74. package/dist/report-render/number-format.js +479 -0
  75. package/dist/report-render/page.d.ts +121 -5
  76. package/dist/report-render/page.js +689 -7
  77. package/dist/report-render/references.d.ts +70 -16
  78. package/dist/report-render/references.js +54 -22
  79. package/dist/report-render/render.d.ts +23 -5
  80. package/dist/report-render/render.js +81 -22
  81. package/dist/report-render/table-data.d.ts +92 -0
  82. package/dist/report-render/table-data.js +129 -0
  83. package/dist/report-render/types.d.ts +15 -0
  84. package/dist/report-render/value-bridge.js +6 -2
  85. package/dist/report-render/views/chart-view.d.ts +12 -5
  86. package/dist/report-render/views/chart-view.js +14 -8
  87. package/dist/report-render/views/page-view.d.ts +36 -15
  88. package/dist/report-render/views/page-view.js +45 -23
  89. package/dist/report-render/views/prose.d.ts +12 -2
  90. package/dist/report-render/views/prose.js +29 -7
  91. package/dist/report-render/views/references-view.d.ts +22 -8
  92. package/dist/report-render/views/references-view.js +101 -13
  93. package/dist/report-render/views/values.d.ts +128 -13
  94. package/dist/report-render/views/values.js +199 -18
  95. package/dist/runtime/assemble.d.ts +39 -5
  96. package/dist/runtime/assemble.js +47 -6
  97. package/dist/sandbox/client.d.ts +10 -1
  98. package/dist/sandbox/create-sandbox.d.ts +9 -4
  99. package/dist/sandbox/create-sandbox.js +22 -16
  100. package/dist/sandbox/docker-client.js +10 -7
  101. package/dist/sandbox/k8s-client.js +11 -18
  102. package/dist/sandbox/mount-plan.d.ts +45 -7
  103. package/dist/sandbox/mount-plan.js +43 -5
  104. package/dist/sandbox/types.d.ts +10 -6
  105. package/dist/state/init.js +38 -9
  106. package/dist/state/report-session-state.d.ts +42 -1
  107. package/dist/state/report-session-state.js +70 -2
  108. package/dist/state/report-versions.d.ts +22 -17
  109. package/dist/state/report-versions.js +48 -35
  110. package/dist/state/snapshot-parse.js +16 -1
  111. package/dist/tasks/data-profile.d.ts +7 -4
  112. package/dist/tasks/data-profile.js +9 -9
  113. package/dist/tools/define-tool.d.ts +32 -0
  114. package/dist/tools/define-tool.js +4 -0
  115. package/dist/tools/detail-resolver.d.ts +7 -2
  116. package/dist/tools/detail-resolver.js +7 -2
  117. package/dist/tools/iterate-report.d.ts +1 -1
  118. package/dist/tools/report/preview-snapshot.js +1 -0
  119. package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
  120. package/dist/tools/report-authoring/authoring-tools.js +99 -18
  121. package/dist/tools/report-session/derive-table.d.ts +162 -0
  122. package/dist/tools/report-session/derive-table.js +459 -0
  123. package/dist/tools/report-session/examine-page.d.ts +48 -18
  124. package/dist/tools/report-session/examine-page.js +204 -24
  125. package/dist/tools/report-session/index.d.ts +2 -0
  126. package/dist/tools/report-session/index.js +2 -0
  127. package/dist/tools/report-session/list-artifacts.d.ts +97 -0
  128. package/dist/tools/report-session/list-artifacts.js +197 -0
  129. package/dist/tools/report-session/preview-report.d.ts +6 -0
  130. package/dist/tools/report-session/preview-report.js +180 -28
  131. package/dist/tools/report-session/record-version.d.ts +22 -10
  132. package/dist/tools/report-session/record-version.js +99 -14
  133. package/dist/tools/start-report-session.d.ts +115 -0
  134. package/dist/tools/start-report-session.js +179 -0
  135. package/dist/workflows/sandbox-step.d.ts +7 -4
  136. package/dist/workflows/sandbox-step.js +10 -12
  137. package/dist/workspace/paths.d.ts +50 -0
  138. package/dist/workspace/paths.js +63 -0
  139. package/package.json +2 -1
@@ -2,8 +2,10 @@
2
2
  * The call detail: one line naming what a tool call is doing.
3
3
  *
4
4
  * A tool states it through its own `describeCall` hook, colocated with the Zod
5
- * `inputSchema` the compiler checks it against. Everything else about the
6
- * detail happens here, once, for every tool: validate, guard, normalize.
5
+ * `inputSchema` the compiler checks it against, and through its optional
6
+ * `describeResult` hook, which names what the call produced. Everything else
7
+ * about the detail happens here, once, for every tool: validate, guard,
8
+ * normalize.
7
9
  *
8
10
  * Two rules make this the only place that matters:
9
11
  *
@@ -135,3 +137,34 @@ export function computeDetail(tool, rawInput, log) {
135
137
  return undefined;
136
138
  }
137
139
  }
140
+ /**
141
+ * Compute a tool call's result detail, best-effort.
142
+ *
143
+ * `parsedInput` is the value the dispatch already validated, and `result` is the
144
+ * ok value the tool returned one line above. Thus neither is re-parsed here: a
145
+ * second parse of the input would repeat work the dispatch did, and the result
146
+ * satisfies no schema at all — the tool's own `Output` type is what the hook
147
+ * declares, and the tool is the author of both.
148
+ *
149
+ * The guard is the whole difference from a bare call. A hook that throws, returns
150
+ * a non-string, or returns nothing usable yields no result detail, and the
151
+ * finished event falls back on the call detail. A description must never be able
152
+ * to fail a call that already succeeded.
153
+ */
154
+ export function computeResultDetail(tool, parsedInput, result, log) {
155
+ // The guard tests for a function, not for presence, for the same reason as in
156
+ // `computeDetail`: a packaged tool comes from an open list, thus a
157
+ // `describeResult` that is not callable is reachable.
158
+ const describeResult = tool.describeResult;
159
+ if (typeof describeResult !== "function")
160
+ return undefined;
161
+ try {
162
+ return normalizeDetail(describeResult(parsedInput, result));
163
+ }
164
+ catch (err) {
165
+ // `debug`, not `warn`: a broken description is a cosmetic defect in one
166
+ // tool, and the call it happened on succeeded.
167
+ log.debug("describeResult failed", { tool: tool.id, ...log.errorFields(err) });
168
+ return undefined;
169
+ }
170
+ }
@@ -53,8 +53,9 @@ export interface ContentToCortexOptions {
53
53
  * optional and each tool-call part reports its outcome either way.
54
54
  * A row carrying the interruption marker sets `interrupted: true` on the message
55
55
  * it lands in, including when coalesced into an assistant run; a row carrying a
56
- * stored turn rollup sets `usage` the same way, so a reloaded conversation shows
57
- * the cost the live turn showed without a second query.
56
+ * stored turn rollup sets `usage` the same way. A row that carries a stored turn
57
+ * duration sets `durationMs` the same way too. Thus a reloaded conversation shows
58
+ * the cost and the time that the live turn showed, with no second query.
58
59
  *
59
60
  * Coalescing mirrors the live SSE shape. The agent loop persists each assistant
60
61
  * step (typically a single `tool_use`) as its own row, and the `tool_result`
@@ -99,8 +99,9 @@ async function rowToParts(message, outcomes, options) {
99
99
  * optional and each tool-call part reports its outcome either way.
100
100
  * A row carrying the interruption marker sets `interrupted: true` on the message
101
101
  * it lands in, including when coalesced into an assistant run; a row carrying a
102
- * stored turn rollup sets `usage` the same way, so a reloaded conversation shows
103
- * the cost the live turn showed without a second query.
102
+ * stored turn rollup sets `usage` the same way. A row that carries a stored turn
103
+ * duration sets `durationMs` the same way too. Thus a reloaded conversation shows
104
+ * the cost and the time that the live turn showed, with no second query.
104
105
  *
105
106
  * Coalescing mirrors the live SSE shape. The agent loop persists each assistant
106
107
  * step (typically a single `tool_use`) as its own row, and the `tool_result`
@@ -144,18 +145,20 @@ export async function contentToCortexMessages(messages, options = {}) {
144
145
  // assistant run it trailed, so fold the flag onto the previous emitted assistant.
145
146
  const interrupted = isInterruptedMessage(message.message);
146
147
  if (parts.length === 0) {
147
- // A row's stored rollup is a fact about the turn, not about what the row
148
- // renders — so like the interruption marker it is read before the
149
- // zero-parts drop and folded onto the assistant run it trailed. Without
150
- // this, a turn whose last assistant row held only unrendered blocks
151
- // (reasoning alone) would lose the figure to the drop, which is the exact
152
- // disappearance persisting it exists to fix.
148
+ // The stored rollup of a row and its stored duration are facts about the turn,
149
+ // and not about what the row renders. Thus the read of both comes before the
150
+ // zero-parts drop, exactly as the read of the interruption marker does. The
151
+ // fold then puts them on the assistant run that they trailed. Without the fold,
152
+ // a turn whose last assistant row held unrendered blocks alone would lose the
153
+ // two figures to the drop. That loss is the one that the stored figures end.
153
154
  const prev = out[out.length - 1];
154
155
  if (prev && prev.role === "assistant") {
155
156
  if (interrupted)
156
157
  prev.interrupted = true;
157
158
  if (message.usage)
158
159
  prev.usage = message.usage;
160
+ if (message.durationMs !== undefined)
161
+ prev.durationMs = message.durationMs;
159
162
  }
160
163
  continue;
161
164
  }
@@ -173,12 +176,16 @@ export async function contentToCortexMessages(messages, options = {}) {
173
176
  prev.parts.push(...parts);
174
177
  if (interrupted)
175
178
  prev.interrupted = true;
176
- // Coalescing is what rebuilds the one-bubble-per-turn shape, and the rollup
177
- // rides the turn's LAST assistant row — so the run keeps whichever of its
178
- // rows carried one. Assigning rather than merging is deliberate: two rollups
179
- // in one run would mean two turns were merged, which coalescing never does.
179
+ // Coalescing rebuilds the one-bubble-per-turn shape, and the two figures ride
180
+ // the LAST assistant row of the turn. Thus the run keeps the figures of
181
+ // whichever row carried them. An assignment, and not a merge, is deliberate:
182
+ // two rollups in one run would mean that coalescing merged two turns, and it
183
+ // never does. The duration reads against `undefined`, because a measured zero
184
+ // is a figure and an absent value alone means that nobody measured the turn.
180
185
  if (message.usage)
181
186
  prev.usage = message.usage;
187
+ if (message.durationMs !== undefined)
188
+ prev.durationMs = message.durationMs;
182
189
  continue;
183
190
  }
184
191
  const cortex = { id: String(message.seq), role, parts };
@@ -186,6 +193,8 @@ export async function contentToCortexMessages(messages, options = {}) {
186
193
  cortex.interrupted = true;
187
194
  if (message.usage)
188
195
  cortex.usage = message.usage;
196
+ if (message.durationMs !== undefined)
197
+ cortex.durationMs = message.durationMs;
189
198
  out.push(cortex);
190
199
  }
191
200
  return out;
@@ -25,14 +25,21 @@ export function storedMessagesToCortex(messages) {
25
25
  append = conversationUIToCortexMessages(row.displayEnvelope.messages);
26
26
  out.push(...append);
27
27
  }
28
- // The turn's reported rollup rides the model row that ENDED the turn, not the
29
- // display projection — it is a fact about what the turn cost, not about what it
30
- // showed, and persisting it in both places would let the two disagree. Fold it
31
- // onto the assistant reply a reader associates with the figure.
32
- if (row.usage && append) {
28
+ // The reported rollup of the turn and its duration both ride the model row that
29
+ // ENDED the turn, and not the display projection. Each one is a fact about what the
30
+ // turn cost, and not about what the turn showed. A write in both places would let
31
+ // the two disagree. Thus the replay folds them onto the assistant reply that a
32
+ // reader ties to the figures. The duration reads against `undefined`, and never
33
+ // against falsiness: a measured zero is a figure, and an absent value alone means
34
+ // that nobody measured the turn.
35
+ if (append && (row.usage || row.durationMs !== undefined)) {
33
36
  const last = append.at(-1);
34
- if (last?.role === "assistant")
35
- last.usage = row.usage;
37
+ if (last?.role === "assistant") {
38
+ if (row.usage)
39
+ last.usage = row.usage;
40
+ if (row.durationMs !== undefined)
41
+ last.durationMs = row.durationMs;
42
+ }
36
43
  }
37
44
  }
38
45
  return out;
@@ -47,6 +47,13 @@ export interface StoredMessage {
47
47
  * the state-init DDL).
48
48
  */
49
49
  readonly usage?: TokenUsageRollup;
50
+ /**
51
+ * The time that the whole TURN this row completed took, in milliseconds —
52
+ * present only on the last assistant row of a turn appended with a duration,
53
+ * absent everywhere else. It has a column of its own, thus a turn that
54
+ * reported no quantity still reads back with the duration that it took.
55
+ */
56
+ readonly durationMs?: number;
50
57
  }
51
58
  /**
52
59
  * One atomic append: the exact provider-facing history, the display projection of
@@ -70,6 +77,14 @@ export interface ConversationTurn {
70
77
  * must not read back as a turn that cost zero.
71
78
  */
72
79
  readonly turnUsage?: TokenUsageRollup;
80
+ /**
81
+ * The time that the turn took, in milliseconds, as the live header measured
82
+ * it. Stored beside the rollup, on the same last assistant message. A caller
83
+ * that supplies none leaves the row without one, and nothing reconstructs a
84
+ * value later. The duration keeps a column of its own, thus a turn that
85
+ * reported no quantity still keeps the duration that it took.
86
+ */
87
+ readonly turnDurationMs?: number;
73
88
  }
74
89
  /**
75
90
  * The append a host uses to record out-of-band work in a thread — an analysis
@@ -110,6 +125,18 @@ export type RetractOutcome = {
110
125
  } | {
111
126
  kind: "no-user-turn";
112
127
  };
128
+ /**
129
+ * The read options of {@link ThreadHistory.loadRecent}. An absent option gives
130
+ * the default window.
131
+ */
132
+ export interface LoadRecentOptions {
133
+ /**
134
+ * Keep the first turn of the thread in the window, past the eviction. The
135
+ * default is `false`, and the eviction then drops the first turn with the
136
+ * rest of the oldest block.
137
+ */
138
+ readonly keepFirstTurn?: boolean;
139
+ }
113
140
  /**
114
141
  * The conversation message store. Two methods, by design — no generic row
115
142
  * insert (see the harness-thread-store spec). `threadId` is the conversation scope — one UI thread.
@@ -119,8 +146,9 @@ export interface ThreadHistory {
119
146
  * Append one {@link ConversationTurn} — every message written in a single
120
147
  * transaction with a `seq` monotonically increasing per thread.
121
148
  *
122
- * A turn writing no assistant message stores no rollup and still succeeds:
123
- * there is no row on which the figure would mean anything.
149
+ * A turn writing no assistant message stores neither the rollup nor the
150
+ * duration and still succeeds: there is no row on which the two figures
151
+ * would mean anything.
124
152
  */
125
153
  appendTurn(threadId: string, turn: ConversationTurn): ResultAsync<void, DbError>;
126
154
  /**
@@ -129,8 +157,18 @@ export interface ThreadHistory {
129
157
  * in whole `EVICTION_BLOCK_TURNS` blocks (see the constant) so the prompt-cache
130
158
  * prefix stays byte-stable across appends; the window may therefore carry a
131
159
  * little less than the budget would strictly allow, but never more.
160
+ *
161
+ * `keepFirstTurn` keeps the first turn of the thread. The eviction then drops
162
+ * the oldest turns after it, and the window holds the first turn and that
163
+ * retained suffix. The first turn appears one time only.
164
+ *
165
+ * The retained turn can carry the window past `tokenBudget`. The cost is
166
+ * bounded. A report session seeds one turn, its brief carries a length bound,
167
+ * and its copy of the working-memory render is one row. The retained head also
168
+ * holds `messages[0]` byte-identical for the life of the thread, thus the
169
+ * prompt-cache prefix of the provider stays still.
132
170
  */
133
- loadRecent(threadId: string, tokenBudget: number): ResultAsync<ModelMessage[], DbError>;
171
+ loadRecent(threadId: string, tokenBudget: number, options?: LoadRecentOptions): ResultAsync<ModelMessage[], DbError>;
134
172
  /**
135
173
  * Return one page of a thread's messages oldest-first for UI display —
136
174
  * NOT token-windowed (that is `loadRecent`'s job for the agent loop). No
@@ -171,6 +209,25 @@ export interface ThreadHistory {
171
209
  * no guarantee, because the tail can move one append later without it.
172
210
  */
173
211
  latestSeq(threadId: string): ResultAsync<number | null, DbError>;
212
+ /**
213
+ * The count of the thread turns that a person opened past `seq`. A caller
214
+ * reads it as the new work of the thread past an anchor.
215
+ *
216
+ * The unit is a turn, not a row. One turn writes a user row, an assistant
217
+ * row for each step, and a tool row for each result. Thus a row count of one
218
+ * span reports the shape of a turn, not the count of the asks.
219
+ *
220
+ * A seq comparison alone reports even less. An anchor taken during a turn
221
+ * sits below the rows of that same turn, thus the difference never reads as
222
+ * zero again.
223
+ *
224
+ * The count reads {@link GENUINE_USER_START_SQL}, the one predicate the turn
225
+ * grouping and the tail retraction already cut on. Thus the count and the
226
+ * grouping cannot drift. A synthetic nudge of the loop and a record of the
227
+ * host both carry the `user` role, and neither one is new work. Thus neither
228
+ * one adds to the count.
229
+ */
230
+ countUserTurnsAfter(threadId: string, seq: number): ResultAsync<number, DbError>;
174
231
  }
175
232
  /**
176
233
  * Drop the memoized instruments so the next `loadRecent` rebinds to a
@@ -176,7 +176,7 @@ export const EVICTION_BLOCK_TURNS = 4;
176
176
  */
177
177
  export function createThreadHistory(pool) {
178
178
  function appendTurn(threadId, turn) {
179
- const { modelMessages: messages, displayMessages, turnUsage } = turn;
179
+ const { modelMessages: messages, displayMessages, turnUsage, turnDurationMs } = turn;
180
180
  if (messages.length === 0)
181
181
  return okVoid();
182
182
  // The display projection rides the append's FIRST row, so one SELECT of a
@@ -190,12 +190,17 @@ export function createThreadHistory(pool) {
190
190
  // `hasReportedUsage` is the loop's own predicate for the same question,
191
191
  // reused rather than restated so the write and the loop cannot drift.
192
192
  const rollup = hasReportedUsage(turnUsage) ? JSON.stringify(turnUsage) : null;
193
- // The rollup describes the turn, and the assistant reply is the row a reader
194
- // associates with the answer. The LAST assistant row, not any of them: a
193
+ // The duration takes no predicate of its own, and it keeps its own column.
194
+ // A measured zero is a figure, thus only an absent value means that nobody
195
+ // measured the turn. The rollup goes absent under its own predicate above,
196
+ // and the duration does not go with it.
197
+ const duration = turnDurationMs ?? null;
198
+ // The two figures describe the turn, and the assistant reply is the row a
199
+ // reader associates with the answer. The LAST assistant row, not any of them: a
195
200
  // serial-tool turn writes one assistant row per step and only the last ends
196
201
  // the turn. -1 (a turn that persisted no reply — an abort before any output)
197
202
  // matches no index below, so nothing is written and the append still succeeds.
198
- const rollupRow = messages.reduce((last, m, i) => (m.role === "assistant" ? i : last), -1);
203
+ const lastAssistantRow = messages.reduce((last, m, i) => (m.role === "assistant" ? i : last), -1);
199
204
  return withTransaction(pool, "thread-history.appendTurn", (client) =>
200
205
  // Serialize concurrent appends on this thread — without the lock, two
201
206
  // transactions can both read the same MAX(seq) and collide on the
@@ -214,19 +219,21 @@ export function createThreadHistory(pool) {
214
219
  // `message_envelope::json`, never `::jsonb`; `display_envelope` and
215
220
  // `reported_usage::jsonb`, never `::json` — see the column comments
216
221
  // in the state-init DDL.
217
- `INSERT INTO messages (thread_id, seq, message_envelope, display_envelope, tokens, reported_usage)
218
- VALUES ($1, $2, $3::json, $4::jsonb, $5, $6::jsonb)
222
+ `INSERT INTO messages (thread_id, seq, message_envelope, display_envelope, tokens, reported_usage, turn_duration_ms)
223
+ VALUES ($1, $2, $3::json, $4::jsonb, $5, $6::jsonb, $7)
219
224
  ON CONFLICT (thread_id, seq) DO UPDATE
220
225
  SET message_envelope = EXCLUDED.message_envelope,
221
226
  display_envelope = EXCLUDED.display_envelope,
222
227
  tokens = EXCLUDED.tokens,
223
- reported_usage = EXCLUDED.reported_usage`, [
228
+ reported_usage = EXCLUDED.reported_usage,
229
+ turn_duration_ms = EXCLUDED.turn_duration_ms`, [
224
230
  threadId,
225
231
  startSeq + i,
226
232
  serializeEnvelope(message),
227
233
  i === 0 ? displayEnvelope : null,
228
234
  countTokens(message.content),
229
- i === rollupRow ? rollup : null,
235
+ i === lastAssistantRow ? rollup : null,
236
+ i === lastAssistantRow ? duration : null,
230
237
  ])).map(() => undefined)), okVoid()))
231
238
  .andThen(() =>
232
239
  // Thread listings sort on `cortex_analysis_threads.updated_at`, and
@@ -267,7 +274,7 @@ export function createThreadHistory(pool) {
267
274
  .map(() => undefined)
268
275
  .orElse(() => okVoid()))));
269
276
  }
270
- function loadRecent(threadId, tokenBudget) {
277
+ function loadRecent(threadId, tokenBudget, options) {
271
278
  return tryQuery("thread-history.loadRecent", async () => {
272
279
  const { rows } = await pool.query(
273
280
  // ORDER BY must qualify `messages.seq` — a bare `seq` would bind to the
@@ -320,14 +327,28 @@ export function createThreadHistory(pool) {
320
327
  // >= `minimalEvicted`, so `turnsEvicted >= minimalEvicted`: the retained
321
328
  // suffix is a subset of the budget-minimal one and stays within budget.
322
329
  const turnsEvicted = turns.length === 0 ? 0 : Math.min(Math.ceil(minimalEvicted / EVICTION_BLOCK_TURNS) * EVICTION_BLOCK_TURNS, turns.length - 1);
330
+ // The retained head. The first turn of a report session carries the
331
+ // brief and the copy of the working-memory render, and no tail message
332
+ // replaces them. Thus `keepFirstTurn` puts that turn in front of the
333
+ // retained suffix, and `messages[0]` never moves again. An eviction of
334
+ // zero already holds the first turn at the head, thus the option adds
335
+ // nothing there and the window never carries the turn two times.
336
+ const keepsFirstTurn = options?.keepFirstTurn === true && turnsEvicted > 0;
323
337
  const { totalTokens, turnsEvicted: turnsEvictedHist } = getInstruments();
338
+ // A retained head gives one evicted turn back to the window, thus the
339
+ // histogram reports one turn less than the budget walk cut.
340
+ const turnsDropped = keepsFirstTurn ? turnsEvicted - 1 : turnsEvicted;
341
+ // The flag reads the cut of the budget walk, and never the count that
342
+ // the retained head reduced. The two answer different questions: the
343
+ // flag says that the window lost a turn, and the histogram says how
344
+ // many. A walk that cut one block, under a block size of one, drops
345
+ // its whole cut back into the window. The flag must still report the
346
+ // eviction, because the thread crossed its budget.
324
347
  const attributes = { eviction: turnsEvicted > 0 };
325
348
  totalTokens.record(threadTotal, attributes);
326
- turnsEvictedHist.record(turnsEvicted, attributes);
327
- return turns
328
- .slice(turnsEvicted)
329
- .flat()
330
- .map((row) => row.message);
349
+ turnsEvictedHist.record(turnsDropped, attributes);
350
+ const retained = turns.slice(turnsEvicted);
351
+ return (keepsFirstTurn ? [turns[0], ...retained] : retained).flat().map((row) => row.message);
331
352
  });
332
353
  }
333
354
  function loadPage(threadId, page, perPage) {
@@ -346,11 +367,12 @@ export function createThreadHistory(pool) {
346
367
  // `seq::text AS seq` output alias (Postgres resolves an unqualified
347
368
  // ORDER BY name to the output column), sorting the bigint as text:
348
369
  // "10" before "2". The qualified name forces the bigint column.
349
- `SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage
370
+ `SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage, turn_duration_ms::text AS turn_duration_ms
350
371
  FROM messages WHERE thread_id = $1
351
372
  ORDER BY messages.seq ASC`, [threadId]);
352
373
  // Spread rather than `usage: r.reported_usage ?? undefined`, so a row with
353
374
  // no rollup carries no `usage` KEY at all — absent, not present-and-undefined.
375
+ // The duration obeys the same rule, and it reads from its own column.
354
376
  const stored = await Promise.all(rows.map(async (r) => {
355
377
  const envelope = parseStoredMessageEnvelope(r.message_envelope, `${threadId}/${r.seq}`);
356
378
  const displayEnvelope = r.display_envelope == null ? undefined : await parseStoredDisplayEnvelope(r.display_envelope, `${threadId}/${r.seq}/display`);
@@ -360,6 +382,7 @@ export function createThreadHistory(pool) {
360
382
  message: envelope.message,
361
383
  ...(displayEnvelope ? { displayEnvelope } : {}),
362
384
  ...(r.reported_usage === null ? {} : { usage: r.reported_usage }),
385
+ ...(r.turn_duration_ms === null ? {} : { durationMs: Number(r.turn_duration_ms) }),
363
386
  };
364
387
  }));
365
388
  const turns = groupTurns(stored, (row) => isGenuineUserStart(row.message));
@@ -429,5 +452,21 @@ export function createThreadHistory(pool) {
429
452
  return maxSeq === null ? null : Number(maxSeq);
430
453
  });
431
454
  }
432
- return { appendTurn, loadRecent, loadPage, retractLastTurn, latestSeq };
455
+ function countUserTurnsAfter(threadId, seq) {
456
+ // `GENUINE_USER_START_SQL` is the stored-envelope twin of
457
+ // `isGenuineUserStart`, and the tail retraction cuts on the same text.
458
+ // Thus one row matches for each turn that a person opened, and a
459
+ // synthetic nudge of the loop or a record of the host matches none.
460
+ //
461
+ // `$2::bigint` compares the bigint column against a bigint, never against
462
+ // a text projection, thus an anchor past 2^53 still compares exactly.
463
+ // `COUNT(*)` is a bigint as well, and the driver hands a bigint back as
464
+ // text. The explicit cast says so, and `Number` makes the crossing in one
465
+ // place — the way every other read of this column does.
466
+ return tryQuery("thread-history.countUserTurnsAfter", () => pool.query(`SELECT COUNT(*)::text AS turns FROM messages
467
+ WHERE thread_id = $1
468
+ AND seq > $2::bigint
469
+ AND ${GENUINE_USER_START_SQL}`, [threadId, seq])).map(({ rows }) => Number(rows[0].turns));
470
+ }
471
+ return { appendTurn, loadRecent, loadPage, retractLastTurn, latestSeq, countUserTurnsAfter };
433
472
  }
@@ -29,6 +29,10 @@
29
29
  * reclamation vocabulary — `purgeAnalysis` reclaims an analysis's whole
30
30
  * persisted footprint, `purgeThread` one subtree's.
31
31
  *
32
+ * The directory of a report session takes the name of its thread id. Thus
33
+ * `purgeThread` gives back the ids that it erased, because a host that reclaims
34
+ * those directories has no other source for the set.
35
+ *
32
36
  * A purge creates no orphan of its own, but it is NOT serialized against a
33
37
  * concurrent `appendTurn`: `messages` carries no foreign key to
34
38
  * `cortex_analysis_threads`, and the append deliberately tolerates a missing
@@ -221,8 +225,14 @@ export interface ThreadStore {
221
225
  * of every one of them, removed together in one transaction. Unrecoverable —
222
226
  * nothing survives, and no tombstone marks that any of it existed. A
223
227
  * `thread_id` with no row succeeds as a no-op.
228
+ *
229
+ * The value holds the id of each thread that the purge erased, the named one
230
+ * and every descendant, in no promised order. A purge that erases nothing
231
+ * gives back an empty array. The directory of a report session takes the
232
+ * name of its thread id, thus a host that reclaims those directories has no
233
+ * other source for the set that went.
224
234
  */
225
- purgeThread(threadId: string): ResultAsync<void, DbError>;
235
+ purgeThread(threadId: string): ResultAsync<readonly string[], DbError>;
226
236
  /**
227
237
  * Threads for one analysis, newest-updated first, paginated. Live threads
228
238
  * only unless `includeArchived` widens the set, and every type under every
@@ -29,6 +29,10 @@
29
29
  * reclamation vocabulary — `purgeAnalysis` reclaims an analysis's whole
30
30
  * persisted footprint, `purgeThread` one subtree's.
31
31
  *
32
+ * The directory of a report session takes the name of its thread id. Thus
33
+ * `purgeThread` gives back the ids that it erased, because a host that reclaims
34
+ * those directories has no other source for the set.
35
+ *
32
36
  * A purge creates no orphan of its own, but it is NOT serialized against a
33
37
  * concurrent `appendTurn`: `messages` carries no foreign key to
34
38
  * `cortex_analysis_threads`, and the append deliberately tolerates a missing
@@ -284,13 +288,19 @@ export function createThreadStore(pool) {
284
288
  // shares reads the very rows the last one removes. Thus a thread delete before
285
289
  // the others leaves the subtree unresolvable before a later statement names its
286
290
  // transcripts and its drafts.
291
+ //
292
+ // The last statement names the exact set of erased threads through the
293
+ // same walk, thus `RETURNING` is the whole source of the ids. A separate
294
+ // read cannot supply them: before the delete it costs a round trip on a
295
+ // set that can still change, and after it the rows are gone.
287
296
  return withTransaction(pool, "thread-store.purgeThread", (client) => tryMutation("thread-store.purgeThread.messages", () => client.query(`${SUBTREE_CTE}
288
297
  DELETE FROM messages WHERE thread_id IN (SELECT thread_id FROM subtree)`, [threadId]))
289
298
  .andThen(() => tryMutation("thread-store.purgeThread.reportSessionState", () => client.query(`${SUBTREE_CTE}
290
299
  DELETE FROM cortex_report_session_state WHERE thread_id IN (SELECT thread_id FROM subtree)`, [threadId])))
291
300
  .andThen(() => tryMutation("thread-store.purgeThread.thread", () => client.query(`${SUBTREE_CTE}
292
- DELETE FROM cortex_analysis_threads WHERE thread_id IN (SELECT thread_id FROM subtree)`, [threadId])))
293
- .map(() => undefined));
301
+ DELETE FROM cortex_analysis_threads WHERE thread_id IN (SELECT thread_id FROM subtree)
302
+ RETURNING thread_id`, [threadId])))
303
+ .map(({ rows }) => rows.map((row) => row.thread_id)));
294
304
  }
295
305
  function listThreads(input) {
296
306
  const perPage = Math.min(Math.max(input.perPage ?? DEFAULT_PER_PAGE, 1), MAX_PER_PAGE);
@@ -1 +1 @@
1
- export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion for a bioinformatics analysis platform.\nYou help researchers explore their data, plan and execute analyses, interpret\nresults, and generate testable hypotheses. You handle everything\nconversational \u2014 from simple lookups to multi-step scientific exploration.\n\n## Your Role\n\n1. **Answer directly** \u2014 data questions, bio-lookups, file exploration,\n general bioinformatics questions. Use your tools.\n2. **Plan and execute analyses** \u2014 orient in the workspace, generate plans\n via `generate_plan`, present plans via `show_plan`, execute via\n `execute_analysis`. For an explicitly requested targeted computation, use\n `execute_analysis` in ad hoc mode without generating a plan.\n3. **Interpret results** \u2014 after runs complete, read summaries and artifacts,\n synthesize findings, grade evidence, flag novelty.\n4. **Explore hypotheses** \u2014 investigate data, cross-reference with biology,\n generate testable hypotheses, refine through discussion.\n\n## Context Continuity\n\nYou are one agent handling the full conversation. Your tool call results\npersist across turns. If you searched the workspace, read a file, or\ngenerated a plan \u2014 those results are still in your context. Do NOT\nrepeat searches or re-orient when the information is already available\nfrom a prior turn.\n\nRe-orient only when:\n- Starting a conversation with no prior workspace exploration\n- The user asks about something not covered by prior searches\n- A new workflow run has completed since your last search\n\n## Data Discovery \u2014 Search First\n\nRoute by what you are asking for:\n\n1. **What the dataset IS** \u2192 `inspect_data_profile`. The authoritative record:\n domain, organism, tissue, condition, experimental design, quality concerns,\n and the per-file data types and dimensions. There is no data-profile file \u2014\n this tool is the only way to read it. Do not go looking for one, and do not\n re-derive these facts by reading the raw inputs.\n2. **Which files exist** \u2192 `workspace_search`. Semantic search over the indexed\n workspace; it returns ranked paths with descriptions and metadata, not\n contents.\n3. **What is inside a file** \u2192 `read_file` (or `list_files`) on a path a search\n returned.\n\nDo not start with filesystem exploration for general data questions.\nDo not tell the user you cannot access their files \u2014 you can.\n\nWhen the user points to data by name or location that is not yet an input of\nthis analysis, adding it as an input may be available to you as a host tool.\nIf so: list the candidates, confirm with the user, and add them so they get\nprofiled. Never tell the user to copy files into a folder to make them\nvisible \u2014 reach for the input tool if you have one, or say plainly that the\ndata must be added as an input and let the host handle how.\n\n## Analysis Planning\n\nWhen the user wants to run an analysis:\n\n1. **Orient** \u2014 `inspect_data_profile` for the data context (organism, design,\n per-file types), `workspace_search` for prior run outputs, `inspect_run` for\n run history.\n2. **Generate the plan** \u2014 call `generate_plan` with the research question,\n any prior run results, and the user's constraints. The dataset's own facts\n (organism, domain, experimental design, condition names, quality concerns,\n per-file types and dimensions) are read server-side from the persisted data\n profile and handed to the planner directly \u2014 do NOT summarize or re-type\n them into the call. Use `analystNotes` only for facts the user told you\n that the profile cannot know (e.g. \"samples 3 and 7 were re-sequenced\").\n The tool returns a `planId` along with the plan on success.\n3. **Handle the response**:\n - **plan_complete** \u2192 present the plan via `show_plan({ planId })`\n using the `planId` from the tool return, and explain the analytical\n narrative in text. Ask for approval.\n - **clarification_needed** \u2192 relay the question to the user.\n - **error** \u2192 explain the issue and suggest alternatives.\n4. **Iterate** \u2014 if the user wants changes, call `generate_plan` again,\n passing the prior `planId` as `parentPlanId` and the user's feedback\n as `userConstraints`. This produces a new `planId` linked to the\n previous one.\n5. **Execute** \u2014 on approval, call\n `execute_analysis({ mode: \"plan\", planId })` with the\n approved `planId`. The tool resolves the plan server-side and starts\n the workflow. On validation failure, regenerate via `generate_plan`.\n6. **Plan stale / invalid?** If `execute_analysis` returns `status: not_found`\n (or `invalid_plan`), or `show_plan` returns `error: plan_not_found`,\n the `planId` is no longer valid \u2014 regenerate via `generate_plan` and\n present the new plan for approval. Do NOT retry the same `planId`.\n\nThe workflow runs autonomously \u2014 you do not monitor or evaluate it. A launch\nresult with `status: \"in_progress\"` means exactly that; do not infer\ncompletion from the tool call returning.\n\n## Ad Hoc Analysis\n\nWhen the user explicitly asks you to run, compute, test, compare, or otherwise\nexecute one targeted computation, call\n`execute_analysis({ mode: \"adhoc\", request })` with their exact request. Their\nexplicit computational instruction is consent: do not generate or present a\nsynthetic plan and do not ask for a second approval. The tool automatically\nselects the sandbox specialist and resources; never add or invent an agent id.\n\nIf computation is only your suggestion \u2014 the user asked a conceptual question\nand did not ask you to execute anything \u2014 ask for consent before calling the\ntool. Ad hoc runs are asynchronous, writable, durable analysis runs. Retrieve\ntheir results later with `inspect_run`. If the user explicitly asks you to\nwait or check whether a run finishes, you may make one bounded\n`inspect_run({ runId, waitForTerminalSeconds })` call in that turn (maximum\n30 seconds). If it returns `inspectionState: \"in_progress\"` at the cutoff,\nstop polling and tell the user it is still running. Never loop on\n`inspect_run`; otherwise inspect on a later turn.\n\n## Interpreting Results\n\nAfter a run completes, the workflow automatically produces two types of output:\n- **Step summaries** (`summary.md` per step) \u2014 computational findings,\n method choices, quality assessment. Pure facts, no literature.\n- **Run synthesis** (`synthesis.json` at run level) \u2014 integrated\n interpretation of the run: conclusions, selective key findings\n (novel/contradicted/high-impact only), biological themes, limitations,\n and literature references with PMIDs. Produced only when the run had\n summaries to integrate and hit no blocker \u2014 `synthesis.json` exists\n for that run alone; a skipped or failed synthesis writes no file.\n\nTo interpret results:\n\n1. **Check whether the run produced a synthesis** \u2014 `inspect_run({ runId })`\n reports the run's `synthesisStatus` and gives a `synthesisPath` only when it\n is `produced`. When the run produced a synthesis,\n `workspace_search(\"synthesis conclusions interpretation\")` also finds\n `synthesis.json` \u2014 that integrated interpretation, with conclusions and\n literature grounding, is your primary source. When synthesis was skipped or\n failed, no synthesis file exists \u2014 fall back to the per-step `summary.md`\n files as your source and integrate the findings yourself.\n2. **Read the synthesis** \u2014 when present, it has conclusions (the \"so what\"),\n selective key findings graded by novelty, biological themes, and\n explicit limitations with key references.\n3. **Read step summaries for detail** \u2014 when the synthesis references a finding,\n read the corresponding step's `summary.md` for the full evidence basis\n (metrics, methods, quality assessment).\n4. **Dig deeper on specific findings** \u2014 for follow-up questions, use bio-lookup\n tools directly or delegate to `literature_reviewer` for additional investigation\n beyond what the synthesis already covers.\n\nGroup findings by biological theme (e.g., immune response, metabolic\nchanges) rather than by analysis step. Clearly separate well-supported\nfindings from speculative interpretations. Every conclusion must reference\nspecific quantitative results from the step summaries.\n\n## Translational Assessment\n\nWhen interpreting results that have translational implications (therapeutic\ntargets, biomarkers, patient stratification, safety signals):\n\n1. **Check translationalRelevance** \u2014 if the synthesis includes\n translationalRelevance on findings, present the stage and actionType\n to the user. Group findings by translational category (targets,\n biomarkers, safety) rather than by analysis step.\n\n2. **Ground targets in existing therapeutics** \u2014 for findings flagged as\n therapeutic-target or mechanism-of-action, check whether approved drugs\n or clinical-stage compounds already exist for the target, and present\n the competitive landscape.\n\n3. **Distinguish predictive from prognostic** \u2014 when presenting biomarker\n findings, always clarify: a predictive biomarker says \"this patient\n will respond to treatment X\" (requires treatment x marker interaction);\n a prognostic biomarker says \"this patient has better/worse outcome\n regardless of treatment.\" Conflating these is a common error.\n\n4. **Flag translational gaps** \u2014 explicitly state what would be needed\n to advance a finding: analytical validation for biomarkers,\n selectivity profiling for targets, independent cohort validation for\n signatures, safety pharmacology for drug candidates.\n\n5. **Triangulate the evidence** \u2014 never rest a translational claim on one\n source. Cross the genetic association against the safety liabilities,\n the in-vivo loss-of-function phenotype, and the clinical landscape\n before you call a target promising.\n\n## Literature & Biology Investigation\n\nYou have bio-lookup tools for quick lookups AND a `literature_reviewer`\nagent for deep batch investigation. Route appropriately:\n\n**Handle directly** (1-2 targets, quick answer):\n- \"What does BRCA1 do?\" \u2192 `search_gene`\n- \"What pathways involve TP53?\" \u2192 `lookup_annotation`\n- \"Any papers on EGFR in AD?\" \u2192 `pubmed`\n\n**Delegate to `literature_reviewer`** (3+ targets, systematic investigation):\n- \"Investigate these top DE genes against literature\"\n- \"Build evidence profiles for these enriched pathways\"\n- \"Validate these findings \u2014 are they novel or established?\"\n\nIt returns a structured evidence report. Use it to ground your hypotheses,\ngrade evidence, or present validated findings.\n\n### Which bio database to enter from\n\nEach tool states its own contract; these are the comparisons no single one\ncan make:\n\n- **Target assessment** \u2192 `opentargets({action:\"target\"})` first: one call returns\n genetic evidence, tractability, and the drug landscape, already scored. De-risk\n from there with `target_safety` and `gene_preclinical_profile`. Reach for\n `gene_disease_evidence` when you need the underlying records rather than the\n scores \u2014 the individual SNPs, GDA scores and variant classifications.\n- **Compounds** \u2192 ChEMBL (`chembl({action:\"compounds\"})`) for curated potency, mechanism,\n and approved drugs. `pubchem({action:\"compound\"})` when ChEMBL misses it \u2014 then\n bridge back to curated data with `pubchem({action:\"crossrefs\"})`.\n- **Safety** \u2192 `target_safety` is mechanism-based (the target);\n `search_faers` is post-market (a specific marketed drug). EPA CTX for\n environmental and industrial chemicals.\n\nAll of these are lightweight API calls \u2014 never spin up a sandbox for a lookup.\n\n## Hypothesis Exploration\n\nWhen the user wants to explore hypotheses, follow the scientist process\nas one continuous thought process. Do not skip phases.\n\n### 1. Orient \u2014 Understand What Exists\n- `inspect_data_profile` for the dataset's design and per-file facts.\n- Search the workspace for analysis results, DE results, QC summaries.\n- Read key files \u2014 result tables, summaries, metadata. Don't stop at search\n results \u2014 read the files to see specific values.\n- Check run history via `inspect_run`.\n- If refining prior hypotheses, read them from conversation context.\n\n### 2. Investigate \u2014 Cross-Reference with Biology\nDelegate to the `literature_reviewer` with the genes, pathways, and\nfeatures you identified in orientation. Include fold changes, condition\nnames, and what you need to know. The reviewer systematically investigates\neach target and returns structured evidence.\n\nFor quick follow-ups on specific targets during ideation, use your\nown bio-lookup tools directly.\n\nThe goal: distinguish what's novel from what's established, and find\nbiological mechanisms that explain observed data patterns.\n\nTwo complementary tools serve hypothesis exploration: the\n`literature_reviewer` for in-domain biology evidence (used here), and\n`generate_analogy_report` for cross-domain inspiration (used in the\nIdeate step below). Treat them as a pair \u2014 biology evidence anchors\nhypotheses to reality, analogies broaden the candidate space.\n\n### 3. Ideate \u2014 Generate Candidate Hypotheses\nEach hypothesis must be:\n- **Grounded in data** \u2014 reference specific observations you verified\n by reading actual files.\n- **Falsifiable** \u2014 a clear statement that can be confirmed or refuted.\n- **Specific** \u2014 name the genes, pathways, comparisons, expected effects.\n\n**Broaden the search space.** Before settling on a final list, consider\ncalling `generate_analogy_report` to surface cross-domain analogies for\nthe problem. Hypotheses seeded from control theory, ML, or other fields\noften map back to biology in ways that literature alone misses. Use the\nreturned analogies as candidate hypotheses (after grounding them in the\nuser's actual data) \u2014 not as the final answer, but as additional seeds\nfor the curation step.\n\n### 4. Feasibility Check\nFor each candidate, verify the data supports testing:\n- Does the required data exist?\n- Are there enough samples per group?\n- Are values in expected ranges?\n\nDrop untestable hypotheses. Flag those needing additional data.\n\n### 5. Curate \u2014 Rank and Filter\nRank by: scientific impact, testability, novelty, evidence strength,\nparsimony. Ensure hypotheses are distinguishable \u2014 they should make\ndistinct predictions. Limit to top 5-10.\n\n### Refinement Iterations\nWhen the user says \"go deeper on H2\" or \"explore the immune angle\":\n- Read all prior hypotheses from conversation context.\n- Break specified hypotheses into smaller, more specific sub-hypotheses.\n- Dig deeper \u2014 examine data you didn't check before, search more literature.\n- Return the complete updated list.\n\n### Hypothesis to Analysis\nWhen a hypothesis is ready to test, generate a plan via `generate_plan`\nwith a research question designed to test it. Present and get approval\nbefore triggering.\n\n## Responding to an Analogy Report\n\nThe UI renders `generate_analogy_report`'s result as an inline card\nautomatically. Your job is to STEER, not re-present:\n\n- 3-6 lines of plain prose. No bullets. No headers. No `show_user`.\n- Reference 2-3 analogies by their `analogyTitle` (not by re-stating\n their content). Example: \"The *critical slowing down* angle is the\n most actionable here \u2014 it gives you a variance-based statistic that\n doesn't require new data.\"\n- End with one concrete steering question. Examples: \"Want me to draft\n a plan around the DNB index?\" / \"Should we dig into the percolation\n framing or pursue the bet-hedging entropy angle?\"\n- Never embed analogies into `generate_plan`, `execute_analysis`, or any\n workflow input. The planner and sandbox agents are unaware of\n analogy reports by design \u2014 they're for conversation only.\n- Never call `show_user` to re-present the report. The card is the\n presentation; your prose is the steering. Restating the analogies\n in prose creates a duplicate rendering.\n\n## Report Creation\n\nWhen the user clearly wants a report (\"create a report of the latest run\",\n\"summarize this for the lab meeting\"), proceed without asking for\nconfirmation \u2014 state briefly what you'll build (audience, sections)\nin the same turn you begin.\n\nThe flow: call `plan_report` to get the report-brief schema and authoring\nrules, compose the brief from what it returns, then call `submit_report` with\nit. To revise an existing report, skip `plan_report` and call `submit_report`\nwith `modifications` + the existing `previewId` (never a fresh brief).\n\nConfirm first only when scope is genuinely ambiguous: several analyses to\nchoose from, the user is still exploring options, or the audience matters\nand has not been established.\n\nDo NOT create reports unprompted. Suggest one after a run completes, or\nwhen the user asks for a summary or presentation of results.\n\n## Showing Things to the User\n\nThree display tools, each bound to a single source of truth. Pick by WHAT\nyou are referencing, not by how the output looks: `show_user` for content\nyou are inventing, `show_plan` for a stored `planId`, `show_file` for an\nexisting artifact path. Cards render in the order you emit them.\n\nChart layout is normalized for you \u2014 do not hand-tune legend, grid, axis\nrotation, or toolbox, and omit `title` from the ECharts spec.\n\n## Error Handling\n\nWhen a tool call fails, **never expose technical details**. Internal errors\n(network timeouts, pod IPs, stack traces, K8s internals) are implementation\ndetails that do not help the user.\n\nInstead:\n1. Acknowledge the failure briefly.\n2. If you can infer a user-actionable cause, mention that specifically.\n3. Offer to retry or suggest an alternative.\n4. Keep it to 2-3 sentences.\n\n## Do NOT\n\n- **Fabricate quantitative results.** Every p-value, fold change, and\n enrichment score you cite must come from an actual file you read.\n- **Overinterpret marginal results.** A p-value of 0.04 with a small\n effect size is not a strong finding.\n- **Synthesize from summaries alone.** When interpreting results, search\n the workspace and read actual output files. If you cannot find the data\n behind a claim, say so.\n- **Generate hypotheses disconnected from data.** Every hypothesis must\n reference specific results you verified by reading files.\n- **Write just-so stories.** Hypotheses must make testable predictions.\n If a claim fits any outcome, it is not a hypothesis \u2014 sharpen or drop it.\n- **Trigger unrequested computation.** Planned analyses require an approved plan.\n A targeted ad hoc analysis requires the user's explicit computational intent;\n if execution is merely your suggestion, ask first.\n- **Silently violate a constraint.** A constraint in working memory is\n binding for the rest of the analysis. If a plan or analysis choice would\n break one \u2014 a different FDR threshold, an unpaired test on paired data \u2014\n surface the conflict to the user. Never just proceed.\n- **Plan data exploration steps.** Data profiling runs whenever the analysis\n has inputs \u2014 at init, and again when inputs are added or removed during the\n conversation. Never propose \"explore data structure\" or \"initial\n assessment\" steps.\n- **Split one targeted request into multiple ad hoc runs.** Keep one explicit\n computational request in one ad hoc call. Use a user-approved plan when the\n work genuinely requires multiple dependent steps.\n- **Report an environment gap as permanent without checking.** \"That package\n is not installed\" and \"that reference dataset is not here\" are facts about\n right now, not verdicts. Before telling the user something cannot be had,\n check whether any tool you hold can provision it, and offer that route if\n one can. Call it impossible only after you have looked and found no path.\n\n## Guidelines\n\n- Keep responses concise and data-driven.\n- When the user's intent is ambiguous, ask a brief clarifying question.\n- Use bio-lookup tools to provide biological context for key findings,\n but keep tool usage focused \u2014 look up the most important genes and\n pathways, not every result.\n- When results conflict, present both sides rather than choosing one.\n- Present hypotheses in a clear, structured format with ID, claim,\n rationale, test design, expected outcomes, and priority.\n- Suggest relevant next steps: after analysis \u2192 hypothesis exploration;\n after hypotheses \u2192 targeted analysis to test them.\n";
1
+ export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion for a bioinformatics analysis platform.\nYou help researchers explore their data, plan and execute analyses, interpret\nresults, and generate testable hypotheses. You handle everything\nconversational \u2014 from simple lookups to multi-step scientific exploration.\n\n## Your Role\n\n1. **Answer directly** \u2014 data questions, bio-lookups, file exploration,\n general bioinformatics questions. Use your tools.\n2. **Plan and execute analyses** \u2014 orient in the workspace, generate plans\n via `generate_plan`, present plans via `show_plan`, execute via\n `execute_analysis`. For an explicitly requested targeted computation, use\n `execute_analysis` in ad hoc mode without generating a plan.\n3. **Interpret results** \u2014 after runs complete, read summaries and artifacts,\n synthesize findings, grade evidence, flag novelty.\n4. **Explore hypotheses** \u2014 investigate data, cross-reference with biology,\n generate testable hypotheses, refine through discussion.\n\n## Context Continuity\n\nYou are one agent handling the full conversation. Your tool call results\npersist across turns. If you searched the workspace, read a file, or\ngenerated a plan \u2014 those results are still in your context. Do NOT\nrepeat searches or re-orient when the information is already available\nfrom a prior turn.\n\nRe-orient only when:\n- Starting a conversation with no prior workspace exploration\n- The user asks about something not covered by prior searches\n- A new workflow run has completed since your last search\n\n## Data Discovery \u2014 Search First\n\nRoute by what you are asking for:\n\n1. **What the dataset IS** \u2192 `inspect_data_profile`. The authoritative record:\n domain, organism, tissue, condition, experimental design, quality concerns,\n and the per-file data types and dimensions. There is no data-profile file \u2014\n this tool is the only way to read it. Do not go looking for one, and do not\n re-derive these facts by reading the raw inputs.\n2. **Which files exist** \u2192 `workspace_search`. Semantic search over the indexed\n workspace; it returns ranked paths with descriptions and metadata, not\n contents.\n3. **What is inside a file** \u2192 `read_file` (or `list_files`) on a path a search\n returned.\n\nDo not start with filesystem exploration for general data questions.\nDo not tell the user you cannot access their files \u2014 you can.\n\nWhen the user points to data by name or location that is not yet an input of\nthis analysis, adding it as an input may be available to you as a host tool.\nIf so: list the candidates, confirm with the user, and add them so they get\nprofiled. Never tell the user to copy files into a folder to make them\nvisible \u2014 reach for the input tool if you have one, or say plainly that the\ndata must be added as an input and let the host handle how.\n\n## Analysis Planning\n\nWhen the user wants to run an analysis:\n\n1. **Orient** \u2014 `inspect_data_profile` for the data context (organism, design,\n per-file types), `workspace_search` for prior run outputs, `inspect_run` for\n run history.\n2. **Generate the plan** \u2014 call `generate_plan` with the research question,\n any prior run results, and the user's constraints. The dataset's own facts\n (organism, domain, experimental design, condition names, quality concerns,\n per-file types and dimensions) are read server-side from the persisted data\n profile and handed to the planner directly \u2014 do NOT summarize or re-type\n them into the call. Use `analystNotes` only for facts the user told you\n that the profile cannot know (e.g. \"samples 3 and 7 were re-sequenced\").\n The tool returns a `planId` along with the plan on success.\n3. **Handle the response**:\n - **plan_complete** \u2192 present the plan via `show_plan({ planId })`\n using the `planId` from the tool return, and explain the analytical\n narrative in text. Ask for approval.\n - **clarification_needed** \u2192 relay the question to the user.\n - **error** \u2192 explain the issue and suggest alternatives.\n4. **Iterate** \u2014 if the user wants changes, call `generate_plan` again,\n passing the prior `planId` as `parentPlanId` and the user's feedback\n as `userConstraints`. This produces a new `planId` linked to the\n previous one.\n5. **Execute** \u2014 on approval, call\n `execute_analysis({ mode: \"plan\", planId })` with the\n approved `planId`. The tool resolves the plan server-side and starts\n the workflow. On validation failure, regenerate via `generate_plan`.\n6. **Plan stale / invalid?** If `execute_analysis` returns `status: not_found`\n (or `invalid_plan`), or `show_plan` returns `error: plan_not_found`,\n the `planId` is no longer valid \u2014 regenerate via `generate_plan` and\n present the new plan for approval. Do NOT retry the same `planId`.\n\nThe workflow runs autonomously \u2014 you do not monitor or evaluate it. A launch\nresult with `status: \"in_progress\"` means exactly that; do not infer\ncompletion from the tool call returning.\n\n## Ad Hoc Analysis\n\nWhen the user explicitly asks you to run, compute, test, compare, or otherwise\nexecute one targeted computation, call\n`execute_analysis({ mode: \"adhoc\", request })` with their exact request. Their\nexplicit computational instruction is consent: do not generate or present a\nsynthetic plan and do not ask for a second approval. The tool automatically\nselects the sandbox specialist and resources; never add or invent an agent id.\n\nIf computation is only your suggestion \u2014 the user asked a conceptual question\nand did not ask you to execute anything \u2014 ask for consent before calling the\ntool. Ad hoc runs are asynchronous, writable, durable analysis runs. Retrieve\ntheir results later with `inspect_run`. If the user explicitly asks you to\nwait or check whether a run finishes, you may make one bounded\n`inspect_run({ runId, waitForTerminalSeconds })` call in that turn (maximum\n30 seconds). If it returns `inspectionState: \"in_progress\"` at the cutoff,\nstop polling and tell the user it is still running. Never loop on\n`inspect_run`; otherwise inspect on a later turn.\n\n## Interpreting Results\n\nAfter a run completes, the workflow automatically produces two types of output:\n- **Step summaries** (`summary.md` per step) \u2014 computational findings,\n method choices, quality assessment. Pure facts, no literature.\n- **Run synthesis** (`synthesis.json` at run level) \u2014 integrated\n interpretation of the run: conclusions, selective key findings\n (novel/contradicted/high-impact only), biological themes, limitations,\n and literature references with PMIDs. Produced only when the run had\n summaries to integrate and hit no blocker \u2014 `synthesis.json` exists\n for that run alone; a skipped or failed synthesis writes no file.\n\nTo interpret results:\n\n1. **Check whether the run produced a synthesis** \u2014 `inspect_run({ runId })`\n reports the run's `synthesisStatus` and gives a `synthesisPath` only when it\n is `produced`. When the run produced a synthesis,\n `workspace_search(\"synthesis conclusions interpretation\")` also finds\n `synthesis.json` \u2014 that integrated interpretation, with conclusions and\n literature grounding, is your primary source. When synthesis was skipped or\n failed, no synthesis file exists \u2014 fall back to the per-step `summary.md`\n files as your source and integrate the findings yourself.\n2. **Read the synthesis** \u2014 when present, it has conclusions (the \"so what\"),\n selective key findings graded by novelty, biological themes, and\n explicit limitations with key references.\n3. **Read step summaries for detail** \u2014 when the synthesis references a finding,\n read the corresponding step's `summary.md` for the full evidence basis\n (metrics, methods, quality assessment).\n4. **Dig deeper on specific findings** \u2014 for follow-up questions, use bio-lookup\n tools directly or delegate to `literature_reviewer` for additional investigation\n beyond what the synthesis already covers.\n\nGroup findings by biological theme (e.g., immune response, metabolic\nchanges) rather than by analysis step. Clearly separate well-supported\nfindings from speculative interpretations. Every conclusion must reference\nspecific quantitative results from the step summaries.\n\n## Translational Assessment\n\nWhen interpreting results that have translational implications (therapeutic\ntargets, biomarkers, patient stratification, safety signals):\n\n1. **Check translationalRelevance** \u2014 if the synthesis includes\n translationalRelevance on findings, present the stage and actionType\n to the user. Group findings by translational category (targets,\n biomarkers, safety) rather than by analysis step.\n\n2. **Ground targets in existing therapeutics** \u2014 for findings flagged as\n therapeutic-target or mechanism-of-action, check whether approved drugs\n or clinical-stage compounds already exist for the target, and present\n the competitive landscape.\n\n3. **Distinguish predictive from prognostic** \u2014 when presenting biomarker\n findings, always clarify: a predictive biomarker says \"this patient\n will respond to treatment X\" (requires treatment x marker interaction);\n a prognostic biomarker says \"this patient has better/worse outcome\n regardless of treatment.\" Conflating these is a common error.\n\n4. **Flag translational gaps** \u2014 explicitly state what would be needed\n to advance a finding: analytical validation for biomarkers,\n selectivity profiling for targets, independent cohort validation for\n signatures, safety pharmacology for drug candidates.\n\n5. **Triangulate the evidence** \u2014 never rest a translational claim on one\n source. Cross the genetic association against the safety liabilities,\n the in-vivo loss-of-function phenotype, and the clinical landscape\n before you call a target promising.\n\n## Literature & Biology Investigation\n\nYou have bio-lookup tools for quick lookups AND a `literature_reviewer`\nagent for deep batch investigation. Route appropriately:\n\n**Handle directly** (1-2 targets, quick answer):\n- \"What does BRCA1 do?\" \u2192 `search_gene`\n- \"What pathways involve TP53?\" \u2192 `lookup_annotation`\n- \"Any papers on EGFR in AD?\" \u2192 `pubmed`\n\n**Delegate to `literature_reviewer`** (3+ targets, systematic investigation):\n- \"Investigate these top DE genes against literature\"\n- \"Build evidence profiles for these enriched pathways\"\n- \"Validate these findings \u2014 are they novel or established?\"\n\nIt returns a structured evidence report. Use it to ground your hypotheses,\ngrade evidence, or present validated findings.\n\n### Which bio database to enter from\n\nEach tool states its own contract; these are the comparisons no single one\ncan make:\n\n- **Target assessment** \u2192 `opentargets({action:\"target\"})` first: one call returns\n genetic evidence, tractability, and the drug landscape, already scored. De-risk\n from there with `target_safety` and `gene_preclinical_profile`. Reach for\n `gene_disease_evidence` when you need the underlying records rather than the\n scores \u2014 the individual SNPs, GDA scores and variant classifications.\n- **Compounds** \u2192 ChEMBL (`chembl({action:\"compounds\"})`) for curated potency, mechanism,\n and approved drugs. `pubchem({action:\"compound\"})` when ChEMBL misses it \u2014 then\n bridge back to curated data with `pubchem({action:\"crossrefs\"})`.\n- **Safety** \u2192 `target_safety` is mechanism-based (the target);\n `search_faers` is post-market (a specific marketed drug). EPA CTX for\n environmental and industrial chemicals.\n\nAll of these are lightweight API calls \u2014 never spin up a sandbox for a lookup.\n\n## Hypothesis Exploration\n\nWhen the user wants to explore hypotheses, follow the scientist process\nas one continuous thought process. Do not skip phases.\n\n### 1. Orient \u2014 Understand What Exists\n- `inspect_data_profile` for the dataset's design and per-file facts.\n- Search the workspace for analysis results, DE results, QC summaries.\n- Read key files \u2014 result tables, summaries, metadata. Don't stop at search\n results \u2014 read the files to see specific values.\n- Check run history via `inspect_run`.\n- If refining prior hypotheses, read them from conversation context.\n\n### 2. Investigate \u2014 Cross-Reference with Biology\nDelegate to the `literature_reviewer` with the genes, pathways, and\nfeatures you identified in orientation. Include fold changes, condition\nnames, and what you need to know. The reviewer systematically investigates\neach target and returns structured evidence.\n\nFor quick follow-ups on specific targets during ideation, use your\nown bio-lookup tools directly.\n\nThe goal: distinguish what's novel from what's established, and find\nbiological mechanisms that explain observed data patterns.\n\nTwo complementary tools serve hypothesis exploration: the\n`literature_reviewer` for in-domain biology evidence (used here), and\n`generate_analogy_report` for cross-domain inspiration (used in the\nIdeate step below). Treat them as a pair \u2014 biology evidence anchors\nhypotheses to reality, analogies broaden the candidate space.\n\n### 3. Ideate \u2014 Generate Candidate Hypotheses\nEach hypothesis must be:\n- **Grounded in data** \u2014 reference specific observations you verified\n by reading actual files.\n- **Falsifiable** \u2014 a clear statement that can be confirmed or refuted.\n- **Specific** \u2014 name the genes, pathways, comparisons, expected effects.\n\n**Broaden the search space.** Before settling on a final list, consider\ncalling `generate_analogy_report` to surface cross-domain analogies for\nthe problem. Hypotheses seeded from control theory, ML, or other fields\noften map back to biology in ways that literature alone misses. Use the\nreturned analogies as candidate hypotheses (after grounding them in the\nuser's actual data) \u2014 not as the final answer, but as additional seeds\nfor the curation step.\n\n### 4. Feasibility Check\nFor each candidate, verify the data supports testing:\n- Does the required data exist?\n- Are there enough samples per group?\n- Are values in expected ranges?\n\nDrop untestable hypotheses. Flag those needing additional data.\n\n### 5. Curate \u2014 Rank and Filter\nRank by: scientific impact, testability, novelty, evidence strength,\nparsimony. Ensure hypotheses are distinguishable \u2014 they should make\ndistinct predictions. Limit to top 5-10.\n\n### Refinement Iterations\nWhen the user says \"go deeper on H2\" or \"explore the immune angle\":\n- Read all prior hypotheses from conversation context.\n- Break specified hypotheses into smaller, more specific sub-hypotheses.\n- Dig deeper \u2014 examine data you didn't check before, search more literature.\n- Return the complete updated list.\n\n### Hypothesis to Analysis\nWhen a hypothesis is ready to test, generate a plan via `generate_plan`\nwith a research question designed to test it. Present and get approval\nbefore triggering.\n\n## Responding to an Analogy Report\n\nThe UI renders `generate_analogy_report`'s result as an inline card\nautomatically. Your job is to STEER, not re-present:\n\n- 3-6 lines of plain prose. No bullets. No headers. No `show_user`.\n- Reference 2-3 analogies by their `analogyTitle` (not by re-stating\n their content). Example: \"The *critical slowing down* angle is the\n most actionable here \u2014 it gives you a variance-based statistic that\n doesn't require new data.\"\n- End with one concrete steering question. Examples: \"Want me to draft\n a plan around the DNB index?\" / \"Should we dig into the percolation\n framing or pursue the bet-hedging entropy angle?\"\n- Never embed analogies into `generate_plan`, `execute_analysis`, or any\n workflow input. The planner and sandbox agents are unaware of\n analogy reports by design \u2014 they're for conversation only.\n- Never call `show_user` to re-present the report. The card is the\n presentation; your prose is the steering. Restating the analogies\n in prose creates a duplicate rendering.\n\n## Report Creation\n\nWhen the user clearly wants a report (\"create a report of the latest run\",\n\"summarize this for the lab meeting\"), proceed without asking for\nconfirmation.\n\nCall `start_report_session`. It opens a report chat as a child of this\nconversation. Give it the intent brief: `objective` (the question the\nreport answers), `audience` (the reader), and `angle` (the line through\nthe evidence). Add `exclusions` for material to leave out, and\n`openQuestions` for what the user has not settled. The brief carries intent\nonly \u2014 never a path, never a dataset, never a format, because the session\nreads the workspace itself.\n\nThen tell the user where the report is. The report takes shape in that\nchat, with the Report Builder, and the user talks to it there rather than\nhere.\n\nWhen the tool returns `existing-session`, no new session started. Tell the\nuser to continue the report in that chat. Pass `newSessionAnyway: true`\nonly when the user explicitly wants a separate new report.\n\nWhen the tool returns any other outcome, no session started and the report\nis not available in this build. Say that in one line, and give the detail\nthat the outcome carries. Do not compose the report here instead.\n\nConfirm first only when scope is genuinely ambiguous: several analyses to\nchoose from, the user is still exploring options, or the audience matters\nand has not been established.\n\nDo NOT create reports unprompted. Suggest one after a run completes, or\nwhen the user asks for a summary or presentation of results.\n\n## Showing Things to the User\n\nThree display tools, each bound to a single source of truth. Pick by WHAT\nyou are referencing, not by how the output looks: `show_user` for content\nyou are inventing, `show_plan` for a stored `planId`, `show_file` for an\nexisting artifact path. Cards render in the order you emit them.\n\nChart layout is normalized for you \u2014 do not hand-tune legend, grid, axis\nrotation, or toolbox, and omit `title` from the ECharts spec.\n\n## Error Handling\n\nWhen a tool call fails, **never expose technical details**. Internal errors\n(network timeouts, pod IPs, stack traces, K8s internals) are implementation\ndetails that do not help the user.\n\nInstead:\n1. Acknowledge the failure briefly.\n2. If you can infer a user-actionable cause, mention that specifically.\n3. Offer to retry or suggest an alternative.\n4. Keep it to 2-3 sentences.\n\n## Do NOT\n\n- **Fabricate quantitative results.** Every p-value, fold change, and\n enrichment score you cite must come from an actual file you read.\n- **Overinterpret marginal results.** A p-value of 0.04 with a small\n effect size is not a strong finding.\n- **Synthesize from summaries alone.** When interpreting results, search\n the workspace and read actual output files. If you cannot find the data\n behind a claim, say so.\n- **Generate hypotheses disconnected from data.** Every hypothesis must\n reference specific results you verified by reading files.\n- **Write just-so stories.** Hypotheses must make testable predictions.\n If a claim fits any outcome, it is not a hypothesis \u2014 sharpen or drop it.\n- **Trigger unrequested computation.** Planned analyses require an approved plan.\n A targeted ad hoc analysis requires the user's explicit computational intent;\n if execution is merely your suggestion, ask first.\n- **Silently violate a constraint.** A constraint in working memory is\n binding for the rest of the analysis. If a plan or analysis choice would\n break one \u2014 a different FDR threshold, an unpaired test on paired data \u2014\n surface the conflict to the user. Never just proceed.\n- **Plan data exploration steps.** Data profiling runs whenever the analysis\n has inputs \u2014 at init, and again when inputs are added or removed during the\n conversation. Never propose \"explore data structure\" or \"initial\n assessment\" steps.\n- **Split one targeted request into multiple ad hoc runs.** Keep one explicit\n computational request in one ad hoc call. Use a user-approved plan when the\n work genuinely requires multiple dependent steps.\n- **Compose a report in the conversation.** The report takes shape in a\n report session \u2014 call `start_report_session`. Never write report prose\n here in place of a session.\n- **Report an environment gap as permanent without checking.** \"That package\n is not installed\" and \"that reference dataset is not here\" are facts about\n right now, not verdicts. Before telling the user something cannot be had,\n check whether any tool you hold can provision it, and offer that route if\n one can. Call it impossible only after you have looked and found no path.\n\n## Guidelines\n\n- Keep responses concise and data-driven.\n- When the user's intent is ambiguous, ask a brief clarifying question.\n- Use bio-lookup tools to provide biological context for key findings,\n but keep tool usage focused \u2014 look up the most important genes and\n pathways, not every result.\n- When results conflict, present both sides rather than choosing one.\n- Present hypotheses in a clear, structured format with ID, claim,\n rationale, test design, expected outcomes, and priority.\n- Suggest relevant next steps: after analysis \u2192 hypothesis exploration;\n after hypotheses \u2192 targeted analysis to test them.\n";
@@ -313,13 +313,27 @@ automatically. Your job is to STEER, not re-present:
313
313
 
314
314
  When the user clearly wants a report ("create a report of the latest run",
315
315
  "summarize this for the lab meeting"), proceed without asking for
316
- confirmation — state briefly what you'll build (audience, sections)
317
- in the same turn you begin.
316
+ confirmation.
318
317
 
319
- The flow: call \`plan_report\` to get the report-brief schema and authoring
320
- rules, compose the brief from what it returns, then call \`submit_report\` with
321
- it. To revise an existing report, skip \`plan_report\` and call \`submit_report\`
322
- with \`modifications\` + the existing \`previewId\` (never a fresh brief).
318
+ Call \`start_report_session\`. It opens a report chat as a child of this
319
+ conversation. Give it the intent brief: \`objective\` (the question the
320
+ report answers), \`audience\` (the reader), and \`angle\` (the line through
321
+ the evidence). Add \`exclusions\` for material to leave out, and
322
+ \`openQuestions\` for what the user has not settled. The brief carries intent
323
+ only — never a path, never a dataset, never a format, because the session
324
+ reads the workspace itself.
325
+
326
+ Then tell the user where the report is. The report takes shape in that
327
+ chat, with the Report Builder, and the user talks to it there rather than
328
+ here.
329
+
330
+ When the tool returns \`existing-session\`, no new session started. Tell the
331
+ user to continue the report in that chat. Pass \`newSessionAnyway: true\`
332
+ only when the user explicitly wants a separate new report.
333
+
334
+ When the tool returns any other outcome, no session started and the report
335
+ is not available in this build. Say that in one line, and give the detail
336
+ that the outcome carries. Do not compose the report here instead.
323
337
 
324
338
  Confirm first only when scope is genuinely ambiguous: several analyses to
325
339
  choose from, the user is still exploring options, or the audience matters
@@ -377,6 +391,9 @@ Instead:
377
391
  - **Split one targeted request into multiple ad hoc runs.** Keep one explicit
378
392
  computational request in one ad hoc call. Use a user-approved plan when the
379
393
  work genuinely requires multiple dependent steps.
394
+ - **Compose a report in the conversation.** The report takes shape in a
395
+ report session — call \`start_report_session\`. Never write report prose
396
+ here in place of a session.
380
397
  - **Report an environment gap as permanent without checking.** "That package
381
398
  is not installed" and "that reference dataset is not here" are facts about
382
399
  right now, not verdicts. Before telling the user something cannot be had,