@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.
@@ -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
+ }
@@ -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;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { JsonValue } from 'e2e';
2
+ /** The step params minus secret projections, which travel only as handles. */
3
+ export declare function nonSecretParams(params: Readonly<Record<string, JsonValue>> | undefined): Record<string, JsonValue>;
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
+ }
@@ -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
+ }
@@ -1,5 +1,13 @@
1
1
  import type { JsonValue, StepExecutorContext } from 'e2e';
2
- import type { ActionSpace } from './elements.ts';
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 choice question as the request sends it. */
13
- interface ChoiceQuestion {
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: Record<string, JsonValue>;
16
- readonly criteria: Record<string, JsonValue | null>;
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, ChoiceQuestion>;
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 one decide request: the operation question plus one target
24
- * question per operation with two or more targets, and a secret question
25
- * when two or more secrets are declared. Operations with no targets are
26
- * left out; a lone target dispatches with no question.
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 decisionRequest(ctx: StepExecutorContext, space: ActionSpace, history: readonly HistoryEntry[], path: string): DecisionRequest;
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 an assertion verdict request: the goal, the page, and the element
37
- * table. No history: an assertion judges the screen alone.
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 assertionRequest(goal: string, path: string, pageText: string, elements: readonly Record<string, JsonValue>[]): DecisionRequest;
40
- /** What a completion check reads: the task, its inputs, the screen, and what the step did. */
41
- export interface CompletionInput {
42
- readonly goal: string;
43
- readonly params: Record<string, JsonValue>;
44
- readonly path: string;
45
- readonly pageText: string;
46
- readonly elements: readonly Record<string, JsonValue>[];
47
- readonly history: readonly HistoryEntry[];
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, and the actions the step took with the values it
52
- * typed. Rejected done/failed claims are left out (they are not actions), and
53
- * so is any model reasoning.
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(input: CompletionInput): DecisionRequest;
56
- export {};
77
+ export declare function completionRequest(ctx: StepExecutorContext, space: ActionSpace, history: readonly HistoryEntry[], path: string, screenshot: Screenshot | undefined): DecisionRequest;