@inflexa-ai/harness 0.23.0 → 0.25.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 (123) hide show
  1. package/README.md +1 -2
  2. package/dist/agents/conversation-agent.d.ts +0 -9
  3. package/dist/agents/sandbox/__fixtures__/deps.js +6 -0
  4. package/dist/agents/sandbox/data-profiler.js +5 -4
  5. package/dist/agents/sandbox/shared.js +11 -0
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/app/data-profile-orientation.js +30 -1
  8. package/dist/app/data-profile-policy.d.ts +33 -7
  9. package/dist/app/data-profile-policy.js +31 -9
  10. package/dist/contracts/chat-parts.d.ts +29 -33
  11. package/dist/contracts/content-url.d.ts +7 -41
  12. package/dist/contracts/content-url.js +7 -48
  13. package/dist/contracts/data-profile.d.ts +166 -0
  14. package/dist/contracts/index.d.ts +3 -2
  15. package/dist/contracts/index.js +1 -1
  16. package/dist/contracts/message.d.ts +1 -1
  17. package/dist/contracts/message.js +1 -1
  18. package/dist/contracts/part-registry.js +2 -3
  19. package/dist/contracts/schemas/chat-parts.d.ts +16 -53
  20. package/dist/contracts/schemas/chat-parts.js +15 -25
  21. package/dist/contracts/schemas/index.d.ts +1 -1
  22. package/dist/contracts/schemas/index.js +1 -1
  23. package/dist/execution/input-signature.d.ts +33 -0
  24. package/dist/execution/input-signature.js +0 -0
  25. package/dist/execution/run-authorizer.d.ts +1 -1
  26. package/dist/execution/run-authorizer.js +1 -1
  27. package/dist/index.d.ts +5 -4
  28. package/dist/index.js +5 -2
  29. package/dist/input-scan/coverage.d.ts +22 -0
  30. package/dist/input-scan/coverage.js +85 -0
  31. package/dist/input-scan/enrich.d.ts +41 -0
  32. package/dist/input-scan/enrich.js +227 -0
  33. package/dist/input-scan/formats.d.ts +41 -0
  34. package/dist/input-scan/formats.js +279 -0
  35. package/dist/input-scan/index.d.ts +12 -0
  36. package/dist/input-scan/index.js +11 -0
  37. package/dist/input-scan/scan.d.ts +50 -0
  38. package/dist/input-scan/scan.js +182 -0
  39. package/dist/input-scan/shapes.d.ts +46 -0
  40. package/dist/input-scan/shapes.js +0 -0
  41. package/dist/input-scan/types.d.ts +172 -0
  42. package/dist/input-scan/types.js +12 -0
  43. package/dist/lib/zod-issues.d.ts +2 -3
  44. package/dist/lib/zod-issues.js +2 -3
  45. package/dist/loop/__fixtures__/scripted-provider.js +3 -1
  46. package/dist/loop/run-agent.d.ts +5 -0
  47. package/dist/loop/run-agent.js +10 -1
  48. package/dist/loop/run-to-terminal.d.ts +1 -1
  49. package/dist/loop/run-to-terminal.js +1 -1
  50. package/dist/memory/card-builders.d.ts +0 -27
  51. package/dist/memory/card-builders.js +0 -75
  52. package/dist/memory/conversation-display-backfill.d.ts +2 -4
  53. package/dist/memory/conversation-display-backfill.js +3 -9
  54. package/dist/memory/conversation-display-storage.d.ts +3 -4
  55. package/dist/memory/conversation-display-storage.js +3 -4
  56. package/dist/memory/reconstruct-cards.d.ts +5 -7
  57. package/dist/memory/reconstruct-cards.js +6 -20
  58. package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
  59. package/dist/prompts/sandbox/data-profiler.js +159 -275
  60. package/dist/provenance/exec-frame.js +9 -3
  61. package/dist/runtime/boot.js +2 -12
  62. package/dist/sandbox/client.d.ts +7 -0
  63. package/dist/sandbox/create-sandbox.js +1 -0
  64. package/dist/sandbox/docker-client.d.ts +1 -0
  65. package/dist/sandbox/docker-client.js +20 -15
  66. package/dist/sandbox/k8s-client.d.ts +6 -5
  67. package/dist/sandbox/k8s-client.js +55 -39
  68. package/dist/sandbox/reaper.d.ts +24 -13
  69. package/dist/sandbox/reaper.js +47 -14
  70. package/dist/sandbox/types.d.ts +6 -5
  71. package/dist/schemas/data-profile-schemas.d.ts +62 -0
  72. package/dist/schemas/data-profile-schemas.js +68 -2
  73. package/dist/state/data-profile.d.ts +7 -88
  74. package/dist/state/index.d.ts +1 -1
  75. package/dist/tasks/data-profile-activity.d.ts +8 -0
  76. package/dist/tasks/data-profile-activity.js +1 -0
  77. package/dist/tasks/data-profile-index.d.ts +45 -0
  78. package/dist/tasks/data-profile-index.js +134 -0
  79. package/dist/tasks/data-profile.d.ts +8 -23
  80. package/dist/tasks/data-profile.js +150 -117
  81. package/dist/tools/report-session/preview-report.d.ts +2 -1
  82. package/dist/tools/report-session/preview-report.js +9 -1
  83. package/dist/tools/research/generate-plan.js +1 -1
  84. package/dist/tools/research/inspect-data-profile.d.ts +30 -4
  85. package/dist/tools/research/inspect-data-profile.js +66 -10
  86. package/dist/tools/start-report-session.js +2 -2
  87. package/dist/tools/workspace/scan-inputs.d.ts +38 -0
  88. package/dist/tools/workspace/scan-inputs.js +61 -0
  89. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  90. package/dist/tools/workspace/workspace-search.js +12 -9
  91. package/dist/workspace/filesystem.d.ts +27 -0
  92. package/dist/workspace/filesystem.js +27 -0
  93. package/dist/workspace/paths.d.ts +0 -21
  94. package/dist/workspace/paths.js +0 -37
  95. package/package.json +1 -3
  96. package/dist/agents/report-builder.d.ts +0 -34
  97. package/dist/agents/report-builder.js +0 -34
  98. package/dist/execution/report-render.d.ts +0 -39
  99. package/dist/execution/report-render.js +0 -98
  100. package/dist/execution/report-runner.d.ts +0 -111
  101. package/dist/execution/report-runner.js +0 -230
  102. package/dist/prompts/report-builder.d.ts +0 -1
  103. package/dist/prompts/report-builder.js +0 -174
  104. package/dist/tools/iterate-report.d.ts +0 -227
  105. package/dist/tools/iterate-report.js +0 -883
  106. package/dist/tools/lib/preview-lock.d.ts +0 -20
  107. package/dist/tools/lib/preview-lock.js +0 -35
  108. package/dist/tools/lib/report-preflight.d.ts +0 -39
  109. package/dist/tools/lib/report-preflight.js +0 -283
  110. package/dist/tools/report/build-report.d.ts +0 -13
  111. package/dist/tools/report/build-report.js +0 -27
  112. package/dist/tools/report/index.d.ts +0 -15
  113. package/dist/tools/report/index.js +0 -15
  114. package/dist/tools/report/mint-preview-url.d.ts +0 -26
  115. package/dist/tools/report/mint-preview-url.js +0 -47
  116. package/dist/tools/report/preview-publisher.d.ts +0 -53
  117. package/dist/tools/report/preview-publisher.js +0 -45
  118. package/dist/tools/report/preview-snapshot.d.ts +0 -34
  119. package/dist/tools/report/preview-snapshot.js +0 -98
  120. package/dist/tools/report/submit-report.d.ts +0 -26
  121. package/dist/tools/report/submit-report.js +0 -148
  122. package/dist/tools/report/version-fs.d.ts +0 -15
  123. package/dist/tools/report/version-fs.js +0 -165
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The input scan — one deterministic pass over a staged input tree.
3
+ *
4
+ * Runs in the harness process over the workspace read seam, which is
5
+ * sandbox-independent: the walk touches names and metadata, and format detection
6
+ * compares a bounded byte prefix against a table. Neither is a decode, so neither
7
+ * needs a container. Only the per-shape header readout (`enrich.ts`) runs a decoder,
8
+ * and it runs in the sandbox.
9
+ *
10
+ * The scan is the SOLE enumeration pass. Before it, the profiler agent discovered
11
+ * structure by issuing one command per input file, so its cost scaled with file
12
+ * count and its step budget did not.
13
+ */
14
+ import { unwrapOrThrow } from "../lib/result.js";
15
+ import { MAGIC_PREFIX_BYTES, detectFormat, extensionChain } from "./formats.js";
16
+ import { observeShapes } from "./shapes.js";
17
+ /**
18
+ * Files walked before the scan reports truncation instead of completing.
19
+ *
20
+ * Header decode is O(shapes), but the walk and its per-file prefix read are O(files),
21
+ * and that arm needs a stated ceiling rather than an implicit one. 100k files is ~30s
22
+ * of stat-and-prefix work against a 20-minute profile deadline, and two orders of
23
+ * magnitude above the largest tree the platform has seen (3513). Past it the scan
24
+ * stops and SAYS it stopped: a silently sampled manifest would describe a fraction of
25
+ * the tree while reading as a description of all of it, which is the failure mode this
26
+ * capability exists to remove.
27
+ */
28
+ export const MAX_SCANNED_FILES = 100_000;
29
+ /** Walk the tree breadth-first, reading a bounded prefix of each file for format detection. */
30
+ async function walk(args) {
31
+ const { session, fs, root, signal } = args;
32
+ const limit = args.limit ?? MAX_SCANNED_FILES;
33
+ const files = [];
34
+ const queue = [root];
35
+ let truncated = false;
36
+ while (queue.length > 0) {
37
+ signal?.throwIfAborted();
38
+ const dir = queue.shift();
39
+ const listing = unwrapOrThrow(await fs.list({ session, path: dir }));
40
+ // A directory that vanished mid-walk, or one the seam refuses, is not a scan
41
+ // failure — the tree is being described, not validated.
42
+ if (listing.kind !== "ok")
43
+ continue;
44
+ for (const entry of listing.entries) {
45
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
46
+ if (entry.type === "directory") {
47
+ queue.push(path);
48
+ continue;
49
+ }
50
+ if (files.length >= limit) {
51
+ truncated = true;
52
+ return { files, truncated };
53
+ }
54
+ const extensions = extensionChain(entry.name);
55
+ const read = unwrapOrThrow(await fs.readBytes({ session, path, length: MAGIC_PREFIX_BYTES }));
56
+ const prefix = read.kind === "ok" ? read.bytes : null;
57
+ const { format, wrapper } = detectFormat({ path, extensions, prefix });
58
+ files.push({
59
+ path,
60
+ size: entry.size ?? 0,
61
+ extensions,
62
+ format,
63
+ ...(wrapper ? { wrapper } : {}),
64
+ });
65
+ }
66
+ }
67
+ return { files, truncated };
68
+ }
69
+ function formatCensus(files) {
70
+ const counts = new Map();
71
+ for (const file of files)
72
+ counts.set(file.format, (counts.get(file.format) ?? 0) + 1);
73
+ return [...counts.entries()].map(([format, count]) => ({ format, count })).sort((a, b) => b.count - a.count || a.format.localeCompare(b.format, "en"));
74
+ }
75
+ /** Assemble a manifest from an already-walked file set. Pure — the seam of the unit tests. */
76
+ export function buildManifest(root, files, truncated, limit = MAX_SCANNED_FILES) {
77
+ const observed = observeShapes(files);
78
+ const manifest = {
79
+ root,
80
+ fileCount: files.length,
81
+ totalBytes: files.reduce((sum, f) => sum + f.size, 0),
82
+ truncated,
83
+ ...(truncated ? { scanLimit: limit } : {}),
84
+ shapesTruncated: observed.shapesTruncated,
85
+ formats: formatCensus(files),
86
+ shapes: observed.shapes,
87
+ valueOverlaps: observed.valueOverlaps,
88
+ unstructured: observed.unstructured,
89
+ };
90
+ return { files, manifest, positionValues: observed.positionValues };
91
+ }
92
+ /** Walk a staged input tree and observe its shapes. No model, no container. */
93
+ export async function scanInputTree(args) {
94
+ const limit = args.limit ?? MAX_SCANNED_FILES;
95
+ const { files, truncated } = await walk(args);
96
+ return buildManifest(args.root, files, truncated, limit);
97
+ }
98
+ function formatBytes(bytes) {
99
+ if (bytes < 1024)
100
+ return `${bytes} B`;
101
+ const units = ["KB", "MB", "GB", "TB"];
102
+ let value = bytes / 1024;
103
+ let unit = 0;
104
+ while (value >= 1024 && unit < units.length - 1) {
105
+ value /= 1024;
106
+ unit++;
107
+ }
108
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
109
+ }
110
+ /**
111
+ * Render a manifest for a briefing or a tool result.
112
+ *
113
+ * Bounded by construction — every list it draws from is already capped — so a tree of
114
+ * three thousand files costs the same context as a tree of thirty. The wording states
115
+ * observations only: it never calls a shape a kind, and it says outright that the
116
+ * grouping decision is the reader's.
117
+ */
118
+ export function renderInputScanManifest(manifest) {
119
+ const lines = [];
120
+ lines.push(`Input scan of ${manifest.root} — ${manifest.fileCount} files, ${formatBytes(manifest.totalBytes)}.`);
121
+ if (manifest.truncated) {
122
+ lines.push(`INCOMPLETE: the walk stopped at its ${manifest.scanLimit ?? MAX_SCANNED_FILES}-file ceiling, so this describes part of the tree.`);
123
+ }
124
+ lines.push(`Formats: ${manifest.formats.map((f) => `${f.format} (${f.count})`).join(", ") || "none"}`);
125
+ lines.push("");
126
+ lines.push("These are OBSERVATIONS of filename structure, not a grouping of the dataset.");
127
+ lines.push("A shape is a set of files whose names differ only at the marked positions; deciding");
128
+ lines.push("what is one kind of data, and what the varying positions mean, is your judgement.");
129
+ if (manifest.shapes.length > 0) {
130
+ lines.push("");
131
+ lines.push(`Shapes (${manifest.shapes.length}${manifest.shapesTruncated ? ", capped — the remainder is in the aggregate below" : ""}):`);
132
+ for (const shape of manifest.shapes) {
133
+ const location = shape.directory ? `${shape.directory}/` : "";
134
+ const wrapper = shape.wrapper ? `, ${shape.wrapper}-compressed` : "";
135
+ lines.push(`- ${shape.id}: ${shape.fileCount} files at ${location}${shape.pattern} (${shape.format}${wrapper}, ${formatBytes(shape.totalBytes)})`);
136
+ for (const position of shape.variablePositions) {
137
+ const prefix = position.prefix ? ` after "${position.prefix}"` : "";
138
+ const shown = position.sampleValues.join(", ");
139
+ const more = position.distinctValues > position.sampleValues.length ? ", …" : "";
140
+ lines.push(` - position <${position.index}>${prefix}: ${position.distinctValues} distinct values — ${shown}${more}`);
141
+ }
142
+ for (const pair of shape.cooccurrence) {
143
+ const crossing = pair.observedPairs === pair.possiblePairs ? "fully crossed" : `partially crossed (${pair.possiblePairs} possible)`;
144
+ lines.push(` - positions <${pair.positions[0]}>×<${pair.positions[1]}>: ${pair.observedPairs} observed combinations, ${crossing}`);
145
+ }
146
+ if (shape.header) {
147
+ const fields = Object.entries(shape.header.fields)
148
+ .map(([key, value]) => `${key}=${value}`)
149
+ .join(", ");
150
+ lines.push(` - header (${shape.header.path}): ${fields || shape.header.unavailable || "no fields read"}`);
151
+ }
152
+ else {
153
+ lines.push(` - example: ${shape.examplePaths[0] ?? "—"}`);
154
+ }
155
+ }
156
+ }
157
+ if (manifest.valueOverlaps.length > 0) {
158
+ lines.push("");
159
+ lines.push("Value overlap between shapes (evidence, NOT an assertion that they share an axis):");
160
+ for (const overlap of manifest.valueOverlaps) {
161
+ const gaps = [];
162
+ if (overlap.onlyInFirst > 0)
163
+ gaps.push(`${overlap.onlyInFirst} only in ${overlap.shapes[0]} (${overlap.onlyInFirstSample.join(", ")})`);
164
+ if (overlap.onlyInSecond > 0)
165
+ gaps.push(`${overlap.onlyInSecond} only in ${overlap.shapes[1]} (${overlap.onlyInSecondSample.join(", ")})`);
166
+ const gapText = gaps.length > 0 ? `; ${gaps.join("; ")}` : "; no gaps";
167
+ lines.push(`- ${overlap.shapes[0]}<${overlap.positions[0]}> vs ${overlap.shapes[1]}<${overlap.positions[1]}>: ` +
168
+ `${overlap.sharedValues} shared values${gapText}`);
169
+ }
170
+ }
171
+ if (manifest.unstructured.count > 0) {
172
+ lines.push("");
173
+ lines.push(`Files sharing no name structure with any other (${manifest.unstructured.count}, ${formatBytes(manifest.unstructured.totalBytes)}):`);
174
+ for (const entry of manifest.unstructured.sample) {
175
+ lines.push(`- ${entry.path} (${entry.format}, ${formatBytes(entry.size)})`);
176
+ }
177
+ if (manifest.unstructured.count > manifest.unstructured.sample.length) {
178
+ lines.push(`- … ${manifest.unstructured.count - manifest.unstructured.sample.length} more not listed`);
179
+ }
180
+ }
181
+ return lines.join("\n");
182
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shape observation — pure functions over filenames and sizes, no I/O.
3
+ *
4
+ * A shape is a mechanical fact: these filenames differ only here. Establishing it
5
+ * needs nothing but names, sizes, and the format already detected, which is why
6
+ * shapes are observed BEFORE any decode and why header enrichment costs O(shapes)
7
+ * rather than O(files).
8
+ *
9
+ * Grouping proceeds in two passes. The first masks digit runs, so `PATIENT_001` and
10
+ * `PATIENT_002` land together while `tumor_001` and `normal_001` do not. The second
11
+ * merges groups that differ in exactly one alphabetic position, which is what makes
12
+ * `tumor`/`normal` one shape with a categorical position rather than two shapes an
13
+ * agent has to notice are related. A file whose structure matches no other survives
14
+ * both passes alone and is reported in the aggregate, never as a shape of one.
15
+ */
16
+ import type { FileShape, ScannedFile, ShapeValueOverlap, UnstructuredFiles } from "./types.js";
17
+ /** Shapes reported before the tail is folded into the aggregate. */
18
+ export declare const MAX_SHAPES = 30;
19
+ /** Values sampled per variable position. The sample is the agent's decision material. */
20
+ export declare const MAX_SAMPLE_VALUES = 20;
21
+ type AtomKind = "alpha" | "digit" | "delim";
22
+ interface Atom {
23
+ readonly kind: AtomKind;
24
+ readonly value: string;
25
+ }
26
+ /**
27
+ * Split a filename stem into runs of letters, runs of digits, and the delimiters
28
+ * between them. Splitting INSIDE a token is what makes `PT001_D0_rep1` three
29
+ * positions rather than three opaque tokens — the nested design is only visible
30
+ * once `PT` and `001` are separate.
31
+ */
32
+ export declare function tokenizeStem(stem: string): readonly Atom[];
33
+ /** The name stem — the basename with its extension chain removed. */
34
+ export declare function nameStem(fileName: string): string;
35
+ export interface ObservedShapes {
36
+ readonly shapes: readonly FileShape[];
37
+ readonly unstructured: UnstructuredFiles;
38
+ readonly valueOverlaps: readonly ShapeValueOverlap[];
39
+ /** Complete per-position value sets, keyed by shape id — the manifest carries only a sample. */
40
+ readonly positionValues: ReadonlyMap<string, readonly (readonly string[])[]>;
41
+ /** True when the shape list was capped and the remainder folded into the aggregate. */
42
+ readonly shapesTruncated: boolean;
43
+ }
44
+ /** Observe the shapes of a scanned file set. Pure — no I/O, no container, no model. */
45
+ export declare function observeShapes(files: readonly ScannedFile[]): ObservedShapes;
46
+ export {};
Binary file
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Input-scan vocabulary.
3
+ *
4
+ * The scan reports what is OBSERVABLE about a staged input tree; it never decides
5
+ * how the tree is grouped (see the input-scan-manifest spec). That separation is
6
+ * carried by the vocabulary itself: a shape is "these filenames differ only here",
7
+ * a kind is "these files are the same sort of thing", and only the profiler agent
8
+ * can establish the second. Nothing here is named `kinds` or `axes`, and no type
9
+ * here is assignable to the profiler's output — a manifest an agent could copy
10
+ * into its submission would make its central judgement invisible.
11
+ */
12
+ /** One file the walk found. `path` is relative to the analysis root. */
13
+ export interface ScannedFile {
14
+ readonly path: string;
15
+ readonly size: number;
16
+ /** Trailing extension components, outermost last: `sample.vcf.gz` → `["vcf", "gz"]`. */
17
+ readonly extensions: readonly string[];
18
+ /** Format identified from magic bytes, falling back to the extension chain, else `"unknown"`. */
19
+ readonly format: string;
20
+ /** Compression wrapper reported ALONGSIDE the inner format, never in place of it. */
21
+ readonly wrapper?: string;
22
+ }
23
+ /**
24
+ * A filename position whose token differs across a shape's members.
25
+ *
26
+ * `sampleValues` is normative, not decoration: a count alone cannot distinguish an
27
+ * identifier (`0001`…`1171`) from a categorical label (`tumor`/`normal`) from a shard
28
+ * index (`chr1`…`chr22`), and those readings imply entirely different kinds.
29
+ */
30
+ export interface VariablePosition {
31
+ /** Token index within the shape's name pattern — the `<n>` placeholder it fills. */
32
+ readonly index: number;
33
+ /** Literal text immediately preceding the token, when any (`PT` in `PT001`). */
34
+ readonly prefix?: string;
35
+ readonly distinctValues: number;
36
+ /** Bounded sample of the values observed at this position. */
37
+ readonly sampleValues: readonly string[];
38
+ }
39
+ /**
40
+ * How two variable positions vary together. A fully-crossed pair is a nested design
41
+ * (every subject × every timepoint); a partially-crossed one is not, and the
42
+ * difference is design evidence a flat file count destroys.
43
+ */
44
+ export interface PositionCooccurrence {
45
+ readonly positions: readonly [number, number];
46
+ /** Distinct value pairs observed. */
47
+ readonly observedPairs: number;
48
+ /** Pairs the two value sets could form. `observedPairs === possiblePairs` is a full crossing. */
49
+ readonly possiblePairs: number;
50
+ }
51
+ /** Header fields read from a bounded prefix of one member of a shape. */
52
+ export interface HeaderReadout {
53
+ /** The member the readout came from. */
54
+ readonly path: string;
55
+ /** Decoder-reported fields, flat and bounded (`delimiter`, `columns`, `sampleCount`, …). */
56
+ readonly fields: Readonly<Record<string, string | number | boolean>>;
57
+ /** Why no readout is present, when the decode did not produce one. */
58
+ readonly unavailable?: string;
59
+ }
60
+ /**
61
+ * A set of files that are mechanically indistinguishable by name structure, format,
62
+ * and location. A shape is an observation, never a grouping of the dataset: the
63
+ * agent may split one shape into several kinds, cover several shapes with one kind,
64
+ * or ignore the shape entirely.
65
+ */
66
+ export interface FileShape {
67
+ /** Stable within one manifest (`shape-1`, `shape-2`, …) so overlaps can name shapes. */
68
+ readonly id: string;
69
+ /** Directory the members share, relative to the analysis root. */
70
+ readonly directory: string;
71
+ /** Name pattern with each variable position rendered as `<n>`. */
72
+ readonly pattern: string;
73
+ readonly format: string;
74
+ readonly wrapper?: string;
75
+ readonly extensions: readonly string[];
76
+ readonly fileCount: number;
77
+ readonly totalBytes: number;
78
+ readonly variablePositions: readonly VariablePosition[];
79
+ readonly cooccurrence: readonly PositionCooccurrence[];
80
+ /** Bounded sample of member paths. */
81
+ readonly examplePaths: readonly string[];
82
+ /** Present once sandbox enrichment has run for this shape. */
83
+ readonly header?: HeaderReadout;
84
+ }
85
+ /**
86
+ * Value-set overlap between two shapes' variable positions, reported WITH its gaps.
87
+ *
88
+ * Correspondence across shapes is heuristic — it depends on stripping shape-specific
89
+ * text before comparing, and near-miss namings defeat it. Reported as evidence it is
90
+ * useful; asserted as a shared axis it is a guess a consumer cannot tell was guessed.
91
+ */
92
+ export interface ShapeValueOverlap {
93
+ readonly shapes: readonly [string, string];
94
+ readonly positions: readonly [number, number];
95
+ readonly sharedValues: number;
96
+ /** Values present in the first shape and absent from the second. */
97
+ readonly onlyInFirst: number;
98
+ readonly onlyInSecond: number;
99
+ /** Bounded samples of the two gaps, so a near-complete correspondence names its misses. */
100
+ readonly onlyInFirstSample: readonly string[];
101
+ readonly onlyInSecondSample: readonly string[];
102
+ }
103
+ /** One member of the no-shared-structure aggregate. */
104
+ export interface UnstructuredEntry {
105
+ readonly path: string;
106
+ readonly size: number;
107
+ readonly format: string;
108
+ }
109
+ /**
110
+ * Files whose name structure matches no other file, in aggregate.
111
+ *
112
+ * Never one shape per file: a tree of arbitrarily named files would otherwise produce
113
+ * a shape count proportional to the file count, which is the unbounded output this
114
+ * capability exists to prevent. Whether these are notable singletons worth prose or
115
+ * an unclassified remainder is the agent's determination.
116
+ */
117
+ export interface UnstructuredFiles {
118
+ readonly count: number;
119
+ readonly totalBytes: number;
120
+ /**
121
+ * Bounded sample covering distinct formats first, then the largest files. A
122
+ * size-ordered sample would bury the metadata sheet, the README, and the paper —
123
+ * one file each — under whatever large files failed to group.
124
+ */
125
+ readonly sample: readonly UnstructuredEntry[];
126
+ }
127
+ /** How many files carry each detected format, across the whole scanned tree. */
128
+ export interface FormatCount {
129
+ readonly format: string;
130
+ readonly count: number;
131
+ }
132
+ /**
133
+ * The agent-facing manifest: bounded by construction, so injecting it into a briefing
134
+ * or returning it from `scan_inputs` costs a fixed amount of context whatever the tree
135
+ * size. The complete per-file list lives on {@link InputScan}, host-side.
136
+ */
137
+ export interface InputScanManifest {
138
+ /** Path scanned, relative to the analysis root. */
139
+ readonly root: string;
140
+ readonly fileCount: number;
141
+ readonly totalBytes: number;
142
+ /**
143
+ * True when the walk hit its file ceiling and stopped. The shapes below then
144
+ * describe a prefix of the tree, not all of it — stated rather than silently
145
+ * sampled.
146
+ */
147
+ readonly truncated: boolean;
148
+ /** The ceiling that bound, when `truncated`. */
149
+ readonly scanLimit?: number;
150
+ /** True when the shape list itself was capped; the remainder is folded into `unstructured`. */
151
+ readonly shapesTruncated: boolean;
152
+ readonly formats: readonly FormatCount[];
153
+ readonly shapes: readonly FileShape[];
154
+ readonly valueOverlaps: readonly ShapeValueOverlap[];
155
+ readonly unstructured: UnstructuredFiles;
156
+ }
157
+ /**
158
+ * The scan's full result. `files` is the complete enumeration — it stays host-side and
159
+ * backs coverage and the index projection; `manifest` is the bounded projection the
160
+ * agent reads.
161
+ */
162
+ export interface InputScan {
163
+ readonly files: readonly ScannedFile[];
164
+ readonly manifest: InputScanManifest;
165
+ /**
166
+ * Per shape, the COMPLETE value set of each variable position, keyed by shape id.
167
+ * Bounded output belongs to the manifest; the entity tier of the index needs every
168
+ * value, and re-deriving them from filenames downstream would duplicate the
169
+ * tokenisation.
170
+ */
171
+ readonly positionValues: ReadonlyMap<string, readonly (readonly string[])[]>;
172
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Input-scan vocabulary.
3
+ *
4
+ * The scan reports what is OBSERVABLE about a staged input tree; it never decides
5
+ * how the tree is grouped (see the input-scan-manifest spec). That separation is
6
+ * carried by the vocabulary itself: a shape is "these filenames differ only here",
7
+ * a kind is "these files are the same sort of thing", and only the profiler agent
8
+ * can establish the second. Nothing here is named `kinds` or `axes`, and no type
9
+ * here is assignable to the profiler's output — a manifest an agent could copy
10
+ * into its submission would make its central judgement invisible.
11
+ */
12
+ export {};
@@ -13,10 +13,9 @@
13
13
  * ` ```json {…}``` `). The payload is complete; only the wrapper makes it a
14
14
  * string.
15
15
  *
16
- * This module owns both halves of the response for the four model-facing
16
+ * This module owns both halves of the response for the three model-facing
17
17
  * validation sites (`loop/run-agent.ts`'s tool-input boundary,
18
- * `execution/run-synthesis.ts`, `tools/research/generate-plan.ts`,
19
- * `tools/iterate-report.ts`):
18
+ * `execution/run-synthesis.ts`, `tools/research/generate-plan.ts`):
20
19
  *
21
20
  * - `hintForZodIssue` — a **diagnostic only**. It parses nothing into the
22
21
  * caller's data; it distinguishes the two modes so the message the model
@@ -13,10 +13,9 @@
13
13
  * ` ```json {…}``` `). The payload is complete; only the wrapper makes it a
14
14
  * string.
15
15
  *
16
- * This module owns both halves of the response for the four model-facing
16
+ * This module owns both halves of the response for the three model-facing
17
17
  * validation sites (`loop/run-agent.ts`'s tool-input boundary,
18
- * `execution/run-synthesis.ts`, `tools/research/generate-plan.ts`,
19
- * `tools/iterate-report.ts`):
18
+ * `execution/run-synthesis.ts`, `tools/research/generate-plan.ts`):
20
19
  *
21
20
  * - `hintForZodIssue` — a **diagnostic only**. It parses nothing into the
22
21
  * caller's data; it distinguishes the two modes so the message the model
@@ -14,9 +14,11 @@ function mapFinishReason(finishReason) {
14
14
  }
15
15
  }
16
16
  export function makeMessage(content, finishReason, usage) {
17
+ const mapped = mapFinishReason(finishReason);
17
18
  return {
18
19
  message: { role: "assistant", content },
19
- finishReason: mapFinishReason(finishReason),
20
+ finishReason: mapped,
21
+ ...(mapped === finishReason ? {} : { rawFinishReason: finishReason }),
20
22
  usage,
21
23
  };
22
24
  }
@@ -8,6 +8,11 @@ import { type AgentRunUsage } from "./metrics.js";
8
8
  import type { AgentDefinition, EmitFn, LoopMessage, RunStep } from "./types.js";
9
9
  export interface AgentFinish {
10
10
  readonly reason: FinishReason | "aborted" | "max_iterations" | "denied";
11
+ /**
12
+ * The endpoint's own word for the stop, when the provider captured one —
13
+ * `reason` normalizes it (an Anthropic `refusal` becomes `content-filter`).
14
+ */
15
+ readonly rawFinishReason?: string;
11
16
  readonly cappedOut: boolean;
12
17
  readonly truncationRecoveries: number;
13
18
  /**
@@ -307,7 +307,16 @@ export async function runAgent(agent, initial, session, opts) {
307
307
  await emit({ type: "iteration", source, index: i, final: true });
308
308
  recordAgentRun({ agentId: agent.id, iterations, cappedOut: false, usage });
309
309
  logFinish("info", reply.finishReason, false);
310
- return { messages, finish: { reason: reply.finishReason, cappedOut: false, truncationRecoveries, ...finishUsage() } };
310
+ return {
311
+ messages,
312
+ finish: {
313
+ reason: reply.finishReason,
314
+ ...(reply.rawFinishReason === undefined ? {} : { rawFinishReason: reply.rawFinishReason }),
315
+ cappedOut: false,
316
+ truncationRecoveries,
317
+ ...finishUsage(),
318
+ },
319
+ };
311
320
  }
312
321
  await emit({ type: "iteration", source, index: i, final: false });
313
322
  log.debug("iteration", { iteration: i, tools: toolCalls.map((t) => t.toolName) });
@@ -2,7 +2,7 @@
2
2
  * `runToTerminal` — terminal-salvage wrapper around `runAgent`.
3
3
  *
4
4
  * Some agents communicate their result EXCLUSIVELY through a terminal tool
5
- * (`submit_plan`, `submit_report`, `submit_profile`, `submit_synthesis`, …):
5
+ * (`submit_plan`, `submit_profile`, `submit_synthesis`, …):
6
6
  * the loop's text reply is discarded and the outcome is read from a closure
7
7
  * cell the terminal tool writes. Plain `runAgent` ends a capped run on a
8
8
  * tool-LESS wrap-up turn, so an agent that burns its whole iteration budget —
@@ -2,7 +2,7 @@
2
2
  * `runToTerminal` — terminal-salvage wrapper around `runAgent`.
3
3
  *
4
4
  * Some agents communicate their result EXCLUSIVELY through a terminal tool
5
- * (`submit_plan`, `submit_report`, `submit_profile`, `submit_synthesis`, …):
5
+ * (`submit_plan`, `submit_profile`, `submit_synthesis`, …):
6
6
  * the loop's text reply is discarded and the outcome is read from a closure
7
7
  * cell the terminal tool writes. Plain `runAgent` ends a capped run on a
8
8
  * tool-LESS wrap-up turn, so an agent that burns its whole iteration budget —
@@ -39,14 +39,6 @@ export interface RunCardData {
39
39
  title: string;
40
40
  stepCount: number;
41
41
  }
42
- export interface PreviewCardData {
43
- id: string;
44
- previewId: string;
45
- version: number;
46
- title: string;
47
- previewPath: string;
48
- format: "html" | "pdf";
49
- }
50
42
  /** Build the `data-plan` payload by loading the stored plan. `ok(null)` when
51
43
  * the plan is missing or no longer parses (absence is NOT an error) — the
52
44
  * caller falls back to a chip. */
@@ -100,22 +92,3 @@ export declare function fileGroupHash(paths: string[], title: string | undefined
100
92
  * path the live tool rejected would be resurrected into a card on reload.
101
93
  */
102
94
  export declare function buildFileReferenceCardData(input: Record<string, unknown>): FileReferenceCardData | null;
103
- /**
104
- * Build the `data-report-preview` payload for an `iterate_report` tool_use. The
105
- * preview's source of truth is the filesystem under
106
- * `previews/{previewId}/`: directory layout yields `version`
107
- * (latest `vN`) + `previewPath`, and `preview-meta.json` yields title/format.
108
- * Mirrors `buildRunCardData` — the live emit knows the server-generated
109
- * `previewId`, while the reconstruct-on-read path resolves it (from the
110
- * iteration's `previewId` input, or else the analysis's latest preview). The
111
- * card is keyed by `previewId` only, so every iteration's tool_use rebuilds
112
- * the same card and the frontend groups them into one with all versions selectable.
113
- * Null when nothing renders — the caller falls back to a chip.
114
- */
115
- export declare function buildPreviewCardData(
116
- /** Absolute host root of the analysis's workspace tree (previews live inside it). */
117
- workspaceRoot: string, opts: {
118
- previewId?: string;
119
- title?: string;
120
- format?: "html" | "pdf";
121
- }): Promise<PreviewCardData | null>;
@@ -16,17 +16,12 @@
16
16
  * under the part `type` directly.
17
17
  */
18
18
  import { createHash } from "node:crypto";
19
- import { readdir, readFile, stat } from "node:fs/promises";
20
- import { join } from "node:path";
21
19
  import { okAsync } from "neverthrow";
22
20
  import { tryQuery } from "../lib/db-result.js";
23
- import { tryFs } from "../lib/fs-result.js";
24
21
  import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
25
22
  import { loadPlan } from "../state/index.js";
26
23
  import { normalizeEchartSpec } from "../tools/display/normalize-echart-spec.js";
27
24
  import { validatePath } from "../tools/lib/path-validation.js";
28
- import { latestPreviewVersion, previewDir, PREVIEWS_ROOT } from "../workspace/paths.js";
29
- const PREVIEW_META_FILE = "preview-meta.json";
30
25
  function isRecord(value) {
31
26
  return typeof value === "object" && value !== null && !Array.isArray(value);
32
27
  }
@@ -176,73 +171,3 @@ export function buildFileReferenceCardData(input) {
176
171
  const id = `pres-${fileGroupHash(files.map((f) => f.path), title)}`;
177
172
  return { id, ...(title !== undefined ? { title } : {}), files };
178
173
  }
179
- /** The stored preview metadata, or `null` when the file is absent — a malformed
180
- * file is a genuine read failure (`err`), folded to `null` at the call site. */
181
- function readPreviewMeta(previewRootAbs) {
182
- const path = join(previewRootAbs, PREVIEW_META_FILE);
183
- return tryFs("cardBuilders.readPreviewMeta", async () => JSON.parse(await readFile(path, "utf8")), { path, onAbsent: () => null });
184
- }
185
- /** Directory names (not files) directly under `dir`, or `[]` when absent. */
186
- function subdirs(dir) {
187
- return tryFs("cardBuilders.subdirs", async () => (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name), {
188
- path: dir,
189
- onAbsent: () => [],
190
- });
191
- }
192
- /** The most-recently-modified previewId under an analysis, or null when none. */
193
- function latestPreviewId(previewsRootAbs) {
194
- return subdirs(previewsRootAbs).map(async (dirs) => {
195
- if (dirs.length <= 1)
196
- return dirs[0] ?? null;
197
- let best = null;
198
- for (const name of dirs) {
199
- const path = join(previewsRootAbs, name);
200
- const mtimeMs = await tryFs("cardBuilders.latestPreviewId.stat", async () => (await stat(path)).mtimeMs, { path }).unwrapOr(null); // skip an unreadable/absent entry
201
- if (mtimeMs !== null && (!best || mtimeMs > best.mtimeMs)) {
202
- best = { name, mtimeMs };
203
- }
204
- }
205
- return best?.name ?? dirs[0];
206
- });
207
- }
208
- /**
209
- * Build the `data-report-preview` payload for an `iterate_report` tool_use. The
210
- * preview's source of truth is the filesystem under
211
- * `previews/{previewId}/`: directory layout yields `version`
212
- * (latest `vN`) + `previewPath`, and `preview-meta.json` yields title/format.
213
- * Mirrors `buildRunCardData` — the live emit knows the server-generated
214
- * `previewId`, while the reconstruct-on-read path resolves it (from the
215
- * iteration's `previewId` input, or else the analysis's latest preview). The
216
- * card is keyed by `previewId` only, so every iteration's tool_use rebuilds
217
- * the same card and the frontend groups them into one with all versions selectable.
218
- * Null when nothing renders — the caller falls back to a chip.
219
- */
220
- export async function buildPreviewCardData(
221
- /** Absolute host root of the analysis's workspace tree (previews live inside it). */
222
- workspaceRoot, opts) {
223
- const previewsRootAbs = join(workspaceRoot, PREVIEWS_ROOT);
224
- const resolveId = opts.previewId ? okAsync(opts.previewId) : latestPreviewId(previewsRootAbs);
225
- return resolveId
226
- .andThen((previewId) => {
227
- if (!previewId)
228
- return okAsync(null);
229
- const previewRootAbs = join(workspaceRoot, previewDir(previewId));
230
- return subdirs(previewRootAbs).andThen((versionDirs) => {
231
- const version = latestPreviewVersion(versionDirs);
232
- if (version === 0)
233
- return okAsync(null);
234
- return readPreviewMeta(previewRootAbs).map((meta) => {
235
- const id = `prev-${createHash("sha256").update(previewId).digest("hex").slice(0, 16)}`;
236
- return {
237
- id,
238
- previewId,
239
- version,
240
- title: opts.title ?? meta?.title ?? "Report",
241
- previewPath: `v${version}/index.html`,
242
- format: opts.format ?? (meta?.format === "pdf" ? "pdf" : "html"),
243
- };
244
- });
245
- });
246
- })
247
- .unwrapOr(null);
248
- }
@@ -1,9 +1,7 @@
1
1
  import type { Pool } from "pg";
2
2
  import type { Tool } from "../tools/define-tool.js";
3
- import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
4
3
  export interface ConversationDisplayBackfillDeps {
5
4
  readonly pool: Pool;
6
- readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
7
5
  /**
8
6
  * The conversation agent's composed roster, used to recover each legacy call's
9
7
  * one-line detail from its persisted input. Tools contributed through the
@@ -17,8 +15,8 @@ export interface ConversationDisplayBackfillDeps {
17
15
  * Freeze the migration renderer's output into display envelopes, once per legacy
18
16
  * turn, so the runtime read path never has to reconstruct anything.
19
17
  *
20
- * Missing mutable resources degrade to a generic, cardless display — a workspace
21
- * that has moved is normal historical absence, not a fault. Malformed stored rows
18
+ * Missing mutable resources degrade to a generic, cardless display — a plan
19
+ * that is absent is normal historical absence, not a fault. Malformed stored rows
22
20
  * and database faults remain startup-fatal: a turn that cannot be migrated must
23
21
  * not be silently skipped and then read as if it had no display.
24
22
  */