@inflexa-ai/harness 0.19.0 → 0.19.1

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 (64) hide show
  1. package/dist/agents/report-session-agent.d.ts +25 -9
  2. package/dist/agents/report-session-agent.js +21 -6
  3. package/dist/app/report-session-runtime.js +26 -0
  4. package/dist/app/spawn-report-session.d.ts +32 -5
  5. package/dist/app/spawn-report-session.js +19 -0
  6. package/dist/config/environment-stores.d.ts +12 -7
  7. package/dist/config/resource-limits.d.ts +6 -10
  8. package/dist/config/resource-limits.js +6 -44
  9. package/dist/contracts/report-reference.d.ts +4 -0
  10. package/dist/contracts/report-reference.js +3 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/lib/chrome.d.ts +14 -0
  13. package/dist/lib/chrome.js +7 -0
  14. package/dist/lib/page-capture.d.ts +47 -0
  15. package/dist/lib/page-capture.js +83 -0
  16. package/dist/loop/run-agent.js +104 -15
  17. package/dist/memory/ai-sdk-message-storage.d.ts +9 -4
  18. package/dist/memory/ai-sdk-message-storage.js +9 -4
  19. package/dist/memory/count-tokens.js +34 -5
  20. package/dist/prompts/report-session.d.ts +1 -1
  21. package/dist/prompts/report-session.js +21 -0
  22. package/dist/providers/ai-sdk.d.ts +81 -3
  23. package/dist/providers/ai-sdk.js +91 -10
  24. package/dist/providers/types.d.ts +17 -0
  25. package/dist/report-model/assert-rules.d.ts +44 -0
  26. package/dist/report-model/assert-rules.js +97 -0
  27. package/dist/report-model/draft-hash.d.ts +14 -0
  28. package/dist/report-model/draft-hash.js +36 -0
  29. package/dist/report-model/fixture-resolver.js +2 -71
  30. package/dist/report-model/production-resolver.d.ts +124 -0
  31. package/dist/report-model/production-resolver.js +801 -0
  32. package/dist/report-model/reference-resolver.d.ts +6 -0
  33. package/dist/report-model/validate.js +5 -0
  34. package/dist/report-render/page.d.ts +23 -0
  35. package/dist/report-render/page.js +36 -3
  36. package/dist/runtime/assemble.d.ts +28 -4
  37. package/dist/runtime/assemble.js +42 -3
  38. package/dist/sandbox/mount-plan.d.ts +7 -0
  39. package/dist/sandbox/mount-plan.js +7 -2
  40. package/dist/state/init.js +20 -7
  41. package/dist/state/report-session-state.d.ts +27 -0
  42. package/dist/state/report-session-state.js +29 -2
  43. package/dist/tasks/extract-values-script.d.ts +64 -0
  44. package/dist/tasks/extract-values-script.js +180 -0
  45. package/dist/tasks/extract-values.d.ts +124 -0
  46. package/dist/tasks/extract-values.js +193 -0
  47. package/dist/tools/define-tool.d.ts +29 -0
  48. package/dist/tools/define-tool.js +26 -0
  49. package/dist/tools/report/preview-snapshot.d.ts +1 -1
  50. package/dist/tools/report/preview-snapshot.js +11 -50
  51. package/dist/tools/report-authoring/authoring-tools.d.ts +54 -4
  52. package/dist/tools/report-authoring/authoring-tools.js +1 -1
  53. package/dist/tools/report-session/examine-page.d.ts +90 -0
  54. package/dist/tools/report-session/examine-page.js +136 -0
  55. package/dist/tools/report-session/index.d.ts +2 -0
  56. package/dist/tools/report-session/index.js +2 -0
  57. package/dist/tools/report-session/preview-report.d.ts +20 -5
  58. package/dist/tools/report-session/preview-report.js +28 -4
  59. package/dist/tools/report-session/record-version.d.ts +102 -0
  60. package/dist/tools/report-session/record-version.js +139 -0
  61. package/dist/tools/sandbox/list-available-packages.js +2 -1
  62. package/dist/tools/sandbox/list-available-refs.d.ts +5 -1
  63. package/dist/tools/sandbox/list-available-refs.js +13 -6
  64. package/package.json +10 -8
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The record tool of a report session.
3
+ *
4
+ * The tool records one version of the report. The store is append-only with no delete, thus a version that
5
+ * the gate did not accept must never exist. The tool runs the whole gate first, and only a pass reaches the
6
+ * store.
7
+ *
8
+ * The gate has three parts, in order of cost. The finish gates the schema, the unique ids, and the
9
+ * structural tier, and a gap list returns as data. The look-before-record rule then compares the seen hash
10
+ * against the hash of the current draft, on the state that the load already read. A never-seen page and a
11
+ * stale look each refuse with a distinct reason, and the rule reads no model judgment. The full validation
12
+ * resolves every reference through the injected resolver, matches each chart encoding, and matches each
13
+ * assert. A failure names the block that holds the failed part, thus the agent repairs one block and not the
14
+ * report at large.
15
+ *
16
+ * The look runs before the validation on purpose. The validation builds the resolver and its prepare pass
17
+ * can start a container, thus the cheap look guards the expensive pass. A never-seen or stale draft refuses
18
+ * before the resolver constructs.
19
+ *
20
+ * On a pass the store records the document that the gate validated, the pinned snapshot, and the anchor from
21
+ * the thread row. The one-per-thread rule of the store bounds a concurrent race, and that refusal returns as
22
+ * data. The outcome of a pass carries the version id.
23
+ */
24
+ import { ok } from "neverthrow";
25
+ import { z } from "zod";
26
+ import { createNoopLogger } from "../../lib/console-logger.js";
27
+ import { describeDbError } from "../../lib/db-result.js";
28
+ import { finishDraft } from "../../report-model/draft-finish.js";
29
+ import { computeDraftHash } from "../../report-model/draft-hash.js";
30
+ import { validateReport } from "../../report-model/validate.js";
31
+ import { defineTool } from "../define-tool.js";
32
+ import { openReportThread } from "../report-authoring/authoring-tools.js";
33
+ /** The empty input. The tool records the current draft of the thread, thus it needs no field. */
34
+ const recordVersionInput = z.object({});
35
+ /** A short account of a record refusal, for the detail of a transient failure. */
36
+ function describeRecordFailure(error) {
37
+ switch (error.type) {
38
+ case "malformed_document":
39
+ return "the gate accepted a document that the store read as malformed";
40
+ case "malformed_snapshot":
41
+ return "the pinned snapshot is malformed";
42
+ case "parent_analysis_mismatch":
43
+ return "the parent version belongs to a different analysis";
44
+ case "thread_already_holds_version":
45
+ return "the thread already holds a recorded version";
46
+ default:
47
+ return describeDbError(error);
48
+ }
49
+ }
50
+ /**
51
+ * Make the record tool over the session-state gateway, the version store, the thread store, and the
52
+ * resolver.
53
+ *
54
+ * The tool reads the thread id from the scope of the call, and it loads the state through the gateway. Thus
55
+ * one factory serves every thread. The tool holds no per-session value.
56
+ */
57
+ export function createRecordVersionTool(deps) {
58
+ const logger = (deps.logger ?? createNoopLogger()).named("record-report-version");
59
+ return defineTool({
60
+ id: "record_report_version",
61
+ description: "Record the current draft as one report version. The tool runs the whole gate first: it finishes the draft, " +
62
+ "resolves each reference, matches each chart encoding, and matches each assert. An incomplete draft gives back the gap list, " +
63
+ "and a failed reference gives back the block that broke. The tool records a version only after the eyes look at the current page. " +
64
+ "A thread holds one version, thus a second record gives back that the thread already holds one.",
65
+ inputSchema: recordVersionInput,
66
+ executionMode: "inline",
67
+ describeCall: "none",
68
+ execute: async (_input, ctx) => {
69
+ const opened = await openReportThread(deps.gateway, ctx.session.scope);
70
+ if (opened.isErr()) {
71
+ return ok({ outcome: "refused", refusal: opened.error });
72
+ }
73
+ const { threadId, analysisId, state, seenDocumentHash } = opened.value;
74
+ const { document: draft, snapshot } = state;
75
+ const finished = finishDraft(draft, snapshot);
76
+ if (!finished.valid) {
77
+ return ok({ outcome: "gaps", gaps: finished.gaps });
78
+ }
79
+ // The look-before-record rule runs before the resolver and the validation, on the state that the
80
+ // load already read. The preview stamps the rendered hash, and the eyes copy it onto the seen
81
+ // hash. A never-seen page carries no seen hash. A stale look carries the hash of an earlier draft,
82
+ // thus a mismatch against the current draft refuses. Each refusal skips the expensive validation.
83
+ const currentHash = computeDraftHash(draft);
84
+ if (seenDocumentHash === null) {
85
+ return ok({ outcome: "never-seen" });
86
+ }
87
+ if (seenDocumentHash !== currentHash) {
88
+ return ok({ outcome: "stale-look" });
89
+ }
90
+ if (deps.makeResolver === undefined) {
91
+ return ok({ outcome: "resolver-unavailable" });
92
+ }
93
+ // The resolver construction resolves the workspace root inside, and that seam throws on an
94
+ // unresolvable root. The guard turns the throw into a typed outcome that names the fault, thus a
95
+ // control-flow exception propagates and the unresolvable root does not.
96
+ let resolver;
97
+ try {
98
+ resolver = deps.makeResolver({ analysisId, auth: ctx.session.auth });
99
+ }
100
+ catch (cause) {
101
+ logger.warn("the workspace root did not resolve", { threadId, analysisId, ...logger.errorFields(cause) });
102
+ return ok({ outcome: "root-unresolvable", detail: "the workspace root did not resolve" });
103
+ }
104
+ const validation = await validateReport(finished.document, snapshot, resolver);
105
+ if (!validation.valid) {
106
+ return ok({
107
+ outcome: "invalid",
108
+ ...(validation.schemaIssues ? { schemaIssues: validation.schemaIssues } : {}),
109
+ ...(validation.duplicateIds ? { duplicateIds: validation.duplicateIds } : {}),
110
+ ...(validation.resolutionFailures ? { resolutionFailures: validation.resolutionFailures } : {}),
111
+ });
112
+ }
113
+ const thread = await deps.threads.getThread(threadId);
114
+ if (thread.isErr()) {
115
+ logger.warn("the thread row read failed", { threadId, analysisId, ...logger.errorFields(thread.error.cause) });
116
+ return ok({ outcome: "record-failed", detail: describeDbError(thread.error) });
117
+ }
118
+ if (thread.value === null) {
119
+ logger.warn("the report thread row is absent at record", { threadId, analysisId });
120
+ return ok({ outcome: "record-failed", detail: "the report thread row is absent" });
121
+ }
122
+ const recorded = await deps.store.record({
123
+ document: finished.document,
124
+ snapshot,
125
+ analysisId,
126
+ threadId,
127
+ parentThreadId: thread.value.parentThreadId,
128
+ parentSeq: thread.value.parentSeq,
129
+ });
130
+ return recorded.match((ref) => ok({ outcome: "recorded", versionId: ref.versionId }), (error) => {
131
+ if (error.type === "thread_already_holds_version") {
132
+ return ok({ outcome: "already-recorded" });
133
+ }
134
+ logger.warn("the version did not record", { threadId, analysisId, reason: error.type });
135
+ return ok({ outcome: "record-failed", detail: describeRecordFailure(error) });
136
+ });
137
+ },
138
+ });
139
+ }
@@ -27,12 +27,13 @@ import { ok } from "neverthrow";
27
27
  import { z } from "zod";
28
28
  import { defineTool } from "../define-tool.js";
29
29
  import { capCodePoints, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
30
+ import { LIBS_CONTAINER_PATH } from "../../sandbox/mount-plan.js";
30
31
  /**
31
32
  * Where the list lives when the host mounts the library store at the same path
32
33
  * the sandbox does. Hosts that do not — the store is baked into the image and
33
34
  * never bind-mounted — inject their own path instead.
34
35
  */
35
- const DEFAULT_PACKAGES_FILE = "/mnt/libs/current/packages.txt";
36
+ const DEFAULT_PACKAGES_FILE = `${LIBS_CONTAINER_PATH}/current/packages.txt`;
36
37
  /**
37
38
  * Default cap on a listing — high enough that the real store is never truncated.
38
39
  * The shipped catalog is ~270 packages (~3 KB, well under a thousand tokens), so
@@ -14,7 +14,11 @@
14
14
  * it at — read where it lives, render where the caller will open it. Reading it
15
15
  * host-side is what lets a planner ask what reference data exists before any
16
16
  * sandbox is created; a step agent asking mid-run sees the same store, because
17
- * the mount and this path are the same bytes.
17
+ * the mount and this path are the same bytes. An omitted `refStorePath` falls
18
+ * back to that same container path — where a host that mounts the store into its
19
+ * own process (a K8s pod holding the ref-store PVC) already sees it. The fallback
20
+ * is stat-verified like any other root, so a host with nothing mounted there
21
+ * still reports the store as unavailable.
18
22
  *
19
23
  * The scan reports paths; meaning is joined on afterwards from the install
20
24
  * receipt and the catalog it names (organism, format, and each file's internal
@@ -14,7 +14,11 @@
14
14
  * it at — read where it lives, render where the caller will open it. Reading it
15
15
  * host-side is what lets a planner ask what reference data exists before any
16
16
  * sandbox is created; a step agent asking mid-run sees the same store, because
17
- * the mount and this path are the same bytes.
17
+ * the mount and this path are the same bytes. An omitted `refStorePath` falls
18
+ * back to that same container path — where a host that mounts the store into its
19
+ * own process (a K8s pod holding the ref-store PVC) already sees it. The fallback
20
+ * is stat-verified like any other root, so a host with nothing mounted there
21
+ * still reports the store as unavailable.
18
22
  *
19
23
  * The scan reports paths; meaning is joined on afterwards from the install
20
24
  * receipt and the catalog it names (organism, format, and each file's internal
@@ -30,9 +34,13 @@ import { z } from "zod";
30
34
  import { capCodePoints, DETAIL_MAX_LENGTH, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
31
35
  import { REFERENCE_DATA_CATALOG } from "../../reference-data/catalog.js";
32
36
  import { parseReferenceInstallReceipt } from "../../reference-data/receipt.js";
37
+ import { REFS_CONTAINER_PATH } from "../../sandbox/mount-plan.js";
33
38
  import { defineTool } from "../define-tool.js";
34
- /** Container path the ref store is mounted at; every reported path is rendered under it. */
35
- const REFS_ROOT = "/mnt/refs";
39
+ /**
40
+ * Container path the ref store is mounted at: every reported path is rendered
41
+ * under it, and it is the read root when the embedder supplies none.
42
+ */
43
+ const REFS_ROOT = REFS_CONTAINER_PATH;
36
44
  const MAX_ENTRIES = 200;
37
45
  const MAX_SCANNED_ENTRIES = 2_000;
38
46
  const MAX_PATH_BYTES = 40_000;
@@ -283,8 +291,6 @@ async function collectFiles(directory, relative, budget) {
283
291
  */
284
292
  async function scanStore(root, relative, recursive = false) {
285
293
  const empty = () => ({ entries: [], scannedEntries: 0, truncated: false });
286
- if (!root)
287
- return { state: "unavailable", ...empty() };
288
294
  try {
289
295
  if (!(await lstat(root)).isDirectory())
290
296
  return { state: "unavailable", ...empty() };
@@ -494,6 +500,7 @@ function renderContent(path, scan, entries) {
494
500
  }
495
501
  /** Create reference discovery over the host-visible reference store. */
496
502
  export function createListAvailableRefsTool(deps) {
503
+ const storeRoot = deps.refStorePath ?? REFS_ROOT;
497
504
  return defineTool({
498
505
  id: "list_available_refs",
499
506
  description: "Resolve reference data you need into a real path. Reports the reference data provisioned for this environment right now — the only reference data an analysis script can open. " +
@@ -570,7 +577,7 @@ export function createListAvailableRefsTool(deps) {
570
577
  // recursive pass a search from the store root only ever sees directories,
571
578
  // which carry no dataset labels, so "find me a regulon network" finds nothing.
572
579
  const needleGiven = (query?.trim() ?? "") !== "";
573
- const raw = await scanStore(deps.refStorePath, posixPath.relative(REFS_ROOT, requested.path) || ".", needleGiven);
580
+ const raw = await scanStore(storeRoot, posixPath.relative(REFS_ROOT, requested.path) || ".", needleGiven);
574
581
  const enriched = enrichEntries(raw.entries, raw);
575
582
  const categories = collectCategories(enriched);
576
583
  const needle = query?.trim().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
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",
@@ -44,11 +44,12 @@
44
44
  "typecheck": "tsc --noEmit"
45
45
  },
46
46
  "dependencies": {
47
- "@ai-sdk/anthropic": "^4.0.20",
48
- "@ai-sdk/openai-compatible": "^3.0.10",
49
- "@ai-sdk/provider": "^4.0.3",
50
- "@ai-sdk/provider-utils": "5.0.10",
51
- "@anthropic-ai/sdk": "^0.107.0",
47
+ "@ai-sdk/anthropic": "^4.0.38",
48
+ "@ai-sdk/openai": "^4.0.39",
49
+ "@ai-sdk/openai-compatible": "^3.0.30",
50
+ "@ai-sdk/provider": "^4.0.7",
51
+ "@ai-sdk/provider-utils": "5.0.27",
52
+ "@anthropic-ai/sdk": "^0.116.0",
52
53
  "@dbos-inc/dbos-sdk": "^4.23.6",
53
54
  "@kubernetes/client-node": "^1.4.0",
54
55
  "@opentelemetry/api": "^1.9.1",
@@ -60,17 +61,18 @@
60
61
  "@opentelemetry/sdk-trace-base": "^2.9.0",
61
62
  "@opentelemetry/sdk-trace-node": "^2.9.0",
62
63
  "@opentelemetry/semantic-conventions": "^1.43.0",
63
- "ai": "^7.0.28",
64
+ "ai": "^7.0.61",
64
65
  "cheerio": "^1.2.0",
65
66
  "dockerode": "^4.0.12",
66
67
  "fast-xml-parser": "^5.10.0",
67
68
  "hono": "^4.12.30",
69
+ "hyparquet": "^1.28.1",
68
70
  "js-tiktoken": "^1.0.21",
69
71
  "mime": "^4.1.0",
70
72
  "nanoid": "^5.1.16",
71
73
  "neverthrow": "^8.2.0",
72
74
  "nunjucks": "^3.2.4",
73
- "openai": "^4.104.0",
75
+ "openai": "^7.4.0",
74
76
  "p-queue": "^9.3.3",
75
77
  "pdf-parse": "^2.4.5",
76
78
  "pg": "^8.22.0",