@e2edev/e2e 0.11.0 → 0.13.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 (344) hide show
  1. package/README.md +7 -7
  2. package/dist/agent/act.d.ts +43 -9
  3. package/dist/agent/act.d.ts.map +1 -1
  4. package/dist/agent/act.js +126 -734
  5. package/dist/agent/act.js.map +1 -1
  6. package/dist/agent/action-dispatcher.d.ts +98 -0
  7. package/dist/agent/action-dispatcher.d.ts.map +1 -0
  8. package/dist/agent/action-dispatcher.js +319 -0
  9. package/dist/agent/action-dispatcher.js.map +1 -0
  10. package/dist/agent/actions.d.ts +16 -1
  11. package/dist/agent/actions.d.ts.map +1 -1
  12. package/dist/agent/actions.js +5 -1
  13. package/dist/agent/actions.js.map +1 -1
  14. package/dist/agent/default-agent.d.ts +17 -8
  15. package/dist/agent/default-agent.d.ts.map +1 -1
  16. package/dist/agent/default-agent.js +65 -43
  17. package/dist/agent/default-agent.js.map +1 -1
  18. package/dist/agent/executor.d.ts +30 -2
  19. package/dist/agent/executor.d.ts.map +1 -1
  20. package/dist/agent/executor.js.map +1 -1
  21. package/dist/agent/index.js +1 -1
  22. package/dist/agent/index.js.map +1 -1
  23. package/dist/agent/interactive-step.d.ts +38 -0
  24. package/dist/agent/interactive-step.d.ts.map +1 -0
  25. package/dist/agent/interactive-step.js +131 -0
  26. package/dist/agent/interactive-step.js.map +1 -0
  27. package/dist/agent/invocation.d.ts +4 -9
  28. package/dist/agent/invocation.d.ts.map +1 -1
  29. package/dist/agent/invocation.js +1 -6
  30. package/dist/agent/invocation.js.map +1 -1
  31. package/dist/agent/model/sdk.d.ts +1 -1
  32. package/dist/agent/model/sdk.js +1 -1
  33. package/dist/agent/model/tool-choice.d.ts +21 -0
  34. package/dist/agent/model/tool-choice.d.ts.map +1 -0
  35. package/dist/agent/model/tool-choice.js +49 -0
  36. package/dist/agent/model/tool-choice.js.map +1 -0
  37. package/dist/agent/observation-feed.d.ts +123 -0
  38. package/dist/agent/observation-feed.d.ts.map +1 -0
  39. package/dist/agent/observation-feed.js +281 -0
  40. package/dist/agent/observation-feed.js.map +1 -0
  41. package/dist/agent/observation.d.ts +14 -1
  42. package/dist/agent/observation.d.ts.map +1 -1
  43. package/dist/agent/observation.js +58 -2
  44. package/dist/agent/observation.js.map +1 -1
  45. package/dist/agent/operation-queue.d.ts +21 -0
  46. package/dist/agent/operation-queue.d.ts.map +1 -0
  47. package/dist/agent/operation-queue.js +27 -0
  48. package/dist/agent/operation-queue.js.map +1 -0
  49. package/dist/agent/pixels.d.ts +21 -0
  50. package/dist/agent/pixels.d.ts.map +1 -0
  51. package/dist/agent/pixels.js +85 -0
  52. package/dist/agent/pixels.js.map +1 -0
  53. package/dist/agent/point-tap.d.ts +52 -0
  54. package/dist/agent/point-tap.d.ts.map +1 -0
  55. package/dist/agent/point-tap.js +88 -0
  56. package/dist/agent/point-tap.js.map +1 -0
  57. package/dist/agent/primitives.d.ts +7 -1
  58. package/dist/agent/primitives.d.ts.map +1 -1
  59. package/dist/agent/primitives.js +96 -25
  60. package/dist/agent/primitives.js.map +1 -1
  61. package/dist/agent/replay.d.ts +8 -3
  62. package/dist/agent/replay.d.ts.map +1 -1
  63. package/dist/agent/replay.js +44 -15
  64. package/dist/agent/replay.js.map +1 -1
  65. package/dist/agent/screen-update.d.ts +50 -18
  66. package/dist/agent/screen-update.d.ts.map +1 -1
  67. package/dist/agent/screen-update.js +76 -94
  68. package/dist/agent/screen-update.js.map +1 -1
  69. package/dist/agent/step-accounting.d.ts +95 -0
  70. package/dist/agent/step-accounting.d.ts.map +1 -0
  71. package/dist/agent/step-accounting.js +201 -0
  72. package/dist/agent/step-accounting.js.map +1 -0
  73. package/dist/agent/step-cache.d.ts.map +1 -1
  74. package/dist/agent/step-cache.js +2 -2
  75. package/dist/agent/step-cache.js.map +1 -1
  76. package/dist/agent/tool-loop.d.ts +4 -4
  77. package/dist/agent/tool-loop.d.ts.map +1 -1
  78. package/dist/agent/tool-loop.js +109 -25
  79. package/dist/agent/tool-loop.js.map +1 -1
  80. package/dist/agent/transcript-compaction.d.ts +38 -0
  81. package/dist/agent/transcript-compaction.d.ts.map +1 -0
  82. package/dist/agent/transcript-compaction.js +165 -0
  83. package/dist/agent/transcript-compaction.js.map +1 -0
  84. package/dist/cache/recorder.d.ts.map +1 -1
  85. package/dist/cache/recorder.js +20 -0
  86. package/dist/cache/recorder.js.map +1 -1
  87. package/dist/cache/trace.d.ts +28 -1
  88. package/dist/cache/trace.d.ts.map +1 -1
  89. package/dist/cache/trace.js +44 -0
  90. package/dist/cache/trace.js.map +1 -1
  91. package/dist/cli/docs-url.d.ts +1 -1
  92. package/dist/cli/docs-url.d.ts.map +1 -1
  93. package/dist/cli/docs-url.js +1 -1
  94. package/dist/cli/docs-url.js.map +1 -1
  95. package/dist/cli/index.d.ts.map +1 -1
  96. package/dist/cli/index.js +71 -26
  97. package/dist/cli/index.js.map +1 -1
  98. package/dist/cli/init/engine-versions.json +1 -1
  99. package/dist/cli/init/engines.d.ts +26 -16
  100. package/dist/cli/init/engines.d.ts.map +1 -1
  101. package/dist/cli/init/engines.js +33 -24
  102. package/dist/cli/init/engines.js.map +1 -1
  103. package/dist/cli/init/gateways.js +3 -3
  104. package/dist/cli/init/mcp-config.d.ts +31 -0
  105. package/dist/cli/init/mcp-config.d.ts.map +1 -0
  106. package/dist/cli/init/mcp-config.js +72 -0
  107. package/dist/cli/init/mcp-config.js.map +1 -0
  108. package/dist/cli/init/scaffold.d.ts +1 -1
  109. package/dist/cli/init/scaffold.js +2 -2
  110. package/dist/cli/init/scaffold.js.map +1 -1
  111. package/dist/cli/init.d.ts.map +1 -1
  112. package/dist/cli/init.js +65 -29
  113. package/dist/cli/init.js.map +1 -1
  114. package/dist/cli/mcp.d.ts +15 -0
  115. package/dist/cli/mcp.d.ts.map +1 -0
  116. package/dist/cli/mcp.js +63 -0
  117. package/dist/cli/mcp.js.map +1 -0
  118. package/dist/collect/collect.d.ts +8 -3
  119. package/dist/collect/collect.d.ts.map +1 -1
  120. package/dist/collect/collect.js +29 -5
  121. package/dist/collect/collect.js.map +1 -1
  122. package/dist/collect/registry.d.ts +20 -5
  123. package/dist/collect/registry.d.ts.map +1 -1
  124. package/dist/collect/registry.js +162 -56
  125. package/dist/collect/registry.js.map +1 -1
  126. package/dist/collect/select.d.ts +9 -2
  127. package/dist/collect/select.d.ts.map +1 -1
  128. package/dist/collect/select.js +60 -12
  129. package/dist/collect/select.js.map +1 -1
  130. package/dist/config/agent.d.ts +1 -7
  131. package/dist/config/agent.d.ts.map +1 -1
  132. package/dist/config/agent.js +5 -8
  133. package/dist/config/agent.js.map +1 -1
  134. package/dist/config/app.d.ts +24 -2
  135. package/dist/config/app.d.ts.map +1 -1
  136. package/dist/config/app.js +82 -15
  137. package/dist/config/app.js.map +1 -1
  138. package/dist/config/resolve.d.ts +23 -6
  139. package/dist/config/resolve.d.ts.map +1 -1
  140. package/dist/config/resolve.js +34 -14
  141. package/dist/config/resolve.js.map +1 -1
  142. package/dist/credentials.d.ts +6 -0
  143. package/dist/credentials.d.ts.map +1 -1
  144. package/dist/credentials.js +9 -0
  145. package/dist/credentials.js.map +1 -1
  146. package/dist/engine/contract.d.ts +9 -2
  147. package/dist/engine/contract.d.ts.map +1 -1
  148. package/dist/engine/contract.js.map +1 -1
  149. package/dist/engine/index.d.ts +18 -4
  150. package/dist/engine/index.d.ts.map +1 -1
  151. package/dist/engine/index.js +8 -0
  152. package/dist/engine/index.js.map +1 -1
  153. package/dist/engine/session.d.ts.map +1 -1
  154. package/dist/engine/session.js +3 -0
  155. package/dist/engine/session.js.map +1 -1
  156. package/dist/engine/surface.d.ts +6 -2
  157. package/dist/engine/surface.d.ts.map +1 -1
  158. package/dist/engine/surface.js.map +1 -1
  159. package/dist/expect/attempt.d.ts +15 -0
  160. package/dist/expect/attempt.d.ts.map +1 -0
  161. package/dist/expect/attempt.js +33 -0
  162. package/dist/expect/attempt.js.map +1 -0
  163. package/dist/expect/index.d.ts +7 -5
  164. package/dist/expect/index.d.ts.map +1 -1
  165. package/dist/expect/index.js +8 -1
  166. package/dist/expect/index.js.map +1 -1
  167. package/dist/expect/poll.d.ts +5 -0
  168. package/dist/expect/poll.d.ts.map +1 -0
  169. package/dist/expect/poll.js +118 -0
  170. package/dist/expect/poll.js.map +1 -0
  171. package/dist/expect/values.d.ts.map +1 -1
  172. package/dist/expect/values.js +6 -1
  173. package/dist/expect/values.js.map +1 -1
  174. package/dist/explore/body.d.ts.map +1 -1
  175. package/dist/explore/body.js +2 -0
  176. package/dist/explore/body.js.map +1 -1
  177. package/dist/explore/executor.js +16 -11
  178. package/dist/explore/executor.js.map +1 -1
  179. package/dist/explore/index.d.ts +3 -2
  180. package/dist/explore/index.d.ts.map +1 -1
  181. package/dist/explore/index.js +11 -9
  182. package/dist/explore/index.js.map +1 -1
  183. package/dist/explore/plan.d.ts.map +1 -1
  184. package/dist/explore/plan.js +16 -5
  185. package/dist/explore/plan.js.map +1 -1
  186. package/dist/explore/progress.d.ts +43 -0
  187. package/dist/explore/progress.d.ts.map +1 -0
  188. package/dist/explore/progress.js +9 -0
  189. package/dist/explore/progress.js.map +1 -0
  190. package/dist/explore/state.d.ts +13 -12
  191. package/dist/explore/state.d.ts.map +1 -1
  192. package/dist/explore/state.js +28 -11
  193. package/dist/explore/state.js.map +1 -1
  194. package/dist/internal/ai-trace.d.ts +2 -0
  195. package/dist/internal/ai-trace.d.ts.map +1 -1
  196. package/dist/internal/ai-trace.js +3 -0
  197. package/dist/internal/ai-trace.js.map +1 -1
  198. package/dist/internal/brands.d.ts +2 -0
  199. package/dist/internal/brands.d.ts.map +1 -1
  200. package/dist/internal/brands.js +2 -0
  201. package/dist/internal/brands.js.map +1 -1
  202. package/dist/internal/geometry.d.ts +13 -0
  203. package/dist/internal/geometry.d.ts.map +1 -0
  204. package/dist/internal/geometry.js +16 -0
  205. package/dist/internal/geometry.js.map +1 -0
  206. package/dist/internal/ids.d.ts +2 -2
  207. package/dist/internal/ids.d.ts.map +1 -1
  208. package/dist/internal/ids.js +3 -3
  209. package/dist/internal/ids.js.map +1 -1
  210. package/dist/internal/package-manager.d.ts +4 -0
  211. package/dist/internal/package-manager.d.ts.map +1 -1
  212. package/dist/internal/package-manager.js +26 -0
  213. package/dist/internal/package-manager.js.map +1 -1
  214. package/dist/internal/urls.d.ts +6 -0
  215. package/dist/internal/urls.d.ts.map +1 -1
  216. package/dist/internal/urls.js +28 -0
  217. package/dist/internal/urls.js.map +1 -1
  218. package/dist/mcp/catalog.d.ts +51 -0
  219. package/dist/mcp/catalog.d.ts.map +1 -0
  220. package/dist/mcp/catalog.js +141 -0
  221. package/dist/mcp/catalog.js.map +1 -0
  222. package/dist/mcp/config.d.ts +11 -0
  223. package/dist/mcp/config.d.ts.map +1 -0
  224. package/dist/mcp/config.js +16 -0
  225. package/dist/mcp/config.js.map +1 -0
  226. package/dist/mcp/server.d.ts +29 -0
  227. package/dist/mcp/server.d.ts.map +1 -0
  228. package/dist/mcp/server.js +86 -0
  229. package/dist/mcp/server.js.map +1 -0
  230. package/dist/mcp/session.d.ts +68 -0
  231. package/dist/mcp/session.d.ts.map +1 -0
  232. package/dist/mcp/session.js +319 -0
  233. package/dist/mcp/session.js.map +1 -0
  234. package/dist/mcp/tools.d.ts +77 -0
  235. package/dist/mcp/tools.d.ts.map +1 -0
  236. package/dist/mcp/tools.js +179 -0
  237. package/dist/mcp/tools.js.map +1 -0
  238. package/dist/report/build.d.ts +8 -0
  239. package/dist/report/build.d.ts.map +1 -1
  240. package/dist/report/build.js +3 -1
  241. package/dist/report/build.js.map +1 -1
  242. package/dist/report/format.d.ts +19 -0
  243. package/dist/report/format.d.ts.map +1 -1
  244. package/dist/report/format.js +42 -0
  245. package/dist/report/format.js.map +1 -1
  246. package/dist/report/junit.d.ts.map +1 -1
  247. package/dist/report/junit.js +4 -1
  248. package/dist/report/junit.js.map +1 -1
  249. package/dist/report/list-explore.d.ts +109 -0
  250. package/dist/report/list-explore.d.ts.map +1 -0
  251. package/dist/report/list-explore.js +388 -0
  252. package/dist/report/list-explore.js.map +1 -0
  253. package/dist/report/list.d.ts +31 -1
  254. package/dist/report/list.d.ts.map +1 -1
  255. package/dist/report/list.js +114 -16
  256. package/dist/report/list.js.map +1 -1
  257. package/dist/report/running-tree.d.ts +7 -7
  258. package/dist/report/running-tree.d.ts.map +1 -1
  259. package/dist/report/running-tree.js +12 -13
  260. package/dist/report/running-tree.js.map +1 -1
  261. package/dist/run/app-ports.d.ts +17 -0
  262. package/dist/run/app-ports.d.ts.map +1 -0
  263. package/dist/run/app-ports.js +74 -0
  264. package/dist/run/app-ports.js.map +1 -0
  265. package/dist/run/events.d.ts +21 -7
  266. package/dist/run/events.d.ts.map +1 -1
  267. package/dist/run/events.js.map +1 -1
  268. package/dist/run/execute.d.ts +2 -2
  269. package/dist/run/execute.d.ts.map +1 -1
  270. package/dist/run/execute.js +42 -20
  271. package/dist/run/execute.js.map +1 -1
  272. package/dist/run/extended-fixtures.d.ts +33 -0
  273. package/dist/run/extended-fixtures.d.ts.map +1 -0
  274. package/dist/run/extended-fixtures.js +91 -0
  275. package/dist/run/extended-fixtures.js.map +1 -0
  276. package/dist/run/fixtures.d.ts +10 -4
  277. package/dist/run/fixtures.d.ts.map +1 -1
  278. package/dist/run/fixtures.js +40 -27
  279. package/dist/run/fixtures.js.map +1 -1
  280. package/dist/run/in-process.d.ts.map +1 -1
  281. package/dist/run/in-process.js +4 -3
  282. package/dist/run/in-process.js.map +1 -1
  283. package/dist/run/managed-process.d.ts +18 -0
  284. package/dist/run/managed-process.d.ts.map +1 -1
  285. package/dist/run/managed-process.js +101 -6
  286. package/dist/run/managed-process.js.map +1 -1
  287. package/dist/run/provision.d.ts +51 -0
  288. package/dist/run/provision.d.ts.map +1 -0
  289. package/dist/run/provision.js +94 -0
  290. package/dist/run/provision.js.map +1 -0
  291. package/dist/run/realm.d.ts +5 -4
  292. package/dist/run/realm.d.ts.map +1 -1
  293. package/dist/run/realm.js +8 -7
  294. package/dist/run/realm.js.map +1 -1
  295. package/dist/run/records.d.ts +4 -0
  296. package/dist/run/records.d.ts.map +1 -1
  297. package/dist/run/runner.d.ts +14 -5
  298. package/dist/run/runner.d.ts.map +1 -1
  299. package/dist/run/runner.js +28 -71
  300. package/dist/run/runner.js.map +1 -1
  301. package/dist/run/scheduler.d.ts +5 -2
  302. package/dist/run/scheduler.d.ts.map +1 -1
  303. package/dist/run/scheduler.js +13 -10
  304. package/dist/run/scheduler.js.map +1 -1
  305. package/dist/run/serial.d.ts +1 -1
  306. package/dist/run/serial.d.ts.map +1 -1
  307. package/dist/run/serial.js +7 -2
  308. package/dist/run/serial.js.map +1 -1
  309. package/dist/run/standalone.d.ts +63 -0
  310. package/dist/run/standalone.d.ts.map +1 -0
  311. package/dist/run/standalone.js +152 -0
  312. package/dist/run/standalone.js.map +1 -0
  313. package/dist/run/units.d.ts +6 -3
  314. package/dist/run/units.d.ts.map +1 -1
  315. package/dist/run/units.js +0 -0
  316. package/dist/run/units.js.map +1 -1
  317. package/dist/run/worker/entry.js +4 -3
  318. package/dist/run/worker/entry.js.map +1 -1
  319. package/dist/run/worker/protocol.d.ts +12 -1
  320. package/dist/run/worker/protocol.d.ts.map +1 -1
  321. package/dist/run/worker/protocol.js.map +1 -1
  322. package/dist/run/worker/session.d.ts +3 -4
  323. package/dist/run/worker/session.d.ts.map +1 -1
  324. package/dist/run/worker/session.js +5 -4
  325. package/dist/run/worker/session.js.map +1 -1
  326. package/dist/types.d.ts +77 -17
  327. package/dist/types.d.ts.map +1 -1
  328. package/package.json +11 -7
  329. package/skills/e2e/SKILL.md +9 -7
  330. package/skills/e2e/references/agent.md +27 -12
  331. package/skills/e2e/references/debugging.md +5 -5
  332. package/skills/e2e/references/explore.md +15 -9
  333. package/skills/e2e/references/mcp.md +88 -0
  334. package/skills/e2e/references/running.md +23 -18
  335. package/skills/e2e/references/setup.md +32 -18
  336. package/skills/e2e/references/writing-tests.md +12 -5
  337. package/dist/agent/model/router.d.ts +0 -36
  338. package/dist/agent/model/router.d.ts.map +0 -1
  339. package/dist/agent/model/router.js +0 -45
  340. package/dist/agent/model/router.js.map +0 -1
  341. package/dist/explore/reporter.d.ts +0 -11
  342. package/dist/explore/reporter.d.ts.map +0 -1
  343. package/dist/explore/reporter.js +0 -54
  344. package/dist/explore/reporter.js.map +0 -1
package/dist/agent/act.js CHANGED
@@ -1,76 +1,35 @@
1
1
  /**
2
2
  * Harness-owned dispatch of one `agent.act()` step.
3
3
  *
4
- * The harness opens the step, owns the deadline, the action budget, origin
5
- * policy, observation redaction, and recording then hands the step to the
6
- * configured executor and maps its verdict back onto the runner's error
7
- * taxonomy. The executor never touches the engine: everything bottoms out in
8
- * the context built here, on the same accounting core (phases.ts) the
9
- * locate/judgment tier runs on. Trace-cache participation replay, live
10
- * recording, staging lives beside the dispatch in `StepTraceSession`.
4
+ * The harness opens the step and hands only the thinking to the configured
5
+ * executor; everything the executor can touch is built here from four
6
+ * collaborators, each owning one concern of the step:
7
+ *
8
+ * - `StepAccounting`: the clock, the budgets, the hard stop, the metrics.
9
+ * - `ObservationFeed`: every capture, the settle, the id ring, the pixel decision.
10
+ * - `ActionDispatcher`: the verb table, target resolution, recording.
11
+ * - `StepTraceSession`: replay, live recording, and the stage-or-evict decision.
12
+ *
13
+ * Capabilities beyond the grammar (secret fills, the trace cache) are modules
14
+ * over those collaborators, borrowed through a small host each. What stays in
15
+ * this file is the wiring, the executor's context, and the mapping of the
16
+ * executor's verdict onto the runner's error taxonomy.
11
17
  */
12
18
  import { writeFileSync } from 'node:fs';
13
19
  import { writeFile } from 'node:fs/promises';
14
20
  import { join } from 'node:path';
15
- import { asEngineError, ConfigurationError, TestError } from '../internal/errors.js';
16
- import { timestamp } from '../internal/ids.js';
17
- import { resolveNavigationUrl } from '../internal/urls.js';
21
+ import { ConfigurationError } from '../internal/errors.js';
18
22
  import { AgentError, CATEGORY_BY_CODE, isAgentError, toAgentError } from './error.js';
19
- import { validateActOptions, validateInstruction, validateParams, validateVerdict, } from './act-validation.js';
23
+ import { validateActOptions, validateInstruction, validateParams, validateVerdict } from './act-validation.js';
24
+ import { ActionDispatcher } from './action-dispatcher.js';
20
25
  import { resolveBoundedBudget, resolveTimeout } from './call-options.js';
21
- import { RUNTIME_CODES, } from './executor.js';
22
- import { isDerivedValue } from './derived.js';
26
+ import { RUNTIME_CODES } from './executor.js';
23
27
  import { projectPriorSteps, serializeLedger } from './ledger.js';
24
28
  import { instantiateLanguageModel } from './model/sdk.js';
25
- import { isTransitionalObservation, observationShape, pixelsForModel, prepareObservation, projectTree, settleObservation, } from './observation.js';
26
- import { observationByteBudget } from './observation-budget.js';
27
- import { boundedOperation, checkStepClock, instrumentPhase, recordPolicyEvent, retryingObserve } from './phases.js';
28
- import { containerKey, describeAction, describeTarget } from './actions.js';
29
- import { describePosition, relocateDescriptor } from '../cache/relocate.js';
30
- import { authorizeSecretFill } from './secrets.js';
31
- import { ModelUsage, tokenFields } from './usage.js';
29
+ import { ObservationFeed } from './observation-feed.js';
30
+ import { OperationQueue } from './operation-queue.js';
31
+ import { StepAccounting } from './step-accounting.js';
32
32
  import { StepTraceSession } from './step-cache.js';
33
- /**
34
- * Ceiling on one targeted grammar action: the time the engine may wait for a
35
- * node to become actionable before the failure goes back to the model.
36
- * `actionTimeout` bounds every engine operation and doubles as the judgment
37
- * tier's clock, so projects raise it for slow models; a tap under a consent
38
- * overlay then sits in the engine's actionability retry for the whole budget
39
- * (90 s in the testbed) before the model learns anything, when the useful
40
- * answer — what is in the way — is known within seconds. Navigation keeps the
41
- * full budget: a slow page really can take that long to load.
42
- */
43
- const MAX_TARGETED_ACTION_MS = 15_000;
44
- /**
45
- * How many times a targeted action re-finds its node after the engine reports
46
- * it stale. One relocation covers a re-render between the observation and the
47
- * action; the second covers a render tick landing between the fresh look and
48
- * the action itself. A control that keeps vanishing faster than that is
49
- * reported to the model as gone.
50
- */
51
- const MAX_STALE_RELOCATIONS = 2;
52
- /**
53
- * The change wait after a scroll or a mutating project tool. A scroll moves
54
- * nothing the tree records and a tool usually changes state the screen shows
55
- * only after a reload, so most of these change no shape at all and a long
56
- * wait is pure cost; a windowed list rendering its next rows, or a tool the
57
- * page reacts to, does so within a few hundred milliseconds.
58
- */
59
- const BRIEF_CHANGE_WAIT_MS = 500;
60
- /**
61
- * Observations kept for resolving an id the newest one no longer carries.
62
- * A turn that batches actions addresses the screen it saw, while every
63
- * action's own look re-observes; on an engine that mints ids per observation
64
- * (a device), each look renumbers the tree. A few looks back is as far as one
65
- * turn can reach.
66
- */
67
- const MAX_RECENT_OBSERVATIONS = 8;
68
- /**
69
- * How far one grammar scroll moves. The engine's default flick is half the
70
- * scrolled box; a deliberate scroll of three quarters keeps every row on
71
- * screen at least once while covering a long feed in fewer actions.
72
- */
73
- const SCROLL_MOMENTUM = 'slow';
74
33
  /** Runs one `agent.act()` call as a harness-dispatched executor step. */
75
34
  export async function runActStep(runtime, instruction, options, extraArguments = 0) {
76
35
  const normalized = validateInstruction(instruction, 'agent.act');
@@ -125,10 +84,10 @@ export async function runAssertStep(runtime, assertion, options) {
125
84
  * passing is what confirms the traces staged before it at attempt end
126
85
  * (cache/context.ts).
127
86
  */
128
- async function dispatchAgentStep(runtime, spec) {
129
- // Resolved before the step opens, so an unknown name fails the call, not a
130
- // recorded step, and the step carries the agent it ran with.
131
- const agent = runtime.select(spec.agent);
87
+ export async function dispatchAgentStep(runtime, spec,
88
+ // Resolved before the step opens, so an unknown name fails the call, not a
89
+ // recorded step, and the step carries the agent it ran with.
90
+ agent = runtime.select(spec.agent)) {
132
91
  return runtime.steps.run('agent', spec.api, spec.instruction, async () => {
133
92
  const dispatch = new ActDispatch(runtime, spec, agent);
134
93
  try {
@@ -152,95 +111,49 @@ async function dispatchAgentStep(runtime, spec) {
152
111
  }
153
112
  }, { verifies: spec.kind === 'assert', agent: agent.name });
154
113
  }
155
- /**
156
- * One step's harness-side state: budgets, the newest observation, and the
157
- * fatal error (if any) that must outrank whatever the executor reports.
158
- */
114
+ /** One step's wiring: the collaborators, the executor's context, and the verdict mapping. */
159
115
  class ActDispatch {
160
116
  runtime;
161
117
  spec;
162
118
  agent;
163
- timeoutMs;
164
- deadline;
165
- maxActions;
166
- maxModelCalls;
167
- metrics = {
168
- modelCalls: 0,
169
- actionSteps: 0,
170
- observationBytes: 0,
171
- contextBytes: 0,
172
- ledgerBytes: 0,
173
- };
174
- latest;
175
- /** The newest observations of the step, oldest first; see MAX_RECENT_OBSERVATIONS. */
176
- recent = [];
177
- explanation;
178
- /** First budget/timeout/cancel failure; runtime truth outranks the verdict. */
179
- hardStop;
180
- /** Aborts the executor on any hard stop, so a step never outlives its clock. */
181
- stepAbort = new AbortController();
182
- /**
183
- * The one signal everything inside the step aborts with: the attempt's
184
- * cancellation or the step's own hard stop. Built once in the constructor
185
- * so the executor context, the replay host, and the settle clock can never
186
- * disagree about what "the step's signal" means.
187
- */
188
- stepSignal;
189
- /**
190
- * Serializes observations and actions in call order. An executor (or an AI
191
- * SDK loop running parallel tool calls) that issues a second action before
192
- * the first settles would otherwise resolve both targets against the same
193
- * pre-action observation — exactly the wrong-node hazard the staleness rule
194
- * exists to prevent. Queued, the second call sees the newest observation
195
- * and a stale id fails loud instead of acting on the wrong node.
196
- *
197
- * Invariant: a serialized body must never call `observe()` or `runAction()`
198
- * itself — the inner call would queue behind its own caller and deadlock.
199
- * Grammar bodies call the session directly, and secret authorization
200
- * never observes; keep it that way.
201
- */
202
- grammarChain = Promise.resolve();
203
- sdkModel;
204
- sdkModelResolved = false;
205
- transcript;
206
- /** Set by `finish()`: late executor accounting must not land on the next step. */
207
- closed = false;
208
- usage = new ModelUsage();
209
- modelProvider;
210
- modelId;
211
- redact;
119
+ accounting;
120
+ feed;
121
+ dispatcher;
212
122
  /** The step's trace-cache session; undefined when caching is off or the kind is not cacheable. */
213
123
  stepCache;
214
124
  /** Timeline index of the step being dispatched. */
215
125
  stepIndex;
216
- /** The last pixel decision recorded on this step: `allowed`, or the withheld reason. */
217
- pixelsDecided;
218
- /**
219
- * The screen shape the newest committed action was resolved against, kept
220
- * until the next settled observation has waited for the screen to leave it.
221
- * Armed by actions whose effect shows in the tree; a secret fill leaves no
222
- * visible trace and a scroll moves nothing the tree records, so neither
223
- * arms it. Without this, the observation after a tap on a link reads the
224
- * old page, stable and wrong, and the model repairs what already worked.
225
- */
226
- pendingChange;
227
- /** The change wait the pending action asked for; undefined takes the default. */
228
- pendingChangeWaitMs;
126
+ explanation;
127
+ transcript;
128
+ sdkModel;
129
+ sdkModelResolved = false;
229
130
  constructor(runtime, spec,
230
131
  /** The agent this step runs with. */
231
132
  agent) {
232
133
  this.runtime = runtime;
233
134
  this.spec = spec;
234
135
  this.agent = agent;
235
- this.stepSignal = AbortSignal.any([runtime.engine.signal, this.stepAbort.signal]);
236
- this.timeoutMs = resolveTimeout(spec.timeout, runtime.config.timeout);
237
- this.deadline = runtime.engine.deadline(this.timeoutMs);
238
- this.maxActions = resolveBoundedBudget(spec.maxSteps, agent.config.maxSteps, 'maxSteps');
239
- this.maxModelCalls = resolveBoundedBudget(spec.maxModelCalls, agent.config.maxModelCalls, 'maxModelCalls');
240
- this.metrics.contextBytes = new TextEncoder().encode(agent.agentContext ?? '').byteLength;
241
- this.redact = runtime.redact;
242
- // The dispatch always runs inside a recorded step (`dispatchAct` opens
243
- // one); the index names the step to the executor and to the trace cache.
136
+ this.accounting = new StepAccounting(runtime, {
137
+ api: spec.api,
138
+ timeoutMs: resolveTimeout(spec.timeout, runtime.config.timeout),
139
+ maxActions: resolveBoundedBudget(spec.maxSteps, agent.config.maxSteps, 'maxSteps'),
140
+ maxModelCalls: resolveBoundedBudget(spec.maxModelCalls, agent.config.maxModelCalls, 'maxModelCalls'),
141
+ contextBytes: new TextEncoder().encode(agent.agentContext ?? '').byteLength,
142
+ });
143
+ // One queue for observations and actions alike: call order is what keeps
144
+ // a batched turn from resolving two targets against one stale screen.
145
+ const queue = new OperationQueue();
146
+ this.feed = new ObservationFeed(runtime, this.accounting, queue, {
147
+ maxObservationBytes: agent.config.maxObservationBytes,
148
+ });
149
+ this.dispatcher = new ActionDispatcher(runtime, this.accounting, this.feed, queue, {
150
+ instruction: spec.instruction,
151
+ params: spec.params,
152
+ secrets: spec.secrets,
153
+ trace: () => this.stepCache,
154
+ });
155
+ // The dispatch always runs inside a recorded step (`dispatchAgentStep`
156
+ // opens one); the index names the step to the executor and to the trace cache.
244
157
  const stepIndex = runtime.steps.currentStepIndex;
245
158
  if (stepIndex === undefined)
246
159
  throw new Error('agent step dispatched outside a recorded step');
@@ -258,13 +171,11 @@ class ActDispatch {
258
171
  params: spec.params,
259
172
  executor: {
260
173
  name: agent.executor.name,
261
- ...(agent.executor.version === undefined
262
- ? {}
263
- : { version: agent.executor.version }),
174
+ ...(agent.executor.version === undefined ? {} : { version: agent.executor.version }),
264
175
  },
265
- redact: this.redact,
176
+ redact: runtime.redact,
266
177
  testIdAttribute: runtime.config.testIdAttribute,
267
- maxActions: this.maxActions,
178
+ maxActions: this.accounting.maxActions,
268
179
  stepIndex,
269
180
  });
270
181
  }
@@ -274,7 +185,7 @@ class ActDispatch {
274
185
  // oxlint-disable-next-line typescript/no-this-alias
275
186
  const dispatch = this;
276
187
  const ledger = serializeLedger(projectPriorSteps(this.runtime.priorSteps()), this.runtime.config.limits.maxLedgerBytes);
277
- this.metrics.ledgerBytes = ledger.bytes;
188
+ this.accounting.metrics.ledgerBytes = ledger.bytes;
278
189
  const replayedPrefix = this.stepCache?.replayedPrefix;
279
190
  return {
280
191
  step: {
@@ -290,7 +201,7 @@ class ActDispatch {
290
201
  target: this.runtime.target,
291
202
  attempt: this.runtime.attempt,
292
203
  ...(replayedPrefix === undefined ? {} : { replayedPrefix }),
293
- signal: this.stepSignal,
204
+ signal: this.accounting.signal,
294
205
  // Resolved on first read, so executors that bring their own model (or
295
206
  // none) never pay for — or fail on — config model resolution.
296
207
  get model() {
@@ -300,14 +211,17 @@ class ActDispatch {
300
211
  ledger: ledger.text,
301
212
  agentContext: this.agent.agentContext,
302
213
  budgets: {
303
- maxActions: this.maxActions,
304
- maxModelCalls: this.maxModelCalls,
305
- actionsUsed: () => this.metrics.actionSteps,
306
- remainingMs: () => this.deadline.remaining(),
307
- recordModelCall: (usage) => this.recordModelCall(usage),
308
- runTool: (call, body) => this.runTool(call, body),
214
+ maxActions: this.accounting.maxActions,
215
+ maxModelCalls: this.accounting.maxModelCalls,
216
+ actionsUsed: () => this.accounting.metrics.actionSteps,
217
+ remainingMs: () => this.accounting.remainingMs(),
218
+ recordModelCall: (usage) => this.accounting.recordModelCall(usage),
219
+ runTool: (call, body) => this.dispatcher.runTool(call, body),
220
+ },
221
+ observe: (options) => this.feed.observe(options),
222
+ get pixelsTainted() {
223
+ return dispatch.runtime.taint.value;
309
224
  },
310
- observe: (options) => this.observe(options),
311
225
  attachTranscript: (text) => {
312
226
  // Debug detail only: transcripts are model prose and can be large.
313
227
  if (this.runtime.debug?.enabled === true && typeof text === 'string' && text !== '') {
@@ -315,53 +229,7 @@ class ActDispatch {
315
229
  }
316
230
  },
317
231
  attachScreenshot: (pixels, label) => this.attachScreenshot(pixels, label),
318
- actions: this.buildActions(),
319
- };
320
- }
321
- /**
322
- * The action grammar, shared verbatim by the executor context and the
323
- * replay engine: a replayed action runs under exactly the same deadline,
324
- * budget, policy, and recording as a live one. Committed actions are
325
- * recorded into the step trace with the node they actually acted on —
326
- * capture at commit time is what makes the descriptor durable evidence
327
- * rather than a guess.
328
- */
329
- buildActions() {
330
- return {
331
- tap: (target) => this.commitTargeted('tap', target, async (node) => {
332
- await this.session.perform(node.ref, { kind: 'tap' }, this.actionOperation());
333
- return { name: 'tap', node };
334
- }),
335
- type: (target, value) => {
336
- if (typeof value !== 'string') {
337
- throw new TestError('INVALID_ARGUMENT', 'type value must be a string');
338
- }
339
- return this.commitTargeted('type', target, async (node) => {
340
- await this.session.perform(node.ref, { kind: 'fill', value, sensitive: false }, this.actionOperation());
341
- return { name: 'type', node, value };
342
- });
343
- },
344
- typeSecret: (target, name) => this.typeSecret(target, name),
345
- press: (target, key) => {
346
- if (typeof key !== 'string' || key.trim() === '' || key.length > 64) {
347
- throw new TestError('INVALID_ARGUMENT', 'press key must be a short non-empty string');
348
- }
349
- return this.commitTargeted('press', target, async (node) => {
350
- await this.session.perform(node.ref, { kind: 'press', key }, this.actionOperation());
351
- return { name: 'press', node, key };
352
- });
353
- },
354
- select: (target, value) => {
355
- if (typeof value !== 'string' || value === '') {
356
- throw new TestError('INVALID_ARGUMENT', 'select value must be a non-empty option label');
357
- }
358
- return this.commitTargeted('selectOption', target, async (node) => {
359
- await this.session.perform(node.ref, { kind: 'selectOption', value }, this.actionOperation());
360
- return { name: 'select', node, value };
361
- });
362
- },
363
- scroll: (direction, target) => this.scroll(direction, target),
364
- navigate: (url) => this.navigate(url),
232
+ actions: this.dispatcher.actions,
365
233
  };
366
234
  }
367
235
  /**
@@ -372,14 +240,15 @@ class ActDispatch {
372
240
  * the promise is abandoned, never awaited past the deadline.
373
241
  */
374
242
  async run() {
243
+ const { accounting } = this;
375
244
  const timer = setTimeout(() => {
376
- this.fatalize(new AgentError('STEP_TIMEOUT', `${this.spec.api} exceeded its ${this.timeoutMs} ms timeout`));
377
- }, Math.max(1, this.deadline.remaining()));
245
+ accounting.fatalize(new AgentError('STEP_TIMEOUT', `${this.spec.api} exceeded its ${accounting.timeoutMs} ms timeout`));
246
+ }, Math.max(1, accounting.remainingMs()));
378
247
  try {
379
248
  return await Promise.race([
380
249
  this.dispatchStep(),
381
250
  new Promise((_, reject) => {
382
- const signal = this.stepAbort.signal;
251
+ const { signal } = accounting;
383
252
  if (signal.aborted) {
384
253
  reject(signal.reason);
385
254
  return;
@@ -392,39 +261,6 @@ class ActDispatch {
392
261
  clearTimeout(timer);
393
262
  }
394
263
  }
395
- async dispatchStep() {
396
- const replayed = await this.stepCache?.begin();
397
- if (replayed !== undefined)
398
- return replayed;
399
- return this.agent.executor.runStep(this.context());
400
- }
401
- /** The cache session's narrow view of this dispatch. */
402
- cacheHost() {
403
- return {
404
- observe: async () => (await this.observeLatest()).nodes,
405
- observeSettled: async () => (await this.observeSettled()).nodes,
406
- actions: this.buildActions(),
407
- signal: this.stepSignal,
408
- remainingMs: () => this.deadline.remaining(),
409
- redact: this.redact,
410
- testIdAttribute: this.runtime.config.testIdAttribute,
411
- currentPath: (nodes) => this.currentPath(nodes !== undefined && nodes === this.latest?.nodes ? this.latest : undefined),
412
- replaying: (active) => this.runtime.steps.replaying(active),
413
- };
414
- }
415
- /** Best-effort current location path + query, for trace preconditions. */
416
- async currentPath(observation) {
417
- const currentUrl = this.session.url;
418
- if (observation?.url === undefined && currentUrl === undefined)
419
- return undefined;
420
- try {
421
- const url = new URL(observation?.url ?? await currentUrl(this.operation()));
422
- return `${url.pathname}${url.search}`;
423
- }
424
- catch {
425
- return undefined;
426
- }
427
- }
428
264
  /** Hands the settled outcome to the cache session, which stages, evicts, or does nothing. */
429
265
  async conclude(outcome) {
430
266
  await this.stepCache?.conclude(outcome, this.explanation);
@@ -432,27 +268,24 @@ class ActDispatch {
432
268
  /** Maps the executor's verdict onto the runner outcome. Fail-closed on hard stops. */
433
269
  settle(verdict) {
434
270
  let settled = verdict;
435
- if (this.hardStop !== undefined) {
436
- if (this.hardStop.code === 'CANCELLED')
437
- throw this.hardStop;
271
+ const hardStop = this.accounting.hardStop;
272
+ if (hardStop !== undefined) {
273
+ if (hardStop.code === 'CANCELLED')
274
+ throw hardStop;
438
275
  if (settled.status === 'passed') {
439
276
  // The step ran out of budget or time mid-flight; an executor cannot
440
277
  // declare success over the runtime's own accounting.
441
- settled = {
442
- status: 'blocked',
443
- summary: this.hardStop.message,
444
- errorCode: this.hardStop.code,
445
- };
278
+ settled = { status: 'blocked', summary: hardStop.message, errorCode: hardStop.code };
446
279
  }
447
280
  else if (settled.errorCode === undefined) {
448
281
  // The executor's analysis stands, but runtime exhaustion is never
449
282
  // hidden from the report: a code-less failure inherits the hard stop.
450
- settled = { ...settled, errorCode: this.hardStop.code };
283
+ settled = { ...settled, errorCode: hardStop.code };
451
284
  }
452
285
  }
453
286
  if (settled.errorCode !== undefined &&
454
287
  RUNTIME_CODES.has(settled.errorCode) &&
455
- !this.vouches(settled.errorCode)) {
288
+ !this.accounting.vouches(settled.errorCode)) {
456
289
  throw this.invented(settled.errorCode);
457
290
  }
458
291
  this.explanation = settled.summary;
@@ -473,132 +306,66 @@ class ActDispatch {
473
306
  settleThrown(error) {
474
307
  if (!RUNTIME_CODES.has(error.code))
475
308
  return error;
476
- if (this.hardStop !== undefined)
477
- return this.hardStop;
478
- if (this.vouches(error.code))
309
+ const hardStop = this.accounting.hardStop;
310
+ if (hardStop !== undefined)
311
+ return hardStop;
312
+ if (this.accounting.vouches(error.code))
479
313
  return error;
480
314
  return this.invented(error.code, error);
481
315
  }
482
- /**
483
- * Whether the runtime's own accounting corroborates a runtime code. Codes
484
- * are runtime-assigned: the recorded hard stop vouches directly, and the
485
- * clock, the budgets, and the abort signal vouch for an executor that
486
- * observed exhaustion before the context machinery did.
487
- */
488
- vouches(code) {
489
- if (this.hardStop?.code === code)
490
- return true;
491
- switch (code) {
492
- case 'CANCELLED':
493
- return this.runtime.engine.signal.aborted;
494
- case 'STEP_TIMEOUT':
495
- return this.deadline.expired();
496
- case 'STEP_BUDGET_EXHAUSTED':
497
- return (this.metrics.actionSteps >= this.maxActions ||
498
- this.metrics.modelCalls >= this.maxModelCalls);
499
- default:
500
- return false;
501
- }
502
- }
503
- invented(code, cause) {
504
- return new AgentError('MODEL_OUTPUT_INVALID', `executor "${this.agent.executor.name}" reported runtime code ${code}, which the runtime never assigned`, cause === undefined ? {} : { cause });
505
- }
506
- /**
507
- * Records the first hard stop. Timeout and cancellation also abort the step
508
- * signal — there is nothing left for the executor to say. Budget exhaustion
509
- * does not: the executor may still catch it and conclude with its own
510
- * analysis (which `settle` stamps with the runtime code), bounded by the
511
- * deadline either way.
512
- */
513
- fatalize(error) {
514
- this.hardStop ??= error;
515
- if (error.code !== 'STEP_BUDGET_EXHAUSTED' && !this.stepAbort.signal.aborted) {
516
- this.stepAbort.abort(this.hardStop);
517
- }
518
- return error;
519
- }
520
- /**
521
- * Counts and accounts one executor-made model call. The budget is enforced:
522
- * the call past the limit records, then hard-stops the step.
523
- */
524
- recordModelCall(usage) {
525
- // An executor abandoned by a hard stop can still report late; the step it
526
- // belonged to is closed, and the recorder's active step is now another.
527
- if (this.closed)
528
- return;
529
- this.metrics.modelCalls += 1;
530
- const tokens = this.usage.record(usage);
531
- if (usage?.provider !== undefined)
532
- this.modelProvider = usage.provider;
533
- if (usage?.modelId !== undefined)
534
- this.modelId = usage.modelId;
535
- this.runtime.steps.recordEvent({
536
- kind: 'model',
537
- startedAt: usage?.startedAt ?? timestamp(),
538
- durationMs: Math.max(0, Math.round(usage?.durationMs ?? 0)),
539
- status: 'passed',
540
- name: 'executor',
541
- count: tokens,
542
- ...tokenFields(usage),
543
- });
544
- this.runtime.debug?.record('agent.model', Math.max(0, Math.round(usage?.durationMs ?? 0)));
545
- if (this.metrics.modelCalls > this.maxModelCalls) {
546
- throw this.fatalize(new AgentError('STEP_BUDGET_EXHAUSTED', `${this.spec.api} exhausted its model-call budget of ${this.maxModelCalls}`));
547
- }
548
- }
549
- /** Claims a mutation slot before any side effect, for both grammar and project tools. */
550
- reserveAction() {
551
- this.checkpoint();
552
- if (this.closed)
553
- throw new AgentError('CANCELLED', 'the step has ended');
554
- if (this.metrics.actionSteps >= this.maxActions) {
555
- throw this.fatalize(new AgentError('STEP_BUDGET_EXHAUSTED', `${this.spec.api} exhausted its action budget of ${this.maxActions}`));
556
- }
557
- // The budget slot is consumed either way: a failed dispatch was an attempt.
558
- this.metrics.actionSteps += 1;
559
- }
560
- /** Records project tools through the same budget and operation queue as grammar actions. */
561
- runTool(call, body) {
562
- const run = async () => {
563
- this.checkpoint();
564
- if (this.closed)
565
- throw new AgentError('CANCELLED', 'the step has ended');
566
- if (call.mutates) {
567
- this.reserveAction();
568
- this.stepCache?.recordGap(call.name);
569
- }
570
- const value = await instrumentPhase(this.runtime, { api: this.spec.api, kind: 'engine', phase: 'agent.action', name: `tool:${call.name}` }, body);
571
- // A tool the page reacts to at once is read after the reaction; one
572
- // whose effect shows only after a reload costs the brief wait, not two seconds.
573
- if (call.mutates)
574
- this.armChange(BRIEF_CHANGE_WAIT_MS);
575
- return value;
576
- };
577
- return call.mutates ? this.serialized(run) : run();
578
- }
579
316
  /** What the settled step did, for the caller of `agent.act()`. */
580
317
  result() {
581
318
  const cache = this.stepCache?.cacheInfo;
582
319
  return {
583
320
  summary: this.explanation ?? '',
584
321
  ...(cache === undefined ? {} : { cache }),
585
- modelCalls: this.metrics.modelCalls,
586
- actions: this.metrics.actionSteps,
322
+ modelCalls: this.accounting.metrics.modelCalls,
323
+ actions: this.accounting.metrics.actionSteps,
587
324
  };
588
325
  }
589
- /** Attaches metrics, model provenance, and the verdict explanation to the step. */
326
+ /** Closes the books and attaches metrics, model provenance, and the verdict explanation to the step. */
590
327
  finish() {
591
- this.closed = true;
328
+ this.accounting.close();
329
+ const { metrics } = this.accounting;
592
330
  const cacheInfo = this.stepCache?.cacheInfo;
331
+ const latest = this.feed.latest;
332
+ const model = this.accounting.modelInfo(this.agent.executor);
593
333
  this.runtime.steps.attachAgentDetails({
594
- metrics: { ...this.metrics },
595
- ...(this.metrics.modelCalls > 0 ? { model: this.modelInfo() } : {}),
334
+ metrics: { ...metrics },
335
+ ...(model === undefined ? {} : { model }),
596
336
  ...(cacheInfo === undefined ? {} : { cache: cacheInfo }),
597
337
  ...(this.explanation !== undefined ? { explanation: this.explanation } : {}),
598
- ...(this.latest !== undefined ? { observationRevision: this.latest.revision } : {}),
338
+ ...(latest !== undefined ? { observationRevision: latest.revision } : {}),
339
+ ...this.feed.visionReport(),
599
340
  });
600
341
  this.writeTranscript();
601
342
  }
343
+ async dispatchStep() {
344
+ const replayed = await this.stepCache?.begin();
345
+ if (replayed !== undefined)
346
+ return replayed;
347
+ return this.agent.executor.runStep(this.context());
348
+ }
349
+ /** The cache session's narrow view of this step. */
350
+ cacheHost() {
351
+ return {
352
+ observe: async () => screenOf(await this.feed.observeLatest()),
353
+ observeSettled: async () => screenOf(await this.feed.observeSettled()),
354
+ actions: this.dispatcher.actions,
355
+ signal: this.accounting.signal,
356
+ remainingMs: () => this.accounting.remainingMs(),
357
+ redact: this.runtime.redact,
358
+ testIdAttribute: this.runtime.config.testIdAttribute,
359
+ currentPath: (nodes) => {
360
+ const latest = this.feed.latest;
361
+ return this.feed.currentPath(nodes !== undefined && nodes === latest?.nodes ? latest : undefined);
362
+ },
363
+ replaying: (active) => this.runtime.steps.replaying(active),
364
+ };
365
+ }
366
+ invented(code, cause) {
367
+ return new AgentError('MODEL_OUTPUT_INVALID', `executor "${this.agent.executor.name}" reported runtime code ${code}, which the runtime never assigned`, cause === undefined ? {} : { cause });
368
+ }
602
369
  /**
603
370
  * Persists observed pixels as a step-attributed `screenshot` artifact and
604
371
  * returns its id. The pixels are the engine's redacted capture, so the file
@@ -620,39 +387,13 @@ class ActDispatch {
620
387
  try {
621
388
  // Model prose and project-tool output are not model input, but they
622
389
  // are a log: the same redactor that guards the tree guards the file.
623
- writeFileSync(join(this.runtime.artifacts.dir, name), this.redact(this.transcript), 'utf8');
390
+ writeFileSync(join(this.runtime.artifacts.dir, name), this.runtime.redact(this.transcript), 'utf8');
624
391
  this.runtime.steps.attachArtifact(this.runtime.artifacts.register('log', name));
625
392
  }
626
393
  catch {
627
394
  // The transcript is best-effort debug detail; never fail the step for it.
628
395
  }
629
396
  }
630
- /**
631
- * Model provenance for the report. The executor is the authority on which
632
- * model answered; an executor that reports nothing is still identified, so a
633
- * step's model calls are never attributed to the wrong tier.
634
- */
635
- modelInfo() {
636
- const executor = this.agent.executor;
637
- const executorVersion = executor.version ?? '0';
638
- return this.usage.report({
639
- provider: this.modelProvider ?? executor.name,
640
- model: this.modelId ?? executor.name,
641
- endpoint: 'provider-default',
642
- adapterVersion: `executor/${executor.name}@${executorVersion}`,
643
- policyVersion: `${executor.name}/${executorVersion}`,
644
- }, this.metrics.modelCalls);
645
- }
646
- get session() {
647
- return this.runtime.engine.session;
648
- }
649
- operation() {
650
- return boundedOperation(this.runtime.engine, this.runtime.config.actionTimeout, this.deadline);
651
- }
652
- /** The operation context of one targeted action; see MAX_TARGETED_ACTION_MS. */
653
- actionOperation() {
654
- return boundedOperation(this.runtime.engine, Math.min(this.runtime.config.actionTimeout, MAX_TARGETED_ACTION_MS), this.deadline);
655
- }
656
397
  /** Resolves the configured model once; executors that never read it never pay. */
657
398
  resolveModel() {
658
399
  if (!this.sdkModelResolved) {
@@ -662,358 +403,9 @@ class ActDispatch {
662
403
  }
663
404
  return this.sdkModel;
664
405
  }
665
- /** Fails when the step is cancelled or out of time; records the hard stop. */
666
- checkpoint(cause) {
667
- try {
668
- checkStepClock({
669
- signal: this.runtime.engine.signal,
670
- deadline: this.deadline,
671
- api: this.spec.api,
672
- timeoutMs: this.timeoutMs,
673
- ...(cause === undefined ? {} : { cause }),
674
- });
675
- }
676
- catch (error) {
677
- if (isAgentError(error))
678
- this.fatalize(error);
679
- throw error;
680
- }
681
- }
682
- /** Chains one grammar operation behind every earlier one, in call order. */
683
- serialized(body) {
684
- // The chain is always already settled-to-undefined, so failures propagate
685
- // to their own caller and never poison the queue.
686
- const run = this.grammarChain.then(body);
687
- this.grammarChain = run.then(() => undefined, () => undefined);
688
- return run;
689
- }
690
- /**
691
- * The executor-facing observe: always settled. An executor observation is
692
- * followed by a model call measured in seconds, so the bounded settle wait
693
- * is noise there — and it guarantees the model never reads a snapshot the
694
- * app is still reacting to, which a fast model turns into a repeated action
695
- * (double-committing a toggle) and a verdict judged on pre-render state.
696
- * Replay's pre-action looks and the cache session's probes settle through
697
- * the same path (`observeSettled`); only replay's polls between retries
698
- * read raw (`observeLatest`).
699
- */
700
- async observe(options = {}) {
701
- if (options === null || typeof options !== 'object') {
702
- throw new TestError('INVALID_ARGUMENT', 'observe options must be an object');
703
- }
704
- const wantPixels = options.pixels === true;
705
- const observation = await this.observeSettled(wantPixels);
706
- // Prefer location from this capture; only engines without it need a separate probe.
707
- const path = await this.currentPath(observation);
708
- return {
709
- revision: observation.revision,
710
- text: observation.text,
711
- truncated: observation.truncated,
712
- viewport: observation.viewport,
713
- ...(path === undefined ? {} : { path: this.redact(path) }),
714
- ...(options.tree === true ? { tree: projectTree(observation.tree, this.redact) } : {}),
715
- ...(wantPixels ? this.pixelsFor(observation) : {}),
716
- };
717
- }
718
- /**
719
- * Pixels for an executor that asked for them, or the reason they are
720
- * withheld — the same decision the judgment tier makes, recorded as a
721
- * policy event whenever it changes within the step.
722
- */
723
- pixelsFor(observation) {
724
- const outcome = pixelsForModel(observation, this.runtime.taint.value);
725
- if ('withheld' in outcome) {
726
- this.recordPixelDecision('denied', outcome.withheld);
727
- return { pixelsWithheld: outcome.withheld };
728
- }
729
- this.recordPixelDecision('allowed');
730
- this.metrics.pixelBytes = Math.max(this.metrics.pixelBytes ?? 0, outcome.pixels.data.byteLength);
731
- return { pixels: outcome.pixels };
732
- }
733
- recordPixelDecision(decision, code) {
734
- const key = code ?? decision;
735
- if (this.pixelsDecided === key)
736
- return;
737
- this.pixelsDecided = key;
738
- this.recordPolicy('vision.pixels', decision, code);
739
- }
740
- observeLatest() {
741
- return this.serialized(() => this.observeNow(false, false));
742
- }
743
- observeSettled(pixels = false) {
744
- return this.serialized(() => this.observeNow(true, pixels));
745
- }
746
- /** One recorded observation; when `settle`, the captures loop inside it. */
747
- async observeNow(settle, pixels) {
748
- this.checkpoint();
749
- // A tainted viewport never captures pixels: the engine would mask what it
750
- // knows about, and the secret may be anywhere on screen by now.
751
- const capturePixels = pixels && !this.runtime.taint.value;
752
- // A settled look consumes the pending change: it waits for the screen to
753
- // leave the pre-action shape once, and later looks read the screen as is.
754
- const changedFrom = settle ? this.pendingChange : undefined;
755
- const changeWaitMs = settle ? this.pendingChangeWaitMs : undefined;
756
- if (settle) {
757
- this.pendingChange = undefined;
758
- this.pendingChangeWaitMs = undefined;
759
- }
760
- const observation = await instrumentPhase(this.runtime, { api: this.spec.api, kind: 'observation', phase: 'agent.observe' }, () => settle
761
- ? settleObservation(() => this.captureObservation(capturePixels), observationShape, {
762
- remainingMs: () => this.deadline.remaining(),
763
- // The step's own hard stop must interrupt a settle sleep too —
764
- // the attempt signal alone would let settling outlive the step
765
- // by one poll interval.
766
- signal: this.stepSignal,
767
- }, { changedFrom, changeWaitMs, transitional: isTransitionalObservation })
768
- : this.captureObservation(capturePixels), (prepared) => ({ count: prepared.nodes.size, bytes: prepared.bytes }));
769
- this.publish(observation);
770
- this.metrics.observationBytes = Math.max(this.metrics.observationBytes, observation.bytes);
771
- return observation;
772
- }
773
- /** Makes an observation the newest and remembers it among the recent ones. */
774
- publish(observation) {
775
- this.latest = observation;
776
- this.recent.push(observation);
777
- if (this.recent.length > MAX_RECENT_OBSERVATIONS)
778
- this.recent.shift();
779
- }
780
- /**
781
- * Arms the change wait: the next settled observation waits for the screen
782
- * to leave the newest observation's shape, for the default two seconds or
783
- * the given brief window.
784
- */
785
- armChange(waitMs) {
786
- if (this.latest === undefined)
787
- return;
788
- this.pendingChange = observationShape(this.latest);
789
- this.pendingChangeWaitMs = waitMs;
790
- }
791
- /** One raw observation capture: retried at the engine, then redacted and bounded. */
792
- async captureObservation(pixels) {
793
- const raw = await retryingObserve({
794
- observe: (operation) => this.session.observe(operation, { pixels }),
795
- operation: () => this.operation(),
796
- guard: (cause) => this.checkpoint(cause),
797
- signal: this.runtime.engine.signal,
798
- api: this.spec.api,
799
- });
800
- return prepareObservation(raw, {
801
- redact: this.runtime.redact,
802
- maxBytes: this.observationByteBudget(pixels),
803
- testIdAttribute: this.runtime.config.testIdAttribute,
804
- });
805
- }
806
- /**
807
- * Bytes one act observation may contribute to a model turn, clamped like a
808
- * judgment's. The loop's history grows past what any one observation costs,
809
- * so the fixed part counted here is what every turn resends: the project
810
- * context and the prior-step ledger.
811
- */
812
- observationByteBudget(pixels) {
813
- const { config } = this.runtime;
814
- return observationByteBudget({
815
- maxObservationBytes: this.agent.config.maxObservationBytes,
816
- maxModelTokensPerCall: config.limits.maxModelTokensPerCall,
817
- }, { fixedBytes: this.metrics.contextBytes + this.metrics.ledgerBytes, pixels });
818
- }
819
- /**
820
- * Resolves an executor target against the newest observation. An id the
821
- * newest observation no longer carries, but a recent one did, is re-found
822
- * in the newest one by its descriptor: a turn that batches actions keeps
823
- * addressing the screen it saw while each action's own look re-observes,
824
- * and an engine that mints ids per observation renumbers the tree between
825
- * them. Exactly one match, or the id counts as gone.
826
- */
827
- resolveTarget(target) {
828
- if (typeof target?.id !== 'string' || target.id === '') {
829
- throw new TestError('INVALID_ARGUMENT', 'action target must be { id: string }');
830
- }
831
- const id = target.id.replace(/^#/, '');
832
- const latest = this.latest;
833
- if (latest === undefined) {
834
- throw new AgentError('LOCATOR_NOT_FOUND', 'no observation has been captured yet; observe before acting');
835
- }
836
- const node = latest.nodes.get(id) ?? this.refound(id, latest);
837
- if (node === undefined) {
838
- throw new AgentError('LOCATOR_NOT_FOUND', `node #${id} is not on the current screen (observation ${latest.revision}); it was removed or never existed`);
839
- }
840
- return node;
841
- }
842
- /** The newest node matching the descriptor of what `id` named in a recent observation, if exactly one. */
843
- refound(id, latest) {
844
- let earlier;
845
- for (let index = this.recent.length - 1; index >= 0 && earlier === undefined; index -= 1) {
846
- earlier = this.recent[index].nodes.get(id);
847
- }
848
- if (earlier === undefined)
849
- return undefined;
850
- const descriptor = describeTarget(earlier, this.redact, this.runtime.config.testIdAttribute);
851
- if (descriptor === undefined)
852
- return undefined;
853
- const relocated = relocateDescriptor(descriptor, latest.nodes, {
854
- redact: this.redact,
855
- testIdAttribute: this.runtime.config.testIdAttribute,
856
- });
857
- return relocated.kind === 'found' ? latest.nodes.get(relocated.id) : undefined;
858
- }
859
- /**
860
- * Runs one grammar action against the action budget, recorded as an engine
861
- * event. The body performs the engine call and returns the committed
862
- * action's recordable descriptor — one value carries both concerns: the
863
- * event's `detail` prose derives from it in a pure hook, and the dispatch
864
- * writes it to the trace cache after the phase settles.
865
- */
866
- runAction(name, body) {
867
- return this.serialized(() => this.runActionNow(name, body));
868
- }
869
- async runActionNow(name, body) {
870
- this.reserveAction();
871
- let action;
872
- try {
873
- action = await instrumentPhase(this.runtime, { api: this.spec.api, kind: 'engine', phase: 'agent.action', name }, body, (committed) => ({
874
- detail: describeAction(committed, this.redact, this.runtime.config.testIdAttribute).summary,
875
- }));
876
- }
877
- catch (cause) {
878
- this.checkpoint(cause);
879
- throw cause;
880
- }
881
- // The effect may still be arriving: the next settled observation waits for
882
- // the screen to leave the shape this action was resolved against. A secret
883
- // fill leaves no visible trace and arms nothing; a scroll waits briefly for
884
- // rows a windowed or lazy list renders.
885
- if (name !== 'typeSecret')
886
- this.armChange(name === 'scroll' ? BRIEF_CHANGE_WAIT_MS : undefined);
887
- if (this.stepCache === undefined)
888
- return;
889
- // A typed value the step derived at run time is this run's data, not the
890
- // flow's: it is recorded as a gap so replay hands over before it rather
891
- // than typing a value the app may not issue again.
892
- if (action.name === 'type' && isDerivedValue(action.value, this.spec.instruction, this.spec.params)) {
893
- this.stepCache.recordGap('type (run-time value)');
894
- return;
895
- }
896
- this.stepCache.record(action);
897
- }
898
- /**
899
- * One action against a resolved node. A node the engine reports stale is
900
- * re-found by its descriptor against a fresh capture and the action retried
901
- * (`MAX_STALE_RELOCATIONS` times): a list that remounts its rows between
902
- * the observation and the action keeps the control on screen under a dead
903
- * handle, and the control, not the handle, is what the model asked for. A
904
- * descriptor that matches nothing or several nodes fails the action instead.
905
- */
906
- commitTargeted(name, target, perform) {
907
- return this.runAction(name, async () => {
908
- let node = this.resolveTarget(target);
909
- // resolveTarget guarantees an observation; it is the one the node came from.
910
- let observation = this.latest;
911
- for (let relocations = 0;; relocations += 1) {
912
- // The container the node sits in is captured with it: that is what
913
- // tells this row's "Delete" from the next row's when the flow replays.
914
- const within = containerKey(node.ref.id, observation.nodes, observation.parents, this.redact);
915
- // When the description still matches several controls, the position among
916
- // them is recorded too; a replay that finds the same number picks the same one.
917
- const position = describePosition(node, within, observation.nodes, {
918
- redact: this.redact,
919
- testIdAttribute: this.runtime.config.testIdAttribute,
920
- });
921
- try {
922
- const action = await perform(node);
923
- return {
924
- ...action,
925
- ...(within === undefined ? {} : { within }),
926
- ...(position === undefined ? {} : { position }),
927
- };
928
- }
929
- catch (cause) {
930
- if (asEngineError(cause)?.code !== 'NODE_STALE')
931
- throw cause;
932
- const relocated = relocations < MAX_STALE_RELOCATIONS ? await this.relocateStale(node) : undefined;
933
- if (relocated === undefined) {
934
- throw new AgentError('LOCATOR_NOT_FOUND', 'the target node left the screen before the action reached it', { cause });
935
- }
936
- node = relocated.node;
937
- observation = relocated.observation;
938
- }
939
- }
940
- });
941
- }
942
- /**
943
- * Re-finds a node that went stale: one fresh capture, then the trace
944
- * recorder's own descriptor matching (`cache/relocate.ts`) against it,
945
- * exactly one match or nothing. The capture is taken directly rather than
946
- * through the serialized observe: this runs inside a serialized action
947
- * body, and a queued observation would wait on its own caller.
948
- */
949
- async relocateStale(stale) {
950
- const options = { redact: this.redact, testIdAttribute: this.runtime.config.testIdAttribute };
951
- const descriptor = describeTarget(stale, this.redact, this.runtime.config.testIdAttribute);
952
- if (descriptor === undefined)
953
- return undefined;
954
- this.checkpoint();
955
- const observation = await instrumentPhase(this.runtime, { api: this.spec.api, kind: 'observation', phase: 'agent.observe', name: 'relocate' }, () => this.captureObservation(false), (prepared) => ({ count: prepared.nodes.size, bytes: prepared.bytes }));
956
- this.publish(observation);
957
- this.metrics.observationBytes = Math.max(this.metrics.observationBytes, observation.bytes);
958
- const relocated = relocateDescriptor(descriptor, observation.nodes, options);
959
- if (relocated.kind !== 'found')
960
- return undefined;
961
- const node = observation.nodes.get(relocated.id);
962
- return node === undefined ? undefined : { node, observation };
963
- }
964
- async scroll(direction, target) {
965
- if (!['up', 'down', 'left', 'right'].includes(direction)) {
966
- throw new TestError('INVALID_ARGUMENT', `invalid scroll direction "${String(direction)}"`);
967
- }
968
- if (target === undefined) {
969
- await this.runAction('scroll', async () => {
970
- await this.session.swipe(direction, SCROLL_MOMENTUM, this.actionOperation());
971
- return { name: 'scroll', direction };
972
- });
973
- return;
974
- }
975
- await this.commitTargeted('scroll', target, async (node) => {
976
- await this.session.perform(node.ref, { kind: 'swipe', direction, momentum: SCROLL_MOMENTUM }, this.actionOperation());
977
- return { name: 'scroll', direction, node };
978
- });
979
- }
980
- /**
981
- * Fills one declared secret. The name must come from the step's own params
982
- * — an executor can never fill a credential the test did not hand it — and
983
- * the fill itself runs the full secret authorization policy: registered credential, origin allowlists, and an editable sink
984
- * whose purpose matches. Pixel evidence is tainted from here on.
985
- */
986
- async typeSecret(target, name) {
987
- const secret = this.spec.secrets.get(name);
988
- if (secret === undefined) {
989
- throw new AgentError('POLICY_DENIED', `secret "${name}" was not declared in this step's params; only declared secrets can be filled`);
990
- }
991
- await this.commitTargeted('typeSecret', target, async (node) => {
992
- const plaintext = await authorizeSecretFill({
993
- session: this.session,
994
- operation: () => this.actionOperation(),
995
- recordPolicy: (policy, decision, code) => this.recordPolicy(policy, decision, code),
996
- }, this.runtime, secret, node);
997
- await this.session.perform(node.ref, { kind: 'fill', value: plaintext, sensitive: true }, this.actionOperation());
998
- this.runtime.taint.value = true;
999
- // Recorded by stable name only; replay re-runs the full authorization.
1000
- return { name: 'typeSecret', node, secret: name };
1001
- });
1002
- }
1003
- recordPolicy(name, decision, code) {
1004
- recordPolicyEvent(this.runtime.steps, name, decision, code);
1005
- }
1006
- async navigate(url) {
1007
- if (typeof url !== 'string' || url.trim() === '') {
1008
- throw new TestError('INVALID_ARGUMENT', 'navigate requires a URL');
1009
- }
1010
- const resolved = resolveNavigationUrl(url, this.runtime.app.base, this.runtime.app.allowedOrigins).url;
1011
- // The raw argument is recorded, not the resolved URL: replay re-resolves
1012
- // through the same base and origin policy this call just passed.
1013
- await this.runAction('navigate', async () => {
1014
- await this.session.app.open(resolved, this.operation());
1015
- return { name: 'navigate', url };
1016
- });
1017
- }
406
+ }
407
+ /** The replay engine's view of a capture: the nodes, and the viewport a recorded point is checked against. */
408
+ function screenOf(observation) {
409
+ return { nodes: observation.nodes, viewport: { width: observation.viewport.width, height: observation.viewport.height } };
1018
410
  }
1019
411
  //# sourceMappingURL=act.js.map