e2e 0.16.0 → 0.18.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.
Files changed (256) hide show
  1. package/README.md +10 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +7 -4
  4. package/dist/agent/action-dispatcher.js +4 -5
  5. package/dist/agent/actions.d.ts +32 -23
  6. package/dist/agent/actions.js +38 -24
  7. package/dist/agent/ai-sdk.js +5 -4
  8. package/dist/agent/error.js +3 -3
  9. package/dist/agent/invocation.js +3 -4
  10. package/dist/agent/model/sdk.js +2 -0
  11. package/dist/agent/model/tool-choice.d.ts +9 -0
  12. package/dist/agent/model/tool-choice.js +14 -0
  13. package/dist/agent/observation-feed.d.ts +5 -6
  14. package/dist/agent/observation-feed.js +8 -11
  15. package/dist/agent/observation.d.ts +40 -11
  16. package/dist/agent/observation.js +109 -59
  17. package/dist/agent/phases.d.ts +7 -1
  18. package/dist/agent/phases.js +12 -3
  19. package/dist/agent/point-tap.d.ts +8 -8
  20. package/dist/agent/point-tap.js +1 -1
  21. package/dist/agent/prompts.js +3 -0
  22. package/dist/agent/replay.d.ts +19 -8
  23. package/dist/agent/replay.js +29 -29
  24. package/dist/agent/scroll-target.d.ts +2 -3
  25. package/dist/agent/step-cache.d.ts +43 -15
  26. package/dist/agent/step-cache.js +156 -67
  27. package/dist/agent/tool-loop.js +94 -5
  28. package/dist/cache/anchors.d.ts +72 -23
  29. package/dist/cache/anchors.js +233 -60
  30. package/dist/cache/context.d.ts +25 -14
  31. package/dist/cache/context.js +50 -8
  32. package/dist/cache/decide.d.ts +2 -2
  33. package/dist/cache/decide.js +4 -6
  34. package/dist/cache/identity.d.ts +18 -4
  35. package/dist/cache/identity.js +9 -4
  36. package/dist/cache/recorder.d.ts +15 -1
  37. package/dist/cache/recorder.js +36 -23
  38. package/dist/cache/rekeyed.d.ts +46 -0
  39. package/dist/cache/rekeyed.js +89 -0
  40. package/dist/cache/relocate.d.ts +14 -14
  41. package/dist/cache/relocate.js +33 -25
  42. package/dist/cache/route.d.ts +32 -33
  43. package/dist/cache/route.js +94 -50
  44. package/dist/cache/store.d.ts +6 -0
  45. package/dist/cache/store.js +21 -1
  46. package/dist/cache/trace.d.ts +53 -14
  47. package/dist/cache/trace.js +76 -30
  48. package/dist/cli/bin.js +2 -2
  49. package/dist/cli/index.js +43 -32
  50. package/dist/cli/init/engines.d.ts +1 -3
  51. package/dist/cli/init/engines.js +2 -2
  52. package/dist/cli/init/gateways.d.ts +1 -1
  53. package/dist/cli/init/gateways.js +11 -1
  54. package/dist/cli/init/mcp-config.d.ts +2 -1
  55. package/dist/cli/init/mcp-config.js +8 -3
  56. package/dist/cli/init/sibling-versions.json +1 -1
  57. package/dist/cli/init/versions.d.ts +0 -8
  58. package/dist/cli/init/versions.js +1 -10
  59. package/dist/cli/mcp.d.ts +3 -2
  60. package/dist/cli/mcp.js +52 -16
  61. package/dist/cli/run-output.d.ts +17 -0
  62. package/dist/cli/run-output.js +45 -0
  63. package/dist/collect/collect.js +8 -4
  64. package/dist/collect/registry.js +27 -16
  65. package/dist/config/compiled-files.d.ts +36 -0
  66. package/dist/config/compiled-files.js +50 -0
  67. package/dist/config/diagnose.js +21 -19
  68. package/dist/config/esm-hooks.d.ts +39 -44
  69. package/dist/config/esm-hooks.js +238 -63
  70. package/dist/config/load.d.ts +1 -7
  71. package/dist/config/load.js +23 -66
  72. package/dist/config/register.d.ts +8 -0
  73. package/dist/config/register.js +9 -0
  74. package/dist/config/resolve.d.ts +1 -0
  75. package/dist/config/resolve.js +33 -1
  76. package/dist/config/specifiers.d.ts +19 -0
  77. package/dist/config/specifiers.js +33 -0
  78. package/dist/config/tsconfig.d.ts +56 -0
  79. package/dist/config/tsconfig.js +115 -0
  80. package/dist/config/typescript.d.ts +19 -0
  81. package/dist/config/typescript.js +253 -0
  82. package/dist/engine/contract.d.ts +1 -0
  83. package/dist/engine/index.d.ts +4 -2
  84. package/dist/engine/session.js +2 -2
  85. package/dist/expect/async.js +4 -2
  86. package/dist/expect/poll-scope.d.ts +52 -0
  87. package/dist/expect/poll-scope.js +113 -0
  88. package/dist/expect/poll.js +33 -6
  89. package/dist/expect/values.js +11 -4
  90. package/dist/internal/abandoned.d.ts +11 -0
  91. package/dist/internal/abandoned.js +23 -0
  92. package/dist/internal/brands.d.ts +2 -0
  93. package/dist/internal/brands.js +2 -0
  94. package/dist/internal/client-identity.d.ts +9 -0
  95. package/dist/internal/client-identity.js +14 -0
  96. package/dist/internal/errors.d.ts +10 -3
  97. package/dist/internal/errors.js +23 -8
  98. package/dist/internal/node-version.d.ts +16 -7
  99. package/dist/internal/node-version.js +50 -15
  100. package/dist/internal/package-manager.js +1 -1
  101. package/dist/internal/realm-slot.d.ts +5 -4
  102. package/dist/internal/redact.d.ts +9 -4
  103. package/dist/internal/redact.js +120 -23
  104. package/dist/internal/time.d.ts +26 -4
  105. package/dist/internal/time.js +58 -14
  106. package/dist/internal/urls.d.ts +9 -5
  107. package/dist/internal/urls.js +19 -10
  108. package/dist/locator/engine.d.ts +12 -2
  109. package/dist/locator/engine.js +55 -14
  110. package/dist/locator/screen.js +59 -9
  111. package/dist/mcp/catalog.d.ts +3 -2
  112. package/dist/mcp/catalog.js +6 -6
  113. package/dist/mcp/output.d.ts +59 -0
  114. package/dist/mcp/output.js +129 -0
  115. package/dist/mcp/server.d.ts +4 -0
  116. package/dist/mcp/server.js +9 -12
  117. package/dist/mcp/session.d.ts +8 -1
  118. package/dist/mcp/session.js +56 -15
  119. package/dist/mcp/tools.js +7 -5
  120. package/dist/oauth/chatgpt.js +3 -23
  121. package/dist/oauth/cli.js +1 -0
  122. package/dist/oauth/copilot.d.ts +5 -4
  123. package/dist/oauth/copilot.js +80 -7
  124. package/dist/oauth/grok.js +2 -1
  125. package/dist/oauth/models.js +2 -1
  126. package/dist/oauth/opencode-console.d.ts +10 -0
  127. package/dist/oauth/opencode-console.js +133 -0
  128. package/dist/oauth/providers/github-copilot.d.ts +12 -0
  129. package/dist/oauth/providers/github-copilot.js +77 -23
  130. package/dist/oauth/providers/opencode-console.d.ts +31 -0
  131. package/dist/oauth/providers/opencode-console.js +148 -0
  132. package/dist/oauth/providers.d.ts +2 -6
  133. package/dist/oauth/providers.js +3 -7
  134. package/dist/oauth/responses.d.ts +9 -0
  135. package/dist/oauth/responses.js +23 -0
  136. package/dist/report/build.d.ts +26 -0
  137. package/dist/report/build.js +16 -8
  138. package/dist/report/failure-text.d.ts +6 -1
  139. package/dist/report/failure-text.js +14 -4
  140. package/dist/report/format.d.ts +10 -5
  141. package/dist/report/format.js +12 -5
  142. package/dist/report/junit.js +11 -12
  143. package/dist/report/list.d.ts +12 -0
  144. package/dist/report/list.js +61 -24
  145. package/dist/report/markdown.d.ts +1 -1
  146. package/dist/report/markdown.js +37 -14
  147. package/dist/report/repeats.d.ts +6 -1
  148. package/dist/report/repeats.js +12 -3
  149. package/dist/run/artifacts.d.ts +22 -4
  150. package/dist/run/artifacts.js +71 -3
  151. package/dist/run/execute.d.ts +34 -5
  152. package/dist/run/execute.js +93 -44
  153. package/dist/run/extended-fixtures.js +6 -2
  154. package/dist/run/failure-evidence.js +10 -4
  155. package/dist/run/fixtures.js +6 -8
  156. package/dist/run/in-process.d.ts +2 -0
  157. package/dist/run/in-process.js +52 -10
  158. package/dist/run/last-run.d.ts +42 -8
  159. package/dist/run/last-run.js +121 -14
  160. package/dist/run/process-output.d.ts +73 -0
  161. package/dist/run/process-output.js +130 -0
  162. package/dist/run/provision.d.ts +2 -0
  163. package/dist/run/provision.js +1 -0
  164. package/dist/run/realm.d.ts +4 -1
  165. package/dist/run/realm.js +20 -6
  166. package/dist/run/records.d.ts +20 -4
  167. package/dist/run/records.js +26 -0
  168. package/dist/run/retry.d.ts +10 -1
  169. package/dist/run/retry.js +30 -12
  170. package/dist/run/runner.d.ts +9 -1
  171. package/dist/run/runner.js +101 -39
  172. package/dist/run/scheduler.js +255 -19
  173. package/dist/run/secrecy.d.ts +22 -19
  174. package/dist/run/secrecy.js +22 -18
  175. package/dist/run/serial.d.ts +75 -2
  176. package/dist/run/serial.js +243 -59
  177. package/dist/run/standalone.js +1 -1
  178. package/dist/run/steps.d.ts +0 -6
  179. package/dist/run/steps.js +3 -20
  180. package/dist/run/trace-redaction.d.ts +5 -6
  181. package/dist/run/trace-redaction.js +5 -6
  182. package/dist/run/worker/entry.js +40 -18
  183. package/dist/run/worker/handle.js +10 -5
  184. package/dist/run/worker/protocol.d.ts +85 -3
  185. package/dist/run/worker/session.d.ts +2 -0
  186. package/dist/run/worker/session.js +29 -0
  187. package/dist/telemetry/environment.d.ts +2 -1
  188. package/dist/telemetry/environment.js +12 -2
  189. package/dist/telemetry/events.js +1 -0
  190. package/dist/telemetry/project.d.ts +5 -6
  191. package/dist/telemetry/project.js +6 -11
  192. package/dist/telemetry/store.d.ts +5 -5
  193. package/dist/telemetry/store.js +5 -7
  194. package/dist/telemetry/telemetry.d.ts +2 -3
  195. package/dist/telemetry/telemetry.js +2 -4
  196. package/dist/types.d.ts +15 -6
  197. package/docs/agent-steps.mdx +1 -0
  198. package/docs/agents.mdx +7 -3
  199. package/docs/api-testing.mdx +2 -1
  200. package/docs/authentication.mdx +6 -3
  201. package/docs/browser.mdx +49 -12
  202. package/docs/bug-bash.mdx +2 -1
  203. package/docs/cache.mdx +66 -19
  204. package/docs/ci.mdx +14 -9
  205. package/docs/coding-agents.mdx +2 -1
  206. package/docs/core-concepts.mdx +208 -0
  207. package/docs/debugging.mdx +25 -12
  208. package/docs/decision-models.mdx +142 -0
  209. package/docs/example-projects.mdx +32 -0
  210. package/docs/executors.mdx +7 -2
  211. package/docs/explore.mdx +1 -0
  212. package/docs/github.mdx +14 -5
  213. package/docs/index.mdx +9 -5
  214. package/docs/integrations/eas.mdx +1 -0
  215. package/docs/integrations/index.mdx +1 -0
  216. package/docs/integrations/kernel.mdx +3 -2
  217. package/docs/migrate/cypress.mdx +26 -3
  218. package/docs/migrate/detox.mdx +24 -1
  219. package/docs/migrate/maestro.mdx +28 -4
  220. package/docs/migrate/playwright.mdx +44 -18
  221. package/docs/migrate/selenium.mdx +27 -3
  222. package/docs/mobile-ci.mdx +6 -5
  223. package/docs/mobile.mdx +96 -8
  224. package/docs/models.mdx +60 -9
  225. package/docs/quickstart.mdx +28 -16
  226. package/docs/reference/agent.mdx +13 -3
  227. package/docs/reference/app.mdx +2 -1
  228. package/docs/reference/cli.mdx +41 -18
  229. package/docs/reference/config.mdx +67 -13
  230. package/docs/reference/engine.mdx +25 -13
  231. package/docs/reference/environment.mdx +10 -2
  232. package/docs/reference/errors.mdx +13 -10
  233. package/docs/reference/expect.mdx +36 -13
  234. package/docs/reference/mcp.mdx +21 -8
  235. package/docs/reference/mobile.mdx +24 -13
  236. package/docs/reference/reporters.mdx +9 -2
  237. package/docs/reference/screen.mdx +49 -16
  238. package/docs/reference/test.mdx +32 -5
  239. package/docs/reference/web.mdx +133 -23
  240. package/docs/security.mdx +58 -42
  241. package/docs/subscriptions.mdx +92 -12
  242. package/docs/telemetry.mdx +9 -6
  243. package/docs/tools.mdx +1 -0
  244. package/docs/{starting-your-app.mdx → web.mdx} +157 -11
  245. package/docs/writing-an-engine.mdx +2 -1
  246. package/package.json +25 -8
  247. package/schema/report-v1.schema.json +14 -1
  248. package/skills/e2e/SKILL.md +5 -3
  249. package/skills/e2e/references/agent.md +33 -11
  250. package/skills/e2e/references/debugging.md +4 -4
  251. package/skills/e2e/references/mcp.md +8 -3
  252. package/skills/e2e/references/running.md +15 -9
  253. package/skills/e2e/references/setup.md +45 -16
  254. package/skills/e2e/references/writing-tests.md +35 -18
  255. package/docs/viewports.mdx +0 -146
  256. package/docs/writing-tests.mdx +0 -256
package/README.md CHANGED
@@ -7,6 +7,10 @@
7
7
  <a href="https://tester.army/discord"><img alt="Join the community on Discord" src="https://img.shields.io/badge/Join%20the%20community-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white&labelColor=000000" /></a>
8
8
  </p>
9
9
 
10
+ <p align="center">
11
+ <a href="https://www.star-history.com/tester-army/e2e"><picture><source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=tester-army/e2e&type=trending&theme=dark" /><img alt="GitHub Trending Repository of the Day" src="https://api.star-history.com/badge?repo=tester-army/e2e&type=trending" /></picture></a>
12
+ </p>
13
+
10
14
  # e2e
11
15
 
12
16
  [e2e](https://tester.army/e2e?utm_source=e2e&utm_medium=github&utm_campaign=readme_intro) is an end-to-end testing framework for web and mobile apps. Describe a goal in natural language and an agent drives the app to reach it. Check the result with locators and assertions in the same test.
@@ -40,6 +44,11 @@ npx e2e init
40
44
  config and an example test. The
41
45
  [quickstart](https://e2e.tester.army/docs/quickstart) covers the rest.
42
46
 
47
+ To see a finished setup in your stack, open
48
+ [`examples/`](https://github.com/tester-army/e2e/tree/main/examples):
49
+ Vite, Next.js, Expo, and SwiftUI, each a standalone project with a passing
50
+ suite.
51
+
43
52
  ## Packages
44
53
 
45
54
  | Package | What it does |
@@ -50,6 +59,7 @@ config and an example test. The
50
59
  | [`@e2e-dev/github`](https://www.npmjs.com/package/@e2e-dev/github) | Reporter that posts results as a pull request comment. |
51
60
  | [`@e2e-dev/kernel`](https://www.npmjs.com/package/@e2e-dev/kernel) | Kernel hosted browsers for the web engine. |
52
61
  | [`@e2e-dev/eas`](https://www.npmjs.com/package/@e2e-dev/eas) | EAS Simulators hosted iOS simulators and Android emulators for the mobile engine. |
62
+ | [`@e2e-dev/decision`](https://e2e.tester.army/docs/decision-models) | Decision-model executors for bounded semantic actions and assertions. |
53
63
 
54
64
  ## Documentation
55
65
 
package/dist/.build.json CHANGED
@@ -1 +1 @@
1
- {"version":"0.16.0"}
1
+ {"version":"0.18.0"}
package/dist/agent/act.js CHANGED
@@ -29,6 +29,7 @@ import { projectPriorSteps, serializeLedger } from './ledger.js';
29
29
  import { instantiateLanguageModel } from './model/sdk.js';
30
30
  import { ObservationFeed } from './observation-feed.js';
31
31
  import { recordPolicyEvent } from './phases.js';
32
+ import { appLocation } from '../cache/route.js';
32
33
  import { OperationQueue } from './operation-queue.js';
33
34
  import { StepAccounting } from './step-accounting.js';
34
35
  import { failedStepOutcome, StepTraceSession } from './step-cache.js';
@@ -199,7 +200,10 @@ class ActDispatch {
199
200
  name: agent.executor.name,
200
201
  ...(agent.executor.version === undefined ? {} : { version: agent.executor.version }),
201
202
  },
203
+ // Redacted already (`dispatchAgentStep`): the key digests the context the executor reads.
204
+ agent: { name: agent.name, context: agent.agentContext },
202
205
  redact: runtime.redact,
206
+ redactCut: runtime.redactCut,
203
207
  maxActions: this.accounting.maxActions,
204
208
  stepIndex,
205
209
  });
@@ -421,11 +425,10 @@ class ActDispatch {
421
425
  const feed = this.feed;
422
426
  return {
423
427
  get traceEligible() { return feed.traceEligible; },
424
- observe: async (mode) => screenOf(await this.feed.probe(mode)),
428
+ observe: async (mode) => screenOf(await this.feed.probe(mode), this.runtime.app.base?.origin),
425
429
  actions: this.dispatcher.actions,
426
430
  signal: this.accounting.signal,
427
431
  remainingMs: () => this.accounting.remainingMs(),
428
- redact: this.runtime.redact,
429
432
  replaying: (active) => {
430
433
  this.runtime.steps.replaying(active);
431
434
  this.accounting.replaying(active);
@@ -474,10 +477,10 @@ class ActDispatch {
474
477
  }
475
478
  }
476
479
  /** The replay engine's view of a capture: the nodes, and the viewport a recorded point is checked against. */
477
- function screenOf(observation) {
480
+ function screenOf(observation, appOrigin) {
478
481
  const metadata = {
479
482
  viewport: { width: observation.viewport.width, height: observation.viewport.height },
480
- ...(observation.path === undefined ? {} : { path: observation.path }),
483
+ ...(observation.location === undefined ? {} : { path: appLocation(observation.location, appOrigin) }),
481
484
  };
482
485
  return observation.kind === 'semantic'
483
486
  ? { ...metadata, kind: 'semantic', nodes: observation.nodes }
@@ -487,10 +487,10 @@ export class ActionDispatcher {
487
487
  }
488
488
  async runActionNow(name, body) {
489
489
  this.accounting.reserveAction();
490
- const redact = this.runtime.redact;
490
+ const redaction = { redact: this.runtime.redact, redactCut: this.runtime.redactCut };
491
491
  let action;
492
492
  try {
493
- action = await instrumentPhase(this.runtime, { api: this.accounting.api, kind: 'engine', phase: 'agent.action', name }, body, (committed) => ({ detail: describeAction(committed, redact).summary }));
493
+ action = await instrumentPhase(this.runtime, { api: this.accounting.api, kind: 'engine', phase: 'agent.action', name }, body, (committed) => ({ detail: describeAction(committed, redaction).summary }));
494
494
  }
495
495
  catch (cause) {
496
496
  this.accounting.checkpoint(cause);
@@ -579,9 +579,8 @@ export class ActionDispatcher {
579
579
  * number picks the same one.
580
580
  */
581
581
  placementOf({ node, observation }) {
582
- const redact = this.runtime.redact;
583
- const within = containerKey(node.ref.id, observation.nodes, observation.parents, redact);
584
- const position = describePosition(node, within, observation.nodes, { redact });
582
+ const within = containerKey(node.ref.id, observation.nodes, observation.parents);
583
+ const position = describePosition(node, within, observation.nodes);
585
584
  return {
586
585
  ...(within === undefined ? {} : { within }),
587
586
  ...(position === undefined ? {} : { position }),
@@ -8,51 +8,52 @@
8
8
  * the same descriptor projection. One owner for all three, so a recorded
9
9
  * summary, a live event line, and a relocation candidate can never drift.
10
10
  */
11
- import type { SemanticNode, ViewportPoint, ViewportSize } from '../engine/surface.ts';
11
+ import type { ViewportPoint, ViewportSize } from '../engine/surface.ts';
12
+ import { type NodeRedaction, type RedactedNode } from './observation.ts';
12
13
  import { type NodeActionName, type PointActionName, type TraceTargetDescriptor, type TracePosition } from '../cache/trace.ts';
13
14
  import type { ScrollDirection } from '../types.ts';
14
15
  /** One committed grammar action, addressed by the node it actually ran against. */
15
16
  export type RecordableAction = ({
16
17
  readonly name: NodeActionName;
17
- readonly node: SemanticNode;
18
+ readonly node: RedactedNode;
18
19
  } & Placement) | ({
19
20
  readonly name: 'type';
20
- readonly node: SemanticNode;
21
+ readonly node: RedactedNode;
21
22
  readonly value: string;
22
23
  } & Placement) | ({
23
24
  readonly name: 'typeSecret';
24
- readonly node: SemanticNode;
25
+ readonly node: RedactedNode;
25
26
  readonly secret: string;
26
27
  } & Placement) | ({
27
28
  readonly name: 'press';
28
- readonly node: SemanticNode;
29
+ readonly node: RedactedNode;
29
30
  readonly key: string;
30
31
  } & Placement) | ({
31
32
  readonly name: 'select';
32
- readonly node: SemanticNode;
33
+ readonly node: RedactedNode;
33
34
  readonly value: string;
34
35
  } & Placement) | ({
35
36
  readonly name: 'check';
36
- readonly node: SemanticNode;
37
+ readonly node: RedactedNode;
37
38
  readonly checked: boolean;
38
39
  } & Placement)
39
40
  /** Project-relative paths as the executor gave them; replay authorizes them again. */
40
41
  | ({
41
42
  readonly name: 'upload';
42
- readonly node: SemanticNode;
43
+ readonly node: RedactedNode;
43
44
  readonly paths: readonly string[];
44
45
  } & Placement)
45
46
  /** The dragged node with its placement, and where it was dropped, with its own. */
46
47
  | ({
47
48
  readonly name: 'drag';
48
- readonly node: SemanticNode;
49
+ readonly node: RedactedNode;
49
50
  readonly destination: {
50
- readonly node: SemanticNode;
51
+ readonly node: RedactedNode;
51
52
  } & Placement;
52
53
  } & Placement) | ({
53
54
  readonly name: 'scroll';
54
55
  readonly direction: ScrollDirection;
55
- readonly node?: SemanticNode;
56
+ readonly node?: RedactedNode;
56
57
  /** The share of the viewport the node covered when scrolled, 0 to 1 (`ScrollAction.spans`). */
57
58
  readonly spans?: number;
58
59
  } & Placement)
@@ -61,7 +62,7 @@ export type RecordableAction = ({
61
62
  readonly name: 'scrollUntil';
62
63
  readonly text: string;
63
64
  readonly direction: ScrollDirection;
64
- readonly node?: SemanticNode;
65
+ readonly node?: RedactedNode;
65
66
  readonly spans?: number;
66
67
  /** How many pages it took; in the summary, never in the trace, since a replay pages for itself. */
67
68
  readonly screens: number;
@@ -92,7 +93,7 @@ export type RecordableAction = ({
92
93
  readonly name: PointActionName;
93
94
  readonly point: ViewportPoint;
94
95
  readonly viewport: ViewportSize;
95
- readonly under?: SemanticNode;
96
+ readonly under?: RedactedNode;
96
97
  };
97
98
  /** Where the node sat when it was acted on: its container's key and its place among identical twins. */
98
99
  export interface Placement {
@@ -100,14 +101,17 @@ export interface Placement {
100
101
  readonly position?: TracePosition;
101
102
  }
102
103
  /**
103
- * The key of the nearest named container a node sits in — the first text of
104
- * its row or list item — when that key says more than the node's own name.
105
- * Ten rows each with a "Delete" button are ten identical descriptors; "Delete
106
- * in the row that starts with Budget draft" is one.
104
+ * The key of the nearest named container a node sits in, the first text of
105
+ * its row or list item or else the container's own label, when that key says
106
+ * more than the node's own name. Ten rows each with a "Delete" button are ten
107
+ * identical descriptors; "Delete in the row that starts with Budget draft" is
108
+ * one. A row whose only text is its own (`<li>Alpha <button><svg/></li>`)
109
+ * is keyed by it. Only the nearest container counts: the list around a row
110
+ * with no text names every row alike, so its key would tell no twin apart.
107
111
  */
108
- export declare function containerKey(id: string, nodes: ReadonlyMap<string, SemanticNode>, parents: ReadonlyMap<string, string>, redact: (text: string) => string): string | undefined;
112
+ export declare function containerKey(id: string, nodes: ReadonlyMap<string, RedactedNode>, parents: ReadonlyMap<string, string>): string | undefined;
109
113
  /** Parent id of every non-root node, derived from the tree the node map indexes. */
110
- export declare function parentsOf(nodes: ReadonlyMap<string, SemanticNode>): ReadonlyMap<string, string>;
114
+ export declare function parentsOf(nodes: ReadonlyMap<string, RedactedNode>): ReadonlyMap<string, string>;
111
115
  /** How one committed action reads back: where it acted, and what it did. */
112
116
  export interface DescribedAction {
113
117
  /**
@@ -124,13 +128,18 @@ export interface DescribedAction {
124
128
  /** Where a drag was dropped, described like its target; only a drag has one. */
125
129
  readonly destination?: TraceTargetDescriptor;
126
130
  }
127
- /** Describes one committed action for recording and for the live event. */
128
- export declare function describeAction(action: RecordableAction, redact: (text: string) => string): DescribedAction;
131
+ /**
132
+ * Describes one committed action for recording and for the live event. Its
133
+ * nodes pass the ledger again as it is now: a secret resolved after the
134
+ * capture (inside the action itself) masks what the capture still shows.
135
+ */
136
+ export declare function describeAction(action: RecordableAction, redaction: NodeRedaction): DescribedAction;
129
137
  /**
130
138
  * Builds the durable descriptor for one resolved node: the semantic fields
131
139
  * replay re-finds it by, plus the engine's structural selector hint (kept as
132
140
  * provenance for tuned policies; the conservative relocator ignores it).
133
141
  * Values a secure node holds are never part of it — descriptors carry how a
134
- * node is named, not what it contains.
142
+ * node is named, not what it contains. The node was redacted with its
143
+ * observation (`redactNode`), each field in the one-line form read here too.
135
144
  */
136
- export declare function describeTarget(node: SemanticNode, redact: (text: string) => string): TraceTargetDescriptor | undefined;
145
+ export declare function describeTarget(node: RedactedNode): TraceTargetDescriptor | undefined;
@@ -8,32 +8,33 @@
8
8
  * the same descriptor projection. One owner for all three, so a recorded
9
9
  * summary, a live event line, and a relocation candidate can never drift.
10
10
  */
11
+ import { redactNode } from './observation.js';
11
12
  import { bound, isNodeAction, MAX_TRACE_DESCRIPTOR_CHARS, MAX_TRACE_SUMMARY_CHARS, } from '../cache/trace.js';
12
13
  import { sanitizeText } from '../internal/errors.js';
13
- import { normalizeText } from '../internal/text.js';
14
+ import { collapseText } from '../internal/text.js';
14
15
  /** Roles whose first text names the thing a control belongs to: a row's key, a list item's title. */
15
16
  const CONTAINER_ROLES = new Set(['row', 'listitem', 'article', 'group', 'region', 'dialog', 'tabpanel']);
16
17
  /** Longest container key kept. */
17
18
  const MAX_WITHIN_CHARS = 80;
18
19
  /**
19
- * The key of the nearest named container a node sits in — the first text of
20
- * its row or list item — when that key says more than the node's own name.
21
- * Ten rows each with a "Delete" button are ten identical descriptors; "Delete
22
- * in the row that starts with Budget draft" is one.
20
+ * The key of the nearest named container a node sits in, the first text of
21
+ * its row or list item or else the container's own label, when that key says
22
+ * more than the node's own name. Ten rows each with a "Delete" button are ten
23
+ * identical descriptors; "Delete in the row that starts with Budget draft" is
24
+ * one. A row whose only text is its own (`<li>Alpha <button><svg/></li>`)
25
+ * is keyed by it. Only the nearest container counts: the list around a row
26
+ * with no text names every row alike, so its key would tell no twin apart.
23
27
  */
24
- export function containerKey(id, nodes, parents, redact) {
28
+ export function containerKey(id, nodes, parents) {
25
29
  const node = nodes.get(id);
26
30
  const own = node === undefined ? '' : squash(node.name ?? node.text ?? '');
27
31
  let cursor = parents.get(id);
28
32
  while (cursor !== undefined) {
29
33
  const container = nodes.get(cursor);
30
34
  if (container !== undefined && CONTAINER_ROLES.has(container.role ?? '')) {
31
- const key = firstLeafText(container);
32
- if (key !== undefined) {
33
- const clean = bound(redact(sanitizeText(key)).replace(/\s+/g, ' ').trim(), MAX_WITHIN_CHARS);
34
- return clean === '' || squash(clean) === own ? undefined : clean;
35
- }
36
- return undefined;
35
+ const key = firstLeafText(container) ?? ownLabel(container);
36
+ const clean = key === undefined ? '' : bound(collapseText(key), MAX_WITHIN_CHARS);
37
+ return clean === '' || squash(clean) === own ? undefined : clean;
37
38
  }
38
39
  cursor = parents.get(cursor);
39
40
  }
@@ -60,6 +61,11 @@ function firstLeafText(node) {
60
61
  }
61
62
  return undefined;
62
63
  }
64
+ /** A container's own name or text, when it has one. */
65
+ function ownLabel(node) {
66
+ const label = (node.name ?? node.text ?? '').trim();
67
+ return label === '' ? undefined : label;
68
+ }
63
69
  function squash(text) {
64
70
  return text.replace(/\s+/g, ' ').trim().toLowerCase();
65
71
  }
@@ -74,16 +80,20 @@ const NODE_ACTION_PROSE = {
74
80
  };
75
81
  /** How each point verb reads: `tap the point (x, y)`, `hover over the point (x, y)`. */
76
82
  const POINT_ACTION_PROSE = { tapAt: 'tap', hoverAt: 'hover over' };
77
- /** Describes one committed action for recording and for the live event. */
78
- export function describeAction(action, redact) {
83
+ /**
84
+ * Describes one committed action for recording and for the live event. Its
85
+ * nodes pass the ledger again as it is now: a secret resolved after the
86
+ * capture (inside the action itself) masks what the capture still shows.
87
+ */
88
+ export function describeAction(action, redaction) {
79
89
  const target = 'node' in action
80
- ? describePlaced(action.node, action, redact)
90
+ ? describePlaced(action.node, action, redaction)
81
91
  : 'point' in action
82
- ? describePlaced(action.under, {}, redact)
92
+ ? describePlaced(action.under, {}, redaction)
83
93
  : undefined;
84
- const destination = action.name === 'drag' ? describePlaced(action.destination.node, action.destination, redact) : undefined;
94
+ const destination = action.name === 'drag' ? describePlaced(action.destination.node, action.destination, redaction) : undefined;
85
95
  const where = describeForSummary(target);
86
- const safe = (value) => quote(redact(sanitizeText(value)));
96
+ const safe = (value) => quote(redaction.redact(sanitizeText(value)));
87
97
  const prose = (() => {
88
98
  if (isNodeAction(action))
89
99
  return NODE_ACTION_PROSE[action.name](where);
@@ -134,13 +144,16 @@ export function describeAction(action, redact) {
134
144
  };
135
145
  }
136
146
  /** A node's durable descriptor with the placement it was acted on in; undefined for no node or nothing durable. */
137
- function describePlaced(node, placement, redact) {
138
- const described = node === undefined ? undefined : describeTarget(node, redact);
147
+ function describePlaced(node, placement, redaction) {
148
+ if (node === undefined)
149
+ return undefined;
150
+ const { children: _children, ...fields } = node;
151
+ const described = describeTarget(redactNode(fields, redaction));
139
152
  if (described === undefined)
140
153
  return undefined;
141
154
  return {
142
155
  ...described,
143
- ...(placement.within === undefined ? {} : { within: bound(placement.within, MAX_WITHIN_CHARS) }),
156
+ ...(placement.within === undefined ? {} : { within: bound(redaction.redact(placement.within), MAX_WITHIN_CHARS) }),
144
157
  ...(placement.position === undefined ? {} : { position: placement.position }),
145
158
  };
146
159
  }
@@ -149,13 +162,14 @@ function describePlaced(node, placement, redact) {
149
162
  * replay re-finds it by, plus the engine's structural selector hint (kept as
150
163
  * provenance for tuned policies; the conservative relocator ignores it).
151
164
  * Values a secure node holds are never part of it — descriptors carry how a
152
- * node is named, not what it contains.
165
+ * node is named, not what it contains. The node was redacted with its
166
+ * observation (`redactNode`), each field in the one-line form read here too.
153
167
  */
154
- export function describeTarget(node, redact) {
168
+ export function describeTarget(node) {
155
169
  const field = (value) => {
156
170
  if (value === undefined)
157
171
  return undefined;
158
- const collapsed = normalizeText(redact(sanitizeText(value)));
172
+ const collapsed = collapseText(value);
159
173
  return collapsed === '' ? undefined : bound(collapsed, MAX_TRACE_DESCRIPTOR_CHARS);
160
174
  };
161
175
  const role = field(node.role);
@@ -23,10 +23,11 @@ export function asSdkLanguageModel(instance) {
23
23
  }
24
24
  /**
25
25
  * The cache lives on `globalThis`, not in module state: config and test
26
- * modules load in an isolated tsx realm, so an executor constructed in config
27
- * code and the runner's own model plumbing would otherwise hold two separate
28
- * caches — and two separate `ai` module instances whose error classes fail
29
- * each other's instanceof checks. One slot, one SDK instance, every realm.
26
+ * modules may import a copy of e2e other than the runner's, so an executor
27
+ * constructed in config code and the runner's own model plumbing would
28
+ * otherwise hold two separate caches, and two separate `ai` module instances
29
+ * whose error classes fail each other's instanceof checks. One slot, one SDK
30
+ * instance, every copy.
30
31
  */
31
32
  const cacheSlot = realmSlot('e2e.ai-sdk.v1');
32
33
  function cache() {
@@ -1,5 +1,5 @@
1
1
  /** Runner-owned agent error classification. */
2
- import { classifyError, E2EError } from '../internal/errors.js';
2
+ import { classifyError, E2EError, isE2EError } from '../internal/errors.js';
3
3
  /**
4
4
  * The single source of truth for the closed agent code set: exit/result class
5
5
  * for every code, the blocked category for codes a `blocked` verdict may
@@ -55,7 +55,7 @@ const MODEL_UNREACHABLE_CODES = new Set(Object.entries(AGENT_CODE_TABLE)
55
55
  * model was shown may be what went wrong.
56
56
  */
57
57
  export function isModelUnreachable(error) {
58
- if (!(error instanceof E2EError) && !isAgentError(error))
58
+ if (!isE2EError(error))
59
59
  return false;
60
60
  return MODEL_UNREACHABLE_CODES.has(error.code);
61
61
  }
@@ -95,7 +95,7 @@ const AGENT_CODES = new Set(Object.keys(CATEGORY_BY_CODE));
95
95
  export function toAgentError(cause) {
96
96
  if (isAgentError(cause))
97
97
  return cause;
98
- const classified = cause instanceof E2EError ? cause : classifyError(cause);
98
+ const classified = classifyError(cause);
99
99
  if (AGENT_CODES.has(classified.code)) {
100
100
  return new AgentError(classified.code, classified.message, { cause });
101
101
  }
@@ -104,14 +104,13 @@ export class Invocation {
104
104
  /** Captures one raw observation through the shared race-hardened path. */
105
105
  captureObservation(pixels) {
106
106
  return retryingObserve({
107
- observe: (operation) => this.session.observe(operation, {
108
- pixels,
109
- pixelFallback: this.options.vision !== false && !this.runtime.exposure.withholdsPixels,
110
- }),
107
+ observe: (operation, pixelFallback) => this.session.observe(operation, { pixels, pixelFallback }),
111
108
  operation: () => this.operation(),
112
109
  guard: (cause) => this.checkDeadline(cause),
113
110
  signal: this.runtime.engine.signal,
114
111
  api: this.options.api,
112
+ fallback: this.options.vision !== false,
113
+ tainted: () => this.runtime.exposure.withholdsPixels,
115
114
  });
116
115
  }
117
116
  /**
@@ -9,6 +9,7 @@
9
9
  import { withHint } from '../../internal/errors.js';
10
10
  import { missingModelError } from '../../config/agent.js';
11
11
  import { aiSdk, asSdkLanguageModel, loadAiSdk } from '../ai-sdk.js';
12
+ import { MODEL_REQUEST_HEADERS } from '../../internal/client-identity.js';
12
13
  import { packageVersion } from '../../internal/package-version.js';
13
14
  import { AgentError } from '../error.js';
14
15
  import { isContextOverflow } from './overflow.js';
@@ -75,6 +76,7 @@ export function createModelAdapter(model, agentName = 'default') {
75
76
  maxOutputTokens: call.maxOutputTokens,
76
77
  ...(providerOptions === undefined ? {} : { providerOptions: providerOptions }),
77
78
  maxRetries: TRANSPORT_RETRIES,
79
+ headers: MODEL_REQUEST_HEADERS,
78
80
  abortSignal: call.signal,
79
81
  timeout: call.timeoutMs,
80
82
  };
@@ -33,6 +33,15 @@ export declare function isForcedToolChoiceDowngraded(warning: {
33
33
  export declare function isForcedToolCallSkipped(error: unknown): error is {
34
34
  readonly content?: readonly unknown[];
35
35
  };
36
+ /**
37
+ * Whether a turn forced to call `complete_step` called other tools only. The
38
+ * AI SDK throws `AI_ToolChoiceViolationError` for that too, with the reply's
39
+ * raw content; it is the model breaking the protocol, as when it names a tool
40
+ * the turn does not offer, not the provider failing.
41
+ */
42
+ export declare function isForcedToolCallMismatched(error: unknown): error is {
43
+ readonly content: readonly unknown[];
44
+ };
36
45
  /**
37
46
  * Whether a provider failure says the model does not accept a forced tool
38
47
  * choice. Reads the message and the raw response body of the error and of
@@ -42,6 +42,20 @@ export function isForcedToolCallSkipped(error) {
42
42
  const record = error;
43
43
  return record.name === 'AI_ToolChoiceViolationError' && record.finishReason === 'stop';
44
44
  }
45
+ /**
46
+ * Whether a turn forced to call `complete_step` called other tools only. The
47
+ * AI SDK throws `AI_ToolChoiceViolationError` for that too, with the reply's
48
+ * raw content; it is the model breaking the protocol, as when it names a tool
49
+ * the turn does not offer, not the provider failing.
50
+ */
51
+ export function isForcedToolCallMismatched(error) {
52
+ if (typeof error !== 'object' || error === null)
53
+ return false;
54
+ const record = error;
55
+ return (record.name === 'AI_ToolChoiceViolationError' &&
56
+ Array.isArray(record.content) &&
57
+ record.content.some((part) => typeof part === 'object' && part !== null && 'type' in part && part.type === 'tool-call'));
58
+ }
45
59
  /** Longest cause chain walked; a wrapped error rarely nests deeper. */
46
60
  const MAX_CAUSE_DEPTH = 8;
47
61
  /**
@@ -7,24 +7,23 @@
7
7
  * executor's redacted view of a capture. Nothing else in the step observes
8
8
  * the engine; the dispatcher and the pixel tier ask the feed.
9
9
  */
10
- import type { SemanticNode } from '../engine/surface.ts';
11
10
  import type { StepAgentDetails } from '../run/steps.ts';
12
11
  import { AgentError } from './error.ts';
13
12
  import type { ExecutorObservation, ExecutorObserveOptions, ExecutorTarget } from './executor.ts';
14
13
  import type { AgentContext } from './invocation.ts';
15
- import { type AgentObservation, type SemanticAgentObservation } from './observation.ts';
14
+ import { type AgentObservation, type RedactedNode, type SemanticAgentObservation } from './observation.ts';
16
15
  import type { OperationQueue } from './operation-queue.ts';
17
16
  import { type SettleMode } from './settle-policy.ts';
18
17
  import type { StepAccounting } from './step-accounting.ts';
19
18
  /** A target resolved to a node of the observation it was found in. */
20
19
  export interface Resolved {
21
- readonly node: SemanticNode;
20
+ readonly node: RedactedNode;
22
21
  readonly observation: SemanticAgentObservation;
23
22
  }
24
23
  /** What a target names on the newest screen: the id as the screen lists it, the node or nothing, and the screen looked at. */
25
24
  export interface Lookup {
26
25
  readonly id: string;
27
- readonly node: SemanticNode | undefined;
26
+ readonly node: RedactedNode | undefined;
28
27
  readonly observation: SemanticAgentObservation;
29
28
  }
30
29
  export interface ObservationFeedOptions {
@@ -116,8 +115,8 @@ export declare class ObservationFeed {
116
115
  * through the queued observe: this runs inside a queued action body, and a
117
116
  * queued observation would wait on its own caller.
118
117
  */
119
- relocate(stale: SemanticNode): Promise<{
120
- node: SemanticNode;
118
+ relocate(stale: RedactedNode): Promise<{
119
+ node: RedactedNode;
121
120
  observation: SemanticAgentObservation;
122
121
  } | undefined>;
123
122
  /**
@@ -132,7 +132,7 @@ export class ObservationFeed {
132
132
  ...metadata,
133
133
  text: observation.text,
134
134
  truncated: observation.truncated,
135
- ...(options.tree === true ? { tree: projectTree(observation.tree, this.runtime.redact, this.runtime.app.base?.origin) } : {}),
135
+ ...(options.tree === true ? { tree: projectTree(observation.tree, this.runtime.app.base?.origin) } : {}),
136
136
  ...pixels,
137
137
  };
138
138
  }
@@ -197,8 +197,7 @@ export class ObservationFeed {
197
197
  * queued observation would wait on its own caller.
198
198
  */
199
199
  async relocate(stale) {
200
- const options = { redact: this.runtime.redact };
201
- const descriptor = describeTarget(stale, options.redact);
200
+ const descriptor = describeTarget(stale);
202
201
  if (descriptor === undefined)
203
202
  return undefined;
204
203
  this.accounting.checkpoint();
@@ -206,7 +205,7 @@ export class ObservationFeed {
206
205
  this.publish(observation);
207
206
  if (observation.kind === 'pixels')
208
207
  return undefined;
209
- const relocated = relocateDescriptor(descriptor, observation.nodes, options);
208
+ const relocated = relocateDescriptor(descriptor, observation.nodes);
210
209
  if (relocated.kind !== 'found')
211
210
  return undefined;
212
211
  const node = observation.nodes.get(relocated.id);
@@ -307,14 +306,13 @@ export class ObservationFeed {
307
306
  async capture(pixels) {
308
307
  this.opening = undefined;
309
308
  const raw = await retryingObserve({
310
- observe: (operation) => this.runtime.engine.session.observe(operation, {
311
- pixels,
312
- pixelFallback: !this.runtime.exposure.withholdsPixels,
313
- }),
309
+ observe: (operation, pixelFallback) => this.runtime.engine.session.observe(operation, { pixels, pixelFallback }),
314
310
  operation: () => this.accounting.operation(),
315
311
  guard: (cause) => this.accounting.checkpoint(cause),
316
312
  signal: this.runtime.engine.signal,
317
313
  api: this.accounting.api,
314
+ fallback: true,
315
+ tainted: () => this.runtime.exposure.withholdsPixels,
318
316
  });
319
317
  if (raw.kind === 'pixels') {
320
318
  this.semanticHistory = false;
@@ -376,11 +374,10 @@ export class ObservationFeed {
376
374
  const earlier = this.lastSeen(id)?.node;
377
375
  if (earlier === undefined)
378
376
  return undefined;
379
- const options = { redact: this.runtime.redact };
380
- const descriptor = describeTarget(earlier, options.redact);
377
+ const descriptor = describeTarget(earlier);
381
378
  if (descriptor === undefined)
382
379
  return undefined;
383
- const relocated = relocateDescriptor(descriptor, latest.nodes, options);
380
+ const relocated = relocateDescriptor(descriptor, latest.nodes);
384
381
  return relocated.kind === 'found' ? latest.nodes.get(relocated.id) : undefined;
385
382
  }
386
383
  }