@inflexa-ai/harness 0.23.0 → 0.24.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 (68) hide show
  1. package/README.md +1 -2
  2. package/dist/agents/conversation-agent.d.ts +0 -9
  3. package/dist/contracts/chat-parts.d.ts +29 -33
  4. package/dist/contracts/content-url.d.ts +7 -41
  5. package/dist/contracts/content-url.js +7 -48
  6. package/dist/contracts/index.d.ts +2 -2
  7. package/dist/contracts/index.js +1 -1
  8. package/dist/contracts/message.d.ts +1 -1
  9. package/dist/contracts/message.js +1 -1
  10. package/dist/contracts/part-registry.js +2 -3
  11. package/dist/contracts/schemas/chat-parts.d.ts +16 -53
  12. package/dist/contracts/schemas/chat-parts.js +15 -25
  13. package/dist/contracts/schemas/index.d.ts +1 -1
  14. package/dist/contracts/schemas/index.js +1 -1
  15. package/dist/execution/run-authorizer.d.ts +1 -1
  16. package/dist/execution/run-authorizer.js +1 -1
  17. package/dist/index.d.ts +1 -3
  18. package/dist/index.js +0 -2
  19. package/dist/lib/zod-issues.d.ts +2 -3
  20. package/dist/lib/zod-issues.js +2 -3
  21. package/dist/loop/__fixtures__/scripted-provider.js +3 -1
  22. package/dist/loop/run-agent.d.ts +5 -0
  23. package/dist/loop/run-agent.js +10 -1
  24. package/dist/loop/run-to-terminal.d.ts +1 -1
  25. package/dist/loop/run-to-terminal.js +1 -1
  26. package/dist/memory/card-builders.d.ts +0 -27
  27. package/dist/memory/card-builders.js +0 -75
  28. package/dist/memory/conversation-display-backfill.d.ts +2 -4
  29. package/dist/memory/conversation-display-backfill.js +3 -9
  30. package/dist/memory/conversation-display-storage.d.ts +3 -4
  31. package/dist/memory/conversation-display-storage.js +3 -4
  32. package/dist/memory/reconstruct-cards.d.ts +5 -7
  33. package/dist/memory/reconstruct-cards.js +6 -20
  34. package/dist/runtime/boot.js +2 -12
  35. package/dist/tools/report-session/preview-report.d.ts +2 -1
  36. package/dist/tools/report-session/preview-report.js +9 -1
  37. package/dist/tools/start-report-session.js +2 -2
  38. package/dist/workspace/paths.d.ts +0 -21
  39. package/dist/workspace/paths.js +0 -37
  40. package/package.json +1 -3
  41. package/dist/agents/report-builder.d.ts +0 -34
  42. package/dist/agents/report-builder.js +0 -34
  43. package/dist/execution/report-render.d.ts +0 -39
  44. package/dist/execution/report-render.js +0 -98
  45. package/dist/execution/report-runner.d.ts +0 -111
  46. package/dist/execution/report-runner.js +0 -230
  47. package/dist/prompts/report-builder.d.ts +0 -1
  48. package/dist/prompts/report-builder.js +0 -174
  49. package/dist/tools/iterate-report.d.ts +0 -227
  50. package/dist/tools/iterate-report.js +0 -883
  51. package/dist/tools/lib/preview-lock.d.ts +0 -20
  52. package/dist/tools/lib/preview-lock.js +0 -35
  53. package/dist/tools/lib/report-preflight.d.ts +0 -39
  54. package/dist/tools/lib/report-preflight.js +0 -283
  55. package/dist/tools/report/build-report.d.ts +0 -13
  56. package/dist/tools/report/build-report.js +0 -27
  57. package/dist/tools/report/index.d.ts +0 -15
  58. package/dist/tools/report/index.js +0 -15
  59. package/dist/tools/report/mint-preview-url.d.ts +0 -26
  60. package/dist/tools/report/mint-preview-url.js +0 -47
  61. package/dist/tools/report/preview-publisher.d.ts +0 -53
  62. package/dist/tools/report/preview-publisher.js +0 -45
  63. package/dist/tools/report/preview-snapshot.d.ts +0 -34
  64. package/dist/tools/report/preview-snapshot.js +0 -98
  65. package/dist/tools/report/submit-report.d.ts +0 -26
  66. package/dist/tools/report/submit-report.js +0 -148
  67. package/dist/tools/report/version-fs.d.ts +0 -15
  68. package/dist/tools/report/version-fs.js +0 -165
@@ -39,14 +39,6 @@ export interface RunCardData {
39
39
  title: string;
40
40
  stepCount: number;
41
41
  }
42
- export interface PreviewCardData {
43
- id: string;
44
- previewId: string;
45
- version: number;
46
- title: string;
47
- previewPath: string;
48
- format: "html" | "pdf";
49
- }
50
42
  /** Build the `data-plan` payload by loading the stored plan. `ok(null)` when
51
43
  * the plan is missing or no longer parses (absence is NOT an error) — the
52
44
  * caller falls back to a chip. */
@@ -100,22 +92,3 @@ export declare function fileGroupHash(paths: string[], title: string | undefined
100
92
  * path the live tool rejected would be resurrected into a card on reload.
101
93
  */
102
94
  export declare function buildFileReferenceCardData(input: Record<string, unknown>): FileReferenceCardData | null;
103
- /**
104
- * Build the `data-report-preview` payload for an `iterate_report` tool_use. The
105
- * preview's source of truth is the filesystem under
106
- * `previews/{previewId}/`: directory layout yields `version`
107
- * (latest `vN`) + `previewPath`, and `preview-meta.json` yields title/format.
108
- * Mirrors `buildRunCardData` — the live emit knows the server-generated
109
- * `previewId`, while the reconstruct-on-read path resolves it (from the
110
- * iteration's `previewId` input, or else the analysis's latest preview). The
111
- * card is keyed by `previewId` only, so every iteration's tool_use rebuilds
112
- * the same card and the frontend groups them into one with all versions selectable.
113
- * Null when nothing renders — the caller falls back to a chip.
114
- */
115
- export declare function buildPreviewCardData(
116
- /** Absolute host root of the analysis's workspace tree (previews live inside it). */
117
- workspaceRoot: string, opts: {
118
- previewId?: string;
119
- title?: string;
120
- format?: "html" | "pdf";
121
- }): Promise<PreviewCardData | null>;
@@ -16,17 +16,12 @@
16
16
  * under the part `type` directly.
17
17
  */
18
18
  import { createHash } from "node:crypto";
19
- import { readdir, readFile, stat } from "node:fs/promises";
20
- import { join } from "node:path";
21
19
  import { okAsync } from "neverthrow";
22
20
  import { tryQuery } from "../lib/db-result.js";
23
- import { tryFs } from "../lib/fs-result.js";
24
21
  import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
25
22
  import { loadPlan } from "../state/index.js";
26
23
  import { normalizeEchartSpec } from "../tools/display/normalize-echart-spec.js";
27
24
  import { validatePath } from "../tools/lib/path-validation.js";
28
- import { latestPreviewVersion, previewDir, PREVIEWS_ROOT } from "../workspace/paths.js";
29
- const PREVIEW_META_FILE = "preview-meta.json";
30
25
  function isRecord(value) {
31
26
  return typeof value === "object" && value !== null && !Array.isArray(value);
32
27
  }
@@ -176,73 +171,3 @@ export function buildFileReferenceCardData(input) {
176
171
  const id = `pres-${fileGroupHash(files.map((f) => f.path), title)}`;
177
172
  return { id, ...(title !== undefined ? { title } : {}), files };
178
173
  }
179
- /** The stored preview metadata, or `null` when the file is absent — a malformed
180
- * file is a genuine read failure (`err`), folded to `null` at the call site. */
181
- function readPreviewMeta(previewRootAbs) {
182
- const path = join(previewRootAbs, PREVIEW_META_FILE);
183
- return tryFs("cardBuilders.readPreviewMeta", async () => JSON.parse(await readFile(path, "utf8")), { path, onAbsent: () => null });
184
- }
185
- /** Directory names (not files) directly under `dir`, or `[]` when absent. */
186
- function subdirs(dir) {
187
- return tryFs("cardBuilders.subdirs", async () => (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name), {
188
- path: dir,
189
- onAbsent: () => [],
190
- });
191
- }
192
- /** The most-recently-modified previewId under an analysis, or null when none. */
193
- function latestPreviewId(previewsRootAbs) {
194
- return subdirs(previewsRootAbs).map(async (dirs) => {
195
- if (dirs.length <= 1)
196
- return dirs[0] ?? null;
197
- let best = null;
198
- for (const name of dirs) {
199
- const path = join(previewsRootAbs, name);
200
- const mtimeMs = await tryFs("cardBuilders.latestPreviewId.stat", async () => (await stat(path)).mtimeMs, { path }).unwrapOr(null); // skip an unreadable/absent entry
201
- if (mtimeMs !== null && (!best || mtimeMs > best.mtimeMs)) {
202
- best = { name, mtimeMs };
203
- }
204
- }
205
- return best?.name ?? dirs[0];
206
- });
207
- }
208
- /**
209
- * Build the `data-report-preview` payload for an `iterate_report` tool_use. The
210
- * preview's source of truth is the filesystem under
211
- * `previews/{previewId}/`: directory layout yields `version`
212
- * (latest `vN`) + `previewPath`, and `preview-meta.json` yields title/format.
213
- * Mirrors `buildRunCardData` — the live emit knows the server-generated
214
- * `previewId`, while the reconstruct-on-read path resolves it (from the
215
- * iteration's `previewId` input, or else the analysis's latest preview). The
216
- * card is keyed by `previewId` only, so every iteration's tool_use rebuilds
217
- * the same card and the frontend groups them into one with all versions selectable.
218
- * Null when nothing renders — the caller falls back to a chip.
219
- */
220
- export async function buildPreviewCardData(
221
- /** Absolute host root of the analysis's workspace tree (previews live inside it). */
222
- workspaceRoot, opts) {
223
- const previewsRootAbs = join(workspaceRoot, PREVIEWS_ROOT);
224
- const resolveId = opts.previewId ? okAsync(opts.previewId) : latestPreviewId(previewsRootAbs);
225
- return resolveId
226
- .andThen((previewId) => {
227
- if (!previewId)
228
- return okAsync(null);
229
- const previewRootAbs = join(workspaceRoot, previewDir(previewId));
230
- return subdirs(previewRootAbs).andThen((versionDirs) => {
231
- const version = latestPreviewVersion(versionDirs);
232
- if (version === 0)
233
- return okAsync(null);
234
- return readPreviewMeta(previewRootAbs).map((meta) => {
235
- const id = `prev-${createHash("sha256").update(previewId).digest("hex").slice(0, 16)}`;
236
- return {
237
- id,
238
- previewId,
239
- version,
240
- title: opts.title ?? meta?.title ?? "Report",
241
- previewPath: `v${version}/index.html`,
242
- format: opts.format ?? (meta?.format === "pdf" ? "pdf" : "html"),
243
- };
244
- });
245
- });
246
- })
247
- .unwrapOr(null);
248
- }
@@ -1,9 +1,7 @@
1
1
  import type { Pool } from "pg";
2
2
  import type { Tool } from "../tools/define-tool.js";
3
- import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
4
3
  export interface ConversationDisplayBackfillDeps {
5
4
  readonly pool: Pool;
6
- readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
7
5
  /**
8
6
  * The conversation agent's composed roster, used to recover each legacy call's
9
7
  * one-line detail from its persisted input. Tools contributed through the
@@ -17,8 +15,8 @@ export interface ConversationDisplayBackfillDeps {
17
15
  * Freeze the migration renderer's output into display envelopes, once per legacy
18
16
  * turn, so the runtime read path never has to reconstruct anything.
19
17
  *
20
- * Missing mutable resources degrade to a generic, cardless display — a workspace
21
- * that has moved is normal historical absence, not a fault. Malformed stored rows
18
+ * Missing mutable resources degrade to a generic, cardless display — a plan
19
+ * that is absent is normal historical absence, not a fault. Malformed stored rows
22
20
  * and database faults remain startup-fatal: a turn that cannot be migrated must
23
21
  * not be silently skipped and then read as if it had no display.
24
22
  */
@@ -10,8 +10,8 @@ const GENUINE_USER_SQL = `m.message_envelope->'message'->>'role' = 'user'
10
10
  * Freeze the migration renderer's output into display envelopes, once per legacy
11
11
  * turn, so the runtime read path never has to reconstruct anything.
12
12
  *
13
- * Missing mutable resources degrade to a generic, cardless display — a workspace
14
- * that has moved is normal historical absence, not a fault. Malformed stored rows
13
+ * Missing mutable resources degrade to a generic, cardless display — a plan
14
+ * that is absent is normal historical absence, not a fault. Malformed stored rows
15
15
  * and database faults remain startup-fatal: a turn that cannot be migrated must
16
16
  * not be silently skipped and then read as if it had no display.
17
17
  */
@@ -64,13 +64,7 @@ export async function backfillConversationDisplayEnvelopes(deps) {
64
64
  });
65
65
  let resolveCard;
66
66
  if (head.analysis_id !== null) {
67
- try {
68
- resolveCard = createCardResolver(deps.pool, head.analysis_id, deps.resolveWorkspaceRoot(head.analysis_id));
69
- }
70
- catch {
71
- // A removed/moved workspace is normal historical absence.
72
- resolveCard = undefined;
73
- }
67
+ resolveCard = createCardResolver(deps.pool, head.analysis_id);
74
68
  }
75
69
  const cortex = await contentToCortexMessages(stored, { resolveCard, resolveDetail });
76
70
  const uiMessages = cortexMessagesToConversationUI(cortex);
@@ -15,7 +15,7 @@
15
15
  * reconstruction.
16
16
  */
17
17
  import { type DataUIPart, type UIMessage } from "ai";
18
- import type { AskPart, FileReferencePart, PlanPart, PresentationPart, PreviewPart, DataPreviewFailedPart, ReportSessionStartedPart, RunCardPart } from "../contracts/chat-parts.js";
18
+ import type { AskPart, ChildSessionStartedPart, FileReferencePart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart } from "../contracts/chat-parts.js";
19
19
  import type { ToolCallOutcome } from "../contracts/chat-events.js";
20
20
  import type { CortexMessage, CortexPart, ToolCallPart } from "../contracts/message.js";
21
21
  export declare const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
@@ -38,9 +38,8 @@ export type ConversationUIData = {
38
38
  "run-card": Payload<RunCardPart>;
39
39
  "file-reference": Payload<FileReferencePart>;
40
40
  ask: Payload<AskPart>;
41
- "report-preview": Payload<PreviewPart>;
42
- "report-preview-failed": Payload<DataPreviewFailedPart>;
43
- "report-session-started": Payload<ReportSessionStartedPart>;
41
+ "child-session-started": Payload<ChildSessionStartedPart>;
42
+ "report-rendered": Payload<ReportRenderedPart>;
44
43
  };
45
44
  export interface ConversationDisplayMetadata {
46
45
  interrupted?: boolean;
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { validateUIMessages } from "ai";
18
18
  import { z } from "zod";
19
- import { AskPartSchema, DataPreviewFailedPartSchema, FileReferencePartSchema, PlanPartSchema, PresentationPartSchema, PreviewPartSchema, ReportSessionStartedPartSchema, RunCardPartSchema, } from "../contracts/schemas/chat-parts.js";
19
+ import { AskPartSchema, ChildSessionStartedPartSchema, FileReferencePartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, } from "../contracts/schemas/chat-parts.js";
20
20
  import { ToolCallOutcomeSchema } from "../contracts/schemas/chat-events.js";
21
21
  export const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
22
22
  const DisplayEnvelopeHeaderSchema = z.object({
@@ -54,9 +54,8 @@ const dataSchemas = {
54
54
  "run-card": RunCardPartSchema.omit({ type: true }).strict(),
55
55
  "file-reference": FileReferencePartSchema.omit({ type: true }).strict(),
56
56
  ask: AskPartSchema.omit({ type: true }).strict(),
57
- "report-preview": PreviewPartSchema.omit({ type: true }).strict(),
58
- "report-preview-failed": DataPreviewFailedPartSchema.omit({ type: true }).strict(),
59
- "report-session-started": ReportSessionStartedPartSchema.omit({ type: true }).strict(),
57
+ "child-session-started": ChildSessionStartedPartSchema.omit({ type: true }).strict(),
58
+ "report-rendered": ReportRenderedPartSchema.omit({ type: true }).strict(),
60
59
  };
61
60
  export function envelopeDisplayMessages(messages) {
62
61
  if (messages.length === 0)
@@ -4,15 +4,13 @@
4
4
  * through the pre-v1 direct-upgrade window, then remove its runtime fallback
5
5
  * in a dedicated compatibility change.
6
6
  *
7
- * `show_plan` / `show_user` / `show_file` / `execute_analysis` / `iterate_report` emit
8
- * `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card` /
9
- * `data-report-preview` cards
7
+ * `show_plan` / `show_user` / `show_file` / `execute_analysis` emit
8
+ * `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card` cards
10
9
  * live over the chat SSE stream. On reload, `content-to-cortex` calls this
11
10
  * resolver for each AI SDK tool-call part: a recognised display tool yields its card (rebuilt via the
12
11
  * shared `card-builders`); anything else yields `null` and falls back to a
13
- * generic `tool-call` chip. Keyed by the analysis `Pool` + id (plan / run) or
14
- * the analysis's workspace root (preview, filesystem-backed) so the card can
15
- * be re-loaded.
12
+ * generic `tool-call` chip. Keyed by the analysis `Pool` + id (plan / run) so
13
+ * the card can be re-loaded.
16
14
  */
17
15
  import type { CortexPart } from "@inflexa-ai/harness/contracts/message.js";
18
16
  import type { Pool } from "pg";
@@ -26,4 +24,4 @@ export interface StoredToolCallForCard {
26
24
  /** @deprecated Used only by the legacy display backfill/fallback. */
27
25
  export type ToolCardResolver = (block: StoredToolCallForCard) => Promise<CortexPart | null>;
28
26
  /** @deprecated Use persisted conversation display envelopes for runtime reads. */
29
- export declare function createCardResolver(pool: Pool, analysisId: string, workspaceRoot: string): ToolCardResolver;
27
+ export declare function createCardResolver(pool: Pool, analysisId: string): ToolCardResolver;
@@ -4,38 +4,24 @@
4
4
  * through the pre-v1 direct-upgrade window, then remove its runtime fallback
5
5
  * in a dedicated compatibility change.
6
6
  *
7
- * `show_plan` / `show_user` / `show_file` / `execute_analysis` / `iterate_report` emit
8
- * `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card` /
9
- * `data-report-preview` cards
7
+ * `show_plan` / `show_user` / `show_file` / `execute_analysis` emit
8
+ * `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card` cards
10
9
  * live over the chat SSE stream. On reload, `content-to-cortex` calls this
11
10
  * resolver for each AI SDK tool-call part: a recognised display tool yields its card (rebuilt via the
12
11
  * shared `card-builders`); anything else yields `null` and falls back to a
13
- * generic `tool-call` chip. Keyed by the analysis `Pool` + id (plan / run) or
14
- * the analysis's workspace root (preview, filesystem-backed) so the card can
15
- * be re-loaded.
12
+ * generic `tool-call` chip. Keyed by the analysis `Pool` + id (plan / run) so
13
+ * the card can be re-loaded.
16
14
  */
17
15
  import { unwrapOrThrow } from "../lib/result.js";
18
16
  import { adHocPlanId, adHocRunId } from "../tools/analysis-invocation.js";
19
17
  import { validatePath } from "../tools/lib/path-validation.js";
20
- import { buildFileReferenceCardData, buildPlanCardData, buildPresentationCardData, buildPreviewCardData, buildRunCardData } from "./card-builders.js";
18
+ import { buildFileReferenceCardData, buildPlanCardData, buildPresentationCardData, buildRunCardData } from "./card-builders.js";
21
19
  /** @deprecated Use persisted conversation display envelopes for runtime reads. */
22
- export function createCardResolver(pool, analysisId, workspaceRoot) {
20
+ export function createCardResolver(pool, analysisId) {
23
21
  return async (block) => {
24
22
  if (block.type !== "tool_use")
25
23
  return null;
26
24
  const input = (block.input ?? {});
27
- // The report tool is `submit_report`; legacy transcripts carry the
28
- // former ids `iterate_report` / `iterateReport` — accept all three. The
29
- // brief-carrying field is still `report`, so the card reads identically.
30
- if (block.name === "submit_report" || block.name === "iterate_report" || block.name === "iterateReport") {
31
- const report = (input.report ?? null);
32
- const card = await buildPreviewCardData(workspaceRoot, {
33
- previewId: typeof input.previewId === "string" ? input.previewId : undefined,
34
- title: report && typeof report.title === "string" ? report.title : undefined,
35
- format: input.format === "pdf" || input.format === "html" ? input.format : undefined,
36
- });
37
- return card ? { type: "data-report-preview", ...card } : null;
38
- }
39
25
  if (block.name === "show_user") {
40
26
  // The live tool rejects a malformed/traversal echart `dataPath` and emits nothing; the
41
27
  // persisted tool_use still carries it, so the reload path MUST re-validate before building
@@ -38,7 +38,6 @@
38
38
  */
39
39
  import { unwrapOrThrow } from "../lib/result.js";
40
40
  import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
41
- import { REPORT_BUILDER_AGENT_ID, REPORT_BUILDER_SKILLS } from "../agents/report-builder.js";
42
41
  import { validateAgentSkills } from "../agents/sandbox/validate-skills.js";
43
42
  import { initCortexState } from "../state/init.js";
44
43
  import { backfillConversationDisplayEnvelopes } from "../memory/conversation-display-backfill.js";
@@ -49,16 +48,8 @@ import { markDraining } from "./lifecycle.js";
49
48
  import { runShutdownSequence } from "./shutdown.js";
50
49
  const noop = () => { };
51
50
  const noopAsync = () => Promise.resolve();
52
- /**
53
- * Every agent whose declared packs must resolve before the first run. The
54
- * report-builder is not in the sandbox catalog — it is not plannable — but it
55
- * declares a pack all the same, and a missing one there surfaces as a failed
56
- * render partway through a report rather than at boot.
57
- */
58
- const SKILL_DECLARING_AGENTS = {
59
- ...SANDBOX_AGENT_META,
60
- [REPORT_BUILDER_AGENT_ID]: { id: REPORT_BUILDER_AGENT_ID, skills: REPORT_BUILDER_SKILLS },
61
- };
51
+ /** Every agent whose declared packs must resolve before the first run. */
52
+ const SKILL_DECLARING_AGENTS = SANDBOX_AGENT_META;
62
53
  export async function bootHarness(deps) {
63
54
  const { core, pool, skillsDir, logger } = deps;
64
55
  (deps.initTelemetry ?? noop)();
@@ -73,7 +64,6 @@ export async function bootHarness(deps) {
73
64
  const conversationAgent = unwrapOrThrow(runtime.agents.forThread("conversation"));
74
65
  await backfillConversationDisplayEnvelopes({
75
66
  pool,
76
- resolveWorkspaceRoot: core.conversation.resolveWorkspaceRoot,
77
67
  tools: conversationAgent.tools,
78
68
  });
79
69
  await deps.beforeLaunch?.();
@@ -28,7 +28,8 @@
28
28
  * render problem, a figure that escapes the workspace root, a write failure, and a stamp failure. The tool
29
29
  * never throws for one of them. When the page
30
30
  * lands, the tool stamps the hash of the rendered draft on the session state, thus the eyes and the record
31
- * know which draft the page shows. The filesystem speaks the
31
+ * know which draft the page shows. It then emits one durable `data-report-rendered` part, thus the
32
+ * transcript places the render and a live client learns that a fresh page exists. The filesystem speaks the
32
33
  * throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
33
34
  * write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
34
35
  * control-flow exception propagate.
@@ -28,12 +28,14 @@
28
28
  * render problem, a figure that escapes the workspace root, a write failure, and a stamp failure. The tool
29
29
  * never throws for one of them. When the page
30
30
  * lands, the tool stamps the hash of the rendered draft on the session state, thus the eyes and the record
31
- * know which draft the page shows. The filesystem speaks the
31
+ * know which draft the page shows. It then emits one durable `data-report-rendered` part, thus the
32
+ * transcript places the render and a live client learns that a fresh page exists. The filesystem speaks the
32
33
  * throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
33
34
  * write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
34
35
  * control-flow exception propagate.
35
36
  */
36
37
  import { err, ok } from "neverthrow";
38
+ import { randomUUID } from "node:crypto";
37
39
  import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
38
40
  import { extname, join } from "node:path";
39
41
  import { relative as relativePosix } from "node:path/posix";
@@ -475,6 +477,12 @@ export function createPreviewReportTool(deps) {
475
477
  logger.warn("the rendered hash did not stamp", { threadId, analysisId, detail });
476
478
  return ok({ outcome: "stamp-failed", pagePath: written.value, detail });
477
479
  }
480
+ // The part rides the rendered arm only. Each degraded arm shows no
481
+ // fresh page, thus it emits nothing.
482
+ await ctx.emit({
483
+ type: "data-report-rendered",
484
+ data: { id: randomUUID(), renderedAt: new Date().toISOString(), title: document.title },
485
+ });
478
486
  const access = await mintAccess(deps.makeSessionPages, analysisId, threadId, ctx.session.auth, logger);
479
487
  return ok({ outcome: "rendered", pagePath: written.value, ...(access !== undefined ? { access } : {}) });
480
488
  },
@@ -173,8 +173,8 @@ export function createStartReportSessionTool(deps) {
173
173
  // names a session that an earlier turn announced, thus a second
174
174
  // part would put a second entry into the transcript.
175
175
  await ctx.emit({
176
- type: "data-report-session-started",
177
- data: { threadId: child.threadId, parentThreadId },
176
+ type: "data-child-session-started",
177
+ data: { threadId: child.threadId, parentThreadId, threadType: "report" },
178
178
  });
179
179
  return ok({ outcome: "started", threadId: child.threadId, title: child.title });
180
180
  }
@@ -159,27 +159,6 @@ export declare function tailWritePrefix(args: {
159
159
  * a step briefing names (see the harness-workspace-tools spec).
160
160
  */
161
161
  export declare function toSandboxPath(workspaceRoot: string, resourceId: string, hostAbsPath: string): string;
162
- /**
163
- * Parent of every preview, workspace-root-relative. Callers that enumerate
164
- * previews join this; callers that address one join {@link previewDir}. Both
165
- * spell the segment through this constant so the tree has one owner.
166
- */
167
- export declare const PREVIEWS_ROOT = "previews";
168
- /**
169
- * Versioned preview directory, workspace-root-relative. Previews live inside
170
- * the analysis tree (`previews/{previewId}/v{N}`); the content-token `res`
171
- * claim keeps the separate `previews/{analysisId}/{previewId}` formula in
172
- * `contracts/content-url.ts` — that is URL space, not a filesystem sub-path,
173
- * and hosts that serve previews map one onto the other themselves.
174
- */
175
- export declare function previewVersionDir(previewId: string, version: number): string;
176
- /** Preview root for a specific preview (all versions), workspace-root-relative. */
177
- export declare function previewDir(previewId: string): string;
178
- /**
179
- * Find the highest version number from a list of version directory names.
180
- * Expects names like "v1", "v2", etc. Returns 0 if no versions found.
181
- */
182
- export declare function latestPreviewVersion(dirNames: string[]): number;
183
162
  /**
184
163
  * Parent of every report session, workspace-root-relative. Callers that
185
164
  * enumerate the sessions join this. Callers that address one join
@@ -151,43 +151,6 @@ export function toSandboxPath(workspaceRoot, resourceId, hostAbsPath) {
151
151
  }
152
152
  return tail === "" ? `/${resourceId}` : `/${resourceId}/${tail}`;
153
153
  }
154
- /**
155
- * Parent of every preview, workspace-root-relative. Callers that enumerate
156
- * previews join this; callers that address one join {@link previewDir}. Both
157
- * spell the segment through this constant so the tree has one owner.
158
- */
159
- export const PREVIEWS_ROOT = "previews";
160
- /**
161
- * Versioned preview directory, workspace-root-relative. Previews live inside
162
- * the analysis tree (`previews/{previewId}/v{N}`); the content-token `res`
163
- * claim keeps the separate `previews/{analysisId}/{previewId}` formula in
164
- * `contracts/content-url.ts` — that is URL space, not a filesystem sub-path,
165
- * and hosts that serve previews map one onto the other themselves.
166
- */
167
- export function previewVersionDir(previewId, version) {
168
- return `${previewDir(previewId)}/v${version}`;
169
- }
170
- /** Preview root for a specific preview (all versions), workspace-root-relative. */
171
- export function previewDir(previewId) {
172
- assertSafeId(previewId, "previewId");
173
- return `${PREVIEWS_ROOT}/${previewId}`;
174
- }
175
- /**
176
- * Find the highest version number from a list of version directory names.
177
- * Expects names like "v1", "v2", etc. Returns 0 if no versions found.
178
- */
179
- export function latestPreviewVersion(dirNames) {
180
- let max = 0;
181
- for (const name of dirNames) {
182
- const match = name.match(/^v(\d+)$/);
183
- if (match) {
184
- const n = parseInt(match[1], 10);
185
- if (n > max)
186
- max = n;
187
- }
188
- }
189
- return max;
190
- }
191
154
  /**
192
155
  * Parent of every report session, workspace-root-relative. Callers that
193
156
  * enumerate the sessions join this. Callers that address one join
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -76,7 +76,6 @@
76
76
  "mime": "^4.1.0",
77
77
  "nanoid": "^5.1.16",
78
78
  "neverthrow": "^8.2.0",
79
- "nunjucks": "^3.2.4",
80
79
  "openai": "^7.4.0",
81
80
  "p-queue": "^9.3.3",
82
81
  "pdf-parse": "^2.4.5",
@@ -89,7 +88,6 @@
89
88
  "@types/bun": "latest",
90
89
  "@types/dockerode": "^3.3.47",
91
90
  "@types/node": "^24.13.3",
92
- "@types/nunjucks": "^3.2.6",
93
91
  "@types/pdf-parse": "^1.1.5",
94
92
  "@types/pg": "^8.20.0",
95
93
  "csstree-validator": "^4.0.1",
@@ -1,34 +0,0 @@
1
- /**
2
- * Report Builder Agent — in-process templating agent (no sandbox).
3
- *
4
- * Authors HTML reports via Jinja2 templating. Runs in the Cortex Node
5
- * process — file IO is in-process via the `versionFs` tool roster, rendering
6
- * is Node-side via Nunjucks (`build_report` tool), and visual validation
7
- * uses headless Chrome via the `preview_snapshot` tool.
8
- *
9
- * The agent receives a complete brief from iterate-report's pre-flight
10
- * (assets staged, columns parsed, sections fully composed) and has no
11
- * discovery surface — its workspace is rooted at the version directory.
12
- *
13
- * NOT a sandbox-agent catalog member (per design Decision #2 — not
14
- * plannable). The full tool roster is constructed inside the runner so
15
- * each tool shares the runner's closure-captured outcome state and
16
- * preview-dir paths; this factory just packages the result into a plain
17
- * `AgentDefinition`.
18
- */
19
- import type { AgentDefinition } from "../loop/types.js";
20
- import type { Tool } from "../tools/define-tool.js";
21
- export declare const REPORT_BUILDER_AGENT_ID = "report-builder";
22
- /** Skill pack the report-builder prompt directs the model to read (design-system reference). */
23
- export declare const REPORT_BUILDER_SKILLS: readonly ["report-html"];
24
- export interface CreateReportBuilderAgentDeps {
25
- /** Model id — provenance / metric label; the provider owns the wire model. */
26
- readonly model: string;
27
- /**
28
- * Full tool roster — the 4 custom report tools plus the in-process
29
- * versionFs surface, all constructed inside the runner so they close
30
- * over the iteration's outcome state and preview-dir paths.
31
- */
32
- readonly tools: readonly Tool[];
33
- }
34
- export declare function createReportBuilderAgent(deps: CreateReportBuilderAgentDeps): AgentDefinition;
@@ -1,34 +0,0 @@
1
- /**
2
- * Report Builder Agent — in-process templating agent (no sandbox).
3
- *
4
- * Authors HTML reports via Jinja2 templating. Runs in the Cortex Node
5
- * process — file IO is in-process via the `versionFs` tool roster, rendering
6
- * is Node-side via Nunjucks (`build_report` tool), and visual validation
7
- * uses headless Chrome via the `preview_snapshot` tool.
8
- *
9
- * The agent receives a complete brief from iterate-report's pre-flight
10
- * (assets staged, columns parsed, sections fully composed) and has no
11
- * discovery surface — its workspace is rooted at the version directory.
12
- *
13
- * NOT a sandbox-agent catalog member (per design Decision #2 — not
14
- * plannable). The full tool roster is constructed inside the runner so
15
- * each tool shares the runner's closure-captured outcome state and
16
- * preview-dir paths; this factory just packages the result into a plain
17
- * `AgentDefinition`.
18
- */
19
- import { reportBuilderPrompt } from "../prompts/report-builder.js";
20
- import { composeSystemPrompt } from "./system-prompt.js";
21
- export const REPORT_BUILDER_AGENT_ID = "report-builder";
22
- /** Skill pack the report-builder prompt directs the model to read (design-system reference). */
23
- export const REPORT_BUILDER_SKILLS = ["report-html"];
24
- /** Runaway guard — Jinja recovery may take several build/edit cycles. */
25
- const REPORT_BUILDER_MAX_ITERATIONS = 75;
26
- export function createReportBuilderAgent(deps) {
27
- return {
28
- id: REPORT_BUILDER_AGENT_ID,
29
- systemPrompt: composeSystemPrompt(reportBuilderPrompt),
30
- model: deps.model,
31
- tools: deps.tools,
32
- maxIterations: REPORT_BUILDER_MAX_ITERATIONS,
33
- };
34
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Report renderer — Node-side Nunjucks render of v{N}/report.html.j2 to v{N}/index.html.
3
- *
4
- * Stateless pure module: no harness imports, no env reads at import time,
5
- * safe to run standalone in tests.
6
- */
7
- export interface RenderReportOptions {
8
- versionDir: string;
9
- templatesDir: string;
10
- }
11
- export interface RenderReportResult {
12
- ok: boolean;
13
- indexBytes: number;
14
- error?: {
15
- kind: "missing-template" | "syntax" | "runtime";
16
- message: string;
17
- line?: number;
18
- snippet?: string;
19
- };
20
- }
21
- /**
22
- * Render `${versionDir}/report.html.j2` to `${versionDir}/index.html`.
23
- *
24
- * Loader resolution order: [versionDir, templatesDir]. `report.html.j2`
25
- * lives in versionDir; `extends "base.html.j2"` and `include "components/..."`
26
- * resolve from templatesDir. Autoescape is OFF — templates own escaping.
27
- *
28
- * `${templatesDir}/echarts-theme.json` is read, parsed, re-stringified, and
29
- * passed as `echarts_theme` (a JSON string) for safe inlining inside a
30
- * `<script>` block.
31
- *
32
- * Returns `{ ok: true, indexBytes }` on success. On failure, returns
33
- * `{ ok: false, indexBytes: 0, error }` with `error.kind`:
34
- * - "missing-template" — `report.html.j2` (or any `extends`/`include` target)
35
- * not found by either loader. Message identifies the missing path.
36
- * - "syntax" — Nunjucks parse error in any loaded template.
37
- * - "runtime" — anything else thrown during render or write.
38
- */
39
- export declare function renderReport(opts: RenderReportOptions): Promise<RenderReportResult>;