@inflexa-ai/harness 0.18.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 (83) hide show
  1. package/dist/agents/report-session-agent.d.ts +72 -0
  2. package/dist/agents/report-session-agent.js +87 -0
  3. package/dist/app/report-session-runtime.d.ts +72 -0
  4. package/dist/app/report-session-runtime.js +183 -0
  5. package/dist/app/spawn-report-session.d.ts +32 -5
  6. package/dist/app/spawn-report-session.js +19 -0
  7. package/dist/config/environment-stores.d.ts +12 -7
  8. package/dist/config/resource-limits.d.ts +6 -10
  9. package/dist/config/resource-limits.js +6 -44
  10. package/dist/contracts/report-reference.d.ts +4 -0
  11. package/dist/contracts/report-reference.js +3 -0
  12. package/dist/index.d.ts +2 -1
  13. package/dist/lib/chrome.d.ts +14 -0
  14. package/dist/lib/chrome.js +7 -0
  15. package/dist/lib/page-capture.d.ts +47 -0
  16. package/dist/lib/page-capture.js +83 -0
  17. package/dist/loop/run-agent.js +104 -15
  18. package/dist/memory/ai-sdk-message-storage.d.ts +9 -4
  19. package/dist/memory/ai-sdk-message-storage.js +9 -4
  20. package/dist/memory/count-tokens.js +34 -5
  21. package/dist/memory/thread-store.d.ts +4 -4
  22. package/dist/memory/thread-store.js +18 -9
  23. package/dist/prompts/report-session.d.ts +1 -0
  24. package/dist/prompts/report-session.js +110 -0
  25. package/dist/providers/ai-sdk.d.ts +158 -3
  26. package/dist/providers/ai-sdk.js +234 -23
  27. package/dist/providers/errors.d.ts +14 -0
  28. package/dist/providers/errors.js +103 -18
  29. package/dist/providers/types.d.ts +33 -0
  30. package/dist/providers/types.js +10 -1
  31. package/dist/report-model/assert-rules.d.ts +44 -0
  32. package/dist/report-model/assert-rules.js +97 -0
  33. package/dist/report-model/draft-hash.d.ts +14 -0
  34. package/dist/report-model/draft-hash.js +36 -0
  35. package/dist/report-model/fixture-resolver.js +2 -71
  36. package/dist/report-model/{mint-snapshot.d.ts → pin-snapshot.d.ts} +7 -7
  37. package/dist/report-model/{mint-snapshot.js → pin-snapshot.js} +7 -7
  38. package/dist/report-model/production-resolver.d.ts +124 -0
  39. package/dist/report-model/production-resolver.js +801 -0
  40. package/dist/report-model/reference-resolver.d.ts +7 -1
  41. package/dist/report-model/validate.d.ts +34 -2
  42. package/dist/report-model/validate.js +37 -17
  43. package/dist/report-render/page.d.ts +23 -0
  44. package/dist/report-render/page.js +36 -3
  45. package/dist/report-render/value-bridge.d.ts +62 -0
  46. package/dist/report-render/value-bridge.js +82 -0
  47. package/dist/runtime/assemble.d.ts +39 -0
  48. package/dist/runtime/assemble.js +67 -6
  49. package/dist/sandbox/mount-plan.d.ts +7 -0
  50. package/dist/sandbox/mount-plan.js +7 -2
  51. package/dist/state/init.js +36 -0
  52. package/dist/state/purge-analysis.js +1 -0
  53. package/dist/state/report-session-state.d.ts +139 -0
  54. package/dist/state/report-session-state.js +177 -0
  55. package/dist/state/report-versions.d.ts +3 -6
  56. package/dist/state/report-versions.js +11 -38
  57. package/dist/state/snapshot-parse.d.ts +34 -0
  58. package/dist/state/snapshot-parse.js +82 -0
  59. package/dist/tasks/extract-values-script.d.ts +64 -0
  60. package/dist/tasks/extract-values-script.js +180 -0
  61. package/dist/tasks/extract-values.d.ts +124 -0
  62. package/dist/tasks/extract-values.js +193 -0
  63. package/dist/tools/ad-hoc-router.d.ts +7 -2
  64. package/dist/tools/ad-hoc-router.js +2 -1
  65. package/dist/tools/define-tool.d.ts +29 -0
  66. package/dist/tools/define-tool.js +26 -0
  67. package/dist/tools/report/preview-snapshot.d.ts +1 -1
  68. package/dist/tools/report/preview-snapshot.js +11 -50
  69. package/dist/tools/report-authoring/authoring-tools.d.ts +181 -24
  70. package/dist/tools/report-authoring/authoring-tools.js +152 -44
  71. package/dist/tools/report-session/examine-page.d.ts +90 -0
  72. package/dist/tools/report-session/examine-page.js +136 -0
  73. package/dist/tools/report-session/index.d.ts +10 -0
  74. package/dist/tools/report-session/index.js +10 -0
  75. package/dist/tools/report-session/preview-report.d.ts +105 -0
  76. package/dist/tools/report-session/preview-report.js +269 -0
  77. package/dist/tools/report-session/record-version.d.ts +102 -0
  78. package/dist/tools/report-session/record-version.js +139 -0
  79. package/dist/tools/research/generate-plan.js +4 -2
  80. package/dist/tools/sandbox/list-available-packages.js +2 -1
  81. package/dist/tools/sandbox/list-available-refs.d.ts +5 -1
  82. package/dist/tools/sandbox/list-available-refs.js +13 -6
  83. package/package.json +10 -8
@@ -17,7 +17,7 @@ import type { Reference, UnresolvedReference } from "../contracts/report-referen
17
17
  *
18
18
  * `rows` is optional because an image is pinned by its hash alone and holds no cells. An empty array in
19
19
  * that position would say that the artifact holds zero rows, which is a different and false claim. The
20
- * mint never populates `rows`. `rows` serves the fixture realization only, and that realization resolves
20
+ * pin never populates `rows`. `rows` serves the fixture realization only, and that realization resolves
21
21
  * from the snapshot itself. A production snapshot pins identity, and a value comes from a read of the
22
22
  * artifact.
23
23
  *
@@ -71,9 +71,15 @@ export type ResolvedValue = {
71
71
  * The `Ok` channel carries the resolved value. The `Err` channel carries the `UnresolvedReference`,
72
72
  * which names the reason and the detail that a reference did not bind. The validator collects each such
73
73
  * `Err` into its report, thus a caller reads the reason as data and resolution never throws.
74
+ *
75
+ * `prepare` is an optional batch step that runs one time before the per-reference loop. A realization
76
+ * that reads storage groups the references by artifact, reads each file one time, and fills a cache.
77
+ * `resolve` then answers from the cache. The method is optional, thus a realization without it keeps the
78
+ * per-reference behavior and the extension breaks no caller.
74
79
  */
75
80
  export interface ReferenceResolver {
76
81
  resolve(reference: Reference, snapshot: ReportSnapshot): Promise<Result<ResolvedValue, UnresolvedReference>>;
82
+ prepare?(references: readonly Reference[], snapshot: ReportSnapshot): Promise<void>;
77
83
  }
78
84
  /**
79
85
  * Give back each name in `columns` that no row holds, in the order of `columns`.
@@ -8,9 +8,16 @@
8
8
  * each reference against the pinned evidence, and it warns about a free numeral in prose.
9
9
  */
10
10
  import type { UnresolvedReference } from "../contracts/report-reference.js";
11
- import { type ReportWarning } from "./block-walk.js";
12
- import { type ReferenceResolver, type ReportSnapshot } from "./reference-resolver.js";
11
+ import { type AnyBlock, type CollectedReference, type ReportWarning } from "./block-walk.js";
12
+ import { type ReferenceResolver, type ReportSnapshot, type ResolvedValue } from "./reference-resolver.js";
13
13
  export type { ReportWarning };
14
+ /**
15
+ * The cap on how many references resolve at the same time.
16
+ *
17
+ * A production resolver reads storage for each reference, and a large report holds hundreds of them. An
18
+ * unbounded fan-out would open one read for each reference at the same moment.
19
+ */
20
+ export declare const RESOLUTION_CONCURRENCY = 8;
14
21
  /** One schema conformance problem, reduced to a path and a message. */
15
22
  export interface SchemaIssue {
16
23
  path: string;
@@ -36,5 +43,30 @@ export type ReportValidation = {
36
43
  resolutionFailures?: ResolutionFailure[];
37
44
  warnings: ReportWarning[];
38
45
  };
46
+ /**
47
+ * Match each column that a chart encoding names against the table that its binding resolved to.
48
+ *
49
+ * The binding schema admits a table reference only, thus a resolved value of another type means that the
50
+ * bound resolver broke its own contract. That is reported and never ignored, because a chart with no
51
+ * table behind it renders nothing and a silent skip would pass it as grounded.
52
+ */
53
+ export declare function checkChartEncoding(entry: CollectedReference, value: ResolvedValue): UnresolvedReference | undefined;
54
+ /** The resolved values and the failures of one resolution pass over the references of a block tree. */
55
+ export interface ReferenceResolution {
56
+ /** The resolved value of each block whose reference resolved, keyed by its block id. */
57
+ resolvedByBlock: Map<string, ResolvedValue>;
58
+ /** Each reference that did not resolve, or whose chart encoding named an absent column. */
59
+ failures: ResolutionFailure[];
60
+ }
61
+ /**
62
+ * Resolve each reference of a block tree under the concurrency bound, and run the chart-encoding match.
63
+ *
64
+ * The walk collects each reference one time, `allWithConcurrency` bounds the fan-out, and
65
+ * `checkChartEncoding` catches a chart that plots a column which the bound table does not hold. The record
66
+ * gate and the preview share this one pass, thus the two refuse the same references. A value-bearing block
67
+ * whose reference resolved carries its value in the map. A claim or a citation reference reads no value
68
+ * from the map. The walk collects every failure, thus a reviewer sees each block that broke.
69
+ */
70
+ export declare function resolveDocumentReferences(sections: readonly AnyBlock[], snapshot: ReportSnapshot, resolver: ReferenceResolver): Promise<ReferenceResolution>;
39
71
  /** Resolve each reference, collect the schema conformance and the resolution outcomes, and warn on prose. */
40
72
  export declare function validateReport(document: unknown, snapshot: ReportSnapshot, resolver: ReferenceResolver): Promise<ReportValidation>;
@@ -17,7 +17,7 @@ import { columnsHeldByNoRow } from "./reference-resolver.js";
17
17
  * A production resolver reads storage for each reference, and a large report holds hundreds of them. An
18
18
  * unbounded fan-out would open one read for each reference at the same moment.
19
19
  */
20
- const RESOLUTION_CONCURRENCY = 8;
20
+ export const RESOLUTION_CONCURRENCY = 8;
21
21
  /**
22
22
  * Match each column that a chart encoding names against the table that its binding resolved to.
23
23
  *
@@ -25,7 +25,7 @@ const RESOLUTION_CONCURRENCY = 8;
25
25
  * bound resolver broke its own contract. That is reported and never ignored, because a chart with no
26
26
  * table behind it renders nothing and a silent skip would pass it as grounded.
27
27
  */
28
- function checkChartEncoding(entry, value) {
28
+ export function checkChartEncoding(entry, value) {
29
29
  const encodingColumns = entry.encodingColumns;
30
30
  if (encodingColumns === undefined || encodingColumns.length === 0) {
31
31
  return undefined;
@@ -43,31 +43,51 @@ function checkChartEncoding(entry, value) {
43
43
  detail: `the encoding names column ${absent.join(", ")}, which the bound table does not hold`,
44
44
  };
45
45
  }
46
- /** Resolve each reference, collect the schema conformance and the resolution outcomes, and warn on prose. */
47
- export async function validateReport(document, snapshot, resolver) {
48
- const parsed = ReportDocumentSchema.safeParse(document);
49
- if (!parsed.success) {
50
- const schemaIssues = parsed.error.issues.map((issue) => ({
51
- path: issue.path.map((segment) => String(segment)).join("."),
52
- message: issue.message,
53
- }));
54
- return { valid: false, schemaIssues, warnings: [] };
46
+ /**
47
+ * Resolve each reference of a block tree under the concurrency bound, and run the chart-encoding match.
48
+ *
49
+ * The walk collects each reference one time, `allWithConcurrency` bounds the fan-out, and
50
+ * `checkChartEncoding` catches a chart that plots a column which the bound table does not hold. The record
51
+ * gate and the preview share this one pass, thus the two refuse the same references. A value-bearing block
52
+ * whose reference resolved carries its value in the map. A claim or a citation reference reads no value
53
+ * from the map. The walk collects every failure, thus a reviewer sees each block that broke.
54
+ */
55
+ export async function resolveDocumentReferences(sections, snapshot, resolver) {
56
+ const { references } = walkBlocks(sections);
57
+ // A realization that reads storage batches its reads here, one time before the loop. It fills a cache
58
+ // that each `resolve` then answers from. A realization without `prepare` keeps the per-reference read.
59
+ if (resolver.prepare !== undefined) {
60
+ await resolver.prepare(references.map((entry) => entry.reference), snapshot);
55
61
  }
56
- const { references, repeatedIds, warnings } = walkBlocks(parsed.data.sections);
57
62
  const resolved = await allWithConcurrency(references.map((entry) => () => resolver.resolve(entry.reference, snapshot).then((result) => ({ entry, result }))), RESOLUTION_CONCURRENCY);
58
- // Collect every failure. A reviewer must see each block that broke, thus the walk does not stop at
59
- // the first unresolved reference. The `Err` channel carries the unresolved reference itself.
60
- const resolutionFailures = [];
63
+ const failures = [];
64
+ const resolvedByBlock = new Map();
61
65
  for (const { entry, result } of resolved) {
62
66
  if (result.isErr()) {
63
- resolutionFailures.push({ blockId: entry.blockId, failure: result.error });
67
+ failures.push({ blockId: entry.blockId, failure: result.error });
64
68
  continue;
65
69
  }
66
70
  const encodingFailure = checkChartEncoding(entry, result.value);
67
71
  if (encodingFailure !== undefined) {
68
- resolutionFailures.push({ blockId: entry.blockId, failure: encodingFailure });
72
+ failures.push({ blockId: entry.blockId, failure: encodingFailure });
73
+ continue;
69
74
  }
75
+ resolvedByBlock.set(entry.blockId, result.value);
76
+ }
77
+ return { resolvedByBlock, failures };
78
+ }
79
+ /** Resolve each reference, collect the schema conformance and the resolution outcomes, and warn on prose. */
80
+ export async function validateReport(document, snapshot, resolver) {
81
+ const parsed = ReportDocumentSchema.safeParse(document);
82
+ if (!parsed.success) {
83
+ const schemaIssues = parsed.error.issues.map((issue) => ({
84
+ path: issue.path.map((segment) => String(segment)).join("."),
85
+ message: issue.message,
86
+ }));
87
+ return { valid: false, schemaIssues, warnings: [] };
70
88
  }
89
+ const { repeatedIds, warnings } = walkBlocks(parsed.data.sections);
90
+ const { failures: resolutionFailures } = await resolveDocumentReferences(parsed.data.sections, snapshot, resolver);
71
91
  if (repeatedIds.length > 0 || resolutionFailures.length > 0) {
72
92
  return {
73
93
  valid: false,
@@ -118,10 +118,33 @@ export declare const ECHARTS_THEME: {
118
118
  };
119
119
  };
120
120
  };
121
+ /**
122
+ * The name of the readiness event, and the name of the window sentinel that guards a late listener. The
123
+ * bootstrap below emits both names, thus this module owns them. A capture that waits for the page reads the
124
+ * same two constants, thus the emitter and the waiter cannot disagree over a rename.
125
+ */
126
+ export declare const THEME_READY_EVENT = "inflexa-theme-ready";
127
+ export declare const THEME_READY_SENTINEL = "__inflexaThemeReady";
128
+ /**
129
+ * The navigation budget and the readiness budget of one page capture, in milliseconds. The readiness budget
130
+ * bounds the wait for the event that the bootstrap dispatches. Every capture of this page reads the same two
131
+ * budgets, thus two capture sites cannot drift apart.
132
+ */
133
+ export declare const PAGE_NAV_TIMEOUT_MS = 20000;
134
+ export declare const PAGE_READY_TIMEOUT_MS = 8000;
121
135
  /**
122
136
  * The page-side script that wires each chart. It finds every chart container, reads the option JSON from
123
137
  * the sibling `<script type="application/json">` element, and initializes ECharts with the registered
124
138
  * theme. The skeleton registers the theme before this script runs. A resize handler keeps each chart
125
139
  * fit to the window.
140
+ *
141
+ * The script signals readiness when the bootstrap completes, and immediately when no chart exists. It sets
142
+ * the `window.__inflexaThemeReady` sentinel and dispatches the `inflexa-theme-ready` event on the document.
143
+ * A reader that captures the page keys on this signal, thus the capture returns when the page is ready and
144
+ * not at a timeout. The sentinel guards a listener that registers after the dispatch, thus a late listener
145
+ * still resolves.
146
+ *
147
+ * Each chart initializes inside a guard. One malformed option is exactly the fault that a look must
148
+ * diagnose, thus it must never stop a sibling chart and it must never withhold the readiness signal.
126
149
  */
127
150
  export declare const CHART_BOOTSTRAP: string;
@@ -274,14 +274,42 @@ export const ECHARTS_THEME = {
274
274
  splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
275
275
  },
276
276
  };
277
+ /**
278
+ * The name of the readiness event, and the name of the window sentinel that guards a late listener. The
279
+ * bootstrap below emits both names, thus this module owns them. A capture that waits for the page reads the
280
+ * same two constants, thus the emitter and the waiter cannot disagree over a rename.
281
+ */
282
+ export const THEME_READY_EVENT = "inflexa-theme-ready";
283
+ export const THEME_READY_SENTINEL = "__inflexaThemeReady";
284
+ /**
285
+ * The navigation budget and the readiness budget of one page capture, in milliseconds. The readiness budget
286
+ * bounds the wait for the event that the bootstrap dispatches. Every capture of this page reads the same two
287
+ * budgets, thus two capture sites cannot drift apart.
288
+ */
289
+ export const PAGE_NAV_TIMEOUT_MS = 20_000;
290
+ export const PAGE_READY_TIMEOUT_MS = 8_000;
277
291
  /**
278
292
  * The page-side script that wires each chart. It finds every chart container, reads the option JSON from
279
293
  * the sibling `<script type="application/json">` element, and initializes ECharts with the registered
280
294
  * theme. The skeleton registers the theme before this script runs. A resize handler keeps each chart
281
295
  * fit to the window.
296
+ *
297
+ * The script signals readiness when the bootstrap completes, and immediately when no chart exists. It sets
298
+ * the `window.__inflexaThemeReady` sentinel and dispatches the `inflexa-theme-ready` event on the document.
299
+ * A reader that captures the page keys on this signal, thus the capture returns when the page is ready and
300
+ * not at a timeout. The sentinel guards a listener that registers after the dispatch, thus a late listener
301
+ * still resolves.
302
+ *
303
+ * Each chart initializes inside a guard. One malformed option is exactly the fault that a look must
304
+ * diagnose, thus it must never stop a sibling chart and it must never withhold the readiness signal.
282
305
  */
283
306
  export const CHART_BOOTSTRAP = `(function () {
307
+ function signalReady() {
308
+ window.${THEME_READY_SENTINEL} = true;
309
+ document.dispatchEvent(new Event(${JSON.stringify(THEME_READY_EVENT)}));
310
+ }
284
311
  if (typeof echarts === "undefined") {
312
+ signalReady();
285
313
  return;
286
314
  }
287
315
  var containers = document.querySelectorAll("[data-echarts-id]");
@@ -291,9 +319,13 @@ export const CHART_BOOTSTRAP = `(function () {
291
319
  if (!optionScript || optionScript.getAttribute("type") !== "application/json") {
292
320
  continue;
293
321
  }
294
- var option = JSON.parse(optionScript.textContent || "{}");
295
- var chart = echarts.init(container, ${JSON.stringify(ECHARTS_THEME_NAME)});
296
- chart.setOption(option);
322
+ try {
323
+ var option = JSON.parse(optionScript.textContent || "{}");
324
+ var chart = echarts.init(container, ${JSON.stringify(ECHARTS_THEME_NAME)});
325
+ chart.setOption(option);
326
+ } catch (cause) {
327
+ console.error("chart bootstrap failed for " + (container.getAttribute("data-echarts-id") || "(no id)") + ": " + (cause && cause.message ? cause.message : cause));
328
+ }
297
329
  }
298
330
  window.addEventListener("resize", function () {
299
331
  var nodes = document.querySelectorAll("[data-echarts-id]");
@@ -304,4 +336,5 @@ export const CHART_BOOTSTRAP = `(function () {
304
336
  }
305
337
  }
306
338
  });
339
+ signalReady();
307
340
  })();`;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The value bridge: a resolved value maps onto a render value, keyed by block id.
3
+ *
4
+ * The resolver gives one `ResolvedValue` for each reference. The renderer takes a `RenderValues` map. The
5
+ * two models agree on a scalar, a table, and a citation, but they part on a figure. A `ResolvedValue`
6
+ * file echo carries a path and a hash, and a page needs a source string. Thus the bridge takes a
7
+ * caller-supplied policy that turns a file echo into a `src` string. The preview tool gives the concrete
8
+ * policy, because the page directory and its asset access are a session concern.
9
+ *
10
+ * The bridge is pure. It reads no file, it resolves no reference, and it imports nothing from the state
11
+ * layer or the tool layer. A block kind that carries no render value maps to no entry. A block kind whose
12
+ * resolved value has the wrong type becomes one typed mismatch. The bridge collects every mismatch, thus
13
+ * one call reports each fault at one time.
14
+ */
15
+ import { type Result } from "neverthrow";
16
+ import type { ResolvedValue } from "../report-model/reference-resolver.js";
17
+ import type { RenderValues } from "./types.js";
18
+ /** The block kinds that carry no render value. Each one renders from the block and the ledger alone. */
19
+ type NoValueKind = "text" | "claim" | "citation" | "section";
20
+ /** The block kinds that carry one render value under the block id in the map. */
21
+ type ValueBearingKind = "metric" | "table" | "chart" | "figure";
22
+ /** The file echo that a figure binding resolves to: a whole-file pin of a path and a hash. */
23
+ export type ResolvedFile = Extract<ResolvedValue, {
24
+ type: "file";
25
+ }>;
26
+ /** The policy that turns a resolved file echo into a figure `src` string. The caller owns the policy. */
27
+ export type FigureSourcePolicy = (file: ResolvedFile) => string;
28
+ /**
29
+ * The resolution result of one block: the block id, the block kind, and the resolved value of its binding.
30
+ *
31
+ * A value-bearing kind carries the resolved value. A no-value kind carries none, thus a text block with
32
+ * no binding is representable and a value on it is not.
33
+ */
34
+ export type BlockResolution = {
35
+ blockId: string;
36
+ kind: NoValueKind;
37
+ } | {
38
+ blockId: string;
39
+ kind: ValueBearingKind;
40
+ resolved: ResolvedValue;
41
+ };
42
+ /**
43
+ * One typed mismatch between a block kind and a resolved value type. `blockId` names the block, and
44
+ * `blockKind` names the kind that sets the expectation. `expected` names the resolved type that the kind
45
+ * needs, and `actual` names the resolved type that arrived.
46
+ */
47
+ export interface BridgeMismatch {
48
+ blockId: string;
49
+ blockKind: ValueBearingKind;
50
+ expected: ResolvedValue["type"];
51
+ actual: ResolvedValue["type"];
52
+ }
53
+ /**
54
+ * Map the resolved values of the blocks onto a `RenderValues` map.
55
+ *
56
+ * The bridge walks each resolution in order. A no-value kind adds no entry. A value-bearing kind whose
57
+ * resolved type matches adds one entry. A value-bearing kind whose resolved type does not match adds one
58
+ * mismatch. A run with a mismatch returns the mismatch list and no map. A run with no mismatch returns the
59
+ * complete map.
60
+ */
61
+ export declare function bridgeValues(resolutions: readonly BlockResolution[], figureSrc: FigureSourcePolicy): Result<RenderValues, BridgeMismatch[]>;
62
+ export {};
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The value bridge: a resolved value maps onto a render value, keyed by block id.
3
+ *
4
+ * The resolver gives one `ResolvedValue` for each reference. The renderer takes a `RenderValues` map. The
5
+ * two models agree on a scalar, a table, and a citation, but they part on a figure. A `ResolvedValue`
6
+ * file echo carries a path and a hash, and a page needs a source string. Thus the bridge takes a
7
+ * caller-supplied policy that turns a file echo into a `src` string. The preview tool gives the concrete
8
+ * policy, because the page directory and its asset access are a session concern.
9
+ *
10
+ * The bridge is pure. It reads no file, it resolves no reference, and it imports nothing from the state
11
+ * layer or the tool layer. A block kind that carries no render value maps to no entry. A block kind whose
12
+ * resolved value has the wrong type becomes one typed mismatch. The bridge collects every mismatch, thus
13
+ * one call reports each fault at one time.
14
+ */
15
+ import { err, ok } from "neverthrow";
16
+ /**
17
+ * Map the resolved values of the blocks onto a `RenderValues` map.
18
+ *
19
+ * The bridge walks each resolution in order. A no-value kind adds no entry. A value-bearing kind whose
20
+ * resolved type matches adds one entry. A value-bearing kind whose resolved type does not match adds one
21
+ * mismatch. A run with a mismatch returns the mismatch list and no map. A run with no mismatch returns the
22
+ * complete map.
23
+ */
24
+ export function bridgeValues(resolutions, figureSrc) {
25
+ const values = {};
26
+ const mismatches = [];
27
+ for (const resolution of resolutions) {
28
+ switch (resolution.kind) {
29
+ case "text":
30
+ case "claim":
31
+ case "citation":
32
+ case "section":
33
+ break;
34
+ case "metric": {
35
+ const value = resolution.resolved;
36
+ if (value.type !== "scalar") {
37
+ mismatches.push(mismatch(resolution.blockId, "metric", "scalar", value.type));
38
+ break;
39
+ }
40
+ values[resolution.blockId] = { type: "scalar", value: value.value };
41
+ break;
42
+ }
43
+ case "table":
44
+ case "chart": {
45
+ const value = resolution.resolved;
46
+ if (value.type !== "table") {
47
+ mismatches.push(mismatch(resolution.blockId, resolution.kind, "table", value.type));
48
+ break;
49
+ }
50
+ values[resolution.blockId] = tableValue(value);
51
+ break;
52
+ }
53
+ case "figure": {
54
+ const value = resolution.resolved;
55
+ if (value.type !== "file") {
56
+ mismatches.push(mismatch(resolution.blockId, "figure", "file", value.type));
57
+ break;
58
+ }
59
+ values[resolution.blockId] = { type: "figure", src: figureSrc(value) };
60
+ break;
61
+ }
62
+ }
63
+ }
64
+ if (mismatches.length > 0) {
65
+ return err(mismatches);
66
+ }
67
+ return ok(values);
68
+ }
69
+ /**
70
+ * Map a resolved table onto a render table. The explicit column order carries through when it is present,
71
+ * and it stays absent otherwise, thus the renderer keeps its first-row order as the default.
72
+ */
73
+ function tableValue(value) {
74
+ if (value.columns !== undefined) {
75
+ return { type: "table", rows: value.rows, columns: value.columns };
76
+ }
77
+ return { type: "table", rows: value.rows };
78
+ }
79
+ /** Make one mismatch that names the block, its kind, the resolved type that it needs, and the type that arrived. */
80
+ function mismatch(blockId, blockKind, expected, actual) {
81
+ return { blockId, blockKind, expected, actual };
82
+ }
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { type Result } from "neverthrow";
18
18
  import { type ConversationAgentDeps } from "../agents/conversation-agent.js";
19
+ import { type ReportSessionRuntime } from "../app/report-session-runtime.js";
19
20
  import type { UsageRecorder } from "../billing/usage-recorder.js";
20
21
  import type { ResourcePolicy } from "../config/resource-limits.js";
21
22
  import type { AgentDefinition } from "../loop/types.js";
@@ -24,8 +25,11 @@ import { type ExecuteAnalysisDeps, type ExecuteAnalysisInput, type ExecuteAnalys
24
25
  import { type SandboxStepDeps, type SandboxStepInput, type SandboxStepResult } from "../workflows/sandbox-step.js";
25
26
  import { type ExecuteTargetAssessmentDeps, type ExecuteTargetAssessmentInput, type ExecuteTargetAssessmentResult } from "../workflows/execute-target-assessment.js";
26
27
  import { type DataProfileDeps, type DataProfileWorkflowInput } from "../tasks/data-profile.js";
28
+ import { type ExtractValuesResult, type ExtractValuesWorkflowInput } from "../tasks/extract-values.js";
27
29
  import { type CitationResolverConfig } from "../citations/resolve.js";
28
30
  import type { CitationResolver } from "../citations/types.js";
31
+ import type { AuthContext } from "../auth/types.js";
32
+ import type { ReferenceResolver } from "../report-model/reference-resolver.js";
29
33
  /** Registered child sandbox-step callable the parent's child dispatch closes over. */
30
34
  export type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxStepResult>;
31
35
  /**
@@ -53,6 +57,7 @@ export interface RegisteredWorkflows {
53
57
  readonly sandboxStep: SandboxStepCallable;
54
58
  readonly executeTargetAssessment: (input: ExecuteTargetAssessmentInput) => Promise<ExecuteTargetAssessmentResult>;
55
59
  readonly dataProfile: (input: DataProfileWorkflowInput) => Promise<void>;
60
+ readonly extractValues: (input: ExtractValuesWorkflowInput) => Promise<ExtractValuesResult>;
56
61
  }
57
62
  /**
58
63
  * Conversation-agent deps minus the workflow callable, the resource policy, and
@@ -61,6 +66,19 @@ export interface RegisteredWorkflows {
61
66
  * workflows see, or a recorder that only half the agent tree reports to.
62
67
  */
63
68
  export type ConversationAssemblyDeps = Omit<ConversationAgentDeps, "executeAnalysisWorkflow" | "resourcePolicy" | "usageRecorder" | "citationResolver">;
69
+ /**
70
+ * What binds one reference resolver: the analysis whose pinned artifacts it reads, and the auth of the tool
71
+ * call that reads them. A resolver serves one analysis, thus the report tools build one for each call.
72
+ */
73
+ export interface ReportResolverScope {
74
+ readonly analysisId: string;
75
+ readonly auth: AuthContext;
76
+ }
77
+ /**
78
+ * The factory that gives the reference resolver of one report tool call. The harness wires the production
79
+ * realization, and an embedder can bind its own at its composition root.
80
+ */
81
+ export type MakeReportReferenceResolver = (scope: ReportResolverScope) => ReferenceResolver;
64
82
  export interface CoreRuntimeDeps {
65
83
  readonly conversation: ConversationAssemblyDeps;
66
84
  readonly workflows: CoreWorkflowDeps;
@@ -78,6 +96,20 @@ export interface CoreRuntimeDeps {
78
96
  readonly usageRecorder?: UsageRecorder;
79
97
  /** Harness-owned citation capability configuration; no ambient lookup occurs. */
80
98
  readonly citationResolverConfig?: CitationResolverConfig;
99
+ /**
100
+ * The byte cap on the in-process host read of a report reference. A file over the
101
+ * cap falls through to the extraction arm. Absent, the resolver uses its 16 MiB
102
+ * default. The embedder tunes it here, thus a host changes it with no harness change.
103
+ */
104
+ readonly reportHostReadCapBytes?: number;
105
+ /**
106
+ * The report reference resolver of an embedder. A managed host reads its pinned
107
+ * artifacts from its own store, thus it binds a realization of the seam here and the
108
+ * report tools resolve through it. Absent, the harness wires the production resolver
109
+ * over the workspace tree and the extraction arm, which is the whole OSS path. A bound
110
+ * factory replaces that wiring, thus `reportHostReadCapBytes` then governs nothing.
111
+ */
112
+ readonly makeReportReferenceResolver?: MakeReportReferenceResolver;
81
113
  }
82
114
  /**
83
115
  * The refusal a thread type carries no agent yet. `report` is a valid
@@ -120,6 +152,13 @@ export interface CoreRuntime {
120
152
  * on the `Result` channel.
121
153
  */
122
154
  readonly agents: ThreadAgentResolver;
155
+ /**
156
+ * The turn-start anchor of a report session. A serving path runs it at the
157
+ * turn start, thus a report tool call never arrives before the session state
158
+ * exists. The gateway stays behind the tool boundary, thus the runtime exposes
159
+ * the anchor operation alone here, and a later change wires it at the turn start.
160
+ */
161
+ readonly reportSession: Pick<ReportSessionRuntime, "ensureSessionState">;
123
162
  readonly workflows: RegisteredWorkflows;
124
163
  readonly citationResolver: CitationResolver;
125
164
  }
@@ -16,12 +16,18 @@
16
16
  */
17
17
  import { err, ok } from "neverthrow";
18
18
  import { createConversationAgent } from "../agents/conversation-agent.js";
19
+ import { createReportSessionAgent } from "../agents/report-session-agent.js";
20
+ import { createReportSessionRuntime } from "../app/report-session-runtime.js";
19
21
  import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
20
22
  import { registerExecuteAnalysis } from "../workflows/execute-analysis.js";
21
23
  import { registerSandboxStep } from "../workflows/sandbox-step.js";
22
24
  import { registerExecuteTargetAssessment, } from "../workflows/execute-target-assessment.js";
23
25
  import { registerDataProfileWorkflow } from "../tasks/data-profile.js";
26
+ import { bindExtractionTrigger, createExtractionArm, registerExtractValuesWorkflow, } from "../tasks/extract-values.js";
24
27
  import { createCitationResolver } from "../citations/resolve.js";
28
+ import { createThreadStore } from "../memory/thread-store.js";
29
+ import { createArtifactReadStore, createProductionResolver } from "../report-model/production-resolver.js";
30
+ import { createReportVersionStore } from "../state/report-versions.js";
25
31
  const _assertDomainError = true;
26
32
  /**
27
33
  * Wrap a type→agent registry as the resolution surface. Held apart from
@@ -60,6 +66,14 @@ export function assembleCoreRuntime(deps) {
60
66
  const executeAnalysis = registerExecuteAnalysis({ ...wf.buildExecuteAnalysis(sandboxStep), citationResolver, usageRecorder });
61
67
  const executeTargetAssessment = registerExecuteTargetAssessment({ ...wf.executeTargetAssessment, usageRecorder });
62
68
  const dataProfile = registerDataProfileWorkflow({ ...wf.dataProfile, usageRecorder });
69
+ // The extraction workflow shares the profile's sandbox and authorization rails, thus it draws the same
70
+ // three seams from the profile deps. The report resolver factory binds the extraction arm over this
71
+ // callable, thus a fall-through report reference reads its file out of process on the profile rails.
72
+ const extractValues = registerExtractValuesWorkflow({
73
+ sandboxClient: wf.dataProfile.sandboxClient,
74
+ runAuthorizer: wf.dataProfile.runAuthorizer,
75
+ ...(wf.dataProfile.logger ? { logger: wf.dataProfile.logger } : {}),
76
+ });
63
77
  const conversationAgent = createConversationAgent({
64
78
  ...conversation,
65
79
  executeAnalysisWorkflow: executeAnalysis,
@@ -67,22 +81,69 @@ export function assembleCoreRuntime(deps) {
67
81
  usageRecorder,
68
82
  citationResolver,
69
83
  });
70
- // The single registration point for every typed agent. One entry today; a
71
- // future thread type's agent plugs in here at assembly with no embedder
72
- // change. `conversation` is required at the type level because boot resolves
73
- // it unconditionally (`boot.ts` backfill) — a dropped registration must fail
74
- // tsc here, not surface only as a boot-time throw. The `Partial` over the
75
- // rest keeps the compiler honest that `report` has no agent yet.
84
+ // The report agent is a singleton over the conversation deps. The session
85
+ // runtime binds the per-session state to the thread behind the tool boundary,
86
+ // thus one definition serves every report thread. Its preview tool writes the
87
+ // page into the analysis tree and returns the path, thus the page write reaches
88
+ // no host seam.
89
+ const reportSession = createReportSessionRuntime({ pool: conversation.pool, ...(conversation.logger ? { logger: conversation.logger } : {}) });
90
+ const reportVersionStore = createReportVersionStore({ pool: conversation.pool, ...(conversation.logger ? { logger: conversation.logger } : {}) });
91
+ const reportThreads = createThreadStore(conversation.pool);
92
+ // The read cache of the production report path, one for each recent analysis. A preview
93
+ // and the record that follows it are two tool calls over one draft, thus they meet the
94
+ // same artifacts. The store carries the streamed hash and the extracted rows between
95
+ // them, and each resolver still binds its own extraction arm below. Thus the auth of a
96
+ // call never outlives the call, and one unchanged file costs one hash and one container.
97
+ const reportArtifactReads = createArtifactReadStore();
98
+ // The reference resolver of the report path, and its production wiring. A resolver reads
99
+ // the pinned artifacts of one analysis, thus the factory binds one analysis and one auth
100
+ // for each tool call. The extraction arm falls through to the registered extract-values
101
+ // callable for an over-cap file or a host parse fault. The cap gates the host read, and
102
+ // the resolver defaults it to 16 MiB.
103
+ const makeProductionReportResolver = (scope) => {
104
+ const arm = createExtractionArm(bindExtractionTrigger({ runAuthorizer: conversation.runAuthorizer, workflow: extractValues }, { auth: scope.auth, analysisId: scope.analysisId }));
105
+ return createProductionResolver({
106
+ workspaceRoot: conversation.resolveWorkspaceRoot(scope.analysisId),
107
+ analysisId: scope.analysisId,
108
+ extractionArm: arm,
109
+ readCache: reportArtifactReads.forAnalysis(scope.analysisId),
110
+ ...(deps.reportHostReadCapBytes !== undefined ? { cap: deps.reportHostReadCapBytes } : {}),
111
+ });
112
+ };
113
+ const makeReportResolver = deps.makeReportReferenceResolver ?? makeProductionReportResolver;
114
+ const reportAgent = createReportSessionAgent({
115
+ model: conversation.model,
116
+ pool: conversation.pool,
117
+ embedding: conversation.embedding,
118
+ workspaceFs: conversation.workspaceFs,
119
+ gateway: reportSession.gateway,
120
+ resolveWorkspaceRoot: conversation.resolveWorkspaceRoot,
121
+ store: reportVersionStore,
122
+ threads: reportThreads,
123
+ chrome: conversation.chrome,
124
+ makeResolver: makeReportResolver,
125
+ ...(conversation.logger ? { logger: conversation.logger } : {}),
126
+ });
127
+ // The single registration point for every typed agent. A future thread type's
128
+ // agent plugs in here at assembly with no embedder change. `conversation` is
129
+ // required at the type level because boot resolves it unconditionally
130
+ // (`boot.ts` backfill). `report` is required now that its agent registers here,
131
+ // so a dropped registration fails tsc, and it does not surface only as a
132
+ // resolution refusal. The `Partial` over the rest keeps the compiler honest
133
+ // about a type that has no agent yet.
76
134
  const agents = {
77
135
  conversation: conversationAgent,
136
+ report: reportAgent,
78
137
  };
79
138
  return {
80
139
  agents: createThreadAgentResolver(agents),
140
+ reportSession: { ensureSessionState: reportSession.ensureSessionState },
81
141
  workflows: {
82
142
  executeAnalysis,
83
143
  sandboxStep,
84
144
  executeTargetAssessment,
85
145
  dataProfile,
146
+ extractValues,
86
147
  },
87
148
  citationResolver,
88
149
  };
@@ -17,6 +17,13 @@
17
17
  * (see {@link buildSessionSubPaths}).
18
18
  */
19
19
  export declare const STEP_SUBDIRS: readonly ["output", "scripts", "figures", "logs", "notebooks"];
20
+ /**
21
+ * Container mountpoints of the two read-only stores. Exported because they are
22
+ * also the discovery tools' default read root: a host whose own process sees a
23
+ * store the way the sandbox does configures nothing further.
24
+ */
25
+ export declare const LIBS_CONTAINER_PATH = "/mnt/libs";
26
+ export declare const REFS_CONTAINER_PATH = "/mnt/refs";
20
27
  export interface MountPlanCoords {
21
28
  analysisId: string;
22
29
  runId: string;
@@ -18,8 +18,13 @@
18
18
  */
19
19
  import { assertSafeId } from "../workspace/paths.js";
20
20
  export const STEP_SUBDIRS = ["output", "scripts", "figures", "logs", "notebooks"];
21
- const LIBS_CONTAINER_PATH = "/mnt/libs";
22
- const REFS_CONTAINER_PATH = "/mnt/refs";
21
+ /**
22
+ * Container mountpoints of the two read-only stores. Exported because they are
23
+ * also the discovery tools' default read root: a host whose own process sees a
24
+ * store the way the sandbox does configures nothing further.
25
+ */
26
+ export const LIBS_CONTAINER_PATH = "/mnt/libs";
27
+ export const REFS_CONTAINER_PATH = "/mnt/refs";
23
28
  /**
24
29
  * The step tail beneath an analysis's workspace root, in both container and
25
30
  * PVC-relative space. The harness owns this layout (workspace-layout spec).