@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
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The authoring tool surface over the draft operations.
|
|
3
3
|
*
|
|
4
|
-
* The factory
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The factory takes a session-state gateway, and it holds no draft. Each tool reads the thread id from the
|
|
5
|
+
* scope of the call, loads the state of that thread, applies the pure operation, and persists the new
|
|
6
|
+
* document. Thus two threads never share one draft, and a landed document outlives a host restart.
|
|
7
7
|
*
|
|
8
|
-
* Each tool maps its flat input onto a core operation, and it applies the pure operation.
|
|
9
|
-
*
|
|
10
|
-
* error channel stays for an unexpected failure.
|
|
8
|
+
* Each tool maps its flat input onto a core operation, and it applies the pure operation. A landed
|
|
9
|
+
* document persists before the tool reports `applied: true`. A refused operation returns typed data in the
|
|
10
|
+
* ok channel, and the error channel stays for an unexpected failure.
|
|
11
|
+
*
|
|
12
|
+
* A call with no report thread in its scope, a thread with no stored state, and a gateway fault each
|
|
13
|
+
* refuse as typed data too. That refusal sits beside the core refusal, and it names the absent thread
|
|
14
|
+
* scope or the absent state.
|
|
11
15
|
*
|
|
12
16
|
* The primitive admits a flat object input only. Thus a destination is four optional fields, and not a
|
|
13
17
|
* nested union. A conflict between the flat fields refuses before the core runs.
|
|
14
18
|
*/
|
|
15
19
|
import { err, ok } from "neverthrow";
|
|
16
20
|
import { z } from "zod";
|
|
21
|
+
import { isSafeId } from "../../workspace/paths.js";
|
|
17
22
|
import { addBlock, changeBlock, locate, moveBlock, removeBlock, setTitle, } from "../../report-model/draft-operations.js";
|
|
18
23
|
import { buildOutline, childOutline, readBlock } from "../../report-model/draft-read.js";
|
|
19
24
|
import { finishDraft } from "../../report-model/draft-finish.js";
|
|
@@ -78,6 +83,44 @@ const setTitleInput = z.object({
|
|
|
78
83
|
title: z.string().min(1),
|
|
79
84
|
});
|
|
80
85
|
const finishDraftInput = z.object({});
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the report thread of a call, and load its state through the gateway.
|
|
88
|
+
*
|
|
89
|
+
* The report thread and its analysis ride on the analysis scope. The thread id becomes one segment of a
|
|
90
|
+
* session directory, thus the safe-id check sits here beside the shape check: a scope whose id carries a
|
|
91
|
+
* separator or a traversal segment names no thread that a tool can write under.
|
|
92
|
+
*
|
|
93
|
+
* The stored analysis of the thread must match the scope. A mismatch reads one analysis's draft and writes
|
|
94
|
+
* into another analysis's workspace, thus the resolution refuses it with a permanent reason that names the
|
|
95
|
+
* two ids. A scope of a different kind, an unsafe id, an absent thread, a wrong thread type, and a gateway
|
|
96
|
+
* fault each refuse as a typed `SessionRefusal` too.
|
|
97
|
+
*
|
|
98
|
+
* The authoring tools and the preview tool share this one resolution, thus the two surfaces accept the
|
|
99
|
+
* same thread id and map a load outcome onto a refusal the same way.
|
|
100
|
+
*/
|
|
101
|
+
export async function openReportThread(gateway, scope) {
|
|
102
|
+
if (scope.kind !== "analysis" || scope.threadId === undefined || !isSafeId(scope.threadId)) {
|
|
103
|
+
return err({ reason: "no-thread-scope", detail: "the scope of the call names no usable report thread id" });
|
|
104
|
+
}
|
|
105
|
+
const { threadId, analysisId } = scope;
|
|
106
|
+
const loaded = await gateway.load(threadId);
|
|
107
|
+
if (loaded.outcome === "absent") {
|
|
108
|
+
return err({ reason: "absent-state", detail: `no report session exists for the thread ${threadId}, and this condition is permanent` });
|
|
109
|
+
}
|
|
110
|
+
if (loaded.outcome === "wrong-type") {
|
|
111
|
+
return err({ reason: "wrong-thread-type", detail: `${loaded.detail}, and this condition is permanent` });
|
|
112
|
+
}
|
|
113
|
+
if (loaded.outcome === "failed") {
|
|
114
|
+
return err({ reason: "state-unavailable", detail: loaded.detail });
|
|
115
|
+
}
|
|
116
|
+
if (loaded.analysisId !== analysisId) {
|
|
117
|
+
return err({
|
|
118
|
+
reason: "scope-analysis-mismatch",
|
|
119
|
+
detail: `the scope names the analysis ${analysisId}, but the thread ${threadId} belongs to the analysis ${loaded.analysisId}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return ok({ threadId, analysisId, state: loaded.state, token: loaded.token, seenDocumentHash: loaded.seenDocumentHash });
|
|
123
|
+
}
|
|
81
124
|
/** Read an absentable field. An explicit `null` and an omitted key both mean that the model gave no value. */
|
|
82
125
|
function given(value) {
|
|
83
126
|
return value ?? undefined;
|
|
@@ -197,37 +240,59 @@ function containerIn(document, parentId) {
|
|
|
197
240
|
/**
|
|
198
241
|
* Every tool of this surface runs inline.
|
|
199
242
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
243
|
+
* A tool loads the state of its thread, applies the pure operation, and persists the result in one body.
|
|
244
|
+
* The reported outline must agree with the row that the body just wrote. A step-mode tool goes through
|
|
245
|
+
* `DBOS.runStep`, which caches the result of a step and replays it without a fresh load. A replay could
|
|
246
|
+
* then report an outline that the current row no longer holds. Inline mode reads and writes the row in one
|
|
247
|
+
* lifetime, thus the outline never drifts from the state.
|
|
205
248
|
*/
|
|
206
249
|
const AUTHORING_EXECUTION_MODE = "inline";
|
|
207
250
|
/**
|
|
208
|
-
* Make the eight authoring tools over
|
|
251
|
+
* Make the eight authoring tools over a session-state gateway.
|
|
209
252
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
253
|
+
* Each tool reads the thread id from the scope of the call, and it loads the state of that thread through
|
|
254
|
+
* the gateway. Thus one factory serves every thread, and two threads never share one draft. A mutation
|
|
255
|
+
* persists the new document before it reports `applied: true`, thus a reported landing is never lost.
|
|
213
256
|
*/
|
|
214
|
-
export function createReportAuthoringTools(
|
|
215
|
-
|
|
216
|
-
|
|
257
|
+
export function createReportAuthoringTools(gateway) {
|
|
258
|
+
/**
|
|
259
|
+
* Resolve the thread of the call, and load its state.
|
|
260
|
+
*
|
|
261
|
+
* The shared `openReportThread` holds the one resolution. A scope of a different kind, an unsafe or
|
|
262
|
+
* absent thread id, an absent row, and a gateway fault each refuse before the call reaches a core
|
|
263
|
+
* operation.
|
|
264
|
+
*/
|
|
265
|
+
const openThread = (ctx) => openReportThread(gateway, ctx.session.scope);
|
|
217
266
|
/**
|
|
218
|
-
*
|
|
267
|
+
* Persist a landing, and report the containers that the operation changed.
|
|
219
268
|
*
|
|
220
|
-
* `holders` names them by id, and it reads the
|
|
221
|
-
* removed block has a holder only in the first and an added block has one only in the second. The
|
|
222
|
-
*
|
|
269
|
+
* `holders` names them by id, and it reads the previous document and the next document, because a
|
|
270
|
+
* removed block has a holder only in the first and an added block has one only in the second. The child
|
|
271
|
+
* order comes out of `next` in either case. A persist fault refuses with `state-unavailable`, thus the
|
|
272
|
+
* tool reports `applied: true` only after the row holds the new document.
|
|
273
|
+
*
|
|
274
|
+
* The persist is a compare-and-swap against the token that the load read. A concurrent turn that landed
|
|
275
|
+
* first turns the persist into a conflict, and the tool refuses with `stale-state`.
|
|
223
276
|
*/
|
|
224
|
-
const land = (
|
|
225
|
-
|
|
226
|
-
|
|
277
|
+
const land = async (threadId, token, previous, result, holders) => {
|
|
278
|
+
if (result.isErr()) {
|
|
279
|
+
return { applied: false, refusal: result.error };
|
|
280
|
+
}
|
|
281
|
+
const next = result.value;
|
|
282
|
+
const persisted = await gateway.persist(threadId, next, token);
|
|
283
|
+
if (persisted.outcome === "conflict") {
|
|
284
|
+
return {
|
|
285
|
+
applied: false,
|
|
286
|
+
refusal: { reason: "stale-state", detail: "another turn changed the report since this turn read it, thus read the state again" },
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (persisted.outcome === "failed") {
|
|
290
|
+
return { applied: false, refusal: { reason: "state-unavailable", detail: persisted.detail } };
|
|
291
|
+
}
|
|
227
292
|
// A move inside one container names the same holder two times.
|
|
228
293
|
const unique = [...new Set(holders(previous, next))];
|
|
229
294
|
return { applied: true, changed: unique.map((parentId) => containerIn(next, parentId)) };
|
|
230
|
-
}
|
|
295
|
+
};
|
|
231
296
|
const refuse = (refusal) => ({ applied: false, refusal });
|
|
232
297
|
const add_block = defineTool({
|
|
233
298
|
id: "add_block",
|
|
@@ -241,16 +306,19 @@ export function createReportAuthoringTools(state) {
|
|
|
241
306
|
const kind = readKind(decodeBlockPayload(input.block));
|
|
242
307
|
return kind !== undefined ? `add ${kind}` : "add a block";
|
|
243
308
|
},
|
|
244
|
-
execute: async (input) => {
|
|
309
|
+
execute: async (input, ctx) => {
|
|
310
|
+
const opened = await openThread(ctx);
|
|
311
|
+
if (opened.isErr()) {
|
|
312
|
+
return ok(refuse(opened.error));
|
|
313
|
+
}
|
|
314
|
+
const { threadId, state, token } = opened.value;
|
|
245
315
|
const destination = toDestination(input);
|
|
246
316
|
if (destination.isErr()) {
|
|
247
317
|
return ok(refuse(destination.error));
|
|
248
318
|
}
|
|
249
319
|
const block = decodeBlockPayload(input.block);
|
|
250
320
|
const addedId = readId(block);
|
|
251
|
-
return ok(land(addBlock(
|
|
252
|
-
addedId === undefined ? undefined : holderIdOf(next, addedId),
|
|
253
|
-
]));
|
|
321
|
+
return ok(await land(threadId, token, state.document, addBlock(state.document, { block, destination: destination.value }, state.snapshot), (_previous, next) => [addedId === undefined ? undefined : holderIdOf(next, addedId)]));
|
|
254
322
|
},
|
|
255
323
|
});
|
|
256
324
|
const change_block = defineTool({
|
|
@@ -260,12 +328,19 @@ export function createReportAuthoringTools(state) {
|
|
|
260
328
|
inputSchema: changeBlockInput,
|
|
261
329
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
262
330
|
describeCall: (input) => `change ${input.targetId}`,
|
|
263
|
-
execute: async (input) => {
|
|
331
|
+
execute: async (input, ctx) => {
|
|
332
|
+
const opened = await openThread(ctx);
|
|
333
|
+
if (opened.isErr()) {
|
|
334
|
+
return ok(refuse(opened.error));
|
|
335
|
+
}
|
|
336
|
+
const { threadId, state, token } = opened.value;
|
|
264
337
|
const operation = toChangeOperation(input);
|
|
265
338
|
if (operation.isErr()) {
|
|
266
339
|
return ok(refuse(operation.error));
|
|
267
340
|
}
|
|
268
|
-
return ok(land(changeBlock(
|
|
341
|
+
return ok(await land(threadId, token, state.document, changeBlock(state.document, operation.value, state.snapshot), (_previous, next) => [
|
|
342
|
+
holderIdOf(next, input.targetId),
|
|
343
|
+
]));
|
|
269
344
|
},
|
|
270
345
|
});
|
|
271
346
|
const remove_block = defineTool({
|
|
@@ -274,7 +349,16 @@ export function createReportAuthoringTools(state) {
|
|
|
274
349
|
inputSchema: removeBlockInput,
|
|
275
350
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
276
351
|
describeCall: (input) => `remove ${input.targetId}`,
|
|
277
|
-
execute: async (input
|
|
352
|
+
execute: async (input, ctx) => {
|
|
353
|
+
const opened = await openThread(ctx);
|
|
354
|
+
if (opened.isErr()) {
|
|
355
|
+
return ok(refuse(opened.error));
|
|
356
|
+
}
|
|
357
|
+
const { threadId, state, token } = opened.value;
|
|
358
|
+
return ok(await land(threadId, token, state.document, removeBlock(state.document, { targetId: input.targetId }, state.snapshot), (previous) => [
|
|
359
|
+
holderIdOf(previous, input.targetId),
|
|
360
|
+
]));
|
|
361
|
+
},
|
|
278
362
|
});
|
|
279
363
|
const move_block = defineTool({
|
|
280
364
|
id: "move_block",
|
|
@@ -283,15 +367,17 @@ export function createReportAuthoringTools(state) {
|
|
|
283
367
|
inputSchema: moveBlockInput,
|
|
284
368
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
285
369
|
describeCall: (input) => `move ${input.targetId}`,
|
|
286
|
-
execute: async (input) => {
|
|
370
|
+
execute: async (input, ctx) => {
|
|
371
|
+
const opened = await openThread(ctx);
|
|
372
|
+
if (opened.isErr()) {
|
|
373
|
+
return ok(refuse(opened.error));
|
|
374
|
+
}
|
|
375
|
+
const { threadId, state, token } = opened.value;
|
|
287
376
|
const destination = toDestination(input);
|
|
288
377
|
if (destination.isErr()) {
|
|
289
378
|
return ok(refuse(destination.error));
|
|
290
379
|
}
|
|
291
|
-
return ok(land(moveBlock(
|
|
292
|
-
holderIdOf(previous, input.targetId),
|
|
293
|
-
holderIdOf(next, input.targetId),
|
|
294
|
-
]));
|
|
380
|
+
return ok(await land(threadId, token, state.document, moveBlock(state.document, { targetId: input.targetId, destination: destination.value }, state.snapshot), (previous, next) => [holderIdOf(previous, input.targetId), holderIdOf(next, input.targetId)]));
|
|
295
381
|
},
|
|
296
382
|
});
|
|
297
383
|
const set_title = defineTool({
|
|
@@ -302,7 +388,14 @@ export function createReportAuthoringTools(state) {
|
|
|
302
388
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
303
389
|
describeCall: (input) => `title the report ${input.title}`,
|
|
304
390
|
// The title sits on the document, thus no container changes its child order.
|
|
305
|
-
execute: async (input
|
|
391
|
+
execute: async (input, ctx) => {
|
|
392
|
+
const opened = await openThread(ctx);
|
|
393
|
+
if (opened.isErr()) {
|
|
394
|
+
return ok(refuse(opened.error));
|
|
395
|
+
}
|
|
396
|
+
const { threadId, state, token } = opened.value;
|
|
397
|
+
return ok(await land(threadId, token, state.document, ok(setTitle(state.document, input.title)), () => []));
|
|
398
|
+
},
|
|
306
399
|
});
|
|
307
400
|
const read_outline = defineTool({
|
|
308
401
|
id: "read_outline",
|
|
@@ -313,7 +406,13 @@ export function createReportAuthoringTools(state) {
|
|
|
313
406
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
314
407
|
// The input carries no field, thus a hook can only restate the name of the tool.
|
|
315
408
|
describeCall: "none",
|
|
316
|
-
execute: async () =>
|
|
409
|
+
execute: async (_input, ctx) => {
|
|
410
|
+
const opened = await openThread(ctx);
|
|
411
|
+
if (opened.isErr()) {
|
|
412
|
+
return ok({ refused: opened.error });
|
|
413
|
+
}
|
|
414
|
+
return ok({ outline: buildOutline(opened.value.state.document) });
|
|
415
|
+
},
|
|
317
416
|
});
|
|
318
417
|
const read_block = defineTool({
|
|
319
418
|
id: "read_block",
|
|
@@ -322,8 +421,12 @@ export function createReportAuthoringTools(state) {
|
|
|
322
421
|
inputSchema: readBlockInput,
|
|
323
422
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
324
423
|
describeCall: (input) => `read ${input.targetId}`,
|
|
325
|
-
execute: async (input) => {
|
|
326
|
-
const
|
|
424
|
+
execute: async (input, ctx) => {
|
|
425
|
+
const opened = await openThread(ctx);
|
|
426
|
+
if (opened.isErr()) {
|
|
427
|
+
return ok({ refused: opened.error });
|
|
428
|
+
}
|
|
429
|
+
const block = readBlock(opened.value.state.document, input.targetId);
|
|
327
430
|
const result = block !== undefined ? { found: true, block } : { found: false };
|
|
328
431
|
return ok(result);
|
|
329
432
|
},
|
|
@@ -336,7 +439,13 @@ export function createReportAuthoringTools(state) {
|
|
|
336
439
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
337
440
|
// The input carries no field, thus a hook can only restate the name of the tool.
|
|
338
441
|
describeCall: "none",
|
|
339
|
-
execute: async () =>
|
|
442
|
+
execute: async (_input, ctx) => {
|
|
443
|
+
const opened = await openThread(ctx);
|
|
444
|
+
if (opened.isErr()) {
|
|
445
|
+
return ok({ refused: opened.error });
|
|
446
|
+
}
|
|
447
|
+
return ok(finishDraft(opened.value.state.document, opened.value.state.snapshot));
|
|
448
|
+
},
|
|
340
449
|
});
|
|
341
450
|
return {
|
|
342
451
|
add_block,
|
|
@@ -347,6 +456,5 @@ export function createReportAuthoringTools(state) {
|
|
|
347
456
|
read_outline,
|
|
348
457
|
read_block,
|
|
349
458
|
finish_draft,
|
|
350
|
-
currentDraft: () => draft,
|
|
351
459
|
};
|
|
352
460
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report-session tool barrel.
|
|
3
|
+
*
|
|
4
|
+
* These tools serve a `report` thread. The runtime constructs them over the session-state gateway and the
|
|
5
|
+
* render seams, thus each tool binds the per-session state to the thread of the call. The barrel exports
|
|
6
|
+
* the factories and their public types for the composition root.
|
|
7
|
+
*/
|
|
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";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report-session tool barrel.
|
|
3
|
+
*
|
|
4
|
+
* These tools serve a `report` thread. The runtime constructs them over the session-state gateway and the
|
|
5
|
+
* render seams, thus each tool binds the per-session state to the thread of the call. The barrel exports
|
|
6
|
+
* the factories and their public types for the composition root.
|
|
7
|
+
*/
|
|
8
|
+
export { createPreviewReportTool } from "./preview-report.js";
|
|
9
|
+
export { createExaminePageTool, } from "./examine-page.js";
|
|
10
|
+
export { createRecordVersionTool } from "./record-version.js";
|