e2e 0.21.1 → 0.21.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.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act-validation.js +1 -0
  4. package/dist/agent/act.js +7 -3
  5. package/dist/agent/action-dispatcher.js +30 -8
  6. package/dist/agent/actions.d.ts +2 -0
  7. package/dist/agent/actions.js +5 -2
  8. package/dist/agent/error.js +1 -0
  9. package/dist/agent/executor.d.ts +14 -6
  10. package/dist/agent/index.js +5 -1
  11. package/dist/agent/invocation.d.ts +3 -0
  12. package/dist/agent/model/sdk.d.ts +5 -0
  13. package/dist/agent/model/sdk.js +18 -1
  14. package/dist/agent/observation.js +1 -0
  15. package/dist/agent/primitives.js +14 -3
  16. package/dist/agent/replay.js +9 -3
  17. package/dist/agent/step-cache.d.ts +3 -2
  18. package/dist/agent/step-cache.js +7 -4
  19. package/dist/agent/tool-loop.js +4 -1
  20. package/dist/cache/context.js +1 -1
  21. package/dist/cache/recorder.d.ts +2 -1
  22. package/dist/cache/recorder.js +11 -2
  23. package/dist/cache/trace-fields.d.ts +10 -0
  24. package/dist/cache/trace-fields.js +17 -0
  25. package/dist/cache/trace.d.ts +9 -1
  26. package/dist/cache/trace.js +26 -7
  27. package/dist/cli/feedback.js +2 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/collect/select.js +12 -2
  30. package/dist/config/agent.d.ts +1 -1
  31. package/dist/config/diagnose.js +9 -3
  32. package/dist/config/esm-hooks.js +3 -2
  33. package/dist/config/resolve.js +3 -2
  34. package/dist/config/typescript.js +52 -2
  35. package/dist/engine/contract.d.ts +3 -2
  36. package/dist/engine/manifest.js +2 -2
  37. package/dist/internal/errors.js +5 -2
  38. package/dist/internal/source.js +2 -0
  39. package/dist/internal/unserved-model.d.ts +5 -0
  40. package/dist/internal/unserved-model.js +22 -0
  41. package/dist/locator/screen.js +1 -2
  42. package/dist/oauth/copilot.js +23 -5
  43. package/dist/oauth/device-code.js +35 -19
  44. package/dist/oauth/errors.d.ts +3 -0
  45. package/dist/oauth/errors.js +26 -11
  46. package/dist/oauth/providers/github-copilot.d.ts +4 -0
  47. package/dist/oauth/providers/github-copilot.js +64 -16
  48. package/dist/oauth/providers/openai.d.ts +2 -2
  49. package/dist/oauth/providers/openai.js +47 -19
  50. package/dist/oauth/providers/opencode-console.js +36 -15
  51. package/dist/oauth/providers/xai.js +18 -9
  52. package/dist/oauth/token-endpoint.d.ts +15 -6
  53. package/dist/oauth/token-endpoint.js +18 -6
  54. package/dist/report/code-frame.js +9 -3
  55. package/dist/report/failure-text.d.ts +10 -2
  56. package/dist/report/failure-text.js +24 -8
  57. package/dist/report/list-model.d.ts +13 -0
  58. package/dist/report/list-steps.d.ts +6 -1
  59. package/dist/report/list-steps.js +29 -0
  60. package/dist/report/list.d.ts +1 -0
  61. package/dist/report/list.js +16 -7
  62. package/dist/report/live-window.js +2 -1
  63. package/dist/report/markdown.js +1 -1
  64. package/dist/run/fixtures.js +2 -0
  65. package/dist/run/host-trace.d.ts +3 -2
  66. package/dist/run/host-trace.js +2 -1
  67. package/dist/run/last-run.d.ts +9 -0
  68. package/dist/run/last-run.js +28 -0
  69. package/dist/run/open-session.d.ts +3 -1
  70. package/dist/run/open-session.js +1 -1
  71. package/dist/run/runner.js +6 -2
  72. package/dist/run/sessions.js +3 -3
  73. package/dist/run/standalone-attempt.d.ts +2 -1
  74. package/dist/run/standalone-attempt.js +3 -1
  75. package/dist/run/steps.d.ts +3 -1
  76. package/dist/run/units.d.ts +8 -0
  77. package/dist/run/units.js +14 -0
  78. package/dist/run/worker-models.d.ts +2 -0
  79. package/dist/run/worker-models.js +7 -0
  80. package/dist/types.d.ts +2 -2
  81. package/docs/authentication.mdx +26 -23
  82. package/docs/browser.mdx +7 -4
  83. package/docs/bug-bash.mdx +2 -2
  84. package/docs/cache.mdx +6 -1
  85. package/docs/ci/bitrise.mdx +1 -1
  86. package/docs/ci/codemagic.mdx +2 -2
  87. package/docs/ci/eas-workflows.mdx +1 -1
  88. package/docs/coding-agents.mdx +7 -35
  89. package/docs/debugging.mdx +24 -36
  90. package/docs/executors.mdx +1 -1
  91. package/docs/github.mdx +2 -1
  92. package/docs/integrations/eas.mdx +1 -1
  93. package/docs/integrations/index.mdx +6 -0
  94. package/docs/integrations/lightpanda.mdx +192 -0
  95. package/docs/integrations/testmu.mdx +152 -0
  96. package/docs/mcp.mdx +123 -0
  97. package/docs/migrate/detox.mdx +1 -1
  98. package/docs/migrate/maestro.mdx +101 -105
  99. package/docs/migrate/playwright.mdx +65 -70
  100. package/docs/mobile.mdx +18 -3
  101. package/docs/models.mdx +4 -2
  102. package/docs/quickstart.mdx +7 -0
  103. package/docs/reference/agent.mdx +369 -277
  104. package/docs/reference/app.mdx +141 -32
  105. package/docs/reference/cli.mdx +674 -558
  106. package/docs/reference/config.mdx +1406 -752
  107. package/docs/reference/engine.mdx +1062 -256
  108. package/docs/reference/environment.mdx +156 -84
  109. package/docs/reference/errors.mdx +106 -72
  110. package/docs/reference/expect.mdx +752 -372
  111. package/docs/reference/mcp.mdx +366 -141
  112. package/docs/reference/mobile.mdx +844 -403
  113. package/docs/reference/reporters.mdx +312 -106
  114. package/docs/reference/runner.mdx +373 -174
  115. package/docs/reference/screen.mdx +931 -398
  116. package/docs/reference/security.mdx +106 -40
  117. package/docs/reference/test.mdx +626 -273
  118. package/docs/reference/web.mdx +956 -533
  119. package/docs/security.mdx +6 -5
  120. package/docs/snippets/model-provider.jsx +763 -0
  121. package/docs/snippets/setup-prompt-card.jsx +22 -0
  122. package/docs/snippets/show-more.jsx +22 -0
  123. package/docs/snippets/video-card.jsx +34 -0
  124. package/docs/snippets/video.jsx +25 -0
  125. package/docs/subscriptions.mdx +10 -3
  126. package/docs/telemetry.mdx +39 -39
  127. package/package.json +3 -2
  128. package/schema/report-v1.schema.json +6 -0
  129. package/skills/e2e/references/agent.md +8 -3
  130. package/skills/e2e/references/debugging.md +1 -1
  131. package/skills/e2e/references/mcp.md +1 -1
  132. package/skills/e2e/references/setup.md +12 -3
  133. package/skills/e2e/references/writing-tests.md +1 -1
@@ -6,100 +6,272 @@ icon: "robot"
6
6
  "twitter:image": "https://e2e.tester.army/docs/images/og/reference/agent.png?v=9a239b0466"
7
7
  ---
8
8
 
9
- Use `act` to interact with the app, `assert` to check the current screen,
10
- `waitFor` to wait for a condition, and `extract` to read structured data.
11
- Each call has its own timeout and model-call budget. The built-in agent
12
- requires a configured model: without one, the first acquisition of the
13
- `agent` fixture records one run-level `MODEL_UNAVAILABLE` and stops the run
14
- (exit 2).
15
- [Custom executors](/executors) can implement `act` and `assert` without one;
16
- `waitFor` and `extract` still need a model.
9
+ The `agent` fixture runs natural-language steps against the app: `act`
10
+ interacts with it, `assert` checks the current screen, `waitFor` waits for a
11
+ condition, and `extract` reads structured data. Each call has its own timeout
12
+ and model-call budget.
13
+
14
+ ```ts title="tests/team.e2e.ts"
15
+ import { test } from 'e2e';
16
+
17
+ test('invite a teammate', async ({ agent }) => {
18
+ await agent.act('invite {email} as an editor', { params: { email: 'ada@example.test' } });
19
+ await agent.waitFor('the members list shows ada@example.test');
20
+ await agent.assert('ada@example.test is listed as an editor');
21
+ });
22
+ ```
23
+
24
+ The built-in agent needs a configured model. Without one, the first
25
+ acquisition of the `agent` fixture records one run-level `MODEL_UNAVAILABLE`
26
+ and stops the run (exit 2). [Custom executors](/executors) can run `act` and
27
+ `assert` without a model; `waitFor` and `extract` still need one.
28
+
29
+ ## Reference
30
+
31
+ | Method | Returns |
32
+ | --- | --- |
33
+ | [`agent.act`](#agent-act) | `Promise<ActResult>` |
34
+ | [`agent.assert`](#agent-assert) | `Promise<void>` |
35
+ | [`agent.waitFor`](#agent-waitfor) | `Promise<void>` |
36
+ | [`agent.extract`](#agent-extract) | `Promise<InferOutput<Schema>>` |
37
+
38
+ [Types](#types) holds the signatures. [Budgets](#budgets) lists each
39
+ method's default timeout, model calls, and action steps, and the rules for
40
+ lowering them. Every method takes `agent`: the
41
+ name of a configured agent (`agents.<name>`) that runs that one call in place
42
+ of the test's.
43
+
44
+ ### agent.act
45
+
46
+ Plans and executes one bounded multi-action flow. A passing verdict resolves
47
+ with an `ActResult`. A failing or blocked one throws an
48
+ [`AgentError`](#agenterror) whose `code` and `blocked` flag say why.
17
49
 
18
50
  ```ts
19
- export interface Agent {
20
- /** Plans and executes a bounded multi-action flow; resolves with what the step did. */
21
- act(instruction: string, options?: ActOptions): Promise<ActResult>;
22
- /** Polls a natural-language condition until true or timed out. */
23
- waitFor(condition: string, options?: WaitForOptions): Promise<void>;
24
- /** Extracts and validates structured screen data. */
25
- extract<Schema extends StandardSchemaV1>(
26
- instruction: string,
27
- options: ExtractOptions<Schema>,
28
- ): Promise<StandardSchemaV1.InferOutput<Schema>>;
29
- /** Judges a natural-language assertion against a fresh observation. */
30
- assert(assertion: string, options?: AssertOptions): Promise<void>;
31
- }
51
+ await agent.act('invite {email} as an editor', { params: { email: 'ada@example.test' } });
52
+
53
+ const member = credentials.user('member');
54
+ const { summary, cache } = await agent.act('Sign in', {
55
+ params: { username: member.username, password: member.password },
56
+ });
32
57
  ```
33
58
 
34
- ## Budgets
59
+ - `instruction` is 1 through 8192 UTF-8 bytes after NFC normalization.
60
+ - The [`ActResult`](#types) carries the step's `summary`, the `modelCalls`
61
+ and `actions` it spent, and `cache`: how the [replay cache](/cache) took
62
+ part.
63
+ - The agent acts through the [action tools](#action-tools) and requests
64
+ screenshots itself through the
65
+ [pixel tools](#pixel-tools-screenshot-and-the-point-verbs).
35
66
 
36
- Model calls count every model request including repair rounds. Action steps
37
- count committed engine actions. Every timeout is additionally capped by the
38
- remaining test timeout.
67
+ | Option | Type | Default |
68
+ | --- | --- | --- |
69
+ | `params` | `AgentParams` | none |
70
+ | `timeout` | `number` | `config.timeout`, 120000 |
71
+ | `maxSteps` | `number` | `agents.<name>.maxSteps`, 25 |
72
+ | `maxModelCalls` | `number` | `agents.<name>.maxModelCalls`, 25 |
73
+ | `agent` | `string` | the test's agent |
74
+
75
+ `params` values come from your code, never from the model. Wrap a value that
76
+ differs on every run in [`unique()`](#unique). The model sees a `Secret` as
77
+ its name and purpose only. Before the runner fills it, it checks the field
78
+ the model picked: an enabled editable input, and a password field for a
79
+ password. `locator.fill` runs no such check, since your code chose the field.
80
+
81
+ A failed verdict is `ACTION_FAILED` unless the executor names a more specific
82
+ code, such as `LOCATOR_NOT_FOUND` or `AUTHENTICATION_FAILED`. A blocked
83
+ verdict means the step could not reach a product verdict. It carries one of
84
+ these codes:
39
85
 
40
- | Method | Model calls | Action steps | Default timeout |
41
- | --- | ---: | ---: | --- |
42
- | `act` | `agents.<name>.maxModelCalls`, 25 | `agents.<name>.maxSteps`, 25 | `config.timeout`, 120000 ms |
43
- | `waitFor` | `agents.<name>.maxModelCalls`, 25 | 0 | `agents.<name>.judgmentTimeout`, 30000 ms |
44
- | `extract` | 2 | 0 | `agents.<name>.judgmentTimeout`, 30000 ms |
45
- | `assert` | 2 | 0 | `agents.<name>.judgmentTimeout`, 30000 ms |
86
+ | Blocked on | Codes |
87
+ | --- | --- |
88
+ | credentials | `AUTH_CREDENTIAL_UNAVAILABLE`, `AUTH_CREDENTIAL_INVALID`, `SECRET_UNAVAILABLE` |
89
+ | environment | `ENVIRONMENT_UNAVAILABLE`, `APP_UNREACHABLE`, `APP_ALREADY_RUNNING` |
90
+ | seed data | `SEED_DATA_MISSING` |
91
+ | test setup | `TEST_SETUP_FAILED`, `APP_NOT_OPEN`, `POLICY_DENIED` |
92
+ | automation | `MODEL_UNAVAILABLE`, `AUTOMATION_UNSUPPORTED`, `STEP_BUDGET_EXHAUSTED`, `STEP_TIMEOUT`, `CONTEXT_OVERFLOW` |
46
93
 
47
- `actionTimeout` limits engine operations, not model calls. Per-call budgets
48
- must be positive integers no larger than the configured limits; a larger one
49
- is `INVALID_ARGUMENT`. All durations are in milliseconds.
94
+ The runner's own stops, and the checks on the call itself:
50
95
 
51
- The `assert` row holds for the built-in agent. When the selected agent has a
52
- custom `executor`, `assert` is dispatched to it as a step: its budget is the
53
- agent's `maxSteps` and `maxModelCalls`, its default timeout is
54
- `config.timeout`, and `vision` on the call is `UNSUPPORTED_CAPABILITY`;
55
- `screenshot` still applies.
96
+ | Throws | When |
97
+ | --- | --- |
98
+ | `STEP_BUDGET_EXHAUSTED`, `STEP_TIMEOUT` | A budget or the deadline ran out. |
99
+ | `CANCELLED` | The runner cancelled the step. |
100
+ | `STEP_NO_CONCLUSION` | The turns ran out without a conclusion. |
101
+ | `CONTEXT_OVERFLOW` | A request exceeded the model's context window. |
102
+ | `MODEL_PROVIDER_FAILED` | The provider's transport failed after its retries. |
103
+ | `MODEL_OUTPUT_INVALID` | A verdict outside the grammar. |
104
+ | `REPLAY_STALE` | Under a strict cache, the step's recording no longer replays. |
105
+ | `INVALID_ARGUMENT` | A malformed instruction, oversized `params`, an unknown option, or a budget above the configured limit. An unknown `agent` name fails before the step opens. |
106
+ | `UNSUPPORTED_CAPABILITY` | `schema` or `vision`, which `act` does not take. |
56
107
 
57
- ## agent.act
108
+ ### agent.assert
109
+
110
+ Checks one fresh observation without polling.
58
111
 
59
112
  ```ts
60
- act(instruction: string, options?: ActOptions): Promise<ActResult>;
113
+ await agent.assert('the dashboard shows a trial badge');
61
114
  ```
62
115
 
63
- Plans and executes one bounded multi-action flow. A passing verdict resolves
64
- with an `ActResult`; a failing or blocked one throws an `AgentError` whose
65
- `code` and `blocked` flag say why.
116
+ - The judge sees the assertion, the current observation, and the agent's
117
+ `context`, never prior steps or `act` summaries. It answers `holds`,
118
+ `fails`, or `inconclusive`.
119
+ - When the subject appears more than once, such as a total in a summary and
120
+ on a pay button, every instance must agree: one that contradicts it fails.
121
+ Name the place (`the order summary total is $42.00`) to judge only that one.
122
+ - A claim about some item, such as `a todo is marked done`, holds when one
123
+ item matches.
124
+
125
+ | Option | Type | Default |
126
+ | --- | --- | --- |
127
+ | `timeout` | `number` | `agents.<name>.judgmentTimeout`, 30000 |
128
+ | `screenshot` | `boolean` | `true` |
129
+ | `vision` | [`VisionMode`](#vision) | `false` |
130
+ | `agent` | `string` | the test's agent |
131
+
132
+ `screenshot` controls the evidence attached after the judgment, passing or
133
+ failing.
134
+ `vision` controls what the model is shown before it. No evidence is captured
135
+ with `screenshot: false`, or once a secret has been filled in the attempt.
136
+
137
+ | Throws | When |
138
+ | --- | --- |
139
+ | `ASSERTION_FAILED` | The judgment is false. |
140
+ | `ASSERTION_INCONCLUSIVE` | The screen did not show enough to decide. |
66
141
 
67
- `instruction` is 1 through 8192 UTF-8 bytes after NFC normalization. An
68
- unknown `agent` name is `INVALID_ARGUMENT` before the step opens.
142
+ Both carry the explanation and, when permitted, a screenshot path. An
143
+ inconclusive judgment made from the tree alone adds a vision hint to its
144
+ message:
69
145
 
70
- Values come from your code through `options.params`, never from the model. The
71
- model sees a `Secret` as its name and purpose only. The runner checks the
72
- field the model picked before it fills the value: it must be an enabled
73
- editable input, and a password goes only into a password field.
74
- `locator.fill` runs no such check, since your code chose the field.
146
+ - The hint reads `pass vision: true when the answer is in pixels`. Until a
147
+ step of the attempt has received pixels, it is worded as a condition
148
+ (`if the engine captures pixels, ...`).
149
+ - There is no hint when the engine declares no screenshot capture, a secret
150
+ was filled, or an earlier step's pixel request was degraded: `vision: true`
151
+ could add no evidence then.
152
+
153
+ <Note>
154
+ A custom `StepExecutor` judges the assertion itself, and `assert` carries the
155
+ executor's explanation alone, with no vision hint. It honors `screenshot`,
156
+ since the runner takes that evidence after the verdict. It receives `vision`
157
+ as `step.vision` when it declares `vision: true`; otherwise `vision` is
158
+ `UNSUPPORTED_CAPABILITY`. Its budget and timeout are under
159
+ [Budgets](#budgets).
160
+ </Note>
161
+
162
+ ### agent.waitFor
163
+
164
+ Polls a natural-language condition until a judgment holds.
75
165
 
76
166
  ```ts
77
- await agent.act('invite {email} as an editor', { params: { email: 'ada@example.test' } });
167
+ await agent.waitFor('the upload progress reports completion', { interval: 500 }); // default: 3000
168
+ ```
78
169
 
79
- const member = credentials.user('member');
80
- const { summary, cache } = await agent.act('Sign in', {
81
- params: { username: member.username, password: member.password },
170
+ - It observes and judges at once, and succeeds on the first judgment that
171
+ holds.
172
+ - After a false or inconclusive judgment it waits out `interval`. Then it
173
+ observes every 500 ms (or every `interval`, when shorter) without a model
174
+ call. It judges again only once the screen differs from the one last
175
+ judged, pixels included under `vision`.
176
+ - Judgments follow the [`agent.assert`](#agent-assert) rules: a value shown
177
+ in more than one place holds only once every instance agrees.
178
+
179
+ | Option | Type | Default |
180
+ | --- | --- | --- |
181
+ | `timeout` | `number` | `agents.<name>.judgmentTimeout`, 30000 |
182
+ | `interval` | `number` | 3000 |
183
+ | `maxModelCalls` | `number` | `agents.<name>.maxModelCalls`, 25 |
184
+ | `vision` | [`VisionMode`](#vision) | `false` |
185
+ | `agent` | `string` | the test's agent |
186
+
187
+ `interval` is the least time between two judgments, 100 through 60000 ms.
188
+
189
+ | Throws | When |
190
+ | --- | --- |
191
+ | `STEP_TIMEOUT` | The deadline expired. |
192
+ | `STEP_BUDGET_EXHAUSTED` | The budget ran out. |
193
+ | `INVALID_ARGUMENT` | An out-of-range option, or one `waitFor` does not take. |
194
+
195
+ `STEP_TIMEOUT` and `STEP_BUDGET_EXHAUSTED` carry the last judgment. When that
196
+ round was inconclusive, they add the same vision hint as `agent.assert`.
197
+
198
+ ### agent.extract
199
+
200
+ Reads structured output from one fresh observation, validates it against
201
+ `schema`, and returns the validated value. It performs no app actions.
202
+
203
+ ```ts
204
+ import { z } from 'zod';
205
+
206
+ const summary = await agent.extract('the workspace name and plan', {
207
+ schema: z.object({ name: z.string(), plan: z.string() }),
82
208
  });
83
209
  ```
84
210
 
85
- The result carries the step's `summary`, the `modelCalls` and `actions` it
86
- spent, and `cache`: how the [replay cache](/cache) took part
87
- (`self-finalized`, `agent-concluded`, or `missed` with a `reason`; absent when
88
- caching is off).
211
+ - When the schema has a JSON Schema projection (zod does), the model receives
212
+ its shape: types, fields, required fields, and allowed literals. Value rules
213
+ such as `min`, `max`, lengths, patterns, and formats stay with your schema,
214
+ which judges the value the model read.
215
+ - The model can answer that the screen does not show the data. That throws
216
+ `ASSERTION_INCONCLUSIVE` with what was missing, instead of returning a
217
+ placeholder such as `""` or `0`. To accept absence as an answer, say so in
218
+ the instruction and the schema (`'the phone number, or null when none is
219
+ shown'` with `z.string().nullable()`).
220
+
221
+ | Option | Type | Default |
222
+ | --- | --- | --- |
223
+ | `schema` | `StandardSchemaV1` | required |
224
+ | `timeout` | `number` | `agents.<name>.judgmentTimeout`, 30000 |
225
+ | `vision` | [`VisionMode`](#vision) | `false` |
226
+ | `agent` | `string` | the test's agent |
89
227
 
90
- ### Action tools
228
+ | Throws | When |
229
+ | --- | --- |
230
+ | `INVALID_ARGUMENT` | The schema is not Standard Schema v1. |
231
+ | `ASSERTION_INCONCLUSIVE` | The screen does not show the data. |
232
+ | `MODEL_OUTPUT_INVALID` | Validation still fails after the repair round. |
91
233
 
92
- The built-in agent offers one tool per verb the target's engine declares.
93
- Every tool takes node ids from the newest observation, returns what changed
94
- on screen, and is recorded for the [replay cache](/cache) with the node it
95
- acted on. A verb the engine does not declare is not offered.
234
+ ## Budgets
235
+
236
+ Model calls count every model request, repair rounds included. Action steps
237
+ count committed engine actions. The remaining test timeout caps every
238
+ timeout.
239
+
240
+ | Method | Model calls | Action steps | Default timeout |
241
+ | --- | ---: | ---: | --- |
242
+ | `act` | `agents.<name>.maxModelCalls`, 25 | `agents.<name>.maxSteps`, 25 | `config.timeout`, 120000 ms |
243
+ | `waitFor` | `agents.<name>.maxModelCalls`, 25 | 0 | `agents.<name>.judgmentTimeout`, 30000 ms |
244
+ | `extract` | 2 | 0 | `agents.<name>.judgmentTimeout`, 30000 ms |
245
+ | `assert` | 2 | 0 | `agents.<name>.judgmentTimeout`, 30000 ms |
246
+
247
+ - `actionTimeout` limits engine operations, not model calls.
248
+ - Per-call budgets (`maxSteps`, `maxModelCalls`) can only lower the
249
+ configured limits. They must be positive integers no larger than those
250
+ limits; a larger one is `INVALID_ARGUMENT`.
251
+ - All durations are in milliseconds.
252
+
253
+ The `assert` row holds for the built-in agent. When the selected agent has a
254
+ custom `executor`, `assert` runs on it as a step. Its budget is the agent's
255
+ `maxSteps` and `maxModelCalls`, and its default timeout is `config.timeout`.
256
+ For `vision` and `screenshot` there, see the Note under
257
+ [`agent.assert`](#agent-assert).
258
+
259
+ ## Action tools
260
+
261
+ The built-in agent offers one tool per verb the target's engine declares, and
262
+ none for a verb it does not. A tool that targets a node takes its id from the
263
+ newest observation. Every tool returns what changed on screen and is
264
+ recorded for the [replay cache](/cache), with its node when it has one. On an engine that declares
265
+ `tapModifiers`, `tap`, `double_tap`, and `right_click` take `modifiers`
266
+ (`Shift`, `Control`, `Alt`, `Meta`, `ControlOrMeta`) to hold keys for the
267
+ click, as a Shift-click extends a selection.
96
268
 
97
269
  | Tool | Engine action | What it does |
98
270
  | --- | --- | --- |
99
- | `tap({ target })` | `tap` | Taps or clicks one node: the gesture for a button, link, menu item, tab, checkbox, row, or field. |
100
- | `double_tap({ target })` | `doubleTap` | Double-clicks one node, for an item that opens or enters editing on the second click. |
271
+ | `tap({ target, modifiers? })` | `tap` | Taps or clicks one node: the gesture for a button, link, menu item, tab, checkbox, row, or field. |
272
+ | `double_tap({ target, modifiers? })` | `doubleTap` | Double-clicks one node, for an item that opens or enters editing on the second click. |
101
273
  | `long_press({ target })` | `longPress` | Presses one node and holds, for a long-press menu or action. |
102
- | `right_click({ target })` | `secondaryTap` | Right-clicks one node to open its context menu. |
274
+ | `right_click({ target, modifiers? })` | `secondaryTap` | Right-clicks one node to open its context menu. |
103
275
  | `hover({ target })` | `hover` | Moves the pointer over one node without pressing: hover menus, flyouts, tooltips, controls revealed on mouse-over. |
104
276
  | `type({ target?, value, replace? })` | `fill`; without `target`, `keyboard` | Replaces the node's value; without a target, types into whatever has focus. |
105
277
  | `type_secret({ target, name })` | `fill` | Fills a secret declared in the step's params. Offered only when the step declares one. |
@@ -107,7 +279,7 @@ acted on. A verb the engine does not declare is not offered.
107
279
  | `select({ target, value })` | `selectOption` | Picks an option by its visible label. |
108
280
  | `check({ target, checked? })` | `check` and `uncheck` | Sets a checkbox, switch, or radio to a state (`true` by default). A control already in that state is left alone, unlike a tap, which flips it. |
109
281
  | `scroll({ direction, target?, times? })` | `swipe` | Scrolls the viewport or one scrollable node, up to 20 screens per call. |
110
- | `scroll_to({ target })` or `scroll_to({ text, direction?, target? })` | `scrollIntoView`; with `text`, `swipe` | With `target` alone, brings one listed node into the viewport, for a node the tree lists below or above the fold. With `text`, pages the list (`target`, else the viewport) screen by screen in `direction` (down by default) until a node reading that text shows (as its whole label, or bounded by nothing of a word, so `Row 12` never stops at `Row 120`; inside the list when `target` names one, so a heading elsewhere reading the same does not end the paging), then brings it in when the engine can: for a row deep in a long or windowed list the tree has not rendered yet. One action however many screens, up to 800; a screen that stops moving or the cap is `LOCATOR_NOT_FOUND`. Each form is offered only with its action kind. |
282
+ | `scroll_to({ target })` or `scroll_to({ text, direction?, target? })` | `scrollIntoView`; with `text`, `swipe` | With `target` alone, brings one listed node into the viewport, for a node the tree lists below or above the fold. With `text`, pages the list (`target`, else the viewport) screen by screen in `direction` (down by default) until a node reading that text shows, then brings it in when the engine can. That is for a row deep in a long or windowed list the tree has not rendered yet. The node's label must be the text, or hold it with no part of a word on either side, so `Row 12` never stops at `Row 120`. When `target` names a list, the node must be inside it, so a heading elsewhere reading the same does not end the paging. One action however many screens, up to 800; a screen that stops moving or the cap is `LOCATOR_NOT_FOUND`. Each form is offered only with its action kind. |
111
283
  | `drag({ target, to })` | `dragTo` | Drags one node and drops it on another: reordering, kanban columns, drop zones. |
112
284
  | `upload({ target, files })` | `setInputFiles` | Attaches files to a file input. Paths are project-relative; see the policy below. |
113
285
  | `navigate({ url })` | `session.open` | Opens a URL or an app-relative path. |
@@ -120,44 +292,53 @@ the next observation checks stability without waiting for a second scroll
120
292
  effect. Engines with `scrollIntoView` still wait for the final adjustment
121
293
  that brings the matched node into view. Replay follows the same rule.
122
294
 
123
- `upload` takes the model's path but the runner decides. Each path is
124
- resolved against the project root the way `locator.setInputFiles` resolves
125
- it, and is refused with `POLICY_DENIED` when it lies outside the project
126
- (through a symlink too) or names a hidden file or a file under a hidden
127
- directory such as `.env`, `.git`, or `.e2e`. A path that names no regular
128
- file is `INVALID_ARGUMENT`, an ordinary action failure the model reads and
129
- corrects. Every refusal and the final allow is a `policy` event named
130
- `upload.path` on the step. Nothing reaches the engine until every path passed.
131
-
132
- The screen the model reads lists the text selected inside the focused field
133
- as `selection="..."` after its value, when the engine reports one: the web
134
- engine does for an input, a textarea, and a `contenteditable` editor, and a
135
- secure field reports none. The runner drops it from a field whose value or
136
- text holds a registered secret, and from a field cut at its length limit when
137
- the selection is not in the part shown, so selected characters of a secret
138
- never reach the model. A `press` of `Shift+ArrowLeft` shows what it selected, and
139
- `times` presses the key several times in one call, so the agent extends a
140
- selection to exactly the word it wants in one more turn before it formats or
141
- replaces it.
142
-
143
- The tap variants are separate tools rather than an option on `tap` on
144
- purpose: offered as an optional `kind`, a model filled it on ordinary
145
- buttons (a double-click on Save), and a double-click on a submit button
146
- submits twice. A tool has to be chosen, and each one says what takes a
147
- plain tap instead.
295
+ `upload` takes the model's path, but the runner decides:
296
+
297
+ - Each path resolves against the project root, as in
298
+ `locator.setInputFiles`.
299
+ - A path outside the project (through a symlink too), or one that names a
300
+ hidden file or a file under a hidden directory such as `.env`, `.git`, or
301
+ `.e2e`, is refused with `POLICY_DENIED`.
302
+ - A path that names no regular file is `INVALID_ARGUMENT`, an ordinary action
303
+ failure the model reads and corrects.
304
+ - Every refusal and the final allow is a `policy` event named `upload.path`
305
+ on the step. Nothing reaches the engine until every path passes.
306
+
307
+ On engines with a keyboard, including the browser and device engines, `type`
308
+ and `press` can omit `target` to act on the focused field. `type` then
309
+ inserts at the caret; `replace: true` clears first. The browser engine
310
+ rejects typing when no editable field has focus. It checks the focused
311
+ element inside shadow roots and frames too, so focus on a button there is
312
+ refused. When an action closes an on-screen keyboard, the result says so. On
313
+ a touch screen that tap was often spent on closing it, and the model acts on
314
+ the control again.
315
+
316
+ When the engine reports text selected inside the focused field, the screen
317
+ the model reads lists it as `selection="..."` after the value. The web engine
318
+ does for an input, a textarea, and a `contenteditable` editor; a secure field
319
+ reports none. The runner drops it from a field whose value or text holds a
320
+ registered secret. It also drops it from a field cut at its length limit when
321
+ the selection is not in the part shown. So selected characters of a secret
322
+ never reach the model. A `press` of `Shift+ArrowLeft` shows what it selected,
323
+ and `times` repeats it in one call. So the agent extends a selection to
324
+ exactly the word it wants in one more turn, before it formats or replaces it.
325
+
326
+ The tap variants are separate tools, not an option on `tap`, on purpose.
327
+ Offered as an optional `kind`, a model filled it on ordinary buttons (a
328
+ double-click on Save), and a double-click on a submit button submits twice.
329
+ A tool has to be chosen, and each one says what takes a plain tap instead.
148
330
 
149
331
  Mobile engines declare `hover`, `doubleTap`, `longPress`, `check`,
150
- `uncheck`, `dragTo`, and `session.back`, so a device offers `hover`,
151
- `double_tap`, `long_press`, `check`, `drag`, and `back`; they declare no
152
- `secondaryTap`, `scrollIntoView`, or `setInputFiles`. Tab switching has no
153
- verb yet: a new tab is invisible to the model.
332
+ `uncheck`, `dragTo`, and `session.back`. So a device offers `hover`,
333
+ `double_tap`, `long_press`, `check`, `drag`, and `back`. They declare no
334
+ `secondaryTap`, `selectOption`, `scrollIntoView`, or `setInputFiles`. Tab
335
+ switching has no verb yet: a new tab is invisible to the model.
154
336
 
155
- ### Pixel tools: screenshot and the point verbs
337
+ ## Pixel tools: screenshot and the point verbs
156
338
 
157
- `act` reads the semantic tree and requests screenshots when it needs them.
158
- A screen with no listed interactive nodes, such as a canvas, opens with a
159
- screenshot. The same model reads the tree and screenshots; there is no
160
- `vision` option for `act`.
339
+ `act` reads the semantic tree and requests screenshots when it needs them;
340
+ the same model reads both. A screen with no listed interactive nodes, such
341
+ as a canvas, opens with a screenshot.
161
342
 
162
343
  The built-in agent offers these tools while no secret has been filled:
163
344
 
@@ -170,172 +351,52 @@ The built-in agent offers these tools while no secret has been filled:
170
351
  | `press_at({ x, y, key })` | Sends a key to the control at the point. If no control is listed, taps to focus and uses the engine's keyboard. |
171
352
  | `select_at({ x, y, value })` | Selects an option by visible label in the control at the point. |
172
353
 
173
- Coordinates refer to the latest screenshot. The model must take a screenshot
174
- before using a point tool and should prefer node ids for listed controls.
175
- The runner checks each point against the tree. A point on a listed control
176
- uses the same policy checks and [replay cache](/cache) recording as an action
177
- by id.
354
+ - Coordinates refer to the latest screenshot. The model must take a
355
+ screenshot before it uses a point tool, and should prefer node ids for
356
+ listed controls.
357
+ - The runner checks each point against the tree. A point on a listed control
358
+ gets the same policy checks and [replay cache](/cache) recording as an
359
+ action by id.
360
+ - `tap_at` needs an engine whose `actions` or `pointerActions` include `tap`,
361
+ and `hover_at` one whose include `hover`. Without the pointer kind, the
362
+ point must land on a listed control.
363
+ - A bare-point tap or hover replays only on a viewport of the recorded size,
364
+ or at the same place inside the listed node that contained it.
365
+ - Once screenshots are in use, the post-action wait also detects pixel
366
+ changes, including canvas redraws.
367
+ - Screenshots have a maximum long side of 768 px. Older images are removed in
368
+ batches, keeping the newest two.
369
+ - A step that sent pixels records `visionInput: true` and
370
+ `metrics.pixelBytes`.
178
371
 
179
- Once screenshots are in use, the post-action wait also detects pixel changes,
180
- including canvas redraws. Screenshots have a maximum long side of 768 px;
181
- older images are removed in batches, keeping the newest two.
182
-
183
- On engines with a keyboard, including the browser and device engines, `type`
184
- and `press` can omit `target` to act on the focused field. `type` inserts at
185
- the caret; `replace: true` clears first. The browser engine rejects typing
186
- when no editable field has focus. The focused element is checked inside shadow
187
- roots and frames, so focus on a button there is refused. Device engines also
188
- offer `dismiss_keyboard`.
189
- When an action closes an on-screen keyboard, the result says so: on a touch
190
- screen that tap was often spent on closing it, and the model acts on the
191
- control again.
372
+ A secret filled during the step withholds every later screenshot
373
+ (`PIXEL_TAINTED`). A step that opens on a tainted viewport offers none of the
374
+ six pixel tools.
192
375
 
193
376
  If semantic capture times out, an engine that proves masking independently
194
377
  can return a fresh screenshot with an explicit unavailable-tree warning.
195
378
  Every `act` observation with unavailable semantics includes that image and
196
- warning automatically. Earlier node ids are invalid, and the next successful
197
- semantic capture presents the whole tree again. The screenshot shows the
198
- current viewport; an unavailable tree does not prove that a control or
199
- message is absent. After a secret fill there is no fallback: `act`, and a
200
- judgment with `vision: true`, fails with `POLICY_DENIED` when semantic
201
- capture times out before the step does.
202
-
203
- If semantic capture fails, trace replay stops and the step cannot record a
204
- new trace entry, even if the tree later recovers.
205
-
206
- A secret filled during the step withholds every later screenshot
207
- (`PIXEL_TAINTED`), and a step that opens on a tainted viewport offers none of
208
- the six pixel tools. `tap_at` needs an engine whose `actions` or
209
- `pointerActions` include `tap`, and `hover_at` one whose include `hover`;
210
- without the pointer kind the point must land on a listed control. A
211
- bare-point tap or hover replays from the [replay cache](/cache) only on a
212
- viewport of the recorded size, or at the same place inside the listed node
213
- that contained it. A step that sent pixels records `visionInput: true` and
214
- `metrics.pixelBytes`.
215
-
216
- A failed or blocked step throws an `AgentError`. A failed verdict is
217
- `ACTION_FAILED` unless the executor names a more specific code, such as
218
- `LOCATOR_NOT_FOUND` or `AUTHENTICATION_FAILED`. A blocked verdict, a step
219
- that could not reach a product verdict, carries one of these codes:
220
-
221
- | Blocked on | Codes |
222
- | --- | --- |
223
- | credentials | `AUTH_CREDENTIAL_UNAVAILABLE`, `AUTH_CREDENTIAL_INVALID`, `SECRET_UNAVAILABLE` |
224
- | environment | `ENVIRONMENT_UNAVAILABLE`, `APP_UNREACHABLE`, `APP_ALREADY_RUNNING` |
225
- | seed data | `SEED_DATA_MISSING` |
226
- | test setup | `TEST_SETUP_FAILED`, `APP_NOT_OPEN`, `POLICY_DENIED` |
227
- | automation | `MODEL_UNAVAILABLE`, `AUTOMATION_UNSUPPORTED`, `STEP_BUDGET_EXHAUSTED`, `STEP_TIMEOUT`, `CONTEXT_OVERFLOW` |
228
-
229
- The runner's own stops are
230
- `STEP_BUDGET_EXHAUSTED` and `STEP_TIMEOUT` when a budget or the deadline ran
231
- out, `CANCELLED`, `STEP_NO_CONCLUSION` when the turns ran out without a
232
- conclusion, `CONTEXT_OVERFLOW`, `MODEL_PROVIDER_FAILED`,
233
- `MODEL_OUTPUT_INVALID` for a verdict outside the grammar, and `REPLAY_STALE`
234
- under a strict cache. Every code is in [Errors](/reference/errors#agenterror).
235
-
236
- `act` also throws `INVALID_ARGUMENT` for a malformed instruction, oversized
237
- `params`, an unknown option, or a budget above the configured limit, and
238
- `UNSUPPORTED_CAPABILITY` for `schema` or `vision`, which `act` does not take.
239
-
240
- ## agent.assert
241
-
242
- ```ts
243
- assert(assertion: string, options?: AssertOptions): Promise<void>;
244
- ```
245
-
246
- Checks one fresh observation without polling. The judge sees only the
247
- assertion and current observation, never prior steps or `act` summaries.
248
- It answers `holds`, `fails`, or `inconclusive`. When what the assertion is
249
- about appears more than once, such as a total in a summary and on a pay
250
- button, every instance must agree: one that contradicts it fails. Name
251
- the place (`the order summary total is $42.00`) to judge only that one. A
252
- claim about some item, such as `a todo is marked done`, still holds when one
253
- item matches.
254
-
255
- Throws `ASSERTION_FAILED` on a false judgment and `ASSERTION_INCONCLUSIVE`
256
- when the screen did not show enough to decide. Both carry the explanation
257
- and, when permitted, a screenshot path. An inconclusive judgment made from
258
- the tree alone adds `pass vision: true when the answer is in pixels` to its
259
- message, worded as a condition (`if the engine captures pixels, ...`) until
260
- a step of the attempt has received pixels. There is no hint when the engine
261
- declares no screenshot capture, a secret was filled, or an earlier step's
262
- pixel request was degraded: `vision: true` could add no evidence then. The
263
- hint is the built-in judge's; with a custom `executor`, `assert` carries the
264
- executor's explanation alone. No evidence is captured with
265
- `screenshot: false` or once a secret has been filled in the attempt.
266
- `screenshot` controls failure evidence attached after the judgment; `vision`
267
- controls what the model is shown before it. A custom `StepExecutor` judges
268
- the assertion itself: it honors `screenshot`, since the runner takes that
269
- evidence after the verdict, and rejects `vision` with
270
- `UNSUPPORTED_CAPABILITY`, since the executor decides what its model sees.
271
-
272
- ```ts
273
- await agent.assert('the dashboard shows a trial badge');
274
- ```
275
-
276
- ## agent.waitFor
277
-
278
- ```ts
279
- waitFor(condition: string, options?: WaitForOptions): Promise<void>;
280
- ```
281
-
282
- Observes and judges at once. After a false or inconclusive judgment it
283
- waits out `interval`, then observes every 500 ms (or every `interval`, when
284
- shorter) without a model call, and judges again only once the screen
285
- differs from the one last judged, pixels included under `vision`. Succeeds
286
- on the first judgment that holds. Judgments follow the `agent.assert`
287
- rules, so a value shown in more than one place holds only once every
288
- instance agrees.
289
-
290
- Throws `STEP_TIMEOUT` on deadline expiry and `STEP_BUDGET_EXHAUSTED` on
291
- budget exhaustion, each carrying the last judgment (with the same vision
292
- hint as `agent.assert` when that round was inconclusive), and
293
- `INVALID_ARGUMENT` for an out-of-range option or one `waitFor` does not
294
- take.
295
-
296
- ```ts
297
- await agent.waitFor('the upload progress reports completion', { interval: 500 });
298
- ```
379
+ warning. Earlier node ids are then invalid, and the next successful semantic
380
+ capture presents the whole tree again. The screenshot shows the current
381
+ viewport; an unavailable tree does not prove that a control or message is
382
+ absent. After a secret fill there is no fallback. When semantic capture times
383
+ out before the step does, `act`, and a judgment with `vision: true`, fails
384
+ with `POLICY_DENIED`.
299
385
 
300
- ## agent.extract
386
+ If semantic capture fails, trace replay stops. The step cannot record a new
387
+ trace entry, even if the tree recovers later.
301
388
 
302
- ```ts
303
- extract<Schema extends StandardSchemaV1>(
304
- instruction: string,
305
- options: ExtractOptions<Schema>,
306
- ): Promise<StandardSchemaV1.InferOutput<Schema>>;
307
- ```
389
+ ## Vision
308
390
 
309
- Takes one fresh observation, requests structured output, validates it, and
310
- returns the validated value. It performs no app actions.
311
-
312
- When the schema has a JSON Schema projection (zod does), the model receives
313
- its shape: types, fields, required fields, and allowed literals. Value rules
314
- such as `min`, `max`, lengths, patterns, and formats stay with your schema,
315
- which judges the value the model read. The model can also answer that the
316
- screen does not show the data, which throws `ASSERTION_INCONCLUSIVE` with
317
- what was missing, instead of returning a placeholder such as `""` or `0`.
318
- To accept absence as an answer, say so in the instruction and the schema
319
- (`'the phone number, or null when none is shown'` with
320
- `z.string().nullable()`).
321
-
322
- The budget is two model calls: one extraction and one repair round for
323
- output that failed validation. Throws `INVALID_ARGUMENT` for a schema that is
324
- not Standard Schema v1, `ASSERTION_INCONCLUSIVE` when the screen does not
325
- show the data, and `MODEL_OUTPUT_INVALID` when validation still fails after
326
- the repair.
391
+ `assert`, `waitFor`, and `extract` accept `vision`; `act` requests its own
392
+ screenshots through the [pixel tools](#pixel-tools-screenshot-and-the-point-verbs).
393
+ A screenshot goes to the model that asked for it, so it must accept images:
394
+ the judge (else `model`) for a judgment, `model` for `act`.
327
395
 
328
396
  ```ts
329
- const summary = await agent.extract('the workspace name and plan', {
330
- schema: z.object({ name: z.string(), plan: z.string() }),
331
- });
397
+ await agent.assert('no overlay covers the checkout form', { vision: 'only' }); // default: false
332
398
  ```
333
399
 
334
- ## Vision
335
-
336
- `assert`, `waitFor`, and `extract` accept `vision`. Screenshots require a
337
- judge model that accepts images.
338
-
339
400
  | `vision` | Evidence sent to the judge |
340
401
  | --- | --- |
341
402
  | `false`, the default | Semantic tree |
@@ -344,30 +405,48 @@ judge model that accepts images.
344
405
 
345
406
  Use `'only'` for visual checks, such as whether an overlay covers a form or a
346
407
  chart looks correct. Screenshots show the current viewport, so scroll to the
347
- relevant content first. `act` requests its own screenshots and does not take
348
- this option.
408
+ relevant content first.
409
+
410
+ Pixels are withheld, and the tree is sent alone, when:
349
411
 
350
- Pixels are withheld and the tree sent alone when a secret fill has tainted
351
- the viewport (`visionDegraded: "PIXEL_TAINTED"` on the step), when the engine
352
- masked fewer regions than the secure fields it observed (`MASKING_UNPROVEN`),
353
- or when the engine cannot capture pixels (`UNSUPPORTED_CAPABILITY`). In
354
- `'only'` mode those cases fail with `POLICY_DENIED` instead.
412
+ - a secret fill has tainted the viewport (`visionDegraded: "PIXEL_TAINTED"`
413
+ on the step),
414
+ - the engine masked fewer regions than the secure fields it observed
415
+ (`MASKING_UNPROVEN`), or
416
+ - the engine cannot capture pixels (`UNSUPPORTED_CAPABILITY`).
417
+
418
+ In `'only'` mode those cases fail with `POLICY_DENIED` instead.
355
419
 
356
420
  With `vision: true` or `'only'`, a recoverable semantic-capture failure can
357
421
  use a fresh screenshot when the engine proves masking independently. With
358
422
  `vision: false`, or after a secret fill, the capture failure stops the
359
- judgment before any model call. A missing tree cannot support a judgment
423
+ judgment before any model call: a missing tree cannot support a judgment
360
424
  about an empty screen.
361
425
 
362
426
  If capture fails before a judgment, the report records zero model calls and
363
427
  omits `observationRevision` and `explanation`. The step error describes the
364
428
  capture failure. An answer the closed grammar rejects is a `schema` event on
365
- the step and earns one repair round; when that answer is rejected too, the
366
- step fails with `MODEL_OUTPUT_INVALID` and its `explanation` is the rejection.
429
+ the step and earns one repair round. When the repaired answer is rejected
430
+ too, the step fails with `MODEL_OUTPUT_INVALID`, and its `explanation` is the
431
+ rejection.
367
432
 
368
433
  ## Types
369
434
 
370
435
  ```ts
436
+ export interface Agent {
437
+ /** Plans and executes a bounded multi-action flow; resolves with what the step did. */
438
+ act(instruction: string, options?: ActOptions): Promise<ActResult>;
439
+ /** Polls a natural-language condition until true or timed out. */
440
+ waitFor(condition: string, options?: WaitForOptions): Promise<void>;
441
+ /** Extracts and validates structured screen data. */
442
+ extract<Schema extends StandardSchemaV1>(
443
+ instruction: string,
444
+ options: ExtractOptions<Schema>,
445
+ ): Promise<StandardSchemaV1.InferOutput<Schema>>;
446
+ /** Judges a natural-language assertion against a fresh observation. */
447
+ assert(assertion: string, options?: AssertOptions): Promise<void>;
448
+ }
449
+
371
450
  /**
372
451
  * Parameters and budgets for one `act` call.
373
452
  */
@@ -471,6 +550,16 @@ export interface StepCacheInfo {
471
550
  notRecorded?: 'param-collision';
472
551
  replayedActions: number;
473
552
  totalActions: number;
553
+ /** The step's cache key, which names its entry file (`<cache.dir>/<entry>.json` in the file store). */
554
+ entry?: string;
555
+ /**
556
+ * Why the replay missed or handed off, past what the reason token says:
557
+ * the recorded action it stopped at and what went wrong there, or the
558
+ * screen the recording started on against the one the step did.
559
+ */
560
+ detail?: string;
561
+ /** Configured model when the consulted entry was recorded; absent on older entries. */
562
+ recordedBy?: { readonly provider: string; readonly model: string };
474
563
  }
475
564
 
476
565
  type TraceReplayMissReason = 'retry' | 'no-entry' | 'invalid-entry' | 'truncated' | 'wrong-context';
@@ -499,12 +588,20 @@ export type AgentParam =
499
588
  Each `StepCacheInfo.reason` is explained under
500
589
  [Why a step was not replayed](/cache#why-a-step-was-not-replayed).
501
590
 
502
- `Unique` is what `unique()` returns: a value that differs on every run, which
503
- the replay cache records as a slot and substitutes on replay. See
504
- [Cache](/cache).
591
+ `Unique` is what [`unique()`](#unique) returns.
505
592
 
506
593
  ## unique
507
594
 
595
+ Marks an `act` param whose value differs on every run. The
596
+ [replay cache](/cache) records a slot instead of the value, and substitutes
597
+ this run's value on replay.
598
+
599
+ ```ts
600
+ await agent.act('create a company named {name}', {
601
+ params: { name: unique(`Acme ${Date.now()}`) },
602
+ });
603
+ ```
604
+
508
605
  ```ts
509
606
  import { unique } from 'e2e';
510
607
 
@@ -521,14 +618,9 @@ export interface Unique {
521
618
  export function unique(value: string): Unique;
522
619
  ```
523
620
 
524
- Throws `INVALID_ARGUMENT` for a value that is not a string, is empty or only
525
- whitespace, or contains the placeholder text `{{param:`.
526
-
527
- ```ts
528
- await agent.act('create a company named {name}', {
529
- params: { name: unique(`Acme ${Date.now()}`) },
530
- });
531
- ```
621
+ | Throws | When |
622
+ | --- | --- |
623
+ | `INVALID_ARGUMENT` | The value is not a string, is empty or only whitespace, or contains the placeholder text `{{param:`. |
532
624
 
533
625
  ## AgentError
534
626