@inflexa-ai/harness 0.25.0 → 0.26.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 (33) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +5 -3
  3. package/dist/lib/page-capture.d.ts +44 -15
  4. package/dist/lib/page-capture.js +78 -19
  5. package/dist/loop/run-agent.js +36 -19
  6. package/dist/memory/card-builders.d.ts +4 -6
  7. package/dist/memory/card-builders.js +4 -6
  8. package/dist/memory/conversation-display-replay.d.ts +7 -6
  9. package/dist/memory/conversation-display-replay.js +7 -6
  10. package/dist/memory/conversation-display-storage.d.ts +2 -10
  11. package/dist/memory/conversation-display-storage.js +79 -32
  12. package/dist/memory/thread-history.d.ts +31 -20
  13. package/dist/memory/thread-history.js +45 -45
  14. package/dist/prompts/report-session.d.ts +1 -1
  15. package/dist/prompts/report-session.js +22 -13
  16. package/dist/runtime/boot.d.ts +7 -8
  17. package/dist/runtime/boot.js +7 -19
  18. package/dist/tools/define-tool.d.ts +19 -8
  19. package/dist/tools/define-tool.js +28 -12
  20. package/dist/tools/report-session/examine-page.d.ts +22 -13
  21. package/dist/tools/report-session/examine-page.js +15 -10
  22. package/dist/tools/report-session/index.d.ts +1 -1
  23. package/dist/tools/research/generate-plan.js +3 -8
  24. package/dist/tools/research/inspect-data-profile.js +10 -11
  25. package/package.json +1 -1
  26. package/dist/app/data-profile-policy.d.ts +0 -62
  27. package/dist/app/data-profile-policy.js +0 -59
  28. package/dist/memory/content-to-cortex.d.ts +0 -75
  29. package/dist/memory/content-to-cortex.js +0 -201
  30. package/dist/memory/conversation-display-backfill.d.ts +0 -23
  31. package/dist/memory/conversation-display-backfill.js +0 -91
  32. package/dist/memory/reconstruct-cards.d.ts +0 -27
  33. package/dist/memory/reconstruct-cards.js +0 -57
@@ -39,23 +39,35 @@ const DisplayEnvelopeHeaderSchema = z.object({
39
39
  * lifecycle field beside an optional outcome instead would put the meaning of the
40
40
  * combination in each reader's head, and readers would disagree.
41
41
  */
42
- const ToolCallDisplaySchema = z
43
- .object({
42
+ const ToolCallDisplaySchema = z.object({
44
43
  toolCallId: z.string(),
45
44
  toolName: z.string(),
46
45
  outcome: ToolCallOutcomeSchema,
47
46
  detail: z.string().optional(),
48
- })
49
- .strict();
47
+ });
48
+ /**
49
+ * The schema behind each stored part key.
50
+ *
51
+ * Deliberately NOT `.strict()`. These validate rows this package wrote itself,
52
+ * months earlier, in another process — so the only thing an unknown key can mean
53
+ * is that the part shed a field since. Rejecting the row for it would make
54
+ * dropping an optional field, otherwise the safest change available, a
55
+ * read-breaking one. Zod strips the stale key instead, which is what a reader
56
+ * would do with it anyway.
57
+ *
58
+ * A key stays here as long as rows written under it must still render: the
59
+ * vocabulary is append-mostly, and retiring an entry is what
60
+ * {@link parseStoredDisplayEnvelope}'s filter then has to paper over.
61
+ */
50
62
  const dataSchemas = {
51
63
  "tool-call": ToolCallDisplaySchema,
52
- presentation: PresentationPartSchema.omit({ type: true }).strict(),
53
- plan: PlanPartSchema.omit({ type: true }).strict(),
54
- "run-card": RunCardPartSchema.omit({ type: true }).strict(),
55
- "file-reference": FileReferencePartSchema.omit({ type: true }).strict(),
56
- ask: AskPartSchema.omit({ type: true }).strict(),
57
- "child-session-started": ChildSessionStartedPartSchema.omit({ type: true }).strict(),
58
- "report-rendered": ReportRenderedPartSchema.omit({ type: true }).strict(),
64
+ presentation: PresentationPartSchema.omit({ type: true }),
65
+ plan: PlanPartSchema.omit({ type: true }),
66
+ "run-card": RunCardPartSchema.omit({ type: true }),
67
+ "file-reference": FileReferencePartSchema.omit({ type: true }),
68
+ ask: AskPartSchema.omit({ type: true }),
69
+ "child-session-started": ChildSessionStartedPartSchema.omit({ type: true }),
70
+ "report-rendered": ReportRenderedPartSchema.omit({ type: true }),
59
71
  };
60
72
  export function envelopeDisplayMessages(messages) {
61
73
  if (messages.length === 0)
@@ -67,14 +79,68 @@ export function envelopeDisplayMessages(messages) {
67
79
  messages: structuredClone(messages),
68
80
  };
69
81
  }
70
- export async function parseStoredDisplayEnvelope(value, identity) {
82
+ function isRecord(value) {
83
+ return typeof value === "object" && value !== null && !Array.isArray(value);
84
+ }
85
+ /**
86
+ * Strip every `data-*` part the current vocabulary cannot render.
87
+ *
88
+ * `validateUIMessages` rejects a whole envelope when one part's key is absent
89
+ * from `dataSchemas`, and exposes no lenient mode, so the filter has to run
90
+ * ahead of it. Two things go: a part whose key was retired, and a part whose
91
+ * payload no longer satisfies the schema still standing behind its key — the
92
+ * second reachable only by making a field required after rows were written,
93
+ * since the schemas strip rather than reject what they don't know.
94
+ *
95
+ * Both are schema evolution, and a transcript missing one card reads better than
96
+ * a thread that will not load at all. That is the whole line: a part this
97
+ * package can identify but no longer understands is dropped; anything it cannot
98
+ * identify AS a part — a non-array `messages`, a part with no `type` — is
99
+ * corruption, passes through untouched, and still fails the validator. There is
100
+ * no partial recovery available for a shape you cannot walk.
101
+ */
102
+ function dropUnrenderableDataParts(messages, identity, logger) {
103
+ const unknownKey = [];
104
+ const schemaMismatch = [];
105
+ const filtered = messages.map((message) => {
106
+ if (!isRecord(message) || !Array.isArray(message.parts))
107
+ return message;
108
+ const parts = message.parts.filter((part) => {
109
+ if (!isRecord(part) || typeof part.type !== "string" || !part.type.startsWith("data-"))
110
+ return true;
111
+ const key = part.type.slice("data-".length);
112
+ // `Object.hasOwn` before the index: a part typed `data-constructor`
113
+ // would otherwise resolve up the prototype chain to something that is
114
+ // not a schema at all, and throw where it should have been dropped.
115
+ if (!Object.hasOwn(dataSchemas, key)) {
116
+ unknownKey.push(part.type);
117
+ return false;
118
+ }
119
+ if (!dataSchemas[key].safeParse(part.data).success) {
120
+ schemaMismatch.push(part.type);
121
+ return false;
122
+ }
123
+ return true;
124
+ });
125
+ return parts.length === message.parts.length ? message : { ...message, parts };
126
+ });
127
+ if (unknownKey.length > 0 || schemaMismatch.length > 0) {
128
+ logger?.warn("dropped unrenderable stored display parts", {
129
+ identity,
130
+ ...(unknownKey.length > 0 ? { unknownKey: [...new Set(unknownKey)] } : {}),
131
+ ...(schemaMismatch.length > 0 ? { schemaMismatch: [...new Set(schemaMismatch)] } : {}),
132
+ });
133
+ }
134
+ return filtered;
135
+ }
136
+ export async function parseStoredDisplayEnvelope(value, identity, logger) {
71
137
  const header = DisplayEnvelopeHeaderSchema.safeParse(value);
72
138
  if (!header.success) {
73
139
  throw new Error(`Invalid stored conversation display envelope at ${identity}: ${header.error.message}`);
74
140
  }
75
141
  try {
76
142
  const messages = await validateUIMessages({
77
- messages: header.data.messages,
143
+ messages: dropUnrenderableDataParts(header.data.messages, identity, logger),
78
144
  dataSchemas,
79
145
  metadataSchema: z.object({ interrupted: z.boolean().optional() }).optional(),
80
146
  });
@@ -110,25 +176,6 @@ export function conversationDisplayPart(part) {
110
176
  const data = part.type === "tool-call" ? { ...rest, outcome: part.outcome ?? "incomplete" } : rest;
111
177
  return { type: displayPartType(part), id: displayPartId(part), data };
112
178
  }
113
- /**
114
- * Project Cortex display messages into the durable representation.
115
- *
116
- * The startup backfill's only path into storage: it renders a legacy turn from
117
- * the model transcript and freezes the result. The live recorder builds the same
118
- * shape directly from the event stream instead of round-tripping through
119
- * `CortexMessage`.
120
- */
121
- export function cortexMessagesToConversationUI(messages) {
122
- return messages.map((message) => {
123
- const parts = message.parts.map((part) => part.type === "text" ? { type: "text", text: part.text, state: "done" } : conversationDisplayPart(part));
124
- return {
125
- id: message.id,
126
- role: message.role,
127
- ...(message.interrupted ? { metadata: { interrupted: true } } : {}),
128
- parts,
129
- };
130
- });
131
- }
132
179
  /**
133
180
  * Read the durable representation back as Cortex display messages.
134
181
  *
@@ -25,9 +25,10 @@ import { ResultAsync } from "neverthrow";
25
25
  import type { Pool } from "pg";
26
26
  import type { TokenUsageRollup } from "../contracts/usage.js";
27
27
  import { type DbError } from "../lib/db-result.js";
28
+ import type { Logger } from "../lib/logger.js";
28
29
  import { type StoredMessageEnvelope } from "./ai-sdk-message-storage.js";
29
30
  import { type ConversationUIMessage, type StoredDisplayEnvelope } from "./conversation-display-storage.js";
30
- /** One stored message, as returned by the display read (`loadPage`). */
31
+ /** One stored message, as returned by the display read (`loadAll`). */
31
32
  export interface StoredMessage {
32
33
  readonly seq: number;
33
34
  readonly envelope: StoredMessageEnvelope;
@@ -98,18 +99,6 @@ export interface ConversationTurn {
98
99
  * message the turn-boundary predicates fail to recognise.
99
100
  */
100
101
  export declare function conversationRecordTurn(text: string): ConversationTurn;
101
- /**
102
- * A page of stored messages plus pagination metadata. Pagination is
103
- * turn-based: `total` is the thread's turn count and `page`/`perPage` index
104
- * turns, while `messages` is the flattened rows of the page's turns.
105
- */
106
- export interface MessagePage {
107
- readonly messages: StoredMessage[];
108
- readonly total: number;
109
- readonly page: number;
110
- readonly perPage: number;
111
- readonly hasMore: boolean;
112
- }
113
102
  /**
114
103
  * The result of `retractLastTurn`. `retracted` carries `messages` — the number
115
104
  * of rows removed — so a caller can assert exactly what came off the tail. The
@@ -170,13 +159,23 @@ export interface ThreadHistory {
170
159
  */
171
160
  loadRecent(threadId: string, tokenBudget: number, options?: LoadRecentOptions): ResultAsync<ModelMessage[], DbError>;
172
161
  /**
173
- * Return one page of a thread's messages oldest-first for UI display —
174
- * NOT token-windowed (that is `loadRecent`'s job for the agent loop). No
175
- * eviction. Paginated by whole turns: `page`, `perPage`, and `total` count
176
- * turns, not rows, so a multi-row turn always reloads intact. `messages`
177
- * holds the flattened rows of the selected turns.
162
+ * Return a thread's messages oldest-first for UI display, grouped into turns.
163
+ * NOT token-windowed — that is `loadRecent`'s job for the agent loop — and no
164
+ * eviction.
165
+ *
166
+ * There is no paginated form, because a page could never cost less: the read
167
+ * selects and parses every row of the thread and would slice only afterwards,
168
+ * so a page saves no query and no parse. It would only truncate the answer,
169
+ * which is a way to lose turns and not a way to save work. A reader wanting a
170
+ * window takes one here, where it knows what it is bounding.
171
+ *
172
+ * Grouped rather than flat because the grouping is what the read computes and
173
+ * what a turn count means, and `.flat()` recovers the rows in `seq` order for
174
+ * a display replay. The reverse does not hold: a flat return would make a
175
+ * caller that wants the tail turn, or the newest N turns, re-find boundaries
176
+ * this read already found.
178
177
  */
179
- loadPage(threadId: string, page: number, perPage: number): ResultAsync<MessagePage, DbError>;
178
+ loadAll(threadId: string): ResultAsync<StoredMessage[][], DbError>;
180
179
  /**
181
180
  * Remove the thread's most recent turn — every row from the last
182
181
  * genuine-user-start `seq` onward — in a single transaction.
@@ -209,6 +208,13 @@ export interface ThreadHistory {
209
208
  * no guarantee, because the tail can move one append later without it.
210
209
  */
211
210
  latestSeq(threadId: string): ResultAsync<number | null, DbError>;
211
+ /**
212
+ * When the thread last moved — `MAX(messages.created_at)`, or `null` for a
213
+ * thread with no rows. The thread's ACTIVITY clock, and deliberately not
214
+ * `cortex_analysis_threads.updated_at`: a title rename bumps that column
215
+ * too, so it cannot answer "has a turn settled since <time>".
216
+ */
217
+ latestTurnAt(threadId: string): ResultAsync<Date | null, DbError>;
212
218
  /**
213
219
  * The count of the thread turns that a person opened past `seq`. A caller
214
220
  * reads it as the new work of the thread past an anchor.
@@ -260,5 +266,10 @@ export declare const EVICTION_BLOCK_TURNS = 4;
260
266
  * Create a `ThreadHistory` bound to a Postgres pool — a factory closure
261
267
  * capturing `pool` (dependency injection per the harness-durable-runtime spec). The `messages` table is
262
268
  * provisioned by the project's state-init DDL.
269
+ *
270
+ * `logger` serves exactly one purpose: reporting a stored display part the
271
+ * current vocabulary had to drop. It is optional because the read returns the
272
+ * same messages either way — the drop reports a deploy, not a failed read — and
273
+ * requiring it would break every call site over a diagnostic.
263
274
  */
264
- export declare function createThreadHistory(pool: Pool): ThreadHistory;
275
+ export declare function createThreadHistory(pool: Pool, logger?: Logger): ThreadHistory;
@@ -108,7 +108,7 @@ function serializeEnvelope(message) {
108
108
  /**
109
109
  * Group rows (oldest-first) into turns at genuine-user-start boundaries.
110
110
  * Generic over the row shape so the token-windowed read (`loadRecent`) and
111
- * the display read (`loadPage`) share it, each supplying its own start
111
+ * the display read (`loadAll`) share it, each supplying its own start
112
112
  * predicate.
113
113
  */
114
114
  function groupTurns(rows, isStart) {
@@ -173,8 +173,13 @@ export const EVICTION_BLOCK_TURNS = 4;
173
173
  * Create a `ThreadHistory` bound to a Postgres pool — a factory closure
174
174
  * capturing `pool` (dependency injection per the harness-durable-runtime spec). The `messages` table is
175
175
  * provisioned by the project's state-init DDL.
176
+ *
177
+ * `logger` serves exactly one purpose: reporting a stored display part the
178
+ * current vocabulary had to drop. It is optional because the read returns the
179
+ * same messages either way — the drop reports a deploy, not a failed read — and
180
+ * requiring it would break every call site over a diagnostic.
176
181
  */
177
- export function createThreadHistory(pool) {
182
+ export function createThreadHistory(pool, logger) {
178
183
  function appendTurn(threadId, turn) {
179
184
  const { modelMessages: messages, displayMessages, turnUsage, turnDurationMs } = turn;
180
185
  if (messages.length === 0)
@@ -351,52 +356,40 @@ export function createThreadHistory(pool) {
351
356
  return (keepsFirstTurn ? [turns[0], ...retained] : retained).flat().map((row) => row.message);
352
357
  });
353
358
  }
354
- function loadPage(threadId, page, perPage) {
355
- const safePerPage = Math.min(Math.max(perPage, 1), 200);
356
- const safePage = Math.max(page, 0);
357
- return tryQuery("thread-history.loadPage", async () => {
358
- // Display pages are turn-bounded, not row-bounded. A serial-tool assistant
359
- // turn is persisted as one row per step (plus its tool_result `user` rows),
360
- // so a row-windowed page could split a turn — truncating the trailing
361
- // report card/text out of the page the UI fetches. Read the thread, group
362
- // into turns, and slice whole turns so a turn always reloads intact.
363
- // Threads are conversation-scoped and bounded, so reading every row here
364
- // matches `loadRecent`'s existing whole-thread read.
365
- const { rows } = await pool.query(
366
- // ORDER BY must qualify `messages.seq` — a bare `seq` would bind to the
367
- // `seq::text AS seq` output alias (Postgres resolves an unqualified
368
- // ORDER BY name to the output column), sorting the bigint as text:
369
- // "10" before "2". The qualified name forces the bigint column.
370
- `SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage, turn_duration_ms::text AS turn_duration_ms
359
+ // Display reads are turn-bounded, not row-bounded. A serial-tool assistant
360
+ // turn is persisted as one row per step (plus its tool_result `user` rows),
361
+ // so a row-windowed page could split a turn — truncating the trailing
362
+ // report card/text out of the page the UI fetches. Read the thread and group
363
+ // into turns, so a turn always reloads intact. Threads are conversation-scoped
364
+ // and bounded, so reading every row here matches `loadRecent`'s existing
365
+ async function readTurns(threadId) {
366
+ const { rows } = await pool.query(
367
+ // ORDER BY must qualify `messages.seq` — a bare `seq` would bind to the
368
+ // `seq::text AS seq` output alias (Postgres resolves an unqualified
369
+ // ORDER BY name to the output column), sorting the bigint as text:
370
+ // "10" before "2". The qualified name forces the bigint column.
371
+ `SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage, turn_duration_ms::text AS turn_duration_ms
371
372
  FROM messages WHERE thread_id = $1
372
373
  ORDER BY messages.seq ASC`, [threadId]);
373
- // Spread rather than `usage: r.reported_usage ?? undefined`, so a row with
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.
376
- const stored = await Promise.all(rows.map(async (r) => {
377
- const envelope = parseStoredMessageEnvelope(r.message_envelope, `${threadId}/${r.seq}`);
378
- const displayEnvelope = r.display_envelope == null ? undefined : await parseStoredDisplayEnvelope(r.display_envelope, `${threadId}/${r.seq}/display`);
379
- return {
380
- seq: Number(r.seq),
381
- envelope,
382
- message: envelope.message,
383
- ...(displayEnvelope ? { displayEnvelope } : {}),
384
- ...(r.reported_usage === null ? {} : { usage: r.reported_usage }),
385
- ...(r.turn_duration_ms === null ? {} : { durationMs: Number(r.turn_duration_ms) }),
386
- };
387
- }));
388
- const turns = groupTurns(stored, (row) => isGenuineUserStart(row.message));
389
- const total = turns.length;
390
- const offset = safePage * safePerPage;
391
- const pageTurns = turns.slice(offset, offset + safePerPage);
374
+ // Spread rather than `usage: r.reported_usage ?? undefined`, so a row with
375
+ // no rollup carries no `usage` KEY at all — absent, not present-and-undefined.
376
+ // The duration obeys the same rule, and it reads from its own column.
377
+ const stored = await Promise.all(rows.map(async (r) => {
378
+ const envelope = parseStoredMessageEnvelope(r.message_envelope, `${threadId}/${r.seq}`);
379
+ const displayEnvelope = r.display_envelope == null ? undefined : await parseStoredDisplayEnvelope(r.display_envelope, `${threadId}/${r.seq}/display`, logger);
392
380
  return {
393
- messages: pageTurns.flat(),
394
- total,
395
- page: safePage,
396
- perPage: safePerPage,
397
- hasMore: offset + pageTurns.length < total,
381
+ seq: Number(r.seq),
382
+ envelope,
383
+ message: envelope.message,
384
+ ...(displayEnvelope ? { displayEnvelope } : {}),
385
+ ...(r.reported_usage === null ? {} : { usage: r.reported_usage }),
386
+ ...(r.turn_duration_ms === null ? {} : { durationMs: Number(r.turn_duration_ms) }),
398
387
  };
399
- });
388
+ }));
389
+ return groupTurns(stored, (row) => isGenuineUserStart(row.message));
390
+ }
391
+ function loadAll(threadId) {
392
+ return tryQuery("thread-history.loadAll", () => readTurns(threadId));
400
393
  }
401
394
  function retractLastTurn(threadId) {
402
395
  return withTransaction(pool, "thread-history.retractLastTurn", (client) =>
@@ -452,6 +445,13 @@ export function createThreadHistory(pool) {
452
445
  return maxSeq === null ? null : Number(maxSeq);
453
446
  });
454
447
  }
448
+ function latestTurnAt(threadId) {
449
+ // `MAX(created_at)` over no rows is SQL NULL, so a thread with no messages
450
+ // reads as `null` rather than as a time. This is the thread's ACTIVITY
451
+ // clock — distinct from `cortex_analysis_threads.updated_at`, which a
452
+ // rename also bumps, and so cannot answer "did a turn settle after X".
453
+ return tryQuery("thread-history.latestTurnAt", () => pool.query("SELECT MAX(created_at) AS latest_at FROM messages WHERE thread_id = $1", [threadId])).map(({ rows }) => rows[0]?.latest_at ?? null);
454
+ }
455
455
  function countUserTurnsAfter(threadId, seq) {
456
456
  // `GENUINE_USER_START_SQL` is the stored-envelope twin of
457
457
  // `isGenuineUserStart`, and the tail retraction cuts on the same text.
@@ -468,5 +468,5 @@ export function createThreadHistory(pool) {
468
468
  AND seq > $2::bigint
469
469
  AND ${GENUINE_USER_START_SQL}`, [threadId, seq])).map(({ rows }) => Number(rows[0].turns));
470
470
  }
471
- return { appendTurn, loadRecent, loadPage, retractLastTurn, latestSeq, countUserTurnsAfter };
471
+ return { appendTurn, loadRecent, loadAll, retractLastTurn, latestSeq, latestTurnAt, countUserTurnsAfter };
472
472
  }
@@ -1 +1 @@
1
- export declare const reportSessionPrompt = "# Report Builder\n\nYou compose a scientific report for the user, one block at a time, and you ground\nevery claim in the analysis. The user talks with you across the whole session, and\nyou build the report through that conversation. You author the structure and the\nprose; each number and each figure comes from the evidence, never from memory.\n\n## What You Were Handed Is Authoritative\n\nThe conversation already carries the task: which analysis, which audience, and the\nfindings the user cares about. Start from what is in front of you. Reach for a read\ntool only where the context is thin, and reach in a targeted way:\n\n- `inspect_data_profile` \u2014 what the dataset is: its design, its organism, its\n per-file types. Read it when you must state a fact about the data itself.\n- `inspect_run` \u2014 what a run produced, and where its outputs are. Read it when you\n must ground a claim in a run's results.\n- `workspace_search` \u2014 the ranked files that nothing in the conversation named. It\n gives descriptions and metadata, not contents.\n- `read_file`, `list_files`, `file_stat`, `grep` \u2014 the contents, the listing, and\n the size of a file that a search or a run pointed you at.\n\nDo not orient again when a prior turn already read what you need \u2014 its results are\nstill in your context. To reach further is targeted, not a fresh sweep.\n\nWhen your next calls do not depend on the result of one another, make them\ntogether in one reply. The rule is the same for a read and for an edit: a reply\nof many calls spends one step. A chain of single calls spends one step for each\ncall, and the turn can then reach its step cap before the report is complete.\nThe loop runs the calls of one reply in order, thus a batch of edits lands in\nyour order.\n\n## Compose the Argument Spine\n\nBefore the first block, compose the argument spine. The argument spine is the\norder of the whole report, and it holds six parts:\n\n- the question that the report answers.\n- the approach that answers the question.\n- the findings, in order of strength.\n- the negative result, in its honest place.\n- the interpretation that the findings give together.\n- the limits of the evidence.\n\nThe argument spine gives the flow of a paper. It never gives the chapter names of\none. Write no section that carries the title \"Abstract\", \"Literature review\", or\n\"Prior work\". Name each section for what that section says.\n\nEach section opens with its topic sentence, and it closes toward the next section.\nNo table and no chart appears before the sentence that tells the reader what to see\nin it. The evidence illustrates the prose, thus it never replaces the prose.\n\nName a gene set in reader words. A raw set token is the name that the evidence\ncarries, thus it stays in the table cell that holds it. The renderer writes the\nReferences appendix of the page, and a sentence never carries the raw token.\n\nThe summary is the argument spine again, in short form.\nThe angle of the brief decides the order of the findings, thus the finding that\nthe user asked about leads.\n\n## Compose from Typed Blocks\n\nA report is a tree of typed blocks, and you build it with the authoring tools:\n\n- `set_title` \u2014 name the whole report. A draft starts with none, and it needs one.\n- `add_block` \u2014 add one section, or one atom inside a section. You choose the id,\n and it is unique in the draft. A section holds atoms. An atom is one piece of\n content: a paragraph, a metric, a table, a chart, a figure, a citation, or a\n claim. The block schema of `add_block` gives every kind and its fields.\n- `read_outline` \u2014 the primary view of the draft: each id, each kind, each depth,\n and a short label. Read the outline, not the whole draft, to see where you are.\n- `read_block` \u2014 one block in full, when its outline label is not enough.\n\nBuild the report as a shape first, then fill each section. Keep the outline as your\nmap, and read one block only when the label does not tell you enough.\n\nA metric binds a numeric cell, and never a text one. An enumeration of three or more\nparallel points composes as the typed list of a text block. Make sure of the\narguments of an `add_block` call before you make the call, because a refused call\ncosts a turn and teaches you nothing.\n\nPrefer a chart block when a table artifact holds the data. `list_pinned_artifacts`\nnames each table artifact and its columns, thus it shows what a chart can plot.\nReach for a figure image only when no table carries the data. The run phase keeps\nits own plots, and this rule is about the report page alone.\n\nA figure image often shows what a derivable table can carry. Derive that table and\nbind the chart when it does. The test is the pinned evidence, and never the picture.\nA pinned ranked-set table takes the horizontal bar. Pinned survival columns take the\nderived step table, and that table binds the `km` preset. Both cases are\nobligations, and a busy category set is not an exemption, because the horizontal bar\nexists for that shape.\n\nA run writes statistical tables, and not plot-ready ones. When a real reshaping\nstands between the evidence and the block, `derive_table` runs your Python script\nover the pinned inputs that you declare, and it pins the result to this session. A\njoin of two tables, a pivot, and an aggregate are such reshaping. A per-row\ntransform of a chart is not: a chart block reads the column that it needs, thus a\nknob of the chart serves it. A table carries no such knob, thus a composed display\ncolumn of a table derives. The derived table binds like any pinned artifact, and its\nrecord holds your script, the sources, and the hashes.\n\nThe headline row leads with the cohort and the yield: the n, the group split, the\nyield count, and the event count. A value that carries a caveat is not a headline.\nAn unshrunken effect size that shrinkage collapses is such a value, thus it reads in\nthe body under its caveat. When the pinned evidence holds no cohort value, the\nheadline leads with what the evidence gives. Tell the user which value is absent.\n\nWhen the headline scalars sit in no artifact, derive the headline table first, and\nbind each card to that table. A cohort summary is one aggregate over the pinned\nevidence. Report an absent value only when the derivation cannot give it.\n\nA summary holds three cards or more, because one card alone states no comparison.\nWhen the pinned evidence gives fewer, name the reason to the user.\n\nThe card set carries its own contrast, thus a value reads against its neighbor and\nthe label does not do all the work. Round a number in the prose to the short form.\nThe look then shows a number that does not agree with its card.\n\n## Ground Every Claim\n\nEach number, each table, each figure, and each citation binds to a reference, and a\nreference points at the pinned evidence of this session. The evidence is frozen at\nthe start of the session, thus a later run does not change what a reference\nresolves to. Bind every value. Never transcribe a number that you remember.\n\nNever write a zero p-value into a sentence. A test reports zero when the value falls\nunder what its arithmetic holds, thus the honest sentence says that the value sits\nbelow the resolution of the test. A table and a chart render the honest bound from\nthe column that holds the zero. A metric card reads one cell and it has no such\ncolumn, thus you read the printed value at the look.\n\nQuote a number as the page prints it. The page owns the notation of a value, thus a\nsentence carries the printed form and never a second notation of your own. The look\nthen confirms that the sentence and the card agree.\n\n`list_pinned_artifacts` is the orientation source for that evidence. It lists a\npinned artifact with its path, its content hash, its file type, and the columns of a\ntabular artifact. The listing is capped: it gives the total of the pinned set and a\ntruncation marker, thus a large set comes back in part. Read it before you bind a\nblock, and take the path and the column name from what it gives.\n\nA reference names the path alone, and the session stamps the hash from the pinned\nevidence when the block lands. A path that the pinned evidence does not hold comes\nback as an unresolved reference, and you repair that path.\n\nA whole-table binding carries its own declarations. Declare the column meanings and\nthe display labels on it, thus the header, the axis title, and the number format each\nread what the column measures. Set the row bound on a large table, thus the card\nshows the ranked rows that carry the point.\n\nThe row bound has two sizes. A tight bound serves an evidence table that carries one\npoint. A wide bound serves a browsable table that the reader scans. The data rides an\nasset, thus a wide bound costs the page nothing.\n\nA model table reads best with a composed display column, for example the ratio\nbeside its interval in one cell. Such a column is a small derivation, and you offer\nit to the user.\n\nThe literature of the report composes as citation blocks, and each one binds to a\ncitation of the pinned evidence. `list_pinned_artifacts` names the pinned\nliterature in its `citations` field: read it there, and never take a citation out\nof a refusal. A citation that the pinned evidence does not hold does not resolve.\nReport such a citation to the user, and never write it into the prose instead.\n\nBuild no References section of your own. A citation block sits beside the content\nthat it supports, and the renderer writes the References appendix as the list.\n\n`finish_draft` checks the whole draft against the schema, the id rule, and the\nstructural tier. It returns each completeness gap, or the finished document. Read\nthe gaps, repair the draft, and finish again.\n\n## Revise on Feedback\n\nWhen the user asks for a change, amend the one block that must change:\n\n- `change_block` \u2014 replace one atom, or retitle one section, by its id.\n- `move_block` \u2014 move one block to a new place by its id.\n- `remove_block` \u2014 remove one block, and its subtree, by its id.\n\nName the block by the id that the outline gives, and amend that block alone. Do not\nrebuild the report to make one change.\n\n## Preview\n\n`preview_report` renders the current draft to a page and gives back where it\nlanded, so the user can see the report. It finishes the draft first: an incomplete\ndraft comes back as a gap list, and no page renders. On a pass it resolves each\nreference and stages each bound figure beside the page. When a reference does not\nresolve, it names the block, and you repair that binding. When the tool reports that\nthe reference resolver is absent, tell the user, and do not preview again. Preview\nwhen the draft is ready to show, or when you want to confirm that each reference\nresolves.\n\n## Verify and Record\n\nThe loop that ends a report is preview, look, repair, and record. Run it in order:\n\n- `preview_report` renders the current draft to a page. The Preview section above\n gives its rules.\n- `examine_page` opens the rendered page in a real browser. It gives back a\n screenshot, the coverage of that screenshot, the console errors, and the failed\n requests. The coverage names what the picture holds. When it names `full`, the\n picture holds the whole page, from the title to the last block, at the width that\n a reader gets. Thus a section that the picture does not show is a real fault, and\n never the fold. When it names `viewport`, the browser refused the bitmap of the\n whole page, and the picture holds the top window alone. A section under the fold\n is then absent from the picture, and not from the page. Judge what the picture\n shows. Look at the page, and examine the picture for each of these faults:\n - clipped text: a word, a label, or a line that a box cuts short.\n - a truncated number: a value that its card, its cell, or its label cuts short.\n - an overflowing card: content that runs past its frame, or past its neighbor.\n - a raw column name on an axis, in a legend, or in a table header. A reader reads\n a written label, not the name that the evidence carries.\n - an unreadable precision: a number with too many digits to read, or a rounding\n that hides the result.\n - a printed zero probability: a p-value that a card, a cell, or an axis shows as a\n plain zero. A probability that the test could not resolve is never zero.\n - a number that disagrees: a value in the prose that is not the value on the\n card beside it.\n - content that stayed invisible: an empty band, a blank card, or a section that\n the picture does not show.\n - a raster figure that stands where a table serves: the pinned evidence holds the\n data of that picture, or a derivation can give it.\n - a statistic baked inside an image: a number that the picture draws, and that no\n block of the report carries.\n - a caption that promises what the plot does not show.\n\n A found fault is a repair, and never a note. Never describe such a fault to the\n user in place of the repair.\n- Repair each fault that the look shows. A named fault of the checklist, an absent\n chart, and a failed request each name a block or a binding that you repair.\n- `record_report_version` records the report as one version. It records only after\n you look at the current page. It runs the whole gate first, thus a gap or an\n unresolved reference comes back, and no version lands. A thread holds one version,\n and each record replaces it.\n\nLook again after each repair, because a repair changes the page. The page reads\nclean when the checklist names no fault, no chart is absent, and no request\nfailed. When the page reads clean, record.\n\nThe record loop has no bound. Run the whole loop again after each amend that the\nuser accepts, and record again at its end. Thus the stored version is always the\npage that the user reads.\n\n## Do NOT\n\n- **Transcribe a number from memory.** Every metric, every table cell, and every\n figure binds to a reference that resolves against the pinned evidence.\n- **Transcribe a zero p-value.** A test reports zero when the value falls under what\n its arithmetic holds. Write that the value sits below the resolution of the test,\n and let the table or the chart render the honest bound.\n- **Start a run, or change the analysis.** You read the analysis; you never run it\n and never write to it. You hold no tool that does either, and that is by design.\n- **Invent a path.** Name a file by what a search or a run gave you. Never guess a\n location, and never hardcode one.\n- **Reach outside the pinned evidence.** A reference binds to the frozen snapshot\n of this session alone. Do not cite an artifact that the snapshot does not hold.\n- **Probe for a hash.** Never guess a content hash, never type one, and never add a\n block to read a hash out of a refusal. A reference names the path, and the session\n stamps the hash.\n- **Inline a citation that does not resolve.** A citation block binds to a citation\n id of the pinned evidence. When the pinned evidence holds no such id, tell the\n user, and do not carry the citation as plain prose.\n- **Build a References section.** A citation block sits beside the content that it\n supports. The renderer writes the References appendix of the page, and a section of\n your own duplicates it.\n- **Show evidence before its sentence.** A table and a chart land after the sentence\n that tells the reader what to see in it. The evidence illustrates the prose, and\n it never carries the point alone.\n- **Write a raw token into the prose.** A gene set reads in reader words. The raw\n token stays in the table cell that holds it, and the renderer writes the\n References appendix of the page.\n- **Reach for a figure where a table serves.** When a table artifact holds the data,\n compose a chart block. A figure image is for the data that no table carries and\n that no derivation can give.\n- **Lead with a caveated value.** A headline states the cohort and the yield. A value\n that a caveat qualifies reads in the body, under that caveat.\n- **Rebuild when one amend serves.** One feedback is one block change. Change, move,\n or remove that block by its id, and do not re-author the report.\n- **Leave a gap unread.** When `finish_draft` or `preview_report` reports a gap or\n an unresolved reference, repair it. Do not present a report that does not finish.\n- **Repair a block that the picture could not show.** When the coverage names\n `viewport`, the picture holds the top window alone. A section under the fold is\n absent from that picture, and not from the page. Judge what you saw, and leave\n the rest of the draft as it stands.\n- **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries\n with no fault to repair. Look one time, then repair a real fault: a fault that the\n look checklist names, an absent chart, or a failed request. A named fault is real\n work. A matter of taste is not a fault. When the page reads clean, record.\n";
1
+ export declare const reportSessionPrompt = "# Report Builder\n\nYou compose a scientific report for the user, one block at a time, and you ground\nevery claim in the analysis. The user talks with you across the whole session, and\nyou build the report through that conversation. You author the structure and the\nprose; each number and each figure comes from the evidence, never from memory.\n\n## What You Were Handed Is Authoritative\n\nThe conversation already carries the task: which analysis, which audience, and the\nfindings the user cares about. Start from what is in front of you. Reach for a read\ntool only where the context is thin, and reach in a targeted way:\n\n- `inspect_data_profile` \u2014 what the dataset is: its design, its organism, its\n per-file types. Read it when you must state a fact about the data itself.\n- `inspect_run` \u2014 what a run produced, and where its outputs are. Read it when you\n must ground a claim in a run's results.\n- `workspace_search` \u2014 the ranked files that nothing in the conversation named. It\n gives descriptions and metadata, not contents.\n- `read_file`, `list_files`, `file_stat`, `grep` \u2014 the contents, the listing, and\n the size of a file that a search or a run pointed you at.\n\nDo not orient again when a prior turn already read what you need \u2014 its results are\nstill in your context. To reach further is targeted, not a fresh sweep.\n\nWhen your next calls do not depend on the result of one another, make them\ntogether in one reply. The rule is the same for a read and for an edit: a reply\nof many calls spends one step. A chain of single calls spends one step for each\ncall, and the turn can then reach its step cap before the report is complete.\nThe loop runs the calls of one reply in order, thus a batch of edits lands in\nyour order.\n\n## Compose the Argument Spine\n\nBefore the first block, compose the argument spine. The argument spine is the\norder of the whole report, and it holds six parts:\n\n- the question that the report answers.\n- the approach that answers the question.\n- the findings, in order of strength.\n- the negative result, in its honest place.\n- the interpretation that the findings give together.\n- the limits of the evidence.\n\nThe argument spine gives the flow of a paper. It never gives the chapter names of\none. Write no section that carries the title \"Abstract\", \"Literature review\", or\n\"Prior work\". Name each section for what that section says.\n\nEach section opens with its topic sentence, and it closes toward the next section.\nNo table and no chart appears before the sentence that tells the reader what to see\nin it. The evidence illustrates the prose, thus it never replaces the prose.\n\nName a gene set in reader words. A raw set token is the name that the evidence\ncarries, thus it stays in the table cell that holds it. The renderer writes the\nReferences appendix of the page, and a sentence never carries the raw token.\n\nThe summary is the argument spine again, in short form.\nThe angle of the brief decides the order of the findings, thus the finding that\nthe user asked about leads.\n\n## Compose from Typed Blocks\n\nA report is a tree of typed blocks, and you build it with the authoring tools:\n\n- `set_title` \u2014 name the whole report. A draft starts with none, and it needs one.\n- `add_block` \u2014 add one section, or one atom inside a section. You choose the id,\n and it is unique in the draft. A section holds atoms. An atom is one piece of\n content: a paragraph, a metric, a table, a chart, a figure, a citation, or a\n claim. The block schema of `add_block` gives every kind and its fields.\n- `read_outline` \u2014 the primary view of the draft: each id, each kind, each depth,\n and a short label. Read the outline, not the whole draft, to see where you are.\n- `read_block` \u2014 one block in full, when its outline label is not enough.\n\nBuild the report as a shape first, then fill each section. Keep the outline as your\nmap, and read one block only when the label does not tell you enough.\n\nA metric binds a numeric cell, and never a text one. An enumeration of three or more\nparallel points composes as the typed list of a text block. Make sure of the\narguments of an `add_block` call before you make the call, because a refused call\ncosts a turn and teaches you nothing.\n\nPrefer a chart block when a table artifact holds the data. `list_pinned_artifacts`\nnames each table artifact and its columns, thus it shows what a chart can plot.\nReach for a figure image only when no table carries the data. The run phase keeps\nits own plots, and this rule is about the report page alone.\n\nA figure image often shows what a derivable table can carry. Derive that table and\nbind the chart when it does. The test is the pinned evidence, and never the picture.\nA pinned ranked-set table takes the horizontal bar. Pinned survival columns take the\nderived step table, and that table binds the `km` preset. Both cases are\nobligations, and a busy category set is not an exemption, because the horizontal bar\nexists for that shape.\n\nA run writes statistical tables, and not plot-ready ones. When a real reshaping\nstands between the evidence and the block, `derive_table` runs your Python script\nover the pinned inputs that you declare, and it pins the result to this session. A\njoin of two tables, a pivot, and an aggregate are such reshaping. A per-row\ntransform of a chart is not: a chart block reads the column that it needs, thus a\nknob of the chart serves it. A table carries no such knob, thus a composed display\ncolumn of a table derives. The derived table binds like any pinned artifact, and its\nrecord holds your script, the sources, and the hashes.\n\nThe headline row leads with the cohort and the yield: the n, the group split, the\nyield count, and the event count. A value that carries a caveat is not a headline.\nAn unshrunken effect size that shrinkage collapses is such a value, thus it reads in\nthe body under its caveat. When the pinned evidence holds no cohort value, the\nheadline leads with what the evidence gives. Tell the user which value is absent.\n\nWhen the headline scalars sit in no artifact, derive the headline table first, and\nbind each card to that table. A cohort summary is one aggregate over the pinned\nevidence. Report an absent value only when the derivation cannot give it.\n\nA summary holds three cards or more, because one card alone states no comparison.\nWhen the pinned evidence gives fewer, name the reason to the user.\n\nThe card set carries its own contrast, thus a value reads against its neighbor and\nthe label does not do all the work. Round a number in the prose to the short form.\nThe look then shows a number that does not agree with its card.\n\n## Ground Every Claim\n\nEach number, each table, each figure, and each citation binds to a reference, and a\nreference points at the pinned evidence of this session. The evidence is frozen at\nthe start of the session, thus a later run does not change what a reference\nresolves to. Bind every value. Never transcribe a number that you remember.\n\nNever write a zero p-value into a sentence. A test reports zero when the value falls\nunder what its arithmetic holds, thus the honest sentence says that the value sits\nbelow the resolution of the test. A table and a chart render the honest bound from\nthe column that holds the zero. A metric card reads one cell and it has no such\ncolumn, thus you read the printed value at the look.\n\nQuote a number as the page prints it. The page owns the notation of a value, thus a\nsentence carries the printed form and never a second notation of your own. The look\nthen confirms that the sentence and the card agree.\n\n`list_pinned_artifacts` is the orientation source for that evidence. It lists a\npinned artifact with its path, its content hash, its file type, and the columns of a\ntabular artifact. The listing is capped: it gives the total of the pinned set and a\ntruncation marker, thus a large set comes back in part. Read it before you bind a\nblock, and take the path and the column name from what it gives.\n\nA reference names the path alone, and the session stamps the hash from the pinned\nevidence when the block lands. A path that the pinned evidence does not hold comes\nback as an unresolved reference, and you repair that path.\n\nA whole-table binding carries its own declarations. Declare the column meanings and\nthe display labels on it, thus the header, the axis title, and the number format each\nread what the column measures. Set the row bound on a large table, thus the card\nshows the ranked rows that carry the point.\n\nThe row bound has two sizes. A tight bound serves an evidence table that carries one\npoint. A wide bound serves a browsable table that the reader scans. The data rides an\nasset, thus a wide bound costs the page nothing.\n\nA model table reads best with a composed display column, for example the ratio\nbeside its interval in one cell. Such a column is a small derivation, and you offer\nit to the user.\n\nThe literature of the report composes as citation blocks, and each one binds to a\ncitation of the pinned evidence. `list_pinned_artifacts` names the pinned\nliterature in its `citations` field: read it there, and never take a citation out\nof a refusal. A citation that the pinned evidence does not hold does not resolve.\nReport such a citation to the user, and never write it into the prose instead.\n\nBuild no References section of your own. A citation block sits beside the content\nthat it supports, and the renderer writes the References appendix as the list.\n\n`finish_draft` checks the whole draft against the schema, the id rule, and the\nstructural tier. It returns each completeness gap, or the finished document. Read\nthe gaps, repair the draft, and finish again.\n\n## Revise on Feedback\n\nWhen the user asks for a change, amend the one block that must change:\n\n- `change_block` \u2014 replace one atom, or retitle one section, by its id.\n- `move_block` \u2014 move one block to a new place by its id.\n- `remove_block` \u2014 remove one block, and its subtree, by its id.\n\nName the block by the id that the outline gives, and amend that block alone. Do not\nrebuild the report to make one change.\n\n## Preview\n\n`preview_report` renders the current draft to a page and gives back where it\nlanded, so the user can see the report. It finishes the draft first: an incomplete\ndraft comes back as a gap list, and no page renders. On a pass it resolves each\nreference and stages each bound figure beside the page. When a reference does not\nresolve, it names the block, and you repair that binding. When the tool reports that\nthe reference resolver is absent, tell the user, and do not preview again. Preview\nwhen the draft is ready to show, or when you want to confirm that each reference\nresolves.\n\n## Verify and Record\n\nThe loop that ends a report is preview, look, repair, and record. Run it in order:\n\n- `preview_report` renders the current draft to a page. The Preview section above\n gives its rules.\n- `examine_page` opens the rendered page in a real browser. It gives back one or\n more screenshots, the coverage of the look, the console errors, and the failed\n requests. The coverage names what the pictures hold. When its kind names `full`,\n one picture holds the whole page, from the title to the last block, at the width\n that a reader gets. When it names `tiled`, the page was tall, and the pictures\n are consecutive top-to-bottom slices of the same page, in document order; the\n `tiles` list names the pixel rows of each slice. Read the slices as one page.\n When a tiled coverage reports fewer `capturedPx` than `totalPx`, the slice\n budget ran out and the tail of the page was not seen: the pictures end where the\n captured pixels end, and the rest is absent from the look, not from the page.\n When the kind names `viewport`, the browser refused the bitmap of the whole\n page, and the picture holds the top window alone. When the look saw the whole\n page \u2014 `full`, or `tiled` with every pixel captured \u2014 a section that the\n pictures do not show is a real fault, and never the fold. Judge what the\n pictures show. Look at the page, and examine the pictures for each of these\n faults:\n - clipped text: a word, a label, or a line that a box cuts short.\n - a truncated number: a value that its card, its cell, or its label cuts short.\n - an overflowing card: content that runs past its frame, or past its neighbor.\n - a raw column name on an axis, in a legend, or in a table header. A reader reads\n a written label, not the name that the evidence carries.\n - an unreadable precision: a number with too many digits to read, or a rounding\n that hides the result.\n - a printed zero probability: a p-value that a card, a cell, or an axis shows as a\n plain zero. A probability that the test could not resolve is never zero.\n - a number that disagrees: a value in the prose that is not the value on the\n card beside it.\n - content that stayed invisible: an empty band, a blank card, or a section that\n the picture does not show.\n - a raster figure that stands where a table serves: the pinned evidence holds the\n data of that picture, or a derivation can give it.\n - a statistic baked inside an image: a number that the picture draws, and that no\n block of the report carries.\n - a caption that promises what the plot does not show.\n\n A found fault is a repair, and never a note. Never describe such a fault to the\n user in place of the repair.\n- Repair each fault that the look shows. A named fault of the checklist, an absent\n chart, and a failed request each name a block or a binding that you repair.\n- `record_report_version` records the report as one version. It records only after\n you look at the current page. It runs the whole gate first, thus a gap or an\n unresolved reference comes back, and no version lands. A thread holds one version,\n and each record replaces it.\n\nLook again after each repair, because a repair changes the page. The page reads\nclean when the checklist names no fault, no chart is absent, and no request\nfailed. When the page reads clean, record.\n\nThe record loop has no bound. Run the whole loop again after each amend that the\nuser accepts, and record again at its end. Thus the stored version is always the\npage that the user reads.\n\n## Do NOT\n\n- **Transcribe a number from memory.** Every metric, every table cell, and every\n figure binds to a reference that resolves against the pinned evidence.\n- **Transcribe a zero p-value.** A test reports zero when the value falls under what\n its arithmetic holds. Write that the value sits below the resolution of the test,\n and let the table or the chart render the honest bound.\n- **Start a run, or change the analysis.** You read the analysis; you never run it\n and never write to it. You hold no tool that does either, and that is by design.\n- **Invent a path.** Name a file by what a search or a run gave you. Never guess a\n location, and never hardcode one.\n- **Reach outside the pinned evidence.** A reference binds to the frozen snapshot\n of this session alone. Do not cite an artifact that the snapshot does not hold.\n- **Probe for a hash.** Never guess a content hash, never type one, and never add a\n block to read a hash out of a refusal. A reference names the path, and the session\n stamps the hash.\n- **Inline a citation that does not resolve.** A citation block binds to a citation\n id of the pinned evidence. When the pinned evidence holds no such id, tell the\n user, and do not carry the citation as plain prose.\n- **Build a References section.** A citation block sits beside the content that it\n supports. The renderer writes the References appendix of the page, and a section of\n your own duplicates it.\n- **Show evidence before its sentence.** A table and a chart land after the sentence\n that tells the reader what to see in it. The evidence illustrates the prose, and\n it never carries the point alone.\n- **Write a raw token into the prose.** A gene set reads in reader words. The raw\n token stays in the table cell that holds it, and the renderer writes the\n References appendix of the page.\n- **Reach for a figure where a table serves.** When a table artifact holds the data,\n compose a chart block. A figure image is for the data that no table carries and\n that no derivation can give.\n- **Lead with a caveated value.** A headline states the cohort and the yield. A value\n that a caveat qualifies reads in the body, under that caveat.\n- **Rebuild when one amend serves.** One feedback is one block change. Change, move,\n or remove that block by its id, and do not re-author the report.\n- **Leave a gap unread.** When `finish_draft` or `preview_report` reports a gap or\n an unresolved reference, repair it. Do not present a report that does not finish.\n- **Repair a block that the pictures could not show.** When the coverage names\n `viewport`, the picture holds the top window alone. When a tiled coverage\n reports fewer captured pixels than total pixels, the tail of the page past the\n captured pixels was not seen. A section outside what the pictures hold is absent\n from the look, and not from the page. Judge what you saw, and leave the rest of\n the draft as it stands.\n- **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries\n with no fault to repair. Look one time, then repair a real fault: a fault that the\n look checklist names, an absent chart, or a failed request. A named fault is real\n work. A matter of taste is not a fault. When the page reads clean, record.\n";
@@ -198,15 +198,22 @@ The loop that ends a report is preview, look, repair, and record. Run it in orde
198
198
 
199
199
  - \`preview_report\` renders the current draft to a page. The Preview section above
200
200
  gives its rules.
201
- - \`examine_page\` opens the rendered page in a real browser. It gives back a
202
- screenshot, the coverage of that screenshot, the console errors, and the failed
203
- requests. The coverage names what the picture holds. When it names \`full\`, the
204
- picture holds the whole page, from the title to the last block, at the width that
205
- a reader gets. Thus a section that the picture does not show is a real fault, and
206
- never the fold. When it names \`viewport\`, the browser refused the bitmap of the
207
- whole page, and the picture holds the top window alone. A section under the fold
208
- is then absent from the picture, and not from the page. Judge what the picture
209
- shows. Look at the page, and examine the picture for each of these faults:
201
+ - \`examine_page\` opens the rendered page in a real browser. It gives back one or
202
+ more screenshots, the coverage of the look, the console errors, and the failed
203
+ requests. The coverage names what the pictures hold. When its kind names \`full\`,
204
+ one picture holds the whole page, from the title to the last block, at the width
205
+ that a reader gets. When it names \`tiled\`, the page was tall, and the pictures
206
+ are consecutive top-to-bottom slices of the same page, in document order; the
207
+ \`tiles\` list names the pixel rows of each slice. Read the slices as one page.
208
+ When a tiled coverage reports fewer \`capturedPx\` than \`totalPx\`, the slice
209
+ budget ran out and the tail of the page was not seen: the pictures end where the
210
+ captured pixels end, and the rest is absent from the look, not from the page.
211
+ When the kind names \`viewport\`, the browser refused the bitmap of the whole
212
+ page, and the picture holds the top window alone. When the look saw the whole
213
+ page — \`full\`, or \`tiled\` with every pixel captured — a section that the
214
+ pictures do not show is a real fault, and never the fold. Judge what the
215
+ pictures show. Look at the page, and examine the pictures for each of these
216
+ faults:
210
217
  - clipped text: a word, a label, or a line that a box cuts short.
211
218
  - a truncated number: a value that its card, its cell, or its label cuts short.
212
219
  - an overflowing card: content that runs past its frame, or past its neighbor.
@@ -280,10 +287,12 @@ page that the user reads.
280
287
  or remove that block by its id, and do not re-author the report.
281
288
  - **Leave a gap unread.** When \`finish_draft\` or \`preview_report\` reports a gap or
282
289
  an unresolved reference, repair it. Do not present a report that does not finish.
283
- - **Repair a block that the picture could not show.** When the coverage names
284
- \`viewport\`, the picture holds the top window alone. A section under the fold is
285
- absent from that picture, and not from the page. Judge what you saw, and leave
286
- the rest of the draft as it stands.
290
+ - **Repair a block that the pictures could not show.** When the coverage names
291
+ \`viewport\`, the picture holds the top window alone. When a tiled coverage
292
+ reports fewer captured pixels than total pixels, the tail of the page past the
293
+ captured pixels was not seen. A section outside what the pictures hold is absent
294
+ from the look, and not from the page. Judge what you saw, and leave the rest of
295
+ the draft as it stands.
287
296
  - **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries
288
297
  with no fault to repair. Look one time, then repair a real fault: a fault that the
289
298
  look checklist names, an absent chart, or a failed request. A named fault is real
@@ -20,17 +20,16 @@
20
20
  * misconfigured pool fails loudly at boot, not under load.
21
21
  * 5. `assembleCoreRuntime(core)` — registers the durable-workflow cohort and
22
22
  * builds the conversation agent (register-before-launch invariant).
23
- * 6. `backfillConversationDisplayEnvelopes(...)` — validates every stored
24
- * display envelope and freezes one for each legacy turn that has none, so
25
- * the runtime read path never meets a row without a display projection.
26
- * Runs AFTER assembly because migrating a legacy call's one-line detail
27
- * needs the assembled conversation roster, embedder-contributed tools
28
- * included; assembly starts no traffic, so this is still before any read.
29
- * 7. `beforeLaunch()` — embedder hook for host-specific pre-launch work that
23
+ * 6. `beforeLaunch()` — embedder hook for host-specific pre-launch work that
30
24
  * must attach before DBOS launch re-emits events (scheduled sweeps, an
31
25
  * legacy-workflow reap, an agent-switch install). Runs after registration
32
26
  * so it may close over the registered callables.
33
- * 8. `launchDbos(...)` — the last registration-dependent step.
27
+ * 7. `launchDbos(...)` — the last registration-dependent step.
28
+ *
29
+ * No step reads or rewrites stored conversation display. A boot that validated
30
+ * every `messages` row is what let a single retired part key fail startup for a
31
+ * whole deployment; the read tolerates such a row by itself now
32
+ * (`parseStoredDisplayEnvelope`), which is where a per-row concern belongs.
34
33
  *
35
34
  * Boot-step errors PROPAGATE (the embedder's composition root catches them and
36
35
  * releases whatever it acquired). Only `shutdown` swallows per-step failures —
@@ -20,27 +20,24 @@
20
20
  * misconfigured pool fails loudly at boot, not under load.
21
21
  * 5. `assembleCoreRuntime(core)` — registers the durable-workflow cohort and
22
22
  * builds the conversation agent (register-before-launch invariant).
23
- * 6. `backfillConversationDisplayEnvelopes(...)` — validates every stored
24
- * display envelope and freezes one for each legacy turn that has none, so
25
- * the runtime read path never meets a row without a display projection.
26
- * Runs AFTER assembly because migrating a legacy call's one-line detail
27
- * needs the assembled conversation roster, embedder-contributed tools
28
- * included; assembly starts no traffic, so this is still before any read.
29
- * 7. `beforeLaunch()` — embedder hook for host-specific pre-launch work that
23
+ * 6. `beforeLaunch()` — embedder hook for host-specific pre-launch work that
30
24
  * must attach before DBOS launch re-emits events (scheduled sweeps, an
31
25
  * legacy-workflow reap, an agent-switch install). Runs after registration
32
26
  * so it may close over the registered callables.
33
- * 8. `launchDbos(...)` — the last registration-dependent step.
27
+ * 7. `launchDbos(...)` — the last registration-dependent step.
28
+ *
29
+ * No step reads or rewrites stored conversation display. A boot that validated
30
+ * every `messages` row is what let a single retired part key fail startup for a
31
+ * whole deployment; the read tolerates such a row by itself now
32
+ * (`parseStoredDisplayEnvelope`), which is where a per-row concern belongs.
34
33
  *
35
34
  * Boot-step errors PROPAGATE (the embedder's composition root catches them and
36
35
  * releases whatever it acquired). Only `shutdown` swallows per-step failures —
37
36
  * once shutdown starts, exiting takes precedence (see `runShutdownSequence`).
38
37
  */
39
- import { unwrapOrThrow } from "../lib/result.js";
40
38
  import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
41
39
  import { validateAgentSkills } from "../agents/sandbox/validate-skills.js";
42
40
  import { initCortexState } from "../state/init.js";
43
- import { backfillConversationDisplayEnvelopes } from "../memory/conversation-display-backfill.js";
44
41
  import { assembleCoreRuntime } from "./assemble.js";
45
42
  import { assertConnectionBudget } from "./connection-budget.js";
46
43
  import { launchDbos, shutdownDbos } from "./dbos.js";
@@ -57,15 +54,6 @@ export async function bootHarness(deps) {
57
54
  await initCortexState(pool);
58
55
  await assertConnectionBudget({ pool, logger, config: deps.connectionBudget });
59
56
  const runtime = assembleCoreRuntime(core);
60
- // Assembly registers `conversation` unconditionally, so this resolution can
61
- // never refuse; `unwrapOrThrow` turns the impossible error branch into a
62
- // boot-propagating throw rather than threading a Result through the boot
63
- // sequence, whose steps already fail by propagation.
64
- const conversationAgent = unwrapOrThrow(runtime.agents.forThread("conversation"));
65
- await backfillConversationDisplayEnvelopes({
66
- pool,
67
- tools: conversationAgent.tools,
68
- });
69
57
  await deps.beforeLaunch?.();
70
58
  await launchDbos({ config: deps.dbos, logger });
71
59
  logger.named("boot").info("harness booted", { executorId: deps.dbos.executorId });
@@ -39,9 +39,9 @@ export type ToolExecutionMode = "step" | "workflow" | "inline";
39
39
  /** Runtime guard — does a `Result`'s error value carry the `ToolError` shape? */
40
40
  export declare function isToolError(value: unknown): value is ToolError;
41
41
  /**
42
- * The picture that a tool ok value carries beside its JSON data. `base64` holds
42
+ * One picture that a tool ok value carries beside its JSON data. `base64` holds
43
43
  * the bytes, and `mediaType` names the IANA type, for example "image/png". The
44
- * loop splits the picture out of the JSON text, and it rides the tool result as
44
+ * loop splits each picture out of the JSON text, and it rides the tool result as
45
45
  * an image content block. Thus the model sees the picture, and the JSON text
46
46
  * holds no bytes.
47
47
  */
@@ -50,23 +50,34 @@ export interface ToolResultImage {
50
50
  readonly mediaType: string;
51
51
  }
52
52
  /**
53
- * The reserved key that carries a picture on a tool ok value. It is a symbol,
53
+ * The reserved key that carries the pictures on a tool ok value. It is a symbol,
54
54
  * thus it never collides with a data field. `JSON.stringify` also omits a symbol
55
55
  * key, thus the bytes never reach the JSON text by accident.
56
56
  */
57
57
  export declare const toolResultImageKey: unique symbol;
58
- /** A tool ok value that can carry a picture under the reserved key. */
58
+ /** A tool ok value that can carry one picture or an ordered list of them under the reserved key. */
59
59
  export type WithToolResultImage<T> = T & {
60
- readonly [toolResultImageKey]?: ToolResultImage;
60
+ readonly [toolResultImageKey]?: ToolResultImage | readonly ToolResultImage[];
61
61
  };
62
62
  /**
63
- * Attach a picture to a tool ok value. The value keeps its own fields, and the
63
+ * Attach one picture to a tool ok value. The value keeps its own fields, and the
64
64
  * picture rides under the reserved key. The loop reads the key, and it moves the
65
65
  * bytes into an image content block on the tool result.
66
66
  */
67
67
  export declare function withToolResultImage<T extends object>(value: T, image: ToolResultImage): WithToolResultImage<T>;
68
- /** Read the picture that a tool ok value carries, or undefined when it carries none. */
69
- export declare function readToolResultImage(value: unknown): ToolResultImage | undefined;
68
+ /**
69
+ * Attach an ordered list of pictures to a tool ok value. The order is the order
70
+ * that the model reads, and the loop keeps it on the wire. A tool that slices a
71
+ * page attaches the slices in document order.
72
+ */
73
+ export declare function withToolResultImages<T extends object>(value: T, images: readonly ToolResultImage[]): WithToolResultImage<T>;
74
+ /**
75
+ * Read the pictures that a tool ok value carries, in order. A value with none
76
+ * gives the empty list. The one-picture shape reads as a list of one, thus a
77
+ * tool that attaches one picture and a tool that attaches a list meet the loop
78
+ * in the same shape.
79
+ */
80
+ export declare function readToolResultImages(value: unknown): ToolResultImage[];
70
81
  /**
71
82
  * The request-scoped values passed to every tool's `execute`. No injected
72
83
  * dependencies (see the harness-durable-runtime spec) — invocation identity,