@openacme/browser 0.4.0 → 0.5.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/dist/binaries.d.ts +63 -0
- package/dist/binaries.d.ts.map +1 -0
- package/dist/binaries.js +159 -0
- package/dist/binaries.js.map +1 -0
- package/dist/chrome.d.ts +22 -3
- package/dist/chrome.d.ts.map +1 -1
- package/dist/chrome.js +117 -16
- package/dist/chrome.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/manager.d.ts +40 -40
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +238 -253
- package/dist/manager.js.map +1 -1
- package/dist/providers/base.d.ts +44 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +2 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/browser-use.d.ts +24 -0
- package/dist/providers/browser-use.d.ts.map +1 -0
- package/dist/providers/browser-use.js +110 -0
- package/dist/providers/browser-use.js.map +1 -0
- package/dist/providers/browserbase.d.ts +29 -0
- package/dist/providers/browserbase.d.ts.map +1 -0
- package/dist/providers/browserbase.js +126 -0
- package/dist/providers/browserbase.js.map +1 -0
- package/dist/providers/firecrawl.d.ts +22 -0
- package/dist/providers/firecrawl.d.ts.map +1 -0
- package/dist/providers/firecrawl.js +101 -0
- package/dist/providers/firecrawl.js.map +1 -0
- package/dist/providers/index.d.ts +20 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +27 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/local.d.ts +35 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +132 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/types.d.ts +27 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
package/dist/manager.js
CHANGED
|
@@ -1,129 +1,173 @@
|
|
|
1
|
-
import { killChrome, launchChrome, resolveExecutableOrThrow, resolveUserDataDir, } from "./chrome.js";
|
|
2
1
|
import { connectOverCdp, isRecoverableDisconnect } from "./cdp.js";
|
|
3
2
|
import { refLocator } from "./refs.js";
|
|
4
3
|
import { ariaSnapshot } from "./snapshot.js";
|
|
5
4
|
const MAX_CONSOLE_PER_PAGE = 200;
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
6
|
+
* Per-agent browser orchestrator. Each agent that calls a browser tool
|
|
7
|
+
* gets its own session — a separate Chrome process for the local provider,
|
|
8
|
+
* a separate cloud session for Browserbase / Browser-Use / Firecrawl.
|
|
9
|
+
* Sessions are lazy and acquired on first tool call.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* space. Cross-agent tab access is refused.
|
|
13
|
-
*
|
|
14
|
-
* Lazy: nothing happens until the first tool call. CDP disconnect on
|
|
15
|
-
* sleep/wake is recovered transparently — `cachedBrowser` is invalidated
|
|
16
|
-
* by the `disconnected` listener and re-established on next call.
|
|
11
|
+
* Why per-agent: shared cookies cascade bans across the workforce, and
|
|
12
|
+
* shared fingerprints get the whole org flagged on social-media-style
|
|
13
|
+
* sites. Each agent is a distinct persona; the runtime should reflect that.
|
|
17
14
|
*/
|
|
18
15
|
export class BrowserManager {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
launchingPromise = null;
|
|
25
|
-
// Tab ownership: targetId -> owning agentId
|
|
26
|
-
tabOwnership = new Map();
|
|
27
|
-
// Per-agent alias spaces (tN ids stable within an agent)
|
|
28
|
-
agentTabs = new Map();
|
|
29
|
-
// The agent's currently-active tab
|
|
30
|
-
activeTabByAgent = new Map(); // agentId -> targetId
|
|
31
|
-
// Per-page transient state (console buffer, pending dialog policy)
|
|
32
|
-
pageState = new WeakMap();
|
|
16
|
+
provider;
|
|
17
|
+
resolveOverrides;
|
|
18
|
+
ensureOverrides;
|
|
19
|
+
instances = new Map();
|
|
20
|
+
connecting = new Map();
|
|
33
21
|
constructor(opts) {
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
22
|
+
this.provider = opts.provider;
|
|
23
|
+
this.resolveOverrides = opts.resolveOverrides ?? null;
|
|
24
|
+
this.ensureOverrides = opts.ensureOverrides ?? null;
|
|
25
|
+
}
|
|
26
|
+
get providerName() {
|
|
27
|
+
return this.provider.name;
|
|
36
28
|
}
|
|
37
29
|
// ───────────────────────── lifecycle ─────────────────────────
|
|
38
|
-
async
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
30
|
+
async getInstance(agentId) {
|
|
31
|
+
if (!agentId)
|
|
32
|
+
throw new Error("BrowserManager calls require an agentId");
|
|
33
|
+
const existing = this.instances.get(agentId);
|
|
34
|
+
if (existing && this.isInstanceAlive(existing))
|
|
35
|
+
return existing;
|
|
36
|
+
if (existing) {
|
|
37
|
+
this.instances.delete(agentId);
|
|
38
|
+
try {
|
|
39
|
+
await existing.acquired.release();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// best-effort
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const inflight = this.connecting.get(agentId);
|
|
46
|
+
if (inflight)
|
|
47
|
+
return inflight;
|
|
48
|
+
const p = this.connectFor(agentId).finally(() => this.connecting.delete(agentId));
|
|
49
|
+
this.connecting.set(agentId, p);
|
|
50
|
+
return p;
|
|
51
|
+
}
|
|
52
|
+
isInstanceAlive(inst) {
|
|
53
|
+
// CDP path: rely on the underlying Browser's connection state.
|
|
54
|
+
// Pre-built context path: by construction, the provider/manager's
|
|
55
|
+
// close listener already drops the entry.
|
|
56
|
+
if (inst.browser)
|
|
57
|
+
return inst.browser.isConnected();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
async connectFor(agentId) {
|
|
61
|
+
let overrides = this.resolveOverrides?.(agentId);
|
|
62
|
+
// Lazy provisioning: if the agent doesn't have a profile yet for the
|
|
63
|
+
// active provider, give AgentManager a chance to create one now and
|
|
64
|
+
// persist it back. First-acquire pays the provision cost; subsequent
|
|
65
|
+
// acquires skip this because the agent def now has the field.
|
|
66
|
+
if (this.ensureOverrides) {
|
|
67
|
+
try {
|
|
68
|
+
overrides = await this.ensureOverrides(agentId, overrides);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// best-effort — fall through with whatever we had
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const acquired = await this.provider.acquire(agentId, { overrides });
|
|
75
|
+
let browser = null;
|
|
76
|
+
let context;
|
|
77
|
+
try {
|
|
78
|
+
if (acquired.preBuiltContext) {
|
|
79
|
+
context = acquired.preBuiltContext;
|
|
80
|
+
}
|
|
81
|
+
else if (acquired.cdpUrl) {
|
|
82
|
+
browser = await connectOverCdp({
|
|
83
|
+
wsUrl: acquired.cdpUrl,
|
|
84
|
+
onDisconnected: (b) => {
|
|
85
|
+
const inst = this.instances.get(agentId);
|
|
86
|
+
if (inst && inst.browser === b)
|
|
87
|
+
this.instances.delete(agentId);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const ctxs = browser.contexts();
|
|
91
|
+
if (ctxs.length === 0)
|
|
92
|
+
throw new Error("CDP browser has no contexts");
|
|
93
|
+
context = ctxs[0];
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
throw new Error("Provider returned neither cdpUrl nor preBuiltContext");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
// Avoid leaking the upstream session when the initial attach fails.
|
|
101
|
+
try {
|
|
102
|
+
await acquired.release();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// best-effort
|
|
106
|
+
}
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
109
|
+
const instance = {
|
|
110
|
+
acquired,
|
|
111
|
+
browser,
|
|
112
|
+
context,
|
|
113
|
+
agentTabs: { next: 1, byTabId: new Map(), byPage: new Map() },
|
|
114
|
+
activeTabId: null,
|
|
115
|
+
pageState: new WeakMap(),
|
|
116
|
+
};
|
|
117
|
+
// For pre-built contexts, mirror what the CDP `disconnected` listener
|
|
118
|
+
// does — drop the cache when the context closes.
|
|
119
|
+
if (!browser) {
|
|
120
|
+
context.once("close", () => {
|
|
121
|
+
if (this.instances.get(agentId) === instance)
|
|
122
|
+
this.instances.delete(agentId);
|
|
58
123
|
});
|
|
59
|
-
this.running = r;
|
|
60
|
-
return r;
|
|
61
|
-
})
|
|
62
|
-
.finally(() => {
|
|
63
|
-
this.launchingPromise = null;
|
|
64
|
-
});
|
|
65
|
-
return this.launchingPromise;
|
|
66
|
-
}
|
|
67
|
-
async getBrowser() {
|
|
68
|
-
if (this.cachedBrowser)
|
|
69
|
-
return this.cachedBrowser;
|
|
70
|
-
if (this.connectingPromise)
|
|
71
|
-
return this.connectingPromise;
|
|
72
|
-
this.connectingPromise = this.connectFresh().finally(() => {
|
|
73
|
-
this.connectingPromise = null;
|
|
74
|
-
});
|
|
75
|
-
return this.connectingPromise;
|
|
76
|
-
}
|
|
77
|
-
async connectFresh() {
|
|
78
|
-
const running = await this.ensureChrome();
|
|
79
|
-
const browser = await connectOverCdp({
|
|
80
|
-
wsUrl: running.cdpUrl,
|
|
81
|
-
onDisconnected: (b) => {
|
|
82
|
-
if (this.cachedBrowser === b)
|
|
83
|
-
this.cachedBrowser = null;
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
this.cachedBrowser = browser;
|
|
87
|
-
return browser;
|
|
88
|
-
}
|
|
89
|
-
async sharedContext() {
|
|
90
|
-
const browser = await this.getBrowser();
|
|
91
|
-
const contexts = browser.contexts();
|
|
92
|
-
if (contexts.length === 0) {
|
|
93
|
-
// Should never happen with connectOverCDP — Chrome always exposes
|
|
94
|
-
// the default context — but defensive.
|
|
95
|
-
throw new Error("No BrowserContext available on the connected Chrome.");
|
|
96
124
|
}
|
|
97
|
-
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
this.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
async
|
|
106
|
-
|
|
125
|
+
this.instances.set(agentId, instance);
|
|
126
|
+
return instance;
|
|
127
|
+
}
|
|
128
|
+
async contextFor(agentId) {
|
|
129
|
+
const inst = await this.getInstance(agentId);
|
|
130
|
+
return { inst, ctx: inst.context };
|
|
131
|
+
}
|
|
132
|
+
/** Release one agent's browser session. Idempotent. */
|
|
133
|
+
async closeAgent(agentId) {
|
|
134
|
+
const inst = this.instances.get(agentId);
|
|
135
|
+
if (inst) {
|
|
136
|
+
this.instances.delete(agentId);
|
|
107
137
|
try {
|
|
108
|
-
|
|
138
|
+
if (inst.browser)
|
|
139
|
+
await inst.browser.close();
|
|
140
|
+
// Pre-built context lifetime is owned by the provider; releaseAgent
|
|
141
|
+
// below closes it.
|
|
109
142
|
}
|
|
110
143
|
catch {
|
|
111
|
-
//
|
|
144
|
+
// best-effort
|
|
112
145
|
}
|
|
113
|
-
this.cachedBrowser = null;
|
|
114
146
|
}
|
|
115
|
-
|
|
116
|
-
await
|
|
117
|
-
|
|
147
|
+
try {
|
|
148
|
+
await this.provider.releaseAgent(agentId);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// best-effort
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Release every agent and shut down the provider. */
|
|
155
|
+
async close() {
|
|
156
|
+
const ids = Array.from(this.instances.keys());
|
|
157
|
+
await Promise.all(ids.map((id) => this.closeAgent(id)));
|
|
158
|
+
try {
|
|
159
|
+
await this.provider.releaseAll();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// best-effort
|
|
118
163
|
}
|
|
119
|
-
this.resetAllTabState();
|
|
120
164
|
}
|
|
121
165
|
// ───────────────────────── per-page observation ─────────────────────────
|
|
122
|
-
observePage(page) {
|
|
123
|
-
if (
|
|
166
|
+
observePage(inst, page) {
|
|
167
|
+
if (inst.pageState.has(page))
|
|
124
168
|
return;
|
|
125
169
|
const state = { console: [], dialogHandler: null };
|
|
126
|
-
|
|
170
|
+
inst.pageState.set(page, state);
|
|
127
171
|
page.on("console", (msg) => {
|
|
128
172
|
if (state.console.length >= MAX_CONSOLE_PER_PAGE)
|
|
129
173
|
state.console.shift();
|
|
@@ -144,132 +188,91 @@ export class BrowserManager {
|
|
|
144
188
|
});
|
|
145
189
|
});
|
|
146
190
|
}
|
|
147
|
-
// ───────────────────────── tab
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const existing = s.byTargetId.get(targetId);
|
|
159
|
-
if (existing)
|
|
191
|
+
// ───────────────────────── tab tracking ─────────────────────────
|
|
192
|
+
/**
|
|
193
|
+
* Tab identity is the Page object itself — stable for the page's
|
|
194
|
+
* lifetime in Playwright. Avoids CDP-specific calls (`newCDPSession`)
|
|
195
|
+
* so Firefox-based backends (Camoufox) work the same as Chromium.
|
|
196
|
+
*/
|
|
197
|
+
trackPage(inst, page) {
|
|
198
|
+
const existing = inst.agentTabs.byPage.get(page);
|
|
199
|
+
if (existing) {
|
|
200
|
+
this.observePage(inst, page);
|
|
201
|
+
inst.activeTabId = existing;
|
|
160
202
|
return existing;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
trackPageForAgent(agentId, page, targetId) {
|
|
169
|
-
this.observePage(page);
|
|
170
|
-
const tabId = this.assignTabAlias(agentId, targetId);
|
|
171
|
-
this.activeTabByAgent.set(agentId, targetId);
|
|
203
|
+
}
|
|
204
|
+
this.observePage(inst, page);
|
|
205
|
+
const tabId = `t${inst.agentTabs.next}`;
|
|
206
|
+
inst.agentTabs.next += 1;
|
|
207
|
+
inst.agentTabs.byTabId.set(tabId, page);
|
|
208
|
+
inst.agentTabs.byPage.set(page, tabId);
|
|
209
|
+
inst.activeTabId = tabId;
|
|
172
210
|
page.once("close", () => {
|
|
173
|
-
|
|
211
|
+
inst.agentTabs.byTabId.delete(tabId);
|
|
212
|
+
inst.agentTabs.byPage.delete(page);
|
|
213
|
+
if (inst.activeTabId === tabId)
|
|
214
|
+
inst.activeTabId = null;
|
|
174
215
|
});
|
|
175
216
|
return tabId;
|
|
176
217
|
}
|
|
177
|
-
releaseTab(targetId) {
|
|
178
|
-
const owner = this.tabOwnership.get(targetId);
|
|
179
|
-
if (!owner)
|
|
180
|
-
return;
|
|
181
|
-
this.tabOwnership.delete(targetId);
|
|
182
|
-
const s = this.agentTabs.get(owner);
|
|
183
|
-
if (s) {
|
|
184
|
-
const alias = s.byTargetId.get(targetId);
|
|
185
|
-
if (alias) {
|
|
186
|
-
s.byTabId.delete(alias);
|
|
187
|
-
s.byTargetId.delete(targetId);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (this.activeTabByAgent.get(owner) === targetId) {
|
|
191
|
-
this.activeTabByAgent.delete(owner);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
async targetIdOf(page) {
|
|
195
|
-
const session = await page.context().newCDPSession(page);
|
|
196
|
-
try {
|
|
197
|
-
const info = (await session.send("Target.getTargetInfo"));
|
|
198
|
-
const id = info.targetInfo?.targetId;
|
|
199
|
-
if (!id)
|
|
200
|
-
throw new Error("Target.getTargetInfo returned no targetId");
|
|
201
|
-
return id;
|
|
202
|
-
}
|
|
203
|
-
finally {
|
|
204
|
-
await session.detach().catch(() => { });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Resolve the Page for `tabId` (or the active tab if omitted) and verify
|
|
209
|
-
* the agent owns it. Throws on missing tab / cross-agent access.
|
|
210
|
-
*/
|
|
211
218
|
async resolvePage(agentId, tabId, opts) {
|
|
212
|
-
const
|
|
213
|
-
let
|
|
219
|
+
const { inst, ctx } = await this.contextFor(agentId);
|
|
220
|
+
let page = null;
|
|
221
|
+
let resolvedTabId = null;
|
|
214
222
|
if (tabId) {
|
|
215
|
-
|
|
216
|
-
if (!
|
|
223
|
+
const candidate = inst.agentTabs.byTabId.get(tabId);
|
|
224
|
+
if (!candidate)
|
|
217
225
|
throw new Error(`Tab ${tabId} not found for this agent.`);
|
|
226
|
+
if (candidate.isClosed()) {
|
|
227
|
+
inst.agentTabs.byTabId.delete(tabId);
|
|
228
|
+
inst.agentTabs.byPage.delete(candidate);
|
|
229
|
+
if (inst.activeTabId === tabId)
|
|
230
|
+
inst.activeTabId = null;
|
|
231
|
+
throw new Error(`Tab ${tabId} is no longer open.`);
|
|
218
232
|
}
|
|
233
|
+
page = candidate;
|
|
234
|
+
resolvedTabId = tabId;
|
|
219
235
|
}
|
|
220
|
-
else {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const page = await this.findPageByTargetId(ctx, targetId);
|
|
226
|
-
if (page) {
|
|
227
|
-
this.observePage(page);
|
|
228
|
-
const resolvedTabId = s.byTargetId.get(targetId);
|
|
229
|
-
return { page, tabId: resolvedTabId, targetId };
|
|
236
|
+
else if (inst.activeTabId) {
|
|
237
|
+
const candidate = inst.agentTabs.byTabId.get(inst.activeTabId);
|
|
238
|
+
if (candidate && !candidate.isClosed()) {
|
|
239
|
+
page = candidate;
|
|
240
|
+
resolvedTabId = inst.activeTabId;
|
|
230
241
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
throw new Error("No tabs owned by this agent. Call browser_navigate first to open one.");
|
|
238
|
-
}
|
|
239
|
-
const page = await ctx.newPage();
|
|
240
|
-
const newTargetId = await this.targetIdOf(page);
|
|
241
|
-
const newTabId = this.trackPageForAgent(agentId, page, newTargetId);
|
|
242
|
-
return { page, tabId: newTabId, targetId: newTargetId };
|
|
243
|
-
}
|
|
244
|
-
async findPageByTargetId(ctx, targetId) {
|
|
245
|
-
for (const p of ctx.pages()) {
|
|
246
|
-
try {
|
|
247
|
-
const tid = await this.targetIdOf(p);
|
|
248
|
-
if (tid === targetId)
|
|
249
|
-
return p;
|
|
242
|
+
else {
|
|
243
|
+
if (candidate) {
|
|
244
|
+
inst.agentTabs.byTabId.delete(inst.activeTabId);
|
|
245
|
+
inst.agentTabs.byPage.delete(candidate);
|
|
246
|
+
}
|
|
247
|
+
inst.activeTabId = null;
|
|
250
248
|
}
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
}
|
|
250
|
+
if (!page) {
|
|
251
|
+
if (!opts.createIfNone) {
|
|
252
|
+
throw new Error("No tabs open for this agent. Call browser_navigate first to open one.");
|
|
253
253
|
}
|
|
254
|
+
page = await ctx.newPage();
|
|
255
|
+
resolvedTabId = this.trackPage(inst, page);
|
|
254
256
|
}
|
|
255
|
-
|
|
257
|
+
this.observePage(inst, page);
|
|
258
|
+
return { inst, page, tabId: resolvedTabId };
|
|
256
259
|
}
|
|
257
260
|
/**
|
|
258
261
|
* Run an action with one-shot reconnect on transient CDP disconnects.
|
|
259
|
-
*
|
|
262
|
+
* Drops the agent's instance + cloud session before retrying so we
|
|
263
|
+
* don't reuse a server-side session that the provider already killed.
|
|
260
264
|
*/
|
|
261
265
|
async withReconnect(agentId, tabId, opts, fn) {
|
|
262
266
|
try {
|
|
263
267
|
const r = await this.resolvePage(agentId, tabId, opts);
|
|
264
|
-
return await fn(r.page, { tabId: r.tabId
|
|
268
|
+
return await fn(r.page, { tabId: r.tabId });
|
|
265
269
|
}
|
|
266
270
|
catch (e) {
|
|
267
271
|
if (!isRecoverableDisconnect(e))
|
|
268
272
|
throw e;
|
|
269
|
-
|
|
270
|
-
this.cachedBrowser = null;
|
|
273
|
+
await this.closeAgent(agentId);
|
|
271
274
|
const r = await this.resolvePage(agentId, tabId, opts);
|
|
272
|
-
return await fn(r.page, { tabId: r.tabId
|
|
275
|
+
return await fn(r.page, { tabId: r.tabId });
|
|
273
276
|
}
|
|
274
277
|
}
|
|
275
278
|
// ───────────────────────── public API ─────────────────────────
|
|
@@ -324,10 +327,7 @@ export class BrowserManager {
|
|
|
324
327
|
async waitFor(agentId, p) {
|
|
325
328
|
return this.withReconnect(agentId, p.tabId, { createIfNone: false }, async (page, ids) => {
|
|
326
329
|
if (p.text) {
|
|
327
|
-
await page
|
|
328
|
-
.getByText(p.text, { exact: false })
|
|
329
|
-
.first()
|
|
330
|
-
.waitFor({ timeout: 30_000 });
|
|
330
|
+
await page.getByText(p.text, { exact: false }).first().waitFor({ timeout: 30_000 });
|
|
331
331
|
}
|
|
332
332
|
else if (p.textGone) {
|
|
333
333
|
await page
|
|
@@ -343,15 +343,14 @@ export class BrowserManager {
|
|
|
343
343
|
}
|
|
344
344
|
async evaluate(agentId, p) {
|
|
345
345
|
return this.withReconnect(agentId, p.tabId, { createIfNone: false }, async (page, ids) => {
|
|
346
|
-
// `function` is treated as an expression body — the model writes
|
|
347
|
-
// something like `document.title` or `(() => Array.from(document.images).length)()`.
|
|
348
346
|
const result = await page.evaluate(p.function);
|
|
349
347
|
return { tabId: ids.tabId, result };
|
|
350
348
|
});
|
|
351
349
|
}
|
|
352
350
|
async consoleMessages(agentId, p) {
|
|
353
351
|
return this.withReconnect(agentId, p.tabId, { createIfNone: false }, async (page, ids) => {
|
|
354
|
-
const
|
|
352
|
+
const { inst } = await this.contextFor(agentId);
|
|
353
|
+
const state = inst.pageState.get(page);
|
|
355
354
|
const messages = state ? [...state.console] : [];
|
|
356
355
|
if (p.clear && state)
|
|
357
356
|
state.console = [];
|
|
@@ -360,26 +359,16 @@ export class BrowserManager {
|
|
|
360
359
|
}
|
|
361
360
|
// ── tabs ──
|
|
362
361
|
async tabsList(agentId) {
|
|
363
|
-
const
|
|
364
|
-
const s = this.agentTabsFor(agentId);
|
|
365
|
-
const active = this.activeTabByAgent.get(agentId);
|
|
362
|
+
const { inst } = await this.contextFor(agentId);
|
|
366
363
|
const out = [];
|
|
367
|
-
for (const page of
|
|
368
|
-
|
|
369
|
-
try {
|
|
370
|
-
targetId = await this.targetIdOf(page);
|
|
371
|
-
}
|
|
372
|
-
catch {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
const tabId = s.byTargetId.get(targetId);
|
|
376
|
-
if (!tabId)
|
|
364
|
+
for (const [tabId, page] of inst.agentTabs.byTabId) {
|
|
365
|
+
if (page.isClosed())
|
|
377
366
|
continue;
|
|
378
367
|
out.push({
|
|
379
368
|
tabId,
|
|
380
369
|
url: page.url(),
|
|
381
370
|
title: await page.title().catch(() => ""),
|
|
382
|
-
active:
|
|
371
|
+
active: tabId === inst.activeTabId,
|
|
383
372
|
});
|
|
384
373
|
}
|
|
385
374
|
out.sort((a, b) => {
|
|
@@ -390,12 +379,11 @@ export class BrowserManager {
|
|
|
390
379
|
return out;
|
|
391
380
|
}
|
|
392
381
|
async tabsNew(agentId, p) {
|
|
393
|
-
const ctx = await this.
|
|
382
|
+
const { inst, ctx } = await this.contextFor(agentId);
|
|
394
383
|
const page = await ctx.newPage();
|
|
395
384
|
if (p.url)
|
|
396
385
|
await page.goto(p.url, { waitUntil: "domcontentloaded" });
|
|
397
|
-
const
|
|
398
|
-
const tabId = this.trackPageForAgent(agentId, page, targetId);
|
|
386
|
+
const tabId = this.trackPage(inst, page);
|
|
399
387
|
return {
|
|
400
388
|
tabId,
|
|
401
389
|
url: page.url(),
|
|
@@ -404,28 +392,34 @@ export class BrowserManager {
|
|
|
404
392
|
};
|
|
405
393
|
}
|
|
406
394
|
async tabsClose(agentId, p) {
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
if (!
|
|
395
|
+
const { inst } = await this.contextFor(agentId);
|
|
396
|
+
const page = inst.agentTabs.byTabId.get(p.tabId);
|
|
397
|
+
if (!page)
|
|
410
398
|
throw new Error(`Tab ${p.tabId} not found for this agent.`);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (page)
|
|
399
|
+
if (!page.isClosed()) {
|
|
400
|
+
// page.close() fires the trackPage close handler which drops the maps.
|
|
414
401
|
await page.close();
|
|
415
|
-
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
inst.agentTabs.byTabId.delete(p.tabId);
|
|
405
|
+
inst.agentTabs.byPage.delete(page);
|
|
406
|
+
if (inst.activeTabId === p.tabId)
|
|
407
|
+
inst.activeTabId = null;
|
|
408
|
+
}
|
|
416
409
|
}
|
|
417
410
|
async tabsSelect(agentId, p) {
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
if (!
|
|
411
|
+
const { inst } = await this.contextFor(agentId);
|
|
412
|
+
const page = inst.agentTabs.byTabId.get(p.tabId);
|
|
413
|
+
if (!page)
|
|
421
414
|
throw new Error(`Tab ${p.tabId} not found for this agent.`);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
415
|
+
if (page.isClosed()) {
|
|
416
|
+
inst.agentTabs.byTabId.delete(p.tabId);
|
|
417
|
+
inst.agentTabs.byPage.delete(page);
|
|
418
|
+
if (inst.activeTabId === p.tabId)
|
|
419
|
+
inst.activeTabId = null;
|
|
426
420
|
throw new Error(`Tab ${p.tabId} is no longer open.`);
|
|
427
421
|
}
|
|
428
|
-
|
|
422
|
+
inst.activeTabId = p.tabId;
|
|
429
423
|
await page.bringToFront().catch(() => { });
|
|
430
424
|
return {
|
|
431
425
|
tabId: p.tabId,
|
|
@@ -467,10 +461,6 @@ export class BrowserManager {
|
|
|
467
461
|
return { tabId: ids.tabId };
|
|
468
462
|
});
|
|
469
463
|
}
|
|
470
|
-
/**
|
|
471
|
-
* Arm a one-shot dialog handler on the page. The handler resolves the
|
|
472
|
-
* NEXT dialog event with accept/dismiss + optional prompt text.
|
|
473
|
-
*/
|
|
474
464
|
async handleDialog(agentId, p) {
|
|
475
465
|
return this.withReconnect(agentId, p.tabId, { createIfNone: false }, async (page, ids) => {
|
|
476
466
|
await new Promise((resolve, reject) => {
|
|
@@ -495,10 +485,7 @@ export class BrowserManager {
|
|
|
495
485
|
};
|
|
496
486
|
page.once("dialog", onDialog);
|
|
497
487
|
});
|
|
498
|
-
return {
|
|
499
|
-
tabId: ids.tabId,
|
|
500
|
-
result: p.accept ? "accepted" : "dismissed",
|
|
501
|
-
};
|
|
488
|
+
return { tabId: ids.tabId, result: p.accept ? "accepted" : "dismissed" };
|
|
502
489
|
});
|
|
503
490
|
}
|
|
504
491
|
async resize(agentId, p) {
|
|
@@ -522,9 +509,7 @@ export class BrowserManager {
|
|
|
522
509
|
async saveAsPdf(agentId, p) {
|
|
523
510
|
return this.withReconnect(agentId, p.tabId, { createIfNone: false }, async (page, ids) => {
|
|
524
511
|
const filename = p.filename ?? `browser-${ids.tabId}-${Date.now()}.pdf`;
|
|
525
|
-
const outPath = filename.includes("/")
|
|
526
|
-
? filename
|
|
527
|
-
: `/tmp/${filename}`;
|
|
512
|
+
const outPath = filename.includes("/") ? filename : `/tmp/${filename}`;
|
|
528
513
|
await page.pdf({ path: outPath, format: "Letter" });
|
|
529
514
|
return { tabId: ids.tabId, path: outPath };
|
|
530
515
|
});
|