@e2e-dev/decision 0.2.0-nightly-20261009094406 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -21
- package/dist/.build.json +1 -1
- package/dist/budget.d.ts +19 -0
- package/dist/budget.js +29 -0
- package/dist/decide.d.ts +8 -1
- package/dist/decide.js +70 -37
- package/dist/dispatch.d.ts +37 -0
- package/dist/dispatch.js +67 -0
- package/dist/elements.d.ts +32 -7
- package/dist/elements.js +153 -74
- package/dist/executor.js +300 -215
- package/dist/hash.d.ts +2 -0
- package/dist/hash.js +21 -0
- package/dist/locator.d.ts +38 -0
- package/dist/locator.js +91 -0
- package/dist/overlay.d.ts +59 -0
- package/dist/overlay.js +121 -0
- package/dist/params.d.ts +3 -0
- package/dist/params.js +39 -0
- package/dist/picks.d.ts +36 -0
- package/dist/picks.js +49 -0
- package/dist/questions.d.ts +56 -35
- package/dist/questions.js +203 -112
- package/dist/text.d.ts +31 -5
- package/dist/text.js +67 -53
- package/dist/types.d.ts +12 -0
- package/package.json +10 -7
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { StepExecutorContext } from 'e2e';
|
|
2
|
+
import type { Decision } from './decide.ts';
|
|
3
|
+
import { type Point, type Screenshot } from './overlay.ts';
|
|
4
|
+
import { type Gates } from './picks.ts';
|
|
5
|
+
import { type DecisionRequest } from './questions.ts';
|
|
6
|
+
/** Asks the decision model one request, or nothing once the step budget is spent. */
|
|
7
|
+
export type AskDecision = (request: DecisionRequest) => Promise<Record<string, Decision> | undefined>;
|
|
8
|
+
/** Where a drawn control is, or why the looks could not say. */
|
|
9
|
+
export type Located = Point | {
|
|
10
|
+
readonly uncertain: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Locates drawn controls on the screenshot and remembers the ones a tap
|
|
14
|
+
* found: two looks per control, the column and row on the plain full
|
|
15
|
+
* screenshot, then again on a zoomed crop with a grid drawn on it. A tap
|
|
16
|
+
* that changed the pixels around it found its control; the same control on
|
|
17
|
+
* the same page is tapped there again with no new looks.
|
|
18
|
+
*/
|
|
19
|
+
export declare class PointLocator {
|
|
20
|
+
private readonly ctx;
|
|
21
|
+
private readonly ask;
|
|
22
|
+
private readonly gates;
|
|
23
|
+
/** Points that landed, by path and the control the text model named. */
|
|
24
|
+
private readonly landed;
|
|
25
|
+
/** The last point tap, settled against the next observation. */
|
|
26
|
+
private pending;
|
|
27
|
+
constructor(ctx: StepExecutorContext, ask: AskDecision, gates: Gates);
|
|
28
|
+
/** A point already known for this control on this page. */
|
|
29
|
+
known(path: string, wanted: string): Point | undefined;
|
|
30
|
+
/** Records a point tap, to be settled by the next observation. */
|
|
31
|
+
tapped(path: string, wanted: string, point: Point, screenshot: Screenshot): void;
|
|
32
|
+
/** Settles the last point tap against the newest pixels: changed means it landed. */
|
|
33
|
+
settle(screenshot: Screenshot | undefined): void;
|
|
34
|
+
/** Two looks for the control, or undefined once the budget is spent. */
|
|
35
|
+
locate(screenshot: Screenshot, path: string, wanted: string): Promise<Located | undefined>;
|
|
36
|
+
/** The column and row scores of a point request, gated like any answer. */
|
|
37
|
+
private scored;
|
|
38
|
+
}
|
package/dist/locator.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { CELL_TARGET, gridFor, pointOf, regionHash, withGrid, zoomAround } from './overlay.js';
|
|
2
|
+
import { describe, gated, invalid, need } from './picks.js';
|
|
3
|
+
import { pointRequest } from './questions.js';
|
|
4
|
+
/**
|
|
5
|
+
* CSS pixels of the region the second look zooms into, and how much it is
|
|
6
|
+
* enlarged: wide enough to hold a first look one column off. A tighter
|
|
7
|
+
* third look was measured to drift on the runner's 768px capture.
|
|
8
|
+
*/
|
|
9
|
+
const ZOOM_BOX = 400;
|
|
10
|
+
const ZOOM_FACTOR = 2;
|
|
11
|
+
/** Columns and rows of the grid drawn on the zoomed region. */
|
|
12
|
+
const ZOOM_CELLS = 8;
|
|
13
|
+
/**
|
|
14
|
+
* Locates drawn controls on the screenshot and remembers the ones a tap
|
|
15
|
+
* found: two looks per control, the column and row on the plain full
|
|
16
|
+
* screenshot, then again on a zoomed crop with a grid drawn on it. A tap
|
|
17
|
+
* that changed the pixels around it found its control; the same control on
|
|
18
|
+
* the same page is tapped there again with no new looks.
|
|
19
|
+
*/
|
|
20
|
+
export class PointLocator {
|
|
21
|
+
ctx;
|
|
22
|
+
ask;
|
|
23
|
+
gates;
|
|
24
|
+
/** Points that landed, by path and the control the text model named. */
|
|
25
|
+
landed = new Map();
|
|
26
|
+
/** The last point tap, settled against the next observation. */
|
|
27
|
+
pending;
|
|
28
|
+
constructor(ctx, ask, gates) {
|
|
29
|
+
this.ctx = ctx;
|
|
30
|
+
this.ask = ask;
|
|
31
|
+
this.gates = gates;
|
|
32
|
+
}
|
|
33
|
+
/** A point already known for this control on this page. */
|
|
34
|
+
known(path, wanted) {
|
|
35
|
+
return this.landed.get(key(path, wanted));
|
|
36
|
+
}
|
|
37
|
+
/** Records a point tap, to be settled by the next observation. */
|
|
38
|
+
tapped(path, wanted, point, screenshot) {
|
|
39
|
+
this.pending = { key: key(path, wanted), point, before: regionHash(screenshot, point, ZOOM_BOX) };
|
|
40
|
+
}
|
|
41
|
+
/** Settles the last point tap against the newest pixels: changed means it landed. */
|
|
42
|
+
settle(screenshot) {
|
|
43
|
+
if (this.pending === undefined)
|
|
44
|
+
return;
|
|
45
|
+
const after = regionHash(screenshot, this.pending.point, ZOOM_BOX);
|
|
46
|
+
if (after !== undefined && after !== this.pending.before)
|
|
47
|
+
this.landed.set(this.pending.key, this.pending.point);
|
|
48
|
+
else
|
|
49
|
+
this.landed.delete(this.pending.key);
|
|
50
|
+
this.pending = undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Two looks for the control, or undefined once the budget is spent. */
|
|
53
|
+
async locate(screenshot, path, wanted) {
|
|
54
|
+
const grid = gridFor(screenshot.width / screenshot.scale, screenshot.height / screenshot.scale, CELL_TARGET);
|
|
55
|
+
const coarse = await this.ask(pointRequest(this.ctx, path, screenshot, grid, 'This image is the full screenshot; no grid is drawn on it.', wanted));
|
|
56
|
+
if (coarse === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
const first = this.scored(coarse);
|
|
59
|
+
if ('uncertain' in first)
|
|
60
|
+
return first;
|
|
61
|
+
const rough = pointOf(grid, first.x, first.y);
|
|
62
|
+
const zoom = zoomAround(screenshot, rough, ZOOM_BOX, ZOOM_FACTOR);
|
|
63
|
+
if (zoom === undefined)
|
|
64
|
+
return rough;
|
|
65
|
+
const side = zoom.screenshot.width;
|
|
66
|
+
const fine = { columns: ZOOM_CELLS, rows: ZOOM_CELLS, cellWidth: side / ZOOM_CELLS, cellHeight: side / ZOOM_CELLS };
|
|
67
|
+
const region = Math.round(side / ZOOM_FACTOR);
|
|
68
|
+
const framing = `This image is a ${ZOOM_FACTOR}x zoom of a ${region} by ${region} pixel region of the screen.`;
|
|
69
|
+
const closer = await this.ask(pointRequest(this.ctx, path, withGrid(zoom.screenshot, fine), fine, framing, wanted));
|
|
70
|
+
if (closer === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
const second = this.scored(closer);
|
|
73
|
+
if ('uncertain' in second)
|
|
74
|
+
return second;
|
|
75
|
+
const inZoom = pointOf(fine, second.x, second.y);
|
|
76
|
+
return { x: Math.round(zoom.origin.x + inZoom.x / zoom.factor), y: Math.round(zoom.origin.y + inZoom.y / zoom.factor) };
|
|
77
|
+
}
|
|
78
|
+
/** The column and row scores of a point request, gated like any answer. */
|
|
79
|
+
scored(answers) {
|
|
80
|
+
const x = need(answers.x, 'x');
|
|
81
|
+
const y = need(answers.y, 'y');
|
|
82
|
+
if (x.score === undefined || y.score === undefined)
|
|
83
|
+
throw invalid('The decision model answered a point question without a score.');
|
|
84
|
+
if (!gated(x, this.gates) || !gated(y, this.gates))
|
|
85
|
+
return { uncertain: `x ${describe(x)}; y ${describe(y)}` };
|
|
86
|
+
return { x: x.score, y: y.score };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function key(path, wanted) {
|
|
90
|
+
return `${path}\0${wanted}`;
|
|
91
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ExecutorPixels } from 'e2e';
|
|
2
|
+
/** Pixels as the decision model receives them: the runner's capture without its masking count. */
|
|
3
|
+
export type Screenshot = Omit<ExecutorPixels, 'maskedRegionCount'>;
|
|
4
|
+
/** A viewport point in CSS pixels. */
|
|
5
|
+
export interface Point {
|
|
6
|
+
readonly x: number;
|
|
7
|
+
readonly y: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A grid over an image: at most ten columns and rows, one score level per
|
|
11
|
+
* column and row. Sizes are in the CSS pixels of the image's coordinates.
|
|
12
|
+
*/
|
|
13
|
+
export interface Grid {
|
|
14
|
+
readonly columns: number;
|
|
15
|
+
readonly rows: number;
|
|
16
|
+
readonly cellWidth: number;
|
|
17
|
+
readonly cellHeight: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Target cell size of a first look in CSS pixels. A 1280 by 720 viewport
|
|
21
|
+
* gets 10 columns of 128px and 9 rows of 80px; measured on gpt-6-luna over
|
|
22
|
+
* a drawn keypad, the probability-weighted column and row land within a
|
|
23
|
+
* column of every key.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CELL_TARGET = 80;
|
|
26
|
+
/**
|
|
27
|
+
* The grid for an image: as close to `cellTarget` CSS pixels per cell as
|
|
28
|
+
* the level cap allows. A 1280 by 720 viewport with an 80px target gets 10
|
|
29
|
+
* columns of 128px and 9 rows of 80px.
|
|
30
|
+
*/
|
|
31
|
+
export declare function gridFor(width: number, height: number, cellTarget: number): Grid;
|
|
32
|
+
/** The point a column and row score name on a grid: the center of the scored position, inside the image. */
|
|
33
|
+
export declare function pointOf(grid: Grid, column: number, row: number): Point;
|
|
34
|
+
/** A zoomed crop: the image, and where its top-left corner sits in the screenshot's CSS pixels. */
|
|
35
|
+
export interface Zoom {
|
|
36
|
+
readonly screenshot: Screenshot;
|
|
37
|
+
readonly origin: Point;
|
|
38
|
+
readonly factor: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The `box` by `box` CSS-pixel region around `center`, kept inside the
|
|
42
|
+
* screenshot and enlarged `factor` times (nearest neighbor), for a finer
|
|
43
|
+
* second look at a drawn control. Undefined when the pixels cannot be decoded.
|
|
44
|
+
*/
|
|
45
|
+
export declare function zoomAround(screenshot: Screenshot, center: Point, box: number, factor: number): Zoom | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* A hash of the `box` by `box` CSS-pixel region around `center`: what a tap
|
|
48
|
+
* there changed, read without the rest of the page. Undefined without
|
|
49
|
+
* pixels or when they cannot be decoded.
|
|
50
|
+
*/
|
|
51
|
+
export declare function regionHash(screenshot: Screenshot | undefined, center: Point, box: number): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The screenshot with the grid's cell borders drawn on it. Measured on
|
|
54
|
+
* gpt-6-luna over a drawn keypad at the runner's 768px capture, the zoomed
|
|
55
|
+
* look lands within 30px of every key with the lines, and a cell number
|
|
56
|
+
* drawn in each corner only covered the glyphs it was meant to help find.
|
|
57
|
+
* Returns the input untouched when it cannot be decoded.
|
|
58
|
+
*/
|
|
59
|
+
export declare function withGrid(screenshot: Screenshot, grid: Grid): Screenshot;
|
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { PNG } from 'pngjs';
|
|
2
|
+
import { fnv1a } from './hash.js';
|
|
3
|
+
/** The Decisions API scores at most ten levels per question. */
|
|
4
|
+
const MAX_LEVELS = 10;
|
|
5
|
+
/**
|
|
6
|
+
* Target cell size of a first look in CSS pixels. A 1280 by 720 viewport
|
|
7
|
+
* gets 10 columns of 128px and 9 rows of 80px; measured on gpt-6-luna over
|
|
8
|
+
* a drawn keypad, the probability-weighted column and row land within a
|
|
9
|
+
* column of every key.
|
|
10
|
+
*/
|
|
11
|
+
export const CELL_TARGET = 80;
|
|
12
|
+
/**
|
|
13
|
+
* The grid for an image: as close to `cellTarget` CSS pixels per cell as
|
|
14
|
+
* the level cap allows. A 1280 by 720 viewport with an 80px target gets 10
|
|
15
|
+
* columns of 128px and 9 rows of 80px.
|
|
16
|
+
*/
|
|
17
|
+
export function gridFor(width, height, cellTarget) {
|
|
18
|
+
const columns = Math.min(MAX_LEVELS, Math.max(1, Math.round(width / cellTarget)));
|
|
19
|
+
const rows = Math.min(MAX_LEVELS, Math.max(1, Math.round(height / cellTarget)));
|
|
20
|
+
return { columns, rows, cellWidth: width / columns, cellHeight: height / rows };
|
|
21
|
+
}
|
|
22
|
+
/** The point a column and row score name on a grid: the center of the scored position, inside the image. */
|
|
23
|
+
export function pointOf(grid, column, row) {
|
|
24
|
+
const clamp = (value, max) => Math.min(max - 1, Math.max(0, Math.round(value)));
|
|
25
|
+
return {
|
|
26
|
+
x: clamp((column + 0.5) * grid.cellWidth, grid.columns * grid.cellWidth),
|
|
27
|
+
y: clamp((row + 0.5) * grid.cellHeight, grid.rows * grid.cellHeight),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The `box` by `box` CSS-pixel region around `center`, kept inside the
|
|
32
|
+
* screenshot and enlarged `factor` times (nearest neighbor), for a finer
|
|
33
|
+
* second look at a drawn control. Undefined when the pixels cannot be decoded.
|
|
34
|
+
*/
|
|
35
|
+
export function zoomAround(screenshot, center, box, factor) {
|
|
36
|
+
const png = decode(screenshot);
|
|
37
|
+
if (png === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const scale = screenshot.scale;
|
|
40
|
+
const cssWidth = png.width / scale;
|
|
41
|
+
const cssHeight = png.height / scale;
|
|
42
|
+
const side = Math.min(box, cssWidth, cssHeight);
|
|
43
|
+
const origin = {
|
|
44
|
+
x: Math.round(Math.max(0, Math.min(cssWidth - side, center.x - side / 2))),
|
|
45
|
+
y: Math.round(Math.max(0, Math.min(cssHeight - side, center.y - side / 2))),
|
|
46
|
+
};
|
|
47
|
+
const size = Math.round(side * factor);
|
|
48
|
+
const out = new PNG({ width: size, height: size });
|
|
49
|
+
for (let y = 0; y < size; y += 1) {
|
|
50
|
+
for (let x = 0; x < size; x += 1) {
|
|
51
|
+
const sx = Math.min(png.width - 1, Math.floor((origin.x + x / factor) * scale));
|
|
52
|
+
const sy = Math.min(png.height - 1, Math.floor((origin.y + y / factor) * scale));
|
|
53
|
+
const from = (sy * png.width + sx) * 4;
|
|
54
|
+
const to = (y * size + x) * 4;
|
|
55
|
+
out.data[to] = png.data[from] ?? 0;
|
|
56
|
+
out.data[to + 1] = png.data[from + 1] ?? 0;
|
|
57
|
+
out.data[to + 2] = png.data[from + 2] ?? 0;
|
|
58
|
+
out.data[to + 3] = 255;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { screenshot: { mediaType: 'image/png', data: new Uint8Array(PNG.sync.write(out)), width: size, height: size, scale: 1 }, origin, factor };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A hash of the `box` by `box` CSS-pixel region around `center`: what a tap
|
|
65
|
+
* there changed, read without the rest of the page. Undefined without
|
|
66
|
+
* pixels or when they cannot be decoded.
|
|
67
|
+
*/
|
|
68
|
+
export function regionHash(screenshot, center, box) {
|
|
69
|
+
if (screenshot === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
const zoom = zoomAround(screenshot, center, box, 1);
|
|
72
|
+
return zoom === undefined ? undefined : fnv1a([zoom.screenshot.data]);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The screenshot with the grid's cell borders drawn on it. Measured on
|
|
76
|
+
* gpt-6-luna over a drawn keypad at the runner's 768px capture, the zoomed
|
|
77
|
+
* look lands within 30px of every key with the lines, and a cell number
|
|
78
|
+
* drawn in each corner only covered the glyphs it was meant to help find.
|
|
79
|
+
* Returns the input untouched when it cannot be decoded.
|
|
80
|
+
*/
|
|
81
|
+
export function withGrid(screenshot, grid) {
|
|
82
|
+
const png = decode(screenshot);
|
|
83
|
+
if (png === undefined)
|
|
84
|
+
return screenshot;
|
|
85
|
+
const scale = screenshot.scale;
|
|
86
|
+
for (let row = 0; row < grid.rows; row += 1) {
|
|
87
|
+
for (let column = 0; column < grid.columns; column += 1) {
|
|
88
|
+
const x0 = Math.round(column * grid.cellWidth * scale);
|
|
89
|
+
const y0 = Math.round(row * grid.cellHeight * scale);
|
|
90
|
+
const x1 = Math.min(png.width, Math.round((column + 1) * grid.cellWidth * scale));
|
|
91
|
+
const y1 = Math.min(png.height, Math.round((row + 1) * grid.cellHeight * scale));
|
|
92
|
+
for (let x = x0; x < x1; x += 1) {
|
|
93
|
+
paint(png, x, y0, GRID);
|
|
94
|
+
paint(png, x, y1 - 1, GRID);
|
|
95
|
+
}
|
|
96
|
+
for (let y = y0; y < y1; y += 1) {
|
|
97
|
+
paint(png, x0, y, GRID);
|
|
98
|
+
paint(png, x1 - 1, y, GRID);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { ...screenshot, data: new Uint8Array(PNG.sync.write(png)) };
|
|
103
|
+
}
|
|
104
|
+
function decode(screenshot) {
|
|
105
|
+
try {
|
|
106
|
+
return PNG.sync.read(Buffer.from(screenshot.data));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const GRID = [255, 0, 255];
|
|
113
|
+
function paint(png, x, y, color) {
|
|
114
|
+
if (x < 0 || y < 0 || x >= png.width || y >= png.height)
|
|
115
|
+
return;
|
|
116
|
+
const offset = (y * png.width + x) * 4;
|
|
117
|
+
png.data[offset] = color[0];
|
|
118
|
+
png.data[offset + 1] = color[1];
|
|
119
|
+
png.data[offset + 2] = color[2];
|
|
120
|
+
png.data[offset + 3] = 255;
|
|
121
|
+
}
|
package/dist/params.d.ts
ADDED
package/dist/params.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The step params minus secret projections, which travel only as handles. */
|
|
2
|
+
export function nonSecretParams(params) {
|
|
3
|
+
const kept = {};
|
|
4
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
5
|
+
const cleaned = withoutSecret(value);
|
|
6
|
+
if (cleaned === undefined)
|
|
7
|
+
continue;
|
|
8
|
+
setKey(kept, key, cleaned);
|
|
9
|
+
}
|
|
10
|
+
return kept;
|
|
11
|
+
}
|
|
12
|
+
/** Removes secret-marker leaves at any depth. Undefined means the value itself was one. */
|
|
13
|
+
function withoutSecret(value) {
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return value.map((item) => withoutSecret(item) ?? null);
|
|
16
|
+
if (typeof value === 'object' && value !== null) {
|
|
17
|
+
const record = value;
|
|
18
|
+
if (record['kind'] === 'secret')
|
|
19
|
+
return undefined;
|
|
20
|
+
const kept = {};
|
|
21
|
+
for (const [key, item] of Object.entries(record)) {
|
|
22
|
+
const cleaned = withoutSecret(item);
|
|
23
|
+
if (cleaned === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
setKey(kept, key, cleaned);
|
|
26
|
+
}
|
|
27
|
+
return kept;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
/** Assigns an own property even for `__proto__`, which a plain assignment would not create. */
|
|
32
|
+
function setKey(record, key, value) {
|
|
33
|
+
if (key === '__proto__') {
|
|
34
|
+
Object.defineProperty(record, key, { value, enumerable: true, configurable: true, writable: true });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
record[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/picks.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AgentError } from 'e2e/agent';
|
|
2
|
+
import type { Decision } from './decide.ts';
|
|
3
|
+
/** The gates every answer passes: the selected option's probability and the provider's confidence. */
|
|
4
|
+
export interface Gates {
|
|
5
|
+
readonly minProbability: number;
|
|
6
|
+
readonly minConfidence: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* What a target question settled: the offered value, with the answer that
|
|
10
|
+
* chose it when one was asked, or why the step cannot go on.
|
|
11
|
+
*/
|
|
12
|
+
export type Chosen<T> = {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly value: T;
|
|
15
|
+
readonly answer?: Decision;
|
|
16
|
+
} | {
|
|
17
|
+
readonly blocked: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a target answer against what was offered. A lone offer needs no
|
|
21
|
+
* answer: the request never asked. The model's `none`, and an answer under
|
|
22
|
+
* the gates, block the step. Anything the request never offered is
|
|
23
|
+
* MODEL_OUTPUT_INVALID: our validation, never the provider's text.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pick<T>(answer: Decision | undefined, offered: ReadonlyMap<string, T>, gates: Gates, what: string, operation: string): Chosen<T>;
|
|
26
|
+
/** What a verdict question can settle on. */
|
|
27
|
+
export type Verdict = 'holds' | 'fails' | 'inconclusive';
|
|
28
|
+
/** The verdict answer, typed, or `below gate` when it fails the gates; a verdict never offered is MODEL_OUTPUT_INVALID. */
|
|
29
|
+
export declare function verdictOf(answer: Decision | undefined, gates: Gates): Verdict | 'below gate';
|
|
30
|
+
/** A model answer the request never offered: our validation, never the provider's text. */
|
|
31
|
+
export declare function invalid(message: string): AgentError;
|
|
32
|
+
/** Requires an answer the model returned; its absence is our bug or a broken transport. */
|
|
33
|
+
export declare function need(answer: Decision | undefined, what: string): Decision;
|
|
34
|
+
export declare function gated(decision: Decision, gates: Gates): boolean;
|
|
35
|
+
/** How an answer reads in a verdict or a turn: `p=0.950, confidence 0.930`. */
|
|
36
|
+
export declare function describe(decision: Decision): string;
|
package/dist/picks.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AgentError } from 'e2e/agent';
|
|
2
|
+
import { NONE } from './questions.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a target answer against what was offered. A lone offer needs no
|
|
5
|
+
* answer: the request never asked. The model's `none`, and an answer under
|
|
6
|
+
* the gates, block the step. Anything the request never offered is
|
|
7
|
+
* MODEL_OUTPUT_INVALID: our validation, never the provider's text.
|
|
8
|
+
*/
|
|
9
|
+
export function pick(answer, offered, gates, what, operation) {
|
|
10
|
+
if (answer === undefined) {
|
|
11
|
+
const [lone] = offered;
|
|
12
|
+
if (offered.size === 1 && lone !== undefined)
|
|
13
|
+
return { key: lone[0], value: lone[1] };
|
|
14
|
+
throw invalid(`The decision model returned no ${what} answer.`);
|
|
15
|
+
}
|
|
16
|
+
if (answer.choice === NONE)
|
|
17
|
+
return { blocked: `The decision model chose ${operation} but no ${what} for it (${describe(answer)}).` };
|
|
18
|
+
const value = offered.get(answer.choice);
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
throw invalid(`The decision model chose an unavailable ${what}.`);
|
|
21
|
+
if (!gated(answer, gates))
|
|
22
|
+
return { blocked: `The ${what} is uncertain (${describe(answer)}).` };
|
|
23
|
+
return { key: answer.choice, value, answer };
|
|
24
|
+
}
|
|
25
|
+
const VERDICTS = new Set(['holds', 'fails', 'inconclusive']);
|
|
26
|
+
/** The verdict answer, typed, or `below gate` when it fails the gates; a verdict never offered is MODEL_OUTPUT_INVALID. */
|
|
27
|
+
export function verdictOf(answer, gates) {
|
|
28
|
+
const verdict = need(answer, 'verdict');
|
|
29
|
+
if (!VERDICTS.has(verdict.choice))
|
|
30
|
+
throw invalid('The decision model chose an unavailable verdict.');
|
|
31
|
+
return gated(verdict, gates) ? verdict.choice : 'below gate';
|
|
32
|
+
}
|
|
33
|
+
/** A model answer the request never offered: our validation, never the provider's text. */
|
|
34
|
+
export function invalid(message) {
|
|
35
|
+
return new AgentError('MODEL_OUTPUT_INVALID', message);
|
|
36
|
+
}
|
|
37
|
+
/** Requires an answer the model returned; its absence is our bug or a broken transport. */
|
|
38
|
+
export function need(answer, what) {
|
|
39
|
+
if (answer === undefined)
|
|
40
|
+
throw invalid(`The decision model returned no ${what} answer.`);
|
|
41
|
+
return answer;
|
|
42
|
+
}
|
|
43
|
+
export function gated(decision, gates) {
|
|
44
|
+
return decision.probability >= gates.minProbability && decision.confidence >= gates.minConfidence;
|
|
45
|
+
}
|
|
46
|
+
/** How an answer reads in a verdict or a turn: `p=0.950, confidence 0.930`. */
|
|
47
|
+
export function describe(decision) {
|
|
48
|
+
return `p=${decision.probability.toFixed(3)}, confidence ${decision.confidence.toFixed(3)}`;
|
|
49
|
+
}
|
package/dist/questions.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { JsonValue, StepExecutorContext } from 'e2e';
|
|
2
|
-
import type
|
|
2
|
+
import { type ActionSpace, type Operation } from './elements.ts';
|
|
3
|
+
import type { Grid, Screenshot } from './overlay.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The fallback every target question carries. A provider that finds no
|
|
6
|
+
* applicable target for an operation the step will not take (OpenAI refuses
|
|
7
|
+
* such a question outright) chooses this instead, and the step reads it as
|
|
8
|
+
* "not this operation".
|
|
9
|
+
*/
|
|
10
|
+
export declare const NONE = "none";
|
|
3
11
|
/** One step of history the decision model reads. */
|
|
4
12
|
export interface HistoryEntry {
|
|
5
13
|
readonly action: string;
|
|
@@ -8,49 +16,62 @@ export interface HistoryEntry {
|
|
|
8
16
|
readonly error?: string;
|
|
9
17
|
readonly replayed?: true;
|
|
10
18
|
readonly uncertain?: true;
|
|
19
|
+
/** What the engine reported for the action, when that tells the model something. */
|
|
20
|
+
readonly note?: string;
|
|
11
21
|
}
|
|
12
|
-
/** One
|
|
13
|
-
|
|
22
|
+
/** One question as the request sends it: a choice over named criteria, or a score over ordered levels. */
|
|
23
|
+
export type Question = {
|
|
14
24
|
readonly type: 'choice';
|
|
15
|
-
readonly instructions:
|
|
16
|
-
readonly criteria: Record<string,
|
|
17
|
-
}
|
|
25
|
+
readonly instructions: string;
|
|
26
|
+
readonly criteria: Record<string, string | null>;
|
|
27
|
+
} | {
|
|
28
|
+
readonly type: 'score';
|
|
29
|
+
readonly instructions: string;
|
|
30
|
+
readonly criteria: readonly (string | null)[];
|
|
31
|
+
};
|
|
18
32
|
export interface DecisionRequest {
|
|
19
33
|
readonly state: Record<string, JsonValue>;
|
|
20
|
-
readonly questions: Record<string,
|
|
34
|
+
readonly questions: Record<string, Question>;
|
|
35
|
+
/** Masked viewport pixels the model sees beside the state, when vision is on and pixels were granted. */
|
|
36
|
+
readonly screenshot?: Screenshot;
|
|
21
37
|
}
|
|
22
38
|
/**
|
|
23
|
-
* Builds
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
39
|
+
* Builds the operation request: one choice over the operations the screen
|
|
40
|
+
* offers, the controls, and the terminals. Every question states the goal
|
|
41
|
+
* and the recent actions in its own instructions: a classifier reads the
|
|
42
|
+
* question before the evidence.
|
|
27
43
|
*/
|
|
28
|
-
export declare function
|
|
29
|
-
/** The step params minus secret projections, which travel only as handles. */
|
|
30
|
-
export declare function nonSecretParams(params: Readonly<Record<string, JsonValue>> | undefined): Record<string, JsonValue>;
|
|
31
|
-
/** Element index behind a target key (`7` for both `7` and `7:2`). */
|
|
32
|
-
export declare function targetKeyIndex(key: string): string;
|
|
33
|
-
/** Element table rows as plain records, shared by decision and verdict states. */
|
|
34
|
-
export declare function elementRecords(space: ActionSpace): Record<string, JsonValue>[];
|
|
44
|
+
export declare function operationRequest(ctx: StepExecutorContext, space: ActionSpace, history: readonly HistoryEntry[], path: string): DecisionRequest;
|
|
35
45
|
/**
|
|
36
|
-
* Builds
|
|
37
|
-
*
|
|
46
|
+
* Builds the target request that follows a chosen operation: its target
|
|
47
|
+
* question when it has two or more targets, the destination question for a
|
|
48
|
+
* drag, and the secret question for a secret fill with two or more secrets
|
|
49
|
+
* declared. A lone target dispatches with no question, so the request may
|
|
50
|
+
* carry none. Every target question carries the `none` fallback, so a
|
|
51
|
+
* provider never has to refuse one. A dependent decision goes in its own
|
|
52
|
+
* request, as the Decisions API asks.
|
|
38
53
|
*/
|
|
39
|
-
export declare function
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
export declare function targetRequest(ctx: StepExecutorContext, space: ActionSpace, operation: Operation, history: readonly HistoryEntry[], path: string): DecisionRequest;
|
|
55
|
+
/**
|
|
56
|
+
* Builds a point request: two score questions over the grid on the image,
|
|
57
|
+
* one for the column and one for the row of the control the goal needs
|
|
58
|
+
* (`target`, as the text model named it). The probability-weighted means
|
|
59
|
+
* give a point finer than any one cell. The state is the goal alone: the
|
|
60
|
+
* element table adds nothing to where a drawn control sits, and measured on
|
|
61
|
+
* gpt-6-luna it pulls the estimate off. A dependent decision goes in its own
|
|
62
|
+
* request, as the Decisions API asks. Sizes are in the pixels of the image
|
|
63
|
+
* the model sees: the runner hands over a downscaled capture, and a model
|
|
64
|
+
* told CSS sizes for a smaller image lands its estimate short.
|
|
65
|
+
*/
|
|
66
|
+
export declare function pointRequest(ctx: StepExecutorContext, path: string, image: Screenshot, grid: Grid, framing: string, target: string): DecisionRequest;
|
|
67
|
+
/** Builds an assertion verdict request: the goal, the page, and the element table. The screen alone, no history. */
|
|
68
|
+
export declare function assertionRequest(goal: string, path: string, space: ActionSpace | undefined, screenshot: Screenshot | undefined): DecisionRequest;
|
|
49
69
|
/**
|
|
50
70
|
* Builds a completion-check request: the goal with its non-secret params, the
|
|
51
|
-
* page, the element table
|
|
52
|
-
* typed
|
|
53
|
-
*
|
|
71
|
+
* page, and the element table in the state; the actions the step took, with
|
|
72
|
+
* the values it typed, in the question only. The same list in the state was
|
|
73
|
+
* measured on gpt-6-luna to pull a verdict toward inconclusive (holds 0.17
|
|
74
|
+
* with it, 0.51 without, on a checked checkbox). Rejected done/failed claims
|
|
75
|
+
* are left out (they are not actions), and so is any model reasoning.
|
|
54
76
|
*/
|
|
55
|
-
export declare function completionRequest(
|
|
56
|
-
export {};
|
|
77
|
+
export declare function completionRequest(ctx: StepExecutorContext, space: ActionSpace, history: readonly HistoryEntry[], path: string, screenshot: Screenshot | undefined): DecisionRequest;
|