humanish 0.145.0 → 0.147.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/actors/computer-use/loop/spend.js +5 -4
  3. package/dist/actors/computer-use/loop/trace.js +3 -0
  4. package/dist/actors/computer-use/loop/types.d.ts +5 -0
  5. package/dist/actors/computer-use/loop/usage.js +10 -2
  6. package/dist/actors/computer-use/openai-conversation.d.ts +7 -0
  7. package/dist/actors/computer-use/openai-conversation.js +19 -2
  8. package/dist/actors/computer-use/openai-provider.d.ts +2 -0
  9. package/dist/actors/computer-use/openai-provider.js +2 -0
  10. package/dist/actors/computer-use/openai-wire.d.ts +4 -0
  11. package/dist/actors/computer-use/openai-wire.js +9 -1
  12. package/dist/actors/contract.d.ts +5 -0
  13. package/dist/analysis/admission.d.ts +8 -3
  14. package/dist/analysis/admission.js +7 -3
  15. package/dist/analysis/automatic-config.js +2 -1
  16. package/dist/analysis/automatic.js +2 -2
  17. package/dist/analysis/evidence.js +8 -4
  18. package/dist/analysis/execute.d.ts +7 -10
  19. package/dist/analysis/execute.js +79 -49
  20. package/dist/analysis/image-tokens.js +9 -3
  21. package/dist/analysis/output-limit.d.ts +12 -0
  22. package/dist/analysis/output-limit.js +34 -0
  23. package/dist/analysis/output-retry.d.ts +20 -0
  24. package/dist/analysis/output-retry.js +63 -0
  25. package/dist/analysis/provider.d.ts +1 -1
  26. package/dist/analysis/provider.js +3 -3
  27. package/dist/analysis/responses.js +1 -1
  28. package/dist/analysis/service.js +2 -2
  29. package/dist/cli/commands/analyze.js +1 -1
  30. package/dist/cli/io.js +2 -0
  31. package/dist/cli/run-findings.js +10 -2
  32. package/dist/evidence/image.d.ts +2 -0
  33. package/dist/evidence/image.js +24 -3
  34. package/dist/evidence/webp-encode-worker.d.ts +1 -0
  35. package/dist/evidence/webp-encode-worker.js +31 -0
  36. package/dist/evidence/webp-transcoder.d.ts +15 -0
  37. package/dist/evidence/webp-transcoder.js +98 -0
  38. package/dist/evidence/webp.d.ts +31 -0
  39. package/dist/evidence/webp.js +94 -0
  40. package/dist/feedback/derivative-text.d.ts +7 -2
  41. package/dist/feedback/derivative-text.js +20 -1
  42. package/dist/feedback/export-bundle.js +79 -25
  43. package/dist/observer-app.html +1 -1
  44. package/dist/remote/launch.js +93 -48
  45. package/dist/remote/runner.d.ts +47 -14
  46. package/dist/remote/runner.js +199 -64
  47. package/dist/routes/computer-use/e2b-desktop/desktop.js +47 -7
  48. package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +4 -2
  49. package/dist/routes/computer-use/e2b-desktop/prepare.js +4 -2
  50. package/dist/routes/computer-use/e2b-desktop/start.d.ts +14 -6
  51. package/dist/routes/computer-use/e2b-desktop/start.js +72 -34
  52. package/dist/routes/computer-use/participant-execution.d.ts +0 -7
  53. package/dist/routes/computer-use/participant-execution.js +6 -28
  54. package/dist/routes/computer-use/participant-frames.d.ts +32 -0
  55. package/dist/routes/computer-use/participant-frames.js +93 -0
  56. package/dist/routes/computer-use/participant-model.js +3 -0
  57. package/dist/routes/computer-use/participant-runs.js +4 -0
  58. package/dist/routes/computer-use/plan.js +5 -1
  59. package/dist/routes/computer-use/result.js +6 -0
  60. package/dist/routes/computer-use/types.d.ts +4 -0
  61. package/dist/routes/shared-world/plan.js +4 -1
  62. package/dist/run/pricing.d.ts +7 -0
  63. package/dist/run/pricing.js +58 -15
  64. package/dist/study/config.js +4 -1
  65. package/dist/study/keys.d.ts +1 -0
  66. package/dist/study/keys.js +1 -0
  67. package/dist/study/parse/actors.js +5 -0
  68. package/dist/study/plan-participants.d.ts +2 -0
  69. package/dist/study/plan-participants.js +2 -0
  70. package/dist/study/types.d.ts +7 -0
  71. package/dist/study/validation.d.ts +5 -0
  72. package/dist/study/validation.js +23 -0
  73. package/dist/substrates/e2b/cdp-probe.d.ts +16 -1
  74. package/dist/substrates/e2b/cdp-probe.js +67 -5
  75. package/dist/substrates/e2b/desktop-browser.js +4 -2
  76. package/dist/substrates/e2b/desktop-cdp.d.ts +9 -2
  77. package/dist/substrates/e2b/desktop-cdp.js +26 -4
  78. package/dist/verify/artifacts.js +2 -1
  79. package/docs/contracts/run-bundle.md +29 -11
  80. package/docs/contracts/schemas.md +20 -12
  81. package/docs/contracts/study-analysis.md +64 -11
  82. package/docs/product/automatic-analysis.md +9 -6
  83. package/package.json +6 -5
@@ -1,43 +1,55 @@
1
- // The second half of an E2B desktop participant's preparation, and the start of its session: speech and
2
- // recording, then the browser or terminal the participant sees, then the screen saver turned off,
3
- // then the live stream. Each step fills the participant's state.
1
+ // The second half of an E2B desktop participant's preparation, and the start of its session: speech,
2
+ // recording, the browser or terminal the participant sees, the wait for its page, the screen saver
3
+ // turned off, then the live stream. Each step fills the participant's state.
4
4
  import { redactText, toErrorMessage } from "../../../evidence/redaction.js";
5
5
  import { CHROME_DEVTOOLS_PORT, DESKTOP_SETTLE_MS, openDesktopBrowserTarget, openDesktopTerminal, startDesktopStream, } from "../../../substrates/e2b/desktop-browser.js";
6
+ import { waitForChromePageLoad } from "../../../substrates/e2b/desktop-cdp.js";
6
7
  import { setPanelClockZone } from "../../../substrates/e2b/desktop-clock.js";
7
8
  import { captureDesktopBrowserGeometry } from "../../../substrates/e2b/desktop-geometry.js";
8
9
  import { prepareDesktopMedia, startE2BDesktopMedia, } from "../../../substrates/e2b/desktop-media.js";
9
10
  import { startE2BDesktopRecording } from "../../../substrates/e2b/desktop-recording.js";
10
11
  import { turnOffScreenSaver } from "../../../substrates/e2b/desktop-screen.js";
11
- import { applyParticipantMobileFidelity, mobileLaunchFlags } from "./fidelity.js";
12
+ import { applyParticipantMobileFidelity, cdpEndpoint, mobileLaunchFlags } from "./fidelity.js";
12
13
  import { noteNetworkStart, readParticipantNetworkStart, startParticipantNetwork, } from "./network.js";
13
- /** Start the declared speech worker and screen recording. A recording that cannot start is a warning. */
14
- export async function startParticipantMedia(ctx, state, desktop) {
14
+ /** Start the declared speech worker. Chrome is launched in its sound env, so it starts first. */
15
+ export async function startParticipantSpeech(ctx, state, desktop) {
16
+ const requestedMedia = ctx.deps.residual.execution?.desktop?.media;
17
+ if (ctx.desktopCliRoute || requestedMedia?.microphone?.source !== "speech")
18
+ return;
19
+ state.speech = await startE2BDesktopMedia({
20
+ desktop,
21
+ media: requestedMedia,
22
+ signal: state.mediaStop.signal,
23
+ onTerminal: () => state.mediaStop.abort(),
24
+ requestTimeoutMs: ctx.deps.requestTimeoutMs,
25
+ });
26
+ }
27
+ /**
28
+ * Whether the study records the desktop's sound. An audible recording runs its own sound server
29
+ * unless speech already did, and Chrome is launched in that server's env, so the recording starts
30
+ * before Chrome. A silent recording needs nothing from Chrome and starts beside it.
31
+ */
32
+ export function recordsSound(ctx) {
33
+ return ctx.deps.residual.execution?.desktop?.recording?.audio === true;
34
+ }
35
+ /** Start the declared screen recording. A recording that cannot start is a warning. */
36
+ export async function startParticipantRecording(ctx, state, desktop) {
15
37
  const { spec, deps, warnings } = ctx;
16
- const requestedMedia = deps.residual.execution?.desktop?.media;
17
- if (!ctx.desktopCliRoute && requestedMedia?.microphone?.source === "speech") {
18
- state.speech = await startE2BDesktopMedia({
38
+ const requestedRecording = deps.residual.execution?.desktop?.recording;
39
+ if (!requestedRecording)
40
+ return;
41
+ try {
42
+ state.recording = await startE2BDesktopRecording({
19
43
  desktop,
20
- media: requestedMedia,
21
- signal: state.mediaStop.signal,
22
- onTerminal: () => state.mediaStop.abort(),
44
+ width: spec.planned.device.resolution[0],
45
+ height: spec.planned.device.resolution[1],
46
+ audio: requestedRecording.audio,
47
+ ...(state.speech === undefined ? {} : { pulseEnv: state.speech.env }),
23
48
  requestTimeoutMs: deps.requestTimeoutMs,
24
49
  });
25
50
  }
26
- const requestedRecording = deps.residual.execution?.desktop?.recording;
27
- if (requestedRecording) {
28
- try {
29
- state.recording = await startE2BDesktopRecording({
30
- desktop,
31
- width: spec.planned.device.resolution[0],
32
- height: spec.planned.device.resolution[1],
33
- audio: requestedRecording.audio,
34
- ...(state.speech === undefined ? {} : { pulseEnv: state.speech.env }),
35
- requestTimeoutMs: deps.requestTimeoutMs,
36
- });
37
- }
38
- catch (error) {
39
- warnings.push(`Desktop recording startup failed; the study continues without video: ${redactText(deps.scrubKnownValues(toErrorMessage(error)))}`);
40
- }
51
+ catch (error) {
52
+ warnings.push(`Desktop recording startup failed; the study continues without video: ${redactText(deps.scrubKnownValues(toErrorMessage(error)))}`);
41
53
  }
42
54
  }
43
55
  /**
@@ -72,7 +84,8 @@ export async function openParticipantSurface(ctx, state, desktop) {
72
84
  // The participant's time zone is TZ on the launch: the browser process and every renderer it
73
85
  // starts read it at startup, so every tab runs in the zone from the first page load. A CDP
74
86
  // timezone override holds only on the one tab whose DevTools session sets it, from after its load.
75
- const mediaEnv = state.speech?.env ?? state.recording?.env;
87
+ // A silent recording may still be starting, and Chrome needs nothing from it.
88
+ const mediaEnv = state.speech?.env ?? (recordsSound(ctx) ? state.recording?.env : undefined);
76
89
  const browserLaunch = await openDesktopBrowserTarget(desktop, targetUrl, deps.requestTimeoutMs, residual.execution?.desktop?.browser, [...emulationFlags, ...(mediaEvidence?.flags ?? [])], timeZone === undefined ? mediaEnv : { ...mediaEnv, TZ: timeZone });
77
90
  state.desktopBrowser =
78
91
  mediaEvidence === undefined
@@ -91,10 +104,10 @@ export async function openParticipantSurface(ctx, state, desktop) {
91
104
  const emulated = emulationFlags.length > 0;
92
105
  noteDevToolsReadiness(ctx, browserLaunch.devTools, emulated);
93
106
  // A mobile-emulated participant's holder captures too; any other gets a capture-only holder,
94
- // started here so the settle wait covers its start.
107
+ // started here so it records the launch page's load while the page wait runs.
95
108
  if (!emulated)
96
109
  await startParticipantNetwork(ctx, state, desktop);
97
- await desktop.wait(DESKTOP_SETTLE_MS).catch(() => undefined);
110
+ await waitForParticipantPage(ctx, state, desktop, browserLaunch.devTools);
98
111
  if (!emulated)
99
112
  await readParticipantNetworkStart(ctx, state, desktop);
100
113
  const network = state.network;
@@ -119,13 +132,38 @@ export async function openParticipantSurface(ctx, state, desktop) {
119
132
  }),
120
133
  });
121
134
  }
135
+ /**
136
+ * Hold the participant's first screenshot until its page has loaded and painted, so the participant
137
+ * does not start on a blank or half-drawn desktop. A Chromium page whose DevTools answered is read over DevTools for at most DESKTOP_SETTLE_MS, so no
138
+ * participant waits longer than the fixed settle this replaced, and the result is a timed phase in
139
+ * the bundle. Any other browser, and a page wait that cannot run, waits DESKTOP_SETTLE_MS.
140
+ */
141
+ async function waitForParticipantPage(ctx, state, desktop, devTools) {
142
+ const { deps } = ctx;
143
+ const load = state.launchedBrowserFamily === "chromium" && devTools?.state === "ready"
144
+ ? await waitForChromePageLoad(desktop, deps.requestTimeoutMs, cdpEndpoint(state.browserLaunchIdentity, ctx.targetUrl), DESKTOP_SETTLE_MS)
145
+ : undefined;
146
+ if (load === undefined) {
147
+ await desktop.wait(DESKTOP_SETTLE_MS).catch(() => undefined);
148
+ return;
149
+ }
150
+ ctx.onSubjectPhase({
151
+ at: new Date(deps.now()).toISOString(),
152
+ type: "cua-lab.browser.page.completed",
153
+ ok: load.loaded,
154
+ durationMs: load.waitedMs,
155
+ message: load.loaded
156
+ ? "The page loaded and painted"
157
+ : `The page had not loaded and painted when the wait ended (${load.seen}); the participant started anyway`,
158
+ });
159
+ }
122
160
  /**
123
161
  * Turn the desktop's screen saver off, so the screen stays on while the participant waits without
124
162
  * input. The XFCE session sets the screen saver's timeout in its first seconds, and the last write
125
- * wins. Nothing waits for those writes: this runs after the browser or terminal has opened and the
126
- * desktop has waited DESKTOP_SETTLE_MS, and on 16 fresh desktops the last write came within 2.5 s
127
- * of a command sent as soon as the desktop was created. A screen saver that cannot be turned off
128
- * is a warning.
163
+ * wins. Nothing waits for those writes: this runs after a Chromium page has loaded, which needs
164
+ * DevTools to answer first (4.8 s or more after launch on live desktops), or after the fixed 8 s
165
+ * wait, and on 16 fresh desktops the last write came within 2.5 s of a command sent as soon as the
166
+ * desktop was created. A screen saver that cannot be turned off is a warning.
129
167
  */
130
168
  export async function keepParticipantScreenOn(ctx, desktop) {
131
169
  const { spec, deps, warnings } = ctx;
@@ -1,12 +1,5 @@
1
- import { type PreparedOutputRoot } from "../../run/contained-output.js";
2
1
  import type { CuaParticipantDeps, CuaParticipantPlan, CuaParticipantResult, DesktopParticipantRun, ParticipantRunOutcome } from "./types.js";
3
2
  import type { RunSubjectProvenance } from "../../run/bundle.js";
4
- /** Build a participant's writeScreenshot closure: writes under screenshots/<screenshotDir>/ and records
5
- * the relative path the trace references (screenshots/<name> at N=1; screenshots/<laneId>/<name>
6
- * at N>1). */
7
- export declare function makeParticipantWriteScreenshot(artifactRoot: PreparedOutputRoot, spec: {
8
- screenshotDir: string;
9
- }, screenshots: string[]): (name: string, bytes: Buffer) => Promise<string>;
10
3
  /** Run one participant against a prepared desktop. The adapter owns provisioning, final
11
4
  * evidence and cleanup; this runner owns the model, trace and participant outcome. */
12
5
  export declare function runCuaParticipant(spec: DesktopParticipantRun, deps: CuaParticipantDeps): Promise<ParticipantRunOutcome>;
@@ -2,36 +2,12 @@ import { scheduledParticipantDeps } from "./participant-runs.js";
2
2
  import { judgeParticipantRecords } from "../../run/judge.js";
3
3
  import { createE2BParticipantDesktop } from "./e2b-desktop/desktop.js";
4
4
  import { createInProcessDesktop } from "./in-process-desktop.js";
5
- import path from "node:path";
6
5
  import { cuaParticipantDiagnostics } from "./diagnostics.js";
7
6
  import { pastEndReason, runOnSchedule } from "../../study/arrivals.js";
8
- import { assertScreenshotEvidence, stripPngMetadataChunks } from "../../evidence/image.js";
9
7
  import { redactText, toErrorMessage } from "../../evidence/redaction.js";
10
- import { assertSafeOutputPathSegment, writeContainedOutputFile, } from "../../run/contained-output.js";
8
+ import { participantFrames } from "./participant-frames.js";
11
9
  import { participantOutcomeOk, participantFactsOf } from "./participant-facts.js";
12
10
  import { closeParticipantModel, judgeParticipantSession, participantSessionOptions, recordParticipantTrace, startParticipantModel, } from "./participant-model.js";
13
- /** Build a participant's writeScreenshot closure: writes under screenshots/<screenshotDir>/ and records
14
- * the relative path the trace references (screenshots/<name> at N=1; screenshots/<laneId>/<name>
15
- * at N>1). */
16
- export function makeParticipantWriteScreenshot(artifactRoot, spec, screenshots) {
17
- if (spec.screenshotDir) {
18
- assertSafeOutputPathSegment(spec.screenshotDir, "Screenshot participant id");
19
- }
20
- const dirParts = spec.screenshotDir ? ["screenshots", spec.screenshotDir] : ["screenshots"];
21
- const relPrefix = spec.screenshotDir
22
- ? path.posix.join("screenshots", spec.screenshotDir)
23
- : "screenshots";
24
- return async (name, bytes) => {
25
- assertSafeOutputPathSegment(name, "Screenshot name");
26
- const rel = path.posix.join(relPrefix, name);
27
- // Evidence holds image data only, whatever the executor's frames carried.
28
- const frame = stripPngMetadataChunks(bytes);
29
- await assertScreenshotEvidence(rel, frame);
30
- await writeContainedOutputFile(artifactRoot, path.join(...dirParts, name), frame);
31
- screenshots.push(rel);
32
- return rel;
33
- };
34
- }
35
11
  /** A blocked participant outcome: the pipeline gate or the study budget skipped it. */
36
12
  function skippedOutcome(spec, reason, arrival) {
37
13
  return {
@@ -65,8 +41,8 @@ export async function runCuaParticipant(spec, deps) {
65
41
  return skippedOutcome(spec, pastEnd);
66
42
  let model = {};
67
43
  const warnings = [];
68
- const screenshots = [];
69
- const writeScreenshot = makeParticipantWriteScreenshot(deps.artifactRoot, spec, screenshots);
44
+ const frames = participantFrames(deps.artifactRoot, spec, { blurred: deps.redactScreenshots });
45
+ const writeScreenshot = frames.write;
70
46
  let session;
71
47
  let sessionError;
72
48
  let providerCleanupError;
@@ -112,6 +88,7 @@ export async function runCuaParticipant(spec, deps) {
112
88
  else
113
89
  sessionError = desktopFailure;
114
90
  }
91
+ const replacedFrames = await frames.finish(session?.trace);
115
92
  if (session)
116
93
  await recordParticipantTrace(spec, deps, session, warnings);
117
94
  const { noEngagement, selfReportedBlocker, reportedFriction } = judgeParticipantSession(session, deps, warnings);
@@ -129,7 +106,8 @@ export async function runCuaParticipant(spec, deps) {
129
106
  ...(providerPolicyError === undefined ? {} : { providerPolicyError }),
130
107
  ...desktopEvidence,
131
108
  killed: released,
132
- screenshots,
109
+ screenshots: frames.screenshots,
110
+ replacedFrames,
133
111
  warnings,
134
112
  noEngagement,
135
113
  selfReportedBlocker,
@@ -0,0 +1,32 @@
1
+ import type { ActorTrace } from "../../actors/contract.js";
2
+ import { type WebpTranscoder } from "../../evidence/webp-transcoder.js";
3
+ import { type PreparedOutputRoot } from "../../run/contained-output.js";
4
+ /** Build a participant's writeScreenshot closure: writes under screenshots/<screenshotDir>/ and records
5
+ * the relative path the trace references (screenshots/<name> at N=1; screenshots/<laneId>/<name>
6
+ * at N>1). */
7
+ export declare function makeParticipantWriteScreenshot(artifactRoot: PreparedOutputRoot, spec: {
8
+ screenshotDir: string;
9
+ }, screenshots: string[]): (name: string, bytes: Buffer) => Promise<string>;
10
+ export interface ParticipantFrames {
11
+ /** The session's writeScreenshot: writes the PNG, then queues its WebP. */
12
+ write(name: string, bytes: Buffer): Promise<string>;
13
+ /** Every frame written, by the path its trace references names. */
14
+ readonly screenshots: string[];
15
+ /**
16
+ * Waits up to FINISH_WAIT_MS for the frames still queued, then points `trace` and `screenshots`
17
+ * at each frame's WebP. Returns the PNGs those WebP replace, which the run removes once its final
18
+ * bundle is written: until then the live trace in run.json still names them.
19
+ */
20
+ finish(trace: ActorTrace | undefined): Promise<string[]>;
21
+ }
22
+ /**
23
+ * `blurred` is the study's policies.redactScreenshots: its frames are the redactor's PNG
24
+ * thumbnails, which stay PNG, since only that shape counts as blurred evidence.
25
+ */
26
+ export declare function participantFrames(artifactRoot: PreparedOutputRoot, spec: {
27
+ screenshotDir: string;
28
+ }, options: {
29
+ blurred: boolean;
30
+ transcoder?: WebpTranscoder;
31
+ finishWaitMs?: number;
32
+ }): ParticipantFrames;
@@ -0,0 +1,93 @@
1
+ // A computer-use participant's screenshots: each frame is written and referenced as PNG during the
2
+ // session, as the live Observer and a mid-run copy read it, and queued for a lossless WebP copy.
3
+ // When the session ends, each frame whose WebP is written is referenced by its WebP instead and
4
+ // its PNG removed, so at no point does a reference name a file that is not there.
5
+ import path from "node:path";
6
+ import { assertScreenshotEvidence, stripPngMetadataChunks } from "../../evidence/image.js";
7
+ import { webpTranscoder } from "../../evidence/webp-transcoder.js";
8
+ import { assertSafeOutputPathSegment, writeContainedOutputFile, } from "../../run/contained-output.js";
9
+ /**
10
+ * How long a finished session waits for its frames still queued. Encoding takes about 330 ms a
11
+ * frame, so this covers a short backlog; a frame not done by then stays PNG.
12
+ */
13
+ const FINISH_WAIT_MS = 5_000;
14
+ /** Build a participant's writeScreenshot closure: writes under screenshots/<screenshotDir>/ and records
15
+ * the relative path the trace references (screenshots/<name> at N=1; screenshots/<laneId>/<name>
16
+ * at N>1). */
17
+ export function makeParticipantWriteScreenshot(artifactRoot, spec, screenshots) {
18
+ if (spec.screenshotDir) {
19
+ assertSafeOutputPathSegment(spec.screenshotDir, "Screenshot participant id");
20
+ }
21
+ const dirParts = spec.screenshotDir ? ["screenshots", spec.screenshotDir] : ["screenshots"];
22
+ const relPrefix = spec.screenshotDir
23
+ ? path.posix.join("screenshots", spec.screenshotDir)
24
+ : "screenshots";
25
+ return async (name, bytes) => {
26
+ assertSafeOutputPathSegment(name, "Screenshot name");
27
+ const rel = path.posix.join(relPrefix, name);
28
+ // Evidence holds image data only, whatever the executor's frames carried.
29
+ const frame = stripPngMetadataChunks(bytes);
30
+ await assertScreenshotEvidence(rel, frame);
31
+ await writeContainedOutputFile(artifactRoot, path.join(...dirParts, name), frame);
32
+ screenshots.push(rel);
33
+ return rel;
34
+ };
35
+ }
36
+ /**
37
+ * `blurred` is the study's policies.redactScreenshots: its frames are the redactor's PNG
38
+ * thumbnails, which stay PNG, since only that shape counts as blurred evidence.
39
+ */
40
+ export function participantFrames(artifactRoot, spec, options) {
41
+ const transcoder = options.blurred ? undefined : (options.transcoder ?? webpTranscoder());
42
+ const finishWaitMs = options.finishWaitMs ?? FINISH_WAIT_MS;
43
+ const screenshots = [];
44
+ const writePng = makeParticipantWriteScreenshot(artifactRoot, spec, screenshots);
45
+ const queued = [];
46
+ /** Each frame whose WebP is written: its PNG path and its WebP path. */
47
+ const done = new Map();
48
+ // Once the session's references are settled, a late WebP is not written: nothing would name it.
49
+ let finished = false;
50
+ return {
51
+ screenshots,
52
+ async write(name, bytes) {
53
+ const rel = await writePng(name, bytes);
54
+ const webpRel = rel.replace(/\.png$/, ".webp");
55
+ const job = webpRel === rel ? undefined : transcoder?.transcode(bytes);
56
+ // A frame the worker cannot take, or fails to encode or write, stays PNG.
57
+ if (job !== undefined)
58
+ queued.push({
59
+ done: job.webp
60
+ .then(async (webp) => {
61
+ if (finished)
62
+ return;
63
+ await writeContainedOutputFile(artifactRoot, webpRel, webp);
64
+ done.set(rel, webpRel);
65
+ })
66
+ .catch(() => undefined),
67
+ abandon: job.abandon,
68
+ });
69
+ return rel;
70
+ },
71
+ async finish(trace) {
72
+ let timer;
73
+ await Promise.race([
74
+ Promise.all(queued.map((job) => job.done)),
75
+ new Promise((resolve) => (timer = setTimeout(resolve, finishWaitMs))),
76
+ ]);
77
+ clearTimeout(timer);
78
+ finished = true;
79
+ const replaced = new Map(done);
80
+ if (trace !== undefined)
81
+ trace.items = trace.items.map((item) => {
82
+ const webp = item.screenshotRef && replaced.get(item.screenshotRef.path);
83
+ return webp === undefined || item.screenshotRef === undefined
84
+ ? item
85
+ : { ...item, screenshotRef: { ...item.screenshotRef, path: webp } };
86
+ });
87
+ screenshots.splice(0, screenshots.length, ...screenshots.map((rel) => replaced.get(rel) ?? rel));
88
+ for (const job of queued)
89
+ job.abandon();
90
+ return [...replaced.keys()];
91
+ },
92
+ };
93
+ }
@@ -118,6 +118,9 @@ export function participantSessionOptions(spec, deps, ready, provider, writeScre
118
118
  ...(spec.planned.limits.maxOutputTokens === undefined
119
119
  ? {}
120
120
  : { maxOutputTokens: spec.planned.limits.maxOutputTokens }),
121
+ ...(spec.planned.limits.serviceTier === undefined
122
+ ? {}
123
+ : { serviceTier: spec.planned.limits.serviceTier }),
121
124
  },
122
125
  ...(maxUsd === undefined
123
126
  ? {}
@@ -152,6 +152,9 @@ function planEntry(spec) {
152
152
  ...(spec.planned.limits.maxOutputTokens === undefined
153
153
  ? {}
154
154
  : { maxOutputTokens: spec.planned.limits.maxOutputTokens }),
155
+ ...(spec.planned.limits.serviceTier === undefined
156
+ ? {}
157
+ : { serviceTier: spec.planned.limits.serviceTier }),
155
158
  ...(spec.planned.targetUrl === undefined
156
159
  ? {}
157
160
  : { targetDigest: digestUrl(spec.planned.targetUrl) }),
@@ -179,6 +182,7 @@ export function formatParticipantPlanEntry(entry) {
179
182
  entry.surface ? `surface=${entry.surface}` : undefined,
180
183
  entry.caseGroup ? `case=${entry.caseGroup}` : undefined,
181
184
  entry.reasoningEffort ? `effort=${entry.reasoningEffort}` : undefined,
185
+ entry.serviceTier ? `tier=${entry.serviceTier}` : undefined,
182
186
  entry.startAfterMs === undefined ? undefined : `start=+${formatDuration(entry.startAfterMs)}`,
183
187
  entry.endAfterMs === undefined ? undefined : `end=+${formatDuration(entry.endAfterMs)}`,
184
188
  entry.maxSessionMs === undefined ? undefined : `cap=${formatDuration(entry.maxSessionMs)}`,
@@ -13,7 +13,7 @@ import { brainOf, callerBrainOf, planCaps, desktopRequirements, planBase, provis
13
13
  import { computerUseParticipants, declaredTargets, } from "../../study/plan-participants.js";
14
14
  import { planStarts, windowDeadlineRefusal } from "../../study/arrivals.js";
15
15
  import { computerUseParticipantCount, participantCountReason } from "../../study/routing.js";
16
- import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, networkCaptureValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
16
+ import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, networkCaptureValidationReason, outputTokenLimitValidationReason, serviceTierValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
17
17
  import { desktopCliProductReason } from "../../study/composition-rules.js";
18
18
  import { isLocalBrowserStudy } from "../../substrates/local/runtime-config.js";
19
19
  import { boundedConcurrency, defaultSessionTimeoutMs, resolveParticipantSandboxMs, } from "./participant-runs.js";
@@ -89,12 +89,16 @@ function unsupportedDeclarationReason(config, hasRunSession, driving, { inProces
89
89
  const reason = desktopMediaValidationReason(config) ||
90
90
  networkCaptureValidationReason(config, "computer-use") ||
91
91
  outputTokenLimitValidationReason(config) ||
92
+ serviceTierValidationReason(config) ||
92
93
  waitLimitValidationReason(config);
93
94
  if (reason)
94
95
  return invalid(reason);
95
96
  if (config.actor?.maxOutputTokens !== undefined &&
96
97
  (hasRunSession || driving.createProvider || driving.inProcess))
97
98
  return invalid("maxOutputTokens cannot be enforced by a custom runSession/provider/executor route.");
99
+ if (config.actor?.serviceTier !== undefined &&
100
+ (hasRunSession || driving.createProvider || driving.inProcess))
101
+ return invalid("serviceTier is not sent by a custom runSession/provider/executor route.");
98
102
  const receivingReason = receivingEmailValidationReason(config);
99
103
  if (receivingReason)
100
104
  return invalid(receivingReason);
@@ -1,3 +1,4 @@
1
+ import { removeContainedOutputFile } from "../../run/contained-output.js";
1
2
  import { adapterScoreFailureMessage, applyBrowserScorer } from "../../study/adapter-extension.js";
2
3
  import { redactText } from "../../evidence/redaction.js";
3
4
  import { foldScorerFailures, judgeExecution, judgeParticipantRecords, OUTCOME_POLICIES, participantHarnessFailed, resultOk, } from "../../run/judge.js";
@@ -260,6 +261,11 @@ export async function finishCuaRun(setup, finish, ran) {
260
261
  execution,
261
262
  policy,
262
263
  });
264
+ // The final bundle names each replaced frame's WebP, so its PNG goes before the Observer
265
+ // verifies the run directory.
266
+ for (const outcome of outcomes ?? [])
267
+ for (const png of outcome.replacedFrames ?? [])
268
+ await removeContainedOutputFile(finished.paths, png);
263
269
  const observer = await finished.renderObserver();
264
270
  // A caller's renderer runs with this process's file access, so after one that reports success
265
271
  // the run directory is checked again before the result points anyone at it. A failed render is
@@ -112,6 +112,8 @@ export interface CuaParticipantPlanEntry {
112
112
  * you read before spending money, so a declared per-participant difference has to be visible there. */
113
113
  reasoningEffort?: string;
114
114
  maxOutputTokens?: number;
115
+ /** The declared OpenAI service tier, which bills at its own rate. */
116
+ serviceTier?: string;
115
117
  /** Present only when a participant overrides subject.appUrl; digest avoids leaking preview hosts in plan logs. */
116
118
  targetDigest?: string;
117
119
  /** The participant's declared start, in ms after the run starts its participants. */
@@ -432,6 +434,8 @@ export interface SandboxReleaseFact {
432
434
  /** One participant's end-to-end run outcome (internal; projected into CuaParticipantResult + the bundle). */
433
435
  export interface ParticipantRunOutcome {
434
436
  spec: DesktopParticipantRun;
437
+ /** The PNG frames its WebP replaced, which the run removes once its final bundle is written. */
438
+ replacedFrames?: string[];
435
439
  session?: CuaLoopResult;
436
440
  /** The harness failed before the session reached a terminal status. */
437
441
  sessionError?: string;
@@ -12,7 +12,7 @@ import { DESKTOP_FILES_ROUTE_REASON } from "../../study/desktop-files.js";
12
12
  import { participantList } from "../../study/study-fields.js";
13
13
  import { PUBLIC_TARGET_OWNER_PATTERN } from "../../study/parse/subject.js";
14
14
  import { REPO_SLUG_PATTERN } from "../../study/parse/values.js";
15
- import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, networkCaptureValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
15
+ import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, networkCaptureValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, serviceTierValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "../../study/validation.js";
16
16
  import { unpricedCapCheck } from "../../study/requirements.js";
17
17
  import { participantsAtOnce, sandboxDeadlineRefusal, sandboxHeadroomMs, } from "../../substrates/e2b/lifetime.js";
18
18
  import { boundedConcurrency } from "../computer-use/participant-runs.js";
@@ -62,6 +62,7 @@ export function planSharedWorldStudy(config, input) {
62
62
  // A library caller's config skips the parser, which refuses these first.
63
63
  studyUrlCredentialReason(config) ??
64
64
  outputTokenLimitValidationReason(config) ??
65
+ serviceTierValidationReason(config) ??
65
66
  waitLimitValidationReason(config) ??
66
67
  (externalPublic
67
68
  ? externalPublicSharedWorldValidationReason(config)
@@ -70,6 +71,8 @@ export function planSharedWorldStudy(config, input) {
70
71
  return refuse(invalid, invalidReason, actor);
71
72
  if (config.actor?.maxOutputTokens !== undefined && input.hasRunSession === true)
72
73
  return refuse(invalid, "maxOutputTokens cannot be enforced by a custom runSession.", actor);
74
+ if (config.actor?.serviceTier !== undefined && input.hasRunSession === true)
75
+ return refuse(invalid, "serviceTier is not sent by a custom runSession.", actor);
73
76
  // A live cap is priced here, at plan time, so its refusal wins over the route's key checks.
74
77
  const unpriced = input.dryRun
75
78
  ? undefined
@@ -30,6 +30,11 @@ export interface ModelRate {
30
30
  until?: string;
31
31
  /** The rates the sheet publishes from the day after `until`. */
32
32
  after?: Pick<ModelRate, "inputUsdPerToken" | "outputUsdPerToken" | "cachedInputUsdPerToken">;
33
+ /** Fast mode rates (OpenAI `service_tier: "fast"`, served as `fast` or `priority`), when the
34
+ * sheet publishes them. The long-context multipliers apply on top. */
35
+ fast?: Pick<ModelRate, "inputUsdPerToken" | "outputUsdPerToken" | "cachedInputUsdPerToken" | "cacheWriteUsdPerToken"> & {
36
+ source?: string;
37
+ };
33
38
  /** The `YYYY-MM-DD` date the entry was last checked against `source`. */
34
39
  asOf: string;
35
40
  /** Public pricing page the number came from (a comment/URL, never a secret). */
@@ -101,6 +106,8 @@ export interface ActorEstimatedCost {
101
106
  /** How many requests crossed the long-context threshold and were re-tiered. Present only
102
107
  * when per-request `turns` records made exact tiering possible. */
103
108
  longContextTurns?: number;
109
+ /** How many requests the provider served in Fast mode and were priced at its rates. */
110
+ fastTurns?: number;
104
111
  };
105
112
  }
106
113
  /** The desktop-minute cost estimate (host-side create->teardown span * a per-minute rate). Same