@oharness/engine 0.10.0 → 0.11.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/dist/acp/claude.d.ts +34 -0
- package/dist/acp/claude.js +122 -0
- package/dist/acp/claude.js.map +1 -0
- package/dist/acp/client.d.ts +193 -0
- package/dist/acp/client.js +279 -0
- package/dist/acp/client.js.map +1 -0
- package/dist/acp/runner.d.ts +80 -0
- package/dist/acp/runner.js +159 -0
- package/dist/acp/runner.js.map +1 -0
- package/dist/agent/agent.d.ts +51 -1
- package/dist/agent/agent.js +322 -1
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/hooks.js +6 -6
- package/dist/agent/hooks.js.map +1 -1
- package/dist/agent/session.d.ts +2 -0
- package/dist/agent/session.js +19 -2
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/subagent.d.ts +9 -0
- package/dist/agent/subagent.js +6 -0
- package/dist/agent/subagent.js.map +1 -1
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.js +3 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/manager.js +4 -0
- package/dist/auth/manager.js.map +1 -1
- package/dist/auth/oauth.d.ts +33 -0
- package/dist/auth/oauth.js +59 -8
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/providers/anthropic.d.ts +25 -5
- package/dist/auth/providers/anthropic.js +47 -6
- package/dist/auth/providers/anthropic.js.map +1 -1
- package/dist/auth/providers/chatgpt.d.ts +8 -0
- package/dist/auth/providers/chatgpt.js +180 -0
- package/dist/auth/providers/chatgpt.js.map +1 -0
- package/dist/auth/providers/openrouter.d.ts +2 -0
- package/dist/auth/providers/openrouter.js +101 -0
- package/dist/auth/providers/openrouter.js.map +1 -0
- package/dist/auth/types.d.ts +15 -0
- package/dist/auth/types.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/plugin.d.ts +6 -1
- package/dist/browser/plugin.js.map +1 -1
- package/dist/browser/session.d.ts +47 -0
- package/dist/browser/session.js +177 -2
- package/dist/browser/session.js.map +1 -1
- package/dist/browser/tools.js +139 -18
- package/dist/browser/tools.js.map +1 -1
- package/dist/computer/cua-driver.d.ts +60 -1
- package/dist/computer/cua-driver.js +149 -5
- package/dist/computer/cua-driver.js.map +1 -1
- package/dist/computer/helper-client.d.ts +3 -1
- package/dist/computer/helper-client.js +3 -0
- package/dist/computer/helper-client.js.map +1 -1
- package/dist/computer/index.d.ts +3 -2
- package/dist/computer/index.js +1 -1
- package/dist/computer/index.js.map +1 -1
- package/dist/computer/mode.d.ts +15 -0
- package/dist/computer/mode.js +11 -1
- package/dist/computer/mode.js.map +1 -1
- package/dist/computer/session.d.ts +36 -7
- package/dist/computer/session.js +118 -17
- package/dist/computer/session.js.map +1 -1
- package/dist/computer/target.d.ts +19 -0
- package/dist/computer/target.js.map +1 -1
- package/dist/computer/tools.js +135 -8
- package/dist/computer/tools.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js.map +1 -1
- package/dist/harness.d.ts +52 -0
- package/dist/harness.js +113 -12
- package/dist/harness.js.map +1 -1
- package/dist/mcp/manager.d.ts +15 -0
- package/dist/mcp/manager.js +28 -1
- package/dist/mcp/manager.js.map +1 -1
- package/dist/providers/catalog.json +14 -3
- package/dist/providers/models-dev.d.ts +15 -0
- package/dist/providers/models-dev.js +14 -0
- package/dist/providers/models-dev.js.map +1 -1
- package/dist/providers/piai.d.ts +38 -1
- package/dist/providers/piai.js +70 -2
- package/dist/providers/piai.js.map +1 -1
- package/dist/providers/registry.d.ts +16 -1
- package/dist/providers/registry.js +42 -11
- package/dist/providers/registry.js.map +1 -1
- package/dist/runtime-plugins/builtin/studio.d.ts +7 -0
- package/dist/runtime-plugins/builtin/studio.js +69 -4
- package/dist/runtime-plugins/builtin/studio.js.map +1 -1
- package/dist/tools/builtin/bash.js +6 -10
- package/dist/tools/builtin/bash.js.map +1 -1
- package/dist/util/shell.d.ts +30 -0
- package/dist/util/shell.js +70 -0
- package/dist/util/shell.js.map +1 -1
- package/package.json +3 -3
- package/skills/media-generation/SKILL.md +22 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createPkce, listenForCallback } from "../oauth.js";
|
|
3
|
+
import { parseJson } from "../../util/json.js";
|
|
4
|
+
/**
|
|
5
|
+
* OpenRouter, with a sign-in that ends in an ordinary key.
|
|
6
|
+
*
|
|
7
|
+
* OpenRouter's OAuth is a public PKCE flow built for apps like this one: no
|
|
8
|
+
* client id to register, and what the code is exchanged for is not an
|
|
9
|
+
* expiring token but a permanent API key the user owns, can see and can
|
|
10
|
+
* revoke on openrouter.ai. So the sign-in is only a way of not copying a key
|
|
11
|
+
* by hand — the result is stored and sent exactly as a pasted key would be,
|
|
12
|
+
* and spends the user's own OpenRouter credits.
|
|
13
|
+
*/
|
|
14
|
+
const AUTHORIZE_URL = "https://openrouter.ai/auth";
|
|
15
|
+
const KEY_URL = "https://openrouter.ai/api/v1/auth/keys";
|
|
16
|
+
const ENV_VAR = "OPENROUTER_API_KEY";
|
|
17
|
+
async function exchangeForKey(code, verifier) {
|
|
18
|
+
const response = await fetch(KEY_URL, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: "S256" }),
|
|
22
|
+
});
|
|
23
|
+
const text = await response.text();
|
|
24
|
+
const parsed = parseJson(text);
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const error = parsed.ok ? parsed.value.error : undefined;
|
|
27
|
+
const detail = typeof error === "string" ? error : (error?.message ?? text.slice(0, 200));
|
|
28
|
+
throw new Error(`OpenRouter refused the sign-in (${response.status}): ${detail}`);
|
|
29
|
+
}
|
|
30
|
+
const key = parsed.ok ? parsed.value.key : undefined;
|
|
31
|
+
if (typeof key !== "string" || !key)
|
|
32
|
+
throw new Error("OpenRouter returned no key");
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
async function signIn() {
|
|
36
|
+
const pkce = createPkce();
|
|
37
|
+
// A path nobody can guess stands in for the state OpenRouter does not echo
|
|
38
|
+
// back; the PKCE verifier is what actually ties the code to this login.
|
|
39
|
+
const path = `/callback/${randomUUID()}`;
|
|
40
|
+
const { server, port, waitForCallback, deliver } = await listenForCallback({ path, state: null });
|
|
41
|
+
const url = new URL(AUTHORIZE_URL);
|
|
42
|
+
url.search = new URLSearchParams({
|
|
43
|
+
callback_url: `http://127.0.0.1:${port}${path}`,
|
|
44
|
+
code_challenge: pkce.challenge,
|
|
45
|
+
code_challenge_method: "S256",
|
|
46
|
+
}).toString();
|
|
47
|
+
return {
|
|
48
|
+
method: "auto",
|
|
49
|
+
url: url.toString(),
|
|
50
|
+
instructions: "Sign in to OpenRouter in your browser; this continues on its own.",
|
|
51
|
+
paste: deliver,
|
|
52
|
+
async callback() {
|
|
53
|
+
try {
|
|
54
|
+
const params = await waitForCallback;
|
|
55
|
+
const key = await exchangeForKey(params.get("code"), pkce.verifier);
|
|
56
|
+
return { type: "success", key, metadata: { source: "oauth" } };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return { type: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
server.close();
|
|
63
|
+
server.closeAllConnections();
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function bearer(key, source) {
|
|
69
|
+
return { headers: { authorization: `Bearer ${key}` }, source };
|
|
70
|
+
}
|
|
71
|
+
export const openrouterAuth = {
|
|
72
|
+
id: "openrouter",
|
|
73
|
+
label: "OpenRouter",
|
|
74
|
+
methods: [
|
|
75
|
+
{
|
|
76
|
+
type: "oauth",
|
|
77
|
+
label: "Sign in with OpenRouter",
|
|
78
|
+
browser: true,
|
|
79
|
+
authorize: signIn,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "api",
|
|
83
|
+
label: "API key",
|
|
84
|
+
prompts: [{ type: "text", key: "key", message: "OpenRouter API key", placeholder: "sk-or-…", secret: true }],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
async loader(credential) {
|
|
88
|
+
const envKey = process.env[ENV_VAR];
|
|
89
|
+
if (envKey)
|
|
90
|
+
return bearer(envKey, `env:${ENV_VAR}`);
|
|
91
|
+
if (credential?.type === "api") {
|
|
92
|
+
return bearer(credential.key, credential.metadata?.["source"] === "oauth" ? "stored:oauth" : "stored:api");
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
},
|
|
96
|
+
async discover() {
|
|
97
|
+
const envKey = process.env[ENV_VAR];
|
|
98
|
+
return envKey ? { credential: { type: "api", key: envKey }, source: `env:${ENV_VAR}` } : undefined;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=openrouter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openrouter.js","sourceRoot":"","sources":["../../../src/auth/providers/openrouter.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -79,6 +79,14 @@ export type AuthorizeResult = {
|
|
|
79
79
|
*/
|
|
80
80
|
method: "auto";
|
|
81
81
|
callback(): Promise<CallbackResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Finish the flow from the address the browser ended on, pasted back
|
|
84
|
+
* by hand — for when the browser runs on another machine (an SSH
|
|
85
|
+
* session) and its redirect to this one's loopback never arrives.
|
|
86
|
+
* Returns why the paste was not accepted, or nothing when it was, in
|
|
87
|
+
* which case `callback()` resolves as if the redirect had come in.
|
|
88
|
+
*/
|
|
89
|
+
paste?(input: string): string | undefined;
|
|
82
90
|
} | {
|
|
83
91
|
/** The user copies a code out of the browser and pastes it back. */
|
|
84
92
|
method: "code";
|
|
@@ -95,6 +103,13 @@ export type CallbackResult = ({
|
|
|
95
103
|
export interface OAuthMethod {
|
|
96
104
|
type: "oauth";
|
|
97
105
|
label: string;
|
|
106
|
+
/**
|
|
107
|
+
* A vendor's own browser sign-in that asks nothing up front and finishes on
|
|
108
|
+
* a loopback redirect — Sign in with ChatGPT, OpenRouter. Marked so a UI
|
|
109
|
+
* can tell it from the OHarness account sign-ins, which ask for an email
|
|
110
|
+
* and go through the website, and offer it as the one button it is.
|
|
111
|
+
*/
|
|
112
|
+
browser?: boolean;
|
|
98
113
|
prompts?: Prompt[];
|
|
99
114
|
authorize(inputs: Record<string, string>): Promise<AuthorizeResult>;
|
|
100
115
|
}
|
package/dist/auth/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmMH,MAAM,UAAU,SAAS,CAAC,UAA2B,EAAE,MAAM,GAAG,MAAM;IACpE,OAAO,UAAU,CAAC,OAAO,GAAG,CAAC,IAAI,UAAU,CAAC,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;AAC7E,CAAC"}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { BrowserSession, originOf } from "./session.ts";
|
|
2
|
-
export type { BrowserSessionOptions, ConsoleEntry, NetworkEntry } from "./session.ts";
|
|
2
|
+
export type { BrowserSessionOptions, BrowserTurnShot, ConsoleEntry, NetworkEntry } from "./session.ts";
|
|
3
3
|
export { createBrowserTools } from "./tools.ts";
|
|
4
4
|
export { findChrome, launchChrome } from "./chrome.ts";
|
|
5
5
|
export type { LaunchChromeOptions } from "./chrome.ts";
|
package/dist/browser/plugin.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Logger } from "../util/logger.ts";
|
|
2
2
|
import type { Tool } from "../tools/types.ts";
|
|
3
|
-
import type { BrowserSessionOptions } from "./session.ts";
|
|
3
|
+
import type { BrowserSessionOptions, BrowserTurnShot } from "./session.ts";
|
|
4
4
|
import type { BrowserProviderPlugin } from "./provider.ts";
|
|
5
5
|
import type { ExternalSite, ImportedSite } from "./login-import.ts";
|
|
6
6
|
import type { BrowserInputEvent, FrameOptions } from "./target.ts";
|
|
@@ -57,6 +57,11 @@ export interface BrowserUseSession {
|
|
|
57
57
|
running: boolean;
|
|
58
58
|
}>;
|
|
59
59
|
close(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* The turn ended: pictures of the tabs it left that the agent did not look
|
|
62
|
+
* at, or none when `capture` is false. Optional for third-party feature plugins.
|
|
63
|
+
*/
|
|
64
|
+
endTurn?(capture: boolean): Promise<BrowserTurnShot[]>;
|
|
60
65
|
/** Name the conversation this browser works for; see BrowserTargetContext.label. */
|
|
61
66
|
setLabel?(label: () => string | undefined): void;
|
|
62
67
|
/** Move to the other browser; the next call opens it. Optional for third-party feature plugins. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/browser/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/browser/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAgFhD,MAAM,UAAU,sBAAsB,CAAC,OAAuC,EAAE;IAC9E,OAAO;QACL,EAAE,EAAE,aAAa;QACjB,aAAa,CAAC,OAAO;YACnB,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC;gBACjC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACxD,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrD,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClD,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC7C,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACvD,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,kBAAkB,CAAC,OAAyB,CAAC;KAClE,CAAC;AACJ,CAAC"}
|
|
@@ -4,6 +4,13 @@ import { LoginImportMemory } from "./login-import.ts";
|
|
|
4
4
|
import type { ExternalSite, ImportedSite } from "./login-import.ts";
|
|
5
5
|
import type { Snapshot, SnapshotOptions } from "./snapshot.ts";
|
|
6
6
|
import type { BrowserInputEvent, BrowserPage, BrowserTargetFactory, FrameOptions } from "./target.ts";
|
|
7
|
+
/** One tab as a turn left it, for the person reading the turn back. */
|
|
8
|
+
export interface BrowserTurnShot {
|
|
9
|
+
mimeType: string;
|
|
10
|
+
data: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
}
|
|
7
14
|
export interface ConsoleEntry {
|
|
8
15
|
level: string;
|
|
9
16
|
text: string;
|
|
@@ -80,6 +87,17 @@ export declare class BrowserSession {
|
|
|
80
87
|
private importing;
|
|
81
88
|
private probing?;
|
|
82
89
|
private readonly suggestions;
|
|
90
|
+
/**
|
|
91
|
+
* Tabs the tools acted on this turn whose last state the agent has not
|
|
92
|
+
* looked at in a screenshot, oldest first. A tab whose last step was a
|
|
93
|
+
* screenshot is already pictured in the transcript.
|
|
94
|
+
*/
|
|
95
|
+
private readonly unseen;
|
|
96
|
+
/**
|
|
97
|
+
* Tabs wearing the working glow this turn, with the browser they are in and
|
|
98
|
+
* the script that puts the glow back after a navigation.
|
|
99
|
+
*/
|
|
100
|
+
private readonly glowing;
|
|
83
101
|
constructor(opts?: BrowserSessionOptions);
|
|
84
102
|
/** Name the conversation this browser works for. Read lazily, so it may change. */
|
|
85
103
|
setLabel(label: () => string | undefined): void;
|
|
@@ -148,6 +166,13 @@ export declare class BrowserSession {
|
|
|
148
166
|
x: number;
|
|
149
167
|
y: number;
|
|
150
168
|
}>;
|
|
169
|
+
/** The border box of a ref, in viewport CSS pixels, after scrolling it into view. */
|
|
170
|
+
boxOf(ref: string): Promise<{
|
|
171
|
+
x: number;
|
|
172
|
+
y: number;
|
|
173
|
+
width: number;
|
|
174
|
+
height: number;
|
|
175
|
+
}>;
|
|
151
176
|
resolveObject(ref: string): Promise<string>;
|
|
152
177
|
/** Every open tab. Starts the browser if nothing has yet. */
|
|
153
178
|
tabs(): Promise<BrowserPage[]>;
|
|
@@ -241,6 +266,28 @@ export declare class BrowserSession {
|
|
|
241
266
|
forward(event: BrowserInputEvent): Promise<void>;
|
|
242
267
|
consoleEntries(): readonly ConsoleEntry[];
|
|
243
268
|
networkEntries(): readonly NetworkEntry[];
|
|
269
|
+
/**
|
|
270
|
+
* Edge the tab in use with the working glow until the turn ends, so a person
|
|
271
|
+
* glancing at their browser can tell the agent is in it. Not the in-app
|
|
272
|
+
* browser: the pane around it already says so. Never seen by the model —
|
|
273
|
+
* screenshots are taken without it, and it is hidden from the
|
|
274
|
+
* accessibility tree.
|
|
275
|
+
*/
|
|
276
|
+
glow(): Promise<void>;
|
|
277
|
+
/** Run `fn` with the glow out of the picture on `page`. */
|
|
278
|
+
withoutGlow<T>(page: BrowserPage, fn: () => Promise<T>): Promise<T>;
|
|
279
|
+
/** Take the glow off, from every tab or only those in one browser. */
|
|
280
|
+
private unglow;
|
|
281
|
+
/** A tool ran against the tab in use; `sawPage` when its last step was a screenshot. */
|
|
282
|
+
noteTool(sawPage: boolean): void;
|
|
283
|
+
/**
|
|
284
|
+
* The turn ended: a picture of each tab it left unseen, for the transcript.
|
|
285
|
+
*
|
|
286
|
+
* The agent works from the accessibility tree and rarely looks, so without
|
|
287
|
+
* this a browser turn is often read back with no picture of the page it
|
|
288
|
+
* left. `capture` false only forgets the turn's tabs.
|
|
289
|
+
*/
|
|
290
|
+
endTurn(capture: boolean): Promise<BrowserTurnShot[]>;
|
|
244
291
|
/** Let go of both browsers: the built-in one closes, the person's is detached. */
|
|
245
292
|
close(): Promise<void>;
|
|
246
293
|
/** Let go of one browser, leaving the other as it is. */
|
package/dist/browser/session.js
CHANGED
|
@@ -20,6 +20,14 @@ import { LoginImportMemory, externalBrowserName, hostInSite, listExternalSites,
|
|
|
20
20
|
*/
|
|
21
21
|
const CONSOLE_LIMIT = 200;
|
|
22
22
|
const NETWORK_LIMIT = 200;
|
|
23
|
+
/**
|
|
24
|
+
* At most this many tabs get a picture when a turn ends: a turn that went
|
|
25
|
+
* through ten pages is read back by its last few, and each is a capture.
|
|
26
|
+
* The page shows one row of them and opens the rest from it.
|
|
27
|
+
*/
|
|
28
|
+
const TURN_SHOT_MAX_TABS = 6;
|
|
29
|
+
/** A tab that does not answer a capture in this long is left out, not waited on. */
|
|
30
|
+
const TURN_SHOT_TIMEOUT_MS = 5_000;
|
|
23
31
|
function emptySide() {
|
|
24
32
|
return { refs: new Map(), wired: new WeakSet() };
|
|
25
33
|
}
|
|
@@ -65,6 +73,17 @@ export class BrowserSession {
|
|
|
65
73
|
importing = false;
|
|
66
74
|
probing;
|
|
67
75
|
suggestions = new Map();
|
|
76
|
+
/**
|
|
77
|
+
* Tabs the tools acted on this turn whose last state the agent has not
|
|
78
|
+
* looked at in a screenshot, oldest first. A tab whose last step was a
|
|
79
|
+
* screenshot is already pictured in the transcript.
|
|
80
|
+
*/
|
|
81
|
+
unseen = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* Tabs wearing the working glow this turn, with the browser they are in and
|
|
84
|
+
* the script that puts the glow back after a navigation.
|
|
85
|
+
*/
|
|
86
|
+
glowing = new Map();
|
|
68
87
|
constructor(opts = {}) {
|
|
69
88
|
this.opts = opts;
|
|
70
89
|
this.policy = opts.mode === "extension" || opts.mode === "auto" ? opts.mode : "builtin";
|
|
@@ -375,6 +394,25 @@ export class BrowserSession {
|
|
|
375
394
|
y: (quad[1] + quad[3] + quad[5] + quad[7]) / 4,
|
|
376
395
|
};
|
|
377
396
|
}
|
|
397
|
+
/** The border box of a ref, in viewport CSS pixels, after scrolling it into view. */
|
|
398
|
+
async boxOf(ref) {
|
|
399
|
+
const page = await this.page();
|
|
400
|
+
const backendNodeId = this.refs.get(ref);
|
|
401
|
+
if (backendNodeId === undefined) {
|
|
402
|
+
throw new BrowserError(`no such element ${ref}. Call browser_read_page to get current refs — they are replaced on every snapshot.`);
|
|
403
|
+
}
|
|
404
|
+
await page.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => { });
|
|
405
|
+
const box = await page.send("DOM.getBoxModel", { backendNodeId }).catch(() => undefined);
|
|
406
|
+
const model = (box?.["model"] ?? {});
|
|
407
|
+
const quad = Array.isArray(model["border"]) ? model["border"] : Array.isArray(model["content"]) ? model["content"] : undefined;
|
|
408
|
+
if (!quad || quad.length < 8)
|
|
409
|
+
throw new BrowserError(`${ref} is not visible on the page`);
|
|
410
|
+
const xs = [quad[0], quad[2], quad[4], quad[6]];
|
|
411
|
+
const ys = [quad[1], quad[3], quad[5], quad[7]];
|
|
412
|
+
const x = Math.min(...xs);
|
|
413
|
+
const y = Math.min(...ys);
|
|
414
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
415
|
+
}
|
|
378
416
|
async resolveObject(ref) {
|
|
379
417
|
const page = await this.page();
|
|
380
418
|
const backendNodeId = this.refs.get(ref);
|
|
@@ -500,10 +538,14 @@ export class BrowserSession {
|
|
|
500
538
|
throw new BrowserError(`no tab ${id}`);
|
|
501
539
|
const wasActive = page === this.active;
|
|
502
540
|
const released = Boolean(target.opened && target.release && !target.opened(id));
|
|
503
|
-
if (released)
|
|
541
|
+
if (released) {
|
|
542
|
+
await this.unglow(undefined, page);
|
|
504
543
|
await target.release(page);
|
|
505
|
-
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
this.glowing.delete(page);
|
|
506
547
|
await page.close();
|
|
548
|
+
}
|
|
507
549
|
if (wasActive)
|
|
508
550
|
this.active = undefined;
|
|
509
551
|
// What is left to work in: in the person's own browser only this
|
|
@@ -595,8 +637,81 @@ export class BrowserSession {
|
|
|
595
637
|
networkEntries() {
|
|
596
638
|
return this.network;
|
|
597
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* Edge the tab in use with the working glow until the turn ends, so a person
|
|
642
|
+
* glancing at their browser can tell the agent is in it. Not the in-app
|
|
643
|
+
* browser: the pane around it already says so. Never seen by the model —
|
|
644
|
+
* screenshots are taken without it, and it is hidden from the
|
|
645
|
+
* accessibility tree.
|
|
646
|
+
*/
|
|
647
|
+
async glow() {
|
|
648
|
+
const page = this.active;
|
|
649
|
+
if (!page || !this.target || this.target.kind === "electron" || this.glowing.has(page))
|
|
650
|
+
return;
|
|
651
|
+
const entry = { side: this.mode };
|
|
652
|
+
this.glowing.set(page, entry);
|
|
653
|
+
const added = await page.send("Page.addScriptToEvaluateOnNewDocument", { source: GLOW_ON }).catch(() => undefined);
|
|
654
|
+
if (typeof added?.["identifier"] === "string")
|
|
655
|
+
entry.script = added["identifier"];
|
|
656
|
+
await page.send("Runtime.evaluate", { expression: GLOW_ON }).catch(() => { });
|
|
657
|
+
}
|
|
658
|
+
/** Run `fn` with the glow out of the picture on `page`. */
|
|
659
|
+
async withoutGlow(page, fn) {
|
|
660
|
+
if (!this.glowing.has(page))
|
|
661
|
+
return fn();
|
|
662
|
+
await page.send("Runtime.evaluate", { expression: glowVisibility("hidden") }).catch(() => { });
|
|
663
|
+
try {
|
|
664
|
+
return await fn();
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
await page.send("Runtime.evaluate", { expression: glowVisibility("visible") }).catch(() => { });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/** Take the glow off, from every tab or only those in one browser. */
|
|
671
|
+
async unglow(side, only) {
|
|
672
|
+
const pages = [...this.glowing].filter(([page, entry]) => (!side || entry.side === side) && (!only || page === only));
|
|
673
|
+
await Promise.all(pages.map(async ([page, entry]) => {
|
|
674
|
+
this.glowing.delete(page);
|
|
675
|
+
if (entry.script)
|
|
676
|
+
await page.send("Page.removeScriptToEvaluateOnNewDocument", { identifier: entry.script }).catch(() => { });
|
|
677
|
+
await page.send("Runtime.evaluate", { expression: GLOW_OFF }).catch(() => { });
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
/** A tool ran against the tab in use; `sawPage` when its last step was a screenshot. */
|
|
681
|
+
noteTool(sawPage) {
|
|
682
|
+
const page = this.active;
|
|
683
|
+
if (!page)
|
|
684
|
+
return;
|
|
685
|
+
this.unseen.delete(page.id);
|
|
686
|
+
if (!sawPage)
|
|
687
|
+
this.unseen.set(page.id, page);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* The turn ended: a picture of each tab it left unseen, for the transcript.
|
|
691
|
+
*
|
|
692
|
+
* The agent works from the accessibility tree and rarely looks, so without
|
|
693
|
+
* this a browser turn is often read back with no picture of the page it
|
|
694
|
+
* left. `capture` false only forgets the turn's tabs.
|
|
695
|
+
*/
|
|
696
|
+
async endTurn(capture) {
|
|
697
|
+
const pages = [...this.unseen.values()].slice(-TURN_SHOT_MAX_TABS);
|
|
698
|
+
this.unseen.clear();
|
|
699
|
+
// Off before the pictures: the transcript shows the page, not the glow.
|
|
700
|
+
await this.unglow();
|
|
701
|
+
if (!capture)
|
|
702
|
+
return [];
|
|
703
|
+
const shots = [];
|
|
704
|
+
for (const page of pages) {
|
|
705
|
+
const shot = await withTimeout(capturePage(page), TURN_SHOT_TIMEOUT_MS).catch(() => undefined);
|
|
706
|
+
if (shot)
|
|
707
|
+
shots.push(shot);
|
|
708
|
+
}
|
|
709
|
+
return shots;
|
|
710
|
+
}
|
|
598
711
|
/** Let go of both browsers: the built-in one closes, the person's is detached. */
|
|
599
712
|
async close() {
|
|
713
|
+
this.unseen.clear();
|
|
714
|
+
await this.unglow();
|
|
600
715
|
this.consoleLog.length = 0;
|
|
601
716
|
this.network.length = 0;
|
|
602
717
|
await Promise.all([this.closeSide("builtin"), this.closeSide("extension")]);
|
|
@@ -605,6 +720,8 @@ export class BrowserSession {
|
|
|
605
720
|
async closeSide(side) {
|
|
606
721
|
const target = this.sides[side].target;
|
|
607
722
|
this.sides[side] = emptySide();
|
|
723
|
+
// The person's tabs outlive the connection; the glow must not.
|
|
724
|
+
await this.unglow(side);
|
|
608
725
|
await target?.close().catch(() => { });
|
|
609
726
|
}
|
|
610
727
|
/**
|
|
@@ -834,4 +951,62 @@ export function originOf(url) {
|
|
|
834
951
|
function errorText(err) {
|
|
835
952
|
return err instanceof Error ? err.message : String(err);
|
|
836
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* The working glow, put into a page from outside it.
|
|
956
|
+
*
|
|
957
|
+
* Built for pages that are not ours: a closed shadow root so the page's CSS
|
|
958
|
+
* cannot reach it, styles through the CSSOM and constructed sheets so a
|
|
959
|
+
* Content-Security-Policy or Trusted Types does not refuse them, top frame
|
|
960
|
+
* only, and nothing a click or a screen reader can land on.
|
|
961
|
+
*/
|
|
962
|
+
const GLOW_TAG = "oharness-glow";
|
|
963
|
+
const GLOW_CSS = ".g,.g::after{position:fixed;inset:0;pointer-events:none}" +
|
|
964
|
+
".g{box-shadow:inset 0 0 0 2px rgba(198,244,50,.9),inset 0 0 16px 2px rgba(198,244,50,.5);animation:in .5s ease}" +
|
|
965
|
+
".g::after{content:'';box-shadow:inset 0 0 60px 12px rgba(198,244,50,.3),inset 0 0 140px 24px rgba(107,92,255,.12);animation:breathe 2.6s ease-in-out infinite}" +
|
|
966
|
+
"@keyframes in{from{opacity:0}}@keyframes breathe{0%,100%{opacity:.55}50%{opacity:1}}" +
|
|
967
|
+
"@media (prefers-reduced-motion:reduce){.g,.g::after{animation:none}}";
|
|
968
|
+
const GLOW_ON = `(() => {
|
|
969
|
+
if (window.top !== window) return;
|
|
970
|
+
const put = () => {
|
|
971
|
+
const root = document.documentElement;
|
|
972
|
+
if (!root || root.querySelector(":scope > ${GLOW_TAG}")) return;
|
|
973
|
+
const host = document.createElement("${GLOW_TAG}");
|
|
974
|
+
host.setAttribute("aria-hidden", "true");
|
|
975
|
+
for (const [key, value] of [["all", "initial"], ["display", "block"], ["position", "fixed"], ["inset", "0"], ["pointer-events", "none"], ["z-index", "2147483647"]]) host.style.setProperty(key, value, "important");
|
|
976
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
977
|
+
const sheet = new CSSStyleSheet();
|
|
978
|
+
sheet.replaceSync(${JSON.stringify(GLOW_CSS)});
|
|
979
|
+
shadow.adoptedStyleSheets = [sheet];
|
|
980
|
+
const ring = document.createElement("div");
|
|
981
|
+
ring.className = "g";
|
|
982
|
+
shadow.appendChild(ring);
|
|
983
|
+
root.appendChild(host);
|
|
984
|
+
};
|
|
985
|
+
if (document.documentElement) put(); else document.addEventListener("readystatechange", put, { once: true });
|
|
986
|
+
})()`;
|
|
987
|
+
const GLOW_OFF = `document.querySelector(${JSON.stringify(GLOW_TAG)})?.remove()`;
|
|
988
|
+
function glowVisibility(value) {
|
|
989
|
+
return `document.querySelector(${JSON.stringify(GLOW_TAG)})?.style.setProperty("visibility", ${JSON.stringify(value)}, "important")`;
|
|
990
|
+
}
|
|
991
|
+
/** A tab as it is now, with what it is called. Undefined for a tab with nothing to show. */
|
|
992
|
+
async function capturePage(page) {
|
|
993
|
+
const result = await page.send("Page.captureScreenshot", { format: "jpeg", quality: 70 });
|
|
994
|
+
const data = result["data"];
|
|
995
|
+
if (typeof data !== "string" || !data)
|
|
996
|
+
return undefined;
|
|
997
|
+
const about = await page
|
|
998
|
+
.send("Runtime.evaluate", { expression: "({ title: document.title, url: location.href })", returnByValue: true })
|
|
999
|
+
.then((r) => (r["result"] ?? {})["value"])
|
|
1000
|
+
.catch(() => undefined);
|
|
1001
|
+
const title = typeof about?.title === "string" ? about.title.trim() : "";
|
|
1002
|
+
const url = typeof about?.url === "string" ? about.url : "";
|
|
1003
|
+
return { mimeType: "image/jpeg", data, ...(title ? { title } : {}), ...(url ? { url } : {}) };
|
|
1004
|
+
}
|
|
1005
|
+
function withTimeout(promise, ms) {
|
|
1006
|
+
let timer;
|
|
1007
|
+
return Promise.race([
|
|
1008
|
+
promise,
|
|
1009
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("timed out")), ms); }),
|
|
1010
|
+
]).finally(() => clearTimeout(timer));
|
|
1011
|
+
}
|
|
837
1012
|
//# sourceMappingURL=session.js.map
|