@mind-harness/mh-browser 0.2.0 → 0.2.2
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 +123 -42
- package/lib/typert.host.js +1 -1
- package/lib/typert.remote-client.js +1 -1
- package/lib/types/service/service.d.ts +8 -4
- package/lib/types/tools/handover.d.ts +1 -1
- package/lib/types/tools/register.d.ts +1 -1
- package/lib/types/tools/session.d.ts +3 -1
- package/lib/types/types.d.ts +8 -5
- package/lib/types/words.d.ts +11 -0
- package/package.json +1 -1
package/extension/manifest.json
CHANGED
package/lib/index.js
CHANGED
|
@@ -447,7 +447,6 @@ function formatSeconds(seconds) {
|
|
|
447
447
|
}
|
|
448
448
|
//#endregion
|
|
449
449
|
//#region lib/types/types.js
|
|
450
|
-
/** Public browser types. Playwright types stay inside the service. */
|
|
451
450
|
var MhBrowserError = class extends Error {
|
|
452
451
|
code;
|
|
453
452
|
page;
|
|
@@ -479,10 +478,12 @@ function unknownTab() {
|
|
|
479
478
|
//#region lib/types/tools/session.js
|
|
480
479
|
/** dsh shows the model only the message, so the message starts with the code. */
|
|
481
480
|
var BrowserToolError = class extends HarnessError {
|
|
481
|
+
code;
|
|
482
482
|
reason;
|
|
483
483
|
constructor(code, reason) {
|
|
484
484
|
super(`${code}: ${reason}`, code);
|
|
485
485
|
this.name = "BrowserToolError";
|
|
486
|
+
this.code = code;
|
|
486
487
|
this.reason = reason;
|
|
487
488
|
}
|
|
488
489
|
};
|
|
@@ -1063,7 +1064,7 @@ const IMAGE_VALUE = {
|
|
|
1063
1064
|
const TAB$1 = "Tab id copied from the latest result in this session: a tabs list, or a page element's tab attribute. Omit it to use the tab of that latest result; if it has no tab, the call fails with INVALID_ARGS. An idle tab may be closed, and a closed id fails with UNKNOWN_TAB: open the address again with browser_open. A tab with unsubmitted input is marked in use and is not closed, and opening or a declared action does not reload it. Tabs are gone after a restart. A page with an Agent View has no tab until you open it with snapshot.";
|
|
1064
1065
|
const TARGET = "A ref copied from this tab's latest snapshot, such as e3, or a selector that matches one element. A ref that is not in the current snapshot fails with STALE_REF.";
|
|
1065
1066
|
const ELEMENT = "What the person sees in the log. It does not locate the element.";
|
|
1066
|
-
const FILENAME = "A path inside the workspace. The result is saved there and the reply is the path and the byte count. Omit it and the result stays in the reply.";
|
|
1067
|
+
const FILENAME = "A path inside the workspace. The result is saved there and the reply is the path and the byte count. A file already at that path is replaced. Omit it and the result stays in the reply.";
|
|
1067
1068
|
const OUTPUT = {
|
|
1068
1069
|
schema: {
|
|
1069
1070
|
type: "object",
|
|
@@ -2961,8 +2962,14 @@ async function launchBrowser(options) {
|
|
|
2961
2962
|
acceptDownloads: true,
|
|
2962
2963
|
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
2963
2964
|
...options.headless ? {} : {
|
|
2965
|
+
viewport: null,
|
|
2964
2966
|
ignoreDefaultArgs: ["--disable-extensions"],
|
|
2965
|
-
args: [
|
|
2967
|
+
args: [
|
|
2968
|
+
"--enable-unsafe-extension-debugging",
|
|
2969
|
+
"--keep-alive-for-test",
|
|
2970
|
+
"--disable-blink-features=AutomationControlled",
|
|
2971
|
+
"--test-type"
|
|
2972
|
+
]
|
|
2966
2973
|
},
|
|
2967
2974
|
...options.browser === "chromium" ? {} : { channel: options.browser },
|
|
2968
2975
|
...options.executablePath !== void 0 ? { executablePath: options.executablePath } : {}
|
|
@@ -4096,6 +4103,22 @@ function watchDialog(page) {
|
|
|
4096
4103
|
};
|
|
4097
4104
|
}
|
|
4098
4105
|
//#endregion
|
|
4106
|
+
//#region lib/types/words.js
|
|
4107
|
+
/** Shown on a session's tab group while a handover is waiting. */
|
|
4108
|
+
const WAITING_TITLE = "等你:";
|
|
4109
|
+
/** The question on the handover card. `reason` is the sentence the model gave. */
|
|
4110
|
+
function handoverQuestion(reason) {
|
|
4111
|
+
return `浏览器交给你了:${reason}`;
|
|
4112
|
+
}
|
|
4113
|
+
/** The option that hands the browser back. */
|
|
4114
|
+
const HAND_BACK = "交回";
|
|
4115
|
+
/** The handover card when the browser has no window: the person cannot act in it. */
|
|
4116
|
+
function stepQuestion(reason) {
|
|
4117
|
+
return `需要你来:${reason}(浏览器没有窗口,这里做不了;写下怎么继续)`;
|
|
4118
|
+
}
|
|
4119
|
+
/** The option on that card when the person has nothing to add. */
|
|
4120
|
+
const REPLIED = "继续";
|
|
4121
|
+
//#endregion
|
|
4099
4122
|
//#region lib/types/service/extension.js
|
|
4100
4123
|
const PACKAGE = "@mind-harness/mh-browser";
|
|
4101
4124
|
const WORKER_MS = 5e3;
|
|
@@ -4760,7 +4783,7 @@ var Windows = class {
|
|
|
4760
4783
|
if (extension === void 0 || groupId === void 0) return;
|
|
4761
4784
|
const title = this.titles.get(owner) ?? "";
|
|
4762
4785
|
const waiting = this.waiting.has(owner);
|
|
4763
|
-
await extension.styleGroup(groupId, waiting ?
|
|
4786
|
+
await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue");
|
|
4764
4787
|
}
|
|
4765
4788
|
async ingest(page) {
|
|
4766
4789
|
if (this.byPage.has(page)) return;
|
|
@@ -4997,6 +5020,9 @@ const DOWNLOAD_EVENT_MS = 3e3;
|
|
|
4997
5020
|
/** More exits than this within the window and the browser is not started again until one ages out. */
|
|
4998
5021
|
const RELAUNCH_LIMIT = 3;
|
|
4999
5022
|
const RELAUNCH_WINDOW_MS = 6e4;
|
|
5023
|
+
/** Agent Views remembered for a later conditional read. Older ones are read again in full. */
|
|
5024
|
+
const DOCUMENT_CAP = 64;
|
|
5025
|
+
const OPEN_ADDRESS_REASON = "only http and https addresses are opened";
|
|
5000
5026
|
const STOP_WAIT_MS = 5e3;
|
|
5001
5027
|
const WAITING_STOPPED = "stopped while waiting";
|
|
5002
5028
|
const STOPPED = Symbol("stopped");
|
|
@@ -5040,10 +5066,10 @@ let MhBrowserService = (() => {
|
|
|
5040
5066
|
windows = (__runInitializers(this, _instanceExtraInitializers), new Windows());
|
|
5041
5067
|
launchOptions;
|
|
5042
5068
|
context;
|
|
5043
|
-
ready;
|
|
5069
|
+
ready = Promise.resolve();
|
|
5044
5070
|
closing = false;
|
|
5045
|
-
/** The browser
|
|
5046
|
-
exited =
|
|
5071
|
+
/** The browser is not running. The next call that needs a page starts it. */
|
|
5072
|
+
exited = true;
|
|
5047
5073
|
exits = [];
|
|
5048
5074
|
/** One queue per owner: a slow wait in one Session does not hold up another. */
|
|
5049
5075
|
queues = /* @__PURE__ */ new Map();
|
|
@@ -5073,7 +5099,7 @@ let MhBrowserService = (() => {
|
|
|
5073
5099
|
this.windows.onTabClosed((tab) => {
|
|
5074
5100
|
this.shown.delete(tab.id);
|
|
5075
5101
|
});
|
|
5076
|
-
this.
|
|
5102
|
+
this.up().catch(() => void 0);
|
|
5077
5103
|
ctx.on("session/disposed", (session) => {
|
|
5078
5104
|
const owner = session.id;
|
|
5079
5105
|
if (owner === void 0) return;
|
|
@@ -5478,6 +5504,10 @@ let MhBrowserService = (() => {
|
|
|
5478
5504
|
* the tab, and that tab is not listed as closed.
|
|
5479
5505
|
*/
|
|
5480
5506
|
async openAddress(owner, item, signal) {
|
|
5507
|
+
if (!openableAddress(item.url)) return {
|
|
5508
|
+
url: item.url,
|
|
5509
|
+
reason: OPEN_ADDRESS_REASON
|
|
5510
|
+
};
|
|
5481
5511
|
const wantsTab = owner === "person" || item.snapshot === true || item.waitFor !== void 0;
|
|
5482
5512
|
if (!wantsTab && this.knownOrigin(item.url)) {
|
|
5483
5513
|
const via = await this.readKnown(owner, item.url, signal);
|
|
@@ -5520,7 +5550,7 @@ let MhBrowserService = (() => {
|
|
|
5520
5550
|
if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
|
|
5521
5551
|
this.rememberOrigin(opened.view.url);
|
|
5522
5552
|
const shown = this.shown.get(opened.live.id);
|
|
5523
|
-
if (shown?.kind === "agent-view") this.
|
|
5553
|
+
if (shown?.kind === "agent-view") this.rememberDocument(shown.pageUrl, shown);
|
|
5524
5554
|
}
|
|
5525
5555
|
/**
|
|
5526
5556
|
* Read a known origin without a tab. `undefined` means this page has to be
|
|
@@ -5564,10 +5594,21 @@ let MhBrowserService = (() => {
|
|
|
5564
5594
|
});
|
|
5565
5595
|
if (stopped(signal)) return STOPPED;
|
|
5566
5596
|
if (shown.kind !== "agent-view") return void 0;
|
|
5567
|
-
this.
|
|
5597
|
+
this.rememberDocument(pageUrl, shown);
|
|
5568
5598
|
this.rememberOrigin(pageUrl);
|
|
5569
5599
|
return this.pageOfShown(owner, shown);
|
|
5570
5600
|
}
|
|
5601
|
+
/** Keep the newest Agent Views. A dropped one is read in full the next time. */
|
|
5602
|
+
rememberDocument(url, shown) {
|
|
5603
|
+
const key = canonical(url);
|
|
5604
|
+
this.documents.delete(key);
|
|
5605
|
+
this.documents.set(key, shown);
|
|
5606
|
+
while (this.documents.size > DOCUMENT_CAP) {
|
|
5607
|
+
const oldest = this.documents.keys().next().value;
|
|
5608
|
+
if (oldest === void 0) return;
|
|
5609
|
+
this.documents.delete(oldest);
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5571
5612
|
/** One page from a document, with no tab. */
|
|
5572
5613
|
pageOfShown(owner, shown) {
|
|
5573
5614
|
const view = {
|
|
@@ -6040,7 +6081,10 @@ let MhBrowserService = (() => {
|
|
|
6040
6081
|
});
|
|
6041
6082
|
}
|
|
6042
6083
|
async aria(tab) {
|
|
6043
|
-
return tab.page.ariaSnapshot({ mode: "ai" }).catch(() =>
|
|
6084
|
+
return tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6085
|
+
this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
|
|
6086
|
+
return "";
|
|
6087
|
+
});
|
|
6044
6088
|
}
|
|
6045
6089
|
async pageFromShown(tab, shown, extra) {
|
|
6046
6090
|
const current = shown ?? {
|
|
@@ -6050,7 +6094,10 @@ let MhBrowserService = (() => {
|
|
|
6050
6094
|
text: "",
|
|
6051
6095
|
snapshot: ""
|
|
6052
6096
|
};
|
|
6053
|
-
const title = tab.dialogHeld ? "" : await tab.page.title().catch(() =>
|
|
6097
|
+
const title = tab.dialogHeld ? "" : await tab.page.title().catch((error) => {
|
|
6098
|
+
this.ctx.logger.debug("page title failed: %s", errorText(error));
|
|
6099
|
+
return "";
|
|
6100
|
+
});
|
|
6054
6101
|
this.windows.noteTitle(tab, title);
|
|
6055
6102
|
const view = {
|
|
6056
6103
|
tab: tab.id,
|
|
@@ -6215,8 +6262,9 @@ let MhBrowserService = (() => {
|
|
|
6215
6262
|
...location === void 0 || location === "" ? {} : { location },
|
|
6216
6263
|
...mediaType === void 0 ? {} : { mediaType }
|
|
6217
6264
|
};
|
|
6218
|
-
} catch {
|
|
6265
|
+
} catch (error) {
|
|
6219
6266
|
if (signal?.aborted === true) throw new MhBrowserError(INTERRUPTED, "stopped after sending; it may have taken effect");
|
|
6267
|
+
this.ctx.logger.debug("declared action was not answered: %s", errorText(error));
|
|
6220
6268
|
return { kind: "lost" };
|
|
6221
6269
|
}
|
|
6222
6270
|
}
|
|
@@ -6318,11 +6366,18 @@ let MhBrowserService = (() => {
|
|
|
6318
6366
|
async start() {
|
|
6319
6367
|
const context = await launchBrowser(this.launchOptions);
|
|
6320
6368
|
if (this.closing) {
|
|
6321
|
-
await
|
|
6369
|
+
await this.closeContext(context);
|
|
6322
6370
|
return;
|
|
6323
6371
|
}
|
|
6324
6372
|
this.context = context;
|
|
6325
|
-
|
|
6373
|
+
try {
|
|
6374
|
+
await this.windows.attach(context, this.launchOptions.profileDir, { headed: !this.launchOptions.headless });
|
|
6375
|
+
} catch (error) {
|
|
6376
|
+
this.context = void 0;
|
|
6377
|
+
this.windows.dropPages();
|
|
6378
|
+
await this.closeContext(context);
|
|
6379
|
+
throw error;
|
|
6380
|
+
}
|
|
6326
6381
|
context.browser()?.on("disconnected", () => {
|
|
6327
6382
|
if (this.closing || this.context !== context) return;
|
|
6328
6383
|
this.onExit();
|
|
@@ -6338,22 +6393,23 @@ let MhBrowserService = (() => {
|
|
|
6338
6393
|
this.ready = Promise.resolve();
|
|
6339
6394
|
}
|
|
6340
6395
|
/**
|
|
6341
|
-
* The browser for a call that needs a page. One that
|
|
6342
|
-
* here. The exit limit stops unattended restarts; a person's request
|
|
6343
|
-
* one start per click and skips it.
|
|
6396
|
+
* The browser for a call that needs a page. One that is not running starts
|
|
6397
|
+
* again here. The exit limit stops unattended restarts; a person's request
|
|
6398
|
+
* is one start per click and skips it.
|
|
6344
6399
|
*/
|
|
6345
6400
|
up(byPerson = false) {
|
|
6346
6401
|
if (!this.exited || this.closing) return this.ready;
|
|
6347
6402
|
const now = Date.now();
|
|
6348
6403
|
while (this.exits.length > 0 && now - (this.exits[0] ?? now) > RELAUNCH_WINDOW_MS) this.exits.shift();
|
|
6349
6404
|
if (!byPerson && this.exits.length > RELAUNCH_LIMIT) {
|
|
6350
|
-
this.ctx.logger.warn("browser keeps
|
|
6351
|
-
return Promise.reject(new MhBrowserError(BROWSER_UNAVAILABLE, `the browser
|
|
6405
|
+
this.ctx.logger.warn("browser keeps stopping; not starting it again yet");
|
|
6406
|
+
return Promise.reject(new MhBrowserError(BROWSER_UNAVAILABLE, `the browser stopped or did not start ${String(this.exits.length)} times within ${String(RELAUNCH_WINDOW_MS / 1e3)} s; try again in a minute`));
|
|
6352
6407
|
}
|
|
6353
6408
|
this.exited = false;
|
|
6354
6409
|
const started = this.launch();
|
|
6355
6410
|
started.then(() => this.windows.resumePersonInput(), () => {
|
|
6356
6411
|
this.exited = true;
|
|
6412
|
+
if (!this.closing) this.exits.push(Date.now());
|
|
6357
6413
|
});
|
|
6358
6414
|
this.ready = started;
|
|
6359
6415
|
return started;
|
|
@@ -6361,10 +6417,15 @@ let MhBrowserService = (() => {
|
|
|
6361
6417
|
async stop() {
|
|
6362
6418
|
this.windows.suppressPersonInput();
|
|
6363
6419
|
await Promise.race([this.ready.catch(() => void 0), new Promise((resolve) => setTimeout(resolve, STOP_WAIT_MS))]);
|
|
6364
|
-
const
|
|
6365
|
-
await Promise.race([this.context?.close().catch(() => void 0), new Promise((resolve) => setTimeout(resolve, 1e3))]);
|
|
6366
|
-
(browser?.process?.())?.kill("SIGKILL");
|
|
6420
|
+
const context = this.context;
|
|
6367
6421
|
this.context = void 0;
|
|
6422
|
+
if (context !== void 0) await this.closeContext(context);
|
|
6423
|
+
}
|
|
6424
|
+
/** Close a context the service is done with, and kill it if close stalls. */
|
|
6425
|
+
async closeContext(context) {
|
|
6426
|
+
const browser = context.browser();
|
|
6427
|
+
await Promise.race([context.close().catch(() => void 0), new Promise((resolve) => setTimeout(resolve, 1e3))]);
|
|
6428
|
+
(browser?.process?.())?.kill("SIGKILL");
|
|
6368
6429
|
}
|
|
6369
6430
|
refuseIfPerson(owner, tab) {
|
|
6370
6431
|
if (this.control.ofTab(owner, tab) !== "person") return;
|
|
@@ -6521,6 +6582,18 @@ function requiresOf(text, pageUrl) {
|
|
|
6521
6582
|
const read = readAgentView(text, pageUrl);
|
|
6522
6583
|
return read.ok ? read.doc.requires : [];
|
|
6523
6584
|
}
|
|
6585
|
+
function openableAddress(url) {
|
|
6586
|
+
if (url === "about:blank") return true;
|
|
6587
|
+
try {
|
|
6588
|
+
const protocol = new URL(url).protocol;
|
|
6589
|
+
return protocol === "http:" || protocol === "https:";
|
|
6590
|
+
} catch {
|
|
6591
|
+
return false;
|
|
6592
|
+
}
|
|
6593
|
+
}
|
|
6594
|
+
function errorText(error) {
|
|
6595
|
+
return error instanceof Error ? error.message : String(error);
|
|
6596
|
+
}
|
|
6524
6597
|
function canonical(url) {
|
|
6525
6598
|
try {
|
|
6526
6599
|
const parsed = new URL(url);
|
|
@@ -6661,18 +6734,30 @@ function browserDownloadTool(ctx, reader) {
|
|
|
6661
6734
|
}
|
|
6662
6735
|
//#endregion
|
|
6663
6736
|
//#region lib/types/tools/handover.js
|
|
6664
|
-
const
|
|
6737
|
+
const THEIRS = "a check that they are human, a login form, a Mind Harness login or consent form, or any form that issues credentials. Those steps are theirs: do not click through them, work around them, or fetch the page another way. A human check can clear by itself; if it is still there after one wait, call this.";
|
|
6738
|
+
const HEADED = [
|
|
6665
6739
|
"Hand the browser to the person and wait until they hand it back.",
|
|
6740
|
+
"The browser is a window on the person's screen; they can see it and act in it.",
|
|
6666
6741
|
"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.",
|
|
6667
|
-
|
|
6742
|
+
`Call it when a site asks for the person themselves: ${THEIRS} A handover usually takes the person seconds.`,
|
|
6743
|
+
"Call it too whenever only the person can continue."
|
|
6744
|
+
].join(" ");
|
|
6745
|
+
const HEADLESS = [
|
|
6746
|
+
"Tell the person about a step only they can take, and wait for their reply.",
|
|
6747
|
+
"The browser has no window: the person cannot see it or act in it, so the step cannot be finished in this browser. Their reply says how to go on.",
|
|
6748
|
+
`Call it when a site asks for the person themselves: ${THEIRS}`,
|
|
6749
|
+
"Call it too whenever only the person can continue."
|
|
6668
6750
|
].join(" ");
|
|
6669
6751
|
const REASON = "The sentence the person sees on the handover card, unchanged.";
|
|
6670
|
-
const TAB =
|
|
6752
|
+
const TAB = {
|
|
6753
|
+
headed: "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.",
|
|
6754
|
+
headless: "Tab id of the page the step is on, copied from a page element's tab attribute; the result carries that page as it is after the reply. Omit it to name no tab; that does not pick the latest result. A page with an Agent View has no tab until you open it with snapshot."
|
|
6755
|
+
};
|
|
6671
6756
|
const ABORT = Symbol("abort");
|
|
6672
|
-
function browserHandoverTool(ctx, reader) {
|
|
6757
|
+
function browserHandoverTool(ctx, reader, headed = true) {
|
|
6673
6758
|
return defineTool({
|
|
6674
6759
|
name: "browser_handover",
|
|
6675
|
-
description:
|
|
6760
|
+
description: headed ? HEADED : HEADLESS,
|
|
6676
6761
|
parameters: {
|
|
6677
6762
|
reason: {
|
|
6678
6763
|
type: "string",
|
|
@@ -6681,7 +6766,7 @@ function browserHandoverTool(ctx, reader) {
|
|
|
6681
6766
|
},
|
|
6682
6767
|
tab: {
|
|
6683
6768
|
type: "string",
|
|
6684
|
-
description: TAB
|
|
6769
|
+
description: TAB[headed ? "headed" : "headless"]
|
|
6685
6770
|
}
|
|
6686
6771
|
},
|
|
6687
6772
|
output: {
|
|
@@ -6723,7 +6808,7 @@ function browserHandoverTool(ctx, reader) {
|
|
|
6723
6808
|
reason: args.reason,
|
|
6724
6809
|
...args.tab === void 0 ? {} : { tab: args.tab }
|
|
6725
6810
|
}, exec.signal);
|
|
6726
|
-
const answer = await askUntilAnswered(ctx, args.reason, exec);
|
|
6811
|
+
const answer = await askUntilAnswered(ctx, args.reason, exec, headed);
|
|
6727
6812
|
if (answer === ABORT || exec.signal.aborted) return quiet();
|
|
6728
6813
|
const note = answer.answers[0]?.custom;
|
|
6729
6814
|
const back = await ctx.mhBrowser.handBack(session.id, note);
|
|
@@ -6731,11 +6816,7 @@ function browserHandoverTool(ctx, reader) {
|
|
|
6731
6816
|
const pages = page === void 0 ? [] : [page];
|
|
6732
6817
|
const block = tabBlock(ctx, reader, session);
|
|
6733
6818
|
return {
|
|
6734
|
-
text: `${[
|
|
6735
|
-
...note === void 0 || note === "" ? [] : [note],
|
|
6736
|
-
`${String(back.operations)} operations`,
|
|
6737
|
-
takeoverText(back)
|
|
6738
|
-
].join("\n")}\n${renderOutcome(pages)}${block.suffix}`,
|
|
6819
|
+
text: `${[...note === void 0 || note === "" ? [] : [note], ...headed ? [`${String(back.operations)} operations`, takeoverText(back)] : []].join("\n")}\n${renderOutcome(pages)}${block.suffix}`,
|
|
6739
6820
|
meta: JSON.parse(JSON.stringify({
|
|
6740
6821
|
...readingsOf(pages, "handover"),
|
|
6741
6822
|
closed: block.closed,
|
|
@@ -6754,14 +6835,14 @@ function browserHandoverTool(ctx, reader) {
|
|
|
6754
6835
|
});
|
|
6755
6836
|
}
|
|
6756
6837
|
/** Closing the card is not an answer. Ask the same question again. */
|
|
6757
|
-
async function askUntilAnswered(ctx, reason, exec) {
|
|
6838
|
+
async function askUntilAnswered(ctx, reason, exec, headed) {
|
|
6758
6839
|
const stopped = aborted(exec.signal);
|
|
6759
6840
|
for (;;) try {
|
|
6760
6841
|
return await Promise.race([ctx.userQuestions.ask({
|
|
6761
6842
|
questions: [{
|
|
6762
6843
|
id: "handback",
|
|
6763
|
-
question:
|
|
6764
|
-
options: [{ label:
|
|
6844
|
+
question: headed ? handoverQuestion(reason) : stepQuestion(reason),
|
|
6845
|
+
options: [{ label: headed ? HAND_BACK : REPLIED }]
|
|
6765
6846
|
}],
|
|
6766
6847
|
...exec.agent !== void 0 ? { agent: exec.agent } : {},
|
|
6767
6848
|
signal: exec.signal
|
|
@@ -6921,7 +7002,7 @@ const OPEN = [
|
|
|
6921
7002
|
"A page lists documents in requires; those are not fetched with it, so open their addresses to read them or to call their actions."
|
|
6922
7003
|
].join(" ");
|
|
6923
7004
|
const PAGES = "The pages to read, 1 to 8, each returned in order as its own element.";
|
|
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.";
|
|
7005
|
+
const URL$1 = "The address to read: from a page shown to you, from the principal, or one you composed; opening a composed address is how it is checked before you use it elsewhere. Only an http or https address is opened.";
|
|
6925
7006
|
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.";
|
|
6926
7007
|
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.";
|
|
6927
7008
|
function browserOpenTool(ctx, reader) {
|
|
@@ -7125,14 +7206,14 @@ function line(receipt) {
|
|
|
7125
7206
|
}
|
|
7126
7207
|
//#endregion
|
|
7127
7208
|
//#region lib/types/tools/register.js
|
|
7128
|
-
function registerBrowserTools(ctx, reader) {
|
|
7209
|
+
function registerBrowserTools(ctx, reader, headed) {
|
|
7129
7210
|
followSessionTitles(ctx);
|
|
7130
7211
|
ctx.tools.register(browserOpenTool(ctx, reader));
|
|
7131
7212
|
for (const tool of pageOperationTools(ctx, reader)) ctx.tools.register(tool);
|
|
7132
7213
|
ctx.tools.register(browserInvokeTool(ctx, reader));
|
|
7133
7214
|
ctx.tools.register(browserDownloadTool(ctx, reader));
|
|
7134
7215
|
ctx.tools.register(browserPresenceTool(ctx, reader));
|
|
7135
|
-
ctx.tools.register(browserHandoverTool(ctx, reader));
|
|
7216
|
+
ctx.tools.register(browserHandoverTool(ctx, reader, headed));
|
|
7136
7217
|
}
|
|
7137
7218
|
//#endregion
|
|
7138
7219
|
//#region lib/types/index.js
|
|
@@ -7159,7 +7240,7 @@ async function apply(ctx, config) {
|
|
|
7159
7240
|
inject: ["mhBrowser", ...inject],
|
|
7160
7241
|
async apply(inner) {
|
|
7161
7242
|
const reader = await registerProjections(inner);
|
|
7162
|
-
registerBrowserTools(inner, reader);
|
|
7243
|
+
registerBrowserTools(inner, reader, !config.headless);
|
|
7163
7244
|
registerPresence(inner, reader);
|
|
7164
7245
|
}
|
|
7165
7246
|
});
|
package/lib/typert.host.js
CHANGED
|
@@ -44,7 +44,7 @@ const $remote = createTypertRemoteInfo({
|
|
|
44
44
|
create: _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema,
|
|
45
45
|
jsonSchema: {"type":"object","properties":{"status":{"type":"string","enum":["revealed","not-handing-over","no-tab"]}},"required":["status"],"additionalProperties":true},
|
|
46
46
|
},
|
|
47
|
-
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":
|
|
47
|
+
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":619,"column":9},
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
}, z)
|
|
@@ -44,7 +44,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
|
|
|
44
44
|
create: _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema,
|
|
45
45
|
jsonSchema: {"type":"object","properties":{"status":{"type":"string","enum":["revealed","not-handing-over","no-tab"]}},"required":["status"],"additionalProperties":true},
|
|
46
46
|
},
|
|
47
|
-
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":
|
|
47
|
+
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":619,"column":9},
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
}, z)
|
|
@@ -36,7 +36,7 @@ export declare class MhBrowserService extends TypertRemoteService {
|
|
|
36
36
|
private context;
|
|
37
37
|
private ready;
|
|
38
38
|
private closing;
|
|
39
|
-
/** The browser
|
|
39
|
+
/** The browser is not running. The next call that needs a page starts it. */
|
|
40
40
|
private exited;
|
|
41
41
|
private readonly exits;
|
|
42
42
|
/** One queue per owner: a slow wait in one Session does not hold up another. */
|
|
@@ -170,6 +170,8 @@ export declare class MhBrowserService extends TypertRemoteService {
|
|
|
170
170
|
* ends the read without falling back to a tab.
|
|
171
171
|
*/
|
|
172
172
|
private readKnown;
|
|
173
|
+
/** Keep the newest Agent Views. A dropped one is read in full the next time. */
|
|
174
|
+
private rememberDocument;
|
|
173
175
|
/** One page from a document, with no tab. */
|
|
174
176
|
private pageOfShown;
|
|
175
177
|
/**
|
|
@@ -225,12 +227,14 @@ export declare class MhBrowserService extends TypertRemoteService {
|
|
|
225
227
|
/** No window opens on exit: the person may have quit it. Calls on old tabs find none. */
|
|
226
228
|
private onExit;
|
|
227
229
|
/**
|
|
228
|
-
* The browser for a call that needs a page. One that
|
|
229
|
-
* here. The exit limit stops unattended restarts; a person's request
|
|
230
|
-
* one start per click and skips it.
|
|
230
|
+
* The browser for a call that needs a page. One that is not running starts
|
|
231
|
+
* again here. The exit limit stops unattended restarts; a person's request
|
|
232
|
+
* is one start per click and skips it.
|
|
231
233
|
*/
|
|
232
234
|
private up;
|
|
233
235
|
private stop;
|
|
236
|
+
/** Close a context the service is done with, and kill it if close stalls. */
|
|
237
|
+
private closeContext;
|
|
234
238
|
private refuseIfPerson;
|
|
235
239
|
private takenOver;
|
|
236
240
|
private presenceUrls;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { type ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
3
3
|
import type { LogReader } from '../projections.ts';
|
|
4
|
-
export declare function browserHandoverTool(ctx: Context, reader: LogReader): ToolDefinition;
|
|
4
|
+
export declare function browserHandoverTool(ctx: Context, reader: LogReader, headed?: boolean): ToolDefinition;
|
|
5
5
|
//# sourceMappingURL=handover.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { LogReader } from '../projections.ts';
|
|
3
|
-
export declare function registerBrowserTools(ctx: Context, reader: LogReader): void;
|
|
3
|
+
export declare function registerBrowserTools(ctx: Context, reader: LogReader, headed: boolean): void;
|
|
4
4
|
//# sourceMappingURL=register.d.ts.map
|
|
@@ -3,10 +3,12 @@ import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
|
3
3
|
import type { Session } from '@deepseek-ai/dsh-session';
|
|
4
4
|
import type { ToolRunContext } from '@deepseek-ai/dsh-tools';
|
|
5
5
|
import type { WriteTarget } from '../service/service.ts';
|
|
6
|
+
import { type BrowserCode } from '../types.ts';
|
|
6
7
|
/** dsh shows the model only the message, so the message starts with the code. */
|
|
7
8
|
export declare class BrowserToolError extends HarnessError {
|
|
9
|
+
readonly code: BrowserCode;
|
|
8
10
|
readonly reason: string;
|
|
9
|
-
constructor(code:
|
|
11
|
+
constructor(code: BrowserCode, reason: string);
|
|
10
12
|
}
|
|
11
13
|
export declare const NO_WORKSPACE_REASON = "this session has no working directory";
|
|
12
14
|
export interface ToolSession {
|
package/lib/types/types.d.ts
CHANGED
|
@@ -175,22 +175,25 @@ export interface TabInfo {
|
|
|
175
175
|
inPresence: boolean;
|
|
176
176
|
mayHaveChanged: boolean;
|
|
177
177
|
}
|
|
178
|
+
/** Every code this package reports. The values are the model-facing contract. */
|
|
179
|
+
export declare const BROWSER_CODES: readonly ["AUTHORITY_MISMATCH", "BROWSER_UNAVAILABLE", "INTERRUPTED", "INVALID_ARGS", "INVALID_INPUT", "ITEM_REFUSED", "MODAL_STATE", "NO_SESSION", "NO_WORKSPACE", "NOT_DECLARED", "NOT_DELIVERED", "OUTSIDE_WORKSPACE", "PERSON_IN_CONTROL", "SANDBOX_DENIED", "STALE_REF", "TAKEN_OVER", "UNKNOWN_TAB"];
|
|
180
|
+
export type BrowserCode = (typeof BROWSER_CODES)[number];
|
|
178
181
|
export declare class MhBrowserError extends Error {
|
|
179
|
-
readonly code:
|
|
182
|
+
readonly code: BrowserCode;
|
|
180
183
|
readonly page?: PageView;
|
|
181
|
-
constructor(code:
|
|
184
|
+
constructor(code: BrowserCode, message: string, page?: PageView);
|
|
182
185
|
}
|
|
183
186
|
/** Owner of the person's own window. It belongs to no Session. */
|
|
184
187
|
export declare const PERSON_OWNER = "person";
|
|
185
|
-
export declare const UNKNOWN_TAB
|
|
188
|
+
export declare const UNKNOWN_TAB: BrowserCode;
|
|
186
189
|
export declare const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
187
190
|
/**
|
|
188
191
|
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
189
192
|
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
190
193
|
*/
|
|
191
|
-
export declare const INTERRUPTED
|
|
194
|
+
export declare const INTERRUPTED: BrowserCode;
|
|
192
195
|
/** The browser did not start or could not be restarted. */
|
|
193
|
-
export declare const BROWSER_UNAVAILABLE
|
|
196
|
+
export declare const BROWSER_UNAVAILABLE: BrowserCode;
|
|
194
197
|
export declare function unknownTab(): PageError;
|
|
195
198
|
export type Download = {
|
|
196
199
|
kind: 'saved';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Shown on a session's tab group while a handover is waiting. */
|
|
2
|
+
export declare const WAITING_TITLE = "\u7B49\u4F60\uFF1A";
|
|
3
|
+
/** The question on the handover card. `reason` is the sentence the model gave. */
|
|
4
|
+
export declare function handoverQuestion(reason: string): string;
|
|
5
|
+
/** The option that hands the browser back. */
|
|
6
|
+
export declare const HAND_BACK = "\u4EA4\u56DE";
|
|
7
|
+
/** The handover card when the browser has no window: the person cannot act in it. */
|
|
8
|
+
export declare function stepQuestion(reason: string): string;
|
|
9
|
+
/** The option on that card when the person has nothing to add. */
|
|
10
|
+
export declare const REPLIED = "\u7EE7\u7EED";
|
|
11
|
+
//# sourceMappingURL=words.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mind-harness/mh-browser",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Opens pages in a real browser for the agent, keeps chosen Agent Views in context, and hands the browser to you when a person is needed.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"files": [
|