@mind-harness/mh-browser 0.1.6 → 0.2.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/extension/manifest.json +1 -1
- package/lib/index.js +735 -689
- package/lib/typert.host.js +1 -1
- package/lib/typert.remote-client.js +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/presence/register.d.ts +2 -1
- package/lib/types/projections.d.ts +2 -12
- package/lib/types/service/service.d.ts +8 -4
- package/lib/types/tools/download.d.ts +2 -1
- package/lib/types/tools/handover.d.ts +2 -1
- package/lib/types/tools/invoke.d.ts +2 -1
- package/lib/types/tools/open.d.ts +2 -1
- package/lib/types/tools/page-ops.d.ts +3 -2
- package/lib/types/tools/presence.d.ts +2 -1
- package/lib/types/tools/register.d.ts +2 -1
- package/lib/types/tools/session.d.ts +3 -1
- package/lib/types/types.d.ts +8 -5
- package/lib/types/words.d.ts +7 -0
- package/package.json +27 -24
package/lib/index.js
CHANGED
|
@@ -3,12 +3,12 @@ import z from "@deepseek-ai/schemastery";
|
|
|
3
3
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import { HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
6
|
-
import {
|
|
6
|
+
import { assertSupportedJsonSchema, defineTool, validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { parse } from "yaml";
|
|
8
9
|
import { constants, readFileSync } from "node:fs";
|
|
9
10
|
import { lstat, mkdir, open, readFile, realpath, unlink, writeFile } from "node:fs/promises";
|
|
10
|
-
import {
|
|
11
|
-
import { assertSupportedJsonSchema, defineTool, validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
|
|
11
|
+
import { z as z$1 } from "zod";
|
|
12
12
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
13
13
|
import { chromium } from "playwright";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -25,493 +25,12 @@ const browserConfig = z.object({
|
|
|
25
25
|
headless: z.boolean().default(false).description("Run the browser without a window.")
|
|
26
26
|
});
|
|
27
27
|
/**
|
|
28
|
-
* Browser profile directory when the configuration names none. Each dsh profile gets its own:
|
|
29
|
-
* a browser profile directory admits one running browser, so two dsh profiles sharing one
|
|
30
|
-
* cannot both start.
|
|
31
|
-
*/
|
|
32
|
-
function defaultProfileDir(profile, home = resolveDshHome()) {
|
|
33
|
-
return profile === void 0 ? join(home, "mh", "browser") : join(home, "mh", "profiles", profile.name, "browser");
|
|
34
|
-
}
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region lib/types/presence/record.js
|
|
37
|
-
/** Structured presence record carried on a message source. The model sees the rendered text, not this. */
|
|
38
|
-
const PRESENCE_SOURCES = [
|
|
39
|
-
"person via sidebar",
|
|
40
|
-
"cwd reference",
|
|
41
|
-
"model"
|
|
42
|
-
];
|
|
43
|
-
//#endregion
|
|
44
|
-
//#region lib/types/presence/fold.js
|
|
45
|
-
function initPresenceLog() {
|
|
46
|
-
return {
|
|
47
|
-
committed: [],
|
|
48
|
-
inbox: {
|
|
49
|
-
"next-step": [],
|
|
50
|
-
"next-turn": []
|
|
51
|
-
},
|
|
52
|
-
discarded: [],
|
|
53
|
-
visible: false,
|
|
54
|
-
hidden: false,
|
|
55
|
-
shown: [],
|
|
56
|
-
ops: []
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function applyPresenceLog(state, event) {
|
|
60
|
-
if (event.type === "compaction/summary") {
|
|
61
|
-
if (!state.visible && state.shown.length === 0) return state;
|
|
62
|
-
return {
|
|
63
|
-
...state,
|
|
64
|
-
visible: false,
|
|
65
|
-
hidden: true,
|
|
66
|
-
shown: []
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
if (event.type === "user/message") {
|
|
70
|
-
const carried = carriedOf(event.data, `history:${String(event.seq)}`);
|
|
71
|
-
if (carried === void 0) return state;
|
|
72
|
-
return {
|
|
73
|
-
...state,
|
|
74
|
-
committed: applyRecord(state.committed, carried.record, "committed"),
|
|
75
|
-
inbox: {
|
|
76
|
-
"next-step": state.inbox["next-step"].map((item) => item?.id === carried.id ? null : item),
|
|
77
|
-
"next-turn": state.inbox["next-turn"].map((item) => item?.id === carried.id ? null : item)
|
|
78
|
-
},
|
|
79
|
-
discarded: [],
|
|
80
|
-
visible: true,
|
|
81
|
-
hidden: false,
|
|
82
|
-
shown: [...state.shown, textDigest(event.data)],
|
|
83
|
-
ops: []
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
if (event.type === "agent/inbox/discarded") {
|
|
87
|
-
const carried = carriedOf(event.data, `discarded:${String(event.seq)}`);
|
|
88
|
-
if (carried === void 0) return state;
|
|
89
|
-
return {
|
|
90
|
-
...state,
|
|
91
|
-
discarded: [...state.discarded, carried]
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
if (event.type === "agent/inbox/spliced") return applySplice(state, event);
|
|
95
|
-
if (event.type === "tool/result") return applyReceipts(state, event);
|
|
96
|
-
return state;
|
|
97
|
-
}
|
|
98
|
-
/** The folded set, with pending reminders and unannounced receipts on top. */
|
|
99
|
-
function presenceSet(state) {
|
|
100
|
-
let pages = state.committed;
|
|
101
|
-
for (const carried of [
|
|
102
|
-
...state.inbox["next-turn"],
|
|
103
|
-
...state.inbox["next-step"],
|
|
104
|
-
...state.discarded
|
|
105
|
-
]) if (carried !== null) pages = applyRecord(pages, carried.record, "pending");
|
|
106
|
-
const removals = [];
|
|
107
|
-
for (const op of state.ops) {
|
|
108
|
-
const index = pages.findIndex((page) => page.url === op.url);
|
|
109
|
-
if (op.op === "remove") {
|
|
110
|
-
if (index < 0) continue;
|
|
111
|
-
pages = pages.toSpliced(index, 1);
|
|
112
|
-
removals.push(op);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (index >= 0) continue;
|
|
116
|
-
pages = [...pages, {
|
|
117
|
-
url: op.url,
|
|
118
|
-
source: "model",
|
|
119
|
-
state: "pending",
|
|
120
|
-
order: pages.length
|
|
121
|
-
}];
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
pages,
|
|
125
|
-
removals
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
/** The text of a reminder, as `shown` records it. */
|
|
129
|
-
function textDigest(message) {
|
|
130
|
-
return createHash("sha256").update(textOf$3(message)).digest("hex");
|
|
131
|
-
}
|
|
132
|
-
function applySplice(state, event) {
|
|
133
|
-
const data = record$3(event.data);
|
|
134
|
-
const target = data.target === "next-turn" ? "next-turn" : "next-step";
|
|
135
|
-
const start = typeof data.start === "number" ? data.start : 0;
|
|
136
|
-
const removedCount = typeof data.removedCount === "number" ? data.removedCount : 0;
|
|
137
|
-
const slots = (Array.isArray(data.inserted) ? data.inserted : []).map((item, index) => carriedOf(item, `splice:${String(event.seq)}:${String(index)}`));
|
|
138
|
-
const list = state.inbox[target];
|
|
139
|
-
const removed = list.slice(start, start + removedCount).filter((item) => item !== null);
|
|
140
|
-
if (removedCount === 0 && slots.length === 0) return state;
|
|
141
|
-
const next = [
|
|
142
|
-
...list.slice(0, start),
|
|
143
|
-
...slots.map((slot) => slot ?? null),
|
|
144
|
-
...list.slice(start + removedCount)
|
|
145
|
-
];
|
|
146
|
-
const announced = slots.some((slot) => slot !== void 0);
|
|
147
|
-
return {
|
|
148
|
-
...state,
|
|
149
|
-
inbox: {
|
|
150
|
-
...state.inbox,
|
|
151
|
-
[target]: next
|
|
152
|
-
},
|
|
153
|
-
discarded: data.outcome === "canceled" ? [...state.discarded, ...removed] : state.discarded,
|
|
154
|
-
ops: announced ? [] : state.ops
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function applyReceipts(state, event) {
|
|
158
|
-
const data = record$3(event.data);
|
|
159
|
-
if (record$3(data.message).isError === true) return state;
|
|
160
|
-
const meta = record$3(data.meta);
|
|
161
|
-
if (meta.by !== "mh-browser" || meta.action !== "presence" || !Array.isArray(meta.receipts)) return state;
|
|
162
|
-
const ops = [];
|
|
163
|
-
for (const item of meta.receipts) {
|
|
164
|
-
const row = record$3(item);
|
|
165
|
-
if (typeof row.url !== "string" || row.url === "") continue;
|
|
166
|
-
if (row.receipt === "added (pending)") ops.push({
|
|
167
|
-
op: "put",
|
|
168
|
-
url: row.url
|
|
169
|
-
});
|
|
170
|
-
if (row.receipt === "removed (pending)") ops.push({
|
|
171
|
-
op: "remove",
|
|
172
|
-
url: row.url
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
return ops.length === 0 ? state : {
|
|
176
|
-
...state,
|
|
177
|
-
ops: [...state.ops, ...ops]
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
function applyRecord(pages, record, state) {
|
|
181
|
-
if (record.kind === "baseline") {
|
|
182
|
-
const next = [];
|
|
183
|
-
for (const entry of record.entries) {
|
|
184
|
-
if (entry.op === "removed") continue;
|
|
185
|
-
next.push(heldFrom(entry, state, next.length));
|
|
186
|
-
}
|
|
187
|
-
return next;
|
|
188
|
-
}
|
|
189
|
-
const next = pages.map((page) => ({ ...page }));
|
|
190
|
-
for (const entry of record.entries) {
|
|
191
|
-
if (entry.op === "removed") {
|
|
192
|
-
const index = next.findIndex((page) => page.url === entry.url);
|
|
193
|
-
if (index >= 0) next.splice(index, 1);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
const index = next.findIndex((page) => page.url === entry.url);
|
|
197
|
-
const held = heldFrom(entry, state, index >= 0 ? next[index]?.order ?? index : next.length);
|
|
198
|
-
if (index >= 0) next[index] = held;
|
|
199
|
-
else next.push(held);
|
|
200
|
-
}
|
|
201
|
-
return next;
|
|
202
|
-
}
|
|
203
|
-
function heldFrom(entry, state, order) {
|
|
204
|
-
const absent = entry.op === "absent";
|
|
205
|
-
return {
|
|
206
|
-
url: entry.url,
|
|
207
|
-
source: entry.source,
|
|
208
|
-
state,
|
|
209
|
-
order,
|
|
210
|
-
seen: absent ? { absent: entry.reason ?? "" } : {
|
|
211
|
-
digest: entry.digest,
|
|
212
|
-
observedAt: entry.observedAt,
|
|
213
|
-
...entry.version === void 0 ? {} : { version: entry.version },
|
|
214
|
-
...entry.id === void 0 ? {} : { id: entry.id }
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
/** A `user/message` event's data is the message itself. */
|
|
219
|
-
function carriedOf(data, fallbackId) {
|
|
220
|
-
const message = record$3(data);
|
|
221
|
-
const source = record$3(message.source);
|
|
222
|
-
if (source.kind !== "mh-browser") return void 0;
|
|
223
|
-
const presence = record$3(source.presence);
|
|
224
|
-
const kind = presence.kind;
|
|
225
|
-
if (kind !== "baseline" && kind !== "delta") return void 0;
|
|
226
|
-
const entries = Array.isArray(presence.entries) ? presence.entries.flatMap((item) => entryOf$1(item)) : [];
|
|
227
|
-
return {
|
|
228
|
-
id: typeof message.id === "string" && message.id !== "" ? message.id : fallbackId,
|
|
229
|
-
record: {
|
|
230
|
-
kind,
|
|
231
|
-
entries,
|
|
232
|
-
...presence.replaces === true ? { replaces: true } : {}
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
function entryOf$1(value) {
|
|
237
|
-
const row = record$3(value);
|
|
238
|
-
const op = row.op;
|
|
239
|
-
if (op !== "present" && op !== "added" && op !== "changed" && op !== "absent" && op !== "removed") return [];
|
|
240
|
-
const url = typeof row.url === "string" ? row.url : "";
|
|
241
|
-
if (url === "") return [];
|
|
242
|
-
const source = isSource(row.source) ? row.source : "model";
|
|
243
|
-
const version = typeof row.version === "string" ? row.version : void 0;
|
|
244
|
-
const id = typeof row.id === "string" && row.id !== "" ? row.id : void 0;
|
|
245
|
-
const reason = typeof row.reason === "string" ? row.reason : void 0;
|
|
246
|
-
return [{
|
|
247
|
-
op,
|
|
248
|
-
url,
|
|
249
|
-
source,
|
|
250
|
-
observedAt: typeof row.observedAt === "string" ? row.observedAt : "",
|
|
251
|
-
digest: typeof row.digest === "string" ? row.digest : "",
|
|
252
|
-
...version === void 0 ? {} : { version },
|
|
253
|
-
...id === void 0 ? {} : { id },
|
|
254
|
-
...reason === void 0 ? {} : { reason }
|
|
255
|
-
}];
|
|
256
|
-
}
|
|
257
|
-
function textOf$3(value) {
|
|
258
|
-
const content = record$3(value).content;
|
|
259
|
-
if (!Array.isArray(content)) return "";
|
|
260
|
-
return content.map((block) => record$3(block).text).filter((text) => typeof text === "string").join("\n");
|
|
261
|
-
}
|
|
262
|
-
function isSource(value) {
|
|
263
|
-
return typeof value === "string" && PRESENCE_SOURCES.includes(value);
|
|
264
|
-
}
|
|
265
|
-
function record$3(value) {
|
|
266
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
267
|
-
return {};
|
|
268
|
-
}
|
|
269
|
-
//#endregion
|
|
270
|
-
//#region lib/types/types.js
|
|
271
|
-
/** Public browser types. Playwright types stay inside the service. */
|
|
272
|
-
var MhBrowserError = class extends Error {
|
|
273
|
-
code;
|
|
274
|
-
page;
|
|
275
|
-
constructor(code, message, page) {
|
|
276
|
-
super(message);
|
|
277
|
-
this.name = "MhBrowserError";
|
|
278
|
-
this.code = code;
|
|
279
|
-
if (page !== void 0) this.page = page;
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
/** Owner of the person's own window. It belongs to no Session. */
|
|
283
|
-
const PERSON_OWNER = "person";
|
|
284
|
-
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
285
|
-
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
286
|
-
/**
|
|
287
|
-
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
288
|
-
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
289
|
-
*/
|
|
290
|
-
const INTERRUPTED = "INTERRUPTED";
|
|
291
|
-
/** The browser did not start or could not be restarted. */
|
|
292
|
-
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
293
|
-
function unknownTab() {
|
|
294
|
-
return {
|
|
295
|
-
url: "",
|
|
296
|
-
reason: UNKNOWN_TAB_REASON
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
//#endregion
|
|
300
|
-
//#region lib/types/tools/session.js
|
|
301
|
-
/** dsh shows the model only the message, so the message starts with the code. */
|
|
302
|
-
var BrowserToolError = class extends HarnessError {
|
|
303
|
-
reason;
|
|
304
|
-
constructor(code, reason) {
|
|
305
|
-
super(`${code}: ${reason}`, code);
|
|
306
|
-
this.name = "BrowserToolError";
|
|
307
|
-
this.reason = reason;
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
const NO_WORKSPACE_REASON$1 = "this session has no working directory";
|
|
311
|
-
/**
|
|
312
|
-
* The session of this tool call. Also records its title for the tab group.
|
|
313
|
-
* No title service, or no title yet, records an empty one.
|
|
314
|
-
*/
|
|
315
|
-
function sessionFor(ctx, exec) {
|
|
316
|
-
const session = sessionOf(exec);
|
|
317
|
-
const titles = ctx.get("sessionTitle");
|
|
318
|
-
ctx.mhBrowser.noteSessionTitle(session.id, titles?.get(session.session)?.title ?? "");
|
|
319
|
-
return session;
|
|
320
|
-
}
|
|
321
|
-
/** A title written after the last tool call still reaches the tab group. */
|
|
322
|
-
function followSessionTitles(ctx) {
|
|
323
|
-
ctx.on("session/event", (session, event) => {
|
|
324
|
-
const { type, data } = event;
|
|
325
|
-
if (type !== "session/title" || typeof data !== "object" || data === null) return;
|
|
326
|
-
const title = data.title;
|
|
327
|
-
if (typeof title === "string") ctx.mhBrowser.noteSessionTitle(session.id, title);
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
function sessionOf(exec) {
|
|
331
|
-
const session = exec.agent?.session;
|
|
332
|
-
if (session === void 0) throw new BrowserToolError("NO_SESSION", "browser tools run only inside an agent session");
|
|
333
|
-
const cwd = session.header.cwd;
|
|
334
|
-
return cwd === void 0 ? {
|
|
335
|
-
id: session.id,
|
|
336
|
-
session
|
|
337
|
-
} : {
|
|
338
|
-
id: session.id,
|
|
339
|
-
cwd,
|
|
340
|
-
session
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Where this session may write a download. With dsh's sandbox policy loaded,
|
|
345
|
-
* its mode decides: `read-only` writes nothing, otherwise the policy's
|
|
346
|
-
* workspace root. Without it, the session cwd.
|
|
347
|
-
*/
|
|
348
|
-
function writeTarget(ctx, session) {
|
|
349
|
-
const policy = ctx.get("sandboxPolicy");
|
|
350
|
-
if (policy === void 0) return session.cwd === void 0 ? {
|
|
351
|
-
denied: NO_WORKSPACE_REASON$1,
|
|
352
|
-
code: "NO_WORKSPACE"
|
|
353
|
-
} : { root: session.cwd };
|
|
354
|
-
const resolved = policy.resolve({ session: session.session });
|
|
355
|
-
if (resolved.mode === "read-only") return {
|
|
356
|
-
denied: "[sandbox: file access denied under read-only mode]",
|
|
357
|
-
code: "SANDBOX_DENIED"
|
|
358
|
-
};
|
|
359
|
-
return { root: resolved.workspaceRoot };
|
|
360
|
-
}
|
|
361
|
-
function rethrow(error, detail) {
|
|
362
|
-
if (error instanceof MhBrowserError || error instanceof BrowserToolError) {
|
|
363
|
-
const reason = error instanceof BrowserToolError ? error.reason : error.message;
|
|
364
|
-
throw new BrowserToolError(error.code, detail === void 0 ? reason : `${reason}\n${detail}`);
|
|
365
|
-
}
|
|
366
|
-
throw error;
|
|
367
|
-
}
|
|
368
|
-
//#endregion
|
|
369
|
-
//#region lib/types/tools/paths.js
|
|
370
|
-
/** Resolve upload paths and reject anything outside the session's cwd. */
|
|
371
|
-
async function filesInWorkspace(cwd, files) {
|
|
372
|
-
if (cwd === void 0) throw new BrowserToolError("NO_WORKSPACE", NO_WORKSPACE_REASON$1);
|
|
373
|
-
const root = await realpath(cwd);
|
|
374
|
-
const resolved = [];
|
|
375
|
-
for (const file of files) {
|
|
376
|
-
const abs = isAbsolute(file) ? file : resolve(cwd, file);
|
|
377
|
-
let real;
|
|
378
|
-
try {
|
|
379
|
-
real = await realpath(abs);
|
|
380
|
-
} catch {
|
|
381
|
-
throw new BrowserToolError("INVALID_ARGS", `no such file: ${file}`);
|
|
382
|
-
}
|
|
383
|
-
const fromRoot = relative(root, real);
|
|
384
|
-
if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${file} is outside the session workspace`);
|
|
385
|
-
resolved.push(real);
|
|
386
|
-
}
|
|
387
|
-
return resolved;
|
|
388
|
-
}
|
|
389
|
-
/** Refuse a filename that leaves the session's write root, without creating it. */
|
|
390
|
-
async function assertInside(target, filename) {
|
|
391
|
-
await place(target, filename);
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Write `filename` under the session's write root. A path that leaves the
|
|
395
|
-
* root is refused, through a symbolic link as well: the deepest existing
|
|
396
|
-
* directory is checked before any is created, and the file itself is not
|
|
397
|
-
* followed if it is a link.
|
|
398
|
-
*/
|
|
399
|
-
async function writeInside(target, filename, data) {
|
|
400
|
-
const file = await place(target, filename);
|
|
401
|
-
assertUnder(file.root, await realpath(await deepestExisting(dirname(file.file))), filename);
|
|
402
|
-
await mkdir(dirname(file.file), { recursive: true });
|
|
403
|
-
const parent = await realpath(dirname(file.file));
|
|
404
|
-
assertUnder(file.root, parent, filename);
|
|
405
|
-
const placed = resolve(parent, basename(file.file));
|
|
406
|
-
let handle;
|
|
407
|
-
try {
|
|
408
|
-
handle = await open(placed, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW);
|
|
409
|
-
} catch (error) {
|
|
410
|
-
if (error.code === "ELOOP") throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
411
|
-
throw error;
|
|
412
|
-
}
|
|
413
|
-
try {
|
|
414
|
-
await handle.writeFile(data);
|
|
415
|
-
} finally {
|
|
416
|
-
await handle.close();
|
|
417
|
-
}
|
|
418
|
-
const bytes = typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
|
|
419
|
-
return {
|
|
420
|
-
path: relative(file.root, placed),
|
|
421
|
-
bytes
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
function assertUnder(root, path, filename) {
|
|
425
|
-
const fromRoot = relative(root, path);
|
|
426
|
-
if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
427
|
-
}
|
|
428
|
-
async function deepestExisting(path) {
|
|
429
|
-
for (let current = path;; current = dirname(current)) try {
|
|
430
|
-
await lstat(current);
|
|
431
|
-
return current;
|
|
432
|
-
} catch {
|
|
433
|
-
if (dirname(current) === current) return current;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
async function place(target, filename) {
|
|
437
|
-
if ("denied" in target) throw new BrowserToolError(target.code, target.denied);
|
|
438
|
-
if (filename.trim() === "") throw new BrowserToolError("INVALID_ARGS", "filename: give a path inside the workspace");
|
|
439
|
-
const root = await realpath(target.root).catch(() => target.root);
|
|
440
|
-
const abs = isAbsolute(filename) ? filename : resolve(root, filename);
|
|
441
|
-
const direct = relative(root, abs);
|
|
442
|
-
if (direct.startsWith("..") || isAbsolute(direct)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
443
|
-
return {
|
|
444
|
-
root,
|
|
445
|
-
file: abs
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
//#endregion
|
|
449
|
-
//#region lib/types/tools/attachments.js
|
|
450
|
-
function initAttachments() {
|
|
451
|
-
return { refs: {} };
|
|
452
|
-
}
|
|
453
|
-
/** Attachments reach the model in user messages and tool results. */
|
|
454
|
-
function applyAttachments(state, event) {
|
|
455
|
-
if (event.type !== "user/message" && event.type !== "tool/result") return state;
|
|
456
|
-
const found = {};
|
|
457
|
-
visit(event.data, 0, found);
|
|
458
|
-
const ids = Object.keys(found).filter((id) => state.refs[id] === void 0);
|
|
459
|
-
if (ids.length === 0) return state;
|
|
460
|
-
const refs = { ...state.refs };
|
|
461
|
-
for (const id of ids) refs[id] = found[id];
|
|
462
|
-
return { refs };
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Host paths for upload values. A value naming an attachment that appeared in
|
|
466
|
-
* this session resolves through the attachment provider; anything else is a
|
|
467
|
-
* path that must resolve inside the session cwd.
|
|
28
|
+
* Browser profile directory when the configuration names none. Each dsh profile gets its own:
|
|
29
|
+
* a browser profile directory admits one running browser, so two dsh profiles sharing one
|
|
30
|
+
* cannot both start.
|
|
468
31
|
*/
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
for (const value of values) {
|
|
472
|
-
const id = attachmentIdOf(value);
|
|
473
|
-
const found = id === void 0 ? void 0 : seen.refs[id];
|
|
474
|
-
if (found !== void 0) {
|
|
475
|
-
paths.push(hostPath(ctx, found));
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
if (typeof value !== "string") throw new BrowserToolError("INVALID_INPUT", "attachment reference is not in this session");
|
|
479
|
-
paths.push(...await filesInWorkspace(cwd, [value]));
|
|
480
|
-
}
|
|
481
|
-
return paths;
|
|
482
|
-
}
|
|
483
|
-
function hostPath(ctx, found) {
|
|
484
|
-
let host;
|
|
485
|
-
try {
|
|
486
|
-
host = found.kind === "image" ? ctx.attachments.imageHostPath(found.ref) : ctx.attachments.fileHostPath(found.ref);
|
|
487
|
-
} catch (error) {
|
|
488
|
-
throw new BrowserToolError("INVALID_INPUT", error instanceof Error ? error.message : String(error));
|
|
489
|
-
}
|
|
490
|
-
if (host === void 0) throw new BrowserToolError("INVALID_INPUT", "attachment has no host path");
|
|
491
|
-
return host;
|
|
492
|
-
}
|
|
493
|
-
function attachmentIdOf(value) {
|
|
494
|
-
if (typeof value === "string") return value;
|
|
495
|
-
if (value !== null && typeof value === "object") {
|
|
496
|
-
const id = value.attachmentId;
|
|
497
|
-
if (typeof id === "string") return id;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
function visit(value, depth, found) {
|
|
501
|
-
if (depth > 12 || value === null || typeof value !== "object") return;
|
|
502
|
-
if (Array.isArray(value)) {
|
|
503
|
-
for (const item of value) visit(item, depth + 1, found);
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
const row = value;
|
|
507
|
-
if (typeof row.attachmentId === "string" && typeof row.bytes === "number") {
|
|
508
|
-
const kind = typeof row.width === "number" && typeof row.mediaType === "string" ? "image" : "file";
|
|
509
|
-
if (kind === "image" || typeof row.name === "string") found[row.attachmentId] = {
|
|
510
|
-
kind,
|
|
511
|
-
ref: row
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
for (const item of Object.values(row)) visit(item, depth + 1, found);
|
|
32
|
+
function defaultProfileDir(profile, home = resolveDshHome()) {
|
|
33
|
+
return profile === void 0 ? join(home, "mh", "browser") : join(home, "mh", "profiles", profile.name, "browser");
|
|
515
34
|
}
|
|
516
35
|
//#endregion
|
|
517
36
|
//#region lib/types/representation/agent-view.js
|
|
@@ -911,20 +430,268 @@ function versionOf$1(view) {
|
|
|
911
430
|
if (representation.kind === "agent-view") return representation.version;
|
|
912
431
|
return representation.agentView?.version;
|
|
913
432
|
}
|
|
914
|
-
function digestOf$1(view) {
|
|
915
|
-
if (view.representation.kind !== "agent-view") return void 0;
|
|
916
|
-
return representationDigest(view.representation.text);
|
|
433
|
+
function digestOf$1(view) {
|
|
434
|
+
if (view.representation.kind !== "agent-view") return void 0;
|
|
435
|
+
return representationDigest(view.representation.text);
|
|
436
|
+
}
|
|
437
|
+
function idOf(view) {
|
|
438
|
+
if (view.representation.kind !== "agent-view") return void 0;
|
|
439
|
+
return agentViewId(view.representation.text, view.url);
|
|
440
|
+
}
|
|
441
|
+
function attr$1(name, value) {
|
|
442
|
+
return ` ${name}="${escapeAttr(value)}"`;
|
|
443
|
+
}
|
|
444
|
+
function formatSeconds(seconds) {
|
|
445
|
+
const rounded = Math.round(seconds * 10) / 10;
|
|
446
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region lib/types/types.js
|
|
450
|
+
var MhBrowserError = class extends Error {
|
|
451
|
+
code;
|
|
452
|
+
page;
|
|
453
|
+
constructor(code, message, page) {
|
|
454
|
+
super(message);
|
|
455
|
+
this.name = "MhBrowserError";
|
|
456
|
+
this.code = code;
|
|
457
|
+
if (page !== void 0) this.page = page;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
/** Owner of the person's own window. It belongs to no Session. */
|
|
461
|
+
const PERSON_OWNER = "person";
|
|
462
|
+
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
463
|
+
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
464
|
+
/**
|
|
465
|
+
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
466
|
+
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
467
|
+
*/
|
|
468
|
+
const INTERRUPTED = "INTERRUPTED";
|
|
469
|
+
/** The browser did not start or could not be restarted. */
|
|
470
|
+
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
471
|
+
function unknownTab() {
|
|
472
|
+
return {
|
|
473
|
+
url: "",
|
|
474
|
+
reason: UNKNOWN_TAB_REASON
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region lib/types/tools/session.js
|
|
479
|
+
/** dsh shows the model only the message, so the message starts with the code. */
|
|
480
|
+
var BrowserToolError = class extends HarnessError {
|
|
481
|
+
code;
|
|
482
|
+
reason;
|
|
483
|
+
constructor(code, reason) {
|
|
484
|
+
super(`${code}: ${reason}`, code);
|
|
485
|
+
this.name = "BrowserToolError";
|
|
486
|
+
this.code = code;
|
|
487
|
+
this.reason = reason;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
const NO_WORKSPACE_REASON$1 = "this session has no working directory";
|
|
491
|
+
/**
|
|
492
|
+
* The session of this tool call. Also records its title for the tab group.
|
|
493
|
+
* No title service, or no title yet, records an empty one.
|
|
494
|
+
*/
|
|
495
|
+
function sessionFor(ctx, exec) {
|
|
496
|
+
const session = sessionOf(exec);
|
|
497
|
+
const titles = ctx.get("sessionTitle");
|
|
498
|
+
ctx.mhBrowser.noteSessionTitle(session.id, titles?.get(session.session)?.title ?? "");
|
|
499
|
+
return session;
|
|
500
|
+
}
|
|
501
|
+
/** A title written after the last tool call still reaches the tab group. */
|
|
502
|
+
function followSessionTitles(ctx) {
|
|
503
|
+
ctx.on("session/event", (session, event) => {
|
|
504
|
+
const { type, data } = event;
|
|
505
|
+
if (type !== "session/title" || typeof data !== "object" || data === null) return;
|
|
506
|
+
const title = data.title;
|
|
507
|
+
if (typeof title === "string") ctx.mhBrowser.noteSessionTitle(session.id, title);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
function sessionOf(exec) {
|
|
511
|
+
const session = exec.agent?.session;
|
|
512
|
+
if (session === void 0) throw new BrowserToolError("NO_SESSION", "browser tools run only inside an agent session");
|
|
513
|
+
const cwd = session.header.cwd;
|
|
514
|
+
return cwd === void 0 ? {
|
|
515
|
+
id: session.id,
|
|
516
|
+
session
|
|
517
|
+
} : {
|
|
518
|
+
id: session.id,
|
|
519
|
+
cwd,
|
|
520
|
+
session
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Where this session may write a download. With dsh's sandbox policy loaded,
|
|
525
|
+
* its mode decides: `read-only` writes nothing, otherwise the policy's
|
|
526
|
+
* workspace root. Without it, the session cwd.
|
|
527
|
+
*/
|
|
528
|
+
function writeTarget(ctx, session) {
|
|
529
|
+
const policy = ctx.get("sandboxPolicy");
|
|
530
|
+
if (policy === void 0) return session.cwd === void 0 ? {
|
|
531
|
+
denied: NO_WORKSPACE_REASON$1,
|
|
532
|
+
code: "NO_WORKSPACE"
|
|
533
|
+
} : { root: session.cwd };
|
|
534
|
+
const resolved = policy.resolve({ session: session.session });
|
|
535
|
+
if (resolved.mode === "read-only") return {
|
|
536
|
+
denied: "[sandbox: file access denied under read-only mode]",
|
|
537
|
+
code: "SANDBOX_DENIED"
|
|
538
|
+
};
|
|
539
|
+
return { root: resolved.workspaceRoot };
|
|
540
|
+
}
|
|
541
|
+
function rethrow(error, detail) {
|
|
542
|
+
if (error instanceof MhBrowserError || error instanceof BrowserToolError) {
|
|
543
|
+
const reason = error instanceof BrowserToolError ? error.reason : error.message;
|
|
544
|
+
throw new BrowserToolError(error.code, detail === void 0 ? reason : `${reason}\n${detail}`);
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region lib/types/tools/paths.js
|
|
550
|
+
/** Resolve upload paths and reject anything outside the session's cwd. */
|
|
551
|
+
async function filesInWorkspace(cwd, files) {
|
|
552
|
+
if (cwd === void 0) throw new BrowserToolError("NO_WORKSPACE", NO_WORKSPACE_REASON$1);
|
|
553
|
+
const root = await realpath(cwd);
|
|
554
|
+
const resolved = [];
|
|
555
|
+
for (const file of files) {
|
|
556
|
+
const abs = isAbsolute(file) ? file : resolve(cwd, file);
|
|
557
|
+
let real;
|
|
558
|
+
try {
|
|
559
|
+
real = await realpath(abs);
|
|
560
|
+
} catch {
|
|
561
|
+
throw new BrowserToolError("INVALID_ARGS", `no such file: ${file}`);
|
|
562
|
+
}
|
|
563
|
+
const fromRoot = relative(root, real);
|
|
564
|
+
if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${file} is outside the session workspace`);
|
|
565
|
+
resolved.push(real);
|
|
566
|
+
}
|
|
567
|
+
return resolved;
|
|
568
|
+
}
|
|
569
|
+
/** Refuse a filename that leaves the session's write root, without creating it. */
|
|
570
|
+
async function assertInside(target, filename) {
|
|
571
|
+
await place(target, filename);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Write `filename` under the session's write root. A path that leaves the
|
|
575
|
+
* root is refused, through a symbolic link as well: the deepest existing
|
|
576
|
+
* directory is checked before any is created, and the file itself is not
|
|
577
|
+
* followed if it is a link.
|
|
578
|
+
*/
|
|
579
|
+
async function writeInside(target, filename, data) {
|
|
580
|
+
const file = await place(target, filename);
|
|
581
|
+
assertUnder(file.root, await realpath(await deepestExisting(dirname(file.file))), filename);
|
|
582
|
+
await mkdir(dirname(file.file), { recursive: true });
|
|
583
|
+
const parent = await realpath(dirname(file.file));
|
|
584
|
+
assertUnder(file.root, parent, filename);
|
|
585
|
+
const placed = resolve(parent, basename(file.file));
|
|
586
|
+
let handle;
|
|
587
|
+
try {
|
|
588
|
+
handle = await open(placed, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (error.code === "ELOOP") throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
await handle.writeFile(data);
|
|
595
|
+
} finally {
|
|
596
|
+
await handle.close();
|
|
597
|
+
}
|
|
598
|
+
const bytes = typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
|
|
599
|
+
return {
|
|
600
|
+
path: relative(file.root, placed),
|
|
601
|
+
bytes
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function assertUnder(root, path, filename) {
|
|
605
|
+
const fromRoot = relative(root, path);
|
|
606
|
+
if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
607
|
+
}
|
|
608
|
+
async function deepestExisting(path) {
|
|
609
|
+
for (let current = path;; current = dirname(current)) try {
|
|
610
|
+
await lstat(current);
|
|
611
|
+
return current;
|
|
612
|
+
} catch {
|
|
613
|
+
if (dirname(current) === current) return current;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async function place(target, filename) {
|
|
617
|
+
if ("denied" in target) throw new BrowserToolError(target.code, target.denied);
|
|
618
|
+
if (filename.trim() === "") throw new BrowserToolError("INVALID_ARGS", "filename: give a path inside the workspace");
|
|
619
|
+
const root = await realpath(target.root).catch(() => target.root);
|
|
620
|
+
const abs = isAbsolute(filename) ? filename : resolve(root, filename);
|
|
621
|
+
const direct = relative(root, abs);
|
|
622
|
+
if (direct.startsWith("..") || isAbsolute(direct)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
623
|
+
return {
|
|
624
|
+
root,
|
|
625
|
+
file: abs
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region lib/types/tools/attachments.js
|
|
630
|
+
function initAttachments() {
|
|
631
|
+
return { refs: {} };
|
|
632
|
+
}
|
|
633
|
+
/** Attachments reach the model in user messages and tool results. */
|
|
634
|
+
function applyAttachments(state, event) {
|
|
635
|
+
if (event.type !== "user/message" && event.type !== "tool/result") return state;
|
|
636
|
+
const found = {};
|
|
637
|
+
visit(event.data, 0, found);
|
|
638
|
+
const ids = Object.keys(found).filter((id) => state.refs[id] === void 0);
|
|
639
|
+
if (ids.length === 0) return state;
|
|
640
|
+
const refs = { ...state.refs };
|
|
641
|
+
for (const id of ids) refs[id] = found[id];
|
|
642
|
+
return { refs };
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Host paths for upload values. A value naming an attachment that appeared in
|
|
646
|
+
* this session resolves through the attachment provider; anything else is a
|
|
647
|
+
* path that must resolve inside the session cwd.
|
|
648
|
+
*/
|
|
649
|
+
async function uploadPaths(ctx, seen, cwd, values) {
|
|
650
|
+
const paths = [];
|
|
651
|
+
for (const value of values) {
|
|
652
|
+
const id = attachmentIdOf(value);
|
|
653
|
+
const found = id === void 0 ? void 0 : seen.refs[id];
|
|
654
|
+
if (found !== void 0) {
|
|
655
|
+
paths.push(hostPath(ctx, found));
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (typeof value !== "string") throw new BrowserToolError("INVALID_INPUT", "attachment reference is not in this session");
|
|
659
|
+
paths.push(...await filesInWorkspace(cwd, [value]));
|
|
660
|
+
}
|
|
661
|
+
return paths;
|
|
917
662
|
}
|
|
918
|
-
function
|
|
919
|
-
|
|
920
|
-
|
|
663
|
+
function hostPath(ctx, found) {
|
|
664
|
+
let host;
|
|
665
|
+
try {
|
|
666
|
+
host = found.kind === "image" ? ctx.attachments.imageHostPath(found.ref) : ctx.attachments.fileHostPath(found.ref);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
throw new BrowserToolError("INVALID_INPUT", error instanceof Error ? error.message : String(error));
|
|
669
|
+
}
|
|
670
|
+
if (host === void 0) throw new BrowserToolError("INVALID_INPUT", "attachment has no host path");
|
|
671
|
+
return host;
|
|
921
672
|
}
|
|
922
|
-
function
|
|
923
|
-
|
|
673
|
+
function attachmentIdOf(value) {
|
|
674
|
+
if (typeof value === "string") return value;
|
|
675
|
+
if (value !== null && typeof value === "object") {
|
|
676
|
+
const id = value.attachmentId;
|
|
677
|
+
if (typeof id === "string") return id;
|
|
678
|
+
}
|
|
924
679
|
}
|
|
925
|
-
function
|
|
926
|
-
|
|
927
|
-
|
|
680
|
+
function visit(value, depth, found) {
|
|
681
|
+
if (depth > 12 || value === null || typeof value !== "object") return;
|
|
682
|
+
if (Array.isArray(value)) {
|
|
683
|
+
for (const item of value) visit(item, depth + 1, found);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const row = value;
|
|
687
|
+
if (typeof row.attachmentId === "string" && typeof row.bytes === "number") {
|
|
688
|
+
const kind = typeof row.width === "number" && typeof row.mediaType === "string" ? "image" : "file";
|
|
689
|
+
if (kind === "image" || typeof row.name === "string") found[row.attachmentId] = {
|
|
690
|
+
kind,
|
|
691
|
+
ref: row
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
for (const item of Object.values(row)) visit(item, depth + 1, found);
|
|
928
695
|
}
|
|
929
696
|
//#endregion
|
|
930
697
|
//#region lib/types/tools/delivery.js
|
|
@@ -1027,18 +794,18 @@ function urlsIn(text) {
|
|
|
1027
794
|
return [...new Set(found)];
|
|
1028
795
|
}
|
|
1029
796
|
function fromTool(event) {
|
|
1030
|
-
const data = record$
|
|
1031
|
-
const message = record$
|
|
797
|
+
const data = record$3(event.data);
|
|
798
|
+
const message = record$3(data.message);
|
|
1032
799
|
const callId = readString(message.toolCallId);
|
|
1033
|
-
const text = textOf$
|
|
800
|
+
const text = textOf$3(message.content);
|
|
1034
801
|
if (message.isError === true) return void 0;
|
|
1035
|
-
const meta = record$
|
|
802
|
+
const meta = record$3(data.meta);
|
|
1036
803
|
if (meta.by !== "mh-browser") return void 0;
|
|
1037
804
|
const pages = meta.pages;
|
|
1038
805
|
if (!Array.isArray(pages)) return void 0;
|
|
1039
806
|
const deliveries = [];
|
|
1040
807
|
for (const page of pages) {
|
|
1041
|
-
const row = record$
|
|
808
|
+
const row = record$3(page);
|
|
1042
809
|
if (row.representation === "header" || readString(row.reason) !== void 0) continue;
|
|
1043
810
|
const url = readString(row.url);
|
|
1044
811
|
if (url === void 0) continue;
|
|
@@ -1064,21 +831,21 @@ function fromTool(event) {
|
|
|
1064
831
|
};
|
|
1065
832
|
}
|
|
1066
833
|
function fromPresence(event) {
|
|
1067
|
-
const message = record$
|
|
1068
|
-
const source = record$
|
|
834
|
+
const message = record$3(event.data);
|
|
835
|
+
const source = record$3(message.source);
|
|
1069
836
|
if (source.kind !== "mh-browser") return void 0;
|
|
1070
|
-
const entries = record$
|
|
837
|
+
const entries = record$3(source.presence).entries;
|
|
1071
838
|
if (!Array.isArray(entries)) return void 0;
|
|
1072
839
|
const deliveries = [];
|
|
1073
840
|
for (const entry of entries) {
|
|
1074
|
-
const row = record$
|
|
841
|
+
const row = record$3(entry);
|
|
1075
842
|
if (row.op === "removed" || row.op === "absent") continue;
|
|
1076
843
|
const url = readString(row.url);
|
|
1077
844
|
if (url === void 0) continue;
|
|
1078
845
|
const digest = readString(row.digest);
|
|
1079
846
|
const version = readString(row.version);
|
|
1080
847
|
const id = readString(row.id);
|
|
1081
|
-
const body = digest === void 0 ? void 0 : shownBody(textOf$
|
|
848
|
+
const body = digest === void 0 ? void 0 : shownBody(textOf$3(message.content), url);
|
|
1082
849
|
deliveries.push({
|
|
1083
850
|
url,
|
|
1084
851
|
seq: event.seq,
|
|
@@ -1091,7 +858,7 @@ function fromPresence(event) {
|
|
|
1091
858
|
}
|
|
1092
859
|
return {
|
|
1093
860
|
deliveries,
|
|
1094
|
-
text: textOf$
|
|
861
|
+
text: textOf$3(message.content)
|
|
1095
862
|
};
|
|
1096
863
|
}
|
|
1097
864
|
const OMITTED_BODY = /* @__PURE__ */ new Set([
|
|
@@ -1136,11 +903,11 @@ function attrOf(header, name) {
|
|
|
1136
903
|
if (value === void 0 || value === "") return void 0;
|
|
1137
904
|
return value.replace(/"/g, "\"").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, "\n").replace(/ /g, "\r").replace(/	/g, " ").replace(/&/g, "&");
|
|
1138
905
|
}
|
|
1139
|
-
function textOf$
|
|
906
|
+
function textOf$3(content) {
|
|
1140
907
|
if (!Array.isArray(content)) return "";
|
|
1141
908
|
const parts = [];
|
|
1142
909
|
for (const block of content) {
|
|
1143
|
-
const text = readString(record$
|
|
910
|
+
const text = readString(record$3(block).text);
|
|
1144
911
|
if (text !== void 0) parts.push(text);
|
|
1145
912
|
}
|
|
1146
913
|
return parts.join("\n");
|
|
@@ -1157,7 +924,7 @@ function sameUrl(left, right) {
|
|
|
1157
924
|
return false;
|
|
1158
925
|
}
|
|
1159
926
|
}
|
|
1160
|
-
function record$
|
|
927
|
+
function record$3(value) {
|
|
1161
928
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
1162
929
|
return {};
|
|
1163
930
|
}
|
|
@@ -1172,9 +939,9 @@ function initTabLog() {
|
|
|
1172
939
|
function applyTabLog(state, event) {
|
|
1173
940
|
const cleared = event.type === "compaction/summary" || event.type === "compaction/prune" || event.surfaceOp === "replace";
|
|
1174
941
|
if (event.type !== "tool/result") return cleared ? withoutSnap(state) : state;
|
|
1175
|
-
const meta = record$
|
|
942
|
+
const meta = record$2(record$2(event.data).meta);
|
|
1176
943
|
if (meta.by !== "mh-browser") return cleared ? withoutSnap(state) : state;
|
|
1177
|
-
const named = Array.isArray(meta.pages) ? meta.pages.map((page) => record$
|
|
944
|
+
const named = Array.isArray(meta.pages) ? meta.pages.map((page) => record$2(page).tab).filter(isTabId) : [];
|
|
1178
945
|
const closed = Array.isArray(meta.closed) ? meta.closed.filter((note) => typeof note === "string").map(tabOfNote) : [];
|
|
1179
946
|
const next = new Set(state.open);
|
|
1180
947
|
for (const tab of named) next.add(tab);
|
|
@@ -1225,11 +992,11 @@ function withoutSnap(state) {
|
|
|
1225
992
|
return rest;
|
|
1226
993
|
}
|
|
1227
994
|
function snapOf(value) {
|
|
1228
|
-
const snap = record$
|
|
995
|
+
const snap = record$2(value);
|
|
1229
996
|
if (!Array.isArray(snap.tabs)) return void 0;
|
|
1230
997
|
return {
|
|
1231
998
|
tabs: snap.tabs.flatMap((item) => {
|
|
1232
|
-
const row = record$
|
|
999
|
+
const row = record$2(item);
|
|
1233
1000
|
if (typeof row.id !== "string" || typeof row.url !== "string") return [];
|
|
1234
1001
|
return [{
|
|
1235
1002
|
id: row.id,
|
|
@@ -1242,71 +1009,10 @@ function snapOf(value) {
|
|
|
1242
1009
|
closed: Array.isArray(snap.closed) ? snap.closed.filter((note) => typeof note === "string") : []
|
|
1243
1010
|
};
|
|
1244
1011
|
}
|
|
1245
|
-
function record$
|
|
1012
|
+
function record$2(value) {
|
|
1246
1013
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1247
1014
|
}
|
|
1248
1015
|
//#endregion
|
|
1249
|
-
//#region lib/types/projections.js
|
|
1250
|
-
/** Host-only folds over the session log; every read of what the log says goes through these. */
|
|
1251
|
-
function registerProjections(ctx) {
|
|
1252
|
-
ctx.sessionProjections.register({
|
|
1253
|
-
key: "mhBrowserDeliveries",
|
|
1254
|
-
stateVersion: 2,
|
|
1255
|
-
stateSchema: plainObject(["deliveries", "tokens"]),
|
|
1256
|
-
init: initDeliveries,
|
|
1257
|
-
apply: (state, event) => applyDelivery(state, {
|
|
1258
|
-
seq: event.seq,
|
|
1259
|
-
type: event.type,
|
|
1260
|
-
data: event.data,
|
|
1261
|
-
..."surfaceOp" in event && event.surfaceOp !== void 0 ? { surfaceOp: event.surfaceOp } : {}
|
|
1262
|
-
})
|
|
1263
|
-
});
|
|
1264
|
-
ctx.sessionProjections.register({
|
|
1265
|
-
key: "mhBrowserPresence",
|
|
1266
|
-
stateVersion: 1,
|
|
1267
|
-
stateSchema: plainObject([
|
|
1268
|
-
"committed",
|
|
1269
|
-
"inbox",
|
|
1270
|
-
"discarded",
|
|
1271
|
-
"shown",
|
|
1272
|
-
"ops"
|
|
1273
|
-
]),
|
|
1274
|
-
init: initPresenceLog,
|
|
1275
|
-
apply: (state, event) => applyPresenceLog(state, event)
|
|
1276
|
-
});
|
|
1277
|
-
ctx.sessionProjections.register({
|
|
1278
|
-
key: "mhBrowserAttachments",
|
|
1279
|
-
stateVersion: 1,
|
|
1280
|
-
stateSchema: plainObject(["refs"]),
|
|
1281
|
-
init: initAttachments,
|
|
1282
|
-
apply: (state, event) => applyAttachments(state, event)
|
|
1283
|
-
});
|
|
1284
|
-
ctx.sessionProjections.register({
|
|
1285
|
-
key: "mhBrowserTabs",
|
|
1286
|
-
stateVersion: 2,
|
|
1287
|
-
stateSchema: plainObject(["open"]),
|
|
1288
|
-
init: initTabLog,
|
|
1289
|
-
apply: (state, event) => applyTabLog(state, event)
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
/** Fails closed when the registry or a unit is not active. */
|
|
1293
|
-
function logReader(ctx) {
|
|
1294
|
-
const read = (session, key) => {
|
|
1295
|
-
const state = ctx.sessionProjections.stateOf(session, key);
|
|
1296
|
-
if (state === void 0) throw new Error(`mh-browser cannot read ${key}: its session projection is not registered`);
|
|
1297
|
-
return state;
|
|
1298
|
-
};
|
|
1299
|
-
return {
|
|
1300
|
-
deliveries: (session) => read(session, "mhBrowserDeliveries"),
|
|
1301
|
-
presence: (session) => read(session, "mhBrowserPresence"),
|
|
1302
|
-
attachments: (session) => read(session, "mhBrowserAttachments"),
|
|
1303
|
-
tabs: (session) => read(session, "mhBrowserTabs")
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
function plainObject(keys) {
|
|
1307
|
-
return z$1.custom((value) => value !== null && typeof value === "object" && keys.every((key) => key in value));
|
|
1308
|
-
}
|
|
1309
|
-
//#endregion
|
|
1310
1016
|
//#region lib/types/tools/page-ops.js
|
|
1311
1017
|
const IMAGE_VALUE = {
|
|
1312
1018
|
type: "object",
|
|
@@ -1358,7 +1064,7 @@ const IMAGE_VALUE = {
|
|
|
1358
1064
|
const TAB$1 = "Tab id copied from the latest result in this session: a tabs list, or a page element's tab attribute. Omit it to use the tab of that latest result; if it has no tab, the call fails with INVALID_ARGS. An idle tab may be closed, and a closed id fails with UNKNOWN_TAB: open the address again with browser_open. A tab with unsubmitted input is marked in use and is not closed, and opening or a declared action does not reload it. Tabs are gone after a restart. A page with an Agent View has no tab until you open it with snapshot.";
|
|
1359
1065
|
const TARGET = "A ref copied from this tab's latest snapshot, such as e3, or a selector that matches one element. A ref that is not in the current snapshot fails with STALE_REF.";
|
|
1360
1066
|
const ELEMENT = "What the person sees in the log. It does not locate the element.";
|
|
1361
|
-
const FILENAME = "A path inside the workspace. The result is saved there and the reply is the path and the byte count. Omit it and the result stays in the reply.";
|
|
1067
|
+
const FILENAME = "A path inside the workspace. The result is saved there and the reply is the path and the byte count. A file already at that path is replaced. Omit it and the result stays in the reply.";
|
|
1362
1068
|
const OUTPUT = {
|
|
1363
1069
|
schema: {
|
|
1364
1070
|
type: "object",
|
|
@@ -1805,8 +1511,7 @@ const OPS = [
|
|
|
1805
1511
|
})
|
|
1806
1512
|
}
|
|
1807
1513
|
];
|
|
1808
|
-
function pageOperationTools(ctx) {
|
|
1809
|
-
const reader = logReader(ctx);
|
|
1514
|
+
function pageOperationTools(ctx, reader) {
|
|
1810
1515
|
return OPS.map((spec) => defineOperation(ctx, reader, spec));
|
|
1811
1516
|
}
|
|
1812
1517
|
function defineOperation(ctx, reader, spec) {
|
|
@@ -1877,9 +1582,9 @@ function defineOperation(ctx, reader, spec) {
|
|
|
1877
1582
|
}
|
|
1878
1583
|
/** Probe the path before the action, then write the result the model would have been shown. */
|
|
1879
1584
|
async function saveResult(target, filename, view) {
|
|
1880
|
-
return writeInside(target, filename, view.shot?.bytes ?? textOf$
|
|
1585
|
+
return writeInside(target, filename, view.shot?.bytes ?? textOf$2(view));
|
|
1881
1586
|
}
|
|
1882
|
-
function textOf$
|
|
1587
|
+
function textOf$2(view) {
|
|
1883
1588
|
if (view.summary !== void 0 && view.summary !== "") return view.detail === void 0 || view.detail === "" ? view.summary : `${view.summary}\n${view.detail}`;
|
|
1884
1589
|
return view.representation.text;
|
|
1885
1590
|
}
|
|
@@ -1895,69 +1600,303 @@ function string(value) {
|
|
|
1895
1600
|
function stringList$1(value) {
|
|
1896
1601
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1897
1602
|
}
|
|
1898
|
-
function optionalString(args, key) {
|
|
1899
|
-
return typeof args[key] === "string" ? { element: args[key] } : {};
|
|
1603
|
+
function optionalString(args, key) {
|
|
1604
|
+
return typeof args[key] === "string" ? { element: args[key] } : {};
|
|
1605
|
+
}
|
|
1606
|
+
function optional(key, value) {
|
|
1607
|
+
return typeof value === "string" ? { [key]: value } : {};
|
|
1608
|
+
}
|
|
1609
|
+
function fieldsOf(value) {
|
|
1610
|
+
if (!Array.isArray(value)) return [];
|
|
1611
|
+
return value.flatMap((item) => {
|
|
1612
|
+
if (item === null || typeof item !== "object") return [];
|
|
1613
|
+
const row = item;
|
|
1614
|
+
const type = row.type;
|
|
1615
|
+
if (type !== "textbox" && type !== "checkbox" && type !== "radio" && type !== "combobox" && type !== "slider") return [];
|
|
1616
|
+
return [{
|
|
1617
|
+
target: string(row.target),
|
|
1618
|
+
...typeof row.element === "string" ? { element: row.element } : {},
|
|
1619
|
+
name: string(row.name),
|
|
1620
|
+
type,
|
|
1621
|
+
value: string(row.value)
|
|
1622
|
+
}];
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
function callIdOf(exec) {
|
|
1626
|
+
return String(exec.callId);
|
|
1627
|
+
}
|
|
1628
|
+
function imageValue(ref) {
|
|
1629
|
+
return {
|
|
1630
|
+
attachmentId: ref.attachmentId,
|
|
1631
|
+
mediaType: ref.mediaType,
|
|
1632
|
+
bytes: ref.bytes,
|
|
1633
|
+
width: ref.width,
|
|
1634
|
+
height: ref.height,
|
|
1635
|
+
...ref.name === void 0 ? {} : { name: ref.name },
|
|
1636
|
+
...ref.originalDimensions === void 0 ? {} : { originalDimensions: { ...ref.originalDimensions } }
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
function imageRef(value) {
|
|
1640
|
+
return {
|
|
1641
|
+
attachmentId: value.attachmentId,
|
|
1642
|
+
mediaType: value.mediaType,
|
|
1643
|
+
bytes: value.bytes,
|
|
1644
|
+
width: value.width,
|
|
1645
|
+
height: value.height,
|
|
1646
|
+
...value.name === void 0 ? {} : { name: value.name },
|
|
1647
|
+
...value.originalDimensions === void 0 ? {} : { originalDimensions: { ...value.originalDimensions } }
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
async function saveShot(ctx, view) {
|
|
1651
|
+
const shot = view.shot;
|
|
1652
|
+
if (shot === void 0) return void 0;
|
|
1653
|
+
try {
|
|
1654
|
+
return await ctx.attachments.saveImage({
|
|
1655
|
+
data: shot.bytes,
|
|
1656
|
+
mediaType: shot.mediaType,
|
|
1657
|
+
name: `${view.tab ?? "page"}.${shot.mediaType.slice(6)}`
|
|
1658
|
+
});
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
ctx.logger.warn("saving the screenshot failed: %o", error);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
function isModifier(value) {
|
|
1665
|
+
return value === "Alt" || value === "Control" || value === "ControlOrMeta" || value === "Meta" || value === "Shift";
|
|
1666
|
+
}
|
|
1667
|
+
//#endregion
|
|
1668
|
+
//#region lib/types/presence/record.js
|
|
1669
|
+
/** Structured presence record carried on a message source. The model sees the rendered text, not this. */
|
|
1670
|
+
const PRESENCE_SOURCES = [
|
|
1671
|
+
"person via sidebar",
|
|
1672
|
+
"cwd reference",
|
|
1673
|
+
"model"
|
|
1674
|
+
];
|
|
1675
|
+
//#endregion
|
|
1676
|
+
//#region lib/types/presence/fold.js
|
|
1677
|
+
function initPresenceLog() {
|
|
1678
|
+
return {
|
|
1679
|
+
committed: [],
|
|
1680
|
+
inbox: {
|
|
1681
|
+
"next-step": [],
|
|
1682
|
+
"next-turn": []
|
|
1683
|
+
},
|
|
1684
|
+
discarded: [],
|
|
1685
|
+
visible: false,
|
|
1686
|
+
hidden: false,
|
|
1687
|
+
shown: [],
|
|
1688
|
+
ops: []
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
function applyPresenceLog(state, event) {
|
|
1692
|
+
if (event.type === "compaction/summary") {
|
|
1693
|
+
if (!state.visible && state.shown.length === 0) return state;
|
|
1694
|
+
return {
|
|
1695
|
+
...state,
|
|
1696
|
+
visible: false,
|
|
1697
|
+
hidden: true,
|
|
1698
|
+
shown: []
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
if (event.type === "user/message") {
|
|
1702
|
+
const carried = carriedOf(event.data, `history:${String(event.seq)}`);
|
|
1703
|
+
if (carried === void 0) return state;
|
|
1704
|
+
return {
|
|
1705
|
+
...state,
|
|
1706
|
+
committed: applyRecord(state.committed, carried.record, "committed"),
|
|
1707
|
+
inbox: {
|
|
1708
|
+
"next-step": state.inbox["next-step"].map((item) => item?.id === carried.id ? null : item),
|
|
1709
|
+
"next-turn": state.inbox["next-turn"].map((item) => item?.id === carried.id ? null : item)
|
|
1710
|
+
},
|
|
1711
|
+
discarded: [],
|
|
1712
|
+
visible: true,
|
|
1713
|
+
hidden: false,
|
|
1714
|
+
shown: [...state.shown, textDigest(event.data)],
|
|
1715
|
+
ops: []
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
if (event.type === "agent/inbox/discarded") {
|
|
1719
|
+
const carried = carriedOf(event.data, `discarded:${String(event.seq)}`);
|
|
1720
|
+
if (carried === void 0) return state;
|
|
1721
|
+
return {
|
|
1722
|
+
...state,
|
|
1723
|
+
discarded: [...state.discarded, carried]
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
if (event.type === "agent/inbox/spliced") return applySplice(state, event);
|
|
1727
|
+
if (event.type === "tool/result") return applyReceipts(state, event);
|
|
1728
|
+
return state;
|
|
1729
|
+
}
|
|
1730
|
+
/** The folded set, with pending reminders and unannounced receipts on top. */
|
|
1731
|
+
function presenceSet(state) {
|
|
1732
|
+
let pages = state.committed;
|
|
1733
|
+
for (const carried of [
|
|
1734
|
+
...state.inbox["next-turn"],
|
|
1735
|
+
...state.inbox["next-step"],
|
|
1736
|
+
...state.discarded
|
|
1737
|
+
]) if (carried !== null) pages = applyRecord(pages, carried.record, "pending");
|
|
1738
|
+
const removals = [];
|
|
1739
|
+
for (const op of state.ops) {
|
|
1740
|
+
const index = pages.findIndex((page) => page.url === op.url);
|
|
1741
|
+
if (op.op === "remove") {
|
|
1742
|
+
if (index < 0) continue;
|
|
1743
|
+
pages = pages.toSpliced(index, 1);
|
|
1744
|
+
removals.push(op);
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
if (index >= 0) continue;
|
|
1748
|
+
pages = [...pages, {
|
|
1749
|
+
url: op.url,
|
|
1750
|
+
source: "model",
|
|
1751
|
+
state: "pending",
|
|
1752
|
+
order: pages.length
|
|
1753
|
+
}];
|
|
1754
|
+
}
|
|
1755
|
+
return {
|
|
1756
|
+
pages,
|
|
1757
|
+
removals
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
/** The text of a reminder, as `shown` records it. */
|
|
1761
|
+
function textDigest(message) {
|
|
1762
|
+
return createHash("sha256").update(textOf$1(message)).digest("hex");
|
|
1900
1763
|
}
|
|
1901
|
-
function
|
|
1902
|
-
|
|
1764
|
+
function applySplice(state, event) {
|
|
1765
|
+
const data = record$1(event.data);
|
|
1766
|
+
const target = data.target === "next-turn" ? "next-turn" : "next-step";
|
|
1767
|
+
const start = typeof data.start === "number" ? data.start : 0;
|
|
1768
|
+
const removedCount = typeof data.removedCount === "number" ? data.removedCount : 0;
|
|
1769
|
+
const slots = (Array.isArray(data.inserted) ? data.inserted : []).map((item, index) => carriedOf(item, `splice:${String(event.seq)}:${String(index)}`));
|
|
1770
|
+
const list = state.inbox[target];
|
|
1771
|
+
const removed = list.slice(start, start + removedCount).filter((item) => item !== null);
|
|
1772
|
+
if (removedCount === 0 && slots.length === 0) return state;
|
|
1773
|
+
const next = [
|
|
1774
|
+
...list.slice(0, start),
|
|
1775
|
+
...slots.map((slot) => slot ?? null),
|
|
1776
|
+
...list.slice(start + removedCount)
|
|
1777
|
+
];
|
|
1778
|
+
const announced = slots.some((slot) => slot !== void 0);
|
|
1779
|
+
return {
|
|
1780
|
+
...state,
|
|
1781
|
+
inbox: {
|
|
1782
|
+
...state.inbox,
|
|
1783
|
+
[target]: next
|
|
1784
|
+
},
|
|
1785
|
+
discarded: data.outcome === "canceled" ? [...state.discarded, ...removed] : state.discarded,
|
|
1786
|
+
ops: announced ? [] : state.ops
|
|
1787
|
+
};
|
|
1903
1788
|
}
|
|
1904
|
-
function
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1789
|
+
function applyReceipts(state, event) {
|
|
1790
|
+
const data = record$1(event.data);
|
|
1791
|
+
if (record$1(data.message).isError === true) return state;
|
|
1792
|
+
const meta = record$1(data.meta);
|
|
1793
|
+
if (meta.by !== "mh-browser" || meta.action !== "presence" || !Array.isArray(meta.receipts)) return state;
|
|
1794
|
+
const ops = [];
|
|
1795
|
+
for (const item of meta.receipts) {
|
|
1796
|
+
const row = record$1(item);
|
|
1797
|
+
if (typeof row.url !== "string" || row.url === "") continue;
|
|
1798
|
+
if (row.receipt === "added (pending)") ops.push({
|
|
1799
|
+
op: "put",
|
|
1800
|
+
url: row.url
|
|
1801
|
+
});
|
|
1802
|
+
if (row.receipt === "removed (pending)") ops.push({
|
|
1803
|
+
op: "remove",
|
|
1804
|
+
url: row.url
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
return ops.length === 0 ? state : {
|
|
1808
|
+
...state,
|
|
1809
|
+
ops: [...state.ops, ...ops]
|
|
1810
|
+
};
|
|
1919
1811
|
}
|
|
1920
|
-
function
|
|
1921
|
-
|
|
1812
|
+
function applyRecord(pages, record, state) {
|
|
1813
|
+
if (record.kind === "baseline") {
|
|
1814
|
+
const next = [];
|
|
1815
|
+
for (const entry of record.entries) {
|
|
1816
|
+
if (entry.op === "removed") continue;
|
|
1817
|
+
next.push(heldFrom(entry, state, next.length));
|
|
1818
|
+
}
|
|
1819
|
+
return next;
|
|
1820
|
+
}
|
|
1821
|
+
const next = pages.map((page) => ({ ...page }));
|
|
1822
|
+
for (const entry of record.entries) {
|
|
1823
|
+
if (entry.op === "removed") {
|
|
1824
|
+
const index = next.findIndex((page) => page.url === entry.url);
|
|
1825
|
+
if (index >= 0) next.splice(index, 1);
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
const index = next.findIndex((page) => page.url === entry.url);
|
|
1829
|
+
const held = heldFrom(entry, state, index >= 0 ? next[index]?.order ?? index : next.length);
|
|
1830
|
+
if (index >= 0) next[index] = held;
|
|
1831
|
+
else next.push(held);
|
|
1832
|
+
}
|
|
1833
|
+
return next;
|
|
1922
1834
|
}
|
|
1923
|
-
function
|
|
1835
|
+
function heldFrom(entry, state, order) {
|
|
1836
|
+
const absent = entry.op === "absent";
|
|
1924
1837
|
return {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1838
|
+
url: entry.url,
|
|
1839
|
+
source: entry.source,
|
|
1840
|
+
state,
|
|
1841
|
+
order,
|
|
1842
|
+
seen: absent ? { absent: entry.reason ?? "" } : {
|
|
1843
|
+
digest: entry.digest,
|
|
1844
|
+
observedAt: entry.observedAt,
|
|
1845
|
+
...entry.version === void 0 ? {} : { version: entry.version },
|
|
1846
|
+
...entry.id === void 0 ? {} : { id: entry.id }
|
|
1847
|
+
}
|
|
1932
1848
|
};
|
|
1933
1849
|
}
|
|
1934
|
-
|
|
1850
|
+
/** A `user/message` event's data is the message itself. */
|
|
1851
|
+
function carriedOf(data, fallbackId) {
|
|
1852
|
+
const message = record$1(data);
|
|
1853
|
+
const source = record$1(message.source);
|
|
1854
|
+
if (source.kind !== "mh-browser") return void 0;
|
|
1855
|
+
const presence = record$1(source.presence);
|
|
1856
|
+
const kind = presence.kind;
|
|
1857
|
+
if (kind !== "baseline" && kind !== "delta") return void 0;
|
|
1858
|
+
const entries = Array.isArray(presence.entries) ? presence.entries.flatMap((item) => entryOf$1(item)) : [];
|
|
1935
1859
|
return {
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
...value.originalDimensions === void 0 ? {} : { originalDimensions: { ...value.originalDimensions } }
|
|
1860
|
+
id: typeof message.id === "string" && message.id !== "" ? message.id : fallbackId,
|
|
1861
|
+
record: {
|
|
1862
|
+
kind,
|
|
1863
|
+
entries,
|
|
1864
|
+
...presence.replaces === true ? { replaces: true } : {}
|
|
1865
|
+
}
|
|
1943
1866
|
};
|
|
1944
1867
|
}
|
|
1945
|
-
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1868
|
+
function entryOf$1(value) {
|
|
1869
|
+
const row = record$1(value);
|
|
1870
|
+
const op = row.op;
|
|
1871
|
+
if (op !== "present" && op !== "added" && op !== "changed" && op !== "absent" && op !== "removed") return [];
|
|
1872
|
+
const url = typeof row.url === "string" ? row.url : "";
|
|
1873
|
+
if (url === "") return [];
|
|
1874
|
+
const source = isSource(row.source) ? row.source : "model";
|
|
1875
|
+
const version = typeof row.version === "string" ? row.version : void 0;
|
|
1876
|
+
const id = typeof row.id === "string" && row.id !== "" ? row.id : void 0;
|
|
1877
|
+
const reason = typeof row.reason === "string" ? row.reason : void 0;
|
|
1878
|
+
return [{
|
|
1879
|
+
op,
|
|
1880
|
+
url,
|
|
1881
|
+
source,
|
|
1882
|
+
observedAt: typeof row.observedAt === "string" ? row.observedAt : "",
|
|
1883
|
+
digest: typeof row.digest === "string" ? row.digest : "",
|
|
1884
|
+
...version === void 0 ? {} : { version },
|
|
1885
|
+
...id === void 0 ? {} : { id },
|
|
1886
|
+
...reason === void 0 ? {} : { reason }
|
|
1887
|
+
}];
|
|
1958
1888
|
}
|
|
1959
|
-
function
|
|
1960
|
-
|
|
1889
|
+
function textOf$1(value) {
|
|
1890
|
+
const content = record$1(value).content;
|
|
1891
|
+
if (!Array.isArray(content)) return "";
|
|
1892
|
+
return content.map((block) => record$1(block).text).filter((text) => typeof text === "string").join("\n");
|
|
1893
|
+
}
|
|
1894
|
+
function isSource(value) {
|
|
1895
|
+
return typeof value === "string" && PRESENCE_SOURCES.includes(value);
|
|
1896
|
+
}
|
|
1897
|
+
function record$1(value) {
|
|
1898
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
1899
|
+
return {};
|
|
1961
1900
|
}
|
|
1962
1901
|
//#endregion
|
|
1963
1902
|
//#region lib/types/presence/notice.js
|
|
@@ -1983,8 +1922,7 @@ const BROWSER_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1983
1922
|
* the reminder after the claimed input. `await next()` runs first, so a
|
|
1984
1923
|
* caller that puts pages and then returns `next()` is folded into this step.
|
|
1985
1924
|
*/
|
|
1986
|
-
function registerPresence(ctx) {
|
|
1987
|
-
const reader = logReader(ctx);
|
|
1925
|
+
function registerPresence(ctx, reader) {
|
|
1988
1926
|
const tails = /* @__PURE__ */ new WeakMap();
|
|
1989
1927
|
const stepTouches = /* @__PURE__ */ new WeakMap();
|
|
1990
1928
|
const executionTouches = /* @__PURE__ */ new Map();
|
|
@@ -2012,8 +1950,7 @@ function registerPresence(ctx) {
|
|
|
2012
1950
|
});
|
|
2013
1951
|
};
|
|
2014
1952
|
const stepIsOpen = (session) => {
|
|
2015
|
-
const boundary = ctx.
|
|
2016
|
-
if (boundary === void 0) throw new Error("mh-browser requires the turnBoundary session projection");
|
|
1953
|
+
const boundary = ctx.agents.turnBoundaryOf(session);
|
|
2017
1954
|
return boundary.openTurnStartSeq !== null && boundary.lastStepBoundary?.kind === "start" && boundary.lastStepBoundary.seq > boundary.openTurnStartSeq;
|
|
2018
1955
|
};
|
|
2019
1956
|
const project = (touch) => {
|
|
@@ -2239,6 +2176,58 @@ function canonical$1(url) {
|
|
|
2239
2176
|
}
|
|
2240
2177
|
}
|
|
2241
2178
|
//#endregion
|
|
2179
|
+
//#region lib/types/projections.js
|
|
2180
|
+
/** Host-only folds over the session log; every read of what the log says goes through these. */
|
|
2181
|
+
async function registerProjections(ctx) {
|
|
2182
|
+
return readerFrom(await ctx.sessionProjections.register({
|
|
2183
|
+
key: "mhBrowserDeliveries",
|
|
2184
|
+
stateVersion: 2,
|
|
2185
|
+
stateSchema: plainObject(["deliveries", "tokens"]),
|
|
2186
|
+
init: initDeliveries,
|
|
2187
|
+
apply: (state, event) => applyDelivery(state, {
|
|
2188
|
+
seq: event.seq,
|
|
2189
|
+
type: event.type,
|
|
2190
|
+
data: event.data,
|
|
2191
|
+
..."surfaceOp" in event && event.surfaceOp !== void 0 ? { surfaceOp: event.surfaceOp } : {}
|
|
2192
|
+
})
|
|
2193
|
+
}), await ctx.sessionProjections.register({
|
|
2194
|
+
key: "mhBrowserPresence",
|
|
2195
|
+
stateVersion: 1,
|
|
2196
|
+
stateSchema: plainObject([
|
|
2197
|
+
"committed",
|
|
2198
|
+
"inbox",
|
|
2199
|
+
"discarded",
|
|
2200
|
+
"shown",
|
|
2201
|
+
"ops"
|
|
2202
|
+
]),
|
|
2203
|
+
init: initPresenceLog,
|
|
2204
|
+
apply: (state, event) => applyPresenceLog(state, event)
|
|
2205
|
+
}), await ctx.sessionProjections.register({
|
|
2206
|
+
key: "mhBrowserAttachments",
|
|
2207
|
+
stateVersion: 1,
|
|
2208
|
+
stateSchema: plainObject(["refs"]),
|
|
2209
|
+
init: initAttachments,
|
|
2210
|
+
apply: (state, event) => applyAttachments(state, event)
|
|
2211
|
+
}), await ctx.sessionProjections.register({
|
|
2212
|
+
key: "mhBrowserTabs",
|
|
2213
|
+
stateVersion: 2,
|
|
2214
|
+
stateSchema: plainObject(["open"]),
|
|
2215
|
+
init: initTabLog,
|
|
2216
|
+
apply: (state, event) => applyTabLog(state, event)
|
|
2217
|
+
}));
|
|
2218
|
+
}
|
|
2219
|
+
function readerFrom(deliveries, presence, attachments, tabs) {
|
|
2220
|
+
return {
|
|
2221
|
+
deliveries: (session) => deliveries.stateOf(session),
|
|
2222
|
+
presence: (session) => presence.stateOf(session),
|
|
2223
|
+
attachments: (session) => attachments.stateOf(session),
|
|
2224
|
+
tabs: (session) => tabs.stateOf(session)
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
function plainObject(keys) {
|
|
2228
|
+
return z$1.custom((value) => value !== null && typeof value === "object" && keys.every((key) => key in value));
|
|
2229
|
+
}
|
|
2230
|
+
//#endregion
|
|
2242
2231
|
//#region lib/types/presence/book.js
|
|
2243
2232
|
/** In-memory presence set for one process. The log is the source of a set that outlives the process. */
|
|
2244
2233
|
var PresenceBook = class {
|
|
@@ -2973,6 +2962,7 @@ async function launchBrowser(options) {
|
|
|
2973
2962
|
acceptDownloads: true,
|
|
2974
2963
|
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
2975
2964
|
...options.headless ? {} : {
|
|
2965
|
+
viewport: null,
|
|
2976
2966
|
ignoreDefaultArgs: ["--disable-extensions"],
|
|
2977
2967
|
args: ["--enable-unsafe-extension-debugging", "--keep-alive-for-test"]
|
|
2978
2968
|
},
|
|
@@ -4108,6 +4098,16 @@ function watchDialog(page) {
|
|
|
4108
4098
|
};
|
|
4109
4099
|
}
|
|
4110
4100
|
//#endregion
|
|
4101
|
+
//#region lib/types/words.js
|
|
4102
|
+
/** Shown on a session's tab group while a handover is waiting. */
|
|
4103
|
+
const WAITING_TITLE = "等你:";
|
|
4104
|
+
/** The question on the handover card. `reason` is the sentence the model gave. */
|
|
4105
|
+
function handoverQuestion(reason) {
|
|
4106
|
+
return `浏览器交给你了:${reason}`;
|
|
4107
|
+
}
|
|
4108
|
+
/** The option that hands the browser back. */
|
|
4109
|
+
const HAND_BACK = "交回";
|
|
4110
|
+
//#endregion
|
|
4111
4111
|
//#region lib/types/service/extension.js
|
|
4112
4112
|
const PACKAGE = "@mind-harness/mh-browser";
|
|
4113
4113
|
const WORKER_MS = 5e3;
|
|
@@ -4772,7 +4772,7 @@ var Windows = class {
|
|
|
4772
4772
|
if (extension === void 0 || groupId === void 0) return;
|
|
4773
4773
|
const title = this.titles.get(owner) ?? "";
|
|
4774
4774
|
const waiting = this.waiting.has(owner);
|
|
4775
|
-
await extension.styleGroup(groupId, waiting ?
|
|
4775
|
+
await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue");
|
|
4776
4776
|
}
|
|
4777
4777
|
async ingest(page) {
|
|
4778
4778
|
if (this.byPage.has(page)) return;
|
|
@@ -5009,6 +5009,9 @@ const DOWNLOAD_EVENT_MS = 3e3;
|
|
|
5009
5009
|
/** More exits than this within the window and the browser is not started again until one ages out. */
|
|
5010
5010
|
const RELAUNCH_LIMIT = 3;
|
|
5011
5011
|
const RELAUNCH_WINDOW_MS = 6e4;
|
|
5012
|
+
/** Agent Views remembered for a later conditional read. Older ones are read again in full. */
|
|
5013
|
+
const DOCUMENT_CAP = 64;
|
|
5014
|
+
const OPEN_ADDRESS_REASON = "only http and https addresses are opened";
|
|
5012
5015
|
const STOP_WAIT_MS = 5e3;
|
|
5013
5016
|
const WAITING_STOPPED = "stopped while waiting";
|
|
5014
5017
|
const STOPPED = Symbol("stopped");
|
|
@@ -5052,10 +5055,10 @@ let MhBrowserService = (() => {
|
|
|
5052
5055
|
windows = (__runInitializers(this, _instanceExtraInitializers), new Windows());
|
|
5053
5056
|
launchOptions;
|
|
5054
5057
|
context;
|
|
5055
|
-
ready;
|
|
5058
|
+
ready = Promise.resolve();
|
|
5056
5059
|
closing = false;
|
|
5057
|
-
/** The browser
|
|
5058
|
-
exited =
|
|
5060
|
+
/** The browser is not running. The next call that needs a page starts it. */
|
|
5061
|
+
exited = true;
|
|
5059
5062
|
exits = [];
|
|
5060
5063
|
/** One queue per owner: a slow wait in one Session does not hold up another. */
|
|
5061
5064
|
queues = /* @__PURE__ */ new Map();
|
|
@@ -5085,7 +5088,7 @@ let MhBrowserService = (() => {
|
|
|
5085
5088
|
this.windows.onTabClosed((tab) => {
|
|
5086
5089
|
this.shown.delete(tab.id);
|
|
5087
5090
|
});
|
|
5088
|
-
this.
|
|
5091
|
+
this.up().catch(() => void 0);
|
|
5089
5092
|
ctx.on("session/disposed", (session) => {
|
|
5090
5093
|
const owner = session.id;
|
|
5091
5094
|
if (owner === void 0) return;
|
|
@@ -5490,6 +5493,10 @@ let MhBrowserService = (() => {
|
|
|
5490
5493
|
* the tab, and that tab is not listed as closed.
|
|
5491
5494
|
*/
|
|
5492
5495
|
async openAddress(owner, item, signal) {
|
|
5496
|
+
if (!openableAddress(item.url)) return {
|
|
5497
|
+
url: item.url,
|
|
5498
|
+
reason: OPEN_ADDRESS_REASON
|
|
5499
|
+
};
|
|
5493
5500
|
const wantsTab = owner === "person" || item.snapshot === true || item.waitFor !== void 0;
|
|
5494
5501
|
if (!wantsTab && this.knownOrigin(item.url)) {
|
|
5495
5502
|
const via = await this.readKnown(owner, item.url, signal);
|
|
@@ -5532,7 +5539,7 @@ let MhBrowserService = (() => {
|
|
|
5532
5539
|
if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
|
|
5533
5540
|
this.rememberOrigin(opened.view.url);
|
|
5534
5541
|
const shown = this.shown.get(opened.live.id);
|
|
5535
|
-
if (shown?.kind === "agent-view") this.
|
|
5542
|
+
if (shown?.kind === "agent-view") this.rememberDocument(shown.pageUrl, shown);
|
|
5536
5543
|
}
|
|
5537
5544
|
/**
|
|
5538
5545
|
* Read a known origin without a tab. `undefined` means this page has to be
|
|
@@ -5576,10 +5583,21 @@ let MhBrowserService = (() => {
|
|
|
5576
5583
|
});
|
|
5577
5584
|
if (stopped(signal)) return STOPPED;
|
|
5578
5585
|
if (shown.kind !== "agent-view") return void 0;
|
|
5579
|
-
this.
|
|
5586
|
+
this.rememberDocument(pageUrl, shown);
|
|
5580
5587
|
this.rememberOrigin(pageUrl);
|
|
5581
5588
|
return this.pageOfShown(owner, shown);
|
|
5582
5589
|
}
|
|
5590
|
+
/** Keep the newest Agent Views. A dropped one is read in full the next time. */
|
|
5591
|
+
rememberDocument(url, shown) {
|
|
5592
|
+
const key = canonical(url);
|
|
5593
|
+
this.documents.delete(key);
|
|
5594
|
+
this.documents.set(key, shown);
|
|
5595
|
+
while (this.documents.size > DOCUMENT_CAP) {
|
|
5596
|
+
const oldest = this.documents.keys().next().value;
|
|
5597
|
+
if (oldest === void 0) return;
|
|
5598
|
+
this.documents.delete(oldest);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5583
5601
|
/** One page from a document, with no tab. */
|
|
5584
5602
|
pageOfShown(owner, shown) {
|
|
5585
5603
|
const view = {
|
|
@@ -6052,7 +6070,10 @@ let MhBrowserService = (() => {
|
|
|
6052
6070
|
});
|
|
6053
6071
|
}
|
|
6054
6072
|
async aria(tab) {
|
|
6055
|
-
return tab.page.ariaSnapshot({ mode: "ai" }).catch(() =>
|
|
6073
|
+
return tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6074
|
+
this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
|
|
6075
|
+
return "";
|
|
6076
|
+
});
|
|
6056
6077
|
}
|
|
6057
6078
|
async pageFromShown(tab, shown, extra) {
|
|
6058
6079
|
const current = shown ?? {
|
|
@@ -6062,7 +6083,10 @@ let MhBrowserService = (() => {
|
|
|
6062
6083
|
text: "",
|
|
6063
6084
|
snapshot: ""
|
|
6064
6085
|
};
|
|
6065
|
-
const title = tab.dialogHeld ? "" : await tab.page.title().catch(() =>
|
|
6086
|
+
const title = tab.dialogHeld ? "" : await tab.page.title().catch((error) => {
|
|
6087
|
+
this.ctx.logger.debug("page title failed: %s", errorText(error));
|
|
6088
|
+
return "";
|
|
6089
|
+
});
|
|
6066
6090
|
this.windows.noteTitle(tab, title);
|
|
6067
6091
|
const view = {
|
|
6068
6092
|
tab: tab.id,
|
|
@@ -6227,8 +6251,9 @@ let MhBrowserService = (() => {
|
|
|
6227
6251
|
...location === void 0 || location === "" ? {} : { location },
|
|
6228
6252
|
...mediaType === void 0 ? {} : { mediaType }
|
|
6229
6253
|
};
|
|
6230
|
-
} catch {
|
|
6254
|
+
} catch (error) {
|
|
6231
6255
|
if (signal?.aborted === true) throw new MhBrowserError(INTERRUPTED, "stopped after sending; it may have taken effect");
|
|
6256
|
+
this.ctx.logger.debug("declared action was not answered: %s", errorText(error));
|
|
6232
6257
|
return { kind: "lost" };
|
|
6233
6258
|
}
|
|
6234
6259
|
}
|
|
@@ -6330,11 +6355,18 @@ let MhBrowserService = (() => {
|
|
|
6330
6355
|
async start() {
|
|
6331
6356
|
const context = await launchBrowser(this.launchOptions);
|
|
6332
6357
|
if (this.closing) {
|
|
6333
|
-
await
|
|
6358
|
+
await this.closeContext(context);
|
|
6334
6359
|
return;
|
|
6335
6360
|
}
|
|
6336
6361
|
this.context = context;
|
|
6337
|
-
|
|
6362
|
+
try {
|
|
6363
|
+
await this.windows.attach(context, this.launchOptions.profileDir, { headed: !this.launchOptions.headless });
|
|
6364
|
+
} catch (error) {
|
|
6365
|
+
this.context = void 0;
|
|
6366
|
+
this.windows.dropPages();
|
|
6367
|
+
await this.closeContext(context);
|
|
6368
|
+
throw error;
|
|
6369
|
+
}
|
|
6338
6370
|
context.browser()?.on("disconnected", () => {
|
|
6339
6371
|
if (this.closing || this.context !== context) return;
|
|
6340
6372
|
this.onExit();
|
|
@@ -6350,22 +6382,23 @@ let MhBrowserService = (() => {
|
|
|
6350
6382
|
this.ready = Promise.resolve();
|
|
6351
6383
|
}
|
|
6352
6384
|
/**
|
|
6353
|
-
* The browser for a call that needs a page. One that
|
|
6354
|
-
* here. The exit limit stops unattended restarts; a person's request
|
|
6355
|
-
* one start per click and skips it.
|
|
6385
|
+
* The browser for a call that needs a page. One that is not running starts
|
|
6386
|
+
* again here. The exit limit stops unattended restarts; a person's request
|
|
6387
|
+
* is one start per click and skips it.
|
|
6356
6388
|
*/
|
|
6357
6389
|
up(byPerson = false) {
|
|
6358
6390
|
if (!this.exited || this.closing) return this.ready;
|
|
6359
6391
|
const now = Date.now();
|
|
6360
6392
|
while (this.exits.length > 0 && now - (this.exits[0] ?? now) > RELAUNCH_WINDOW_MS) this.exits.shift();
|
|
6361
6393
|
if (!byPerson && this.exits.length > RELAUNCH_LIMIT) {
|
|
6362
|
-
this.ctx.logger.warn("browser keeps
|
|
6363
|
-
return Promise.reject(new MhBrowserError(BROWSER_UNAVAILABLE, `the browser
|
|
6394
|
+
this.ctx.logger.warn("browser keeps stopping; not starting it again yet");
|
|
6395
|
+
return Promise.reject(new MhBrowserError(BROWSER_UNAVAILABLE, `the browser stopped or did not start ${String(this.exits.length)} times within ${String(RELAUNCH_WINDOW_MS / 1e3)} s; try again in a minute`));
|
|
6364
6396
|
}
|
|
6365
6397
|
this.exited = false;
|
|
6366
6398
|
const started = this.launch();
|
|
6367
6399
|
started.then(() => this.windows.resumePersonInput(), () => {
|
|
6368
6400
|
this.exited = true;
|
|
6401
|
+
if (!this.closing) this.exits.push(Date.now());
|
|
6369
6402
|
});
|
|
6370
6403
|
this.ready = started;
|
|
6371
6404
|
return started;
|
|
@@ -6373,10 +6406,15 @@ let MhBrowserService = (() => {
|
|
|
6373
6406
|
async stop() {
|
|
6374
6407
|
this.windows.suppressPersonInput();
|
|
6375
6408
|
await Promise.race([this.ready.catch(() => void 0), new Promise((resolve) => setTimeout(resolve, STOP_WAIT_MS))]);
|
|
6376
|
-
const
|
|
6377
|
-
await Promise.race([this.context?.close().catch(() => void 0), new Promise((resolve) => setTimeout(resolve, 1e3))]);
|
|
6378
|
-
(browser?.process?.())?.kill("SIGKILL");
|
|
6409
|
+
const context = this.context;
|
|
6379
6410
|
this.context = void 0;
|
|
6411
|
+
if (context !== void 0) await this.closeContext(context);
|
|
6412
|
+
}
|
|
6413
|
+
/** Close a context the service is done with, and kill it if close stalls. */
|
|
6414
|
+
async closeContext(context) {
|
|
6415
|
+
const browser = context.browser();
|
|
6416
|
+
await Promise.race([context.close().catch(() => void 0), new Promise((resolve) => setTimeout(resolve, 1e3))]);
|
|
6417
|
+
(browser?.process?.())?.kill("SIGKILL");
|
|
6380
6418
|
}
|
|
6381
6419
|
refuseIfPerson(owner, tab) {
|
|
6382
6420
|
if (this.control.ofTab(owner, tab) !== "person") return;
|
|
@@ -6533,6 +6571,18 @@ function requiresOf(text, pageUrl) {
|
|
|
6533
6571
|
const read = readAgentView(text, pageUrl);
|
|
6534
6572
|
return read.ok ? read.doc.requires : [];
|
|
6535
6573
|
}
|
|
6574
|
+
function openableAddress(url) {
|
|
6575
|
+
if (url === "about:blank") return true;
|
|
6576
|
+
try {
|
|
6577
|
+
const protocol = new URL(url).protocol;
|
|
6578
|
+
return protocol === "http:" || protocol === "https:";
|
|
6579
|
+
} catch {
|
|
6580
|
+
return false;
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
function errorText(error) {
|
|
6584
|
+
return error instanceof Error ? error.message : String(error);
|
|
6585
|
+
}
|
|
6536
6586
|
function canonical(url) {
|
|
6537
6587
|
try {
|
|
6538
6588
|
const parsed = new URL(url);
|
|
@@ -6599,8 +6649,7 @@ function stopped(signal) {
|
|
|
6599
6649
|
//#region lib/types/tools/download.js
|
|
6600
6650
|
const DOWNLOAD = ["Download one file into the session workspace's downloads/ directory, then read it with bash or fs.", "Use it for a file that requires login: the browser sends the session's cookies, bash does not."].join(" ");
|
|
6601
6651
|
const URL$2 = "The file address: from a page shown to you, from the principal, or one you composed. An address that cannot be fetched returns download-error and writes nothing. An existing name under downloads/ gets a numeric suffix.";
|
|
6602
|
-
function browserDownloadTool(ctx) {
|
|
6603
|
-
const reader = logReader(ctx);
|
|
6652
|
+
function browserDownloadTool(ctx, reader) {
|
|
6604
6653
|
return defineTool({
|
|
6605
6654
|
name: "browser_download",
|
|
6606
6655
|
description: DOWNLOAD,
|
|
@@ -6682,8 +6731,7 @@ const HANDOVER = [
|
|
|
6682
6731
|
const REASON = "The sentence the person sees on the handover card, unchanged.";
|
|
6683
6732
|
const TAB = "Tab id of the human page to show the person, copied from a page element's tab attribute. Omit it to hand over without naming a tab; that does not pick the latest result. A page with an Agent View has no tab until you open it with snapshot.";
|
|
6684
6733
|
const ABORT = Symbol("abort");
|
|
6685
|
-
function browserHandoverTool(ctx) {
|
|
6686
|
-
const reader = logReader(ctx);
|
|
6734
|
+
function browserHandoverTool(ctx, reader) {
|
|
6687
6735
|
return defineTool({
|
|
6688
6736
|
name: "browser_handover",
|
|
6689
6737
|
description: HANDOVER,
|
|
@@ -6774,8 +6822,8 @@ async function askUntilAnswered(ctx, reason, exec) {
|
|
|
6774
6822
|
return await Promise.race([ctx.userQuestions.ask({
|
|
6775
6823
|
questions: [{
|
|
6776
6824
|
id: "handback",
|
|
6777
|
-
question:
|
|
6778
|
-
options: [{ label:
|
|
6825
|
+
question: handoverQuestion(reason),
|
|
6826
|
+
options: [{ label: HAND_BACK }]
|
|
6779
6827
|
}],
|
|
6780
6828
|
...exec.agent !== void 0 ? { agent: exec.agent } : {},
|
|
6781
6829
|
signal: exec.signal
|
|
@@ -6819,8 +6867,7 @@ const PAGE = "The url attribute of a page element in a tool result or a presence
|
|
|
6819
6867
|
const NAME = "Action name copied from that page's Agent View actions.";
|
|
6820
6868
|
const ITEM = "Item id copied from an item block, as a string. Required when the action's scope is item; omit it for a resource action.";
|
|
6821
6869
|
const INPUT = "Only fields declared by that action's inputSchema; omit it when the action declares none. A field with contentEncoding binary takes a path inside the session cwd or an attachment reference.";
|
|
6822
|
-
function browserInvokeTool(ctx) {
|
|
6823
|
-
const reader = logReader(ctx);
|
|
6870
|
+
function browserInvokeTool(ctx, reader) {
|
|
6824
6871
|
return defineTool({
|
|
6825
6872
|
name: "browser_invoke",
|
|
6826
6873
|
description: INVOKE,
|
|
@@ -6936,11 +6983,10 @@ const OPEN = [
|
|
|
6936
6983
|
"A page lists documents in requires; those are not fetched with it, so open their addresses to read them or to call their actions."
|
|
6937
6984
|
].join(" ");
|
|
6938
6985
|
const PAGES = "The pages to read, 1 to 8, each returned in order as its own element.";
|
|
6939
|
-
const URL$1 = "The address to read: from a page shown to you, from the principal, or one you composed; opening a composed address is how it is checked before you use it elsewhere.";
|
|
6986
|
+
const URL$1 = "The address to read: from a page shown to you, from the principal, or one you composed; opening a composed address is how it is checked before you use it elsewhere. Only an http or https address is opened.";
|
|
6940
6987
|
const WAIT = "Optional wait after the document loads for text to appear or disappear, or for time seconds; at least one must be set. If not met, the page is still returned and waited says so.";
|
|
6941
6988
|
const SNAPSHOT = "Ask for the human page: open a tab and return its snapshot, for page operations and handover. A page without an Agent View already does.";
|
|
6942
|
-
function browserOpenTool(ctx) {
|
|
6943
|
-
const reader = logReader(ctx);
|
|
6989
|
+
function browserOpenTool(ctx, reader) {
|
|
6944
6990
|
return defineTool({
|
|
6945
6991
|
name: "browser_open",
|
|
6946
6992
|
description: OPEN,
|
|
@@ -7064,8 +7110,7 @@ const PRESENCE = [
|
|
|
7064
7110
|
].join(" ");
|
|
7065
7111
|
const PUT = "Addresses to keep: from a page shown to you, a presence reminder, or the principal; at most 8. A page that cannot be read shows as absent in the next presence reminder. At least one of put and remove is required.";
|
|
7066
7112
|
const REMOVE = "Addresses to stop keeping, copied from a presence reminder; at most 8.";
|
|
7067
|
-
function browserPresenceTool(ctx) {
|
|
7068
|
-
const reader = logReader(ctx);
|
|
7113
|
+
function browserPresenceTool(ctx, reader) {
|
|
7069
7114
|
return defineTool({
|
|
7070
7115
|
name: "browser_presence",
|
|
7071
7116
|
description: PRESENCE,
|
|
@@ -7142,14 +7187,14 @@ function line(receipt) {
|
|
|
7142
7187
|
}
|
|
7143
7188
|
//#endregion
|
|
7144
7189
|
//#region lib/types/tools/register.js
|
|
7145
|
-
function registerBrowserTools(ctx) {
|
|
7190
|
+
function registerBrowserTools(ctx, reader) {
|
|
7146
7191
|
followSessionTitles(ctx);
|
|
7147
|
-
ctx.tools.register(browserOpenTool(ctx));
|
|
7148
|
-
for (const tool of pageOperationTools(ctx)) ctx.tools.register(tool);
|
|
7149
|
-
ctx.tools.register(browserInvokeTool(ctx));
|
|
7150
|
-
ctx.tools.register(browserDownloadTool(ctx));
|
|
7151
|
-
ctx.tools.register(browserPresenceTool(ctx));
|
|
7152
|
-
ctx.tools.register(browserHandoverTool(ctx));
|
|
7192
|
+
ctx.tools.register(browserOpenTool(ctx, reader));
|
|
7193
|
+
for (const tool of pageOperationTools(ctx, reader)) ctx.tools.register(tool);
|
|
7194
|
+
ctx.tools.register(browserInvokeTool(ctx, reader));
|
|
7195
|
+
ctx.tools.register(browserDownloadTool(ctx, reader));
|
|
7196
|
+
ctx.tools.register(browserPresenceTool(ctx, reader));
|
|
7197
|
+
ctx.tools.register(browserHandoverTool(ctx, reader));
|
|
7153
7198
|
}
|
|
7154
7199
|
//#endregion
|
|
7155
7200
|
//#region lib/types/index.js
|
|
@@ -7158,10 +7203,11 @@ const inject = [
|
|
|
7158
7203
|
"tools",
|
|
7159
7204
|
"userQuestions",
|
|
7160
7205
|
"attachments",
|
|
7161
|
-
"sessionProjections"
|
|
7206
|
+
"sessionProjections",
|
|
7207
|
+
"agents"
|
|
7162
7208
|
];
|
|
7163
7209
|
const Config = browserConfig;
|
|
7164
|
-
function apply(ctx, config) {
|
|
7210
|
+
async function apply(ctx, config) {
|
|
7165
7211
|
ctx.plugin({
|
|
7166
7212
|
name: "mh-browser-slot",
|
|
7167
7213
|
inject: ["browserUse", "mhBrowser"],
|
|
@@ -7169,14 +7215,14 @@ function apply(ctx, config) {
|
|
|
7169
7215
|
inner.browserUse.register("mh-browser");
|
|
7170
7216
|
}
|
|
7171
7217
|
});
|
|
7172
|
-
ctx.plugin(MhBrowserService, config);
|
|
7173
|
-
ctx.plugin({
|
|
7218
|
+
await ctx.plugin(MhBrowserService, config);
|
|
7219
|
+
await ctx.plugin({
|
|
7174
7220
|
name: "mh-browser-runtime",
|
|
7175
7221
|
inject: ["mhBrowser", ...inject],
|
|
7176
|
-
apply(inner) {
|
|
7177
|
-
registerProjections(inner);
|
|
7178
|
-
registerBrowserTools(inner);
|
|
7179
|
-
registerPresence(inner);
|
|
7222
|
+
async apply(inner) {
|
|
7223
|
+
const reader = await registerProjections(inner);
|
|
7224
|
+
registerBrowserTools(inner, reader);
|
|
7225
|
+
registerPresence(inner, reader);
|
|
7180
7226
|
}
|
|
7181
7227
|
});
|
|
7182
7228
|
}
|