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.
- package/CHANGELOG.md +21 -0
- package/dist/actors/computer-use/loop/spend.js +5 -4
- package/dist/actors/computer-use/loop/trace.js +3 -0
- package/dist/actors/computer-use/loop/types.d.ts +5 -0
- package/dist/actors/computer-use/loop/usage.js +10 -2
- package/dist/actors/computer-use/openai-conversation.d.ts +7 -0
- package/dist/actors/computer-use/openai-conversation.js +19 -2
- package/dist/actors/computer-use/openai-provider.d.ts +2 -0
- package/dist/actors/computer-use/openai-provider.js +2 -0
- package/dist/actors/computer-use/openai-wire.d.ts +4 -0
- package/dist/actors/computer-use/openai-wire.js +9 -1
- package/dist/actors/contract.d.ts +5 -0
- package/dist/analysis/admission.d.ts +8 -3
- package/dist/analysis/admission.js +7 -3
- package/dist/analysis/automatic-config.js +2 -1
- package/dist/analysis/automatic.js +2 -2
- package/dist/analysis/evidence.js +8 -4
- package/dist/analysis/execute.d.ts +7 -10
- package/dist/analysis/execute.js +79 -49
- package/dist/analysis/image-tokens.js +9 -3
- package/dist/analysis/output-limit.d.ts +12 -0
- package/dist/analysis/output-limit.js +34 -0
- package/dist/analysis/output-retry.d.ts +20 -0
- package/dist/analysis/output-retry.js +63 -0
- package/dist/analysis/provider.d.ts +1 -1
- package/dist/analysis/provider.js +3 -3
- package/dist/analysis/responses.js +1 -1
- package/dist/analysis/service.js +2 -2
- package/dist/cli/commands/analyze.js +1 -1
- package/dist/cli/io.js +2 -0
- package/dist/cli/run-findings.js +10 -2
- package/dist/evidence/image.d.ts +2 -0
- package/dist/evidence/image.js +24 -3
- package/dist/evidence/webp-encode-worker.d.ts +1 -0
- package/dist/evidence/webp-encode-worker.js +31 -0
- package/dist/evidence/webp-transcoder.d.ts +15 -0
- package/dist/evidence/webp-transcoder.js +98 -0
- package/dist/evidence/webp.d.ts +31 -0
- package/dist/evidence/webp.js +94 -0
- package/dist/feedback/derivative-text.d.ts +7 -2
- package/dist/feedback/derivative-text.js +20 -1
- package/dist/feedback/export-bundle.js +79 -25
- package/dist/observer-app.html +1 -1
- package/dist/remote/launch.js +93 -48
- package/dist/remote/runner.d.ts +47 -14
- package/dist/remote/runner.js +199 -64
- package/dist/routes/computer-use/e2b-desktop/desktop.js +47 -7
- package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +4 -2
- package/dist/routes/computer-use/e2b-desktop/prepare.js +4 -2
- package/dist/routes/computer-use/e2b-desktop/start.d.ts +14 -6
- package/dist/routes/computer-use/e2b-desktop/start.js +72 -34
- package/dist/routes/computer-use/participant-execution.d.ts +0 -7
- package/dist/routes/computer-use/participant-execution.js +6 -28
- package/dist/routes/computer-use/participant-frames.d.ts +32 -0
- package/dist/routes/computer-use/participant-frames.js +93 -0
- package/dist/routes/computer-use/participant-model.js +3 -0
- package/dist/routes/computer-use/participant-runs.js +4 -0
- package/dist/routes/computer-use/plan.js +5 -1
- package/dist/routes/computer-use/result.js +6 -0
- package/dist/routes/computer-use/types.d.ts +4 -0
- package/dist/routes/shared-world/plan.js +4 -1
- package/dist/run/pricing.d.ts +7 -0
- package/dist/run/pricing.js +58 -15
- package/dist/study/config.js +4 -1
- package/dist/study/keys.d.ts +1 -0
- package/dist/study/keys.js +1 -0
- package/dist/study/parse/actors.js +5 -0
- package/dist/study/plan-participants.d.ts +2 -0
- package/dist/study/plan-participants.js +2 -0
- package/dist/study/types.d.ts +7 -0
- package/dist/study/validation.d.ts +5 -0
- package/dist/study/validation.js +23 -0
- package/dist/substrates/e2b/cdp-probe.d.ts +16 -1
- package/dist/substrates/e2b/cdp-probe.js +67 -5
- package/dist/substrates/e2b/desktop-browser.js +4 -2
- package/dist/substrates/e2b/desktop-cdp.d.ts +9 -2
- package/dist/substrates/e2b/desktop-cdp.js +26 -4
- package/dist/verify/artifacts.js +2 -1
- package/docs/contracts/run-bundle.md +29 -11
- package/docs/contracts/schemas.md +20 -12
- package/docs/contracts/study-analysis.md +64 -11
- package/docs/product/automatic-analysis.md +9 -6
- 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
|
|
2
|
-
// recording,
|
|
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
|
|
14
|
-
export async function
|
|
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
|
|
17
|
-
if (!
|
|
18
|
-
|
|
38
|
+
const requestedRecording = deps.residual.execution?.desktop?.recording;
|
|
39
|
+
if (!requestedRecording)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
state.recording = await startE2BDesktopRecording({
|
|
19
43
|
desktop,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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 {
|
|
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
|
|
69
|
-
const writeScreenshot =
|
|
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
|
package/dist/run/pricing.d.ts
CHANGED
|
@@ -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
|