@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.
- package/dist/agents/report-session-agent.d.ts +72 -0
- package/dist/agents/report-session-agent.js +87 -0
- package/dist/app/report-session-runtime.d.ts +72 -0
- package/dist/app/report-session-runtime.js +183 -0
- package/dist/app/spawn-report-session.d.ts +32 -5
- package/dist/app/spawn-report-session.js +19 -0
- package/dist/config/environment-stores.d.ts +12 -7
- package/dist/config/resource-limits.d.ts +6 -10
- package/dist/config/resource-limits.js +6 -44
- package/dist/contracts/report-reference.d.ts +4 -0
- package/dist/contracts/report-reference.js +3 -0
- package/dist/index.d.ts +2 -1
- package/dist/lib/chrome.d.ts +14 -0
- package/dist/lib/chrome.js +7 -0
- package/dist/lib/page-capture.d.ts +47 -0
- package/dist/lib/page-capture.js +83 -0
- package/dist/loop/run-agent.js +104 -15
- package/dist/memory/ai-sdk-message-storage.d.ts +9 -4
- package/dist/memory/ai-sdk-message-storage.js +9 -4
- package/dist/memory/count-tokens.js +34 -5
- package/dist/memory/thread-store.d.ts +4 -4
- package/dist/memory/thread-store.js +18 -9
- package/dist/prompts/report-session.d.ts +1 -0
- package/dist/prompts/report-session.js +110 -0
- package/dist/providers/ai-sdk.d.ts +158 -3
- package/dist/providers/ai-sdk.js +234 -23
- package/dist/providers/errors.d.ts +14 -0
- package/dist/providers/errors.js +103 -18
- package/dist/providers/types.d.ts +33 -0
- package/dist/providers/types.js +10 -1
- package/dist/report-model/assert-rules.d.ts +44 -0
- package/dist/report-model/assert-rules.js +97 -0
- package/dist/report-model/draft-hash.d.ts +14 -0
- package/dist/report-model/draft-hash.js +36 -0
- package/dist/report-model/fixture-resolver.js +2 -71
- package/dist/report-model/{mint-snapshot.d.ts → pin-snapshot.d.ts} +7 -7
- package/dist/report-model/{mint-snapshot.js → pin-snapshot.js} +7 -7
- package/dist/report-model/production-resolver.d.ts +124 -0
- package/dist/report-model/production-resolver.js +801 -0
- package/dist/report-model/reference-resolver.d.ts +7 -1
- package/dist/report-model/validate.d.ts +34 -2
- package/dist/report-model/validate.js +37 -17
- package/dist/report-render/page.d.ts +23 -0
- package/dist/report-render/page.js +36 -3
- package/dist/report-render/value-bridge.d.ts +62 -0
- package/dist/report-render/value-bridge.js +82 -0
- package/dist/runtime/assemble.d.ts +39 -0
- package/dist/runtime/assemble.js +67 -6
- package/dist/sandbox/mount-plan.d.ts +7 -0
- package/dist/sandbox/mount-plan.js +7 -2
- package/dist/state/init.js +36 -0
- package/dist/state/purge-analysis.js +1 -0
- package/dist/state/report-session-state.d.ts +139 -0
- package/dist/state/report-session-state.js +177 -0
- package/dist/state/report-versions.d.ts +3 -6
- package/dist/state/report-versions.js +11 -38
- package/dist/state/snapshot-parse.d.ts +34 -0
- package/dist/state/snapshot-parse.js +82 -0
- package/dist/tasks/extract-values-script.d.ts +64 -0
- package/dist/tasks/extract-values-script.js +180 -0
- package/dist/tasks/extract-values.d.ts +124 -0
- package/dist/tasks/extract-values.js +193 -0
- package/dist/tools/ad-hoc-router.d.ts +7 -2
- package/dist/tools/ad-hoc-router.js +2 -1
- package/dist/tools/define-tool.d.ts +29 -0
- package/dist/tools/define-tool.js +26 -0
- package/dist/tools/report/preview-snapshot.d.ts +1 -1
- package/dist/tools/report/preview-snapshot.js +11 -50
- package/dist/tools/report-authoring/authoring-tools.d.ts +181 -24
- package/dist/tools/report-authoring/authoring-tools.js +152 -44
- package/dist/tools/report-session/examine-page.d.ts +90 -0
- package/dist/tools/report-session/examine-page.js +136 -0
- package/dist/tools/report-session/index.d.ts +10 -0
- package/dist/tools/report-session/index.js +10 -0
- package/dist/tools/report-session/preview-report.d.ts +105 -0
- package/dist/tools/report-session/preview-report.js +269 -0
- package/dist/tools/report-session/record-version.d.ts +102 -0
- package/dist/tools/report-session/record-version.js +139 -0
- package/dist/tools/research/generate-plan.js +4 -2
- package/dist/tools/sandbox/list-available-packages.js +2 -1
- package/dist/tools/sandbox/list-available-refs.d.ts +5 -1
- package/dist/tools/sandbox/list-available-refs.js +13 -6
- package/package.json +10 -8
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The render-and-preview tool of a report session.
|
|
3
|
+
*
|
|
4
|
+
* The tool renders the current draft of a thread. It runs the finish first, because a mid-composition
|
|
5
|
+
* draft is not a `ReportDocument`. A gap list returns as data, and no render runs. On a pass the tool
|
|
6
|
+
* resolves each reference through the injected resolver, bridges the resolved values into the render model,
|
|
7
|
+
* and renders the page.
|
|
8
|
+
*
|
|
9
|
+
* The page and its staged image assets land in `report-sessions/{threadId}/` under the workspace root. That
|
|
10
|
+
* namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
|
|
11
|
+
* trees. The result carries the absolute page path, thus a local host shows the page with no seam.
|
|
12
|
+
*
|
|
13
|
+
* The result carries no access grant. `PreviewPublisher` authorizes the URL space
|
|
14
|
+
* `previews/{analysisId}/{previewId}` (`contracts/content-url.ts`), and that space cannot name a page of
|
|
15
|
+
* this tree. A hosted view of a session page is a later capability, with a URL space of its own.
|
|
16
|
+
*
|
|
17
|
+
* Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
|
|
18
|
+
* absence, an unresolvable root at resolver construction, an unresolved reference, a bridge mismatch, a
|
|
19
|
+
* render problem, a figure that escapes the workspace root, a write failure, and a stamp failure. The tool
|
|
20
|
+
* never throws for one of them. When the page
|
|
21
|
+
* lands, the tool stamps the hash of the rendered draft on the session state, thus the eyes and the record
|
|
22
|
+
* know which draft the page shows. The filesystem speaks the
|
|
23
|
+
* throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
|
|
24
|
+
* write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
|
|
25
|
+
* control-flow exception propagate.
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
29
|
+
import type { AuthContext } from "../../auth/types.js";
|
|
30
|
+
import { type Logger } from "../../lib/logger.js";
|
|
31
|
+
import { type FinishGap } from "../../report-model/draft-finish.js";
|
|
32
|
+
import type { ReferenceResolver } from "../../report-model/reference-resolver.js";
|
|
33
|
+
import { type ResolutionFailure } from "../../report-model/validate.js";
|
|
34
|
+
import { type BridgeMismatch } from "../../report-render/value-bridge.js";
|
|
35
|
+
import type { RenderProblem } from "../../report-render/types.js";
|
|
36
|
+
import { type Tool } from "../define-tool.js";
|
|
37
|
+
import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
|
|
38
|
+
/** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
|
|
39
|
+
declare const previewReportInput: z.ZodObject<{}, z.core.$strip>;
|
|
40
|
+
export type PreviewReportInput = z.infer<typeof previewReportInput>;
|
|
41
|
+
/**
|
|
42
|
+
* The typed outcome of the preview tool. Each arm is ok-channel data, thus the tool never throws for a
|
|
43
|
+
* degraded condition. `rendered` carries the absolute page path.
|
|
44
|
+
*/
|
|
45
|
+
export type PreviewReportResult = {
|
|
46
|
+
outcome: "refused";
|
|
47
|
+
refusal: SessionRefusal;
|
|
48
|
+
} | {
|
|
49
|
+
outcome: "gaps";
|
|
50
|
+
gaps: FinishGap[];
|
|
51
|
+
} | {
|
|
52
|
+
outcome: "resolver-unavailable";
|
|
53
|
+
} | {
|
|
54
|
+
outcome: "root-unresolvable";
|
|
55
|
+
detail: string;
|
|
56
|
+
} | {
|
|
57
|
+
outcome: "unresolved-references";
|
|
58
|
+
unresolved: ResolutionFailure[];
|
|
59
|
+
} | {
|
|
60
|
+
outcome: "bridge-mismatch";
|
|
61
|
+
mismatches: BridgeMismatch[];
|
|
62
|
+
} | {
|
|
63
|
+
outcome: "render-problems";
|
|
64
|
+
problems: RenderProblem[];
|
|
65
|
+
} | {
|
|
66
|
+
outcome: "figure-out-of-scope";
|
|
67
|
+
blockId: string;
|
|
68
|
+
path: string;
|
|
69
|
+
} | {
|
|
70
|
+
outcome: "write-failed";
|
|
71
|
+
detail: string;
|
|
72
|
+
} | {
|
|
73
|
+
outcome: "stamp-failed";
|
|
74
|
+
pagePath: string;
|
|
75
|
+
detail: string;
|
|
76
|
+
} | {
|
|
77
|
+
outcome: "rendered";
|
|
78
|
+
pagePath: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The construction deps of the preview tool.
|
|
82
|
+
*
|
|
83
|
+
* `resolveWorkspaceRoot` maps the analysis of the call onto its workspace root, thus one singleton tool
|
|
84
|
+
* serves every analysis and it resolves the root per call from the scope. `makeResolver` is optional, because
|
|
85
|
+
* a resolver realization can be absent. It binds one analysis, thus the tool makes the resolver over the
|
|
86
|
+
* scope of the call.
|
|
87
|
+
*/
|
|
88
|
+
export interface PreviewReportToolDeps {
|
|
89
|
+
readonly gateway: ReportSessionStateGateway;
|
|
90
|
+
readonly makeResolver?: (scope: {
|
|
91
|
+
analysisId: string;
|
|
92
|
+
auth: AuthContext;
|
|
93
|
+
}) => ReferenceResolver;
|
|
94
|
+
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
95
|
+
readonly logger?: Logger;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Make the render-and-preview tool over the session-state gateway and the render seams.
|
|
99
|
+
*
|
|
100
|
+
* The tool reads the thread id from the scope of the call, and it loads the state through the gateway. Thus
|
|
101
|
+
* one factory serves every thread. The tool holds no per-session value, and it resolves the root of the
|
|
102
|
+
* analysis of the call.
|
|
103
|
+
*/
|
|
104
|
+
export declare function createPreviewReportTool(deps: PreviewReportToolDeps): Tool<PreviewReportInput, PreviewReportResult>;
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The render-and-preview tool of a report session.
|
|
3
|
+
*
|
|
4
|
+
* The tool renders the current draft of a thread. It runs the finish first, because a mid-composition
|
|
5
|
+
* draft is not a `ReportDocument`. A gap list returns as data, and no render runs. On a pass the tool
|
|
6
|
+
* resolves each reference through the injected resolver, bridges the resolved values into the render model,
|
|
7
|
+
* and renders the page.
|
|
8
|
+
*
|
|
9
|
+
* The page and its staged image assets land in `report-sessions/{threadId}/` under the workspace root. That
|
|
10
|
+
* namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
|
|
11
|
+
* trees. The result carries the absolute page path, thus a local host shows the page with no seam.
|
|
12
|
+
*
|
|
13
|
+
* The result carries no access grant. `PreviewPublisher` authorizes the URL space
|
|
14
|
+
* `previews/{analysisId}/{previewId}` (`contracts/content-url.ts`), and that space cannot name a page of
|
|
15
|
+
* this tree. A hosted view of a session page is a later capability, with a URL space of its own.
|
|
16
|
+
*
|
|
17
|
+
* Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
|
|
18
|
+
* absence, an unresolvable root at resolver construction, an unresolved reference, a bridge mismatch, a
|
|
19
|
+
* render problem, a figure that escapes the workspace root, a write failure, and a stamp failure. The tool
|
|
20
|
+
* never throws for one of them. When the page
|
|
21
|
+
* lands, the tool stamps the hash of the rendered draft on the session state, thus the eyes and the record
|
|
22
|
+
* know which draft the page shows. The filesystem speaks the
|
|
23
|
+
* throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
|
|
24
|
+
* write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
|
|
25
|
+
* control-flow exception propagate.
|
|
26
|
+
*/
|
|
27
|
+
import { err, ok } from "neverthrow";
|
|
28
|
+
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
29
|
+
import { extname, join } from "node:path";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
import { resolveWorkspacePath } from "../../workspace/paths.js";
|
|
32
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
33
|
+
import { describeFsError, tryFsWrite } from "../../lib/fs-result.js";
|
|
34
|
+
import { defaultErrorFields } from "../../lib/logger.js";
|
|
35
|
+
import { computeDraftHash } from "../../report-model/draft-hash.js";
|
|
36
|
+
import { finishDraft } from "../../report-model/draft-finish.js";
|
|
37
|
+
import { resolveDocumentReferences } from "../../report-model/validate.js";
|
|
38
|
+
import { bridgeValues } from "../../report-render/value-bridge.js";
|
|
39
|
+
import { renderReportPage } from "../../report-render/render.js";
|
|
40
|
+
import { defineTool } from "../define-tool.js";
|
|
41
|
+
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
42
|
+
/** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
|
|
43
|
+
const previewReportInput = z.object({});
|
|
44
|
+
/**
|
|
45
|
+
* The staged file name of one bound image: its content hash with the original extension.
|
|
46
|
+
*
|
|
47
|
+
* The colon of the `algorithm:hex` hash is not safe in a file name and in a relative source, thus the slug
|
|
48
|
+
* replaces each non-word run with one dash. The hash is unique, thus two figures with one basename cannot
|
|
49
|
+
* collide on the staged name.
|
|
50
|
+
*/
|
|
51
|
+
function assetFileName(file) {
|
|
52
|
+
const slug = file.hash.replace(/[^a-z0-9]+/gi, "-");
|
|
53
|
+
return slug + extname(file.path);
|
|
54
|
+
}
|
|
55
|
+
/** The relative source of a staged figure. The page directory is self-contained, thus the source is `assets/<name>`. */
|
|
56
|
+
function figureSourcePolicy(file) {
|
|
57
|
+
return `assets/${assetFileName(file)}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Pair each block with the resolved value of its binding, in document order.
|
|
61
|
+
*
|
|
62
|
+
* A value-bearing kind carries the resolved value of its one reference, looked up by the block id from
|
|
63
|
+
* the map that the resolution pass filled. A no-value kind carries none. Where a reference sits in a
|
|
64
|
+
* block is the knowledge of `block-walk.ts`, thus this walk reads the block id alone and never a binding
|
|
65
|
+
* field.
|
|
66
|
+
*
|
|
67
|
+
* The switch is exhaustive over the eight block kinds. A ninth kind reaches the end with no return, and
|
|
68
|
+
* the declared return type fails the build. Thus the walk cannot drop a kind in silence. A value-bearing
|
|
69
|
+
* block reaches here only when its reference resolved, because an unresolved reference short-circuits
|
|
70
|
+
* before this walk.
|
|
71
|
+
*/
|
|
72
|
+
function collectResolutions(blocks, resolvedByBlock) {
|
|
73
|
+
const resolutions = [];
|
|
74
|
+
const visit = (block) => {
|
|
75
|
+
switch (block.kind) {
|
|
76
|
+
case "section":
|
|
77
|
+
resolutions.push({ blockId: block.id, kind: "section" });
|
|
78
|
+
for (const child of block.blocks) {
|
|
79
|
+
visit(child);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
case "text":
|
|
83
|
+
case "claim":
|
|
84
|
+
case "citation":
|
|
85
|
+
resolutions.push({ blockId: block.id, kind: block.kind });
|
|
86
|
+
return;
|
|
87
|
+
case "metric":
|
|
88
|
+
case "table":
|
|
89
|
+
case "chart":
|
|
90
|
+
case "figure": {
|
|
91
|
+
const resolved = resolvedByBlock.get(block.id);
|
|
92
|
+
if (resolved !== undefined) {
|
|
93
|
+
resolutions.push({ blockId: block.id, kind: block.kind, resolved });
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
for (const block of blocks) {
|
|
100
|
+
visit(block);
|
|
101
|
+
}
|
|
102
|
+
return resolutions;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve each reference of the document, and split the resolutions from the unresolved references.
|
|
106
|
+
*
|
|
107
|
+
* The shared `resolveDocumentReferences` walks the tree, resolves each reference under the concurrency
|
|
108
|
+
* bound, and runs the chart-encoding match, thus the preview and the record gate refuse the same
|
|
109
|
+
* references. On a clean pass the resolutions map onto the render model in document order.
|
|
110
|
+
*/
|
|
111
|
+
async function resolveDocument(document, resolver, snapshot) {
|
|
112
|
+
const { resolvedByBlock, failures } = await resolveDocumentReferences(document.sections, snapshot, resolver);
|
|
113
|
+
if (failures.length > 0) {
|
|
114
|
+
return { resolutions: [], unresolved: failures };
|
|
115
|
+
}
|
|
116
|
+
return { resolutions: collectResolutions(document.sections, resolvedByBlock), unresolved: [] };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the page root, stage each bound image, and write the page.
|
|
120
|
+
*
|
|
121
|
+
* The workspace-root seam signals an unresolvable resource by a throw, thus the resolution sits inside
|
|
122
|
+
* the protection and its throw becomes a value. Each `fs` call runs through `tryFsWrite`, the one
|
|
123
|
+
* sanctioned `fs` guard, thus a genuine I/O fault becomes a value and a control-flow exception such as a
|
|
124
|
+
* cancellation propagates.
|
|
125
|
+
*
|
|
126
|
+
* A bound figure names a snapshot path, which is untrusted. A registered `../../` path escapes the root,
|
|
127
|
+
* thus the containment test runs before any copy and reuses the one workspace-path resolver. A source
|
|
128
|
+
* outside the root refuses and names the block, and no copy runs.
|
|
129
|
+
*/
|
|
130
|
+
async function renderToWorkspace(args) {
|
|
131
|
+
let root;
|
|
132
|
+
try {
|
|
133
|
+
root = args.resolveWorkspaceRoot(args.analysisId);
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
return err({ kind: "fs", error: { type: "read_failed", op: "preview.resolveWorkspaceRoot", path: args.analysisId, cause } });
|
|
137
|
+
}
|
|
138
|
+
const sessionDir = join(root, "report-sessions", args.threadId);
|
|
139
|
+
const assetsDir = join(sessionDir, "assets");
|
|
140
|
+
const pagePath = join(sessionDir, "index.html");
|
|
141
|
+
// Each bound figure, contained and deduplicated by its staged name, mapped to its host source.
|
|
142
|
+
const sources = new Map();
|
|
143
|
+
for (const resolution of args.resolutions) {
|
|
144
|
+
if (resolution.kind !== "figure" || resolution.resolved.type !== "file") {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const file = resolution.resolved;
|
|
148
|
+
const name = assetFileName(file);
|
|
149
|
+
if (sources.has(name)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const resolved = resolveWorkspacePath({ workspaceRoot: root, analysisId: args.analysisId, path: file.path });
|
|
153
|
+
if (resolved.kind !== "ok") {
|
|
154
|
+
return err({ kind: "figure-out-of-scope", blockId: resolution.blockId, path: file.path });
|
|
155
|
+
}
|
|
156
|
+
sources.set(name, resolved.absolute);
|
|
157
|
+
}
|
|
158
|
+
const madeSession = await tryFsWrite("preview.mkdir", () => mkdir(sessionDir, { recursive: true }), { path: sessionDir });
|
|
159
|
+
if (madeSession.isErr()) {
|
|
160
|
+
return err({ kind: "fs", error: madeSession.error });
|
|
161
|
+
}
|
|
162
|
+
if (sources.size > 0) {
|
|
163
|
+
const madeAssets = await tryFsWrite("preview.mkdir", () => mkdir(assetsDir, { recursive: true }), { path: assetsDir });
|
|
164
|
+
if (madeAssets.isErr()) {
|
|
165
|
+
return err({ kind: "fs", error: madeAssets.error });
|
|
166
|
+
}
|
|
167
|
+
for (const [name, source] of sources) {
|
|
168
|
+
const copied = await tryFsWrite("preview.copyFile", () => copyFile(source, join(assetsDir, name)), { path: source });
|
|
169
|
+
if (copied.isErr()) {
|
|
170
|
+
return err({ kind: "fs", error: copied.error });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const wrote = await tryFsWrite("preview.writeFile", () => writeFile(pagePath, args.page, "utf8"), { path: pagePath });
|
|
175
|
+
if (wrote.isErr()) {
|
|
176
|
+
return err({ kind: "fs", error: wrote.error });
|
|
177
|
+
}
|
|
178
|
+
return ok(pagePath);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Make the render-and-preview tool over the session-state gateway and the render seams.
|
|
182
|
+
*
|
|
183
|
+
* The tool reads the thread id from the scope of the call, and it loads the state through the gateway. Thus
|
|
184
|
+
* one factory serves every thread. The tool holds no per-session value, and it resolves the root of the
|
|
185
|
+
* analysis of the call.
|
|
186
|
+
*/
|
|
187
|
+
export function createPreviewReportTool(deps) {
|
|
188
|
+
const logger = (deps.logger ?? createNoopLogger()).named("preview-report");
|
|
189
|
+
return defineTool({
|
|
190
|
+
id: "preview_report",
|
|
191
|
+
description: "Render the current draft to a self-contained HTML page, and give back the page path. " +
|
|
192
|
+
"The tool finishes the draft first: an incomplete draft gives back the gap list, and no page renders. " +
|
|
193
|
+
"On a pass it resolves each reference, stages each bound image beside the page, and writes the page. " +
|
|
194
|
+
"Use it to see the report, and to confirm that each reference resolves.",
|
|
195
|
+
inputSchema: previewReportInput,
|
|
196
|
+
executionMode: "inline",
|
|
197
|
+
describeCall: "none",
|
|
198
|
+
execute: async (_input, ctx) => {
|
|
199
|
+
const opened = await openReportThread(deps.gateway, ctx.session.scope);
|
|
200
|
+
if (opened.isErr()) {
|
|
201
|
+
return ok({ outcome: "refused", refusal: opened.error });
|
|
202
|
+
}
|
|
203
|
+
const { threadId, analysisId, state } = opened.value;
|
|
204
|
+
const { document: draft, snapshot } = state;
|
|
205
|
+
const finished = finishDraft(draft, snapshot);
|
|
206
|
+
if (!finished.valid) {
|
|
207
|
+
return ok({ outcome: "gaps", gaps: finished.gaps });
|
|
208
|
+
}
|
|
209
|
+
const document = finished.document;
|
|
210
|
+
if (deps.makeResolver === undefined) {
|
|
211
|
+
return ok({ outcome: "resolver-unavailable" });
|
|
212
|
+
}
|
|
213
|
+
// The resolver construction resolves the workspace root inside, and that seam throws on an
|
|
214
|
+
// unresolvable root. The guard turns the throw into a typed outcome that names the fault, thus a
|
|
215
|
+
// control-flow exception propagates and the unresolvable root does not.
|
|
216
|
+
let resolver;
|
|
217
|
+
try {
|
|
218
|
+
resolver = deps.makeResolver({ analysisId, auth: ctx.session.auth });
|
|
219
|
+
}
|
|
220
|
+
catch (cause) {
|
|
221
|
+
logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
222
|
+
return ok({ outcome: "root-unresolvable", detail: "the workspace root did not resolve" });
|
|
223
|
+
}
|
|
224
|
+
const { resolutions, unresolved } = await resolveDocument(document, resolver, snapshot);
|
|
225
|
+
if (unresolved.length > 0) {
|
|
226
|
+
return ok({ outcome: "unresolved-references", unresolved });
|
|
227
|
+
}
|
|
228
|
+
const bridged = bridgeValues(resolutions, figureSourcePolicy);
|
|
229
|
+
if (bridged.isErr()) {
|
|
230
|
+
return ok({ outcome: "bridge-mismatch", mismatches: bridged.error });
|
|
231
|
+
}
|
|
232
|
+
const rendered = renderReportPage(document, bridged.value);
|
|
233
|
+
if (rendered.isErr()) {
|
|
234
|
+
return ok({ outcome: "render-problems", problems: rendered.error });
|
|
235
|
+
}
|
|
236
|
+
const written = await renderToWorkspace({
|
|
237
|
+
resolveWorkspaceRoot: deps.resolveWorkspaceRoot,
|
|
238
|
+
analysisId,
|
|
239
|
+
threadId,
|
|
240
|
+
resolutions,
|
|
241
|
+
page: rendered.value,
|
|
242
|
+
});
|
|
243
|
+
if (written.isErr()) {
|
|
244
|
+
const failure = written.error;
|
|
245
|
+
if (failure.kind === "figure-out-of-scope") {
|
|
246
|
+
// A bound figure names a path outside the workspace root. The record gate refuses the
|
|
247
|
+
// same figure, thus the preview refuses it too and names the block.
|
|
248
|
+
logger.warn("a bound figure escapes the workspace root", { threadId, analysisId, blockId: failure.blockId, path: failure.path });
|
|
249
|
+
return ok({ outcome: "figure-out-of-scope", blockId: failure.blockId, path: failure.path });
|
|
250
|
+
}
|
|
251
|
+
// An unresolvable root, a full volume, and a denied write each arrive here as a value. The
|
|
252
|
+
// tool contract is ok-channel data for each degraded condition, thus the fault becomes an
|
|
253
|
+
// outcome. The log keeps the full fault, because the outcome carries the description alone.
|
|
254
|
+
logger.warn("the page did not land", { threadId, analysisId, ...defaultErrorFields(failure.error.cause) });
|
|
255
|
+
return ok({ outcome: "write-failed", detail: describeFsError(failure.error) });
|
|
256
|
+
}
|
|
257
|
+
// The page shows the draft that the finish read. The stamp records that draft, thus the eyes
|
|
258
|
+
// copy this hash and the record compares against it. A failed stamp is a transient store fault,
|
|
259
|
+
// and the page stays on disk. The agent runs the preview again to stamp the marker.
|
|
260
|
+
const stamped = await deps.gateway.stampRendered(threadId, computeDraftHash(draft));
|
|
261
|
+
if (stamped.outcome !== "stamped") {
|
|
262
|
+
const detail = stamped.outcome === "failed" ? stamped.detail : "the session state row is absent";
|
|
263
|
+
logger.warn("the rendered hash did not stamp", { threadId, analysisId, detail });
|
|
264
|
+
return ok({ outcome: "stamp-failed", pagePath: written.value, detail });
|
|
265
|
+
}
|
|
266
|
+
return ok({ outcome: "rendered", pagePath: written.value });
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record tool of a report session.
|
|
3
|
+
*
|
|
4
|
+
* The tool records one version of the report. The store is append-only with no delete, thus a version that
|
|
5
|
+
* the gate did not accept must never exist. The tool runs the whole gate first, and only a pass reaches the
|
|
6
|
+
* store.
|
|
7
|
+
*
|
|
8
|
+
* The gate has three parts, in order of cost. The finish gates the schema, the unique ids, and the
|
|
9
|
+
* structural tier, and a gap list returns as data. The look-before-record rule then compares the seen hash
|
|
10
|
+
* against the hash of the current draft, on the state that the load already read. A never-seen page and a
|
|
11
|
+
* stale look each refuse with a distinct reason, and the rule reads no model judgment. The full validation
|
|
12
|
+
* resolves every reference through the injected resolver, matches each chart encoding, and matches each
|
|
13
|
+
* assert. A failure names the block that holds the failed part, thus the agent repairs one block and not the
|
|
14
|
+
* report at large.
|
|
15
|
+
*
|
|
16
|
+
* The look runs before the validation on purpose. The validation builds the resolver and its prepare pass
|
|
17
|
+
* can start a container, thus the cheap look guards the expensive pass. A never-seen or stale draft refuses
|
|
18
|
+
* before the resolver constructs.
|
|
19
|
+
*
|
|
20
|
+
* On a pass the store records the document that the gate validated, the pinned snapshot, and the anchor from
|
|
21
|
+
* the thread row. The one-per-thread rule of the store bounds a concurrent race, and that refusal returns as
|
|
22
|
+
* data. The outcome of a pass carries the version id.
|
|
23
|
+
*/
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import type { AuthContext } from "../../auth/types.js";
|
|
26
|
+
import type { Logger } from "../../lib/logger.js";
|
|
27
|
+
import type { ThreadStore } from "../../memory/thread-store.js";
|
|
28
|
+
import { type FinishGap } from "../../report-model/draft-finish.js";
|
|
29
|
+
import type { ReferenceResolver } from "../../report-model/reference-resolver.js";
|
|
30
|
+
import { type ResolutionFailure, type SchemaIssue } from "../../report-model/validate.js";
|
|
31
|
+
import type { ReportVersionStore } from "../../state/report-versions.js";
|
|
32
|
+
import { type Tool } from "../define-tool.js";
|
|
33
|
+
import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
|
|
34
|
+
/** The empty input. The tool records the current draft of the thread, thus it needs no field. */
|
|
35
|
+
declare const recordVersionInput: z.ZodObject<{}, z.core.$strip>;
|
|
36
|
+
export type RecordVersionInput = z.infer<typeof recordVersionInput>;
|
|
37
|
+
/**
|
|
38
|
+
* The typed outcome of the record tool. Each arm is ok-channel data, thus the tool never throws for a
|
|
39
|
+
* degraded condition.
|
|
40
|
+
*
|
|
41
|
+
* `gaps` names each completeness gap of the draft. `never-seen` means that no eyes ran on the current draft.
|
|
42
|
+
* `stale-look` means that the eyes ran, and the agent then changed the draft. `root-unresolvable` means that
|
|
43
|
+
* the resolver construction cannot resolve the workspace root. `invalid` names each gate failure, and a
|
|
44
|
+
* resolution failure carries the block that holds it. `already-recorded` means that the thread already holds
|
|
45
|
+
* a version. `recorded` carries the version id.
|
|
46
|
+
*/
|
|
47
|
+
export type RecordVersionResult = {
|
|
48
|
+
outcome: "refused";
|
|
49
|
+
refusal: SessionRefusal;
|
|
50
|
+
} | {
|
|
51
|
+
outcome: "gaps";
|
|
52
|
+
gaps: FinishGap[];
|
|
53
|
+
} | {
|
|
54
|
+
outcome: "never-seen";
|
|
55
|
+
} | {
|
|
56
|
+
outcome: "stale-look";
|
|
57
|
+
} | {
|
|
58
|
+
outcome: "resolver-unavailable";
|
|
59
|
+
} | {
|
|
60
|
+
outcome: "root-unresolvable";
|
|
61
|
+
detail: string;
|
|
62
|
+
} | {
|
|
63
|
+
outcome: "invalid";
|
|
64
|
+
schemaIssues?: SchemaIssue[];
|
|
65
|
+
duplicateIds?: string[];
|
|
66
|
+
resolutionFailures?: ResolutionFailure[];
|
|
67
|
+
} | {
|
|
68
|
+
outcome: "already-recorded";
|
|
69
|
+
} | {
|
|
70
|
+
outcome: "record-failed";
|
|
71
|
+
detail: string;
|
|
72
|
+
} | {
|
|
73
|
+
outcome: "recorded";
|
|
74
|
+
versionId: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The construction deps of the record tool.
|
|
78
|
+
*
|
|
79
|
+
* `store` is the append-only version store. `threads` reads the anchor of the report thread, thus the
|
|
80
|
+
* version carries the parent conversation and the transcript position. `makeResolver` is optional, because a
|
|
81
|
+
* resolver realization can be absent, and the gate needs one. It binds one analysis, thus the tool makes the
|
|
82
|
+
* resolver over the scope of the call.
|
|
83
|
+
*/
|
|
84
|
+
export interface RecordVersionToolDeps {
|
|
85
|
+
readonly gateway: ReportSessionStateGateway;
|
|
86
|
+
readonly store: ReportVersionStore;
|
|
87
|
+
readonly threads: Pick<ThreadStore, "getThread">;
|
|
88
|
+
readonly makeResolver?: (scope: {
|
|
89
|
+
analysisId: string;
|
|
90
|
+
auth: AuthContext;
|
|
91
|
+
}) => ReferenceResolver;
|
|
92
|
+
readonly logger?: Logger;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Make the record tool over the session-state gateway, the version store, the thread store, and the
|
|
96
|
+
* resolver.
|
|
97
|
+
*
|
|
98
|
+
* The tool reads the thread id from the scope of the call, and it loads the state through the gateway. Thus
|
|
99
|
+
* one factory serves every thread. The tool holds no per-session value.
|
|
100
|
+
*/
|
|
101
|
+
export declare function createRecordVersionTool(deps: RecordVersionToolDeps): Tool<RecordVersionInput, RecordVersionResult>;
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record tool of a report session.
|
|
3
|
+
*
|
|
4
|
+
* The tool records one version of the report. The store is append-only with no delete, thus a version that
|
|
5
|
+
* the gate did not accept must never exist. The tool runs the whole gate first, and only a pass reaches the
|
|
6
|
+
* store.
|
|
7
|
+
*
|
|
8
|
+
* The gate has three parts, in order of cost. The finish gates the schema, the unique ids, and the
|
|
9
|
+
* structural tier, and a gap list returns as data. The look-before-record rule then compares the seen hash
|
|
10
|
+
* against the hash of the current draft, on the state that the load already read. A never-seen page and a
|
|
11
|
+
* stale look each refuse with a distinct reason, and the rule reads no model judgment. The full validation
|
|
12
|
+
* resolves every reference through the injected resolver, matches each chart encoding, and matches each
|
|
13
|
+
* assert. A failure names the block that holds the failed part, thus the agent repairs one block and not the
|
|
14
|
+
* report at large.
|
|
15
|
+
*
|
|
16
|
+
* The look runs before the validation on purpose. The validation builds the resolver and its prepare pass
|
|
17
|
+
* can start a container, thus the cheap look guards the expensive pass. A never-seen or stale draft refuses
|
|
18
|
+
* before the resolver constructs.
|
|
19
|
+
*
|
|
20
|
+
* On a pass the store records the document that the gate validated, the pinned snapshot, and the anchor from
|
|
21
|
+
* the thread row. The one-per-thread rule of the store bounds a concurrent race, and that refusal returns as
|
|
22
|
+
* data. The outcome of a pass carries the version id.
|
|
23
|
+
*/
|
|
24
|
+
import { ok } from "neverthrow";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
27
|
+
import { describeDbError } from "../../lib/db-result.js";
|
|
28
|
+
import { finishDraft } from "../../report-model/draft-finish.js";
|
|
29
|
+
import { computeDraftHash } from "../../report-model/draft-hash.js";
|
|
30
|
+
import { validateReport } from "../../report-model/validate.js";
|
|
31
|
+
import { defineTool } from "../define-tool.js";
|
|
32
|
+
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
33
|
+
/** The empty input. The tool records the current draft of the thread, thus it needs no field. */
|
|
34
|
+
const recordVersionInput = z.object({});
|
|
35
|
+
/** A short account of a record refusal, for the detail of a transient failure. */
|
|
36
|
+
function describeRecordFailure(error) {
|
|
37
|
+
switch (error.type) {
|
|
38
|
+
case "malformed_document":
|
|
39
|
+
return "the gate accepted a document that the store read as malformed";
|
|
40
|
+
case "malformed_snapshot":
|
|
41
|
+
return "the pinned snapshot is malformed";
|
|
42
|
+
case "parent_analysis_mismatch":
|
|
43
|
+
return "the parent version belongs to a different analysis";
|
|
44
|
+
case "thread_already_holds_version":
|
|
45
|
+
return "the thread already holds a recorded version";
|
|
46
|
+
default:
|
|
47
|
+
return describeDbError(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Make the record tool over the session-state gateway, the version store, the thread store, and the
|
|
52
|
+
* resolver.
|
|
53
|
+
*
|
|
54
|
+
* The tool reads the thread id from the scope of the call, and it loads the state through the gateway. Thus
|
|
55
|
+
* one factory serves every thread. The tool holds no per-session value.
|
|
56
|
+
*/
|
|
57
|
+
export function createRecordVersionTool(deps) {
|
|
58
|
+
const logger = (deps.logger ?? createNoopLogger()).named("record-report-version");
|
|
59
|
+
return defineTool({
|
|
60
|
+
id: "record_report_version",
|
|
61
|
+
description: "Record the current draft as one report version. The tool runs the whole gate first: it finishes the draft, " +
|
|
62
|
+
"resolves each reference, matches each chart encoding, and matches each assert. An incomplete draft gives back the gap list, " +
|
|
63
|
+
"and a failed reference gives back the block that broke. The tool records a version only after the eyes look at the current page. " +
|
|
64
|
+
"A thread holds one version, thus a second record gives back that the thread already holds one.",
|
|
65
|
+
inputSchema: recordVersionInput,
|
|
66
|
+
executionMode: "inline",
|
|
67
|
+
describeCall: "none",
|
|
68
|
+
execute: async (_input, ctx) => {
|
|
69
|
+
const opened = await openReportThread(deps.gateway, ctx.session.scope);
|
|
70
|
+
if (opened.isErr()) {
|
|
71
|
+
return ok({ outcome: "refused", refusal: opened.error });
|
|
72
|
+
}
|
|
73
|
+
const { threadId, analysisId, state, seenDocumentHash } = opened.value;
|
|
74
|
+
const { document: draft, snapshot } = state;
|
|
75
|
+
const finished = finishDraft(draft, snapshot);
|
|
76
|
+
if (!finished.valid) {
|
|
77
|
+
return ok({ outcome: "gaps", gaps: finished.gaps });
|
|
78
|
+
}
|
|
79
|
+
// The look-before-record rule runs before the resolver and the validation, on the state that the
|
|
80
|
+
// load already read. The preview stamps the rendered hash, and the eyes copy it onto the seen
|
|
81
|
+
// hash. A never-seen page carries no seen hash. A stale look carries the hash of an earlier draft,
|
|
82
|
+
// thus a mismatch against the current draft refuses. Each refusal skips the expensive validation.
|
|
83
|
+
const currentHash = computeDraftHash(draft);
|
|
84
|
+
if (seenDocumentHash === null) {
|
|
85
|
+
return ok({ outcome: "never-seen" });
|
|
86
|
+
}
|
|
87
|
+
if (seenDocumentHash !== currentHash) {
|
|
88
|
+
return ok({ outcome: "stale-look" });
|
|
89
|
+
}
|
|
90
|
+
if (deps.makeResolver === undefined) {
|
|
91
|
+
return ok({ outcome: "resolver-unavailable" });
|
|
92
|
+
}
|
|
93
|
+
// The resolver construction resolves the workspace root inside, and that seam throws on an
|
|
94
|
+
// unresolvable root. The guard turns the throw into a typed outcome that names the fault, thus a
|
|
95
|
+
// control-flow exception propagates and the unresolvable root does not.
|
|
96
|
+
let resolver;
|
|
97
|
+
try {
|
|
98
|
+
resolver = deps.makeResolver({ analysisId, auth: ctx.session.auth });
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
logger.warn("the workspace root did not resolve", { threadId, analysisId, ...logger.errorFields(cause) });
|
|
102
|
+
return ok({ outcome: "root-unresolvable", detail: "the workspace root did not resolve" });
|
|
103
|
+
}
|
|
104
|
+
const validation = await validateReport(finished.document, snapshot, resolver);
|
|
105
|
+
if (!validation.valid) {
|
|
106
|
+
return ok({
|
|
107
|
+
outcome: "invalid",
|
|
108
|
+
...(validation.schemaIssues ? { schemaIssues: validation.schemaIssues } : {}),
|
|
109
|
+
...(validation.duplicateIds ? { duplicateIds: validation.duplicateIds } : {}),
|
|
110
|
+
...(validation.resolutionFailures ? { resolutionFailures: validation.resolutionFailures } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const thread = await deps.threads.getThread(threadId);
|
|
114
|
+
if (thread.isErr()) {
|
|
115
|
+
logger.warn("the thread row read failed", { threadId, analysisId, ...logger.errorFields(thread.error.cause) });
|
|
116
|
+
return ok({ outcome: "record-failed", detail: describeDbError(thread.error) });
|
|
117
|
+
}
|
|
118
|
+
if (thread.value === null) {
|
|
119
|
+
logger.warn("the report thread row is absent at record", { threadId, analysisId });
|
|
120
|
+
return ok({ outcome: "record-failed", detail: "the report thread row is absent" });
|
|
121
|
+
}
|
|
122
|
+
const recorded = await deps.store.record({
|
|
123
|
+
document: finished.document,
|
|
124
|
+
snapshot,
|
|
125
|
+
analysisId,
|
|
126
|
+
threadId,
|
|
127
|
+
parentThreadId: thread.value.parentThreadId,
|
|
128
|
+
parentSeq: thread.value.parentSeq,
|
|
129
|
+
});
|
|
130
|
+
return recorded.match((ref) => ok({ outcome: "recorded", versionId: ref.versionId }), (error) => {
|
|
131
|
+
if (error.type === "thread_already_holds_version") {
|
|
132
|
+
return ok({ outcome: "already-recorded" });
|
|
133
|
+
}
|
|
134
|
+
logger.warn("the version did not record", { threadId, analysisId, reason: error.type });
|
|
135
|
+
return ok({ outcome: "record-failed", detail: describeRecordFailure(error) });
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -30,6 +30,7 @@ import { composeSystemPrompt } from "../../agents/system-prompt.js";
|
|
|
30
30
|
import { DEFAULT_SALVAGE_ITERATIONS, runToTerminal } from "../../loop/run-to-terminal.js";
|
|
31
31
|
import { passthroughStep } from "../../loop/run-step.js";
|
|
32
32
|
import { forSubAgent, scopeResource } from "../../auth/types.js";
|
|
33
|
+
import { effectiveDeadlineMs } from "../../providers/types.js";
|
|
33
34
|
import { defineTool } from "../define-tool.js";
|
|
34
35
|
import { createListAvailablePackagesTool } from "../sandbox/list-available-packages.js";
|
|
35
36
|
import { createListAvailableRefsTool } from "../sandbox/list-available-refs.js";
|
|
@@ -776,6 +777,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
776
777
|
...(input.priorRuns ? ["", "## Prior Run Results", input.priorRuns] : []),
|
|
777
778
|
...(input.userConstraints ? ["", "## User Constraints", input.userConstraints] : []),
|
|
778
779
|
].join("\n");
|
|
780
|
+
const planTimeoutMs = effectiveDeadlineMs(deps.conversation.provider, PLAN_TIMEOUT_MS);
|
|
779
781
|
// Opens the invocation. Two things are only knowable here and both are
|
|
780
782
|
// first-order suspects when a planner runs long and returns nothing: the size
|
|
781
783
|
// of the seed it was handed, and which of its blocks made it that size. The
|
|
@@ -788,7 +790,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
788
790
|
model: deps.conversation.model,
|
|
789
791
|
maxIterations: PLANNER_MAX_ITERATIONS,
|
|
790
792
|
salvageIterations: DEFAULT_SALVAGE_ITERATIONS,
|
|
791
|
-
timeoutMs:
|
|
793
|
+
timeoutMs: planTimeoutMs,
|
|
792
794
|
grounding: grounding.kind,
|
|
793
795
|
...(input.parentPlanId ? { parentPlanId: input.parentPlanId } : {}),
|
|
794
796
|
seedChars: {
|
|
@@ -818,7 +820,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
818
820
|
};
|
|
819
821
|
// Merge the outer abort signal with a wall-clock timeout — either
|
|
820
822
|
// cancels the planner promptly.
|
|
821
|
-
const signal = AbortSignal.any([ctx.signal, AbortSignal.timeout(
|
|
823
|
+
const signal = AbortSignal.any([ctx.signal, AbortSignal.timeout(planTimeoutMs)]);
|
|
822
824
|
let runError = null;
|
|
823
825
|
let run = null;
|
|
824
826
|
try {
|
|
@@ -27,12 +27,13 @@ import { ok } from "neverthrow";
|
|
|
27
27
|
import { z } from "zod";
|
|
28
28
|
import { defineTool } from "../define-tool.js";
|
|
29
29
|
import { capCodePoints, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
|
|
30
|
+
import { LIBS_CONTAINER_PATH } from "../../sandbox/mount-plan.js";
|
|
30
31
|
/**
|
|
31
32
|
* Where the list lives when the host mounts the library store at the same path
|
|
32
33
|
* the sandbox does. Hosts that do not — the store is baked into the image and
|
|
33
34
|
* never bind-mounted — inject their own path instead.
|
|
34
35
|
*/
|
|
35
|
-
const DEFAULT_PACKAGES_FILE =
|
|
36
|
+
const DEFAULT_PACKAGES_FILE = `${LIBS_CONTAINER_PATH}/current/packages.txt`;
|
|
36
37
|
/**
|
|
37
38
|
* Default cap on a listing — high enough that the real store is never truncated.
|
|
38
39
|
* The shipped catalog is ~270 packages (~3 KB, well under a thousand tokens), so
|