humanish 0.111.0 → 0.111.1
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 +16 -0
- package/CONTEXT.md +1 -1
- package/dist/actors/codex/restricted-participant-policy.d.ts +2 -0
- package/dist/actors/codex/restricted-participant-policy.js +30 -2
- package/dist/actors/codex/restricted-participant.js +1 -1
- package/dist/actors/computer-use/executor-diagnostic.d.ts +15 -0
- package/dist/actors/computer-use/executor-diagnostic.js +105 -0
- package/dist/actors/computer-use/executor-error.d.ts +18 -3
- package/dist/actors/computer-use/executor-error.js +30 -4
- package/dist/actors/computer-use/loop/actions.d.ts +6 -3
- package/dist/actors/computer-use/loop/actions.js +27 -7
- package/dist/actors/computer-use/loop/ending.js +9 -1
- package/dist/actors/computer-use/loop/types.d.ts +14 -0
- package/dist/actors/computer-use/loop.d.ts +1 -1
- package/dist/actors/computer-use/loop.js +1 -1
- package/dist/actors/computer-use/openai-context.d.ts +8 -2
- package/dist/actors/computer-use/openai-context.js +91 -26
- package/dist/actors/computer-use/openai-provider.js +55 -12
- package/dist/actors/computer-use/openai-requests.d.ts +2 -1
- package/dist/actors/computer-use/openai-requests.js +4 -1
- package/dist/actors/contract.d.ts +7 -1
- package/dist/browser-control/client.js +5 -5
- package/dist/browser-control/protocol.d.ts +30 -1
- package/dist/browser-control/protocol.js +19 -2
- package/dist/browser-control/transport.d.ts +3 -2
- package/dist/browser-control/transport.js +12 -7
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-shared-world.js +2 -2
- package/dist/cli/commands/study.js +1 -2
- package/dist/cli/doctor.js +11 -0
- package/dist/evidence/encoded-text.d.ts +3 -3
- package/dist/evidence/encoded-text.js +10 -5
- package/dist/evidence/redaction.d.ts +2 -2
- package/dist/evidence/redaction.js +18 -7
- package/dist/guest/browser-tools.js +5 -1
- package/dist/guest/chromium-text.js +42 -18
- package/dist/guest/desktop-executor.js +5 -1
- package/dist/keys/key-resolution.d.ts +3 -0
- package/dist/keys/key-resolution.js +15 -2
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -0
- package/dist/routes/computer-use/bundle-parts.js +7 -2
- package/dist/routes/computer-use/fanout-bundle.js +4 -2
- package/dist/routes/computer-use/local-vm.js +1 -2
- package/dist/routes/computer-use/participant-model.js +6 -1
- package/dist/routes/computer-use/plan.js +3 -4
- package/dist/routes/computer-use/route.js +1 -2
- package/dist/routes/computer-use/setup.js +2 -2
- package/dist/routes/preview.js +1 -0
- package/dist/routes/scripted/plan.js +4 -5
- package/dist/routes/scripted/route.js +2 -2
- package/dist/routes/shared-world/plan.js +4 -5
- package/dist/routes/shared-world/route.js +2 -2
- package/dist/routes/shared-world/setup.js +1 -0
- package/dist/routes/terminal/dry-run.js +1 -0
- package/dist/routes/terminal/live-finish.js +24 -5
- package/dist/routes/terminal/live-sandbox.js +5 -1
- package/dist/routes/terminal/plan.js +3 -4
- package/dist/routes/terminal/recorder.d.ts +6 -0
- package/dist/routes/terminal/recorder.js +61 -6
- package/dist/routes/terminal/route.js +1 -2
- package/dist/routes/terminal/session.js +2 -1
- package/dist/routes/terminal/token-usage.d.ts +3 -0
- package/dist/routes/terminal/token-usage.js +5 -0
- package/dist/routes/terminal/types.d.ts +2 -1
- package/dist/routes/terminal/types.js +2 -1
- package/dist/run/dry-run.js +1 -0
- package/dist/run/results.d.ts +2 -0
- package/dist/run/run.d.ts +2 -0
- package/dist/run/run.js +16 -2
- package/dist/run/terminal-contract.d.ts +10 -0
- package/dist/run/terminal-contract.js +27 -9
- package/dist/study/composition-rules.js +17 -18
- package/dist/study/doctor.js +4 -5
- package/dist/study/migrate/v2.d.ts +5 -0
- package/dist/study/migrate/v2.js +23 -0
- package/dist/study/parse/front.js +2 -21
- package/dist/study/parse/url-credentials.d.ts +3 -3
- package/dist/study/parse/url-credentials.js +155 -13
- package/dist/study/persona-resolve.js +2 -2
- package/dist/study/plan-base.js +3 -4
- package/dist/study/plan-participants.js +3 -3
- package/dist/study/plan-types.d.ts +2 -0
- package/dist/study/plan.js +11 -5
- package/dist/study/routing.js +11 -11
- package/dist/study/run-study-options.js +2 -2
- package/dist/study/study-fields.d.ts +1 -13
- package/dist/study/study-fields.js +0 -24
- package/dist/study/summary.js +7 -7
- package/dist/study/types.d.ts +1 -0
- package/dist/study/validation.js +11 -11
- package/dist/study/warnings.d.ts +8 -0
- package/dist/study/warnings.js +22 -7
- package/dist/substrates/local/runtime-config.js +6 -6
- package/docs/architecture/browser-control.md +12 -2
- package/docs/architecture/guest-desktop.md +7 -1
- package/docs/architecture/terminal-product-route.md +3 -2
- package/docs/contracts/schemas.md +16 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,22 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.111.1: E2B URLs in study URLs, fan-out dry runs verify, zero-data-retention cache holds (2026-10-06)
|
|
12
|
+
|
|
13
|
+
humanish 0.111.1 fixes a study URL refusal and two verify problems that an adopter met on 0.111.0,
|
|
14
|
+
and the input cost of long computer-use sessions on zero-data-retention OpenAI organizations. A
|
|
15
|
+
study URL may carry an E2B app URL in a query parameter or fragment, which 0.110.2 through 0.111.0
|
|
16
|
+
refused as a credential. A computer-use dry run with more than one participant against an E2B app
|
|
17
|
+
URL verifies `share_ready`, and verify finds a hex-encoded secret or E2B URL after `=`. On a
|
|
18
|
+
zero-data-retention organization, a participant past about 64,000 tokens of carried conversation
|
|
19
|
+
keeps the provider's prompt cache, and an OpenAI 404 for a stored item switches it to carrying its
|
|
20
|
+
own conversation where 0.111.0 stopped it. A Codex participant's wait over 30 seconds is shortened
|
|
21
|
+
to 30 seconds, a terminal session past 512 KiB of output keeps its verdict, and `humanish doctor`
|
|
22
|
+
in a terminal study's sandbox reports the egress placeholder as no key. `execution.egressAllow` on
|
|
23
|
+
a route other than `terminal` now warns, and 0.112.0 refuses it there.
|
|
24
|
+
|
|
25
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.111.1)
|
|
26
|
+
|
|
11
27
|
## 0.111.0: StudyConfig has the keys of a v3 study file (2026-10-05)
|
|
12
28
|
|
|
13
29
|
humanish 0.111.0 is the first breaking release under the compatibility policy. It breaks library
|
package/CONTEXT.md
CHANGED
|
@@ -33,7 +33,7 @@ bundle and output fields keep older spellings; the second table maps them.
|
|
|
33
33
|
| participant desktop | The desktop one computer-use or shared-world participant drives: a substrate plus the route's concerns, such as subject provisioning, comms and the participant's plan. It lives in the route, so `src/substrates/` holds only provider primitives ([ADR 0004](docs/decisions/0004-participant-desktops-stay-in-the-route.md)). | `ParticipantDesktop` in `src/routes/computer-use/participant-desktop.ts`; `createE2BParticipantDesktop` in `src/routes/computer-use/e2b-desktop/desktop.ts`; the local VM's in `src/routes/computer-use/local-vm.ts` |
|
|
34
34
|
| receipt | A line in a run's `sandbox-receipts.ndjson` that records a sandbox's id, provider, the participant or subject it belongs to (`laneId`), and its time limit as soon as the sandbox is created, before any work runs in it. `humanish reclaim` kills sandboxes by these ids after an interrupted run. | `SandboxReceipt` in `src/run/sandbox-receipts.ts`; `reclaimRunSandboxes` in `src/run/reclaim.ts` |
|
|
35
35
|
| owner tags | The E2B metadata every sandbox humanish creates carries: `tool: humanish`, the run's `runId`, and `runKey`, a digest of the run directory's inode and creation time. The run journals them in an owner line before its first create. `humanish reclaim` lists sandboxes that match all three, which finds one whose id never reached a receipt. | `sandboxOwnerTags` in `src/run/sandbox-creates.ts`; `findE2BSandboxesByTags` in `src/substrates/e2b/sandbox.ts` |
|
|
36
|
-
| topology | Whether participants share one instance of the subject. `route: shared-world` gives one for all; every other route gives each participant its own (`per-lane-worlds`). `run.json` records it as `sharedWorld.topology`. | `
|
|
36
|
+
| topology | Whether participants share one instance of the subject. `route: shared-world` gives one for all; every other route gives each participant its own (`per-lane-worlds`). `run.json` records it as `sharedWorld.topology`. | `route` in `StudyConfig` (`src/study/types.ts`); `isSharedWorldComposition` in `src/study/routing.ts` |
|
|
37
37
|
| plane | The one instance of the subject that a shared-world study's participants use together. A `provisioned` plane is a `clone` or `local-tree` subject that humanish serves in a sandbox, with a declared subject state. An `external-public` plane is a deployment already running at `subject.appUrl`, whose owner the study names in `subject.publicTarget.owner`. Each plane has at least two participants. | `SharedWorldPlane` in `src/study/plan-types.ts`; `src/routes/shared-world/` |
|
|
38
38
|
| stream | One live or recorded view of a participant, such as a browser or a terminal, in `run.json` and the Observer. | `RunStream` in `src/run/streams.ts` |
|
|
39
39
|
| Observer | The single-file review page built from `observer/observer-data.json` (`humanish.observer-data.v1`). | `observer/` (the page), `src/observer/` (data, render, serve) |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { browserControlActionSchema, browserOnlyControlActionSchema, validateBrowserControlAction, } from "../../browser-control/protocol.js";
|
|
2
|
+
import { BROWSER_CONTROL_LIMITS, browserControlActionSchema, browserOnlyControlActionSchema, validateBrowserControlAction, } from "../../browser-control/protocol.js";
|
|
3
3
|
import { validClosingReport } from "../computer-use/loop.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,6 +21,8 @@ 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,
|
|
24
26
|
requestMs: 180_000,
|
|
25
27
|
cleanupMs: 5000,
|
|
26
28
|
});
|
|
@@ -49,15 +51,41 @@ export const PARTICIPANT_FINAL_SCHEMA = {
|
|
|
49
51
|
},
|
|
50
52
|
},
|
|
51
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Shorten each wait longer than one browser-control request may wait. A failed tool call ends the
|
|
56
|
+
* native run, so a wait that asks too much would otherwise end the whole session.
|
|
57
|
+
*/
|
|
58
|
+
function shortenWaits(value) {
|
|
59
|
+
const actions = value?.actions;
|
|
60
|
+
if (typeof value !== "object" || !Array.isArray(actions))
|
|
61
|
+
return { value, shortened: [] };
|
|
62
|
+
const shortened = [];
|
|
63
|
+
const limit = PARTICIPANT_LIMITS.waitMs;
|
|
64
|
+
const next = actions.map((action, index) => {
|
|
65
|
+
const wait = action;
|
|
66
|
+
if (wait?.kind !== "wait" ||
|
|
67
|
+
typeof wait.ms !== "number" ||
|
|
68
|
+
!Number.isFinite(wait.ms) ||
|
|
69
|
+
wait.ms <= limit)
|
|
70
|
+
return action;
|
|
71
|
+
shortened.push({ index, requestedMs: wait.ms, ms: limit });
|
|
72
|
+
return { ...wait, ms: limit };
|
|
73
|
+
});
|
|
74
|
+
return shortened.length === 0
|
|
75
|
+
? { value, shortened }
|
|
76
|
+
: { value: { ...value, actions: next }, shortened };
|
|
77
|
+
}
|
|
52
78
|
/** humanish tool arguments, validated before the shared executor sees a batch. */
|
|
53
79
|
export function parseParticipantTool(value, speechEnabled = false) {
|
|
54
80
|
if (Buffer.byteLength(JSON.stringify(value) ?? "") > PARTICIPANT_LIMITS.output)
|
|
55
81
|
throw new Error("invalid_response");
|
|
56
|
-
const
|
|
82
|
+
const { value: admitted, shortened } = shortenWaits(value);
|
|
83
|
+
const v = toolInput(speechEnabled).parse(admitted);
|
|
57
84
|
if (Buffer.byteLength(v.narration) > PARTICIPANT_LIMITS.narration)
|
|
58
85
|
throw new Error("invalid_response");
|
|
59
86
|
return {
|
|
60
87
|
actions: v.actions.map(validateBrowserControlAction),
|
|
88
|
+
...(shortened.length === 0 ? {} : { shortenedWaits: shortened }),
|
|
61
89
|
pendingSafetyChecks: [],
|
|
62
90
|
done: false,
|
|
63
91
|
...(v.narration ? { message: v.narration } : {}),
|
|
@@ -39,7 +39,7 @@ function deferred() {
|
|
|
39
39
|
return { promise, resolve, reject };
|
|
40
40
|
}
|
|
41
41
|
// prose-check: model prompt (Codex reads this tool description, not a person)
|
|
42
|
-
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.${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.`;
|
|
42
|
+
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 ${L.waitMs} ms; a longer wait is shortened to ${L.waitMs} 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.`;
|
|
43
43
|
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 and frictionReports. Report observed confusion and recovered mistakes as well as blockers. Do not invent observations.`;
|
|
44
44
|
/** Turns and failures from the native run, handed in order to the next provider request. */
|
|
45
45
|
class ParticipantEvents {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The driver steps a diagnostic can name. */
|
|
2
|
+
export declare const CUA_DIAGNOSTIC_STEPS: readonly ["owner_window_check", "cdp_session", "frame_tree", "isolated_world", "focus_probe", "insert_text", "cdp_detach", "text_port", "channel", "channel_write"];
|
|
3
|
+
export type CuaDiagnosticStep = (typeof CUA_DIAGNOSTIC_STEPS)[number];
|
|
4
|
+
/** The kinds of failure a diagnostic can name. */
|
|
5
|
+
export declare const CUA_DIAGNOSTIC_CATEGORIES: readonly ["target_closed", "detached", "navigation", "timeout", "protocol_error", "channel_closed", "unknown"];
|
|
6
|
+
export type CuaDiagnosticCategory = (typeof CUA_DIAGNOSTIC_CATEGORIES)[number];
|
|
7
|
+
export interface CuaExecutorDiagnostic {
|
|
8
|
+
readonly step: CuaDiagnosticStep;
|
|
9
|
+
readonly category: CuaDiagnosticCategory;
|
|
10
|
+
}
|
|
11
|
+
/** The diagnostic for `error`, thrown during `step`: the step and a category, never its text. */
|
|
12
|
+
export declare function classifyExecutorFailure(step: CuaDiagnosticStep, error: unknown): CuaExecutorDiagnostic;
|
|
13
|
+
export declare function isCuaExecutorDiagnostic(value: unknown): value is CuaExecutorDiagnostic;
|
|
14
|
+
/** The fixed sentence for a trace or a failure record, built only from the two words. */
|
|
15
|
+
export declare function describeExecutorDiagnostic(diagnostic: CuaExecutorDiagnostic): string;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The diagnostic an executor attaches to a failure its driver did not declare: which step failed
|
|
2
|
+
// and what kind of failure it was, as two fixed words. It carries no error text. A browser error
|
|
3
|
+
// message can hold a form value, page text, a cookie pair or a file path, and redaction on the
|
|
4
|
+
// host cannot be relied on to catch all of them, so the message never leaves the classifier.
|
|
5
|
+
/** The driver steps a diagnostic can name. */
|
|
6
|
+
export const CUA_DIAGNOSTIC_STEPS = Object.freeze([
|
|
7
|
+
"owner_window_check",
|
|
8
|
+
"cdp_session",
|
|
9
|
+
"frame_tree",
|
|
10
|
+
"isolated_world",
|
|
11
|
+
"focus_probe",
|
|
12
|
+
"insert_text",
|
|
13
|
+
"cdp_detach",
|
|
14
|
+
"text_port",
|
|
15
|
+
"channel",
|
|
16
|
+
"channel_write",
|
|
17
|
+
]);
|
|
18
|
+
/** The kinds of failure a diagnostic can name. */
|
|
19
|
+
export const CUA_DIAGNOSTIC_CATEGORIES = Object.freeze([
|
|
20
|
+
"target_closed",
|
|
21
|
+
"detached",
|
|
22
|
+
"navigation",
|
|
23
|
+
"timeout",
|
|
24
|
+
"protocol_error",
|
|
25
|
+
"channel_closed",
|
|
26
|
+
"unknown",
|
|
27
|
+
]);
|
|
28
|
+
const STEP_LABELS = Object.freeze({
|
|
29
|
+
owner_window_check: "checking the owner window",
|
|
30
|
+
cdp_session: "opening a DevTools session (newCDPSession)",
|
|
31
|
+
frame_tree: "reading the frame tree (Page.getFrameTree)",
|
|
32
|
+
isolated_world: "creating the isolated world (Page.createIsolatedWorld)",
|
|
33
|
+
focus_probe: "probing focus (Runtime.evaluate)",
|
|
34
|
+
insert_text: "inserting text (Input.insertText)",
|
|
35
|
+
cdp_detach: "detaching the DevTools session (CDPSession.detach)",
|
|
36
|
+
text_port: "running a text port step",
|
|
37
|
+
channel: "reading the browser-control channel",
|
|
38
|
+
channel_write: "writing to the browser-control channel",
|
|
39
|
+
});
|
|
40
|
+
const CATEGORY_SENTENCES = Object.freeze({
|
|
41
|
+
target_closed: "the page, its browser context or the browser closed",
|
|
42
|
+
detached: "the DevTools session or the frame detached",
|
|
43
|
+
navigation: "the page navigated or its document was replaced",
|
|
44
|
+
timeout: "the call timed out",
|
|
45
|
+
protocol_error: "the browser refused a DevTools protocol call",
|
|
46
|
+
channel_closed: "the channel to the guest closed",
|
|
47
|
+
unknown: "the error matched no known kind",
|
|
48
|
+
});
|
|
49
|
+
/** Error classes that name their category on their own. */
|
|
50
|
+
const ERROR_CLASSES = Object.freeze({
|
|
51
|
+
TargetClosedError: "target_closed",
|
|
52
|
+
TimeoutError: "timeout",
|
|
53
|
+
ProtocolError: "protocol_error",
|
|
54
|
+
});
|
|
55
|
+
/** Node system error codes a broken channel or socket reports. */
|
|
56
|
+
const SYSTEM_CODES = Object.freeze({
|
|
57
|
+
EPIPE: "channel_closed",
|
|
58
|
+
ECONNRESET: "channel_closed",
|
|
59
|
+
ERR_STREAM_DESTROYED: "channel_closed",
|
|
60
|
+
ERR_STREAM_PREMATURE_CLOSE: "channel_closed",
|
|
61
|
+
ERR_STREAM_WRITE_AFTER_END: "channel_closed",
|
|
62
|
+
ETIMEDOUT: "timeout",
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Known Playwright and Chromium message shapes, checked in order; the first match names the
|
|
66
|
+
* category. The message is only tested here, never copied.
|
|
67
|
+
*/
|
|
68
|
+
const MESSAGE_SHAPES = Object.freeze([
|
|
69
|
+
[/detached|No session with given id/i, "detached"],
|
|
70
|
+
[
|
|
71
|
+
/\b(?:target|page|context|browser|session)\b[^.]*\b(?:closed|crashed|disconnected)\b/i,
|
|
72
|
+
"target_closed",
|
|
73
|
+
],
|
|
74
|
+
[/navigat|Execution context was destroyed|Cannot find context with specified id/i, "navigation"],
|
|
75
|
+
[/timed? ?out|timeout/i, "timeout"],
|
|
76
|
+
[/\bProtocol error\b/i, "protocol_error"],
|
|
77
|
+
]);
|
|
78
|
+
function categoryOf(error) {
|
|
79
|
+
if (!(error instanceof Error))
|
|
80
|
+
return "unknown";
|
|
81
|
+
if (Object.hasOwn(ERROR_CLASSES, error.name))
|
|
82
|
+
return ERROR_CLASSES[error.name];
|
|
83
|
+
const code = error.code;
|
|
84
|
+
if (typeof code === "string" && Object.hasOwn(SYSTEM_CODES, code))
|
|
85
|
+
return SYSTEM_CODES[code];
|
|
86
|
+
const message = typeof error.message === "string" ? error.message : "";
|
|
87
|
+
return MESSAGE_SHAPES.find(([shape]) => shape.test(message))?.[1] ?? "unknown";
|
|
88
|
+
}
|
|
89
|
+
/** The diagnostic for `error`, thrown during `step`: the step and a category, never its text. */
|
|
90
|
+
export function classifyExecutorFailure(step, error) {
|
|
91
|
+
return { step, category: categoryOf(error) };
|
|
92
|
+
}
|
|
93
|
+
export function isCuaExecutorDiagnostic(value) {
|
|
94
|
+
if (typeof value !== "object" || value === null)
|
|
95
|
+
return false;
|
|
96
|
+
const { step, category } = value;
|
|
97
|
+
return (Object.keys(value).length === 2 &&
|
|
98
|
+
CUA_DIAGNOSTIC_STEPS.includes(step) &&
|
|
99
|
+
CUA_DIAGNOSTIC_CATEGORIES.includes(category));
|
|
100
|
+
}
|
|
101
|
+
/** The fixed sentence for a trace or a failure record, built only from the two words. */
|
|
102
|
+
export function describeExecutorDiagnostic(diagnostic) {
|
|
103
|
+
const { step, category } = diagnostic;
|
|
104
|
+
return `${step}, ${category}: ${CATEGORY_SENTENCES[category]} while ${STEP_LABELS[step]}`;
|
|
105
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CuaExecutorDiagnostic } from "./executor-diagnostic.js";
|
|
1
2
|
declare const messages: Readonly<{
|
|
2
3
|
executor_closed: "Desktop session is closed.";
|
|
3
4
|
executor_not_ready: "Desktop executor is not ready.";
|
|
@@ -14,17 +15,31 @@ declare const messages: Readonly<{
|
|
|
14
15
|
}>;
|
|
15
16
|
export type CuaExecutorErrorCode = keyof typeof messages;
|
|
16
17
|
export type CuaExecutorDisposition = "not_dispatched" | "outcome_uncertain";
|
|
18
|
+
/**
|
|
19
|
+
* Why an executor refused an action before dispatch, when the participant can act on it. Each is
|
|
20
|
+
* a fixed word: `extra_tab` means the local browser types only while the study's tab is its only
|
|
21
|
+
* tab, and another tab is open.
|
|
22
|
+
*/
|
|
23
|
+
export declare const CUA_REJECTION_REASONS: readonly ["extra_tab"];
|
|
24
|
+
export type CuaRejectionReason = (typeof CUA_REJECTION_REASONS)[number];
|
|
17
25
|
export declare function isCuaExecutorErrorCode(value: unknown): value is CuaExecutorErrorCode;
|
|
18
26
|
/**
|
|
19
27
|
* An executor's bounded failure declaration. No backend message, cause, action payload or
|
|
20
|
-
* transport details belong here
|
|
21
|
-
* began; a lost acknowledgement or partial
|
|
28
|
+
* transport details belong here; `diagnostic` names a failed step and a category in fixed words.
|
|
29
|
+
* `not_dispatched` requires evidence that dispatch never began; a lost acknowledgement or partial
|
|
30
|
+
* action is `outcome_uncertain`, never a retry.
|
|
31
|
+
* `reason` is allowed only on an `action_rejected` / `not_dispatched` refusal.
|
|
22
32
|
* Import from the same installation as the loop: names and lookalike objects do not qualify.
|
|
23
33
|
*/
|
|
24
34
|
export declare class ComputerUseExecutorError extends Error {
|
|
25
35
|
readonly code: CuaExecutorErrorCode;
|
|
26
36
|
readonly disposition: CuaExecutorDisposition;
|
|
27
|
-
|
|
37
|
+
readonly reason?: CuaRejectionReason;
|
|
38
|
+
readonly diagnostic?: CuaExecutorDiagnostic;
|
|
39
|
+
constructor(code: CuaExecutorErrorCode, disposition: CuaExecutorDisposition, details?: {
|
|
40
|
+
reason?: CuaRejectionReason | undefined;
|
|
41
|
+
diagnostic?: CuaExecutorDiagnostic | undefined;
|
|
42
|
+
});
|
|
28
43
|
}
|
|
29
44
|
/** A forged prototype is not an executor declaration. */
|
|
30
45
|
export declare function isComputerUseExecutorError(error: unknown): error is ComputerUseExecutorError;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCuaExecutorDiagnostic } from "./executor-diagnostic.js";
|
|
1
2
|
const messages = Object.freeze({
|
|
2
3
|
executor_closed: "Desktop session is closed.",
|
|
3
4
|
executor_not_ready: "Desktop executor is not ready.",
|
|
@@ -12,32 +13,57 @@ const messages = Object.freeze({
|
|
|
12
13
|
execution_failed: "Desktop executor could not complete the request.",
|
|
13
14
|
cancelled: "Desktop executor request was cancelled.",
|
|
14
15
|
});
|
|
16
|
+
/**
|
|
17
|
+
* Why an executor refused an action before dispatch, when the participant can act on it. Each is
|
|
18
|
+
* a fixed word: `extra_tab` means the local browser types only while the study's tab is its only
|
|
19
|
+
* tab, and another tab is open.
|
|
20
|
+
*/
|
|
21
|
+
export const CUA_REJECTION_REASONS = Object.freeze(["extra_tab"]);
|
|
22
|
+
function isCuaRejectionReason(value) {
|
|
23
|
+
return CUA_REJECTION_REASONS.includes(value);
|
|
24
|
+
}
|
|
15
25
|
export function isCuaExecutorErrorCode(value) {
|
|
16
26
|
return typeof value === "string" && Object.hasOwn(messages, value);
|
|
17
27
|
}
|
|
18
28
|
const executorErrors = new WeakSet();
|
|
19
29
|
/**
|
|
20
30
|
* An executor's bounded failure declaration. No backend message, cause, action payload or
|
|
21
|
-
* transport details belong here
|
|
22
|
-
* began; a lost acknowledgement or partial
|
|
31
|
+
* transport details belong here; `diagnostic` names a failed step and a category in fixed words.
|
|
32
|
+
* `not_dispatched` requires evidence that dispatch never began; a lost acknowledgement or partial
|
|
33
|
+
* action is `outcome_uncertain`, never a retry.
|
|
34
|
+
* `reason` is allowed only on an `action_rejected` / `not_dispatched` refusal.
|
|
23
35
|
* Import from the same installation as the loop: names and lookalike objects do not qualify.
|
|
24
36
|
*/
|
|
25
37
|
export class ComputerUseExecutorError extends Error {
|
|
26
38
|
code;
|
|
27
39
|
disposition;
|
|
28
|
-
|
|
40
|
+
reason;
|
|
41
|
+
diagnostic;
|
|
42
|
+
constructor(code, disposition, details = {}) {
|
|
43
|
+
const { reason, diagnostic } = details;
|
|
29
44
|
if (!isCuaExecutorErrorCode(code) ||
|
|
30
|
-
(disposition !== "not_dispatched" && disposition !== "outcome_uncertain")
|
|
45
|
+
(disposition !== "not_dispatched" && disposition !== "outcome_uncertain") ||
|
|
46
|
+
(reason !== undefined &&
|
|
47
|
+
(!isCuaRejectionReason(reason) ||
|
|
48
|
+
code !== "action_rejected" ||
|
|
49
|
+
disposition !== "not_dispatched")) ||
|
|
50
|
+
(diagnostic !== undefined && !isCuaExecutorDiagnostic(diagnostic))) {
|
|
31
51
|
throw new TypeError("Invalid desktop executor error declaration.");
|
|
32
52
|
}
|
|
33
53
|
super(messages[code]);
|
|
34
54
|
this.name = "ComputerUseExecutorError";
|
|
35
55
|
this.code = code;
|
|
36
56
|
this.disposition = disposition;
|
|
57
|
+
if (reason !== undefined)
|
|
58
|
+
this.reason = reason;
|
|
59
|
+
if (diagnostic !== undefined)
|
|
60
|
+
this.diagnostic = Object.freeze({ step: diagnostic.step, category: diagnostic.category });
|
|
37
61
|
// Keep the values used in durable diagnostics finite even for JavaScript callers.
|
|
38
62
|
Object.defineProperties(this, {
|
|
39
63
|
code: { writable: false, configurable: false },
|
|
40
64
|
disposition: { writable: false, configurable: false },
|
|
65
|
+
reason: { writable: false, configurable: false },
|
|
66
|
+
diagnostic: { writable: false, configurable: false },
|
|
41
67
|
});
|
|
42
68
|
executorErrors.add(this);
|
|
43
69
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LoopSession } from "./session.js";
|
|
2
|
-
import type { CuaAction, CuaTurnRequest } from "./types.js";
|
|
2
|
+
import type { CuaAction, CuaTurnRequest, ShortenedWait } 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
|
/**
|
|
@@ -15,7 +15,10 @@ export declare function isIdleTurn(actions: readonly CuaAction[]): boolean;
|
|
|
15
15
|
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 provider shortened a wait or a pre-dispatch
|
|
20
|
+
* rejection stopped the batch.
|
|
21
|
+
*/
|
|
19
22
|
readonly hint: string | undefined;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
@@ -23,4 +26,4 @@ export interface ActionBatch {
|
|
|
23
26
|
* leaves the desktop usable; a failed desktop command skips its action; any other failure ends
|
|
24
27
|
* the session.
|
|
25
28
|
*/
|
|
26
|
-
export declare function runActionBatch(session: LoopSession, actions: readonly CuaAction[]): Promise<ActionBatch>;
|
|
29
|
+
export declare function runActionBatch(session: LoopSession, actions: readonly CuaAction[], shortenedWaits?: readonly ShortenedWait[]): Promise<ActionBatch>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { classifyCuaAction } from "../../affordance.js";
|
|
2
2
|
import { commandFailureInfo, isCommandExitError } from "../../../substrates/command-failure.js";
|
|
3
|
-
import { ComputerUseExecutorError, isComputerUseExecutorError } from "../executor-error.js";
|
|
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
6
|
// The participant's actions: their public labels, what counts as a material action, and how one
|
|
@@ -101,21 +101,39 @@ export function isIdleTurn(actions) {
|
|
|
101
101
|
function isMaterialAttempt(action, status) {
|
|
102
102
|
return !isIdleAction(action) && status !== "skipped" && status !== "not_dispatched";
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
/** What the participant can do about a rejection whose reason the executor named. */
|
|
105
|
+
const REJECTION_REMEDIES = Object.freeze({
|
|
106
|
+
extra_tab: "It was rejected because more than one browser tab is open: typing works only while the study's original tab is the only tab. Switch to the other tab and close it, keep the study's original tab open, then type again.",
|
|
107
|
+
});
|
|
108
|
+
function rejectedActionHint(title, reason) {
|
|
109
|
+
const remedy = reason === undefined ? "" : ` ${REJECTION_REMEDIES[reason]}`;
|
|
110
|
+
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
|
+
function shortenedWaitHint(wait) {
|
|
113
|
+
return `Your wait of ${wait.requestedMs}ms was shortened to ${wait.ms}ms, the longest one wait action lasts. To keep waiting, send another wait.`;
|
|
114
|
+
}
|
|
115
|
+
/** Record a wait the provider shortened, and return the participant's note about it. */
|
|
116
|
+
function recordShortenedWait(session, title, wait) {
|
|
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);
|
|
106
119
|
}
|
|
107
120
|
/**
|
|
108
121
|
* Dispatch one turn's actions in order. A declared pre-dispatch rejection stops the batch and
|
|
109
122
|
* leaves the desktop usable; a failed desktop command skips its action; any other failure ends
|
|
110
123
|
* the session.
|
|
111
124
|
*/
|
|
112
|
-
export async function runActionBatch(session, actions) {
|
|
125
|
+
export async function runActionBatch(session, actions, shortenedWaits = []) {
|
|
113
126
|
const execution = { actions: [] };
|
|
114
127
|
const { actionHistory, trace } = session;
|
|
128
|
+
const notes = [];
|
|
129
|
+
const hint = () => (notes.length === 0 ? undefined : notes.join(" "));
|
|
115
130
|
for (const [index, action] of actions.entries()) {
|
|
116
131
|
if (session.signal?.aborted)
|
|
117
132
|
throw new CuaAbortError();
|
|
118
133
|
const title = describeCuaAction(action);
|
|
134
|
+
const shortened = shortenedWaits.find((wait) => wait.index === index);
|
|
135
|
+
if (shortened !== undefined)
|
|
136
|
+
notes.push(recordShortenedWait(session, title, shortened));
|
|
119
137
|
actionHistory.lastActionTitle = title;
|
|
120
138
|
actionHistory.recentActionTitles.push(title);
|
|
121
139
|
if (actionHistory.recentActionTitles.length > RECENT_ACTION_TITLES)
|
|
@@ -147,8 +165,10 @@ export async function runActionBatch(session, actions) {
|
|
|
147
165
|
for (let later = index + 1; later < actions.length; later++) {
|
|
148
166
|
execution.actions.push({ index: later, status: "not_dispatched" });
|
|
149
167
|
}
|
|
150
|
-
|
|
151
|
-
|
|
168
|
+
const { reason } = error;
|
|
169
|
+
trace.record("notice", () => notice("warn", "action rejected before dispatch", session.redactNarration(`action: ${title}; code: action_rejected; disposition: not_dispatched;${reason === undefined ? "" : ` reason: ${reason};`} remaining batch actions not dispatched: ${actions.length - index - 1}`)));
|
|
170
|
+
notes.push(rejectedActionHint(title, reason));
|
|
171
|
+
return { execution, hint: hint() };
|
|
152
172
|
}
|
|
153
173
|
if (isComputerUseExecutorError(error) || !isCommandExitError(error))
|
|
154
174
|
throw error;
|
|
@@ -171,7 +191,7 @@ export async function runActionBatch(session, actions) {
|
|
|
171
191
|
: {}),
|
|
172
192
|
}));
|
|
173
193
|
}
|
|
174
|
-
return { execution, hint:
|
|
194
|
+
return { execution, hint: hint() };
|
|
175
195
|
}
|
|
176
196
|
function countAttempt(session, action, title, status) {
|
|
177
197
|
if (!isMaterialAttempt(action, status))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeExecutorDiagnostic } from "../executor-diagnostic.js";
|
|
1
2
|
import { isComputerUseExecutorError } from "../executor-error.js";
|
|
2
3
|
import { isComputerUsePromptRefusedError, isComputerUseProviderError } from "../provider-error.js";
|
|
3
4
|
import { CuaAbortError, CuaDeadlineError } from "./race.js";
|
|
@@ -278,17 +279,24 @@ export function stopForError(session, error) {
|
|
|
278
279
|
};
|
|
279
280
|
}
|
|
280
281
|
if (isComputerUseExecutorError(error)) {
|
|
282
|
+
// The cause of a failure the driver did not declare, such as a closed browser target.
|
|
283
|
+
const cause = error.diagnostic === undefined
|
|
284
|
+
? undefined
|
|
285
|
+
: `cause: ${describeExecutorDiagnostic(error.diagnostic)}`;
|
|
281
286
|
const detail = () => [
|
|
282
287
|
`phase: ${redact(session.phase)}`,
|
|
283
288
|
`code: ${error.code}`,
|
|
284
289
|
`disposition: ${error.disposition}`,
|
|
290
|
+
cause,
|
|
285
291
|
lastAction(),
|
|
286
292
|
]
|
|
287
293
|
.filter(Boolean)
|
|
288
294
|
.join("; ");
|
|
289
295
|
return {
|
|
290
296
|
completionReason: "harness_error",
|
|
291
|
-
reason: `desktop executor error: ${error.code}
|
|
297
|
+
reason: [`desktop executor error: ${error.code}`, `disposition: ${error.disposition}`, cause]
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join("; "),
|
|
292
300
|
evidence: {
|
|
293
301
|
kind: "notice",
|
|
294
302
|
body: () => ({ ...notice("error", "desktop executor error", detail()), ...screenshot }),
|
|
@@ -55,6 +55,15 @@ 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
|
+
}
|
|
58
67
|
/** A captured desktop state: the (optional) frame plus a coarse signature for progress. */
|
|
59
68
|
export interface CuaObservation {
|
|
60
69
|
/**
|
|
@@ -162,6 +171,11 @@ export interface CuaTurn {
|
|
|
162
171
|
message?: string;
|
|
163
172
|
/** Actions to perform this turn. Empty means the model is done. */
|
|
164
173
|
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>;
|
|
165
179
|
/** Safety checks the provider flagged this turn. Non-empty pauses the run. */
|
|
166
180
|
pendingSafetyChecks: CuaSafetyCheck[];
|
|
167
181
|
/** Token accounting for this turn, if available. */
|
|
@@ -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, } from "./loop/types.js";
|
|
2
|
+
export type { CuaAction, CuaExecutor, CuaLiveMetadata, CuaLoopOptions, CuaLoopResult, CuaObservation, CuaProvider, CuaSafetyCheck, CuaSpendGate, CuaTurn, CuaTurnRequest, ShortenedWait, } from "./loop/types.js";
|
|
3
3
|
export { describeCuaAction } from "./loop/actions.js";
|
|
4
4
|
export { stableProgressKey } from "./loop/backstop.js";
|
|
5
5
|
export { validClosingReport } from "./loop/debrief.js";
|
|
@@ -121,7 +121,7 @@ async function runTurns(session, conversation) {
|
|
|
121
121
|
await observer.observeFinalTasks(turnNumber);
|
|
122
122
|
return ended;
|
|
123
123
|
}
|
|
124
|
-
const batch = await runActionBatch(session, turn.actions);
|
|
124
|
+
const batch = await runActionBatch(session, turn.actions, turn.shortenedWaits);
|
|
125
125
|
conversation.previousExecution = batch.execution;
|
|
126
126
|
const checkpoint = await observer.checkpoint(turnNumber);
|
|
127
127
|
if ("stop" in checkpoint)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The estimated input a carried conversation may reach before
|
|
1
|
+
/** The estimated input a carried conversation may reach before it is cut. */
|
|
2
2
|
export declare const CONTEXT_TOKEN_BUDGET = 64000;
|
|
3
3
|
/** What the next request would carry, for the trace. */
|
|
4
4
|
export interface CarriedContextSize {
|
|
@@ -14,7 +14,11 @@ export declare class CarriedConversation {
|
|
|
14
14
|
/** The latest accepted reply's output items, which the next request answers. */
|
|
15
15
|
private pending;
|
|
16
16
|
private readonly notes;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The turns whose note lines were dropped to keep the note under its cap. Lines are dropped in
|
|
19
|
+
* turn order from just after the head, so they are one run of consecutive turns.
|
|
20
|
+
*/
|
|
21
|
+
private elided;
|
|
18
22
|
private collapsedTurns;
|
|
19
23
|
constructor(budget?: number);
|
|
20
24
|
/**
|
|
@@ -27,6 +31,8 @@ export declare class CarriedConversation {
|
|
|
27
31
|
/** The carried context's size, for the trace. */
|
|
28
32
|
size(): CarriedContextSize;
|
|
29
33
|
private trim;
|
|
34
|
+
private capNote;
|
|
35
|
+
private noteText;
|
|
30
36
|
private noteItem;
|
|
31
37
|
}
|
|
32
38
|
/**
|