@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Mind Harness",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "permissions": ["tabGroups"],
6
6
  "background": { "service_worker": "sw.js" }
7
7
  }
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: ["--enable-unsafe-extension-debugging", "--keep-alive-for-test"]
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 ? `等你:${title}` : title, waiting ? "red" : "blue");
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 exited. The next call that needs a page starts it again. */
5046
- exited = false;
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.ready = this.launch();
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.documents.set(canonical(shown.pageUrl), shown);
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.documents.set(pageUrl, shown);
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 context.close().catch(() => void 0);
6369
+ await this.closeContext(context);
6322
6370
  return;
6323
6371
  }
6324
6372
  this.context = context;
6325
- await this.windows.attach(context, this.launchOptions.profileDir, { headed: !this.launchOptions.headless });
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 exited starts again
6342
- * here. The exit limit stops unattended restarts; a person's request is
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 exiting; not starting it again yet");
6351
- return Promise.reject(new MhBrowserError(BROWSER_UNAVAILABLE, `the browser exited ${String(this.exits.length)} times within ${String(RELAUNCH_WINDOW_MS / 1e3)} s; try again in a minute`));
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 browser = this.context?.browser();
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 HANDOVER = [
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
- "Call it instead of filling or submitting a login form, a Mind Harness login or consent form, or any form that issues credentials, and whenever only the person can continue."
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 = "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.";
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: HANDOVER,
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: `浏览器交给你了:${reason}`,
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
  });
@@ -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":616,"column":9},
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":616,"column":9},
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 exited. The next call that needs a page starts it again. */
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 exited starts again
229
- * here. The exit limit stops unattended restarts; a person's request is
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: string, reason: string);
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 {
@@ -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: string;
182
+ readonly code: BrowserCode;
180
183
  readonly page?: PageView;
181
- constructor(code: string, message: string, page?: PageView);
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 = "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 = "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 = "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.0",
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": [