@undo76/agent-dom 0.1.0 → 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/README.md CHANGED
@@ -96,17 +96,32 @@ A locator throws if it finds zero or multiple elements. This keeps agent actions
96
96
 
97
97
  ## Ref lifetime
98
98
 
99
- Refs belong to one observation. A `MutationObserver` tracks changes to the document, including attribute and text changes. If the DOM changes, an action using an older ref throws `StaleElementReferenceError` and the agent must observe again.
99
+ A ref identifies one element in the last observation. By default (`stale: "connected"`) it stays valid until that **own** element leaves the document, so unrelated page churn streamed chat, spinners, clocks, framework re-renders does not stop you acting.
100
100
 
101
101
  ```ts
102
- const first = page.observe();
103
- const button = first.findByRole("button").ref;
102
+ const obs = page.observe({ interactiveOnly: true });
103
+ const link = obs.findByRole("link", { name: "Home" }).ref;
104
+
105
+ chat.append(document.createTextNode("thinking…")); // sibling subtree mutates
106
+ page.click(link); // works
107
+
108
+ sidebar.querySelector("a")?.remove();
109
+ page.click(link); // StaleElementReferenceError
110
+ ```
111
+
112
+ Opt into strict snapshot semantics with `stale: "generation"`, where any mutation under the root invalidates every older ref:
113
+
114
+ ```ts
115
+ const strict = createAgentPage(window, { stale: "generation" });
116
+ const ref = strict.observe().findByRole("button").ref;
104
117
 
105
118
  document.body.append(document.createElement("div"));
106
119
 
107
- page.click(button); // throws StaleElementReferenceError
120
+ strict.click(ref); // throws StaleElementReferenceError
108
121
  ```
109
122
 
123
+ The trade is freshness, not safety: under `"connected"` a ref can resolve to an element whose text changed since you observed it. Re-observe when the label is the contract (totals, quantities, confirm dialogs); `disabled` and removed elements are still caught.
124
+
110
125
  ## Browser boundaries
111
126
 
112
127
  The library traverses the current document and open shadow roots. Normal page JavaScript cannot inspect closed shadow roots or cross-origin iframe documents. A browser extension can inject one `AgentPage` into each permitted frame and merge the results in a coordinator.
package/dist/index.cjs CHANGED
@@ -462,12 +462,14 @@ var AgentPage = class {
462
462
  root;
463
463
  #documentGeneration = 0;
464
464
  #snapshotGeneration = 0;
465
+ #stale;
465
466
  #refState;
466
467
  #observation;
467
468
  #observer;
468
469
  constructor(window, options = {}) {
469
470
  this.window = window;
470
471
  this.root = options.root ?? window.document;
472
+ this.#stale = options.stale ?? "connected";
471
473
  const document = ownerDocument(this.root);
472
474
  if (document.defaultView !== window) {
473
475
  throw new TypeError("The root must belong to the supplied window.");
@@ -568,7 +570,9 @@ var AgentPage = class {
568
570
  #freshObservation() {
569
571
  this.#syncMutations();
570
572
  if (!this.#observation || !this.#refState) return this.observe();
571
- if (this.#refState.documentGeneration !== this.#documentGeneration) return this.observe();
573
+ if (this.#stale === "generation" && this.#refState.documentGeneration !== this.#documentGeneration) {
574
+ return this.observe();
575
+ }
572
576
  return this.#observation;
573
577
  }
574
578
  #resolve(ref) {
@@ -576,7 +580,7 @@ var AgentPage = class {
576
580
  const normalized = normalizeRef(ref);
577
581
  const state = this.#refState;
578
582
  if (!state) throw new ElementNotFoundError("Observe the page before using a ref.");
579
- if (state.documentGeneration !== this.#documentGeneration) {
583
+ if (this.#stale === "generation" && state.documentGeneration !== this.#documentGeneration) {
580
584
  throw new StaleElementReferenceError(normalized, state.snapshotGeneration, this.#snapshotGeneration + 1);
581
585
  }
582
586
  const element = state.refs.get(normalized);
package/dist/index.d.cts CHANGED
@@ -74,7 +74,19 @@ type AgentAction = {
74
74
  };
75
75
  interface CreateAgentPageOptions {
76
76
  root?: Document | Element | ShadowRoot;
77
+ /**
78
+ * When a ref stops being usable.
79
+ *
80
+ * - `"connected"` (default): a ref stays valid until *its own* element is removed.
81
+ * Real pages never stop mutating — streaming chat, spinners, clocks, toasts,
82
+ * virtualised lists, framework re-renders. A policy that reacted to all of it
83
+ * would mean no action ever lands on a live page.
84
+ * - `"generation"`: any mutation anywhere under the root invalidates every ref from
85
+ * the previous observation. Opt in for stepwise harnesses that must be current.
86
+ */
87
+ stale?: StalePolicy;
77
88
  }
89
+ type StalePolicy = "generation" | "connected";
78
90
 
79
91
  declare class AgentPage {
80
92
  #private;
@@ -125,4 +137,4 @@ declare class ActionError extends AgentDomError {
125
137
  readonly name: string;
126
138
  }
127
139
 
128
- export { ActionError, type AgentAction, AgentDomError, AgentObservation, AgentPage, type BrowserWindow, type CreateAgentPageOptions, ElementNotFoundError, type ElementState, type Observation, type ObserveOptions, type ObservedElement, type RoleLocatorOptions, StaleElementReferenceError, type TextMatch, createAgentPage };
140
+ export { ActionError, type AgentAction, AgentDomError, AgentObservation, AgentPage, type BrowserWindow, type CreateAgentPageOptions, ElementNotFoundError, type ElementState, type Observation, type ObserveOptions, type ObservedElement, type RoleLocatorOptions, StaleElementReferenceError, type StalePolicy, type TextMatch, createAgentPage };
package/dist/index.d.ts CHANGED
@@ -74,7 +74,19 @@ type AgentAction = {
74
74
  };
75
75
  interface CreateAgentPageOptions {
76
76
  root?: Document | Element | ShadowRoot;
77
+ /**
78
+ * When a ref stops being usable.
79
+ *
80
+ * - `"connected"` (default): a ref stays valid until *its own* element is removed.
81
+ * Real pages never stop mutating — streaming chat, spinners, clocks, toasts,
82
+ * virtualised lists, framework re-renders. A policy that reacted to all of it
83
+ * would mean no action ever lands on a live page.
84
+ * - `"generation"`: any mutation anywhere under the root invalidates every ref from
85
+ * the previous observation. Opt in for stepwise harnesses that must be current.
86
+ */
87
+ stale?: StalePolicy;
77
88
  }
89
+ type StalePolicy = "generation" | "connected";
78
90
 
79
91
  declare class AgentPage {
80
92
  #private;
@@ -125,4 +137,4 @@ declare class ActionError extends AgentDomError {
125
137
  readonly name: string;
126
138
  }
127
139
 
128
- export { ActionError, type AgentAction, AgentDomError, AgentObservation, AgentPage, type BrowserWindow, type CreateAgentPageOptions, ElementNotFoundError, type ElementState, type Observation, type ObserveOptions, type ObservedElement, type RoleLocatorOptions, StaleElementReferenceError, type TextMatch, createAgentPage };
140
+ export { ActionError, type AgentAction, AgentDomError, AgentObservation, AgentPage, type BrowserWindow, type CreateAgentPageOptions, ElementNotFoundError, type ElementState, type Observation, type ObserveOptions, type ObservedElement, type RoleLocatorOptions, StaleElementReferenceError, type StalePolicy, type TextMatch, createAgentPage };
package/dist/index.js CHANGED
@@ -434,12 +434,14 @@ var AgentPage = class {
434
434
  root;
435
435
  #documentGeneration = 0;
436
436
  #snapshotGeneration = 0;
437
+ #stale;
437
438
  #refState;
438
439
  #observation;
439
440
  #observer;
440
441
  constructor(window, options = {}) {
441
442
  this.window = window;
442
443
  this.root = options.root ?? window.document;
444
+ this.#stale = options.stale ?? "connected";
443
445
  const document = ownerDocument(this.root);
444
446
  if (document.defaultView !== window) {
445
447
  throw new TypeError("The root must belong to the supplied window.");
@@ -540,7 +542,9 @@ var AgentPage = class {
540
542
  #freshObservation() {
541
543
  this.#syncMutations();
542
544
  if (!this.#observation || !this.#refState) return this.observe();
543
- if (this.#refState.documentGeneration !== this.#documentGeneration) return this.observe();
545
+ if (this.#stale === "generation" && this.#refState.documentGeneration !== this.#documentGeneration) {
546
+ return this.observe();
547
+ }
544
548
  return this.#observation;
545
549
  }
546
550
  #resolve(ref) {
@@ -548,7 +552,7 @@ var AgentPage = class {
548
552
  const normalized = normalizeRef(ref);
549
553
  const state = this.#refState;
550
554
  if (!state) throw new ElementNotFoundError("Observe the page before using a ref.");
551
- if (state.documentGeneration !== this.#documentGeneration) {
555
+ if (this.#stale === "generation" && state.documentGeneration !== this.#documentGeneration) {
552
556
  throw new StaleElementReferenceError(normalized, state.snapshotGeneration, this.#snapshotGeneration + 1);
553
557
  }
554
558
  const element = state.refs.get(normalized);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undo76/agent-dom",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A browser-native semantic DOM API for AI agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",