@mind-harness/mh-browser 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extension/manifest.json +1 -1
- package/lib/index.js +674 -680
- 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/browser.d.ts +7 -0
- 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/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";
|
|
@@ -16,502 +16,21 @@ import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
|
|
|
16
16
|
//#region lib/types/config.js
|
|
17
17
|
const browserConfig = z.object({
|
|
18
18
|
presence: z.object({ maxBytes: z.number().step(1).min(1).required().description("Most bytes of page text one presence reminder may carry.") }).required().description("Pages kept in context across steps."),
|
|
19
|
-
browser: z.union([
|
|
20
|
-
"chrome",
|
|
21
|
-
"msedge",
|
|
22
|
-
"chromium"
|
|
23
|
-
]).default("chrome").description("Which installed browser to drive. chromium is the one Playwright downloads."),
|
|
24
|
-
profileDir: z.string().description("Browser profile directory, kept between runs. Defaults to mh/profiles/<dsh profile>/browser under the dsh home."),
|
|
25
|
-
headless: z.boolean().default(false).description("Run the browser without a window.")
|
|
26
|
-
});
|
|
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
|
-
}
|
|
19
|
+
browser: z.union([
|
|
20
|
+
"chrome",
|
|
21
|
+
"msedge",
|
|
22
|
+
"chromium"
|
|
23
|
+
]).default("chrome").description("Which installed browser to drive. chromium is the one Playwright downloads."),
|
|
24
|
+
profileDir: z.string().description("Browser profile directory, kept between runs. Defaults to mh/profiles/<dsh profile>/browser under the dsh home."),
|
|
25
|
+
headless: z.boolean().default(false).description("Run the browser without a window.")
|
|
26
|
+
});
|
|
464
27
|
/**
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
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
|
|
@@ -906,25 +425,272 @@ function pageBody(view) {
|
|
|
906
425
|
if (view.status !== void 0) return `The site answered HTTP ${String(view.status)}; the ${view.mediaType} body is not shown.`;
|
|
907
426
|
return "Not an HTML page; use browser_download to save it into the workspace.";
|
|
908
427
|
}
|
|
909
|
-
function versionOf$1(view) {
|
|
910
|
-
const representation = view.representation;
|
|
911
|
-
if (representation.kind === "agent-view") return representation.version;
|
|
912
|
-
return representation.agentView?.version;
|
|
428
|
+
function versionOf$1(view) {
|
|
429
|
+
const representation = view.representation;
|
|
430
|
+
if (representation.kind === "agent-view") return representation.version;
|
|
431
|
+
return representation.agentView?.version;
|
|
432
|
+
}
|
|
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
|
+
/** Public browser types. Playwright types stay inside the service. */
|
|
451
|
+
var MhBrowserError = class extends Error {
|
|
452
|
+
code;
|
|
453
|
+
page;
|
|
454
|
+
constructor(code, message, page) {
|
|
455
|
+
super(message);
|
|
456
|
+
this.name = "MhBrowserError";
|
|
457
|
+
this.code = code;
|
|
458
|
+
if (page !== void 0) this.page = page;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
/** Owner of the person's own window. It belongs to no Session. */
|
|
462
|
+
const PERSON_OWNER = "person";
|
|
463
|
+
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
464
|
+
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
465
|
+
/**
|
|
466
|
+
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
467
|
+
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
468
|
+
*/
|
|
469
|
+
const INTERRUPTED = "INTERRUPTED";
|
|
470
|
+
/** The browser did not start or could not be restarted. */
|
|
471
|
+
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
472
|
+
function unknownTab() {
|
|
473
|
+
return {
|
|
474
|
+
url: "",
|
|
475
|
+
reason: UNKNOWN_TAB_REASON
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region lib/types/tools/session.js
|
|
480
|
+
/** dsh shows the model only the message, so the message starts with the code. */
|
|
481
|
+
var BrowserToolError = class extends HarnessError {
|
|
482
|
+
reason;
|
|
483
|
+
constructor(code, reason) {
|
|
484
|
+
super(`${code}: ${reason}`, code);
|
|
485
|
+
this.name = "BrowserToolError";
|
|
486
|
+
this.reason = reason;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
const NO_WORKSPACE_REASON$1 = "this session has no working directory";
|
|
490
|
+
/**
|
|
491
|
+
* The session of this tool call. Also records its title for the tab group.
|
|
492
|
+
* No title service, or no title yet, records an empty one.
|
|
493
|
+
*/
|
|
494
|
+
function sessionFor(ctx, exec) {
|
|
495
|
+
const session = sessionOf(exec);
|
|
496
|
+
const titles = ctx.get("sessionTitle");
|
|
497
|
+
ctx.mhBrowser.noteSessionTitle(session.id, titles?.get(session.session)?.title ?? "");
|
|
498
|
+
return session;
|
|
499
|
+
}
|
|
500
|
+
/** A title written after the last tool call still reaches the tab group. */
|
|
501
|
+
function followSessionTitles(ctx) {
|
|
502
|
+
ctx.on("session/event", (session, event) => {
|
|
503
|
+
const { type, data } = event;
|
|
504
|
+
if (type !== "session/title" || typeof data !== "object" || data === null) return;
|
|
505
|
+
const title = data.title;
|
|
506
|
+
if (typeof title === "string") ctx.mhBrowser.noteSessionTitle(session.id, title);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function sessionOf(exec) {
|
|
510
|
+
const session = exec.agent?.session;
|
|
511
|
+
if (session === void 0) throw new BrowserToolError("NO_SESSION", "browser tools run only inside an agent session");
|
|
512
|
+
const cwd = session.header.cwd;
|
|
513
|
+
return cwd === void 0 ? {
|
|
514
|
+
id: session.id,
|
|
515
|
+
session
|
|
516
|
+
} : {
|
|
517
|
+
id: session.id,
|
|
518
|
+
cwd,
|
|
519
|
+
session
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Where this session may write a download. With dsh's sandbox policy loaded,
|
|
524
|
+
* its mode decides: `read-only` writes nothing, otherwise the policy's
|
|
525
|
+
* workspace root. Without it, the session cwd.
|
|
526
|
+
*/
|
|
527
|
+
function writeTarget(ctx, session) {
|
|
528
|
+
const policy = ctx.get("sandboxPolicy");
|
|
529
|
+
if (policy === void 0) return session.cwd === void 0 ? {
|
|
530
|
+
denied: NO_WORKSPACE_REASON$1,
|
|
531
|
+
code: "NO_WORKSPACE"
|
|
532
|
+
} : { root: session.cwd };
|
|
533
|
+
const resolved = policy.resolve({ session: session.session });
|
|
534
|
+
if (resolved.mode === "read-only") return {
|
|
535
|
+
denied: "[sandbox: file access denied under read-only mode]",
|
|
536
|
+
code: "SANDBOX_DENIED"
|
|
537
|
+
};
|
|
538
|
+
return { root: resolved.workspaceRoot };
|
|
539
|
+
}
|
|
540
|
+
function rethrow(error, detail) {
|
|
541
|
+
if (error instanceof MhBrowserError || error instanceof BrowserToolError) {
|
|
542
|
+
const reason = error instanceof BrowserToolError ? error.reason : error.message;
|
|
543
|
+
throw new BrowserToolError(error.code, detail === void 0 ? reason : `${reason}\n${detail}`);
|
|
544
|
+
}
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region lib/types/tools/paths.js
|
|
549
|
+
/** Resolve upload paths and reject anything outside the session's cwd. */
|
|
550
|
+
async function filesInWorkspace(cwd, files) {
|
|
551
|
+
if (cwd === void 0) throw new BrowserToolError("NO_WORKSPACE", NO_WORKSPACE_REASON$1);
|
|
552
|
+
const root = await realpath(cwd);
|
|
553
|
+
const resolved = [];
|
|
554
|
+
for (const file of files) {
|
|
555
|
+
const abs = isAbsolute(file) ? file : resolve(cwd, file);
|
|
556
|
+
let real;
|
|
557
|
+
try {
|
|
558
|
+
real = await realpath(abs);
|
|
559
|
+
} catch {
|
|
560
|
+
throw new BrowserToolError("INVALID_ARGS", `no such file: ${file}`);
|
|
561
|
+
}
|
|
562
|
+
const fromRoot = relative(root, real);
|
|
563
|
+
if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${file} is outside the session workspace`);
|
|
564
|
+
resolved.push(real);
|
|
565
|
+
}
|
|
566
|
+
return resolved;
|
|
567
|
+
}
|
|
568
|
+
/** Refuse a filename that leaves the session's write root, without creating it. */
|
|
569
|
+
async function assertInside(target, filename) {
|
|
570
|
+
await place(target, filename);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Write `filename` under the session's write root. A path that leaves the
|
|
574
|
+
* root is refused, through a symbolic link as well: the deepest existing
|
|
575
|
+
* directory is checked before any is created, and the file itself is not
|
|
576
|
+
* followed if it is a link.
|
|
577
|
+
*/
|
|
578
|
+
async function writeInside(target, filename, data) {
|
|
579
|
+
const file = await place(target, filename);
|
|
580
|
+
assertUnder(file.root, await realpath(await deepestExisting(dirname(file.file))), filename);
|
|
581
|
+
await mkdir(dirname(file.file), { recursive: true });
|
|
582
|
+
const parent = await realpath(dirname(file.file));
|
|
583
|
+
assertUnder(file.root, parent, filename);
|
|
584
|
+
const placed = resolve(parent, basename(file.file));
|
|
585
|
+
let handle;
|
|
586
|
+
try {
|
|
587
|
+
handle = await open(placed, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (error.code === "ELOOP") throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
await handle.writeFile(data);
|
|
594
|
+
} finally {
|
|
595
|
+
await handle.close();
|
|
596
|
+
}
|
|
597
|
+
const bytes = typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
|
|
598
|
+
return {
|
|
599
|
+
path: relative(file.root, placed),
|
|
600
|
+
bytes
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function assertUnder(root, path, filename) {
|
|
604
|
+
const fromRoot = relative(root, path);
|
|
605
|
+
if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
606
|
+
}
|
|
607
|
+
async function deepestExisting(path) {
|
|
608
|
+
for (let current = path;; current = dirname(current)) try {
|
|
609
|
+
await lstat(current);
|
|
610
|
+
return current;
|
|
611
|
+
} catch {
|
|
612
|
+
if (dirname(current) === current) return current;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async function place(target, filename) {
|
|
616
|
+
if ("denied" in target) throw new BrowserToolError(target.code, target.denied);
|
|
617
|
+
if (filename.trim() === "") throw new BrowserToolError("INVALID_ARGS", "filename: give a path inside the workspace");
|
|
618
|
+
const root = await realpath(target.root).catch(() => target.root);
|
|
619
|
+
const abs = isAbsolute(filename) ? filename : resolve(root, filename);
|
|
620
|
+
const direct = relative(root, abs);
|
|
621
|
+
if (direct.startsWith("..") || isAbsolute(direct)) throw new BrowserToolError("OUTSIDE_WORKSPACE", `${filename} is outside the session workspace`);
|
|
622
|
+
return {
|
|
623
|
+
root,
|
|
624
|
+
file: abs
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region lib/types/tools/attachments.js
|
|
629
|
+
function initAttachments() {
|
|
630
|
+
return { refs: {} };
|
|
913
631
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
632
|
+
/** Attachments reach the model in user messages and tool results. */
|
|
633
|
+
function applyAttachments(state, event) {
|
|
634
|
+
if (event.type !== "user/message" && event.type !== "tool/result") return state;
|
|
635
|
+
const found = {};
|
|
636
|
+
visit(event.data, 0, found);
|
|
637
|
+
const ids = Object.keys(found).filter((id) => state.refs[id] === void 0);
|
|
638
|
+
if (ids.length === 0) return state;
|
|
639
|
+
const refs = { ...state.refs };
|
|
640
|
+
for (const id of ids) refs[id] = found[id];
|
|
641
|
+
return { refs };
|
|
917
642
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
643
|
+
/**
|
|
644
|
+
* Host paths for upload values. A value naming an attachment that appeared in
|
|
645
|
+
* this session resolves through the attachment provider; anything else is a
|
|
646
|
+
* path that must resolve inside the session cwd.
|
|
647
|
+
*/
|
|
648
|
+
async function uploadPaths(ctx, seen, cwd, values) {
|
|
649
|
+
const paths = [];
|
|
650
|
+
for (const value of values) {
|
|
651
|
+
const id = attachmentIdOf(value);
|
|
652
|
+
const found = id === void 0 ? void 0 : seen.refs[id];
|
|
653
|
+
if (found !== void 0) {
|
|
654
|
+
paths.push(hostPath(ctx, found));
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (typeof value !== "string") throw new BrowserToolError("INVALID_INPUT", "attachment reference is not in this session");
|
|
658
|
+
paths.push(...await filesInWorkspace(cwd, [value]));
|
|
659
|
+
}
|
|
660
|
+
return paths;
|
|
921
661
|
}
|
|
922
|
-
function
|
|
923
|
-
|
|
662
|
+
function hostPath(ctx, found) {
|
|
663
|
+
let host;
|
|
664
|
+
try {
|
|
665
|
+
host = found.kind === "image" ? ctx.attachments.imageHostPath(found.ref) : ctx.attachments.fileHostPath(found.ref);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
throw new BrowserToolError("INVALID_INPUT", error instanceof Error ? error.message : String(error));
|
|
668
|
+
}
|
|
669
|
+
if (host === void 0) throw new BrowserToolError("INVALID_INPUT", "attachment has no host path");
|
|
670
|
+
return host;
|
|
924
671
|
}
|
|
925
|
-
function
|
|
926
|
-
|
|
927
|
-
|
|
672
|
+
function attachmentIdOf(value) {
|
|
673
|
+
if (typeof value === "string") return value;
|
|
674
|
+
if (value !== null && typeof value === "object") {
|
|
675
|
+
const id = value.attachmentId;
|
|
676
|
+
if (typeof id === "string") return id;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function visit(value, depth, found) {
|
|
680
|
+
if (depth > 12 || value === null || typeof value !== "object") return;
|
|
681
|
+
if (Array.isArray(value)) {
|
|
682
|
+
for (const item of value) visit(item, depth + 1, found);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const row = value;
|
|
686
|
+
if (typeof row.attachmentId === "string" && typeof row.bytes === "number") {
|
|
687
|
+
const kind = typeof row.width === "number" && typeof row.mediaType === "string" ? "image" : "file";
|
|
688
|
+
if (kind === "image" || typeof row.name === "string") found[row.attachmentId] = {
|
|
689
|
+
kind,
|
|
690
|
+
ref: row
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
for (const item of Object.values(row)) visit(item, depth + 1, found);
|
|
928
694
|
}
|
|
929
695
|
//#endregion
|
|
930
696
|
//#region lib/types/tools/delivery.js
|
|
@@ -1027,18 +793,18 @@ function urlsIn(text) {
|
|
|
1027
793
|
return [...new Set(found)];
|
|
1028
794
|
}
|
|
1029
795
|
function fromTool(event) {
|
|
1030
|
-
const data = record$
|
|
1031
|
-
const message = record$
|
|
796
|
+
const data = record$3(event.data);
|
|
797
|
+
const message = record$3(data.message);
|
|
1032
798
|
const callId = readString(message.toolCallId);
|
|
1033
|
-
const text = textOf$
|
|
799
|
+
const text = textOf$3(message.content);
|
|
1034
800
|
if (message.isError === true) return void 0;
|
|
1035
|
-
const meta = record$
|
|
801
|
+
const meta = record$3(data.meta);
|
|
1036
802
|
if (meta.by !== "mh-browser") return void 0;
|
|
1037
803
|
const pages = meta.pages;
|
|
1038
804
|
if (!Array.isArray(pages)) return void 0;
|
|
1039
805
|
const deliveries = [];
|
|
1040
806
|
for (const page of pages) {
|
|
1041
|
-
const row = record$
|
|
807
|
+
const row = record$3(page);
|
|
1042
808
|
if (row.representation === "header" || readString(row.reason) !== void 0) continue;
|
|
1043
809
|
const url = readString(row.url);
|
|
1044
810
|
if (url === void 0) continue;
|
|
@@ -1064,21 +830,21 @@ function fromTool(event) {
|
|
|
1064
830
|
};
|
|
1065
831
|
}
|
|
1066
832
|
function fromPresence(event) {
|
|
1067
|
-
const message = record$
|
|
1068
|
-
const source = record$
|
|
833
|
+
const message = record$3(event.data);
|
|
834
|
+
const source = record$3(message.source);
|
|
1069
835
|
if (source.kind !== "mh-browser") return void 0;
|
|
1070
|
-
const entries = record$
|
|
836
|
+
const entries = record$3(source.presence).entries;
|
|
1071
837
|
if (!Array.isArray(entries)) return void 0;
|
|
1072
838
|
const deliveries = [];
|
|
1073
839
|
for (const entry of entries) {
|
|
1074
|
-
const row = record$
|
|
840
|
+
const row = record$3(entry);
|
|
1075
841
|
if (row.op === "removed" || row.op === "absent") continue;
|
|
1076
842
|
const url = readString(row.url);
|
|
1077
843
|
if (url === void 0) continue;
|
|
1078
844
|
const digest = readString(row.digest);
|
|
1079
845
|
const version = readString(row.version);
|
|
1080
846
|
const id = readString(row.id);
|
|
1081
|
-
const body = digest === void 0 ? void 0 : shownBody(textOf$
|
|
847
|
+
const body = digest === void 0 ? void 0 : shownBody(textOf$3(message.content), url);
|
|
1082
848
|
deliveries.push({
|
|
1083
849
|
url,
|
|
1084
850
|
seq: event.seq,
|
|
@@ -1091,7 +857,7 @@ function fromPresence(event) {
|
|
|
1091
857
|
}
|
|
1092
858
|
return {
|
|
1093
859
|
deliveries,
|
|
1094
|
-
text: textOf$
|
|
860
|
+
text: textOf$3(message.content)
|
|
1095
861
|
};
|
|
1096
862
|
}
|
|
1097
863
|
const OMITTED_BODY = /* @__PURE__ */ new Set([
|
|
@@ -1136,11 +902,11 @@ function attrOf(header, name) {
|
|
|
1136
902
|
if (value === void 0 || value === "") return void 0;
|
|
1137
903
|
return value.replace(/"/g, "\"").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, "\n").replace(/ /g, "\r").replace(/	/g, " ").replace(/&/g, "&");
|
|
1138
904
|
}
|
|
1139
|
-
function textOf$
|
|
905
|
+
function textOf$3(content) {
|
|
1140
906
|
if (!Array.isArray(content)) return "";
|
|
1141
907
|
const parts = [];
|
|
1142
908
|
for (const block of content) {
|
|
1143
|
-
const text = readString(record$
|
|
909
|
+
const text = readString(record$3(block).text);
|
|
1144
910
|
if (text !== void 0) parts.push(text);
|
|
1145
911
|
}
|
|
1146
912
|
return parts.join("\n");
|
|
@@ -1157,7 +923,7 @@ function sameUrl(left, right) {
|
|
|
1157
923
|
return false;
|
|
1158
924
|
}
|
|
1159
925
|
}
|
|
1160
|
-
function record$
|
|
926
|
+
function record$3(value) {
|
|
1161
927
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
1162
928
|
return {};
|
|
1163
929
|
}
|
|
@@ -1172,9 +938,9 @@ function initTabLog() {
|
|
|
1172
938
|
function applyTabLog(state, event) {
|
|
1173
939
|
const cleared = event.type === "compaction/summary" || event.type === "compaction/prune" || event.surfaceOp === "replace";
|
|
1174
940
|
if (event.type !== "tool/result") return cleared ? withoutSnap(state) : state;
|
|
1175
|
-
const meta = record$
|
|
941
|
+
const meta = record$2(record$2(event.data).meta);
|
|
1176
942
|
if (meta.by !== "mh-browser") return cleared ? withoutSnap(state) : state;
|
|
1177
|
-
const named = Array.isArray(meta.pages) ? meta.pages.map((page) => record$
|
|
943
|
+
const named = Array.isArray(meta.pages) ? meta.pages.map((page) => record$2(page).tab).filter(isTabId) : [];
|
|
1178
944
|
const closed = Array.isArray(meta.closed) ? meta.closed.filter((note) => typeof note === "string").map(tabOfNote) : [];
|
|
1179
945
|
const next = new Set(state.open);
|
|
1180
946
|
for (const tab of named) next.add(tab);
|
|
@@ -1225,11 +991,11 @@ function withoutSnap(state) {
|
|
|
1225
991
|
return rest;
|
|
1226
992
|
}
|
|
1227
993
|
function snapOf(value) {
|
|
1228
|
-
const snap = record$
|
|
994
|
+
const snap = record$2(value);
|
|
1229
995
|
if (!Array.isArray(snap.tabs)) return void 0;
|
|
1230
996
|
return {
|
|
1231
997
|
tabs: snap.tabs.flatMap((item) => {
|
|
1232
|
-
const row = record$
|
|
998
|
+
const row = record$2(item);
|
|
1233
999
|
if (typeof row.id !== "string" || typeof row.url !== "string") return [];
|
|
1234
1000
|
return [{
|
|
1235
1001
|
id: row.id,
|
|
@@ -1242,71 +1008,10 @@ function snapOf(value) {
|
|
|
1242
1008
|
closed: Array.isArray(snap.closed) ? snap.closed.filter((note) => typeof note === "string") : []
|
|
1243
1009
|
};
|
|
1244
1010
|
}
|
|
1245
|
-
function record$
|
|
1011
|
+
function record$2(value) {
|
|
1246
1012
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1247
1013
|
}
|
|
1248
1014
|
//#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
1015
|
//#region lib/types/tools/page-ops.js
|
|
1311
1016
|
const IMAGE_VALUE = {
|
|
1312
1017
|
type: "object",
|
|
@@ -1805,8 +1510,7 @@ const OPS = [
|
|
|
1805
1510
|
})
|
|
1806
1511
|
}
|
|
1807
1512
|
];
|
|
1808
|
-
function pageOperationTools(ctx) {
|
|
1809
|
-
const reader = logReader(ctx);
|
|
1513
|
+
function pageOperationTools(ctx, reader) {
|
|
1810
1514
|
return OPS.map((spec) => defineOperation(ctx, reader, spec));
|
|
1811
1515
|
}
|
|
1812
1516
|
function defineOperation(ctx, reader, spec) {
|
|
@@ -1877,9 +1581,9 @@ function defineOperation(ctx, reader, spec) {
|
|
|
1877
1581
|
}
|
|
1878
1582
|
/** Probe the path before the action, then write the result the model would have been shown. */
|
|
1879
1583
|
async function saveResult(target, filename, view) {
|
|
1880
|
-
return writeInside(target, filename, view.shot?.bytes ?? textOf$
|
|
1584
|
+
return writeInside(target, filename, view.shot?.bytes ?? textOf$2(view));
|
|
1881
1585
|
}
|
|
1882
|
-
function textOf$
|
|
1586
|
+
function textOf$2(view) {
|
|
1883
1587
|
if (view.summary !== void 0 && view.summary !== "") return view.detail === void 0 || view.detail === "" ? view.summary : `${view.summary}\n${view.detail}`;
|
|
1884
1588
|
return view.representation.text;
|
|
1885
1589
|
}
|
|
@@ -1892,72 +1596,306 @@ function tabOf(reader, session, tab) {
|
|
|
1892
1596
|
function string(value) {
|
|
1893
1597
|
return typeof value === "string" ? value : "";
|
|
1894
1598
|
}
|
|
1895
|
-
function stringList$1(value) {
|
|
1896
|
-
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1599
|
+
function stringList$1(value) {
|
|
1600
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1601
|
+
}
|
|
1602
|
+
function optionalString(args, key) {
|
|
1603
|
+
return typeof args[key] === "string" ? { element: args[key] } : {};
|
|
1604
|
+
}
|
|
1605
|
+
function optional(key, value) {
|
|
1606
|
+
return typeof value === "string" ? { [key]: value } : {};
|
|
1607
|
+
}
|
|
1608
|
+
function fieldsOf(value) {
|
|
1609
|
+
if (!Array.isArray(value)) return [];
|
|
1610
|
+
return value.flatMap((item) => {
|
|
1611
|
+
if (item === null || typeof item !== "object") return [];
|
|
1612
|
+
const row = item;
|
|
1613
|
+
const type = row.type;
|
|
1614
|
+
if (type !== "textbox" && type !== "checkbox" && type !== "radio" && type !== "combobox" && type !== "slider") return [];
|
|
1615
|
+
return [{
|
|
1616
|
+
target: string(row.target),
|
|
1617
|
+
...typeof row.element === "string" ? { element: row.element } : {},
|
|
1618
|
+
name: string(row.name),
|
|
1619
|
+
type,
|
|
1620
|
+
value: string(row.value)
|
|
1621
|
+
}];
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
function callIdOf(exec) {
|
|
1625
|
+
return String(exec.callId);
|
|
1626
|
+
}
|
|
1627
|
+
function imageValue(ref) {
|
|
1628
|
+
return {
|
|
1629
|
+
attachmentId: ref.attachmentId,
|
|
1630
|
+
mediaType: ref.mediaType,
|
|
1631
|
+
bytes: ref.bytes,
|
|
1632
|
+
width: ref.width,
|
|
1633
|
+
height: ref.height,
|
|
1634
|
+
...ref.name === void 0 ? {} : { name: ref.name },
|
|
1635
|
+
...ref.originalDimensions === void 0 ? {} : { originalDimensions: { ...ref.originalDimensions } }
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function imageRef(value) {
|
|
1639
|
+
return {
|
|
1640
|
+
attachmentId: value.attachmentId,
|
|
1641
|
+
mediaType: value.mediaType,
|
|
1642
|
+
bytes: value.bytes,
|
|
1643
|
+
width: value.width,
|
|
1644
|
+
height: value.height,
|
|
1645
|
+
...value.name === void 0 ? {} : { name: value.name },
|
|
1646
|
+
...value.originalDimensions === void 0 ? {} : { originalDimensions: { ...value.originalDimensions } }
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
async function saveShot(ctx, view) {
|
|
1650
|
+
const shot = view.shot;
|
|
1651
|
+
if (shot === void 0) return void 0;
|
|
1652
|
+
try {
|
|
1653
|
+
return await ctx.attachments.saveImage({
|
|
1654
|
+
data: shot.bytes,
|
|
1655
|
+
mediaType: shot.mediaType,
|
|
1656
|
+
name: `${view.tab ?? "page"}.${shot.mediaType.slice(6)}`
|
|
1657
|
+
});
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
ctx.logger.warn("saving the screenshot failed: %o", error);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function isModifier(value) {
|
|
1664
|
+
return value === "Alt" || value === "Control" || value === "ControlOrMeta" || value === "Meta" || value === "Shift";
|
|
1665
|
+
}
|
|
1666
|
+
//#endregion
|
|
1667
|
+
//#region lib/types/presence/record.js
|
|
1668
|
+
/** Structured presence record carried on a message source. The model sees the rendered text, not this. */
|
|
1669
|
+
const PRESENCE_SOURCES = [
|
|
1670
|
+
"person via sidebar",
|
|
1671
|
+
"cwd reference",
|
|
1672
|
+
"model"
|
|
1673
|
+
];
|
|
1674
|
+
//#endregion
|
|
1675
|
+
//#region lib/types/presence/fold.js
|
|
1676
|
+
function initPresenceLog() {
|
|
1677
|
+
return {
|
|
1678
|
+
committed: [],
|
|
1679
|
+
inbox: {
|
|
1680
|
+
"next-step": [],
|
|
1681
|
+
"next-turn": []
|
|
1682
|
+
},
|
|
1683
|
+
discarded: [],
|
|
1684
|
+
visible: false,
|
|
1685
|
+
hidden: false,
|
|
1686
|
+
shown: [],
|
|
1687
|
+
ops: []
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
function applyPresenceLog(state, event) {
|
|
1691
|
+
if (event.type === "compaction/summary") {
|
|
1692
|
+
if (!state.visible && state.shown.length === 0) return state;
|
|
1693
|
+
return {
|
|
1694
|
+
...state,
|
|
1695
|
+
visible: false,
|
|
1696
|
+
hidden: true,
|
|
1697
|
+
shown: []
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
if (event.type === "user/message") {
|
|
1701
|
+
const carried = carriedOf(event.data, `history:${String(event.seq)}`);
|
|
1702
|
+
if (carried === void 0) return state;
|
|
1703
|
+
return {
|
|
1704
|
+
...state,
|
|
1705
|
+
committed: applyRecord(state.committed, carried.record, "committed"),
|
|
1706
|
+
inbox: {
|
|
1707
|
+
"next-step": state.inbox["next-step"].map((item) => item?.id === carried.id ? null : item),
|
|
1708
|
+
"next-turn": state.inbox["next-turn"].map((item) => item?.id === carried.id ? null : item)
|
|
1709
|
+
},
|
|
1710
|
+
discarded: [],
|
|
1711
|
+
visible: true,
|
|
1712
|
+
hidden: false,
|
|
1713
|
+
shown: [...state.shown, textDigest(event.data)],
|
|
1714
|
+
ops: []
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
if (event.type === "agent/inbox/discarded") {
|
|
1718
|
+
const carried = carriedOf(event.data, `discarded:${String(event.seq)}`);
|
|
1719
|
+
if (carried === void 0) return state;
|
|
1720
|
+
return {
|
|
1721
|
+
...state,
|
|
1722
|
+
discarded: [...state.discarded, carried]
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
if (event.type === "agent/inbox/spliced") return applySplice(state, event);
|
|
1726
|
+
if (event.type === "tool/result") return applyReceipts(state, event);
|
|
1727
|
+
return state;
|
|
1728
|
+
}
|
|
1729
|
+
/** The folded set, with pending reminders and unannounced receipts on top. */
|
|
1730
|
+
function presenceSet(state) {
|
|
1731
|
+
let pages = state.committed;
|
|
1732
|
+
for (const carried of [
|
|
1733
|
+
...state.inbox["next-turn"],
|
|
1734
|
+
...state.inbox["next-step"],
|
|
1735
|
+
...state.discarded
|
|
1736
|
+
]) if (carried !== null) pages = applyRecord(pages, carried.record, "pending");
|
|
1737
|
+
const removals = [];
|
|
1738
|
+
for (const op of state.ops) {
|
|
1739
|
+
const index = pages.findIndex((page) => page.url === op.url);
|
|
1740
|
+
if (op.op === "remove") {
|
|
1741
|
+
if (index < 0) continue;
|
|
1742
|
+
pages = pages.toSpliced(index, 1);
|
|
1743
|
+
removals.push(op);
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
if (index >= 0) continue;
|
|
1747
|
+
pages = [...pages, {
|
|
1748
|
+
url: op.url,
|
|
1749
|
+
source: "model",
|
|
1750
|
+
state: "pending",
|
|
1751
|
+
order: pages.length
|
|
1752
|
+
}];
|
|
1753
|
+
}
|
|
1754
|
+
return {
|
|
1755
|
+
pages,
|
|
1756
|
+
removals
|
|
1757
|
+
};
|
|
1897
1758
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1759
|
+
/** The text of a reminder, as `shown` records it. */
|
|
1760
|
+
function textDigest(message) {
|
|
1761
|
+
return createHash("sha256").update(textOf$1(message)).digest("hex");
|
|
1900
1762
|
}
|
|
1901
|
-
function
|
|
1902
|
-
|
|
1763
|
+
function applySplice(state, event) {
|
|
1764
|
+
const data = record$1(event.data);
|
|
1765
|
+
const target = data.target === "next-turn" ? "next-turn" : "next-step";
|
|
1766
|
+
const start = typeof data.start === "number" ? data.start : 0;
|
|
1767
|
+
const removedCount = typeof data.removedCount === "number" ? data.removedCount : 0;
|
|
1768
|
+
const slots = (Array.isArray(data.inserted) ? data.inserted : []).map((item, index) => carriedOf(item, `splice:${String(event.seq)}:${String(index)}`));
|
|
1769
|
+
const list = state.inbox[target];
|
|
1770
|
+
const removed = list.slice(start, start + removedCount).filter((item) => item !== null);
|
|
1771
|
+
if (removedCount === 0 && slots.length === 0) return state;
|
|
1772
|
+
const next = [
|
|
1773
|
+
...list.slice(0, start),
|
|
1774
|
+
...slots.map((slot) => slot ?? null),
|
|
1775
|
+
...list.slice(start + removedCount)
|
|
1776
|
+
];
|
|
1777
|
+
const announced = slots.some((slot) => slot !== void 0);
|
|
1778
|
+
return {
|
|
1779
|
+
...state,
|
|
1780
|
+
inbox: {
|
|
1781
|
+
...state.inbox,
|
|
1782
|
+
[target]: next
|
|
1783
|
+
},
|
|
1784
|
+
discarded: data.outcome === "canceled" ? [...state.discarded, ...removed] : state.discarded,
|
|
1785
|
+
ops: announced ? [] : state.ops
|
|
1786
|
+
};
|
|
1903
1787
|
}
|
|
1904
|
-
function
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1788
|
+
function applyReceipts(state, event) {
|
|
1789
|
+
const data = record$1(event.data);
|
|
1790
|
+
if (record$1(data.message).isError === true) return state;
|
|
1791
|
+
const meta = record$1(data.meta);
|
|
1792
|
+
if (meta.by !== "mh-browser" || meta.action !== "presence" || !Array.isArray(meta.receipts)) return state;
|
|
1793
|
+
const ops = [];
|
|
1794
|
+
for (const item of meta.receipts) {
|
|
1795
|
+
const row = record$1(item);
|
|
1796
|
+
if (typeof row.url !== "string" || row.url === "") continue;
|
|
1797
|
+
if (row.receipt === "added (pending)") ops.push({
|
|
1798
|
+
op: "put",
|
|
1799
|
+
url: row.url
|
|
1800
|
+
});
|
|
1801
|
+
if (row.receipt === "removed (pending)") ops.push({
|
|
1802
|
+
op: "remove",
|
|
1803
|
+
url: row.url
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
return ops.length === 0 ? state : {
|
|
1807
|
+
...state,
|
|
1808
|
+
ops: [...state.ops, ...ops]
|
|
1809
|
+
};
|
|
1919
1810
|
}
|
|
1920
|
-
function
|
|
1921
|
-
|
|
1811
|
+
function applyRecord(pages, record, state) {
|
|
1812
|
+
if (record.kind === "baseline") {
|
|
1813
|
+
const next = [];
|
|
1814
|
+
for (const entry of record.entries) {
|
|
1815
|
+
if (entry.op === "removed") continue;
|
|
1816
|
+
next.push(heldFrom(entry, state, next.length));
|
|
1817
|
+
}
|
|
1818
|
+
return next;
|
|
1819
|
+
}
|
|
1820
|
+
const next = pages.map((page) => ({ ...page }));
|
|
1821
|
+
for (const entry of record.entries) {
|
|
1822
|
+
if (entry.op === "removed") {
|
|
1823
|
+
const index = next.findIndex((page) => page.url === entry.url);
|
|
1824
|
+
if (index >= 0) next.splice(index, 1);
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
const index = next.findIndex((page) => page.url === entry.url);
|
|
1828
|
+
const held = heldFrom(entry, state, index >= 0 ? next[index]?.order ?? index : next.length);
|
|
1829
|
+
if (index >= 0) next[index] = held;
|
|
1830
|
+
else next.push(held);
|
|
1831
|
+
}
|
|
1832
|
+
return next;
|
|
1922
1833
|
}
|
|
1923
|
-
function
|
|
1834
|
+
function heldFrom(entry, state, order) {
|
|
1835
|
+
const absent = entry.op === "absent";
|
|
1924
1836
|
return {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1837
|
+
url: entry.url,
|
|
1838
|
+
source: entry.source,
|
|
1839
|
+
state,
|
|
1840
|
+
order,
|
|
1841
|
+
seen: absent ? { absent: entry.reason ?? "" } : {
|
|
1842
|
+
digest: entry.digest,
|
|
1843
|
+
observedAt: entry.observedAt,
|
|
1844
|
+
...entry.version === void 0 ? {} : { version: entry.version },
|
|
1845
|
+
...entry.id === void 0 ? {} : { id: entry.id }
|
|
1846
|
+
}
|
|
1932
1847
|
};
|
|
1933
1848
|
}
|
|
1934
|
-
|
|
1849
|
+
/** A `user/message` event's data is the message itself. */
|
|
1850
|
+
function carriedOf(data, fallbackId) {
|
|
1851
|
+
const message = record$1(data);
|
|
1852
|
+
const source = record$1(message.source);
|
|
1853
|
+
if (source.kind !== "mh-browser") return void 0;
|
|
1854
|
+
const presence = record$1(source.presence);
|
|
1855
|
+
const kind = presence.kind;
|
|
1856
|
+
if (kind !== "baseline" && kind !== "delta") return void 0;
|
|
1857
|
+
const entries = Array.isArray(presence.entries) ? presence.entries.flatMap((item) => entryOf$1(item)) : [];
|
|
1935
1858
|
return {
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
...value.originalDimensions === void 0 ? {} : { originalDimensions: { ...value.originalDimensions } }
|
|
1859
|
+
id: typeof message.id === "string" && message.id !== "" ? message.id : fallbackId,
|
|
1860
|
+
record: {
|
|
1861
|
+
kind,
|
|
1862
|
+
entries,
|
|
1863
|
+
...presence.replaces === true ? { replaces: true } : {}
|
|
1864
|
+
}
|
|
1943
1865
|
};
|
|
1944
1866
|
}
|
|
1945
|
-
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1867
|
+
function entryOf$1(value) {
|
|
1868
|
+
const row = record$1(value);
|
|
1869
|
+
const op = row.op;
|
|
1870
|
+
if (op !== "present" && op !== "added" && op !== "changed" && op !== "absent" && op !== "removed") return [];
|
|
1871
|
+
const url = typeof row.url === "string" ? row.url : "";
|
|
1872
|
+
if (url === "") return [];
|
|
1873
|
+
const source = isSource(row.source) ? row.source : "model";
|
|
1874
|
+
const version = typeof row.version === "string" ? row.version : void 0;
|
|
1875
|
+
const id = typeof row.id === "string" && row.id !== "" ? row.id : void 0;
|
|
1876
|
+
const reason = typeof row.reason === "string" ? row.reason : void 0;
|
|
1877
|
+
return [{
|
|
1878
|
+
op,
|
|
1879
|
+
url,
|
|
1880
|
+
source,
|
|
1881
|
+
observedAt: typeof row.observedAt === "string" ? row.observedAt : "",
|
|
1882
|
+
digest: typeof row.digest === "string" ? row.digest : "",
|
|
1883
|
+
...version === void 0 ? {} : { version },
|
|
1884
|
+
...id === void 0 ? {} : { id },
|
|
1885
|
+
...reason === void 0 ? {} : { reason }
|
|
1886
|
+
}];
|
|
1958
1887
|
}
|
|
1959
|
-
function
|
|
1960
|
-
|
|
1888
|
+
function textOf$1(value) {
|
|
1889
|
+
const content = record$1(value).content;
|
|
1890
|
+
if (!Array.isArray(content)) return "";
|
|
1891
|
+
return content.map((block) => record$1(block).text).filter((text) => typeof text === "string").join("\n");
|
|
1892
|
+
}
|
|
1893
|
+
function isSource(value) {
|
|
1894
|
+
return typeof value === "string" && PRESENCE_SOURCES.includes(value);
|
|
1895
|
+
}
|
|
1896
|
+
function record$1(value) {
|
|
1897
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
1898
|
+
return {};
|
|
1961
1899
|
}
|
|
1962
1900
|
//#endregion
|
|
1963
1901
|
//#region lib/types/presence/notice.js
|
|
@@ -1983,8 +1921,7 @@ const BROWSER_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1983
1921
|
* the reminder after the claimed input. `await next()` runs first, so a
|
|
1984
1922
|
* caller that puts pages and then returns `next()` is folded into this step.
|
|
1985
1923
|
*/
|
|
1986
|
-
function registerPresence(ctx) {
|
|
1987
|
-
const reader = logReader(ctx);
|
|
1924
|
+
function registerPresence(ctx, reader) {
|
|
1988
1925
|
const tails = /* @__PURE__ */ new WeakMap();
|
|
1989
1926
|
const stepTouches = /* @__PURE__ */ new WeakMap();
|
|
1990
1927
|
const executionTouches = /* @__PURE__ */ new Map();
|
|
@@ -2012,8 +1949,7 @@ function registerPresence(ctx) {
|
|
|
2012
1949
|
});
|
|
2013
1950
|
};
|
|
2014
1951
|
const stepIsOpen = (session) => {
|
|
2015
|
-
const boundary = ctx.
|
|
2016
|
-
if (boundary === void 0) throw new Error("mh-browser requires the turnBoundary session projection");
|
|
1952
|
+
const boundary = ctx.agents.turnBoundaryOf(session);
|
|
2017
1953
|
return boundary.openTurnStartSeq !== null && boundary.lastStepBoundary?.kind === "start" && boundary.lastStepBoundary.seq > boundary.openTurnStartSeq;
|
|
2018
1954
|
};
|
|
2019
1955
|
const project = (touch) => {
|
|
@@ -2239,6 +2175,58 @@ function canonical$1(url) {
|
|
|
2239
2175
|
}
|
|
2240
2176
|
}
|
|
2241
2177
|
//#endregion
|
|
2178
|
+
//#region lib/types/projections.js
|
|
2179
|
+
/** Host-only folds over the session log; every read of what the log says goes through these. */
|
|
2180
|
+
async function registerProjections(ctx) {
|
|
2181
|
+
return readerFrom(await ctx.sessionProjections.register({
|
|
2182
|
+
key: "mhBrowserDeliveries",
|
|
2183
|
+
stateVersion: 2,
|
|
2184
|
+
stateSchema: plainObject(["deliveries", "tokens"]),
|
|
2185
|
+
init: initDeliveries,
|
|
2186
|
+
apply: (state, event) => applyDelivery(state, {
|
|
2187
|
+
seq: event.seq,
|
|
2188
|
+
type: event.type,
|
|
2189
|
+
data: event.data,
|
|
2190
|
+
..."surfaceOp" in event && event.surfaceOp !== void 0 ? { surfaceOp: event.surfaceOp } : {}
|
|
2191
|
+
})
|
|
2192
|
+
}), await ctx.sessionProjections.register({
|
|
2193
|
+
key: "mhBrowserPresence",
|
|
2194
|
+
stateVersion: 1,
|
|
2195
|
+
stateSchema: plainObject([
|
|
2196
|
+
"committed",
|
|
2197
|
+
"inbox",
|
|
2198
|
+
"discarded",
|
|
2199
|
+
"shown",
|
|
2200
|
+
"ops"
|
|
2201
|
+
]),
|
|
2202
|
+
init: initPresenceLog,
|
|
2203
|
+
apply: (state, event) => applyPresenceLog(state, event)
|
|
2204
|
+
}), await ctx.sessionProjections.register({
|
|
2205
|
+
key: "mhBrowserAttachments",
|
|
2206
|
+
stateVersion: 1,
|
|
2207
|
+
stateSchema: plainObject(["refs"]),
|
|
2208
|
+
init: initAttachments,
|
|
2209
|
+
apply: (state, event) => applyAttachments(state, event)
|
|
2210
|
+
}), await ctx.sessionProjections.register({
|
|
2211
|
+
key: "mhBrowserTabs",
|
|
2212
|
+
stateVersion: 2,
|
|
2213
|
+
stateSchema: plainObject(["open"]),
|
|
2214
|
+
init: initTabLog,
|
|
2215
|
+
apply: (state, event) => applyTabLog(state, event)
|
|
2216
|
+
}));
|
|
2217
|
+
}
|
|
2218
|
+
function readerFrom(deliveries, presence, attachments, tabs) {
|
|
2219
|
+
return {
|
|
2220
|
+
deliveries: (session) => deliveries.stateOf(session),
|
|
2221
|
+
presence: (session) => presence.stateOf(session),
|
|
2222
|
+
attachments: (session) => attachments.stateOf(session),
|
|
2223
|
+
tabs: (session) => tabs.stateOf(session)
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
function plainObject(keys) {
|
|
2227
|
+
return z$1.custom((value) => value !== null && typeof value === "object" && keys.every((key) => key in value));
|
|
2228
|
+
}
|
|
2229
|
+
//#endregion
|
|
2242
2230
|
//#region lib/types/presence/book.js
|
|
2243
2231
|
/** In-memory presence set for one process. The log is the source of a set that outlives the process. */
|
|
2244
2232
|
var PresenceBook = class {
|
|
@@ -2952,6 +2940,15 @@ function missingExecutable(error) {
|
|
|
2952
2940
|
return /ENOENT|doesn.t exist|Executable doesn't exist|not found/i.test(message);
|
|
2953
2941
|
}
|
|
2954
2942
|
/**
|
|
2943
|
+
* Playwright appends `--no-sandbox` unless this is true, and branded Chrome
|
|
2944
|
+
* then warns that stability and security will suffer. An installed browser
|
|
2945
|
+
* sandboxes on every platform. Playwright's own Chromium does too, except on
|
|
2946
|
+
* Linux, where that build has no usable sandbox helper and would fail to start.
|
|
2947
|
+
*/
|
|
2948
|
+
function chromiumSandboxEnabled(browser, platform = process.platform) {
|
|
2949
|
+
return platform !== "linux" || browser !== "chromium";
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2955
2952
|
* One persistent context for the whole process. `chromium` omits `channel`
|
|
2956
2953
|
* so Playwright uses its own browser; the others use a local install.
|
|
2957
2954
|
* A missing browser is reported and not downloaded.
|
|
@@ -2962,6 +2959,7 @@ async function launchBrowser(options) {
|
|
|
2962
2959
|
return await chromium.launchPersistentContext(options.profileDir, {
|
|
2963
2960
|
headless: options.headless,
|
|
2964
2961
|
acceptDownloads: true,
|
|
2962
|
+
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
2965
2963
|
...options.headless ? {} : {
|
|
2966
2964
|
ignoreDefaultArgs: ["--disable-extensions"],
|
|
2967
2965
|
args: ["--enable-unsafe-extension-debugging", "--keep-alive-for-test"]
|
|
@@ -6589,8 +6587,7 @@ function stopped(signal) {
|
|
|
6589
6587
|
//#region lib/types/tools/download.js
|
|
6590
6588
|
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(" ");
|
|
6591
6589
|
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.";
|
|
6592
|
-
function browserDownloadTool(ctx) {
|
|
6593
|
-
const reader = logReader(ctx);
|
|
6590
|
+
function browserDownloadTool(ctx, reader) {
|
|
6594
6591
|
return defineTool({
|
|
6595
6592
|
name: "browser_download",
|
|
6596
6593
|
description: DOWNLOAD,
|
|
@@ -6667,13 +6664,12 @@ function browserDownloadTool(ctx) {
|
|
|
6667
6664
|
const HANDOVER = [
|
|
6668
6665
|
"Hand the browser to the person and wait until they hand it back.",
|
|
6669
6666
|
"The person can take over a tab at any time; that stops the action in progress on that tab, and a tab they have touched stays with them until they hand it back. Other tabs keep working.",
|
|
6670
|
-
"Call it instead of filling or submitting a login form, a
|
|
6667
|
+
"Call it instead of filling or submitting a login form, a Mind Harness login or consent form, or any form that issues credentials, and whenever only the person can continue."
|
|
6671
6668
|
].join(" ");
|
|
6672
6669
|
const REASON = "The sentence the person sees on the handover card, unchanged.";
|
|
6673
6670
|
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.";
|
|
6674
6671
|
const ABORT = Symbol("abort");
|
|
6675
|
-
function browserHandoverTool(ctx) {
|
|
6676
|
-
const reader = logReader(ctx);
|
|
6672
|
+
function browserHandoverTool(ctx, reader) {
|
|
6677
6673
|
return defineTool({
|
|
6678
6674
|
name: "browser_handover",
|
|
6679
6675
|
description: HANDOVER,
|
|
@@ -6809,8 +6805,7 @@ const PAGE = "The url attribute of a page element in a tool result or a presence
|
|
|
6809
6805
|
const NAME = "Action name copied from that page's Agent View actions.";
|
|
6810
6806
|
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.";
|
|
6811
6807
|
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.";
|
|
6812
|
-
function browserInvokeTool(ctx) {
|
|
6813
|
-
const reader = logReader(ctx);
|
|
6808
|
+
function browserInvokeTool(ctx, reader) {
|
|
6814
6809
|
return defineTool({
|
|
6815
6810
|
name: "browser_invoke",
|
|
6816
6811
|
description: INVOKE,
|
|
@@ -6929,8 +6924,7 @@ const PAGES = "The pages to read, 1 to 8, each returned in order as its own elem
|
|
|
6929
6924
|
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.";
|
|
6930
6925
|
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.";
|
|
6931
6926
|
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.";
|
|
6932
|
-
function browserOpenTool(ctx) {
|
|
6933
|
-
const reader = logReader(ctx);
|
|
6927
|
+
function browserOpenTool(ctx, reader) {
|
|
6934
6928
|
return defineTool({
|
|
6935
6929
|
name: "browser_open",
|
|
6936
6930
|
description: OPEN,
|
|
@@ -7054,8 +7048,7 @@ const PRESENCE = [
|
|
|
7054
7048
|
].join(" ");
|
|
7055
7049
|
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.";
|
|
7056
7050
|
const REMOVE = "Addresses to stop keeping, copied from a presence reminder; at most 8.";
|
|
7057
|
-
function browserPresenceTool(ctx) {
|
|
7058
|
-
const reader = logReader(ctx);
|
|
7051
|
+
function browserPresenceTool(ctx, reader) {
|
|
7059
7052
|
return defineTool({
|
|
7060
7053
|
name: "browser_presence",
|
|
7061
7054
|
description: PRESENCE,
|
|
@@ -7132,14 +7125,14 @@ function line(receipt) {
|
|
|
7132
7125
|
}
|
|
7133
7126
|
//#endregion
|
|
7134
7127
|
//#region lib/types/tools/register.js
|
|
7135
|
-
function registerBrowserTools(ctx) {
|
|
7128
|
+
function registerBrowserTools(ctx, reader) {
|
|
7136
7129
|
followSessionTitles(ctx);
|
|
7137
|
-
ctx.tools.register(browserOpenTool(ctx));
|
|
7138
|
-
for (const tool of pageOperationTools(ctx)) ctx.tools.register(tool);
|
|
7139
|
-
ctx.tools.register(browserInvokeTool(ctx));
|
|
7140
|
-
ctx.tools.register(browserDownloadTool(ctx));
|
|
7141
|
-
ctx.tools.register(browserPresenceTool(ctx));
|
|
7142
|
-
ctx.tools.register(browserHandoverTool(ctx));
|
|
7130
|
+
ctx.tools.register(browserOpenTool(ctx, reader));
|
|
7131
|
+
for (const tool of pageOperationTools(ctx, reader)) ctx.tools.register(tool);
|
|
7132
|
+
ctx.tools.register(browserInvokeTool(ctx, reader));
|
|
7133
|
+
ctx.tools.register(browserDownloadTool(ctx, reader));
|
|
7134
|
+
ctx.tools.register(browserPresenceTool(ctx, reader));
|
|
7135
|
+
ctx.tools.register(browserHandoverTool(ctx, reader));
|
|
7143
7136
|
}
|
|
7144
7137
|
//#endregion
|
|
7145
7138
|
//#region lib/types/index.js
|
|
@@ -7148,10 +7141,11 @@ const inject = [
|
|
|
7148
7141
|
"tools",
|
|
7149
7142
|
"userQuestions",
|
|
7150
7143
|
"attachments",
|
|
7151
|
-
"sessionProjections"
|
|
7144
|
+
"sessionProjections",
|
|
7145
|
+
"agents"
|
|
7152
7146
|
];
|
|
7153
7147
|
const Config = browserConfig;
|
|
7154
|
-
function apply(ctx, config) {
|
|
7148
|
+
async function apply(ctx, config) {
|
|
7155
7149
|
ctx.plugin({
|
|
7156
7150
|
name: "mh-browser-slot",
|
|
7157
7151
|
inject: ["browserUse", "mhBrowser"],
|
|
@@ -7159,14 +7153,14 @@ function apply(ctx, config) {
|
|
|
7159
7153
|
inner.browserUse.register("mh-browser");
|
|
7160
7154
|
}
|
|
7161
7155
|
});
|
|
7162
|
-
ctx.plugin(MhBrowserService, config);
|
|
7163
|
-
ctx.plugin({
|
|
7156
|
+
await ctx.plugin(MhBrowserService, config);
|
|
7157
|
+
await ctx.plugin({
|
|
7164
7158
|
name: "mh-browser-runtime",
|
|
7165
7159
|
inject: ["mhBrowser", ...inject],
|
|
7166
|
-
apply(inner) {
|
|
7167
|
-
registerProjections(inner);
|
|
7168
|
-
registerBrowserTools(inner);
|
|
7169
|
-
registerPresence(inner);
|
|
7160
|
+
async apply(inner) {
|
|
7161
|
+
const reader = await registerProjections(inner);
|
|
7162
|
+
registerBrowserTools(inner, reader);
|
|
7163
|
+
registerPresence(inner, reader);
|
|
7170
7164
|
}
|
|
7171
7165
|
});
|
|
7172
7166
|
}
|