humanish 0.129.0 → 0.131.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/CHANGELOG.md +27 -0
- package/dist/actors/codex/restricted-participant.d.ts +2 -2
- package/dist/actors/codex/restricted-participant.js +3 -3
- package/dist/actors/computer-use/loop/actions.d.ts +17 -2
- package/dist/actors/computer-use/loop/actions.js +24 -5
- package/dist/actors/computer-use/loop/backstop.d.ts +9 -1
- package/dist/actors/computer-use/loop/backstop.js +4 -3
- package/dist/actors/computer-use/loop/clock.d.ts +4 -4
- package/dist/actors/computer-use/loop/clock.js +5 -5
- package/dist/actors/computer-use/loop/observation.d.ts +21 -2
- package/dist/actors/computer-use/loop/observation.js +50 -8
- package/dist/actors/computer-use/loop/trace.js +5 -1
- package/dist/actors/computer-use/loop/types.d.ts +30 -19
- package/dist/actors/computer-use/loop/watch.d.ts +42 -0
- package/dist/actors/computer-use/loop/watch.js +260 -0
- package/dist/actors/computer-use/loop.js +18 -4
- package/dist/actors/computer-use/wait.d.ts +36 -18
- package/dist/actors/computer-use/wait.js +51 -22
- package/dist/actors/contract.d.ts +34 -0
- package/dist/actors/local-agent/cli.js +5 -0
- package/dist/actors/stop-cause.d.ts +1 -1
- package/dist/analysis/automatic.js +0 -1
- package/dist/analysis/evidence-sources.d.ts +44 -7
- package/dist/analysis/evidence-sources.js +40 -6
- package/dist/analysis/evidence.js +96 -122
- package/dist/analysis/service.js +12 -6
- package/dist/analysis/source.d.ts +28 -0
- package/dist/analysis/source.js +118 -0
- package/dist/evidence/frame-change.d.ts +39 -0
- package/dist/evidence/frame-change.js +293 -0
- package/dist/feedback/export.js +0 -1
- package/dist/observer/data.d.ts +2 -2
- package/dist/observer-app.html +1 -1
- package/dist/run/bundle-shape.d.ts +2 -2
- package/dist/run/bundle.d.ts +8 -1
- package/dist/run/detail.js +4 -1
- package/dist/run/participant-records.d.ts +1 -1
- package/dist/run/stream-shape.d.ts +2 -2
- package/dist/run/stream-trace.d.ts +21 -7
- package/dist/run/stream-trace.js +42 -8
- package/dist/run/streams.d.ts +18 -0
- package/dist/study/bundle-estimate.d.ts +4 -1
- package/dist/study/bundle-estimate.js +40 -15
- package/dist/study/plan-participants.d.ts +1 -1
- package/dist/study/types.d.ts +11 -7
- package/dist/study/warnings.js +1 -1
- package/dist/verify/verify.js +28 -21
- package/docs/architecture/actor-contract.md +8 -0
- package/docs/architecture/browser-control.md +11 -9
- package/docs/contracts/run-bundle.md +2 -2
- package/docs/contracts/schemas.md +48 -8
- package/docs/contracts/study-analysis.md +13 -1
- package/package.json +1 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { watchFrames, } from "../../../evidence/frame-change.js";
|
|
2
|
+
import { plural } from "../../../run/text.js";
|
|
3
|
+
import { isIdleTurn } from "./actions.js";
|
|
4
|
+
import { progressKeyOf } from "./backstop.js";
|
|
5
|
+
import { clockEndStop } from "./clock.js";
|
|
6
|
+
import { timeLimit } from "./ending.js";
|
|
7
|
+
import { CuaAbortError, CuaStallError, raceSessionDeadline } from "./race.js";
|
|
8
|
+
import { spendStop } from "./spend.js";
|
|
9
|
+
/**
|
|
10
|
+
* The watches of one participant's idle turns. Between turns it keeps the run of idle turns the
|
|
11
|
+
* doubling counts: a watch that ended at its limit or on a small change makes the next one in the
|
|
12
|
+
* run longer, and a turn with another action ends the run.
|
|
13
|
+
*/
|
|
14
|
+
export class IdleWatcher {
|
|
15
|
+
session;
|
|
16
|
+
observer;
|
|
17
|
+
/** The limit of the run's last watch, while that watch ended at its limit or on a small change. */
|
|
18
|
+
grownFromMs;
|
|
19
|
+
constructor(session, observer) {
|
|
20
|
+
this.session = session;
|
|
21
|
+
this.observer = observer;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Watch the screen in place of this turn's waits. Undefined, with nothing run, for a turn with
|
|
25
|
+
* another action, which ends the run, or with no wait time (screenshots only, or waits of 0 ms),
|
|
26
|
+
* which leaves the run as it is. `reference` is the observation the participant chose to wait on.
|
|
27
|
+
*/
|
|
28
|
+
async watch(turnNumber, reference, actions) {
|
|
29
|
+
const { session } = this;
|
|
30
|
+
if (!isIdleTurn(actions)) {
|
|
31
|
+
this.grownFromMs = undefined;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
// A named wait past maxWaitMs, the longest one wait sent to the desktop lasts, is watched for
|
|
35
|
+
// its length: a participant told to wait 10 minutes waits 10 minutes. A turn's named waits
|
|
36
|
+
// together last at most watchMaxMs, in order, so a Codex call of 4 long waits stays open no
|
|
37
|
+
// longer than one. A wait that names no length lasts idleWaitMs.
|
|
38
|
+
const asked = actions.flatMap((action) => action.kind === "wait" ? [Math.max(0, action.ms ?? session.waits.idleWaitMs)] : []);
|
|
39
|
+
let namedMs = 0;
|
|
40
|
+
const lengths = actions.flatMap((action) => {
|
|
41
|
+
if (action.kind !== "wait")
|
|
42
|
+
return [];
|
|
43
|
+
if (action.ms === undefined)
|
|
44
|
+
return [session.waits.idleWaitMs];
|
|
45
|
+
const ms = Math.min(Math.max(0, action.ms), session.waits.watchMaxMs - namedMs);
|
|
46
|
+
namedMs += ms;
|
|
47
|
+
return [ms];
|
|
48
|
+
});
|
|
49
|
+
const plannedMs = lengths.reduce((sum, ms) => sum + ms, 0);
|
|
50
|
+
if (plannedMs === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const capMs = Math.max(plannedMs, session.waits.watchMaxMs);
|
|
53
|
+
// Never shorter than the turn's own planned length, so a long named wait is not cut short.
|
|
54
|
+
const limitMs = this.grownFromMs === undefined
|
|
55
|
+
? plannedMs
|
|
56
|
+
: Math.min(capMs, Math.max(plannedMs, 2 * this.grownFromMs));
|
|
57
|
+
const ending = await watchScreen(session, this.observer, turnNumber, reference, limitMs);
|
|
58
|
+
// A small change that held still (a status label, or a counter the rule did not hold back)
|
|
59
|
+
// wakes the participant and keeps the doubling, so a repeating small change cannot keep every
|
|
60
|
+
// watch short.
|
|
61
|
+
const grows = ending.wake === "limit" || (ending.seen?.verdict === "settled" && ending.seen.small);
|
|
62
|
+
this.grownFromMs = grows ? limitMs : undefined;
|
|
63
|
+
return {
|
|
64
|
+
wake: ending.wake,
|
|
65
|
+
...(ending.observation === undefined ? {} : { observation: ending.observation }),
|
|
66
|
+
...(ending.stop === undefined ? {} : { stop: ending.stop }),
|
|
67
|
+
hint: hintFor(ending, grows && limitMs < capMs ? capMs : undefined),
|
|
68
|
+
waits: {
|
|
69
|
+
...splitWatch(lengths, limitMs, ending.ms),
|
|
70
|
+
sessionEnded: ending.stop !== undefined,
|
|
71
|
+
watch: {
|
|
72
|
+
limitMs,
|
|
73
|
+
askedMs: asked.reduce((sum, ms) => sum + ms, 0),
|
|
74
|
+
wake: ending.wake,
|
|
75
|
+
looks: ending.looks,
|
|
76
|
+
...(ending.area === undefined ? {} : { area: { ...ending.area } }),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function watchScreen(session, observer, turnNumber, reference, limitMs) {
|
|
83
|
+
const { lookEveryMs, settleWaitMs } = session.waits;
|
|
84
|
+
const startedAt = session.now();
|
|
85
|
+
const limitAt = startedAt + limitMs;
|
|
86
|
+
const deadlineAt = session.startedAtMs + session.timeoutMs;
|
|
87
|
+
const end = session.settings.clock?.end;
|
|
88
|
+
const endAt = end?.atMs;
|
|
89
|
+
// A wind-down already under way bounds no watch: the participant was told when it started.
|
|
90
|
+
const windDownAt = end !== undefined && end.atMs - end.windDownMs > startedAt
|
|
91
|
+
? end.atMs - end.windDownMs
|
|
92
|
+
: undefined;
|
|
93
|
+
// A reply that came back past the end or the deadline starts no watch.
|
|
94
|
+
if (session.signal?.aborted)
|
|
95
|
+
throw new CuaAbortError();
|
|
96
|
+
if (endAt !== undefined && startedAt >= endAt)
|
|
97
|
+
return {
|
|
98
|
+
wake: "clock",
|
|
99
|
+
ms: 0,
|
|
100
|
+
looks: 0,
|
|
101
|
+
...stopAt(clockEndStop(session, reference, startedAt)),
|
|
102
|
+
};
|
|
103
|
+
if (startedAt >= deadlineAt)
|
|
104
|
+
return { wake: "stopped", ms: 0, looks: 0, stop: timeLimit(session) };
|
|
105
|
+
session.trace.record("notice", () => ({
|
|
106
|
+
lifecycle: "started",
|
|
107
|
+
title: "watching the screen",
|
|
108
|
+
text: `until the screen changes or ${limitMs}ms pass, looking every ${lookEveryMs}ms and at the end; no model request until then`,
|
|
109
|
+
}));
|
|
110
|
+
session.flush();
|
|
111
|
+
const frames = reference.screenshot === undefined ? undefined : watchFrames(reference.screenshot);
|
|
112
|
+
const referenceKey = progressKeyOf(reference);
|
|
113
|
+
let looks = 0;
|
|
114
|
+
let quickLooks = 0;
|
|
115
|
+
let lastLook;
|
|
116
|
+
let nextLookAt = startedAt + lookEveryMs;
|
|
117
|
+
for (;;) {
|
|
118
|
+
if (session.signal?.aborted)
|
|
119
|
+
throw new CuaAbortError();
|
|
120
|
+
session.phase = `watching the screen during turn ${turnNumber}`;
|
|
121
|
+
const target = Math.min(nextLookAt, limitAt, windDownAt ?? limitAt, endAt ?? limitAt, deadlineAt);
|
|
122
|
+
const sleptFrom = session.now();
|
|
123
|
+
await raceSessionDeadline(session.sleep(Math.max(0, target - sleptFrom)), deadlineAt - sleptFrom, session.signal);
|
|
124
|
+
// An injected clock need not move with the injected sleep; the watch's schedule still does.
|
|
125
|
+
const at = Math.max(session.now(), target);
|
|
126
|
+
// The wait lasts until the watch returns, so it includes the look that ended it, and a stalled
|
|
127
|
+
// look's observe and retry.
|
|
128
|
+
const lasted = () => Math.max(session.now(), at) - startedAt;
|
|
129
|
+
// No observe after the end or the deadline: one started with no time left would fail, and at
|
|
130
|
+
// the end the session would end as timed out. The stop is read at `at`: a host timer can fire
|
|
131
|
+
// a millisecond before the clock reaches the end.
|
|
132
|
+
if (endAt !== undefined && at >= endAt)
|
|
133
|
+
return {
|
|
134
|
+
wake: "clock",
|
|
135
|
+
ms: lasted(),
|
|
136
|
+
looks,
|
|
137
|
+
...stopAt(clockEndStop(session, lastLook ?? reference, at)),
|
|
138
|
+
};
|
|
139
|
+
if (at >= deadlineAt)
|
|
140
|
+
return { wake: "stopped", ms: lasted(), looks, stop: timeLimit(session) };
|
|
141
|
+
let look;
|
|
142
|
+
try {
|
|
143
|
+
look = await observer.look(turnNumber);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof CuaStallError)
|
|
147
|
+
return { wake: "stalled", ms: lasted(), looks };
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
looks += 1;
|
|
151
|
+
lastLook = look.observation;
|
|
152
|
+
const ms = lasted();
|
|
153
|
+
const ended = (wake, seen) => ({
|
|
154
|
+
wake,
|
|
155
|
+
ms,
|
|
156
|
+
looks,
|
|
157
|
+
observation: look.observation,
|
|
158
|
+
...(seen === undefined ? {} : { seen }),
|
|
159
|
+
...(seen?.verdict === "settled" ? { area: seen.area } : {}),
|
|
160
|
+
});
|
|
161
|
+
// A condition comes first: the checkpoint persists the look that matched it (a task completed
|
|
162
|
+
// at it is counted already), and the budget then stops the session before the next request.
|
|
163
|
+
if (look.condition)
|
|
164
|
+
return ended("condition");
|
|
165
|
+
const overBudget = spendStop(session);
|
|
166
|
+
if (overBudget !== undefined)
|
|
167
|
+
return { ...ended("stopped"), stop: overBudget };
|
|
168
|
+
if (look.speech)
|
|
169
|
+
return ended("speech");
|
|
170
|
+
const seen = compareLook(frames, referenceKey, look.observation, at);
|
|
171
|
+
if (seen.verdict === "settled")
|
|
172
|
+
return ended("changed", seen);
|
|
173
|
+
const windingDown = windDownAt !== undefined && at >= windDownAt;
|
|
174
|
+
// A change still moving at the last look, such as a video, ended nothing: the participant
|
|
175
|
+
// sees it in that look's frame, the hint says a large part was changing, and the next watch
|
|
176
|
+
// grows as after a still screen.
|
|
177
|
+
if (at >= limitAt || windingDown)
|
|
178
|
+
return ended(windingDown ? "clock" : "limit", seen);
|
|
179
|
+
const largeMove = seen.verdict === "changing" && seen.lookSoon;
|
|
180
|
+
quickLooks = largeMove && quickLooks < 2 ? quickLooks + 1 : 0;
|
|
181
|
+
nextLookAt = at + (quickLooks > 0 ? settleWaitMs : lookEveryMs);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const stopAt = (stop) => (stop === undefined ? {} : { stop });
|
|
185
|
+
/**
|
|
186
|
+
* Whether a look shows a change that wakes the participant. A frame is compared with the reference
|
|
187
|
+
* frame tile by tile; an observation without one (a state executor) by its progress key.
|
|
188
|
+
*/
|
|
189
|
+
function compareLook(frames, referenceKey, observation, atMs) {
|
|
190
|
+
if (frames === undefined || observation.screenshot === undefined)
|
|
191
|
+
return progressKeyOf(observation) === referenceKey
|
|
192
|
+
? { verdict: "unchanged" }
|
|
193
|
+
: { verdict: "settled", area: { x: 0, y: 0, width: 0, height: 0 }, small: false };
|
|
194
|
+
return frames.next(observation.screenshot, atMs);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Each wait's part of a watch that lasted `ms`: the limit is shared among the waits in proportion
|
|
198
|
+
* to their planned lengths, and the watch covers them in order. Later waits get 0.
|
|
199
|
+
*/
|
|
200
|
+
function splitWatch(lengths, limitMs, ms) {
|
|
201
|
+
const plannedMs = lengths.reduce((sum, length) => sum + length, 0);
|
|
202
|
+
const starts = [];
|
|
203
|
+
const ends = [];
|
|
204
|
+
let planned = 0;
|
|
205
|
+
for (const length of lengths) {
|
|
206
|
+
starts.push(Math.floor((limitMs * planned) / plannedMs));
|
|
207
|
+
planned += length;
|
|
208
|
+
ends.push(Math.floor((limitMs * planned) / plannedMs));
|
|
209
|
+
}
|
|
210
|
+
const found = ends.findIndex((shareEnd, index) => shareEnd > starts[index] && ms <= shareEnd);
|
|
211
|
+
const endedOn = found === -1 ? lengths.length - 1 : found;
|
|
212
|
+
return {
|
|
213
|
+
ms: lengths.map((_, index) => index < endedOn ? ends[index] - starts[index] : index === endedOn ? ms - starts[index] : 0),
|
|
214
|
+
endedOn,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The next request's note. A clock ending says nothing: the clock text that follows says it. A
|
|
219
|
+
* small change is named by its place, so a participant is not told the screen changed when what
|
|
220
|
+
* changed was a counter or a timestamp. `growsToMs` adds the growth sentence when the next watch
|
|
221
|
+
* doubles.
|
|
222
|
+
*/
|
|
223
|
+
function hintFor(ending, growsToMs) {
|
|
224
|
+
const lasted = `Your wait lasted ${duration(ending.ms)}`;
|
|
225
|
+
const grows = growsToMs === undefined
|
|
226
|
+
? ""
|
|
227
|
+
: ` A wait right after it can last twice as long, at most ${duration(growsToMs)}.`;
|
|
228
|
+
const { seen } = ending;
|
|
229
|
+
switch (ending.wake) {
|
|
230
|
+
case "changed":
|
|
231
|
+
return seen?.verdict === "settled" && seen.small
|
|
232
|
+
? `${lasted} and ended when a small part of the screen changed and then stayed the same for over a minute, ${place(seen.area)}.${grows}`
|
|
233
|
+
: `${lasted} and ended when the screen changed.`;
|
|
234
|
+
case "speech":
|
|
235
|
+
return `${lasted} and ended when you heard speech.`;
|
|
236
|
+
case "condition":
|
|
237
|
+
return `${lasted} and ended when the page changed.`;
|
|
238
|
+
// A small change seen less than 65 s before the limit has not held still yet, so the hint
|
|
239
|
+
// never says the screen stayed the same.
|
|
240
|
+
case "limit":
|
|
241
|
+
if (seen?.verdict === "changing" && seen.lookSoon)
|
|
242
|
+
return `${lasted}; a large part of the screen was changing at its last check.${grows}`;
|
|
243
|
+
if (seen?.verdict !== "settled" && seen?.held !== undefined)
|
|
244
|
+
return `${lasted}; a small part of the screen changed and stayed the same, ${place(seen.held)}. A small change does not end a wait.${grows}`;
|
|
245
|
+
return `${lasted}; no change on the screen held still long enough to end it.${grows}`;
|
|
246
|
+
case "stalled":
|
|
247
|
+
return `${lasted}; the screen could not be checked during it.`;
|
|
248
|
+
case "clock":
|
|
249
|
+
case "stopped":
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const place = (area) => `at x ${area.x}, y ${area.y} (${area.width} x ${area.height} pixels)`;
|
|
254
|
+
function duration(ms) {
|
|
255
|
+
if (ms < 1_000)
|
|
256
|
+
return plural(ms, "millisecond");
|
|
257
|
+
if (ms < 120_000)
|
|
258
|
+
return plural(Math.round(ms / 1_000), "second");
|
|
259
|
+
return plural(Math.round(ms / 60_000), "minute");
|
|
260
|
+
}
|
|
@@ -14,6 +14,7 @@ import { LoopSession } from "./loop/session.js";
|
|
|
14
14
|
import { spendStop, unknownSpendStop } from "./loop/spend.js";
|
|
15
15
|
import { accountBillingConflicts } from "./loop/usage.js";
|
|
16
16
|
import { loopResult, notice } from "./loop/trace.js";
|
|
17
|
+
import { IdleWatcher } from "./loop/watch.js";
|
|
17
18
|
export { describeCuaAction } from "./loop/actions.js";
|
|
18
19
|
export { stableProgressKey } from "./loop/backstop.js";
|
|
19
20
|
export { IMPRESSIONS_ASK } from "./loop/impressions.js";
|
|
@@ -75,6 +76,7 @@ export async function runComputerUseLoopWithTaps(options) {
|
|
|
75
76
|
*/
|
|
76
77
|
async function runTurns(session, conversation) {
|
|
77
78
|
const observer = new DesktopObserver(session);
|
|
79
|
+
const watcher = new IdleWatcher(session, observer);
|
|
78
80
|
const opening = await observer.checkpoint(0);
|
|
79
81
|
if ("stop" in opening)
|
|
80
82
|
return opening.stop;
|
|
@@ -142,9 +144,13 @@ async function runTurns(session, conversation) {
|
|
|
142
144
|
? { ...ended, debriefTrigger: { kind: "participant_end", observation } }
|
|
143
145
|
: ended;
|
|
144
146
|
}
|
|
145
|
-
const
|
|
147
|
+
const watched = await watcher.watch(turnNumber, observation, turn.actions);
|
|
148
|
+
const batch = await runActionBatch(session, turn.actions, watched?.waits);
|
|
146
149
|
conversation.previousExecution = batch.execution;
|
|
147
|
-
|
|
150
|
+
// An earlier reply's unknown usage still stops the session before its closing request.
|
|
151
|
+
if (watched?.stop !== undefined)
|
|
152
|
+
return unknownSpendStop(session) ?? watched.stop;
|
|
153
|
+
const checkpoint = await observer.checkpoint(turnNumber, watched?.observation);
|
|
148
154
|
if ("stop" in checkpoint)
|
|
149
155
|
return checkpoint.stop;
|
|
150
156
|
observation = checkpoint.observation;
|
|
@@ -153,11 +159,19 @@ async function runTurns(session, conversation) {
|
|
|
153
159
|
observation,
|
|
154
160
|
heardNewSpeech: observer.heardNewSpeech,
|
|
155
161
|
waitingIsTheTask: heldToScheduledEnd(session),
|
|
162
|
+
screenChanged: watched?.wake === "changed" ||
|
|
163
|
+
watched?.wake === "speech" ||
|
|
164
|
+
watched?.wake === "condition",
|
|
156
165
|
}, { idleSteps: session.idleSteps, noProgressSteps: session.noProgressSteps });
|
|
157
166
|
backstop = step.backstop;
|
|
158
|
-
const stalled = applyBackstop(session, conversation, step, [
|
|
167
|
+
const stalled = applyBackstop(session, conversation, step, [
|
|
168
|
+
watched?.hint,
|
|
169
|
+
checkpoint.hint,
|
|
170
|
+
batch.hint,
|
|
171
|
+
]);
|
|
172
|
+
// A turn that reached the participant's end is that end, never the backstop's.
|
|
159
173
|
if (stalled !== undefined)
|
|
160
|
-
return stalled;
|
|
174
|
+
return clockEndStop(session, observation) ?? stalled;
|
|
161
175
|
const unknownSpend = unknownSpendStop(session);
|
|
162
176
|
if (unknownSpend !== undefined)
|
|
163
177
|
return unknownSpend;
|
|
@@ -7,31 +7,54 @@ export declare const CUA_WAIT_LIMITS: Readonly<{
|
|
|
7
7
|
*/
|
|
8
8
|
stepMs: 30000;
|
|
9
9
|
/**
|
|
10
|
-
* The longest one wait action lasts when a study does not set
|
|
11
|
-
* has no speech (defaultMaxWaitMs). Two minutes covers waiting in a lobby or for an email to
|
|
10
|
+
* The longest one wait action sent to the desktop lasts when a study does not set
|
|
11
|
+
* actor.maxWaitMs and the desktop has no speech (defaultMaxWaitMs). Two minutes covers waiting in a lobby or for an email to
|
|
12
12
|
* arrive, and the participant still sees a fresh screenshot at least that often.
|
|
13
13
|
*/
|
|
14
14
|
defaultMaxMs: 120000;
|
|
15
|
+
/**
|
|
16
|
+
* The longest a watch over an unchanged screen grows to, and the longest the named waits of a
|
|
17
|
+
* turn that only waits are watched for together, when a study does not set actor.maxWaitMs. Each further watch
|
|
18
|
+
* in a row that ends with no change lasts twice as long as the one before, so on a static screen
|
|
19
|
+
* a participant settles at one model request per 10 minutes.
|
|
20
|
+
*/
|
|
21
|
+
defaultWatchMaxMs: 600000;
|
|
15
22
|
/**
|
|
16
23
|
* How long a wait that names no duration lasts in a turn that only waits or takes screenshots,
|
|
17
24
|
* when a study does not set actor.idleWaitMs. OpenAI's computer-use `wait` names no duration. A
|
|
18
25
|
* participant told to wait about a minute sent ten in one turn, which lasts 50 s at this length;
|
|
19
|
-
* one wait a turn makes a one-minute wait about six turns.
|
|
26
|
+
* one wait a turn makes a one-minute wait about six turns. It is also how often a watch looks.
|
|
20
27
|
*/
|
|
21
28
|
defaultIdleMs: 5000;
|
|
22
29
|
/**
|
|
23
30
|
* How long a wait that names no duration lasts after the participant acts in the same turn, so
|
|
24
31
|
* the screen can settle before the next screenshot. Nearly every OpenAI computer-use wait follows
|
|
25
|
-
* a click or keypress this way, so a longer one would slow every run.
|
|
32
|
+
* a click or keypress this way, so a longer one would slow every run. A watch looks again this
|
|
33
|
+
* soon after a large part of the screen moved.
|
|
26
34
|
*/
|
|
27
35
|
settleMs: 500;
|
|
36
|
+
/**
|
|
37
|
+
* The longest gap between two looks of a watch on a desktop with speech. Heard speech reaches the
|
|
38
|
+
* participant with a look, and the desktop session ends when more utterances arrive between two
|
|
39
|
+
* observations than it holds; 30 s is the spacing the speech default kept before watches.
|
|
40
|
+
*/
|
|
41
|
+
speechLookMs: 30000;
|
|
28
42
|
/** The smallest actor.maxWaitMs or actor.idleWaitMs a study may set. */
|
|
29
43
|
leastMs: 1000;
|
|
30
|
-
/** The largest actor.
|
|
31
|
-
|
|
44
|
+
/** The largest actor.idleWaitMs a study may set. */
|
|
45
|
+
mostIdleMs: 600000;
|
|
46
|
+
/** The largest actor.maxWaitMs a study may set: a watch may grow to an hour. */
|
|
47
|
+
mostMaxMs: 3600000;
|
|
32
48
|
}>;
|
|
33
|
-
/**
|
|
34
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The wait lengths one session applies. The trace records the first three as `waitSettings`.
|
|
51
|
+
* `watchMaxMs` is the longest a watch grows to over an unchanged screen and the longest a watched
|
|
52
|
+
* turn's named waits last together, and `lookEveryMs` how often a watch looks.
|
|
53
|
+
*/
|
|
54
|
+
export type WaitSettings = Readonly<ActorWaitSettings> & {
|
|
55
|
+
readonly watchMaxMs: number;
|
|
56
|
+
readonly lookEveryMs: number;
|
|
57
|
+
};
|
|
35
58
|
/** One wait as the desktop runs it. */
|
|
36
59
|
export interface PlannedWait {
|
|
37
60
|
/** How long the wait lasts. */
|
|
@@ -41,13 +64,6 @@ export interface PlannedWait {
|
|
|
41
64
|
/** What the participant asked for, when that was longer than maxWaitMs. */
|
|
42
65
|
readonly shortenedFromMs?: number;
|
|
43
66
|
}
|
|
44
|
-
/**
|
|
45
|
-
* The longest one wait action lasts when a study does not set actor.maxWaitMs. On a desktop with
|
|
46
|
-
* speech, heard speech reaches the participant only with a screenshot, and the desktop session ends
|
|
47
|
-
* when more utterances arrive between two screenshots than it holds. A wait takes no screenshot, so
|
|
48
|
-
* there the default keeps one wait to one desktop call.
|
|
49
|
-
*/
|
|
50
|
-
export declare function defaultMaxWaitMs(speechEnabled: boolean): number;
|
|
51
67
|
/**
|
|
52
68
|
* Why these wait fields cannot run, or null. `prefix` names their owner in the message: `actor.`
|
|
53
69
|
* in a study file, empty for loop options. `maxPrefix` names the owner of maxWaitMs when it is
|
|
@@ -59,8 +75,9 @@ export declare function waitFieldsReason(fields: {
|
|
|
59
75
|
}, prefix: string, maxPrefix?: string): string | null;
|
|
60
76
|
/**
|
|
61
77
|
* The wait lengths a session applies: the study's values, or their defaults. The idle wait never
|
|
62
|
-
* outlasts the longest wait, which on a desktop with speech defaults to 30 s.
|
|
63
|
-
*
|
|
78
|
+
* outlasts the longest wait, which on a desktop with speech defaults to 30 s. A watch grows to
|
|
79
|
+
* actor.maxWaitMs, else 10 minutes, and looks every idle wait, at most every 30 s on a desktop with
|
|
80
|
+
* speech. Throws a RangeError for fields waitFieldsReason refuses.
|
|
64
81
|
*/
|
|
65
82
|
export declare function resolveWaitSettings(options: {
|
|
66
83
|
readonly maxWaitMs?: number | undefined;
|
|
@@ -70,6 +87,7 @@ export declare function resolveWaitSettings(options: {
|
|
|
70
87
|
/**
|
|
71
88
|
* How long one wait lasts and the desktop calls it takes. A wait that names no duration lasts
|
|
72
89
|
* idleWaitMs when its turn only waits or takes screenshots (`idleTurn`), and settleWaitMs after an
|
|
73
|
-
* action. A longer wait than maxWaitMs is shortened to it
|
|
90
|
+
* action. A longer wait than maxWaitMs is shortened to it, and a negative one lasts 0 ms. 70 s is
|
|
91
|
+
* sent as 30 s, 30 s and 10 s.
|
|
74
92
|
*/
|
|
75
93
|
export declare function planWait(settings: WaitSettings, requestedMs: number | undefined, idleTurn: boolean): PlannedWait;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// How long one participant wait action lasts, for every provider and desktop. A study sets the
|
|
2
2
|
// longest (actor.maxWaitMs) and how long a wait that names no duration lasts while the participant
|
|
3
|
-
// only waits (actor.idleWaitMs). The loop asks planWait for each wait
|
|
4
|
-
// in steps one desktop call carries
|
|
3
|
+
// only waits (actor.idleWaitMs). The loop asks planWait for each wait. A turn with another action
|
|
4
|
+
// sends its waits to the desktop in steps one desktop call carries; a turn that only waits watches
|
|
5
|
+
// the screen instead (loop/watch.ts), for its waits' lengths, the named ones together up to
|
|
6
|
+
// watchMaxMs, or longer as the watches of idle turns in a row grow.
|
|
5
7
|
export const CUA_WAIT_LIMITS = Object.freeze({
|
|
6
8
|
/**
|
|
7
9
|
* The longest wait one desktop call is given. A browser-control request must be answered within
|
|
@@ -10,35 +12,51 @@ export const CUA_WAIT_LIMITS = Object.freeze({
|
|
|
10
12
|
*/
|
|
11
13
|
stepMs: 30_000,
|
|
12
14
|
/**
|
|
13
|
-
* The longest one wait action lasts when a study does not set
|
|
14
|
-
* has no speech (defaultMaxWaitMs). Two minutes covers waiting in a lobby or for an email to
|
|
15
|
+
* The longest one wait action sent to the desktop lasts when a study does not set
|
|
16
|
+
* actor.maxWaitMs and the desktop has no speech (defaultMaxWaitMs). Two minutes covers waiting in a lobby or for an email to
|
|
15
17
|
* arrive, and the participant still sees a fresh screenshot at least that often.
|
|
16
18
|
*/
|
|
17
19
|
defaultMaxMs: 120_000,
|
|
20
|
+
/**
|
|
21
|
+
* The longest a watch over an unchanged screen grows to, and the longest the named waits of a
|
|
22
|
+
* turn that only waits are watched for together, when a study does not set actor.maxWaitMs. Each further watch
|
|
23
|
+
* in a row that ends with no change lasts twice as long as the one before, so on a static screen
|
|
24
|
+
* a participant settles at one model request per 10 minutes.
|
|
25
|
+
*/
|
|
26
|
+
defaultWatchMaxMs: 600_000,
|
|
18
27
|
/**
|
|
19
28
|
* How long a wait that names no duration lasts in a turn that only waits or takes screenshots,
|
|
20
29
|
* when a study does not set actor.idleWaitMs. OpenAI's computer-use `wait` names no duration. A
|
|
21
30
|
* participant told to wait about a minute sent ten in one turn, which lasts 50 s at this length;
|
|
22
|
-
* one wait a turn makes a one-minute wait about six turns.
|
|
31
|
+
* one wait a turn makes a one-minute wait about six turns. It is also how often a watch looks.
|
|
23
32
|
*/
|
|
24
33
|
defaultIdleMs: 5_000,
|
|
25
34
|
/**
|
|
26
35
|
* How long a wait that names no duration lasts after the participant acts in the same turn, so
|
|
27
36
|
* the screen can settle before the next screenshot. Nearly every OpenAI computer-use wait follows
|
|
28
|
-
* a click or keypress this way, so a longer one would slow every run.
|
|
37
|
+
* a click or keypress this way, so a longer one would slow every run. A watch looks again this
|
|
38
|
+
* soon after a large part of the screen moved.
|
|
29
39
|
*/
|
|
30
40
|
settleMs: 500,
|
|
41
|
+
/**
|
|
42
|
+
* The longest gap between two looks of a watch on a desktop with speech. Heard speech reaches the
|
|
43
|
+
* participant with a look, and the desktop session ends when more utterances arrive between two
|
|
44
|
+
* observations than it holds; 30 s is the spacing the speech default kept before watches.
|
|
45
|
+
*/
|
|
46
|
+
speechLookMs: 30_000,
|
|
31
47
|
/** The smallest actor.maxWaitMs or actor.idleWaitMs a study may set. */
|
|
32
48
|
leastMs: 1_000,
|
|
33
|
-
/** The largest actor.
|
|
34
|
-
|
|
49
|
+
/** The largest actor.idleWaitMs a study may set. */
|
|
50
|
+
mostIdleMs: 600_000,
|
|
51
|
+
/** The largest actor.maxWaitMs a study may set: a watch may grow to an hour. */
|
|
52
|
+
mostMaxMs: 3_600_000,
|
|
35
53
|
});
|
|
36
|
-
/** A whole number of milliseconds
|
|
37
|
-
function isWaitMs(value) {
|
|
54
|
+
/** A whole number of milliseconds from CUA_WAIT_LIMITS.leastMs to `mostMs`. */
|
|
55
|
+
function isWaitMs(value, mostMs) {
|
|
38
56
|
return (typeof value === "number" &&
|
|
39
57
|
Number.isSafeInteger(value) &&
|
|
40
58
|
value >= CUA_WAIT_LIMITS.leastMs &&
|
|
41
|
-
value <=
|
|
59
|
+
value <= mostMs);
|
|
42
60
|
}
|
|
43
61
|
/**
|
|
44
62
|
* The longest one wait action lasts when a study does not set actor.maxWaitMs. On a desktop with
|
|
@@ -46,12 +64,16 @@ function isWaitMs(value) {
|
|
|
46
64
|
* when more utterances arrive between two screenshots than it holds. A wait takes no screenshot, so
|
|
47
65
|
* there the default keeps one wait to one desktop call.
|
|
48
66
|
*/
|
|
49
|
-
|
|
67
|
+
function defaultMaxWaitMs(speechEnabled) {
|
|
50
68
|
return speechEnabled ? CUA_WAIT_LIMITS.stepMs : CUA_WAIT_LIMITS.defaultMaxMs;
|
|
51
69
|
}
|
|
52
70
|
const FIELDS = [
|
|
53
|
-
["maxWaitMs", "the longest one wait action lasts"],
|
|
54
|
-
[
|
|
71
|
+
["maxWaitMs", "the longest one wait action lasts", CUA_WAIT_LIMITS.mostMaxMs],
|
|
72
|
+
[
|
|
73
|
+
"idleWaitMs",
|
|
74
|
+
"how long a wait with no duration lasts while the participant only waits",
|
|
75
|
+
CUA_WAIT_LIMITS.mostIdleMs,
|
|
76
|
+
],
|
|
55
77
|
];
|
|
56
78
|
/**
|
|
57
79
|
* Why these wait fields cannot run, or null. `prefix` names their owner in the message: `actor.`
|
|
@@ -59,9 +81,9 @@ const FIELDS = [
|
|
|
59
81
|
* another, as for a participant's own idleWaitMs under actor.maxWaitMs.
|
|
60
82
|
*/
|
|
61
83
|
export function waitFieldsReason(fields, prefix, maxPrefix = prefix) {
|
|
62
|
-
for (const [name, meaning] of FIELDS) {
|
|
63
|
-
if (fields[name] !== undefined && !isWaitMs(fields[name]))
|
|
64
|
-
return `${name === "maxWaitMs" ? maxPrefix : prefix}${name}, ${meaning}, must be a whole number of milliseconds from ${CUA_WAIT_LIMITS.leastMs} to ${
|
|
84
|
+
for (const [name, meaning, mostMs] of FIELDS) {
|
|
85
|
+
if (fields[name] !== undefined && !isWaitMs(fields[name], mostMs))
|
|
86
|
+
return `${name === "maxWaitMs" ? maxPrefix : prefix}${name}, ${meaning}, must be a whole number of milliseconds from ${CUA_WAIT_LIMITS.leastMs} to ${mostMs}.`;
|
|
65
87
|
}
|
|
66
88
|
const { maxWaitMs, idleWaitMs } = fields;
|
|
67
89
|
if (typeof maxWaitMs === "number" && typeof idleWaitMs === "number" && idleWaitMs > maxWaitMs)
|
|
@@ -70,27 +92,34 @@ export function waitFieldsReason(fields, prefix, maxPrefix = prefix) {
|
|
|
70
92
|
}
|
|
71
93
|
/**
|
|
72
94
|
* The wait lengths a session applies: the study's values, or their defaults. The idle wait never
|
|
73
|
-
* outlasts the longest wait, which on a desktop with speech defaults to 30 s.
|
|
74
|
-
*
|
|
95
|
+
* outlasts the longest wait, which on a desktop with speech defaults to 30 s. A watch grows to
|
|
96
|
+
* actor.maxWaitMs, else 10 minutes, and looks every idle wait, at most every 30 s on a desktop with
|
|
97
|
+
* speech. Throws a RangeError for fields waitFieldsReason refuses.
|
|
75
98
|
*/
|
|
76
99
|
export function resolveWaitSettings(options) {
|
|
77
100
|
const reason = waitFieldsReason(options, "");
|
|
78
101
|
if (reason !== null)
|
|
79
102
|
throw new RangeError(reason);
|
|
80
103
|
const maxWaitMs = options.maxWaitMs ?? defaultMaxWaitMs(options.speechEnabled);
|
|
104
|
+
const idleWaitMs = Math.min(options.idleWaitMs ?? CUA_WAIT_LIMITS.defaultIdleMs, maxWaitMs);
|
|
81
105
|
return {
|
|
82
106
|
maxWaitMs,
|
|
83
|
-
idleWaitMs
|
|
107
|
+
idleWaitMs,
|
|
84
108
|
settleWaitMs: CUA_WAIT_LIMITS.settleMs,
|
|
109
|
+
watchMaxMs: options.maxWaitMs ?? CUA_WAIT_LIMITS.defaultWatchMaxMs,
|
|
110
|
+
lookEveryMs: options.speechEnabled
|
|
111
|
+
? Math.min(idleWaitMs, CUA_WAIT_LIMITS.speechLookMs)
|
|
112
|
+
: idleWaitMs,
|
|
85
113
|
};
|
|
86
114
|
}
|
|
87
115
|
/**
|
|
88
116
|
* How long one wait lasts and the desktop calls it takes. A wait that names no duration lasts
|
|
89
117
|
* idleWaitMs when its turn only waits or takes screenshots (`idleTurn`), and settleWaitMs after an
|
|
90
|
-
* action. A longer wait than maxWaitMs is shortened to it
|
|
118
|
+
* action. A longer wait than maxWaitMs is shortened to it, and a negative one lasts 0 ms. 70 s is
|
|
119
|
+
* sent as 30 s, 30 s and 10 s.
|
|
91
120
|
*/
|
|
92
121
|
export function planWait(settings, requestedMs, idleTurn) {
|
|
93
|
-
const askedMs = requestedMs ?? (idleTurn ? settings.idleWaitMs : settings.settleWaitMs);
|
|
122
|
+
const askedMs = Math.max(0, requestedMs ?? (idleTurn ? settings.idleWaitMs : settings.settleWaitMs));
|
|
94
123
|
const ms = Math.min(askedMs, settings.maxWaitMs);
|
|
95
124
|
const { stepMs } = CUA_WAIT_LIMITS;
|
|
96
125
|
const steps = [];
|
|
@@ -87,6 +87,40 @@ export interface ActorTraceItem {
|
|
|
87
87
|
/** On a computer-use `screenshot` item: the page that observation was on. Additive: absent when
|
|
88
88
|
* the observation read no URL, on other producers and in older bundles. */
|
|
89
89
|
page?: TracePage;
|
|
90
|
+
/** On the computer-use `ui_action` of the wait during which a watch ended: the watch the loop
|
|
91
|
+
* ran in place of a turn that only waited. Additive: absent on plain waits, on other items and
|
|
92
|
+
* producers, and in older bundles. */
|
|
93
|
+
watch?: ActorWatch;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A watch over the screen while a computer-use participant only waited: the loop looked every
|
|
97
|
+
* idle wait and asked the model again when the screen changed and held still, or at the limit.
|
|
98
|
+
*/
|
|
99
|
+
export interface ActorWatch {
|
|
100
|
+
/** How long the watch could last, in milliseconds. */
|
|
101
|
+
limitMs: number;
|
|
102
|
+
/**
|
|
103
|
+
* How long the turn's waits asked for, in milliseconds: the lengths they named, and the idle
|
|
104
|
+
* wait for each that named none. Above limitMs when the named waits together passed the longest
|
|
105
|
+
* a watch grows to (actor.maxWaitMs, else 10 minutes).
|
|
106
|
+
*/
|
|
107
|
+
askedMs: number;
|
|
108
|
+
/**
|
|
109
|
+
* Why it ended: `changed` (a change held still), `speech` (the participant heard speech),
|
|
110
|
+
* `condition` (a study stop condition, dwell trigger or task matched a look), `limit` (its time
|
|
111
|
+
* passed with no change holding still), `clock` (the participant's wind-down or end arrived),
|
|
112
|
+
* `stalled` (a look got no frame), `stopped` (the study budget or the session's time ran out).
|
|
113
|
+
*/
|
|
114
|
+
wake: "changed" | "speech" | "condition" | "limit" | "clock" | "stalled" | "stopped";
|
|
115
|
+
/** How many times it looked at the screen. */
|
|
116
|
+
looks: number;
|
|
117
|
+
/** The region of change that woke it, in pixels from the frame's top-left; coordinates only. */
|
|
118
|
+
area?: {
|
|
119
|
+
x: number;
|
|
120
|
+
y: number;
|
|
121
|
+
width: number;
|
|
122
|
+
height: number;
|
|
123
|
+
};
|
|
90
124
|
}
|
|
91
125
|
export interface ActorCapabilities {
|
|
92
126
|
headless: boolean;
|
|
@@ -190,6 +190,11 @@ export function promptFor(request, screenshotPath) {
|
|
|
190
190
|
"giving up, and put your closing words in message. When done=true, set outcome: reached if " +
|
|
191
191
|
"the task is finished, blocked if something in the app stopped you, not_reached if you are " +
|
|
192
192
|
"stopping for another reason. Otherwise outcome is null.",
|
|
193
|
+
"A turn whose actions are only waits ends when the screen changes and holds still, or when its " +
|
|
194
|
+
"waits' ms pass. While no change on the screen holds still, each further turn in a row that " +
|
|
195
|
+
"only waits lasts twice as long as the one before, up to the study's longest wait, and the note " +
|
|
196
|
+
"from the harness says how long you waited and why it ended. A wait among other actions lasts " +
|
|
197
|
+
"its ms.",
|
|
193
198
|
`Reply with ONLY a JSON object of this shape: ${shape}`,
|
|
194
199
|
hint,
|
|
195
200
|
].join("\n");
|
|
@@ -6,4 +6,4 @@ export interface ActorEnding {
|
|
|
6
6
|
/** Shared wording for consumers that already hold a validated, finite stop cause. */
|
|
7
7
|
export declare function actorStopCauseLabel(cause: ActorEnding["cause"]): string;
|
|
8
8
|
/** Project recorded control evidence, never participant prose. Leave the actor untouched. */
|
|
9
|
-
export declare function actorEnding(actor: ActorTrace | undefined): ActorEnding | undefined;
|
|
9
|
+
export declare function actorEnding(actor: Pick<ActorTrace, "stopCause" | "items" | "completionReason"> | undefined): ActorEnding | undefined;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { resolveRunPath } from "../run/locate.js";
|
|
3
|
-
import {} from "../run/bundle.js";
|
|
4
3
|
import { refreshObserver, analyzeRun, readCompletedAnalysisSource, resolveAnalysisRun, } from "./service.js";
|
|
5
4
|
import { preferLargerAnalysisOutput, ANALYSIS_PROMPT_VERSION } from "./execute.js";
|
|
6
5
|
import { captureEvidence } from "./evidence.js";
|