@inflexa-ai/harness 0.8.0 → 0.9.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 (32) hide show
  1. package/dist/contracts/message.d.ts +6 -0
  2. package/dist/execution/report-runner.d.ts +6 -1
  3. package/dist/execution/report-runner.js +1 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/loop/run-agent.d.ts +1 -1
  6. package/dist/loop/run-agent.js +55 -2
  7. package/dist/memory/ai-sdk-message-storage.d.ts +42 -0
  8. package/dist/memory/ai-sdk-message-storage.js +62 -0
  9. package/dist/memory/content-to-cortex.d.ts +8 -1
  10. package/dist/memory/content-to-cortex.js +36 -4
  11. package/dist/memory/thread-history.d.ts +37 -0
  12. package/dist/memory/thread-history.js +74 -7
  13. package/dist/prompts/conversation.d.ts +1 -1
  14. package/dist/prompts/conversation.js +3 -3
  15. package/dist/prompts/report-builder.d.ts +1 -1
  16. package/dist/prompts/report-builder.js +1 -1
  17. package/dist/providers/streaming-chat.d.ts +11 -2
  18. package/dist/providers/streaming-chat.js +31 -6
  19. package/dist/providers/types.d.ts +10 -1
  20. package/dist/tools/iterate-report.d.ts +0 -1
  21. package/dist/tools/iterate-report.js +125 -11
  22. package/dist/tools/report/mint-preview-url.d.ts +1 -1
  23. package/dist/tools/report/mint-preview-url.js +7 -5
  24. package/dist/tools/report/preview-publisher.d.ts +16 -0
  25. package/dist/tools/report/preview-publisher.js +20 -0
  26. package/dist/tools/report/preview-snapshot.d.ts +9 -1
  27. package/dist/tools/report/preview-snapshot.js +15 -1
  28. package/dist/tools/report/submit-report.d.ts +1 -0
  29. package/dist/tools/report/submit-report.js +32 -11
  30. package/dist/tools/report/version-fs.d.ts +4 -4
  31. package/dist/tools/report/version-fs.js +24 -17
  32. package/package.json +1 -1
@@ -3,6 +3,7 @@
3
3
  * `data-report-preview` until this tool writes the success outcome.
4
4
  *
5
5
  * Checks:
6
+ * - report.html.j2 exists at versionDir/ — the report itself
6
7
  * - index.html exists at versionDir/ with size > 0
7
8
  * - no unrendered `{{ … }}` or `{% … %}` left in the output
8
9
  * - all referenced asset paths resolve to files on disk
@@ -23,26 +24,46 @@ const UNRENDERED_JINJA = /\{\{[^{}]*\}\}|\{%-?\s*\w/;
23
24
  export function createSubmitReportTool(state) {
24
25
  return defineTool({
25
26
  id: "submit_report",
26
- description: "Declare the report ready. Validates index.html exists, has no " +
27
- "unrendered Jinja, and that all referenced asset paths resolve. " +
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. " +
28
30
  "You MUST call this to finish — the runner ignores any other claim " +
29
31
  "of success. Returns problems[] when checks fail; fix and re-submit. " +
30
- "Use `notes` to surface caveats to the caller (e.g. files skipped due " +
31
- "to the 50MB asset size limit, sections rendered with partial data, " +
32
- "missing inputs that forced a fallback) — these reach the conversation " +
33
- "agent and the user in the iteration result.",
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.",
34
35
  inputSchema: z.object({
35
36
  notes: z
36
37
  .array(z.string())
37
38
  .optional()
38
39
  .describe("Caveats / issues to surface to the caller. One short sentence " +
39
- "per item. Examples: 'skipped raw_counts.h5ad (1.2GB > 50MB cap)', " +
40
- "'pathway-enrichment section omitted — no GSEA output found'. " +
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'. " +
41
43
  "Leave empty when the report shipped cleanly."),
42
44
  }),
43
45
  execute: async (input) => {
44
46
  const problems = [];
45
- // 1. index.html exists and is non-empty
47
+ // 1. report.html.j2 exists — the report itself, as opposed to the
48
+ // artifact rendered from it. Nothing downstream re-derives this: a
49
+ // version that is finalized without a template persists as one, and
50
+ // the next iteration copies a template that isn't there, then tells
51
+ // the builder to edit it in place. Checked first because an absent
52
+ // template is the cause the checks below can only report symptoms of.
53
+ const templatePath = join(state.versionDir, "report.html.j2");
54
+ let hasTemplate;
55
+ try {
56
+ hasTemplate = (await stat(templatePath)).isFile();
57
+ }
58
+ catch {
59
+ hasTemplate = false;
60
+ }
61
+ if (!hasTemplate) {
62
+ problems.push("report.html.j2 is missing — a report is authored as report.html.j2 and " +
63
+ "rendered into index.html by build_report; a hand-written index.html is " +
64
+ "not a report. Write the template, then call build_report.");
65
+ }
66
+ // 2. index.html exists and is non-empty
46
67
  const indexPath = join(state.versionDir, "index.html");
47
68
  let html;
48
69
  try {
@@ -57,13 +78,13 @@ export function createSubmitReportTool(state) {
57
78
  problems.push("index.html does not exist — call build_report first");
58
79
  return ok({ ok: false, problems });
59
80
  }
60
- // 2. No unrendered Jinja markers
81
+ // 3. No unrendered Jinja markers
61
82
  if (UNRENDERED_JINJA.test(html)) {
62
83
  const match = html.match(UNRENDERED_JINJA);
63
84
  problems.push(`unrendered Jinja in output (e.g. ${JSON.stringify(match?.[0] ?? "")}); ` +
64
85
  "build_report likely failed silently or report.html.j2 was edited after build");
65
86
  }
66
- // 3. Asset reference resolution
87
+ // 4. Asset reference resolution
67
88
  const refs = extractLocalRefs(html);
68
89
  for (const ref of refs) {
69
90
  const resolved = resolveLocalRef(ref, state.versionDir, state.assetsDir);
@@ -2,10 +2,10 @@
2
2
  * In-process workspace surface for the report-builder agent.
3
3
  *
4
4
  * Tight read/write/mkdir surface scoped to the version directory.
5
- * Agent-supplied paths are resolved against `versionDir`; any path that
6
- * escapes is rejected as `out_of_scope`. Constructed inside the
7
- * iterate-report runner so it always shares the same `versionDir` as the
8
- * rest of the report tools.
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
9
  */
10
10
  import { type Tool } from "../define-tool.js";
11
11
  export interface VersionFsToolsState {
@@ -2,10 +2,10 @@
2
2
  * In-process workspace surface for the report-builder agent.
3
3
  *
4
4
  * Tight read/write/mkdir surface scoped to the version directory.
5
- * Agent-supplied paths are resolved against `versionDir`; any path that
6
- * escapes is rejected as `out_of_scope`. Constructed inside the
7
- * iterate-report runner so it always shares the same `versionDir` as the
8
- * rest of the report tools.
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
9
  */
10
10
  import { mkdir as fsMkdir, readFile as fsReadFile, stat as fsStat, writeFile as fsWriteFile } from "node:fs/promises";
11
11
  import { dirname, resolve as resolvePath, sep } from "node:path";
@@ -19,13 +19,15 @@ function resolveAgentPath(agentPath, versionDir) {
19
19
  if (agentPath.length === 0 || agentPath.includes("\0")) {
20
20
  return { kind: "out_of_scope", reason: "empty or null-byte path" };
21
21
  }
22
- // Strip a leading slash so an agent typing "/index.html" is treated as
23
- // "index.html" relative to versionDir (the version dir is the root).
24
- const stripped = agentPath.replace(/^\/+/, "");
25
- if (!stripped) {
26
- return { kind: "out_of_scope", reason: "path resolves to the version dir itself" };
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}` };
27
29
  }
28
- const absolute = resolvePath(versionDir, stripped);
30
+ const absolute = resolvePath(versionDir, agentPath);
29
31
  if (absolute !== versionDir && !absolute.startsWith(versionDir + sep)) {
30
32
  return { kind: "out_of_scope", reason: `path escapes version dir: ${agentPath}` };
31
33
  }
@@ -34,9 +36,10 @@ function resolveAgentPath(agentPath, versionDir) {
34
36
  export function createVersionFsTools(state) {
35
37
  const writeFile = defineTool({
36
38
  id: "write_file",
37
- description: "Write a UTF-8 text file under the report's version directory. Use " +
38
- "relative paths (e.g. `report.html.j2`, `assets/foo.csv`). Returns " +
39
- "`out_of_scope` for any path that escapes the version dir.",
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.",
40
43
  inputSchema: z.object({
41
44
  path: z.string().min(1),
42
45
  content: z.string(),
@@ -62,8 +65,9 @@ export function createVersionFsTools(state) {
62
65
  id: "edit_file",
63
66
  description: "Surgical edit of an existing UTF-8 file inside the report's version " +
64
67
  "directory: replace the first occurrence of `oldText` with `newText`. " +
65
- "Returns `not_found` if the file doesn't exist, `no_match` if " +
66
- "`oldText` isn't present.",
68
+ "Paths are relative to that directory; an absolute or escaping path " +
69
+ "returns `out_of_scope`. Returns `not_found` if the file doesn't " +
70
+ "exist, `no_match` if `oldText` isn't present.",
67
71
  inputSchema: z.object({
68
72
  path: z.string().min(1),
69
73
  oldText: z.string().min(1),
@@ -102,7 +106,9 @@ export function createVersionFsTools(state) {
102
106
  });
103
107
  const readFile = defineTool({
104
108
  id: "read_file",
105
- description: "Read a UTF-8 file inside the report's version directory. Returns " + "truncated content (capped at 256 KiB) for large files.",
109
+ description: "Read a UTF-8 file inside the report's version directory. Paths are " +
110
+ "relative to it; an absolute or escaping path returns `out_of_scope`. " +
111
+ "Returns truncated content (capped at 256 KiB) for large files.",
106
112
  inputSchema: z.object({
107
113
  path: z.string().min(1),
108
114
  }),
@@ -137,7 +143,8 @@ export function createVersionFsTools(state) {
137
143
  });
138
144
  const mkdir = defineTool({
139
145
  id: "mkdir",
140
- description: "Create a directory (recursive) inside the report's version directory.",
146
+ description: "Create a directory (recursive) inside the report's version directory. " +
147
+ "Paths are relative to it; an absolute or escaping path returns `out_of_scope`.",
141
148
  inputSchema: z.object({
142
149
  path: z.string().min(1),
143
150
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness \u2014 hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",