@inflexa-ai/harness 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/report-session-agent.d.ts +25 -9
- package/dist/agents/report-session-agent.js +21 -6
- package/dist/app/report-session-runtime.js +26 -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/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +21 -0
- package/dist/providers/ai-sdk.d.ts +81 -3
- package/dist/providers/ai-sdk.js +91 -10
- package/dist/providers/types.d.ts +17 -0
- 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/production-resolver.d.ts +124 -0
- package/dist/report-model/production-resolver.js +801 -0
- package/dist/report-model/reference-resolver.d.ts +6 -0
- package/dist/report-model/validate.js +5 -0
- package/dist/report-render/page.d.ts +23 -0
- package/dist/report-render/page.js +36 -3
- package/dist/runtime/assemble.d.ts +28 -4
- package/dist/runtime/assemble.js +42 -3
- package/dist/sandbox/mount-plan.d.ts +7 -0
- package/dist/sandbox/mount-plan.js +7 -2
- package/dist/state/init.js +20 -7
- package/dist/state/report-session-state.d.ts +27 -0
- package/dist/state/report-session-state.js +29 -2
- 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/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 +54 -4
- package/dist/tools/report-authoring/authoring-tools.js +1 -1
- 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 +2 -0
- package/dist/tools/report-session/index.js +2 -0
- package/dist/tools/report-session/preview-report.d.ts +20 -5
- package/dist/tools/report-session/preview-report.js +28 -4
- package/dist/tools/report-session/record-version.d.ts +102 -0
- package/dist/tools/report-session/record-version.js +139 -0
- 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
|
@@ -157,15 +157,20 @@ export interface ReportSessionState {
|
|
|
157
157
|
*/
|
|
158
158
|
export type SessionStateToken = DraftDocument | null;
|
|
159
159
|
/**
|
|
160
|
-
* The outcome of a gateway load. `found` carries the state, the analysis that owns the thread,
|
|
161
|
-
* concurrency token. `absent` and `wrong-type`
|
|
162
|
-
* that names its cause.
|
|
160
|
+
* The outcome of a gateway load. `found` carries the state, the analysis that owns the thread, the
|
|
161
|
+
* concurrency token, and the seen-document hash of the look-before-record rule. `absent` and `wrong-type`
|
|
162
|
+
* are permanent conditions, and `failed` is a transient fault that names its cause.
|
|
163
|
+
*
|
|
164
|
+
* `seenDocumentHash` is the hash that the last eyes capture saw, or `null` before the first look. The record
|
|
165
|
+
* tool compares it against the hash of the current draft, thus a never-seen page and a stale look each
|
|
166
|
+
* refuse.
|
|
163
167
|
*/
|
|
164
168
|
export type SessionStateLoad = {
|
|
165
169
|
outcome: "found";
|
|
166
170
|
state: ReportSessionState;
|
|
167
171
|
analysisId: string;
|
|
168
172
|
token: SessionStateToken;
|
|
173
|
+
seenDocumentHash: string | null;
|
|
169
174
|
} | {
|
|
170
175
|
outcome: "absent";
|
|
171
176
|
} | {
|
|
@@ -184,6 +189,35 @@ export type SessionStatePersist = {
|
|
|
184
189
|
outcome: "failed";
|
|
185
190
|
detail: string;
|
|
186
191
|
};
|
|
192
|
+
/**
|
|
193
|
+
* The outcome of a gateway stamp. `stamped` wrote the marker. `absent` means that no row holds the thread.
|
|
194
|
+
* `failed` is a transient store fault that names its cause. Each arm is plain data, thus a stamp never
|
|
195
|
+
* throws for one of them.
|
|
196
|
+
*/
|
|
197
|
+
export type StampResult = {
|
|
198
|
+
outcome: "stamped";
|
|
199
|
+
} | {
|
|
200
|
+
outcome: "absent";
|
|
201
|
+
} | {
|
|
202
|
+
outcome: "failed";
|
|
203
|
+
detail: string;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* The outcome of the seen stamp through the gateway. `stamped` copied a rendered hash onto the seen hash.
|
|
207
|
+
* `no-rendered` means that the row holds no rendered hash to copy, thus no preview stamped one. `absent`
|
|
208
|
+
* means that no row holds the thread. `failed` is a transient store fault that names its cause. Each arm is
|
|
209
|
+
* plain data, thus a stamp never throws for one of them.
|
|
210
|
+
*/
|
|
211
|
+
export type SeenStampResult = {
|
|
212
|
+
outcome: "stamped";
|
|
213
|
+
} | {
|
|
214
|
+
outcome: "no-rendered";
|
|
215
|
+
} | {
|
|
216
|
+
outcome: "absent";
|
|
217
|
+
} | {
|
|
218
|
+
outcome: "failed";
|
|
219
|
+
detail: string;
|
|
220
|
+
};
|
|
187
221
|
/**
|
|
188
222
|
* The session-state gateway that the tools read and write.
|
|
189
223
|
*
|
|
@@ -195,13 +229,29 @@ export type SessionStatePersist = {
|
|
|
195
229
|
export interface ReportSessionStateGateway {
|
|
196
230
|
load(threadId: string): Promise<SessionStateLoad>;
|
|
197
231
|
persist(threadId: string, document: DraftDocument, expected: SessionStateToken): Promise<SessionStatePersist>;
|
|
232
|
+
/**
|
|
233
|
+
* Stamp the hash of the rendered draft on the session state. The preview calls it when the page lands,
|
|
234
|
+
* thus the runtime knows which draft the page shows.
|
|
235
|
+
*/
|
|
236
|
+
stampRendered(threadId: string, hash: string): Promise<StampResult>;
|
|
237
|
+
/**
|
|
238
|
+
* Copy the rendered hash onto the seen hash. The eyes call it after a capture, thus the seen hash holds
|
|
239
|
+
* the hash of the draft that the picture shows, and never the current one. The outcome names whether a
|
|
240
|
+
* rendered hash existed to copy, thus the eyes tell a real stamp from a missed one.
|
|
241
|
+
*/
|
|
242
|
+
stampSeen(threadId: string): Promise<SeenStampResult>;
|
|
198
243
|
}
|
|
199
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* The thread of a call, its analysis, the loaded state, the concurrency token that the persist compares
|
|
246
|
+
* against, and the seen-document hash of the look-before-record rule.
|
|
247
|
+
*/
|
|
200
248
|
export interface OpenedThread {
|
|
201
249
|
readonly threadId: string;
|
|
202
250
|
readonly analysisId: string;
|
|
203
251
|
readonly state: ReportSessionState;
|
|
204
252
|
readonly token: SessionStateToken;
|
|
253
|
+
/** The hash that the last eyes capture saw, or `null` before the first look. */
|
|
254
|
+
readonly seenDocumentHash: string | null;
|
|
205
255
|
}
|
|
206
256
|
/**
|
|
207
257
|
* Resolve the report thread of a call, and load its state through the gateway.
|
|
@@ -119,7 +119,7 @@ export async function openReportThread(gateway, scope) {
|
|
|
119
119
|
detail: `the scope names the analysis ${analysisId}, but the thread ${threadId} belongs to the analysis ${loaded.analysisId}`,
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
return ok({ threadId, analysisId, state: loaded.state, token: loaded.token });
|
|
122
|
+
return ok({ threadId, analysisId, state: loaded.state, token: loaded.token, seenDocumentHash: loaded.seenDocumentHash });
|
|
123
123
|
}
|
|
124
124
|
/** Read an absentable field. An explicit `null` and an omitted key both mean that the model gave no value. */
|
|
125
125
|
function given(value) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eyes tool of a report session.
|
|
3
|
+
*
|
|
4
|
+
* The tool opens the rendered page of a thread in headless Chrome, and it gives back what a reviewer would
|
|
5
|
+
* see: the screenshot, the console errors, and the failed requests. The agent reads the picture and the
|
|
6
|
+
* faults, decides, and repairs. The tool never judges, thus it never blocks the loop.
|
|
7
|
+
*
|
|
8
|
+
* The session tree has no URL space, thus the tool navigates to the page file through a `file://` URL. The
|
|
9
|
+
* page lives at `report-sessions/{threadId}/index.html` under the workspace root, which the preview writes.
|
|
10
|
+
* A missed page means that no preview ran, and it is a typed outcome, not a throw.
|
|
11
|
+
*
|
|
12
|
+
* The `file://` URL resolves on the filesystem of the Chrome sidecar, because the connection is out of
|
|
13
|
+
* process. Thus the sidecar must mount the workspace tree of the harness host at the same path. A sidecar
|
|
14
|
+
* with no such mount reports the page as an unreachable request, and the tool gives back that fault.
|
|
15
|
+
*
|
|
16
|
+
* A composition that names no browser and injects no capture seam has no eyes at all. The tool reports that
|
|
17
|
+
* condition once, up front, and it stamps nothing. A per-attempt capture failure would instead read as a
|
|
18
|
+
* transient fault and invite a repeat of a call that can never pass.
|
|
19
|
+
*
|
|
20
|
+
* On a capture the tool copies the rendered hash onto the seen hash through the gateway. Thus the look
|
|
21
|
+
* counts, and the record tool lets the current draft record. The copy takes the rendered hash and never the
|
|
22
|
+
* current one, thus a later edit makes the look stale and the record refuses.
|
|
23
|
+
*
|
|
24
|
+
* The gateway reports whether a rendered hash existed to copy. When the row holds none, no preview stamped
|
|
25
|
+
* one and the look cannot count. The tool then gives a missed-stamp outcome that directs a new preview,
|
|
26
|
+
* because a repeated look never stamps a marker that no preview wrote.
|
|
27
|
+
*
|
|
28
|
+
* The chrome navigation and the workspace-root seam each speak the throw protocol. The tool runs the capture
|
|
29
|
+
* inside a guard, thus a genuine fault becomes a typed outcome and a control-flow exception propagates.
|
|
30
|
+
*/
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { type ChromeConfig } from "../../lib/chrome.js";
|
|
33
|
+
import { type Logger } from "../../lib/logger.js";
|
|
34
|
+
import { type CapturePage, type FailedRequest, type PageCapture } from "../../lib/page-capture.js";
|
|
35
|
+
import type { ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
36
|
+
import { type Tool } from "../define-tool.js";
|
|
37
|
+
import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
|
|
38
|
+
export type { CapturePage, FailedRequest, PageCapture };
|
|
39
|
+
/** The empty input. The tool examines the current page of the thread, thus it needs no field. */
|
|
40
|
+
declare const examinePageInput: z.ZodObject<{}, z.core.$strip>;
|
|
41
|
+
export type ExaminePageInput = z.infer<typeof examinePageInput>;
|
|
42
|
+
/**
|
|
43
|
+
* The typed outcome of the eyes tool. Each arm is ok-channel data, thus the tool never throws for a
|
|
44
|
+
* degraded condition. `examined` carries the console errors, the failed requests, and the page path. The
|
|
45
|
+
* screenshot does not ride the JSON. It rides the image path of the tool result, thus the model sees the
|
|
46
|
+
* picture and the JSON text holds no bytes. `missed-stamp` means that the row holds no rendered hash, thus
|
|
47
|
+
* no preview stamped one and the agent must run a new preview before the next look. `no-browser` means that
|
|
48
|
+
* the composition gives no browser, thus no look is possible at all and a repeat gives the same answer.
|
|
49
|
+
*/
|
|
50
|
+
export type ExaminePageResult = {
|
|
51
|
+
outcome: "refused";
|
|
52
|
+
refusal: SessionRefusal;
|
|
53
|
+
} | {
|
|
54
|
+
outcome: "no-browser";
|
|
55
|
+
detail: string;
|
|
56
|
+
} | {
|
|
57
|
+
outcome: "no-page";
|
|
58
|
+
} | {
|
|
59
|
+
outcome: "missed-stamp";
|
|
60
|
+
} | {
|
|
61
|
+
outcome: "capture-failed";
|
|
62
|
+
detail: string;
|
|
63
|
+
} | {
|
|
64
|
+
outcome: "examined";
|
|
65
|
+
consoleErrors: string[];
|
|
66
|
+
failedRequests: FailedRequest[];
|
|
67
|
+
pagePath: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The construction deps of the eyes tool.
|
|
71
|
+
*
|
|
72
|
+
* `resolveWorkspaceRoot` maps the analysis of the call onto its workspace root, thus one singleton tool
|
|
73
|
+
* serves every analysis. `chrome` configures the headless browser, and the sidecar that it names must mount
|
|
74
|
+
* the workspace tree at the same path, because the tool navigates to a `file://` URL. `capture` is optional
|
|
75
|
+
* and defaults to the shared capture, thus a test injects a seam that reads no browser.
|
|
76
|
+
*/
|
|
77
|
+
export interface ExaminePageToolDeps {
|
|
78
|
+
readonly gateway: ReportSessionStateGateway;
|
|
79
|
+
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
80
|
+
readonly chrome: ChromeConfig;
|
|
81
|
+
readonly capture?: CapturePage;
|
|
82
|
+
readonly logger?: Logger;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Make the eyes tool over the session-state gateway, the workspace-root seam, and the chrome config.
|
|
86
|
+
*
|
|
87
|
+
* The tool reads the thread id from the scope of the call, and it resolves the page path under the workspace
|
|
88
|
+
* root. Thus one factory serves every thread. The tool holds no per-session value.
|
|
89
|
+
*/
|
|
90
|
+
export declare function createExaminePageTool(deps: ExaminePageToolDeps): Tool<ExaminePageInput, ExaminePageResult>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eyes tool of a report session.
|
|
3
|
+
*
|
|
4
|
+
* The tool opens the rendered page of a thread in headless Chrome, and it gives back what a reviewer would
|
|
5
|
+
* see: the screenshot, the console errors, and the failed requests. The agent reads the picture and the
|
|
6
|
+
* faults, decides, and repairs. The tool never judges, thus it never blocks the loop.
|
|
7
|
+
*
|
|
8
|
+
* The session tree has no URL space, thus the tool navigates to the page file through a `file://` URL. The
|
|
9
|
+
* page lives at `report-sessions/{threadId}/index.html` under the workspace root, which the preview writes.
|
|
10
|
+
* A missed page means that no preview ran, and it is a typed outcome, not a throw.
|
|
11
|
+
*
|
|
12
|
+
* The `file://` URL resolves on the filesystem of the Chrome sidecar, because the connection is out of
|
|
13
|
+
* process. Thus the sidecar must mount the workspace tree of the harness host at the same path. A sidecar
|
|
14
|
+
* with no such mount reports the page as an unreachable request, and the tool gives back that fault.
|
|
15
|
+
*
|
|
16
|
+
* A composition that names no browser and injects no capture seam has no eyes at all. The tool reports that
|
|
17
|
+
* condition once, up front, and it stamps nothing. A per-attempt capture failure would instead read as a
|
|
18
|
+
* transient fault and invite a repeat of a call that can never pass.
|
|
19
|
+
*
|
|
20
|
+
* On a capture the tool copies the rendered hash onto the seen hash through the gateway. Thus the look
|
|
21
|
+
* counts, and the record tool lets the current draft record. The copy takes the rendered hash and never the
|
|
22
|
+
* current one, thus a later edit makes the look stale and the record refuses.
|
|
23
|
+
*
|
|
24
|
+
* The gateway reports whether a rendered hash existed to copy. When the row holds none, no preview stamped
|
|
25
|
+
* one and the look cannot count. The tool then gives a missed-stamp outcome that directs a new preview,
|
|
26
|
+
* because a repeated look never stamps a marker that no preview wrote.
|
|
27
|
+
*
|
|
28
|
+
* The chrome navigation and the workspace-root seam each speak the throw protocol. The tool runs the capture
|
|
29
|
+
* inside a guard, thus a genuine fault becomes a typed outcome and a control-flow exception propagates.
|
|
30
|
+
*/
|
|
31
|
+
import { ok } from "neverthrow";
|
|
32
|
+
import { access } from "node:fs/promises";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
import { pathToFileURL } from "node:url";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
import { hasBrowserUrl } from "../../lib/chrome.js";
|
|
37
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
38
|
+
import { defaultErrorFields } from "../../lib/logger.js";
|
|
39
|
+
import { capturePage } from "../../lib/page-capture.js";
|
|
40
|
+
import { defineTool, withToolResultImage } from "../define-tool.js";
|
|
41
|
+
import { openReportThread } from "../report-authoring/authoring-tools.js";
|
|
42
|
+
/** The empty input. The tool examines the current page of the thread, thus it needs no field. */
|
|
43
|
+
const examinePageInput = z.object({});
|
|
44
|
+
/** The IANA media type of the screenshot. `page.screenshot` gives PNG bytes. */
|
|
45
|
+
const SCREENSHOT_MEDIA_TYPE = "image/png";
|
|
46
|
+
/** The line that the agent reads when the composition gives no browser. */
|
|
47
|
+
const NO_BROWSER_DETAIL = "the composition gives no browser, thus this session cannot look at its page";
|
|
48
|
+
/**
|
|
49
|
+
* Make the eyes tool over the session-state gateway, the workspace-root seam, and the chrome config.
|
|
50
|
+
*
|
|
51
|
+
* The tool reads the thread id from the scope of the call, and it resolves the page path under the workspace
|
|
52
|
+
* root. Thus one factory serves every thread. The tool holds no per-session value.
|
|
53
|
+
*/
|
|
54
|
+
export function createExaminePageTool(deps) {
|
|
55
|
+
const logger = (deps.logger ?? createNoopLogger()).named("examine-page");
|
|
56
|
+
// An injected seam is the eyes of a test. A named browser is the eyes of a deployment. With neither, the
|
|
57
|
+
// composition has no eyes, and that answer is fixed at construction.
|
|
58
|
+
const eyesAvailable = deps.capture !== undefined || hasBrowserUrl(deps.chrome);
|
|
59
|
+
const capture = deps.capture ?? ((url) => capturePage(deps.chrome, url));
|
|
60
|
+
return defineTool({
|
|
61
|
+
id: "examine_page",
|
|
62
|
+
description: "Open the rendered report page in a real headless browser, and report what you see. " +
|
|
63
|
+
"Give back a screenshot, the console errors, and the failed requests. " +
|
|
64
|
+
"Run it after the preview to look at the current page, and to confirm that the layout and the charts read clean. " +
|
|
65
|
+
"The report tool records a version only after you look at the current page. " +
|
|
66
|
+
"If the page has no confirmed render, run the preview again first.",
|
|
67
|
+
inputSchema: examinePageInput,
|
|
68
|
+
executionMode: "inline",
|
|
69
|
+
describeCall: "none",
|
|
70
|
+
execute: async (_input, ctx) => {
|
|
71
|
+
// The check runs before every read, thus one clear signal replaces a capture failure for each
|
|
72
|
+
// page. The arm stamps no seen hash, because no eyes saw the page.
|
|
73
|
+
if (!eyesAvailable) {
|
|
74
|
+
logger.warn("the composition gives no browser, thus no look can run");
|
|
75
|
+
return ok({ outcome: "no-browser", detail: NO_BROWSER_DETAIL });
|
|
76
|
+
}
|
|
77
|
+
const opened = await openReportThread(deps.gateway, ctx.session.scope);
|
|
78
|
+
if (opened.isErr()) {
|
|
79
|
+
return ok({ outcome: "refused", refusal: opened.error });
|
|
80
|
+
}
|
|
81
|
+
const { threadId, analysisId } = opened.value;
|
|
82
|
+
let root;
|
|
83
|
+
try {
|
|
84
|
+
root = deps.resolveWorkspaceRoot(analysisId);
|
|
85
|
+
}
|
|
86
|
+
catch (cause) {
|
|
87
|
+
logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
88
|
+
return ok({ outcome: "capture-failed", detail: "the workspace root did not resolve" });
|
|
89
|
+
}
|
|
90
|
+
const relativePagePath = join("report-sessions", threadId, "index.html");
|
|
91
|
+
const pagePath = join(root, relativePagePath);
|
|
92
|
+
try {
|
|
93
|
+
await access(pagePath);
|
|
94
|
+
}
|
|
95
|
+
catch (cause) {
|
|
96
|
+
if (cause.code === "ENOENT") {
|
|
97
|
+
// No preview wrote the page yet. The agent runs the preview first.
|
|
98
|
+
return ok({ outcome: "no-page" });
|
|
99
|
+
}
|
|
100
|
+
logger.warn("the session page could not be read", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
101
|
+
return ok({ outcome: "capture-failed", detail: "the session page could not be read" });
|
|
102
|
+
}
|
|
103
|
+
let captured;
|
|
104
|
+
try {
|
|
105
|
+
captured = await capture(pathToFileURL(pagePath).href);
|
|
106
|
+
}
|
|
107
|
+
catch (cause) {
|
|
108
|
+
logger.warn("the page capture failed", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
109
|
+
return ok({ outcome: "capture-failed", detail: cause instanceof Error ? cause.message : String(cause) });
|
|
110
|
+
}
|
|
111
|
+
const stamped = await deps.gateway.stampSeen(threadId);
|
|
112
|
+
if (stamped.outcome === "no-rendered") {
|
|
113
|
+
// The row holds no rendered hash, thus no preview stamped one and the look cannot count. A
|
|
114
|
+
// repeated look never fixes this, because it copies a marker that no preview wrote. The agent
|
|
115
|
+
// runs a new preview, which stamps the rendered hash for the next look.
|
|
116
|
+
logger.warn("the seen stamp found no rendered hash", { threadId, analysisId });
|
|
117
|
+
return ok({ outcome: "missed-stamp" });
|
|
118
|
+
}
|
|
119
|
+
// A transient stamp fault and an absent row each leave the seen hash short, and the record gate
|
|
120
|
+
// then refuses a never-seen page. A later look re-stamps the marker, thus the picture stays valid
|
|
121
|
+
// and the tool gives it back.
|
|
122
|
+
if (stamped.outcome !== "stamped") {
|
|
123
|
+
const detail = stamped.outcome === "failed" ? stamped.detail : "the session state row is absent";
|
|
124
|
+
logger.warn("the seen hash did not stamp", { threadId, analysisId, detail });
|
|
125
|
+
}
|
|
126
|
+
// The screenshot rides the image path of the tool result, thus the model sees the picture. The
|
|
127
|
+
// JSON keeps the faults and the page path only, thus the JSON text holds no bytes.
|
|
128
|
+
return ok(withToolResultImage({
|
|
129
|
+
outcome: "examined",
|
|
130
|
+
consoleErrors: captured.consoleErrors,
|
|
131
|
+
failedRequests: captured.failedRequests,
|
|
132
|
+
pagePath: relativePagePath,
|
|
133
|
+
}, { base64: captured.screenshotBase64, mediaType: SCREENSHOT_MEDIA_TYPE }));
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -6,3 +6,5 @@
|
|
|
6
6
|
* the factories and their public types for the composition root.
|
|
7
7
|
*/
|
|
8
8
|
export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps } from "./preview-report.js";
|
|
9
|
+
export { createExaminePageTool, type CapturePage, type ExaminePageInput, type ExaminePageResult, type ExaminePageToolDeps, type FailedRequest, type PageCapture, } from "./examine-page.js";
|
|
10
|
+
export { createRecordVersionTool, type RecordVersionInput, type RecordVersionResult, type RecordVersionToolDeps } from "./record-version.js";
|
|
@@ -15,14 +15,18 @@
|
|
|
15
15
|
* this tree. A hosted view of a session page is a later capability, with a URL space of its own.
|
|
16
16
|
*
|
|
17
17
|
* Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
|
|
18
|
-
* absence, an
|
|
19
|
-
* workspace root,
|
|
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
|
|
20
23
|
* throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
|
|
21
24
|
* write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
|
|
22
25
|
* control-flow exception propagate.
|
|
23
26
|
*/
|
|
24
27
|
import { z } from "zod";
|
|
25
28
|
import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
29
|
+
import type { AuthContext } from "../../auth/types.js";
|
|
26
30
|
import { type Logger } from "../../lib/logger.js";
|
|
27
31
|
import { type FinishGap } from "../../report-model/draft-finish.js";
|
|
28
32
|
import type { ReferenceResolver } from "../../report-model/reference-resolver.js";
|
|
@@ -46,6 +50,9 @@ export type PreviewReportResult = {
|
|
|
46
50
|
gaps: FinishGap[];
|
|
47
51
|
} | {
|
|
48
52
|
outcome: "resolver-unavailable";
|
|
53
|
+
} | {
|
|
54
|
+
outcome: "root-unresolvable";
|
|
55
|
+
detail: string;
|
|
49
56
|
} | {
|
|
50
57
|
outcome: "unresolved-references";
|
|
51
58
|
unresolved: ResolutionFailure[];
|
|
@@ -62,6 +69,10 @@ export type PreviewReportResult = {
|
|
|
62
69
|
} | {
|
|
63
70
|
outcome: "write-failed";
|
|
64
71
|
detail: string;
|
|
72
|
+
} | {
|
|
73
|
+
outcome: "stamp-failed";
|
|
74
|
+
pagePath: string;
|
|
75
|
+
detail: string;
|
|
65
76
|
} | {
|
|
66
77
|
outcome: "rendered";
|
|
67
78
|
pagePath: string;
|
|
@@ -70,12 +81,16 @@ export type PreviewReportResult = {
|
|
|
70
81
|
* The construction deps of the preview tool.
|
|
71
82
|
*
|
|
72
83
|
* `resolveWorkspaceRoot` maps the analysis of the call onto its workspace root, thus one singleton tool
|
|
73
|
-
* serves every analysis and it resolves the root per call from the scope. `
|
|
74
|
-
* resolver realization can be absent.
|
|
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.
|
|
75
87
|
*/
|
|
76
88
|
export interface PreviewReportToolDeps {
|
|
77
89
|
readonly gateway: ReportSessionStateGateway;
|
|
78
|
-
readonly
|
|
90
|
+
readonly makeResolver?: (scope: {
|
|
91
|
+
analysisId: string;
|
|
92
|
+
auth: AuthContext;
|
|
93
|
+
}) => ReferenceResolver;
|
|
79
94
|
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
80
95
|
readonly logger?: Logger;
|
|
81
96
|
}
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
* this tree. A hosted view of a session page is a later capability, with a URL space of its own.
|
|
16
16
|
*
|
|
17
17
|
* Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
|
|
18
|
-
* absence, an
|
|
19
|
-
* workspace root,
|
|
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
|
|
20
23
|
* throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
|
|
21
24
|
* write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
|
|
22
25
|
* control-flow exception propagate.
|
|
@@ -29,6 +32,7 @@ import { resolveWorkspacePath } from "../../workspace/paths.js";
|
|
|
29
32
|
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
30
33
|
import { describeFsError, tryFsWrite } from "../../lib/fs-result.js";
|
|
31
34
|
import { defaultErrorFields } from "../../lib/logger.js";
|
|
35
|
+
import { computeDraftHash } from "../../report-model/draft-hash.js";
|
|
32
36
|
import { finishDraft } from "../../report-model/draft-finish.js";
|
|
33
37
|
import { resolveDocumentReferences } from "../../report-model/validate.js";
|
|
34
38
|
import { bridgeValues } from "../../report-render/value-bridge.js";
|
|
@@ -203,10 +207,21 @@ export function createPreviewReportTool(deps) {
|
|
|
203
207
|
return ok({ outcome: "gaps", gaps: finished.gaps });
|
|
204
208
|
}
|
|
205
209
|
const document = finished.document;
|
|
206
|
-
if (deps.
|
|
210
|
+
if (deps.makeResolver === undefined) {
|
|
207
211
|
return ok({ outcome: "resolver-unavailable" });
|
|
208
212
|
}
|
|
209
|
-
|
|
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);
|
|
210
225
|
if (unresolved.length > 0) {
|
|
211
226
|
return ok({ outcome: "unresolved-references", unresolved });
|
|
212
227
|
}
|
|
@@ -239,6 +254,15 @@ export function createPreviewReportTool(deps) {
|
|
|
239
254
|
logger.warn("the page did not land", { threadId, analysisId, ...defaultErrorFields(failure.error.cause) });
|
|
240
255
|
return ok({ outcome: "write-failed", detail: describeFsError(failure.error) });
|
|
241
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
|
+
}
|
|
242
266
|
return ok({ outcome: "rendered", pagePath: written.value });
|
|
243
267
|
},
|
|
244
268
|
});
|
|
@@ -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 {};
|