@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.
- package/README.md +1 -2
- package/dist/agents/conversation-agent.d.ts +0 -9
- package/dist/agents/sandbox/__fixtures__/deps.js +6 -0
- package/dist/agents/sandbox/data-profiler.js +5 -4
- package/dist/agents/sandbox/shared.js +11 -0
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/data-profile-orientation.js +30 -1
- package/dist/app/data-profile-policy.d.ts +33 -7
- package/dist/app/data-profile-policy.js +31 -9
- package/dist/contracts/chat-parts.d.ts +29 -33
- package/dist/contracts/content-url.d.ts +7 -41
- package/dist/contracts/content-url.js +7 -48
- package/dist/contracts/data-profile.d.ts +166 -0
- package/dist/contracts/index.d.ts +3 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/message.d.ts +1 -1
- package/dist/contracts/message.js +1 -1
- package/dist/contracts/part-registry.js +2 -3
- package/dist/contracts/schemas/chat-parts.d.ts +16 -53
- package/dist/contracts/schemas/chat-parts.js +15 -25
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/input-signature.d.ts +33 -0
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/run-authorizer.d.ts +1 -1
- package/dist/execution/run-authorizer.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -2
- package/dist/input-scan/coverage.d.ts +22 -0
- package/dist/input-scan/coverage.js +85 -0
- package/dist/input-scan/enrich.d.ts +41 -0
- package/dist/input-scan/enrich.js +227 -0
- package/dist/input-scan/formats.d.ts +41 -0
- package/dist/input-scan/formats.js +279 -0
- package/dist/input-scan/index.d.ts +12 -0
- package/dist/input-scan/index.js +11 -0
- package/dist/input-scan/scan.d.ts +50 -0
- package/dist/input-scan/scan.js +182 -0
- package/dist/input-scan/shapes.d.ts +46 -0
- package/dist/input-scan/shapes.js +0 -0
- package/dist/input-scan/types.d.ts +172 -0
- package/dist/input-scan/types.js +12 -0
- package/dist/lib/zod-issues.d.ts +2 -3
- package/dist/lib/zod-issues.js +2 -3
- package/dist/loop/__fixtures__/scripted-provider.js +3 -1
- package/dist/loop/run-agent.d.ts +5 -0
- package/dist/loop/run-agent.js +10 -1
- package/dist/loop/run-to-terminal.d.ts +1 -1
- package/dist/loop/run-to-terminal.js +1 -1
- package/dist/memory/card-builders.d.ts +0 -27
- package/dist/memory/card-builders.js +0 -75
- package/dist/memory/conversation-display-backfill.d.ts +2 -4
- package/dist/memory/conversation-display-backfill.js +3 -9
- package/dist/memory/conversation-display-storage.d.ts +3 -4
- package/dist/memory/conversation-display-storage.js +3 -4
- package/dist/memory/reconstruct-cards.d.ts +5 -7
- package/dist/memory/reconstruct-cards.js +6 -20
- package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
- package/dist/prompts/sandbox/data-profiler.js +159 -275
- package/dist/provenance/exec-frame.js +9 -3
- package/dist/runtime/boot.js +2 -12
- package/dist/sandbox/client.d.ts +7 -0
- package/dist/sandbox/create-sandbox.js +1 -0
- package/dist/sandbox/docker-client.d.ts +1 -0
- package/dist/sandbox/docker-client.js +20 -15
- package/dist/sandbox/k8s-client.d.ts +6 -5
- package/dist/sandbox/k8s-client.js +55 -39
- package/dist/sandbox/reaper.d.ts +24 -13
- package/dist/sandbox/reaper.js +47 -14
- package/dist/sandbox/types.d.ts +6 -5
- package/dist/schemas/data-profile-schemas.d.ts +62 -0
- package/dist/schemas/data-profile-schemas.js +68 -2
- package/dist/state/data-profile.d.ts +7 -88
- package/dist/state/index.d.ts +1 -1
- package/dist/tasks/data-profile-activity.d.ts +8 -0
- package/dist/tasks/data-profile-activity.js +1 -0
- package/dist/tasks/data-profile-index.d.ts +45 -0
- package/dist/tasks/data-profile-index.js +134 -0
- package/dist/tasks/data-profile.d.ts +8 -23
- package/dist/tasks/data-profile.js +150 -117
- package/dist/tools/report-session/preview-report.d.ts +2 -1
- package/dist/tools/report-session/preview-report.js +9 -1
- package/dist/tools/research/generate-plan.js +1 -1
- package/dist/tools/research/inspect-data-profile.d.ts +30 -4
- package/dist/tools/research/inspect-data-profile.js +66 -10
- package/dist/tools/start-report-session.js +2 -2
- package/dist/tools/workspace/scan-inputs.d.ts +38 -0
- package/dist/tools/workspace/scan-inputs.js +61 -0
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/tools/workspace/workspace-search.js +12 -9
- package/dist/workspace/filesystem.d.ts +27 -0
- package/dist/workspace/filesystem.js +27 -0
- package/dist/workspace/paths.d.ts +0 -21
- package/dist/workspace/paths.js +0 -37
- package/package.json +1 -3
- package/dist/agents/report-builder.d.ts +0 -34
- package/dist/agents/report-builder.js +0 -34
- package/dist/execution/report-render.d.ts +0 -39
- package/dist/execution/report-render.js +0 -98
- package/dist/execution/report-runner.d.ts +0 -111
- package/dist/execution/report-runner.js +0 -230
- package/dist/prompts/report-builder.d.ts +0 -1
- package/dist/prompts/report-builder.js +0 -174
- package/dist/tools/iterate-report.d.ts +0 -227
- package/dist/tools/iterate-report.js +0 -883
- package/dist/tools/lib/preview-lock.d.ts +0 -20
- package/dist/tools/lib/preview-lock.js +0 -35
- package/dist/tools/lib/report-preflight.d.ts +0 -39
- package/dist/tools/lib/report-preflight.js +0 -283
- package/dist/tools/report/build-report.d.ts +0 -13
- package/dist/tools/report/build-report.js +0 -27
- package/dist/tools/report/index.d.ts +0 -15
- package/dist/tools/report/index.js +0 -15
- package/dist/tools/report/mint-preview-url.d.ts +0 -26
- package/dist/tools/report/mint-preview-url.js +0 -47
- package/dist/tools/report/preview-publisher.d.ts +0 -53
- package/dist/tools/report/preview-publisher.js +0 -45
- package/dist/tools/report/preview-snapshot.d.ts +0 -34
- package/dist/tools/report/preview-snapshot.js +0 -98
- package/dist/tools/report/submit-report.d.ts +0 -26
- package/dist/tools/report/submit-report.js +0 -148
- package/dist/tools/report/version-fs.d.ts +0 -15
- 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 {};
|
package/dist/lib/zod-issues.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/lib/zod-issues.js
CHANGED
|
@@ -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
|
|
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:
|
|
20
|
+
finishReason: mapped,
|
|
21
|
+
...(mapped === finishReason ? {} : { rawFinishReason: finishReason }),
|
|
20
22
|
usage,
|
|
21
23
|
};
|
|
22
24
|
}
|
package/dist/loop/run-agent.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -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 {
|
|
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`, `
|
|
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`, `
|
|
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
|
|
21
|
-
* that
|
|
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
|
*/
|