@inflexa-ai/harness 0.19.0 → 0.19.2
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 +32 -9
- package/dist/agents/report-session-agent.js +22 -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/auth/local-run-authorizer.d.ts +3 -2
- package/dist/auth/local-run-authorizer.js +4 -2
- 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/execution/run-authorizer.d.ts +18 -0
- package/dist/execution/run-canceler.d.ts +67 -0
- package/dist/execution/run-canceler.js +130 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -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/assets.d.ts +42 -0
- package/dist/report-render/assets.js +61 -0
- package/dist/report-render/chart.js +31 -10
- package/dist/report-render/design.d.ts +120 -0
- package/dist/report-render/design.js +808 -0
- package/dist/report-render/page.d.ts +51 -111
- package/dist/report-render/page.js +158 -268
- package/dist/report-render/render.js +34 -11
- package/dist/report-render/views/chart-view.d.ts +6 -3
- package/dist/report-render/views/chart-view.js +10 -5
- package/dist/report-render/views/page-view.d.ts +19 -8
- package/dist/report-render/views/page-view.js +31 -11
- package/dist/report-render/views/prose.d.ts +1 -0
- package/dist/report-render/views/prose.js +7 -13
- package/dist/report-render/views/references-view.js +5 -5
- package/dist/report-render/views/values.d.ts +15 -6
- package/dist/report-render/views/values.js +29 -10
- package/dist/runtime/assemble.d.ts +38 -4
- package/dist/runtime/assemble.js +43 -3
- package/dist/sandbox/k8s-client.js +18 -0
- package/dist/sandbox/mount-plan.d.ts +7 -0
- package/dist/sandbox/mount-plan.js +7 -2
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/state/analyses.d.ts +3 -26
- package/dist/state/analyses.js +6 -64
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +2 -2
- package/dist/state/init.js +23 -10
- package/dist/state/report-session-state.d.ts +27 -0
- package/dist/state/report-session-state.js +29 -2
- package/dist/state/runs.d.ts +10 -0
- package/dist/state/runs.js +21 -0
- package/dist/tasks/data-profile.d.ts +4 -0
- package/dist/tasks/data-profile.js +14 -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/approval/gateway.d.ts +8 -1
- package/dist/tools/approval/gateway.js +9 -2
- package/dist/tools/approval/queries.d.ts +9 -2
- package/dist/tools/approval/queries.js +12 -4
- 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 +3 -1
- package/dist/tools/report-session/index.js +3 -1
- package/dist/tools/report-session/preview-report.d.ts +31 -6
- package/dist/tools/report-session/preview-report.js +63 -16
- 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/dist/workflows/sandbox-step.d.ts +4 -0
- package/dist/workflows/sandbox-step.js +21 -2
- package/package.json +14 -8
|
@@ -19,11 +19,9 @@
|
|
|
19
19
|
import { ok } from "neverthrow";
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { defineTool } from "../define-tool.js";
|
|
22
|
-
import {
|
|
22
|
+
import { capturePage } from "../../lib/page-capture.js";
|
|
23
23
|
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
24
24
|
import { describeMintFailure } from "./preview-publisher.js";
|
|
25
|
-
const NAV_TIMEOUT_MS = 20_000;
|
|
26
|
-
const READY_TIMEOUT_MS = 8_000;
|
|
27
25
|
const WAIT_MS_CAP = 30_000;
|
|
28
26
|
export function createPreviewSnapshotTool(state) {
|
|
29
27
|
const logger = (state.logger ?? createNoopLogger()).named("preview-snapshot").with({ previewId: state.previewId });
|
|
@@ -75,53 +73,16 @@ export function createPreviewSnapshotTool(state) {
|
|
|
75
73
|
state.urlCell.expiresAt = result.data.expiresAt;
|
|
76
74
|
}
|
|
77
75
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
url: req.url(),
|
|
89
|
-
reason: req.failure()?.errorText ?? "unknown",
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
await page.goto(url, {
|
|
93
|
-
waitUntil: "networkidle2",
|
|
94
|
-
timeout: NAV_TIMEOUT_MS,
|
|
95
|
-
});
|
|
96
|
-
// Wait for the inflexa-theme-ready event (fired after echarts theme
|
|
97
|
-
// registers). Check the `__inflexaThemeReady` sentinel first because
|
|
98
|
-
// the page already deferred its dispatch — by the time we get here
|
|
99
|
-
// via networkidle2, the event has likely already fired and a plain
|
|
100
|
-
// addEventListener would block forever. The callback runs in the
|
|
101
|
-
// browser context.
|
|
102
|
-
await page
|
|
103
|
-
.evaluate(new Function("timeout", "return new Promise(function(resolve){if(window.__inflexaThemeReady){resolve();return;}var t=setTimeout(resolve,timeout);document.addEventListener('inflexa-theme-ready',function(){clearTimeout(t);resolve();},{once:true});});"), READY_TIMEOUT_MS)
|
|
104
|
-
.catch(() => {
|
|
105
|
-
/* fall through — capture state as-is */
|
|
106
|
-
});
|
|
107
|
-
if (input.waitForSelector) {
|
|
108
|
-
await page.waitForSelector(input.waitForSelector, { timeout: 5_000 }).catch(() => {
|
|
109
|
-
/* not fatal */
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
if (input.waitMs)
|
|
113
|
-
await new Promise((r) => setTimeout(r, input.waitMs));
|
|
114
|
-
const screenshot = await page.screenshot({
|
|
115
|
-
encoding: "base64",
|
|
116
|
-
fullPage: false,
|
|
117
|
-
});
|
|
118
|
-
return {
|
|
119
|
-
ok: true,
|
|
120
|
-
screenshotBase64: typeof screenshot === "string" ? screenshot : Buffer.from(screenshot).toString("base64"),
|
|
121
|
-
consoleErrors,
|
|
122
|
-
failedRequests,
|
|
123
|
-
};
|
|
124
|
-
}));
|
|
76
|
+
const captured = await capturePage(state.chrome, url, {
|
|
77
|
+
...(input.waitForSelector !== undefined ? { waitForSelector: input.waitForSelector } : {}),
|
|
78
|
+
...(input.waitMs !== undefined ? { waitMs: input.waitMs } : {}),
|
|
79
|
+
});
|
|
80
|
+
return ok({
|
|
81
|
+
ok: true,
|
|
82
|
+
screenshotBase64: captured.screenshotBase64,
|
|
83
|
+
consoleErrors: captured.consoleErrors,
|
|
84
|
+
failedRequests: captured.failedRequests,
|
|
85
|
+
});
|
|
125
86
|
}
|
|
126
87
|
catch (err) {
|
|
127
88
|
return ok({
|
|
@@ -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
|
+
}
|
|
@@ -5,4 +5,6 @@
|
|
|
5
5
|
* render seams, thus each tool binds the per-session state to the thread of the call. The barrel exports
|
|
6
6
|
* the factories and their public types for the composition root.
|
|
7
7
|
*/
|
|
8
|
-
export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps } from "./preview-report.js";
|
|
8
|
+
export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps, type ResolvePageAsset, } 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";
|
|
@@ -5,4 +5,6 @@
|
|
|
5
5
|
* render seams, thus each tool binds the per-session state to the thread of the call. The barrel exports
|
|
6
6
|
* the factories and their public types for the composition root.
|
|
7
7
|
*/
|
|
8
|
-
export { createPreviewReportTool } from "./preview-report.js";
|
|
8
|
+
export { createPreviewReportTool, } from "./preview-report.js";
|
|
9
|
+
export { createExaminePageTool, } from "./examine-page.js";
|
|
10
|
+
export { createRecordVersionTool } from "./record-version.js";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* resolves each reference through the injected resolver, bridges the resolved values into the render model,
|
|
7
7
|
* and renders the page.
|
|
8
8
|
*
|
|
9
|
-
* The page and its staged
|
|
9
|
+
* The page and its staged assets land in `report-sessions/{threadId}/` under the workspace root. That
|
|
10
10
|
* namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
|
|
11
11
|
* trees. The result carries the absolute page path, thus a local host shows the page with no seam.
|
|
12
12
|
*
|
|
@@ -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,21 +69,39 @@ 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;
|
|
68
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* The lookup of the source file of one staged asset. It maps the module specifier of a manifest entry
|
|
82
|
+
* (`report-render/assets.ts`) onto an absolute path on disk.
|
|
83
|
+
*/
|
|
84
|
+
export type ResolvePageAsset = (specifier: string) => string;
|
|
69
85
|
/**
|
|
70
86
|
* The construction deps of the preview tool.
|
|
71
87
|
*
|
|
72
88
|
* `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.
|
|
89
|
+
* serves every analysis and it resolves the root per call from the scope. `makeResolver` is optional, because
|
|
90
|
+
* a resolver realization can be absent. It binds one analysis, thus the tool makes the resolver over the
|
|
91
|
+
* scope of the call.
|
|
92
|
+
*
|
|
93
|
+
* `resolvePageAsset` is optional, and absent it resolves each specifier against the installation of the
|
|
94
|
+
* harness. An embedder that ships the asset bytes packed, for example a compiled single-file binary with no
|
|
95
|
+
* `node_modules` tree, materializes them to disk and binds its own lookup here.
|
|
75
96
|
*/
|
|
76
97
|
export interface PreviewReportToolDeps {
|
|
77
98
|
readonly gateway: ReportSessionStateGateway;
|
|
78
|
-
readonly
|
|
99
|
+
readonly makeResolver?: (scope: {
|
|
100
|
+
analysisId: string;
|
|
101
|
+
auth: AuthContext;
|
|
102
|
+
}) => ReferenceResolver;
|
|
79
103
|
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
104
|
+
readonly resolvePageAsset?: ResolvePageAsset;
|
|
80
105
|
readonly logger?: Logger;
|
|
81
106
|
}
|
|
82
107
|
/**
|