@mind-harness/mh-browser 0.1.6 → 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 +666 -682
- 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/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,515 +3,34 @@ 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";
|
|
15
15
|
import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
|
|
16
|
-
//#region lib/types/config.js
|
|
17
|
-
const browserConfig = z.object({
|
|
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
|
-
}
|
|
16
|
+
//#region lib/types/config.js
|
|
17
|
+
const browserConfig = z.object({
|
|
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
|
+
});
|
|
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 {
|
|
@@ -6599,8 +6587,7 @@ function stopped(signal) {
|
|
|
6599
6587
|
//#region lib/types/tools/download.js
|
|
6600
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(" ");
|
|
6601
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.";
|
|
6602
|
-
function browserDownloadTool(ctx) {
|
|
6603
|
-
const reader = logReader(ctx);
|
|
6590
|
+
function browserDownloadTool(ctx, reader) {
|
|
6604
6591
|
return defineTool({
|
|
6605
6592
|
name: "browser_download",
|
|
6606
6593
|
description: DOWNLOAD,
|
|
@@ -6682,8 +6669,7 @@ const HANDOVER = [
|
|
|
6682
6669
|
const REASON = "The sentence the person sees on the handover card, unchanged.";
|
|
6683
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.";
|
|
6684
6671
|
const ABORT = Symbol("abort");
|
|
6685
|
-
function browserHandoverTool(ctx) {
|
|
6686
|
-
const reader = logReader(ctx);
|
|
6672
|
+
function browserHandoverTool(ctx, reader) {
|
|
6687
6673
|
return defineTool({
|
|
6688
6674
|
name: "browser_handover",
|
|
6689
6675
|
description: HANDOVER,
|
|
@@ -6819,8 +6805,7 @@ const PAGE = "The url attribute of a page element in a tool result or a presence
|
|
|
6819
6805
|
const NAME = "Action name copied from that page's Agent View actions.";
|
|
6820
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.";
|
|
6821
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.";
|
|
6822
|
-
function browserInvokeTool(ctx) {
|
|
6823
|
-
const reader = logReader(ctx);
|
|
6808
|
+
function browserInvokeTool(ctx, reader) {
|
|
6824
6809
|
return defineTool({
|
|
6825
6810
|
name: "browser_invoke",
|
|
6826
6811
|
description: INVOKE,
|
|
@@ -6939,8 +6924,7 @@ const PAGES = "The pages to read, 1 to 8, each returned in order as its own elem
|
|
|
6939
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.";
|
|
6940
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.";
|
|
6941
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.";
|
|
6942
|
-
function browserOpenTool(ctx) {
|
|
6943
|
-
const reader = logReader(ctx);
|
|
6927
|
+
function browserOpenTool(ctx, reader) {
|
|
6944
6928
|
return defineTool({
|
|
6945
6929
|
name: "browser_open",
|
|
6946
6930
|
description: OPEN,
|
|
@@ -7064,8 +7048,7 @@ const PRESENCE = [
|
|
|
7064
7048
|
].join(" ");
|
|
7065
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.";
|
|
7066
7050
|
const REMOVE = "Addresses to stop keeping, copied from a presence reminder; at most 8.";
|
|
7067
|
-
function browserPresenceTool(ctx) {
|
|
7068
|
-
const reader = logReader(ctx);
|
|
7051
|
+
function browserPresenceTool(ctx, reader) {
|
|
7069
7052
|
return defineTool({
|
|
7070
7053
|
name: "browser_presence",
|
|
7071
7054
|
description: PRESENCE,
|
|
@@ -7142,14 +7125,14 @@ function line(receipt) {
|
|
|
7142
7125
|
}
|
|
7143
7126
|
//#endregion
|
|
7144
7127
|
//#region lib/types/tools/register.js
|
|
7145
|
-
function registerBrowserTools(ctx) {
|
|
7128
|
+
function registerBrowserTools(ctx, reader) {
|
|
7146
7129
|
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));
|
|
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));
|
|
7153
7136
|
}
|
|
7154
7137
|
//#endregion
|
|
7155
7138
|
//#region lib/types/index.js
|
|
@@ -7158,10 +7141,11 @@ const inject = [
|
|
|
7158
7141
|
"tools",
|
|
7159
7142
|
"userQuestions",
|
|
7160
7143
|
"attachments",
|
|
7161
|
-
"sessionProjections"
|
|
7144
|
+
"sessionProjections",
|
|
7145
|
+
"agents"
|
|
7162
7146
|
];
|
|
7163
7147
|
const Config = browserConfig;
|
|
7164
|
-
function apply(ctx, config) {
|
|
7148
|
+
async function apply(ctx, config) {
|
|
7165
7149
|
ctx.plugin({
|
|
7166
7150
|
name: "mh-browser-slot",
|
|
7167
7151
|
inject: ["browserUse", "mhBrowser"],
|
|
@@ -7169,14 +7153,14 @@ function apply(ctx, config) {
|
|
|
7169
7153
|
inner.browserUse.register("mh-browser");
|
|
7170
7154
|
}
|
|
7171
7155
|
});
|
|
7172
|
-
ctx.plugin(MhBrowserService, config);
|
|
7173
|
-
ctx.plugin({
|
|
7156
|
+
await ctx.plugin(MhBrowserService, config);
|
|
7157
|
+
await ctx.plugin({
|
|
7174
7158
|
name: "mh-browser-runtime",
|
|
7175
7159
|
inject: ["mhBrowser", ...inject],
|
|
7176
|
-
apply(inner) {
|
|
7177
|
-
registerProjections(inner);
|
|
7178
|
-
registerBrowserTools(inner);
|
|
7179
|
-
registerPresence(inner);
|
|
7160
|
+
async apply(inner) {
|
|
7161
|
+
const reader = await registerProjections(inner);
|
|
7162
|
+
registerBrowserTools(inner, reader);
|
|
7163
|
+
registerPresence(inner, reader);
|
|
7180
7164
|
}
|
|
7181
7165
|
});
|
|
7182
7166
|
}
|