@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
@@ -1,98 +0,0 @@
1
- /**
2
- * preview_snapshot tool — navigate headless Chrome to the rendered report,
3
- * wait for the `inflexa-theme-ready` event, capture a screenshot + console
4
- * messages + failed network requests in a single deterministic call.
5
- *
6
- * Lazily mints a preview URL via the `PreviewPublisher` seam on first use,
7
- * sharing the cached URL with the `mint_preview_url` tool via a
8
- * closure-captured `PreviewUrlCell`.
9
- *
10
- * Returned screenshot is base64-encoded PNG bytes; the agent can inspect
11
- * console errors / failed requests to decide whether to fix the template
12
- * or move on to submit_report.
13
- *
14
- * A seam that cannot mint is also reported through the injected `Logger`:
15
- * this tool is the build's only visual verification, so an unavailable seam
16
- * means that verification silently did not happen — a condition an operator
17
- * has to be able to see without reading the model transcript.
18
- */
19
- import { ok } from "neverthrow";
20
- import { z } from "zod";
21
- import { defineTool } from "../define-tool.js";
22
- import { capturePage } from "../../lib/page-capture.js";
23
- import { createNoopLogger } from "../../lib/console-logger.js";
24
- import { describeMintFailure } from "./preview-publisher.js";
25
- const WAIT_MS_CAP = 30_000;
26
- export function createPreviewSnapshotTool(state) {
27
- const logger = (state.logger ?? createNoopLogger()).named("preview-snapshot").with({ previewId: state.previewId });
28
- return defineTool({
29
- id: "preview_snapshot",
30
- description: "Render the report in a real headless browser and report what you " +
31
- "see. Returns a screenshot, console errors, and failed network " +
32
- "requests. Use this after build_report to verify the layout, charts, " +
33
- "and data loads correctly.",
34
- inputSchema: z.object({
35
- waitForSelector: z
36
- .string()
37
- .optional()
38
- .describe("Optional CSS selector to wait for after the page is loaded — useful " + "when a specific chart needs to be visible before the screenshot."),
39
- waitMs: z
40
- .number()
41
- .finite()
42
- .int()
43
- .min(0)
44
- .max(WAIT_MS_CAP)
45
- .optional()
46
- .describe("Optional extra wait in ms after the theme-ready event (e.g., for chart paint). Capped at 30s."),
47
- }),
48
- describeCall: "none",
49
- execute: async (input) => {
50
- let url = state.urlCell.url;
51
- const expiresAt = state.urlCell.expiresAt;
52
- if (url && expiresAt && new Date(expiresAt).getTime() < Date.now() + 60_000) {
53
- url = undefined; // refresh if within 60s of expiry
54
- }
55
- if (!url) {
56
- const result = await state.previews.mintPreviewAccess(state.resourceId, state.previewId);
57
- if (!result.ok) {
58
- const reason = result.error.message?.trim();
59
- logger.warn("preview access unavailable — visual verification did not run", {
60
- version: state.currentVersion,
61
- ...(result.status !== undefined ? { status: result.status } : {}),
62
- ...(reason ? { reason } : {}),
63
- });
64
- return ok({
65
- ok: false,
66
- consoleErrors: [],
67
- failedRequests: [],
68
- error: describeMintFailure(result),
69
- });
70
- }
71
- url = `${result.data.baseUrl.replace(/\/?$/, "/")}v${state.currentVersion}/index.html?t=${result.data.token}`;
72
- state.urlCell.url = url;
73
- state.urlCell.expiresAt = result.data.expiresAt;
74
- }
75
- try {
76
- const captured = await capturePage(state.chrome, url, {
77
- ...(input.waitForSelector !== undefined ? { waitForSelector: input.waitForSelector } : {}),
78
- ...(input.waitMs !== undefined ? { waitMs: input.waitMs } : {}),
79
- });
80
- return ok({
81
- ok: true,
82
- screenshotBase64: captured.screenshotBase64,
83
- coverage: captured.coverage,
84
- consoleErrors: captured.consoleErrors,
85
- failedRequests: captured.failedRequests,
86
- });
87
- }
88
- catch (err) {
89
- return ok({
90
- ok: false,
91
- consoleErrors: [],
92
- failedRequests: [],
93
- error: err instanceof Error ? err.message : String(err),
94
- });
95
- }
96
- },
97
- });
98
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * submit_report tool — postcondition gate. The runner refuses to emit
3
- * `data-report-preview` until this tool writes the success outcome.
4
- *
5
- * Checks:
6
- * - report.html.j2 exists at versionDir/ — the report itself
7
- * - index.html exists at versionDir/ with size > 0
8
- * - no unrendered `{{ … }}` or `{% … %}` left in the output
9
- * - all referenced asset paths resolve to files on disk
10
- *
11
- * On success, writes `{ ok: true, notes }` into the runner's closure-captured
12
- * outcome variable.
13
- */
14
- import { type Tool } from "../define-tool.js";
15
- /** Terminal outcome the submit-report tool writes into the runner closure. */
16
- export type ReportOutcome = {
17
- ok: true;
18
- notes: readonly string[];
19
- };
20
- export interface SubmitReportToolState {
21
- readonly versionDir: string;
22
- readonly assetsDir: string;
23
- /** Write-through for the runner's closure-captured outcome variable. */
24
- setOutcome(outcome: ReportOutcome): void;
25
- }
26
- export declare function createSubmitReportTool(state: SubmitReportToolState): Tool;
@@ -1,148 +0,0 @@
1
- /**
2
- * submit_report tool — postcondition gate. The runner refuses to emit
3
- * `data-report-preview` until this tool writes the success outcome.
4
- *
5
- * Checks:
6
- * - report.html.j2 exists at versionDir/ — the report itself
7
- * - index.html exists at versionDir/ with size > 0
8
- * - no unrendered `{{ … }}` or `{% … %}` left in the output
9
- * - all referenced asset paths resolve to files on disk
10
- *
11
- * On success, writes `{ ok: true, notes }` into the runner's closure-captured
12
- * outcome variable.
13
- */
14
- import { readFile, stat } from "node:fs/promises";
15
- import { join, resolve, sep } from "node:path";
16
- import { ok } from "neverthrow";
17
- import { z } from "zod";
18
- import { defineTool } from "../define-tool.js";
19
- // `[^{}]*` accepts any expression Nunjucks allows between delimiters
20
- // (filters `{{ x | tojson }}`, inline conditionals `{{ a if b else c }}`,
21
- // bracket access `{{ d['k'] }}`) but stops at nested braces — which aren't
22
- // valid Nunjucks anyway. The `{%-?` arm covers whitespace-trimming tags.
23
- const UNRENDERED_JINJA = /\{\{[^{}]*\}\}|\{%-?\s*\w/;
24
- export function createSubmitReportTool(state) {
25
- return defineTool({
26
- id: "submit_report",
27
- description: "Declare the report ready. Validates report.html.j2 and index.html " +
28
- "exist, that the output has no unrendered Jinja, and that all " +
29
- "referenced asset paths resolve. " +
30
- "You MUST call this to finish — the runner ignores any other claim " +
31
- "of success. Returns problems[] when checks fail; fix and re-submit. " +
32
- "Use `notes` to surface caveats to the caller (e.g. sections rendered " +
33
- "with partial data, missing inputs that forced a fallback) — these " +
34
- "reach the conversation agent and the user in the iteration result.",
35
- inputSchema: z.object({
36
- notes: z
37
- .array(z.string())
38
- .optional()
39
- .describe("Caveats / issues to surface to the caller. One short sentence " +
40
- "per item. Examples: 'pathway-enrichment section omitted — no " +
41
- "GSEA output found', 'cohort table rendered from 8 of 12 " +
42
- "samples — the rest had no QC row'. " +
43
- "Leave empty when the report shipped cleanly."),
44
- }),
45
- describeCall: "none",
46
- execute: async (input) => {
47
- const problems = [];
48
- // 1. report.html.j2 exists — the report itself, as opposed to the
49
- // artifact rendered from it. Nothing downstream re-derives this: a
50
- // version that is finalized without a template persists as one, and
51
- // the next iteration copies a template that isn't there, then tells
52
- // the builder to edit it in place. Checked first because an absent
53
- // template is the cause the checks below can only report symptoms of.
54
- const templatePath = join(state.versionDir, "report.html.j2");
55
- let hasTemplate;
56
- try {
57
- hasTemplate = (await stat(templatePath)).isFile();
58
- }
59
- catch {
60
- hasTemplate = false;
61
- }
62
- if (!hasTemplate) {
63
- problems.push("report.html.j2 is missing — a report is authored as report.html.j2 and " +
64
- "rendered into index.html by build_report; a hand-written index.html is " +
65
- "not a report. Write the template, then call build_report.");
66
- }
67
- // 2. index.html exists and is non-empty
68
- const indexPath = join(state.versionDir, "index.html");
69
- let html;
70
- try {
71
- const s = await stat(indexPath);
72
- if (!s.isFile() || s.size === 0) {
73
- problems.push("index.html is empty or not a regular file");
74
- return ok({ ok: false, problems });
75
- }
76
- html = await readFile(indexPath, "utf8");
77
- }
78
- catch {
79
- problems.push("index.html does not exist — call build_report first");
80
- return ok({ ok: false, problems });
81
- }
82
- // 3. No unrendered Jinja markers
83
- if (UNRENDERED_JINJA.test(html)) {
84
- const match = html.match(UNRENDERED_JINJA);
85
- problems.push(`unrendered Jinja in output (e.g. ${JSON.stringify(match?.[0] ?? "")}); ` +
86
- "build_report likely failed silently or report.html.j2 was edited after build");
87
- }
88
- // 4. Asset reference resolution
89
- const refs = extractLocalRefs(html);
90
- for (const ref of refs) {
91
- const resolved = resolveLocalRef(ref, state.versionDir, state.assetsDir);
92
- if (!resolved) {
93
- problems.push(`asset reference does not resolve: ${ref}`);
94
- continue;
95
- }
96
- try {
97
- const s = await stat(resolved);
98
- if (!s.isFile())
99
- problems.push(`asset reference is not a regular file: ${ref}`);
100
- }
101
- catch {
102
- problems.push(`asset reference missing on disk: ${ref}`);
103
- }
104
- }
105
- if (problems.length === 0) {
106
- const notes = (input.notes ?? []).map((n) => n.trim()).filter(Boolean);
107
- state.setOutcome({ ok: true, notes });
108
- return ok({ ok: true, problems: [] });
109
- }
110
- return ok({ ok: false, problems });
111
- },
112
- });
113
- }
114
- const SRC_HREF_RE = /\b(?:src|href)\s*=\s*["']([^"']+)["']/gi;
115
- const FETCH_RE = /\bfetch\s*\(\s*["'`]([^"'`]+)["'`]/gi;
116
- function extractLocalRefs(html) {
117
- const refs = new Set();
118
- for (const m of html.matchAll(SRC_HREF_RE))
119
- refs.add(m[1]);
120
- for (const m of html.matchAll(FETCH_RE))
121
- refs.add(m[1]);
122
- // Filter to local refs only — drop absolute URLs and data: / blob: schemes.
123
- const out = [];
124
- for (const ref of refs) {
125
- if (/^[a-z][a-z0-9+.-]*:/i.test(ref))
126
- continue; // http:, https:, data:, blob:
127
- if (ref.startsWith("//"))
128
- continue; // protocol-relative
129
- if (ref.startsWith("#"))
130
- continue; // anchor
131
- out.push(ref);
132
- }
133
- return out;
134
- }
135
- function resolveLocalRef(ref, versionDir, assetsDir) {
136
- // Strip leading slash, query string, and fragment.
137
- const cleaned = ref.split("?")[0].split("#")[0].replace(/^\/+/, "");
138
- if (!cleaned)
139
- return undefined;
140
- const candidate = resolve(versionDir, cleaned);
141
- // The candidate must stay under either versionDir or assetsDir (the
142
- // shared assets dir is the symlink target — both are valid roots).
143
- const inVersion = candidate.startsWith(versionDir + sep) || candidate === versionDir;
144
- const inAssets = candidate.startsWith(assetsDir + sep) || candidate === assetsDir;
145
- if (!inVersion && !inAssets)
146
- return undefined;
147
- return candidate;
148
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * In-process workspace surface for the report-builder agent.
3
- *
4
- * Tight read/write/mkdir surface scoped to the version directory.
5
- * Agent-supplied paths are relative to `versionDir`; an absolute path, or one
6
- * that escapes the directory, is rejected as `out_of_scope`. Constructed
7
- * inside the iterate-report runner so it always shares the same `versionDir`
8
- * as the rest of the report tools.
9
- */
10
- import { type Tool } from "../define-tool.js";
11
- export interface VersionFsToolsState {
12
- /** Absolute path to the version directory. All paths resolve under this. */
13
- readonly versionDir: string;
14
- }
15
- export declare function createVersionFsTools(state: VersionFsToolsState): Tool[];
@@ -1,165 +0,0 @@
1
- /**
2
- * In-process workspace surface for the report-builder agent.
3
- *
4
- * Tight read/write/mkdir surface scoped to the version directory.
5
- * Agent-supplied paths are relative to `versionDir`; an absolute path, or one
6
- * that escapes the directory, is rejected as `out_of_scope`. Constructed
7
- * inside the iterate-report runner so it always shares the same `versionDir`
8
- * as the rest of the report tools.
9
- */
10
- import { mkdir as fsMkdir, readFile as fsReadFile, stat as fsStat, writeFile as fsWriteFile } from "node:fs/promises";
11
- import { dirname, resolve as resolvePath, sep } from "node:path";
12
- import { ok } from "neverthrow";
13
- import { z } from "zod";
14
- import { defineTool } from "../define-tool.js";
15
- const MAX_READ_BYTES = 256 * 1024;
16
- const MAX_WRITE_BYTES = 1 * 1024 * 1024;
17
- /** Resolve an agent-supplied path against `versionDir`, rejecting escapes. */
18
- function resolveAgentPath(agentPath, versionDir) {
19
- if (agentPath.length === 0 || agentPath.includes("\0")) {
20
- return { kind: "out_of_scope", reason: "empty or null-byte path" };
21
- }
22
- // A leading slash is refused rather than trimmed. Trimming it makes the
23
- // containment check below unreachable for absolute paths: a deep one such
24
- // as "/previews/abc/v1/report.html.j2" would land a real file several
25
- // directories below versionDir and report success, and the builder — which
26
- // has no directory-listing tool — cannot find where its template went.
27
- if (agentPath.startsWith("/")) {
28
- return { kind: "out_of_scope", reason: `absolute path rejected, use one relative to the version dir: ${agentPath}` };
29
- }
30
- const absolute = resolvePath(versionDir, agentPath);
31
- if (absolute !== versionDir && !absolute.startsWith(versionDir + sep)) {
32
- return { kind: "out_of_scope", reason: `path escapes version dir: ${agentPath}` };
33
- }
34
- return { kind: "ok", absolute };
35
- }
36
- export function createVersionFsTools(state) {
37
- const writeFile = defineTool({
38
- id: "write_file",
39
- description: "Write a UTF-8 text file under the report's version directory. Paths " +
40
- "are relative to it (e.g. `report.html.j2`, `assets/foo.csv`); an " +
41
- "absolute path, or one that escapes the version dir, returns " +
42
- "`out_of_scope` and writes nothing.",
43
- inputSchema: z.object({
44
- path: z.string().min(1),
45
- content: z.string(),
46
- }),
47
- describeCall: "none",
48
- execute: async ({ path, content }) => {
49
- const resolved = resolveAgentPath(path, state.versionDir);
50
- if (resolved.kind === "out_of_scope") {
51
- return ok({ status: "out_of_scope", reason: resolved.reason });
52
- }
53
- const bytes = Buffer.byteLength(content, "utf8");
54
- if (bytes > MAX_WRITE_BYTES) {
55
- return ok({
56
- status: "too_large",
57
- reason: `write rejected: ${bytes} bytes exceeds ${MAX_WRITE_BYTES} cap`,
58
- });
59
- }
60
- await fsMkdir(dirname(resolved.absolute), { recursive: true });
61
- await fsWriteFile(resolved.absolute, content, "utf8");
62
- return ok({ status: "ok", bytesWritten: bytes });
63
- },
64
- });
65
- const editFile = defineTool({
66
- id: "edit_file",
67
- description: "Surgical edit of an existing UTF-8 file inside the report's version " +
68
- "directory: replace the first occurrence of `oldText` with `newText`. " +
69
- "Paths are relative to that directory; an absolute or escaping path " +
70
- "returns `out_of_scope`. Returns `not_found` if the file doesn't " +
71
- "exist, `no_match` if `oldText` isn't present.",
72
- inputSchema: z.object({
73
- path: z.string().min(1),
74
- oldText: z.string().min(1),
75
- newText: z.string(),
76
- }),
77
- describeCall: "none",
78
- execute: async ({ path, oldText, newText }) => {
79
- const resolved = resolveAgentPath(path, state.versionDir);
80
- if (resolved.kind === "out_of_scope") {
81
- return ok({ status: "out_of_scope", reason: resolved.reason });
82
- }
83
- let content;
84
- try {
85
- content = await fsReadFile(resolved.absolute, "utf8");
86
- }
87
- catch (err) {
88
- if (err.code === "ENOENT") {
89
- return ok({ status: "not_found" });
90
- }
91
- throw err;
92
- }
93
- const idx = content.indexOf(oldText);
94
- if (idx === -1) {
95
- return ok({ status: "no_match" });
96
- }
97
- const next = content.slice(0, idx) + newText + content.slice(idx + oldText.length);
98
- const bytes = Buffer.byteLength(next, "utf8");
99
- if (bytes > MAX_WRITE_BYTES) {
100
- return ok({
101
- status: "too_large",
102
- reason: `post-edit size ${bytes} bytes exceeds ${MAX_WRITE_BYTES} cap`,
103
- });
104
- }
105
- await fsWriteFile(resolved.absolute, next, "utf8");
106
- return ok({ status: "ok", bytesWritten: bytes });
107
- },
108
- });
109
- const readFile = defineTool({
110
- id: "read_file",
111
- description: "Read a UTF-8 file inside the report's version directory. Paths are " +
112
- "relative to it; an absolute or escaping path returns `out_of_scope`. " +
113
- "Returns truncated content (capped at 256 KiB) for large files.",
114
- inputSchema: z.object({
115
- path: z.string().min(1),
116
- }),
117
- describeCall: "none",
118
- execute: async ({ path }) => {
119
- const resolved = resolveAgentPath(path, state.versionDir);
120
- if (resolved.kind === "out_of_scope") {
121
- return ok({ status: "out_of_scope", reason: resolved.reason });
122
- }
123
- let st;
124
- try {
125
- st = await fsStat(resolved.absolute);
126
- }
127
- catch (err) {
128
- if (err.code === "ENOENT") {
129
- return ok({ status: "not_found" });
130
- }
131
- throw err;
132
- }
133
- if (!st.isFile()) {
134
- return ok({ status: "not_found" });
135
- }
136
- const buf = await fsReadFile(resolved.absolute);
137
- const truncated = buf.length > MAX_READ_BYTES;
138
- const sliced = truncated ? buf.subarray(0, MAX_READ_BYTES) : buf;
139
- return ok({
140
- status: "ok",
141
- content: sliced.toString("utf8"),
142
- truncated,
143
- totalBytes: st.size,
144
- });
145
- },
146
- });
147
- const mkdir = defineTool({
148
- id: "mkdir",
149
- description: "Create a directory (recursive) inside the report's version directory. " +
150
- "Paths are relative to it; an absolute or escaping path returns `out_of_scope`.",
151
- inputSchema: z.object({
152
- path: z.string().min(1),
153
- }),
154
- describeCall: "none",
155
- execute: async ({ path }) => {
156
- const resolved = resolveAgentPath(path, state.versionDir);
157
- if (resolved.kind === "out_of_scope") {
158
- return ok({ status: "out_of_scope", reason: resolved.reason });
159
- }
160
- await fsMkdir(resolved.absolute, { recursive: true });
161
- return ok({ status: "ok" });
162
- },
163
- });
164
- return [writeFile, editFile, readFile, mkdir];
165
- }