humanish 0.116.0 → 0.118.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 +40 -0
- package/dist/actors/codex/restricted-participant-policy.d.ts +0 -2
- package/dist/actors/codex/restricted-participant-policy.js +4 -32
- package/dist/actors/codex/restricted-participant.d.ts +5 -0
- package/dist/actors/codex/restricted-participant.js +3 -2
- package/dist/actors/computer-use/actor.d.ts +4 -0
- package/dist/actors/computer-use/actor.js +2 -0
- package/dist/actors/computer-use/loop/actions.d.ts +4 -4
- package/dist/actors/computer-use/loop/actions.js +25 -18
- package/dist/actors/computer-use/loop/session.d.ts +3 -0
- package/dist/actors/computer-use/loop/session.js +9 -1
- package/dist/actors/computer-use/loop/trace.js +1 -0
- package/dist/actors/computer-use/loop/types.d.ts +16 -14
- package/dist/actors/computer-use/loop.d.ts +1 -1
- package/dist/actors/computer-use/loop.js +1 -1
- package/dist/actors/computer-use/wait.d.ts +74 -0
- package/dist/actors/computer-use/wait.js +100 -0
- package/dist/actors/contract.d.ts +14 -0
- package/dist/analysis/admission.d.ts +7 -2
- package/dist/analysis/admission.js +12 -3
- package/dist/analysis/automatic-config.d.ts +6 -1
- package/dist/analysis/automatic-config.js +12 -11
- package/dist/analysis/execute.d.ts +19 -7
- package/dist/analysis/execute.js +55 -17
- package/dist/browser-control/protocol.d.ts +86 -6
- package/dist/browser-control/protocol.js +13 -6
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/study-format.js +8 -11
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +2 -2
- package/dist/cli/commands/study-route-shared-world.js +5 -3
- package/dist/cli/commands/study-route-terminal.js +2 -2
- package/dist/cli/commands/study-run.js +3 -3
- package/dist/cli/commands/study.js +2 -2
- package/dist/cli/doctor.js +11 -1
- package/dist/cli/io.js +3 -2
- package/dist/comms/lease-store.d.ts +1 -1
- package/dist/comms/lease-store.js +3 -2
- package/dist/comms/receiving-types.d.ts +2 -0
- package/dist/comms/receiving-types.js +2 -0
- package/dist/guest/desktop-executor.js +3 -1
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/e2b-desktop/prepare.js +40 -31
- package/dist/routes/computer-use/fanout-bundle.js +2 -1
- package/dist/routes/computer-use/fanout-records.js +2 -1
- package/dist/routes/computer-use/local-vm.js +5 -2
- package/dist/routes/computer-use/participant-model.js +9 -0
- package/dist/routes/computer-use/participant-runs.js +3 -9
- package/dist/routes/computer-use/plan.d.ts +3 -1
- package/dist/routes/computer-use/plan.js +29 -9
- package/dist/routes/computer-use/single-bundle.js +7 -4
- package/dist/routes/computer-use/subject-projection.js +2 -0
- package/dist/routes/computer-use/types.d.ts +1 -1
- package/dist/routes/scripted/plan.d.ts +4 -1
- package/dist/routes/scripted/plan.js +25 -2
- package/dist/routes/scripted/setup.js +1 -4
- package/dist/routes/scripted/types.d.ts +1 -1
- package/dist/routes/shared-world/participant-specs.js +2 -2
- package/dist/routes/shared-world/plan.d.ts +3 -1
- package/dist/routes/shared-world/plan.js +39 -7
- package/dist/routes/shared-world/route.js +2 -2
- package/dist/routes/terminal/live-sandbox.d.ts +1 -1
- package/dist/routes/terminal/live-sandbox.js +14 -11
- package/dist/routes/terminal/sandbox.d.ts +4 -3
- package/dist/routes/terminal/sandbox.js +5 -5
- package/dist/run/bundle-shape.js +2 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/projection.js +7 -2
- package/dist/run/route-shell.js +10 -5
- package/dist/run/subject-name.d.ts +11 -0
- package/dist/run/subject-name.js +11 -0
- package/dist/study/automatic-analysis-plan.d.ts +28 -0
- package/dist/study/automatic-analysis-plan.js +57 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/config.js +10 -21
- package/dist/study/doctor.js +17 -3
- package/dist/study/keys.d.ts +2 -0
- package/dist/study/keys.js +2 -0
- package/dist/study/migrate/v2.js +2 -1
- package/dist/study/parse/actors.d.ts +1 -1
- package/dist/study/parse/actors.js +9 -1
- package/dist/study/parse/front.d.ts +1 -1
- package/dist/study/parse/front.js +7 -1
- package/dist/study/plan-base.d.ts +2 -0
- package/dist/study/plan-base.js +3 -0
- package/dist/study/plan-participants.d.ts +4 -0
- package/dist/study/plan-participants.js +6 -5
- package/dist/study/plan-types.d.ts +8 -3
- package/dist/study/plan.d.ts +4 -4
- package/dist/study/plan.js +21 -12
- package/dist/study/preflight-result.js +2 -2
- package/dist/study/preflight.d.ts +2 -1
- package/dist/study/preflight.js +12 -3
- package/dist/study/routing.d.ts +5 -4
- package/dist/study/routing.js +16 -7
- package/dist/study/summary.d.ts +3 -3
- package/dist/study/summary.js +3 -5
- package/dist/study/types.d.ts +14 -0
- package/dist/study/validation.d.ts +7 -2
- package/dist/study/validation.js +30 -14
- package/dist/study/warnings.js +12 -0
- package/dist/subject/serve.d.ts +3 -0
- package/dist/subject/serve.js +4 -1
- package/dist/substrates/detached.js +14 -7
- package/dist/substrates/e2b/desktop-executor.d.ts +0 -2
- package/dist/substrates/e2b/desktop-executor.js +3 -3
- package/dist/substrates/e2b/lifetime.d.ts +75 -4
- package/dist/substrates/e2b/lifetime.js +134 -10
- package/dist/substrates/e2b/subject-sandbox.js +2 -6
- package/dist/tui-app.js +119 -118
- package/dist/verify/artifacts.d.ts +19 -4
- package/dist/verify/artifacts.js +39 -8
- package/dist/verify/verify.js +6 -15
- package/docs/architecture/browser-control.md +10 -4
- package/docs/contracts/run-bundle.md +7 -6
- package/docs/contracts/schemas.md +33 -19
- package/docs/contracts/study-analysis.md +2 -2
- package/docs/principles/invariants-and-defaults.md +1 -1
- package/docs/product/automatic-analysis.md +4 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,46 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.118.0: Up to 100 participants, E2B concurrent sandboxes, study check plans like run, analysis range (2026-10-09)
|
|
12
|
+
|
|
13
|
+
humanish 0.118.0 lets a computer-use study have up to 100 participants, up from 16, holds a
|
|
14
|
+
shared-world study to the same 100, and runs them within the number of sandboxes the E2B plan runs
|
|
15
|
+
at once. The new setting `HUMANISH_E2B_MAX_CONCURRENT_SANDBOXES` gives that number (20 when unset,
|
|
16
|
+
E2B Hobby's limit; Pro runs 100). A study with more participants than that runs them in waves, an
|
|
17
|
+
`execution.concurrency` above it is refused before any sandbox is created, and `doctor` shows it. A
|
|
18
|
+
live run of more than 16 participants records its automatic analysis as skipped with
|
|
19
|
+
`AUTOMATIC_ANALYSIS_PARTICIPANT_LIMIT`, because one analysis reads at most 16 participants. `study
|
|
20
|
+
check` now plans a study the way `humanish run` does and fails a study the run refuses, and a
|
|
21
|
+
shared-world or scripted clone study whose sandbox deadline passes the ceiling is refused at plan
|
|
22
|
+
time. The analysis line that `run`, `study check` and the TUI print ends its expected range at the
|
|
23
|
+
largest run admission admits (`$0.75 to $2.83` for one participant at the default $3 cap, where it
|
|
24
|
+
read `$3.79`) and states the rule as admission applies it. An in-process `local-app` run records
|
|
25
|
+
`subject.source: local-app`, a desktop-cli dry run names its product, and `verify` names an unlisted
|
|
26
|
+
folder and the 10,000-entry stop in their own sentences. In `humanish tui`, the key legend says what
|
|
27
|
+
Enter and Esc do while an action is armed, a study's median cost includes its analyses, and Esc
|
|
28
|
+
after entering a key returns to the screen the keys were opened from.
|
|
29
|
+
|
|
30
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.118.0)
|
|
31
|
+
|
|
32
|
+
## 0.117.0: Long participant waits, OpenAI wait length, serve.build fails fast, TUI confirmations (2026-10-09)
|
|
33
|
+
|
|
34
|
+
humanish 0.117.0 lets a participant wait through a long hosted study, gives an OpenAI
|
|
35
|
+
participant's wait a usable length, and fails a broken `serve.build` within seconds. A
|
|
36
|
+
participant's wait longer than one desktop call now lasts as long as it asked, up to the new study
|
|
37
|
+
field `actor.maxWaitMs` (1 second to 10 minutes; 2 minutes by default, 30 seconds on a desktop with
|
|
38
|
+
speech), and goes to the desktop in steps of at most 30 seconds. A Codex participant's longer wait
|
|
39
|
+
was cut to 30 seconds before. An `openai-computer-use` participant's `wait` names no duration and
|
|
40
|
+
lasted half a second on a hosted desktop; it now lasts `actor.idleWaitMs` (5 seconds by default) in
|
|
41
|
+
a turn that only waits, and 500 ms after an action in the same turn. The participant's `actor.json`
|
|
42
|
+
records these lengths as `waitSettings`. A `serve.install`, `serve.build` or seed step that bash
|
|
43
|
+
cannot parse fails within seconds with bash's error, where the run waited out the step's limit (10
|
|
44
|
+
minutes for `serve.build`), and a failed `serve.build` on the computer-use route has its own error
|
|
45
|
+
code. In `humanish tui`, a two-Enter confirmation ends when the cursor leaves it. The terminal
|
|
46
|
+
route's check after its sandbox kill carries the run's E2B key, and browser Back in the Observer
|
|
47
|
+
clears a stale saved-moment message.
|
|
48
|
+
|
|
49
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.117.0)
|
|
50
|
+
|
|
11
51
|
## 0.116.0: Hosted keyboard input, E2B sandbox ceiling, bounded run reads, run header, TUI live starts (2026-10-09)
|
|
12
52
|
|
|
13
53
|
humanish 0.116.0 delivers a hosted participant's arrow keys and punctuation, lets the operator
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { participantActionSchema, participantSpeechActionSchema, } from "../../browser-control/protocol.js";
|
|
3
3
|
import { closingReportSchema, strictOutputSchema, } from "../closing-report.js";
|
|
4
4
|
/** The declared profile. A participant replaces cliVersion with its detected CLI release, or with
|
|
5
5
|
* this host's newest qualified release before its first launch. */
|
|
@@ -21,15 +21,13 @@ export const PARTICIPANT_LIMITS = Object.freeze({
|
|
|
21
21
|
narration: 2000,
|
|
22
22
|
output: 256 * 1024,
|
|
23
23
|
actions: 4,
|
|
24
|
-
/** The longest wait one action lasts; a longer one is shortened to it. */
|
|
25
|
-
waitMs: BROWSER_CONTROL_LIMITS.waitMs,
|
|
26
24
|
requestMs: 180_000,
|
|
27
25
|
cleanupMs: 5000,
|
|
28
26
|
});
|
|
29
27
|
const toolInput = (speechEnabled) => z.strictObject({
|
|
30
28
|
narration: z.string().max(PARTICIPANT_LIMITS.narration),
|
|
31
29
|
actions: z
|
|
32
|
-
.array(speechEnabled ?
|
|
30
|
+
.array(speechEnabled ? participantSpeechActionSchema : participantActionSchema)
|
|
33
31
|
.min(1)
|
|
34
32
|
.max(PARTICIPANT_LIMITS.actions),
|
|
35
33
|
});
|
|
@@ -43,41 +41,15 @@ const participantFinal = z.strictObject({
|
|
|
43
41
|
...closingReportSchema.shape,
|
|
44
42
|
});
|
|
45
43
|
export const PARTICIPANT_FINAL_SCHEMA = strictOutputSchema(participantFinal);
|
|
46
|
-
/**
|
|
47
|
-
* Shorten each wait longer than one browser-control request may wait. A failed tool call ends the
|
|
48
|
-
* native run, so a wait that asks too much would otherwise end the whole session.
|
|
49
|
-
*/
|
|
50
|
-
function shortenWaits(value) {
|
|
51
|
-
const actions = value?.actions;
|
|
52
|
-
if (typeof value !== "object" || !Array.isArray(actions))
|
|
53
|
-
return { value, shortened: [] };
|
|
54
|
-
const shortened = [];
|
|
55
|
-
const limit = PARTICIPANT_LIMITS.waitMs;
|
|
56
|
-
const next = actions.map((action, index) => {
|
|
57
|
-
const wait = action;
|
|
58
|
-
if (wait?.kind !== "wait" ||
|
|
59
|
-
typeof wait.ms !== "number" ||
|
|
60
|
-
!Number.isFinite(wait.ms) ||
|
|
61
|
-
wait.ms <= limit)
|
|
62
|
-
return action;
|
|
63
|
-
shortened.push({ index, requestedMs: wait.ms, ms: limit });
|
|
64
|
-
return { ...wait, ms: limit };
|
|
65
|
-
});
|
|
66
|
-
return shortened.length === 0
|
|
67
|
-
? { value, shortened }
|
|
68
|
-
: { value: { ...value, actions: next }, shortened };
|
|
69
|
-
}
|
|
70
44
|
/** humanish tool arguments, validated before the shared executor sees a batch. */
|
|
71
45
|
export function parseParticipantTool(value, speechEnabled = false) {
|
|
72
46
|
if (Buffer.byteLength(JSON.stringify(value) ?? "") > PARTICIPANT_LIMITS.output)
|
|
73
47
|
throw new Error("invalid_response");
|
|
74
|
-
const
|
|
75
|
-
const v = toolInput(speechEnabled).parse(admitted);
|
|
48
|
+
const v = toolInput(speechEnabled).parse(value);
|
|
76
49
|
if (Buffer.byteLength(v.narration) > PARTICIPANT_LIMITS.narration)
|
|
77
50
|
throw new Error("invalid_response");
|
|
78
51
|
return {
|
|
79
|
-
actions: v.actions
|
|
80
|
-
...(shortened.length === 0 ? {} : { shortenedWaits: shortened }),
|
|
52
|
+
actions: v.actions,
|
|
81
53
|
pendingSafetyChecks: [],
|
|
82
54
|
done: false,
|
|
83
55
|
...(v.narration ? { message: v.narration } : {}),
|
|
@@ -17,6 +17,11 @@ export interface RestrictedParticipantOptions {
|
|
|
17
17
|
reasoningEffort?: ReasoningEffort;
|
|
18
18
|
/** Admit speech actions and heard-speaker evidence for a speech-capable desktop only. */
|
|
19
19
|
speechEnabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The study's actor.maxWaitMs, which the loop applies and the tool description states. Default
|
|
22
|
+
* defaultMaxWaitMs(speechEnabled), the same default the loop applies.
|
|
23
|
+
*/
|
|
24
|
+
maxWaitMs?: number;
|
|
20
25
|
}
|
|
21
26
|
/** What doctor's hosted-participant check found, before any turn. */
|
|
22
27
|
export interface ParticipantReadiness {
|
|
@@ -3,6 +3,7 @@ import { defaultCodexCliVersion, untestedOperatorReleaseWarning } from "./codex-
|
|
|
3
3
|
import { IMPRESSIONS_ASK, } from "../computer-use/loop.js";
|
|
4
4
|
import { ComputerUseProviderError, isComputerUseProviderError, } from "../computer-use/provider-error.js";
|
|
5
5
|
import { validateBrowserControlPng, validateHeardSpeech } from "../../browser-control/protocol.js";
|
|
6
|
+
import { defaultMaxWaitMs } from "../computer-use/wait.js";
|
|
6
7
|
import { createRestrictedCodexSession, } from "./restricted-session.js";
|
|
7
8
|
import { PARTICIPANT_PROFILE, PARTICIPANT_LIMITS as L, PARTICIPANT_FINAL_SCHEMA, participantToolSchema, parseParticipantTool, parseParticipantFinal, } from "./restricted-participant-policy.js";
|
|
8
9
|
import { truncatedFrameWarning, unknownNotificationsWarning } from "./restricted-notifications.js";
|
|
@@ -40,7 +41,7 @@ function deferred() {
|
|
|
40
41
|
return { promise, resolve, reject };
|
|
41
42
|
}
|
|
42
43
|
// prose-check: model prompt (Codex reads this tool description, not a person)
|
|
43
|
-
const toolDescription = (speechEnabled) => `Act on the participant's browser through humanish. Submit one to four UI actions${speechEnabled ? ", including speak when you need to reply aloud," : ""} and a short public comment; never private reasoning. humanish returns a JSON STRING with execution acknowledgments${speechEnabled ? ", speech heard from the actual participant speaker sink," : ""} and a fresh screenshot. In Code Mode use: const r = JSON.parse(await tools.humanish_ui({narration: "...", actions: [...]})); text({acknowledgments: r.acknowledgments${speechEnabled ? ", heardSpeech: r.heardSpeech" : ""}, contextHint: r.contextHint, closing: r.closing}); image(r.imageUrl). Call serially and inspect each returned screenshot${speechEnabled ? " and heardSpeech array" : ""} before deciding what to do next. A wait action lasts at most ${
|
|
44
|
+
const toolDescription = (speechEnabled, maxWaitMs) => `Act on the participant's browser through humanish. Submit one to four UI actions${speechEnabled ? ", including speak when you need to reply aloud," : ""} and a short public comment; never private reasoning. humanish returns a JSON STRING with execution acknowledgments${speechEnabled ? ", speech heard from the actual participant speaker sink," : ""} and a fresh screenshot. In Code Mode use: const r = JSON.parse(await tools.humanish_ui({narration: "...", actions: [...]})); text({acknowledgments: r.acknowledgments${speechEnabled ? ", heardSpeech: r.heardSpeech" : ""}, contextHint: r.contextHint, closing: r.closing}); image(r.imageUrl). Call serially and inspect each returned screenshot${speechEnabled ? " and heardSpeech array" : ""} before deciding what to do next. A wait action lasts at most ${maxWaitMs} ms; a longer wait is shortened to ${maxWaitMs} ms, so wait longer with another call.${speechEnabled ? " Use speak only after the visible UI shows that you joined the call and your microphone is unmuted; it sends audio into that call." : ""} Acknowledged input does not prove an application outcome. If closing is true, stop calling tools and give your final account.`;
|
|
44
45
|
const participantInstructions = (prompt, speechEnabled) => `${prompt}\n\nYou are the study participant throughout this conversation, including its closing account. Use only the supplied screenshots and humanish_ui tool to interact. The tool returns a JSON string: parse it, inspect acknowledgments${speechEnabled ? " and heardSpeech captured from the actual participant speaker sink" : ""}, and display imageUrl with Code Mode image(). Do not print the image data URL as text. Keep your persona and earlier observations throughout the session.${speechEnabled ? " A speak action plays into the participant microphone; use it only after the visible UI shows that you joined the call and the microphone is unmuted." : ""} Speak publicly about your experience, never reveal private reasoning. Completed inputs do not prove application outcomes; verify on the next screenshot. When the task ends, return only the required final JSON with outcome, summary, frictionReports and impressions. Report observed confusion and recovered mistakes as well as blockers. ${IMPRESSIONS_ASK} Do not invent observations.`;
|
|
45
46
|
/** Turns and failures from the native run, handed in order to the next provider request. */
|
|
46
47
|
class ParticipantEvents {
|
|
@@ -191,7 +192,7 @@ function participantSession(options, speechEnabled, effort, call) {
|
|
|
191
192
|
reasoningEffort: effort,
|
|
192
193
|
tool: {
|
|
193
194
|
name: "humanish_ui",
|
|
194
|
-
description: toolDescription(speechEnabled),
|
|
195
|
+
description: toolDescription(speechEnabled, options.maxWaitMs ?? defaultMaxWaitMs(speechEnabled)),
|
|
195
196
|
inputSchema: participantToolSchema(speechEnabled),
|
|
196
197
|
call,
|
|
197
198
|
},
|
|
@@ -38,6 +38,10 @@ export interface CuaActorSessionOptions {
|
|
|
38
38
|
acknowledgeSafetyChecks?: (checks: CuaSafetyCheck[]) => CuaSafetyCheck[] | null;
|
|
39
39
|
idleSteps?: number;
|
|
40
40
|
noProgressSteps?: number;
|
|
41
|
+
/** The longest one wait action lasts, forwarded to the loop. See LoopRunOptions.maxWaitMs. */
|
|
42
|
+
maxWaitMs?: number;
|
|
43
|
+
/** How long a wait with no duration lasts in an idle turn. See LoopRunOptions.idleWaitMs. */
|
|
44
|
+
idleWaitMs?: number;
|
|
41
45
|
/**
|
|
42
46
|
* Redact persisted screenshots (blur+downscale). Default `false`: full fidelity for local use.
|
|
43
47
|
* Set true for unowned subjects or share-as-is bundles. The provider always sees raw frames.
|
|
@@ -31,6 +31,8 @@ export async function runCuaActorSession(options) {
|
|
|
31
31
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
32
32
|
...(options.idleSteps === undefined ? {} : { idleSteps: options.idleSteps }),
|
|
33
33
|
...(options.noProgressSteps === undefined ? {} : { noProgressSteps: options.noProgressSteps }),
|
|
34
|
+
...(options.maxWaitMs === undefined ? {} : { maxWaitMs: options.maxWaitMs }),
|
|
35
|
+
...(options.idleWaitMs === undefined ? {} : { idleWaitMs: options.idleWaitMs }),
|
|
34
36
|
...(options.acknowledgeSafetyChecks === undefined
|
|
35
37
|
? {}
|
|
36
38
|
: { acknowledgeSafetyChecks: options.acknowledgeSafetyChecks }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LoopSession } from "./session.js";
|
|
2
|
-
import type { CuaAction, CuaTurnRequest
|
|
2
|
+
import type { CuaAction, CuaTurnRequest } from "./types.js";
|
|
3
3
|
/** A public-safe one-line action label. Never includes raw typed text. */
|
|
4
4
|
export declare function describeCuaAction(action: CuaAction): string;
|
|
5
5
|
/**
|
|
@@ -16,8 +16,8 @@ export interface ActionBatch {
|
|
|
16
16
|
/** Host input acknowledgements for the provider's next request. */
|
|
17
17
|
readonly execution: NonNullable<CuaTurnRequest["previousExecution"]>;
|
|
18
18
|
/**
|
|
19
|
-
* Guidance for the next request, set when the
|
|
20
|
-
*
|
|
19
|
+
* Guidance for the next request, set when the loop shortened a wait or a pre-dispatch rejection
|
|
20
|
+
* stopped the batch.
|
|
21
21
|
*/
|
|
22
22
|
readonly hint: string | undefined;
|
|
23
23
|
}
|
|
@@ -26,4 +26,4 @@ export interface ActionBatch {
|
|
|
26
26
|
* leaves the desktop usable; a failed desktop command skips its action; any other failure ends
|
|
27
27
|
* the session.
|
|
28
28
|
*/
|
|
29
|
-
export declare function runActionBatch(session: LoopSession, actions: readonly CuaAction[]
|
|
29
|
+
export declare function runActionBatch(session: LoopSession, actions: readonly CuaAction[]): Promise<ActionBatch>;
|
|
@@ -3,6 +3,7 @@ import { commandFailureInfo, isCommandExitError } from "../../../substrates/comm
|
|
|
3
3
|
import { ComputerUseExecutorError, isComputerUseExecutorError, } from "../executor-error.js";
|
|
4
4
|
import { CuaAbortError, CuaDeadlineError, CuaStallError, raceCallBound, raceSessionDeadline, requestScope, } from "./race.js";
|
|
5
5
|
import { notice } from "./trace.js";
|
|
6
|
+
import { planWait } from "../wait.js";
|
|
6
7
|
// The participant's actions: their public labels, what counts as a material action, and how one
|
|
7
8
|
// turn's batch is dispatched to the desktop.
|
|
8
9
|
/** Click/scroll coordinates are bucketed to this many pixels before fingerprinting, so a one-pixel
|
|
@@ -109,31 +110,31 @@ function rejectedActionHint(title, reason) {
|
|
|
109
110
|
const remedy = reason === undefined ? "" : ` ${REJECTION_REMEDIES[reason]}`;
|
|
110
111
|
return `Your action (${title}) was rejected before dispatch. No input from that action or the rest of its batch was sent.${remedy} Choose your next action from the fresh screenshot; do not assume the rejected action succeeded.`;
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
session.trace.record("notice", () => notice("warn", "wait shortened", `action: ${title}; requested: ${wait.requestedMs}ms; waited: ${wait.ms}ms, the longest one wait action lasts; the session continued`));
|
|
118
|
-
return shortenedWaitHint(wait);
|
|
113
|
+
/** Record a wait the loop shortened, and return the participant's note about it. */
|
|
114
|
+
function recordShortenedWait(session, title, requestedMs) {
|
|
115
|
+
const ms = session.waits.maxWaitMs;
|
|
116
|
+
session.trace.record("notice", () => notice("warn", "wait shortened", `action: ${title}; requested: ${requestedMs}ms; waited: ${ms}ms, the longest one wait action lasts (actor.maxWaitMs); the session continued`));
|
|
117
|
+
return `Your wait of ${requestedMs}ms was shortened to ${ms}ms, the longest one wait action lasts. To keep waiting, send another wait.`;
|
|
119
118
|
}
|
|
120
119
|
/**
|
|
121
120
|
* Dispatch one turn's actions in order. A declared pre-dispatch rejection stops the batch and
|
|
122
121
|
* leaves the desktop usable; a failed desktop command skips its action; any other failure ends
|
|
123
122
|
* the session.
|
|
124
123
|
*/
|
|
125
|
-
export async function runActionBatch(session, actions
|
|
124
|
+
export async function runActionBatch(session, actions) {
|
|
126
125
|
const execution = { actions: [] };
|
|
127
126
|
const { actionHistory, trace } = session;
|
|
128
127
|
const notes = [];
|
|
129
128
|
const hint = () => (notes.length === 0 ? undefined : notes.join(" "));
|
|
130
|
-
|
|
129
|
+
const idleTurn = isIdleTurn(actions);
|
|
130
|
+
for (const [index, requested] of actions.entries()) {
|
|
131
131
|
if (session.signal?.aborted)
|
|
132
132
|
throw new CuaAbortError();
|
|
133
|
+
const wait = requested.kind === "wait" ? planWait(session.waits, requested.ms, idleTurn) : undefined;
|
|
134
|
+
const action = wait === undefined ? requested : { kind: "wait", ms: wait.ms };
|
|
133
135
|
const title = describeCuaAction(action);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
notes.push(recordShortenedWait(session, title, shortened));
|
|
136
|
+
if (wait?.shortenedFromMs !== undefined)
|
|
137
|
+
notes.push(recordShortenedWait(session, title, wait.shortenedFromMs));
|
|
137
138
|
actionHistory.lastActionTitle = title;
|
|
138
139
|
actionHistory.recentActionTitles.push(title);
|
|
139
140
|
if (actionHistory.recentActionTitles.length > RECENT_ACTION_TITLES)
|
|
@@ -146,7 +147,7 @@ export async function runActionBatch(session, actions, shortenedWaits = []) {
|
|
|
146
147
|
session.phase = `executing ${title}`;
|
|
147
148
|
let status;
|
|
148
149
|
try {
|
|
149
|
-
status = await dispatchAction(session, action, title);
|
|
150
|
+
status = await dispatchAction(session, action, title, wait?.steps);
|
|
150
151
|
}
|
|
151
152
|
catch (error) {
|
|
152
153
|
const declared = isComputerUseExecutorError(error)
|
|
@@ -201,17 +202,23 @@ function countAttempt(session, action, title, status) {
|
|
|
201
202
|
}
|
|
202
203
|
/**
|
|
203
204
|
* Execute one action. Idle actions only look: a `wait` that hangs inside the SDK has in
|
|
204
|
-
* effect waited, so a stalled one is skipped with a notice
|
|
205
|
-
* chose. An action is recorded as completed only after
|
|
205
|
+
* effect waited, so a stalled one is skipped with a notice, its remaining steps are not sent, and
|
|
206
|
+
* it loses nothing the participant chose. An action is recorded as completed only after every
|
|
207
|
+
* desktop call it takes resolves. A wait is sent as `waitSteps`, its planWait steps.
|
|
206
208
|
*/
|
|
207
|
-
async function dispatchAction(session, action, title) {
|
|
209
|
+
async function dispatchAction(session, action, title, waitSteps) {
|
|
208
210
|
if (!isIdleAction(action)) {
|
|
209
211
|
await executeAction(session, action, title);
|
|
210
212
|
return "completed";
|
|
211
213
|
}
|
|
212
|
-
const
|
|
214
|
+
const calls = waitSteps?.map((ms) => ({ kind: "wait", ms })) ?? [action];
|
|
213
215
|
try {
|
|
214
|
-
|
|
216
|
+
for (const call of calls) {
|
|
217
|
+
if (session.signal?.aborted)
|
|
218
|
+
throw new CuaAbortError();
|
|
219
|
+
const boundMs = session.observationTimeoutMs + (call.kind === "wait" ? (call.ms ?? 0) : 0);
|
|
220
|
+
await executeAction(session, call, title, boundMs);
|
|
221
|
+
}
|
|
215
222
|
return "completed";
|
|
216
223
|
}
|
|
217
224
|
catch (error) {
|
|
@@ -5,6 +5,7 @@ import type { DebriefTrigger, Stop } from "./ending.js";
|
|
|
5
5
|
import { TraceRecorder } from "./trace.js";
|
|
6
6
|
import type { CuaExecutor, CuaProvider, CuaSafetyCheck, LoopRunOptions } from "./types.js";
|
|
7
7
|
import { UsageLedger } from "./usage.js";
|
|
8
|
+
import { type WaitSettings } from "../wait.js";
|
|
8
9
|
export type ScreenshotRef = NonNullable<ActorTraceItem["screenshotRef"]>;
|
|
9
10
|
/**
|
|
10
11
|
* The options with no default, read once at entry and passed through. Defaulted options and
|
|
@@ -34,6 +35,8 @@ export declare class LoopSession {
|
|
|
34
35
|
readonly timeoutMs: number;
|
|
35
36
|
readonly idleSteps: number;
|
|
36
37
|
readonly noProgressSteps: number;
|
|
38
|
+
/** How long each wait lasts: the study's wait fields or their defaults. */
|
|
39
|
+
readonly waits: WaitSettings;
|
|
37
40
|
readonly redactScreenshots: boolean;
|
|
38
41
|
readonly now: () => number;
|
|
39
42
|
readonly acknowledgeSafetyChecks: (checks: CuaSafetyCheck[]) => CuaSafetyCheck[] | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TaskTracker } from "../../../study/tasks.js";
|
|
2
2
|
import { TraceRecorder } from "./trace.js";
|
|
3
3
|
import { UsageLedger } from "./usage.js";
|
|
4
|
+
import { resolveWaitSettings } from "../wait.js";
|
|
4
5
|
// The state one loop session shares across its phases: the options as read at entry, the session
|
|
5
6
|
// clock, the trace being recorded, the usage ledger, and what the participant has done so far.
|
|
6
7
|
// Waiting is a legitimate strategy. A persona verifying a sign-up by email polls its inbox
|
|
@@ -24,6 +25,8 @@ export class LoopSession {
|
|
|
24
25
|
timeoutMs;
|
|
25
26
|
idleSteps;
|
|
26
27
|
noProgressSteps;
|
|
28
|
+
/** How long each wait lasts: the study's wait fields or their defaults. */
|
|
29
|
+
waits;
|
|
27
30
|
redactScreenshots;
|
|
28
31
|
// Injected functions are wrapped so they are called without a receiver, as plain functions.
|
|
29
32
|
now;
|
|
@@ -67,7 +70,7 @@ export class LoopSession {
|
|
|
67
70
|
scrubText;
|
|
68
71
|
constructor(options) {
|
|
69
72
|
// Read once, in this order: a caller mutating its options object mid-run changes nothing.
|
|
70
|
-
const { instructions, provider, executor, persona, redaction, timeoutMs, now, signal, idleSteps = DEFAULT_IDLE_STEPS, noProgressSteps = DEFAULT_NO_PROGRESS_STEPS, acknowledgeSafetyChecks = () => null, redactScreenshots = false, scrubText = (text) => text, writeScreenshot = async (name) => `screenshots/${name}`, stopWhen, dwell, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), tasks, maxUsd, overRunBudget, estimateTurnCostUsd, requireReportedUsageForSpendCap = false, onObservedUrl, onMessage, onScreenshot, onTrace, } = options;
|
|
73
|
+
const { instructions, provider, executor, persona, redaction, timeoutMs, now, signal, idleSteps = DEFAULT_IDLE_STEPS, noProgressSteps = DEFAULT_NO_PROGRESS_STEPS, maxWaitMs, idleWaitMs, acknowledgeSafetyChecks = () => null, redactScreenshots = false, scrubText = (text) => text, writeScreenshot = async (name) => `screenshots/${name}`, stopWhen, dwell, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), tasks, maxUsd, overRunBudget, estimateTurnCostUsd, requireReportedUsageForSpendCap = false, onObservedUrl, onMessage, onScreenshot, onTrace, } = options;
|
|
71
74
|
this.settings = {
|
|
72
75
|
instructions,
|
|
73
76
|
persona,
|
|
@@ -89,6 +92,11 @@ export class LoopSession {
|
|
|
89
92
|
this.timeoutMs = timeoutMs;
|
|
90
93
|
this.idleSteps = idleSteps;
|
|
91
94
|
this.noProgressSteps = noProgressSteps;
|
|
95
|
+
this.waits = resolveWaitSettings({
|
|
96
|
+
maxWaitMs,
|
|
97
|
+
idleWaitMs,
|
|
98
|
+
speechEnabled: executor.speechEnabled === true,
|
|
99
|
+
});
|
|
92
100
|
this.redactScreenshots = redactScreenshots;
|
|
93
101
|
this.now = () => now();
|
|
94
102
|
this.acknowledgeSafetyChecks = (checks) => acknowledgeSafetyChecks(checks);
|
|
@@ -123,6 +123,7 @@ export function loopResult(session, stop, debrief) {
|
|
|
123
123
|
: { maxOutputTokens: provider.modelSettings.maxOutputTokens }),
|
|
124
124
|
},
|
|
125
125
|
}),
|
|
126
|
+
waitSettings: { ...session.waits },
|
|
126
127
|
counts,
|
|
127
128
|
items: recorder.items,
|
|
128
129
|
...(session.actionHistory.affordances.length > 0
|
|
@@ -55,15 +55,6 @@ export type CuaAction = {
|
|
|
55
55
|
} | {
|
|
56
56
|
kind: "screenshot";
|
|
57
57
|
};
|
|
58
|
-
/** A wait action whose duration the provider shortened before the loop dispatched it. */
|
|
59
|
-
export interface ShortenedWait {
|
|
60
|
-
/** The action's index in its turn. */
|
|
61
|
-
index: number;
|
|
62
|
-
/** What the participant asked for. */
|
|
63
|
-
requestedMs: number;
|
|
64
|
-
/** What the action waits: the `ms` it carries. */
|
|
65
|
-
ms: number;
|
|
66
|
-
}
|
|
67
58
|
/** A captured desktop state: the (optional) frame plus a coarse signature for progress. */
|
|
68
59
|
export interface CuaObservation {
|
|
69
60
|
/**
|
|
@@ -171,11 +162,6 @@ export interface CuaTurn {
|
|
|
171
162
|
message?: string;
|
|
172
163
|
/** Actions to perform this turn. Empty means the model is done. */
|
|
173
164
|
actions: CuaAction[];
|
|
174
|
-
/**
|
|
175
|
-
* Waits the provider shortened to the longest wait its desktop accepts, by index into
|
|
176
|
-
* `actions`. The loop records each one and tells the participant on its next request.
|
|
177
|
-
*/
|
|
178
|
-
shortenedWaits?: ReadonlyArray<ShortenedWait>;
|
|
179
165
|
/** Safety checks the provider flagged this turn. Non-empty pauses the run. */
|
|
180
166
|
pendingSafetyChecks: CuaSafetyCheck[];
|
|
181
167
|
/** Token accounting for this turn, if available. */
|
|
@@ -304,6 +290,22 @@ export interface CuaLoopOptions {
|
|
|
304
290
|
idleSteps?: number;
|
|
305
291
|
/** No-progress streak that trips the backstop. Default 20. */
|
|
306
292
|
noProgressSteps?: number;
|
|
293
|
+
/**
|
|
294
|
+
* The longest one `wait` action lasts, in ms: the study's actor.maxWaitMs. A longer wait is
|
|
295
|
+
* shortened to it, the trace records a `wait shortened` notice, and the participant is told on its
|
|
296
|
+
* next request. A wait longer than one desktop call carries is sent in steps. A whole number from
|
|
297
|
+
* 1000 to 600000; any other value throws a RangeError before the session starts. Default
|
|
298
|
+
* defaultMaxWaitMs: 120000, or 30000 when the executor is speechEnabled.
|
|
299
|
+
*/
|
|
300
|
+
maxWaitMs?: number;
|
|
301
|
+
/**
|
|
302
|
+
* How long a `wait` that names no duration lasts, in ms, when its turn only waits or takes
|
|
303
|
+
* screenshots: the study's actor.idleWaitMs. After an action in the same turn such a wait lasts
|
|
304
|
+
* 500 ms. A whole number from 1000 to 600000 and no longer than an explicit maxWaitMs; any other
|
|
305
|
+
* value throws a RangeError before the session starts. Default 5000. The session never applies
|
|
306
|
+
* more than its maxWaitMs, and the trace records the applied lengths as `waitSettings`.
|
|
307
|
+
*/
|
|
308
|
+
idleWaitMs?: number;
|
|
307
309
|
/**
|
|
308
310
|
* If the model flags safety checks, decide which to acknowledge. Returning the
|
|
309
311
|
* list proceeds (the acks are echoed back on the next turn's request); returning
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CuaLoopOptions, CuaLoopResult, LoopRunOptions } from "./loop/types.js";
|
|
2
|
-
export type { CuaAction, CuaExecutor, CuaLiveMetadata, CuaLoopOptions, CuaLoopResult, CuaObservation, CuaProvider, CuaSafetyCheck, CuaSpendGate, CuaTurn, CuaTurnRequest,
|
|
2
|
+
export type { CuaAction, CuaExecutor, CuaLiveMetadata, CuaLoopOptions, CuaLoopResult, CuaObservation, CuaProvider, CuaSafetyCheck, CuaSpendGate, CuaTurn, CuaTurnRequest, } from "./loop/types.js";
|
|
3
3
|
export { describeCuaAction } from "./loop/actions.js";
|
|
4
4
|
export { stableProgressKey } from "./loop/backstop.js";
|
|
5
5
|
export { IMPRESSIONS_ASK } from "./loop/impressions.js";
|
|
@@ -133,7 +133,7 @@ async function runTurns(session, conversation) {
|
|
|
133
133
|
? { ...ended, debriefTrigger: { kind: "participant_end", observation } }
|
|
134
134
|
: ended;
|
|
135
135
|
}
|
|
136
|
-
const batch = await runActionBatch(session, turn.actions
|
|
136
|
+
const batch = await runActionBatch(session, turn.actions);
|
|
137
137
|
conversation.previousExecution = batch.execution;
|
|
138
138
|
const checkpoint = await observer.checkpoint(turnNumber);
|
|
139
139
|
if ("stop" in checkpoint)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ActorWaitSettings } from "../contract.js";
|
|
2
|
+
export declare const CUA_WAIT_LIMITS: Readonly<{
|
|
3
|
+
/**
|
|
4
|
+
* The longest wait one desktop call is given. A browser-control request must be answered within
|
|
5
|
+
* its 35 s deadline, and a hosted desktop runs a wait as a sandbox command that times out after
|
|
6
|
+
* 60 s, so the loop sends a longer wait as consecutive steps of at most this.
|
|
7
|
+
*/
|
|
8
|
+
stepMs: 30000;
|
|
9
|
+
/**
|
|
10
|
+
* The longest one wait action lasts when a study does not set actor.maxWaitMs and the desktop
|
|
11
|
+
* has no speech (defaultMaxWaitMs). Two minutes covers waiting in a lobby or for an email to
|
|
12
|
+
* arrive, and the participant still sees a fresh screenshot at least that often.
|
|
13
|
+
*/
|
|
14
|
+
defaultMaxMs: 120000;
|
|
15
|
+
/**
|
|
16
|
+
* How long a wait that names no duration lasts in a turn that only waits or takes screenshots,
|
|
17
|
+
* when a study does not set actor.idleWaitMs. OpenAI's computer-use `wait` names no duration. A
|
|
18
|
+
* 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.
|
|
20
|
+
*/
|
|
21
|
+
defaultIdleMs: 5000;
|
|
22
|
+
/**
|
|
23
|
+
* How long a wait that names no duration lasts after the participant acts in the same turn, so
|
|
24
|
+
* 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.
|
|
26
|
+
*/
|
|
27
|
+
settleMs: 500;
|
|
28
|
+
/** The smallest actor.maxWaitMs or actor.idleWaitMs a study may set. */
|
|
29
|
+
leastMs: 1000;
|
|
30
|
+
/** The largest actor.maxWaitMs or actor.idleWaitMs a study may set. */
|
|
31
|
+
mostMs: 600000;
|
|
32
|
+
}>;
|
|
33
|
+
/** The wait lengths one session applies; the trace records them as `waitSettings`. */
|
|
34
|
+
export type WaitSettings = Readonly<ActorWaitSettings>;
|
|
35
|
+
/** One wait as the desktop runs it. */
|
|
36
|
+
export interface PlannedWait {
|
|
37
|
+
/** How long the wait lasts. */
|
|
38
|
+
readonly ms: number;
|
|
39
|
+
/** The desktop calls it takes, in order, each at most CUA_WAIT_LIMITS.stepMs. */
|
|
40
|
+
readonly steps: readonly number[];
|
|
41
|
+
/** What the participant asked for, when that was longer than maxWaitMs. */
|
|
42
|
+
readonly shortenedFromMs?: number;
|
|
43
|
+
}
|
|
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
|
+
/**
|
|
52
|
+
* Why these wait fields cannot run, or null. `prefix` names their owner in the message: `actor.`
|
|
53
|
+
* in a study file, empty for loop options.
|
|
54
|
+
*/
|
|
55
|
+
export declare function waitFieldsReason(fields: {
|
|
56
|
+
readonly maxWaitMs?: unknown;
|
|
57
|
+
readonly idleWaitMs?: unknown;
|
|
58
|
+
}, prefix: string): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* The wait lengths a session applies: the study's values, or their defaults. The idle wait never
|
|
61
|
+
* outlasts the longest wait, which on a desktop with speech defaults to 30 s. Throws a RangeError
|
|
62
|
+
* for fields waitFieldsReason refuses.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveWaitSettings(options: {
|
|
65
|
+
readonly maxWaitMs?: number | undefined;
|
|
66
|
+
readonly idleWaitMs?: number | undefined;
|
|
67
|
+
readonly speechEnabled: boolean;
|
|
68
|
+
}): WaitSettings;
|
|
69
|
+
/**
|
|
70
|
+
* How long one wait lasts and the desktop calls it takes. A wait that names no duration lasts
|
|
71
|
+
* idleWaitMs when its turn only waits or takes screenshots (`idleTurn`), and settleWaitMs after an
|
|
72
|
+
* action. A longer wait than maxWaitMs is shortened to it. 70 s is sent as 30 s, 30 s and 10 s.
|
|
73
|
+
*/
|
|
74
|
+
export declare function planWait(settings: WaitSettings, requestedMs: number | undefined, idleTurn: boolean): PlannedWait;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// How long one participant wait action lasts, for every provider and desktop. A study sets the
|
|
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 and sends it to the desktop
|
|
4
|
+
// in steps one desktop call carries.
|
|
5
|
+
export const CUA_WAIT_LIMITS = Object.freeze({
|
|
6
|
+
/**
|
|
7
|
+
* The longest wait one desktop call is given. A browser-control request must be answered within
|
|
8
|
+
* its 35 s deadline, and a hosted desktop runs a wait as a sandbox command that times out after
|
|
9
|
+
* 60 s, so the loop sends a longer wait as consecutive steps of at most this.
|
|
10
|
+
*/
|
|
11
|
+
stepMs: 30_000,
|
|
12
|
+
/**
|
|
13
|
+
* The longest one wait action lasts when a study does not set actor.maxWaitMs and the desktop
|
|
14
|
+
* has no speech (defaultMaxWaitMs). Two minutes covers waiting in a lobby or for an email to
|
|
15
|
+
* arrive, and the participant still sees a fresh screenshot at least that often.
|
|
16
|
+
*/
|
|
17
|
+
defaultMaxMs: 120_000,
|
|
18
|
+
/**
|
|
19
|
+
* How long a wait that names no duration lasts in a turn that only waits or takes screenshots,
|
|
20
|
+
* when a study does not set actor.idleWaitMs. OpenAI's computer-use `wait` names no duration. A
|
|
21
|
+
* 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.
|
|
23
|
+
*/
|
|
24
|
+
defaultIdleMs: 5_000,
|
|
25
|
+
/**
|
|
26
|
+
* How long a wait that names no duration lasts after the participant acts in the same turn, so
|
|
27
|
+
* 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.
|
|
29
|
+
*/
|
|
30
|
+
settleMs: 500,
|
|
31
|
+
/** The smallest actor.maxWaitMs or actor.idleWaitMs a study may set. */
|
|
32
|
+
leastMs: 1_000,
|
|
33
|
+
/** The largest actor.maxWaitMs or actor.idleWaitMs a study may set. */
|
|
34
|
+
mostMs: 600_000,
|
|
35
|
+
});
|
|
36
|
+
/** A whole number of milliseconds within the range actor.maxWaitMs and actor.idleWaitMs accept. */
|
|
37
|
+
function isWaitMs(value) {
|
|
38
|
+
return (typeof value === "number" &&
|
|
39
|
+
Number.isSafeInteger(value) &&
|
|
40
|
+
value >= CUA_WAIT_LIMITS.leastMs &&
|
|
41
|
+
value <= CUA_WAIT_LIMITS.mostMs);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The longest one wait action lasts when a study does not set actor.maxWaitMs. On a desktop with
|
|
45
|
+
* speech, heard speech reaches the participant only with a screenshot, and the desktop session ends
|
|
46
|
+
* when more utterances arrive between two screenshots than it holds. A wait takes no screenshot, so
|
|
47
|
+
* there the default keeps one wait to one desktop call.
|
|
48
|
+
*/
|
|
49
|
+
export function defaultMaxWaitMs(speechEnabled) {
|
|
50
|
+
return speechEnabled ? CUA_WAIT_LIMITS.stepMs : CUA_WAIT_LIMITS.defaultMaxMs;
|
|
51
|
+
}
|
|
52
|
+
const FIELDS = [
|
|
53
|
+
["maxWaitMs", "the longest one wait action lasts"],
|
|
54
|
+
["idleWaitMs", "how long a wait with no duration lasts while the participant only waits"],
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Why these wait fields cannot run, or null. `prefix` names their owner in the message: `actor.`
|
|
58
|
+
* in a study file, empty for loop options.
|
|
59
|
+
*/
|
|
60
|
+
export function waitFieldsReason(fields, prefix) {
|
|
61
|
+
for (const [name, meaning] of FIELDS) {
|
|
62
|
+
if (fields[name] !== undefined && !isWaitMs(fields[name]))
|
|
63
|
+
return `${prefix}${name}, ${meaning}, must be a whole number of milliseconds from ${CUA_WAIT_LIMITS.leastMs} to ${CUA_WAIT_LIMITS.mostMs}.`;
|
|
64
|
+
}
|
|
65
|
+
const { maxWaitMs, idleWaitMs } = fields;
|
|
66
|
+
if (typeof maxWaitMs === "number" && typeof idleWaitMs === "number" && idleWaitMs > maxWaitMs)
|
|
67
|
+
return `${prefix}idleWaitMs (${idleWaitMs}) is longer than ${prefix}maxWaitMs (${maxWaitMs}), the longest one wait action lasts. Lower ${prefix}idleWaitMs or raise ${prefix}maxWaitMs.`;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The wait lengths a session applies: the study's values, or their defaults. The idle wait never
|
|
72
|
+
* outlasts the longest wait, which on a desktop with speech defaults to 30 s. Throws a RangeError
|
|
73
|
+
* for fields waitFieldsReason refuses.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveWaitSettings(options) {
|
|
76
|
+
const reason = waitFieldsReason(options, "");
|
|
77
|
+
if (reason !== null)
|
|
78
|
+
throw new RangeError(reason);
|
|
79
|
+
const maxWaitMs = options.maxWaitMs ?? defaultMaxWaitMs(options.speechEnabled);
|
|
80
|
+
return {
|
|
81
|
+
maxWaitMs,
|
|
82
|
+
idleWaitMs: Math.min(options.idleWaitMs ?? CUA_WAIT_LIMITS.defaultIdleMs, maxWaitMs),
|
|
83
|
+
settleWaitMs: CUA_WAIT_LIMITS.settleMs,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* How long one wait lasts and the desktop calls it takes. A wait that names no duration lasts
|
|
88
|
+
* idleWaitMs when its turn only waits or takes screenshots (`idleTurn`), and settleWaitMs after an
|
|
89
|
+
* action. A longer wait than maxWaitMs is shortened to it. 70 s is sent as 30 s, 30 s and 10 s.
|
|
90
|
+
*/
|
|
91
|
+
export function planWait(settings, requestedMs, idleTurn) {
|
|
92
|
+
const askedMs = requestedMs ?? (idleTurn ? settings.idleWaitMs : settings.settleWaitMs);
|
|
93
|
+
const ms = Math.min(askedMs, settings.maxWaitMs);
|
|
94
|
+
const { stepMs } = CUA_WAIT_LIMITS;
|
|
95
|
+
const steps = [];
|
|
96
|
+
for (let left = ms; left > stepMs; left -= stepMs)
|
|
97
|
+
steps.push(stepMs);
|
|
98
|
+
steps.push(ms - steps.length * stepMs);
|
|
99
|
+
return { ms, steps, ...(askedMs > ms ? { shortenedFromMs: askedMs } : {}) };
|
|
100
|
+
}
|