@tangle-network/browser-agent-driver 0.33.3 → 0.35.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 (489) hide show
  1. package/README.md +42 -1
  2. package/discovery/llms.txt +27 -0
  3. package/discovery/tangle-agent.json +36 -0
  4. package/dist/brain/action-parse.d.ts +16 -0
  5. package/dist/brain/action-parse.d.ts.map +1 -0
  6. package/dist/brain/action-parse.js +191 -0
  7. package/dist/brain/action-parse.js.map +1 -0
  8. package/dist/brain/decide.d.ts +58 -0
  9. package/dist/brain/decide.d.ts.map +1 -0
  10. package/dist/brain/decide.js +418 -0
  11. package/dist/brain/decide.js.map +1 -0
  12. package/dist/brain/history-compact.d.ts +19 -0
  13. package/dist/brain/history-compact.d.ts.map +1 -0
  14. package/dist/brain/history-compact.js +153 -0
  15. package/dist/brain/history-compact.js.map +1 -0
  16. package/dist/brain/index.d.ts +78 -152
  17. package/dist/brain/index.d.ts.map +1 -1
  18. package/dist/brain/index.js +62 -2268
  19. package/dist/brain/index.js.map +1 -1
  20. package/dist/brain/model-client.d.ts +101 -0
  21. package/dist/brain/model-client.d.ts.map +1 -0
  22. package/dist/brain/model-client.js +391 -0
  23. package/dist/brain/model-client.js.map +1 -0
  24. package/dist/brain/plan.d.ts +57 -0
  25. package/dist/brain/plan.d.ts.map +1 -0
  26. package/dist/brain/plan.js +194 -0
  27. package/dist/brain/plan.js.map +1 -0
  28. package/dist/brain/prompts.d.ts +37 -0
  29. package/dist/brain/prompts.d.ts.map +1 -0
  30. package/dist/brain/prompts.js +455 -0
  31. package/dist/brain/prompts.js.map +1 -0
  32. package/dist/brain/provider-fetch.d.ts +26 -0
  33. package/dist/brain/provider-fetch.d.ts.map +1 -0
  34. package/dist/brain/provider-fetch.js +45 -0
  35. package/dist/brain/provider-fetch.js.map +1 -0
  36. package/dist/brain/snapshot-budget.d.ts +20 -0
  37. package/dist/brain/snapshot-budget.d.ts.map +1 -0
  38. package/dist/brain/snapshot-budget.js +202 -0
  39. package/dist/brain/snapshot-budget.js.map +1 -0
  40. package/dist/brain/system-prompt.d.ts +67 -0
  41. package/dist/brain/system-prompt.d.ts.map +1 -0
  42. package/dist/brain/system-prompt.js +128 -0
  43. package/dist/brain/system-prompt.js.map +1 -0
  44. package/dist/brain/tasks/design-audit.d.ts +33 -0
  45. package/dist/brain/tasks/design-audit.d.ts.map +1 -0
  46. package/dist/brain/tasks/design-audit.js +100 -0
  47. package/dist/brain/tasks/design-audit.js.map +1 -0
  48. package/dist/brain/tasks/evaluate.d.ts +25 -0
  49. package/dist/brain/tasks/evaluate.d.ts.map +1 -0
  50. package/dist/brain/tasks/evaluate.js +55 -0
  51. package/dist/brain/tasks/evaluate.js.map +1 -0
  52. package/dist/brain/tasks/goal-verification.d.ts +32 -0
  53. package/dist/brain/tasks/goal-verification.d.ts.map +1 -0
  54. package/dist/brain/tasks/goal-verification.js +68 -0
  55. package/dist/brain/tasks/goal-verification.js.map +1 -0
  56. package/dist/brain/tasks/knowledge.d.ts +26 -0
  57. package/dist/brain/tasks/knowledge.d.ts.map +1 -0
  58. package/dist/brain/tasks/knowledge.js +56 -0
  59. package/dist/brain/tasks/knowledge.js.map +1 -0
  60. package/dist/brain/tasks/link-scout.d.ts +37 -0
  61. package/dist/brain/tasks/link-scout.d.ts.map +1 -0
  62. package/dist/brain/tasks/link-scout.js +69 -0
  63. package/dist/brain/tasks/link-scout.js.map +1 -0
  64. package/dist/brain/types.d.ts +55 -0
  65. package/dist/brain/types.d.ts.map +1 -0
  66. package/dist/brain/types.js +7 -0
  67. package/dist/brain/types.js.map +1 -0
  68. package/dist/browser/stealth-init-script.d.ts +9 -0
  69. package/dist/browser/stealth-init-script.d.ts.map +1 -0
  70. package/dist/browser/stealth-init-script.js +128 -0
  71. package/dist/browser/stealth-init-script.js.map +1 -0
  72. package/dist/browser/storage-state.d.ts +29 -0
  73. package/dist/browser/storage-state.d.ts.map +1 -0
  74. package/dist/browser/storage-state.js +47 -0
  75. package/dist/browser/storage-state.js.map +1 -0
  76. package/dist/browser-launch.d.ts +6 -0
  77. package/dist/browser-launch.d.ts.map +1 -1
  78. package/dist/browser-launch.js +3 -0
  79. package/dist/browser-launch.js.map +1 -1
  80. package/dist/cli/args.d.ts +141 -0
  81. package/dist/cli/args.d.ts.map +1 -0
  82. package/dist/cli/args.js +167 -0
  83. package/dist/cli/args.js.map +1 -0
  84. package/dist/cli/benchmark-sync.d.ts +9 -0
  85. package/dist/cli/benchmark-sync.d.ts.map +1 -0
  86. package/dist/cli/benchmark-sync.js +43 -0
  87. package/dist/cli/benchmark-sync.js.map +1 -0
  88. package/dist/cli/build-config.d.ts +11 -0
  89. package/dist/cli/build-config.d.ts.map +1 -0
  90. package/dist/cli/build-config.js +355 -0
  91. package/dist/cli/build-config.js.map +1 -0
  92. package/dist/cli/commands/auth.d.ts +3 -0
  93. package/dist/cli/commands/auth.d.ts.map +1 -0
  94. package/dist/cli/commands/auth.js +36 -0
  95. package/dist/cli/commands/auth.js.map +1 -0
  96. package/dist/cli/commands/chrome-debug.d.ts +7 -0
  97. package/dist/cli/commands/chrome-debug.d.ts.map +1 -0
  98. package/dist/cli/commands/chrome-debug.js +11 -0
  99. package/dist/cli/commands/chrome-debug.js.map +1 -0
  100. package/dist/cli/commands/design-audit.d.ts +3 -0
  101. package/dist/cli/commands/design-audit.d.ts.map +1 -0
  102. package/dist/cli/commands/design-audit.js +68 -0
  103. package/dist/cli/commands/design-audit.js.map +1 -0
  104. package/dist/cli/commands/preview.d.ts +14 -0
  105. package/dist/cli/commands/preview.d.ts.map +1 -0
  106. package/dist/cli/commands/preview.js +31 -0
  107. package/dist/cli/commands/preview.js.map +1 -0
  108. package/dist/cli/commands/run.d.ts +3 -0
  109. package/dist/cli/commands/run.d.ts.map +1 -0
  110. package/dist/cli/commands/run.js +740 -0
  111. package/dist/cli/commands/run.js.map +1 -0
  112. package/dist/cli/commands/runs.d.ts +8 -0
  113. package/dist/cli/commands/runs.d.ts.map +1 -0
  114. package/dist/cli/commands/runs.js +35 -0
  115. package/dist/cli/commands/runs.js.map +1 -0
  116. package/dist/cli/commands/share.d.ts +10 -0
  117. package/dist/cli/commands/share.d.ts.map +1 -0
  118. package/dist/cli/commands/share.js +32 -0
  119. package/dist/cli/commands/share.js.map +1 -0
  120. package/dist/cli/commands/showcase.d.ts +17 -0
  121. package/dist/cli/commands/showcase.d.ts.map +1 -0
  122. package/dist/cli/commands/showcase.js +20 -0
  123. package/dist/cli/commands/showcase.js.map +1 -0
  124. package/dist/cli/commands/snapshot.d.ts +12 -0
  125. package/dist/cli/commands/snapshot.d.ts.map +1 -0
  126. package/dist/cli/commands/snapshot.js +24 -0
  127. package/dist/cli/commands/snapshot.js.map +1 -0
  128. package/dist/cli/commands/view.d.ts +7 -0
  129. package/dist/cli/commands/view.d.ts.map +1 -0
  130. package/dist/cli/commands/view.js +23 -0
  131. package/dist/cli/commands/view.js.map +1 -0
  132. package/dist/cli/constants.d.ts +6 -0
  133. package/dist/cli/constants.d.ts.map +1 -0
  134. package/dist/cli/constants.js +3 -0
  135. package/dist/cli/constants.js.map +1 -0
  136. package/dist/cli/version.d.ts +7 -0
  137. package/dist/cli/version.d.ts.map +1 -0
  138. package/dist/cli/version.js +16 -0
  139. package/dist/cli/version.js.map +1 -0
  140. package/dist/cli-design-audit.d.ts +44 -36
  141. package/dist/cli-design-audit.d.ts.map +1 -1
  142. package/dist/cli-design-audit.js +137 -1646
  143. package/dist/cli-design-audit.js.map +1 -1
  144. package/dist/cli-jobs.js +2 -1
  145. package/dist/cli-jobs.js.map +1 -1
  146. package/dist/cli.js +66 -1697
  147. package/dist/cli.js.map +1 -1
  148. package/dist/config.d.ts +12 -0
  149. package/dist/config.d.ts.map +1 -1
  150. package/dist/config.js +20 -2
  151. package/dist/config.js.map +1 -1
  152. package/dist/design/audit/evolve/agent.d.ts +23 -0
  153. package/dist/design/audit/evolve/agent.d.ts.map +1 -0
  154. package/dist/design/audit/evolve/agent.js +273 -0
  155. package/dist/design/audit/evolve/agent.js.map +1 -0
  156. package/dist/design/audit/evolve/css.d.ts +10 -0
  157. package/dist/design/audit/evolve/css.d.ts.map +1 -0
  158. package/dist/design/audit/evolve/css.js +206 -0
  159. package/dist/design/audit/evolve/css.js.map +1 -0
  160. package/dist/design/audit/evolve/index.d.ts +13 -0
  161. package/dist/design/audit/evolve/index.d.ts.map +1 -0
  162. package/dist/design/audit/evolve/index.js +12 -0
  163. package/dist/design/audit/evolve/index.js.map +1 -0
  164. package/dist/design/audit/evolve/report.d.ts +3 -0
  165. package/dist/design/audit/evolve/report.d.ts.map +1 -0
  166. package/dist/design/audit/evolve/report.js +45 -0
  167. package/dist/design/audit/evolve/report.js.map +1 -0
  168. package/dist/design/audit/evolve/types.d.ts +14 -0
  169. package/dist/design/audit/evolve/types.d.ts.map +1 -0
  170. package/dist/design/audit/evolve/types.js +2 -0
  171. package/dist/design/audit/evolve/types.js.map +1 -0
  172. package/dist/design/audit/pipeline.d.ts +12 -0
  173. package/dist/design/audit/pipeline.d.ts.map +1 -1
  174. package/dist/design/audit/pipeline.js +43 -27
  175. package/dist/design/audit/pipeline.js.map +1 -1
  176. package/dist/design/audit/reference/artifact/build.d.ts +33 -0
  177. package/dist/design/audit/reference/artifact/build.d.ts.map +1 -0
  178. package/dist/design/audit/reference/artifact/build.js +64 -0
  179. package/dist/design/audit/reference/artifact/build.js.map +1 -0
  180. package/dist/design/audit/reference/artifact/render.d.ts +39 -0
  181. package/dist/design/audit/reference/artifact/render.d.ts.map +1 -0
  182. package/dist/design/audit/reference/artifact/render.js +213 -0
  183. package/dist/design/audit/reference/artifact/render.js.map +1 -0
  184. package/dist/design/audit/reference/artifact/text.d.ts +11 -0
  185. package/dist/design/audit/reference/artifact/text.d.ts.map +1 -0
  186. package/dist/design/audit/reference/artifact/text.js +20 -0
  187. package/dist/design/audit/reference/artifact/text.js.map +1 -0
  188. package/dist/design/audit/reference/artifact/to-findings.d.ts +57 -0
  189. package/dist/design/audit/reference/artifact/to-findings.d.ts.map +1 -0
  190. package/dist/design/audit/reference/artifact/to-findings.js +314 -0
  191. package/dist/design/audit/reference/artifact/to-findings.js.map +1 -0
  192. package/dist/design/audit/reference/config.d.ts +50 -0
  193. package/dist/design/audit/reference/config.d.ts.map +1 -0
  194. package/dist/design/audit/reference/config.js +108 -0
  195. package/dist/design/audit/reference/config.js.map +1 -0
  196. package/dist/design/audit/reference/contracts.d.ts +1030 -0
  197. package/dist/design/audit/reference/contracts.d.ts.map +1 -0
  198. package/dist/design/audit/reference/contracts.js +28 -0
  199. package/dist/design/audit/reference/contracts.js.map +1 -0
  200. package/dist/design/audit/reference/corpus/build.d.ts +115 -0
  201. package/dist/design/audit/reference/corpus/build.d.ts.map +1 -0
  202. package/dist/design/audit/reference/corpus/build.js +152 -0
  203. package/dist/design/audit/reference/corpus/build.js.map +1 -0
  204. package/dist/design/audit/reference/corpus/schema.d.ts +33 -0
  205. package/dist/design/audit/reference/corpus/schema.d.ts.map +1 -0
  206. package/dist/design/audit/reference/corpus/schema.js +249 -0
  207. package/dist/design/audit/reference/corpus/schema.js.map +1 -0
  208. package/dist/design/audit/reference/corpus/store.d.ts +26 -0
  209. package/dist/design/audit/reference/corpus/store.d.ts.map +1 -0
  210. package/dist/design/audit/reference/corpus/store.js +104 -0
  211. package/dist/design/audit/reference/corpus/store.js.map +1 -0
  212. package/dist/design/audit/reference/dna/delta.d.ts +21 -0
  213. package/dist/design/audit/reference/dna/delta.d.ts.map +1 -0
  214. package/dist/design/audit/reference/dna/delta.js +126 -0
  215. package/dist/design/audit/reference/dna/delta.js.map +1 -0
  216. package/dist/design/audit/reference/dna/derive.d.ts +31 -0
  217. package/dist/design/audit/reference/dna/derive.d.ts.map +1 -0
  218. package/dist/design/audit/reference/dna/derive.js +381 -0
  219. package/dist/design/audit/reference/dna/derive.js.map +1 -0
  220. package/dist/design/audit/reference/dna/descriptor.d.ts +36 -0
  221. package/dist/design/audit/reference/dna/descriptor.d.ts.map +1 -0
  222. package/dist/design/audit/reference/dna/descriptor.js +209 -0
  223. package/dist/design/audit/reference/dna/descriptor.js.map +1 -0
  224. package/dist/design/audit/reference/dna/page-adapter.d.ts +17 -0
  225. package/dist/design/audit/reference/dna/page-adapter.d.ts.map +1 -0
  226. package/dist/design/audit/reference/dna/page-adapter.js +35 -0
  227. package/dist/design/audit/reference/dna/page-adapter.js.map +1 -0
  228. package/dist/design/audit/reference/dna/scroll-capture.d.ts +93 -0
  229. package/dist/design/audit/reference/dna/scroll-capture.d.ts.map +1 -0
  230. package/dist/design/audit/reference/dna/scroll-capture.js +374 -0
  231. package/dist/design/audit/reference/dna/scroll-capture.js.map +1 -0
  232. package/dist/design/audit/reference/engine/budget.d.ts +63 -0
  233. package/dist/design/audit/reference/engine/budget.d.ts.map +1 -0
  234. package/dist/design/audit/reference/engine/budget.js +110 -0
  235. package/dist/design/audit/reference/engine/budget.js.map +1 -0
  236. package/dist/design/audit/reference/engine/core.d.ts +32 -0
  237. package/dist/design/audit/reference/engine/core.d.ts.map +1 -0
  238. package/dist/design/audit/reference/engine/core.js +261 -0
  239. package/dist/design/audit/reference/engine/core.js.map +1 -0
  240. package/dist/design/audit/reference/engine/guard.d.ts +43 -0
  241. package/dist/design/audit/reference/engine/guard.d.ts.map +1 -0
  242. package/dist/design/audit/reference/engine/guard.js +43 -0
  243. package/dist/design/audit/reference/engine/guard.js.map +1 -0
  244. package/dist/design/audit/reference/engine/score-core.d.ts +48 -0
  245. package/dist/design/audit/reference/engine/score-core.d.ts.map +1 -0
  246. package/dist/design/audit/reference/engine/score-core.js +134 -0
  247. package/dist/design/audit/reference/engine/score-core.js.map +1 -0
  248. package/dist/design/audit/reference/engine/wiring.d.ts +35 -0
  249. package/dist/design/audit/reference/engine/wiring.d.ts.map +1 -0
  250. package/dist/design/audit/reference/engine/wiring.js +80 -0
  251. package/dist/design/audit/reference/engine/wiring.js.map +1 -0
  252. package/dist/design/audit/reference/eval/taste-core.d.ts +77 -0
  253. package/dist/design/audit/reference/eval/taste-core.d.ts.map +1 -0
  254. package/dist/design/audit/reference/eval/taste-core.js +95 -0
  255. package/dist/design/audit/reference/eval/taste-core.js.map +1 -0
  256. package/dist/design/audit/reference/generate/generator.d.ts +49 -0
  257. package/dist/design/audit/reference/generate/generator.d.ts.map +1 -0
  258. package/dist/design/audit/reference/generate/generator.js +100 -0
  259. package/dist/design/audit/reference/generate/generator.js.map +1 -0
  260. package/dist/design/audit/reference/generate/parse.d.ts +28 -0
  261. package/dist/design/audit/reference/generate/parse.d.ts.map +1 -0
  262. package/dist/design/audit/reference/generate/parse.js +220 -0
  263. package/dist/design/audit/reference/generate/parse.js.map +1 -0
  264. package/dist/design/audit/reference/generate/prompt.d.ts +36 -0
  265. package/dist/design/audit/reference/generate/prompt.d.ts.map +1 -0
  266. package/dist/design/audit/reference/generate/prompt.js +172 -0
  267. package/dist/design/audit/reference/generate/prompt.js.map +1 -0
  268. package/dist/design/audit/reference/index.d.ts +26 -0
  269. package/dist/design/audit/reference/index.d.ts.map +1 -0
  270. package/dist/design/audit/reference/index.js +31 -0
  271. package/dist/design/audit/reference/index.js.map +1 -0
  272. package/dist/design/audit/reference/judge/image-clamp.d.ts +34 -0
  273. package/dist/design/audit/reference/judge/image-clamp.d.ts.map +1 -0
  274. package/dist/design/audit/reference/judge/image-clamp.js +88 -0
  275. package/dist/design/audit/reference/judge/image-clamp.js.map +1 -0
  276. package/dist/design/audit/reference/judge/pairwise.d.ts +34 -0
  277. package/dist/design/audit/reference/judge/pairwise.d.ts.map +1 -0
  278. package/dist/design/audit/reference/judge/pairwise.js +105 -0
  279. package/dist/design/audit/reference/judge/pairwise.js.map +1 -0
  280. package/dist/design/audit/reference/judge/parse.d.ts +31 -0
  281. package/dist/design/audit/reference/judge/parse.d.ts.map +1 -0
  282. package/dist/design/audit/reference/judge/parse.js +113 -0
  283. package/dist/design/audit/reference/judge/parse.js.map +1 -0
  284. package/dist/design/audit/reference/judge/prompt.d.ts +38 -0
  285. package/dist/design/audit/reference/judge/prompt.d.ts.map +1 -0
  286. package/dist/design/audit/reference/judge/prompt.js +123 -0
  287. package/dist/design/audit/reference/judge/prompt.js.map +1 -0
  288. package/dist/design/audit/reference/judge/quality.d.ts +28 -0
  289. package/dist/design/audit/reference/judge/quality.d.ts.map +1 -0
  290. package/dist/design/audit/reference/judge/quality.js +62 -0
  291. package/dist/design/audit/reference/judge/quality.js.map +1 -0
  292. package/dist/design/audit/reference/judge/rank.d.ts +41 -0
  293. package/dist/design/audit/reference/judge/rank.d.ts.map +1 -0
  294. package/dist/design/audit/reference/judge/rank.js +196 -0
  295. package/dist/design/audit/reference/judge/rank.js.map +1 -0
  296. package/dist/design/audit/reference/judge/text-judge.d.ts +31 -0
  297. package/dist/design/audit/reference/judge/text-judge.d.ts.map +1 -0
  298. package/dist/design/audit/reference/judge/text-judge.js +43 -0
  299. package/dist/design/audit/reference/judge/text-judge.js.map +1 -0
  300. package/dist/design/audit/reference/judge/vision-judge.d.ts +59 -0
  301. package/dist/design/audit/reference/judge/vision-judge.d.ts.map +1 -0
  302. package/dist/design/audit/reference/judge/vision-judge.js +150 -0
  303. package/dist/design/audit/reference/judge/vision-judge.js.map +1 -0
  304. package/dist/design/audit/reference/judge/vision-model.d.ts +65 -0
  305. package/dist/design/audit/reference/judge/vision-model.d.ts.map +1 -0
  306. package/dist/design/audit/reference/judge/vision-model.js +110 -0
  307. package/dist/design/audit/reference/judge/vision-model.js.map +1 -0
  308. package/dist/design/audit/reference/pipeline/evaluate-reference.d.ts +68 -0
  309. package/dist/design/audit/reference/pipeline/evaluate-reference.d.ts.map +1 -0
  310. package/dist/design/audit/reference/pipeline/evaluate-reference.js +52 -0
  311. package/dist/design/audit/reference/pipeline/evaluate-reference.js.map +1 -0
  312. package/dist/design/audit/reference/reference-context.d.ts +31 -0
  313. package/dist/design/audit/reference/reference-context.d.ts.map +1 -0
  314. package/dist/design/audit/reference/reference-context.js +73 -0
  315. package/dist/design/audit/reference/reference-context.js.map +1 -0
  316. package/dist/design/audit/reference/retrieval/embedding-hash.d.ts +30 -0
  317. package/dist/design/audit/reference/retrieval/embedding-hash.d.ts.map +1 -0
  318. package/dist/design/audit/reference/retrieval/embedding-hash.js +87 -0
  319. package/dist/design/audit/reference/retrieval/embedding-hash.js.map +1 -0
  320. package/dist/design/audit/reference/retrieval/embedding-openai.d.ts +38 -0
  321. package/dist/design/audit/reference/retrieval/embedding-openai.d.ts.map +1 -0
  322. package/dist/design/audit/reference/retrieval/embedding-openai.js +73 -0
  323. package/dist/design/audit/reference/retrieval/embedding-openai.js.map +1 -0
  324. package/dist/design/audit/reference/retrieval/matcher.d.ts +34 -0
  325. package/dist/design/audit/reference/retrieval/matcher.d.ts.map +1 -0
  326. package/dist/design/audit/reference/retrieval/matcher.js +107 -0
  327. package/dist/design/audit/reference/retrieval/matcher.js.map +1 -0
  328. package/dist/design/audit/reference/run.d.ts +59 -0
  329. package/dist/design/audit/reference/run.d.ts.map +1 -0
  330. package/dist/design/audit/reference/run.js +99 -0
  331. package/dist/design/audit/reference/run.js.map +1 -0
  332. package/dist/design/audit/report.d.ts +35 -0
  333. package/dist/design/audit/report.d.ts.map +1 -0
  334. package/dist/design/audit/report.js +195 -0
  335. package/dist/design/audit/report.js.map +1 -0
  336. package/dist/design/audit/tokens/extract.d.ts +71 -0
  337. package/dist/design/audit/tokens/extract.d.ts.map +1 -0
  338. package/dist/design/audit/tokens/extract.js +1034 -0
  339. package/dist/design/audit/tokens/extract.js.map +1 -0
  340. package/dist/design/audit/types.d.ts +8 -0
  341. package/dist/design/audit/types.d.ts.map +1 -1
  342. package/dist/design/compare.d.ts.map +1 -1
  343. package/dist/design/compare.js +2 -6
  344. package/dist/design/compare.js.map +1 -1
  345. package/dist/design/cookie-consent.d.ts +3 -0
  346. package/dist/design/cookie-consent.d.ts.map +1 -0
  347. package/dist/design/cookie-consent.js +26 -0
  348. package/dist/design/cookie-consent.js.map +1 -0
  349. package/dist/design/viewports.d.ts +18 -0
  350. package/dist/design/viewports.d.ts.map +1 -0
  351. package/dist/design/viewports.js +10 -0
  352. package/dist/design/viewports.js.map +1 -0
  353. package/dist/index.d.ts +2 -2
  354. package/dist/index.d.ts.map +1 -1
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/memory/store.d.ts +19 -1
  358. package/dist/memory/store.d.ts.map +1 -1
  359. package/dist/memory/store.js +47 -0
  360. package/dist/memory/store.js.map +1 -1
  361. package/dist/provider-defaults.d.ts +18 -0
  362. package/dist/provider-defaults.d.ts.map +1 -1
  363. package/dist/provider-defaults.js +33 -0
  364. package/dist/provider-defaults.js.map +1 -1
  365. package/dist/runner/allowed-domains.d.ts +11 -0
  366. package/dist/runner/allowed-domains.d.ts.map +1 -0
  367. package/dist/runner/allowed-domains.js +14 -0
  368. package/dist/runner/allowed-domains.js.map +1 -0
  369. package/dist/runner/batch-fill.d.ts +28 -0
  370. package/dist/runner/batch-fill.d.ts.map +1 -0
  371. package/dist/runner/batch-fill.js +80 -0
  372. package/dist/runner/batch-fill.js.map +1 -0
  373. package/dist/runner/completion-policy.d.ts +26 -0
  374. package/dist/runner/completion-policy.d.ts.map +1 -0
  375. package/dist/runner/completion-policy.js +76 -0
  376. package/dist/runner/completion-policy.js.map +1 -0
  377. package/dist/runner/constants.d.ts +14 -0
  378. package/dist/runner/constants.d.ts.map +1 -0
  379. package/dist/runner/constants.js +23 -0
  380. package/dist/runner/constants.js.map +1 -0
  381. package/dist/runner/decision-screenshot.d.ts +24 -0
  382. package/dist/runner/decision-screenshot.d.ts.map +1 -0
  383. package/dist/runner/decision-screenshot.js +23 -0
  384. package/dist/runner/decision-screenshot.js.map +1 -0
  385. package/dist/runner/domain-boundary.d.ts +36 -0
  386. package/dist/runner/domain-boundary.d.ts.map +1 -0
  387. package/dist/runner/domain-boundary.js +81 -0
  388. package/dist/runner/domain-boundary.js.map +1 -0
  389. package/dist/runner/effect-verify.d.ts +28 -0
  390. package/dist/runner/effect-verify.d.ts.map +1 -0
  391. package/dist/runner/effect-verify.js +42 -0
  392. package/dist/runner/effect-verify.js.map +1 -0
  393. package/dist/runner/execute-plan.d.ts +58 -0
  394. package/dist/runner/execute-plan.d.ts.map +1 -0
  395. package/dist/runner/execute-plan.js +426 -0
  396. package/dist/runner/execute-plan.js.map +1 -0
  397. package/dist/runner/max-turns-extension.d.ts +19 -0
  398. package/dist/runner/max-turns-extension.d.ts.map +1 -0
  399. package/dist/runner/max-turns-extension.js +25 -0
  400. package/dist/runner/max-turns-extension.js.map +1 -0
  401. package/dist/runner/micro-plan.d.ts +11 -0
  402. package/dist/runner/micro-plan.d.ts.map +1 -0
  403. package/dist/runner/micro-plan.js +29 -0
  404. package/dist/runner/micro-plan.js.map +1 -0
  405. package/dist/runner/prompt-snippets.d.ts +5 -0
  406. package/dist/runner/prompt-snippets.d.ts.map +1 -0
  407. package/dist/runner/prompt-snippets.js +13 -0
  408. package/dist/runner/prompt-snippets.js.map +1 -0
  409. package/dist/runner/replay/contracts.d.ts +300 -0
  410. package/dist/runner/replay/contracts.d.ts.map +1 -0
  411. package/dist/runner/replay/contracts.js +42 -0
  412. package/dist/runner/replay/contracts.js.map +1 -0
  413. package/dist/runner/replay/controller.d.ts +27 -0
  414. package/dist/runner/replay/controller.d.ts.map +1 -0
  415. package/dist/runner/replay/controller.js +197 -0
  416. package/dist/runner/replay/controller.js.map +1 -0
  417. package/dist/runner/replay/guard.d.ts +28 -0
  418. package/dist/runner/replay/guard.d.ts.map +1 -0
  419. package/dist/runner/replay/guard.js +100 -0
  420. package/dist/runner/replay/guard.js.map +1 -0
  421. package/dist/runner/runner.d.ts +44 -51
  422. package/dist/runner/runner.d.ts.map +1 -1
  423. package/dist/runner/runner.js +155 -851
  424. package/dist/runner/runner.js.map +1 -1
  425. package/dist/runner/scout-feedback.d.ts +51 -0
  426. package/dist/runner/scout-feedback.d.ts.map +1 -0
  427. package/dist/runner/scout-feedback.js +149 -0
  428. package/dist/runner/scout-feedback.js.map +1 -0
  429. package/dist/skills/macro-loader.d.ts +1 -1
  430. package/dist/test-runner.d.ts.map +1 -1
  431. package/dist/test-runner.js +27 -1
  432. package/dist/test-runner.js.map +1 -1
  433. package/dist/types/actions.d.ts +241 -0
  434. package/dist/types/actions.d.ts.map +1 -0
  435. package/dist/types/actions.js +5 -0
  436. package/dist/types/actions.js.map +1 -0
  437. package/dist/types/config.d.ts +211 -0
  438. package/dist/types/config.d.ts.map +1 -0
  439. package/dist/types/config.js +2 -0
  440. package/dist/types/config.js.map +1 -0
  441. package/dist/types/design-audit.d.ts +92 -0
  442. package/dist/types/design-audit.d.ts.map +1 -0
  443. package/dist/types/design-audit.js +5 -0
  444. package/dist/types/design-audit.js.map +1 -0
  445. package/dist/types/design-tokens.d.ts +138 -0
  446. package/dist/types/design-tokens.d.ts.map +1 -0
  447. package/dist/types/design-tokens.js +5 -0
  448. package/dist/types/design-tokens.js.map +1 -0
  449. package/dist/types/page.d.ts +16 -0
  450. package/dist/types/page.d.ts.map +1 -0
  451. package/dist/types/page.js +2 -0
  452. package/dist/types/page.js.map +1 -0
  453. package/dist/types/plan.d.ts +50 -0
  454. package/dist/types/plan.d.ts.map +1 -0
  455. package/dist/types/plan.js +2 -0
  456. package/dist/types/plan.js.map +1 -0
  457. package/dist/types/preview.d.ts +9 -0
  458. package/dist/types/preview.d.ts.map +1 -0
  459. package/dist/types/preview.js +5 -0
  460. package/dist/types/preview.js.map +1 -0
  461. package/dist/types/result.d.ts +62 -0
  462. package/dist/types/result.d.ts.map +1 -0
  463. package/dist/types/result.js +2 -0
  464. package/dist/types/result.js.map +1 -0
  465. package/dist/types/scenario.d.ts +20 -0
  466. package/dist/types/scenario.d.ts.map +1 -0
  467. package/dist/types/scenario.js +5 -0
  468. package/dist/types/scenario.js.map +1 -0
  469. package/dist/types/test-runner.d.ts +145 -0
  470. package/dist/types/test-runner.d.ts.map +1 -0
  471. package/dist/types/test-runner.js +10 -0
  472. package/dist/types/test-runner.js.map +1 -0
  473. package/dist/types/trajectory.d.ts +38 -0
  474. package/dist/types/trajectory.d.ts.map +1 -0
  475. package/dist/types/trajectory.js +2 -0
  476. package/dist/types/trajectory.js.map +1 -0
  477. package/dist/types/turn.d.ts +50 -0
  478. package/dist/types/turn.d.ts.map +1 -0
  479. package/dist/types/turn.js +2 -0
  480. package/dist/types/turn.js.map +1 -0
  481. package/dist/types.d.ts +16 -1017
  482. package/dist/types.d.ts.map +1 -1
  483. package/dist/types.js +15 -8
  484. package/dist/types.js.map +1 -1
  485. package/dist/wallet/rpc-interception.d.ts +15 -0
  486. package/dist/wallet/rpc-interception.d.ts.map +1 -0
  487. package/dist/wallet/rpc-interception.js +95 -0
  488. package/dist/wallet/rpc-interception.js.map +1 -0
  489. package/package.json +23 -5
@@ -25,12 +25,18 @@ import { ContextBudget } from '../context-budget.js';
25
25
  import { runOverridePipeline } from '../override-pipeline.js';
26
26
  import { withRetry, findElementForRef, safeHostname, pushGoalVerificationEvidence } from './utils.js';
27
27
  import { runExtractWithIndex, formatExtractWithIndexResult } from '../drivers/extract-with-index.js';
28
- import { buildSearchResultsGuidance, buildVisibleLinkRecommendation, getVisibleLinkRecommendation, getRankedVisibleLinkCandidates, rankSearchCandidates } from './search-guidance.js';
28
+ import { buildSearchResultsGuidance, buildVisibleLinkRecommendation, getVisibleLinkRecommendation } from './search-guidance.js';
29
29
  import { buildGoalVerificationClaim, collectSearchWorkflowEvidence, shouldAcceptSearchWorkflowCompletion, shouldAcceptScriptBackedCompletion, detectCompletionContentTypeMismatch } from './goal-verification.js';
30
- import { verifyExpectedEffect } from './effect-verification.js';
31
30
  import { detectAiTanglePartnerTemplateVisibleState, detectAiTangleVerifiedOutputState, shouldEscalateVision } from './page-analysis.js';
32
- import { shouldUseVisibleLinkScout, shouldUseVisibleLinkScoutPage, shouldUseBoundedBranchExplorer, inspectBranchPreview, scoreBranchPreview } from './scout.js';
33
31
  import { buildOverrideProducers, buildScoutLinkRecommendationText, buildBranchLinkRecommendationText } from './overrides.js';
32
+ import { buildSearchResultsScoutFeedbackImpl, buildVisibleLinkScoutRecommendationImpl, buildBranchLinkRecommendationImpl, } from './scout-feedback.js';
33
+ import { filterScoutCandidatesByAllowedDomainsImpl, inspectDisallowedSearchClickImpl, enforceAllowedDomainBoundaryImpl, } from './domain-boundary.js';
34
+ import { attachDecisionScreenshotImpl } from './decision-screenshot.js';
35
+ import { verifyEffectImpl } from './effect-verify.js';
36
+ import { executePlanImpl } from './execute-plan.js';
37
+ import { verifyExpectedEffect } from './effect-verification.js';
38
+ import { createReplayController } from './replay/controller.js';
39
+ import { createReplayGuard } from './replay/guard.js';
34
40
  import { RunRegistry } from '../memory/run-registry.js';
35
41
  import { ensureBus } from './events.js';
36
42
  import { DecisionCache } from './decision-cache.js';
@@ -40,81 +46,15 @@ import { matchDeterministicPattern } from './deterministic-patterns.js';
40
46
  import { deriveDirectStartUrl, shouldAcceptRolledBookingCompletion } from './direct-start.js';
41
47
  import { prepareGoogleFlightsSearch, shouldAcceptRolledGoogleFlightsCompletion } from './google-flights-preflight.js';
42
48
  import { containsSelfContradictingCompletion } from './completion-language.js';
43
- /**
44
- * Detect when the agent is filling a multi-field form one input at a time and
45
- * inject a hint that demands a `fill` batch on the next turn.
46
- *
47
- * Trigger conditions (all must hold):
48
- * 1. The agent's most recent action was a single-step `type` on the
49
- * current URL
50
- * 2. The current snapshot has 2+ unused fillable refs (textbox /
51
- * searchbox / combobox) that the agent hasn't typed into yet
52
- * 3. We haven't already injected this hint in the last turn (to avoid
53
- * hint loops if the agent ignores it)
54
- *
55
- * The detector fires after one type action when two or more unused fields
56
- * remain, which catches common two-field-per-step forms before the agent
57
- * burns extra turns.
58
- *
59
- * The hint is high-priority (100) so it survives ctxBudget truncation, and
60
- * it explicitly lists the unused @refs from the current snapshot so the LLM
61
- * doesn't have to guess. The injection is gated by BAD_BATCH_HINT=0 for
62
- * rollback.
63
- */
64
- export function detectBatchFillOpportunity(turns, state) {
65
- if (turns.length === 0)
66
- return null;
67
- const lastTurn = turns[turns.length - 1];
68
- // Last action must be a single-step type on the current URL
69
- if (lastTurn.action.action !== 'type')
70
- return null;
71
- if (lastTurn.state.url !== state.url)
72
- return null;
73
- // Collect the @refs the agent has typed into across the ENTIRE run on
74
- // the same URL. The detector should never ask the agent to re-fill a
75
- // field it already filled, even if the earlier fill happened many
76
- // turns ago.
77
- const usedRefs = new Set();
78
- for (const t of turns) {
79
- if (t.state.url !== state.url)
80
- continue;
81
- const a = t.action;
82
- if (a.action === 'type' && 'selector' in a && typeof a.selector === 'string') {
83
- usedRefs.add(a.selector);
84
- }
85
- if (a.action === 'fill') {
86
- for (const k of Object.keys(a.fields ?? {}))
87
- usedRefs.add(k);
88
- for (const k of Object.keys(a.selects ?? {}))
89
- usedRefs.add(k);
90
- for (const k of a.checks ?? [])
91
- usedRefs.add(k);
92
- }
93
- }
94
- // Find unused fillable refs in the current snapshot. We look for textbox,
95
- // searchbox, combobox, and spinbutton roles — anything that takes text.
96
- // Snapshot lines look like: ` - textbox "First name" [ref=t1f2a]`
97
- const unusedRefs = [];
98
- for (const line of state.snapshot.split('\n')) {
99
- const match = line.match(/\b(textbox|searchbox|combobox|spinbutton)\b[^"]*"([^"]*)"[^[]*\[ref=([^\]]+)\]/i);
100
- if (!match)
101
- continue;
102
- const role = match[1].toLowerCase();
103
- const name = match[2];
104
- const ref = `@${match[3]}`;
105
- if (usedRefs.has(ref))
106
- continue;
107
- unusedRefs.push({ ref, name, role });
108
- }
109
- // Need at least 2 unused fields to make batching worthwhile
110
- if (unusedRefs.length < 2)
111
- return null;
112
- const refList = unusedRefs
113
- .slice(0, 12) // cap so we don't explode the prompt
114
- .map((u) => ` - ${u.ref} (${u.role}: "${u.name}")`)
115
- .join('\n');
116
- return `\n[BATCH FILL REQUIRED]\nYou just typed into a single field, but ${unusedRefs.length} more fillable fields are visible on this same form. STOP. Your NEXT action MUST be a \`fill\` action that batches ALL remaining unused fields on this page in one turn. Do not emit another single-step \`type\` — emit \`fill\` with multiple entries.\n\nUnused fillable @refs from the current snapshot (use these in your \`fill.fields\` map):\n${refList}\n\nExample:\n{\"action\":\"fill\",\"fields\":{\"${unusedRefs[0].ref}\":\"value1\",\"${unusedRefs[1].ref}\":\"value2\"}}\n`;
117
- }
49
+ import { detectBatchFillOpportunity } from './batch-fill.js';
50
+ import { DEFAULT_MAX_TURNS, DEFAULT_RETRIES, DEFAULT_RETRY_DELAY_MS, DEFAULT_SUPERVISOR, EXTENSION_TURNS_GRANTED, } from './constants.js';
51
+ import { DEFI_BRAIN_CONTEXT } from './prompt-snippets.js';
52
+ import { hasPlaceholderPattern, isMeaningfulRunScriptOutput } from './completion-policy.js';
53
+ import { selectMicroPlanFollowUps } from './micro-plan.js';
54
+ import { decideMaxTurnsExtension } from './max-turns-extension.js';
55
+ // Re-export helpers that moved into sibling modules so existing import paths
56
+ // (e.g. tests importing from './runner.js') keep working unchanged.
57
+ export { detectBatchFillOpportunity, hasPlaceholderPattern, isMeaningfulRunScriptOutput };
118
58
  /** Build a structured session record from a completed run */
119
59
  function buildSession(scenario, result) {
120
60
  const lastTurn = result.turns[result.turns.length - 1];
@@ -129,99 +69,6 @@ function buildSession(scenario, result) {
129
69
  durationMs: result.totalMs,
130
70
  };
131
71
  }
132
- const DEFAULT_MAX_TURNS = 20;
133
- const DEFAULT_RETRIES = 3;
134
- const DEFAULT_RETRY_DELAY_MS = 1000;
135
- const DEFAULT_MICRO_PLAN_ACTIONS = 2;
136
- // Safe action verbs for micro-plans emitted by the model.
137
- const SAFE_MICRO_ACTIONS = new Set(['click', 'type', 'press', 'hover', 'select', 'scroll', 'wait', 'clickAt', 'typeAt', 'clickLabel', 'typeLabel']);
138
- const DEFAULT_SUPERVISOR = {
139
- enabled: true,
140
- useVision: true,
141
- minTurnsBeforeInvoke: 5,
142
- cooldownTurns: 3,
143
- maxInterventions: 2,
144
- hardStallWindow: 4,
145
- };
146
- const DEFI_BRAIN_CONTEXT = '\nWALLET/DeFi MODE ACTIVE — crypto app patterns:\n' +
147
- '- PERSISTENT WIDGETS: Many DeFi apps have always-on support/chat widgets (bottom-right) that show as alertdialog. These CANNOT be dismissed. Ignore them and interact with the page directly.\n' +
148
- '- WALLET CONNECTION: Look for "Connect Wallet" button, select MetaMask. The wallet extension handles the approval popup automatically.\n' +
149
- '- TOKEN SELECTION: If you need to change a token, click the token selector button (usually shows token symbol/icon), search for the token name, and select it from the dropdown.\n' +
150
- '- TRANSACTION FLOW: Enter amount → wait for quote/estimate to load → click action button (Swap/Supply/etc.) → review dialog appears → stop before confirming in MetaMask.\n' +
151
- '- LOADING STATES: DeFi apps make many RPC calls. Wait for balances and quotes to appear before clicking action buttons. If a button says "Enter amount" or is disabled, the app is still loading.\n' +
152
- '- NATIVE ETH: Prefer native ETH over wrapped tokens (WETH) when possible — avoids ERC-20 spending approval steps.\n' +
153
- '- NETWORK SELECTOR: Do NOT change the network/chain. If a network dropdown opens accidentally, close it immediately.\n' +
154
- '- COOKIE BANNERS: Dismiss immediately via Escape or Reject button — don\'t spend multiple turns on consent dialogs.\n';
155
- /**
156
- * Detect placeholder patterns in a planner-generated complete.result.
157
- *
158
- * The planner has to commit to its `complete.result` text BEFORE any prior
159
- * runScript step actually runs, so on extraction tasks it fabricates
160
- * placeholders. We detect those patterns and substitute the runScript
161
- * output (deterministic, no extra LLM call).
162
- *
163
- * Patterns we catch:
164
- * - JSON `null` literals (e.g. `{"x": null, "y": null}`)
165
- * - "<from prior step>", "<placeholder>", "<value from ...>", "<extracted ...>", "<observed ...>"
166
- * - "{{...}}" template markers
167
- *
168
- * Conservative on purpose — we only substitute when the planner clearly
169
- * didn't know real values at planning time. A complete.result that contains
170
- * actual data (no nulls, no placeholder markers) passes through unchanged.
171
- */
172
- export function hasPlaceholderPattern(text) {
173
- if (typeof text !== 'string' || text.length === 0)
174
- return false;
175
- if (/<from prior step>|<placeholder>|<value from|<extracted|<observed|<previous step|<runscript output>/i.test(text)) {
176
- return true;
177
- }
178
- if (/\{\{[^}]+\}\}/.test(text)) {
179
- return true;
180
- }
181
- // JSON-shape detection: a result that parses as JSON and contains null
182
- // values is almost always a planner-fabricated extraction shell. Pure
183
- // strings with the word "null" elsewhere don't match because we look
184
- // for the JSON null literal pattern (`: null` or `[null`).
185
- if (/:\s*null\b|\[\s*null\b/.test(text)) {
186
- return true;
187
- }
188
- return false;
189
- }
190
- /** Returns true only when runScript output contains usable extracted data. */
191
- export function isMeaningfulRunScriptOutput(output) {
192
- if (typeof output !== 'string')
193
- return false;
194
- const trimmed = output.trim();
195
- if (trimmed.length === 0)
196
- return false;
197
- if (trimmed === 'null' || trimmed === 'undefined' || trimmed === '""' || trimmed === "''")
198
- return false;
199
- // Empty JSON shells: `{}`, `[]`, `{"x": null}`, `[null, null]`
200
- if (trimmed === '{}' || trimmed === '[]')
201
- return false;
202
- if (hasPlaceholderPattern(trimmed))
203
- return false;
204
- // If the output parses as JSON and EVERY top-level value is null/empty,
205
- // treat it as not meaningful. This catches `{"x": null, "y": ""}` even
206
- // though the placeholder regex would already catch the null one.
207
- try {
208
- const parsed = JSON.parse(trimmed);
209
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
210
- const values = Object.values(parsed);
211
- if (values.length > 0) {
212
- const allEmpty = values.every((v) => v === null || v === undefined || v === '' || v === 0);
213
- if (allEmpty)
214
- return false;
215
- }
216
- }
217
- if (Array.isArray(parsed) && parsed.length === 0)
218
- return false;
219
- }
220
- catch {
221
- // Not JSON, that's fine — fall through to "meaningful" if we got here.
222
- }
223
- return true;
224
- }
225
72
  export function shouldUsePlannerForScenario(scenario, mode = 'always') {
226
73
  if (mode !== 'auto')
227
74
  return true;
@@ -242,6 +89,12 @@ export function shouldUsePlannerForScenario(scenario, mode = 'always') {
242
89
  return true;
243
90
  }
244
91
  export class BrowserAgent {
92
+ // Public fields below satisfy the extracted host interfaces (RunnerScoutHost,
93
+ // RunnerDomainHost, RunnerDecisionScreenshotHost, RunnerEffectVerifyHost,
94
+ // RunnerExecuteHost). The free functions in scout-feedback.ts,
95
+ // domain-boundary.ts, decision-screenshot.ts, effect-verify.ts, and
96
+ // execute-plan.ts read them through those interfaces; `implements` makes tsc
97
+ // prove the surface is complete.
245
98
  driver;
246
99
  brain;
247
100
  config;
@@ -249,11 +102,15 @@ export class BrowserAgent {
249
102
  beforeTurn;
250
103
  onPhaseTiming;
251
104
  referenceTrajectory;
105
+ replayPlan;
252
106
  projectStore;
253
107
  runRegistry;
254
108
  knowledge;
255
109
  selectorCache;
110
+ // Public so the extracted effect-verify.ts / execute-plan.ts hosts can read
111
+ // and write the cached post-action snapshot.
256
112
  cachedPostState;
113
+ // Public so the extracted execute-plan.ts host can emit loop events.
257
114
  bus;
258
115
  currentRunId = '';
259
116
  // In-session decision cache. Lazy-skips brain.decide() when the (snapshot,
@@ -272,6 +129,7 @@ export class BrowserAgent {
272
129
  this.beforeTurn = options.beforeTurn;
273
130
  this.onPhaseTiming = options.onPhaseTiming;
274
131
  this.referenceTrajectory = options.referenceTrajectory;
132
+ this.replayPlan = options.replayPlan;
275
133
  this.bus = ensureBus(options.eventBus);
276
134
  this.extensions = options.extensions;
277
135
  if (this.extensions) {
@@ -335,9 +193,6 @@ export class BrowserAgent {
335
193
  // when they reach the configured cap while still making page progress.
336
194
  let maxTurns = isVisionMode ? Math.max(baseMaxTurns, 30) : baseMaxTurns;
337
195
  let extensionGranted = false;
338
- const EXTENSION_TURNS_GRANTED = 5;
339
- const EXTENSION_HARD_CAP = 25;
340
- const EXTENSION_PROGRESS_LOOKBACK = 3;
341
196
  const retries = this.config.retries ?? DEFAULT_RETRIES;
342
197
  const retryDelayMs = this.config.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
343
198
  const turns = [];
@@ -499,6 +354,59 @@ export class BrowserAgent {
499
354
  maxInterventions: this.config.supervisor?.maxInterventions ?? DEFAULT_SUPERVISOR.maxInterventions,
500
355
  hardStallWindow: this.config.supervisor?.hardStallWindow ?? DEFAULT_SUPERVISOR.hardStallWindow,
501
356
  };
357
+ // Replay-first path (sibling to the planner-first path below): when replay
358
+ // is enabled and a strict-matched plan was supplied, re-execute the recorded
359
+ // steps with ZERO per-action LLM calls. A fully-replayed-and-verified
360
+ // outcome returns immediately; anything else (a self-heal abort, or a clean
361
+ // replay whose goal verification did not confirm achievement) falls through
362
+ // to the planner / per-action loop FROM THE CURRENT PAGE STATE — the page is
363
+ // already navigated above, so there is no re-navigation. Replay appends to
364
+ // the same `turns` array, so the unified timeline is preserved and the loop
365
+ // offset (`plannerStartTurn`) is advanced past the replayed turns.
366
+ let replayTurnsConsumed = 0;
367
+ if (this.config.replay?.enabled && this.replayPlan && scenario.startUrl) {
368
+ const replayOutcome = await createReplayController().replay(this.replayPlan, {
369
+ goal: scenario.goal,
370
+ driver: this.driver,
371
+ guard: createReplayGuard(),
372
+ verifyGoal: (state, goal, claimed) => this.brain.verifyGoalCompletion(state, goal, claimed),
373
+ verifyEffect: (input) => verifyExpectedEffect(input),
374
+ ...(scenario.signal ? { signal: scenario.signal } : {}),
375
+ ...(this.config.debug ? { debug: true } : {}),
376
+ onEvent: (event) => this.handleReplayEvent(runId, event),
377
+ });
378
+ for (const turn of replayOutcome.turns) {
379
+ turns.push(turn);
380
+ this.onTurn?.(turn);
381
+ }
382
+ replayTurnsConsumed = turns.length;
383
+ if (replayOutcome.kind === 'completed' && replayOutcome.goalVerification.achieved) {
384
+ return buildResult({
385
+ success: true,
386
+ result: replayOutcome.finalResult,
387
+ turns,
388
+ totalMs: Date.now() - startTime,
389
+ goalVerification: replayOutcome.goalVerification,
390
+ });
391
+ }
392
+ // healed | completed-but-not-achieved → fall through from the live state.
393
+ // (`no-candidate` is impossible here — a plan was supplied.)
394
+ if (replayOutcome.kind !== 'no-candidate') {
395
+ // Only prime the loop's first observe when we captured a usable
396
+ // snapshot; an empty snapshot would otherwise short-circuit observe().
397
+ if (replayOutcome.lastState.snapshot) {
398
+ this.cachedPostState = replayOutcome.lastState;
399
+ }
400
+ const fallbackReason = replayOutcome.kind === 'healed'
401
+ ? replayOutcome.reason
402
+ : `replay completed ${replayOutcome.completedSteps}/${replayOutcome.totalSteps} steps but goal verification did not confirm achievement`;
403
+ this.brain.injectFeedback(`[REPLAY] A recorded shortcut for this goal was attempted but did not fully complete it (${fallbackReason}). `
404
+ + `${replayTurnsConsumed} step(s) were already executed against the live page. Continue toward the goal from the current page state.`);
405
+ if (this.config.debug) {
406
+ console.log(`[Runner] Replay fell through after ${replayTurnsConsumed} step(s): ${fallbackReason}`);
407
+ }
408
+ }
409
+ }
502
410
  // Planner-first path: make one LLM call to generate a plan, then execute
503
411
  // it deterministically. On deviation, replan from the current page state
504
412
  // with deviation context; after the retry budget is exhausted, fall back
@@ -507,7 +415,7 @@ export class BrowserAgent {
507
415
  // Plan execution writes to the same `turns` array, so post-run analysis
508
416
  // sees a unified timeline regardless of which path completed the run.
509
417
  let planFallbackContext = '';
510
- let plannerStartTurn = 0;
418
+ let plannerStartTurn = replayTurnsConsumed;
511
419
  const plannerEnabled = this.config.plannerEnabled === true && process.env.BAD_PLANNER !== '0'
512
420
  && shouldUsePlannerForScenario(scenario, this.config.plannerMode ?? 'always');
513
421
  const maxReplans = 3;
@@ -538,7 +446,9 @@ export class BrowserAgent {
538
446
  if (initialState) {
539
447
  this.cachedPostState = initialState;
540
448
  let planLoopState = initialState;
541
- let cumulativeTurnsConsumed = 0;
449
+ // Start past any replayed turns so the planner's turn numbering and the
450
+ // per-action loop offset stay contiguous with the replay timeline.
451
+ let cumulativeTurnsConsumed = replayTurnsConsumed;
542
452
  let attempt = 0;
543
453
  let lastDeviationReason = '';
544
454
  let lastFailedStepIndex = 0;
@@ -675,24 +585,23 @@ export class BrowserAgent {
675
585
  for (let i = 1 + plannerStartTurn; i <= maxTurns; i++) {
676
586
  // Grant a one-time max-turns extension when the run reaches its cap
677
587
  // while still making recent progress.
678
- if (i === maxTurns
679
- && !extensionGranted
680
- && !isVisionMode
681
- && maxTurns < EXTENSION_HARD_CAP
682
- && runState.lastProgressTurn >= maxTurns - EXTENSION_PROGRESS_LOOKBACK) {
683
- const extendedMax = Math.min(maxTurns + EXTENSION_TURNS_GRANTED, EXTENSION_HARD_CAP);
684
- const extra = extendedMax - maxTurns;
685
- if (extra > 0) {
686
- this.bus.emitNow({
687
- type: 'recovery-fired',
688
- runId,
689
- turn: i,
690
- strategy: 'max-turns-extension',
691
- feedback: `Granted +${extra} extra turns (cap ${maxTurns} → ${extendedMax}); progress at turn ${runState.lastProgressTurn}.`,
692
- });
693
- maxTurns = extendedMax;
694
- extensionGranted = true;
695
- }
588
+ const turnsExtension = decideMaxTurnsExtension({
589
+ turn: i,
590
+ maxTurns,
591
+ extensionGranted,
592
+ isVisionMode,
593
+ lastProgressTurn: runState.lastProgressTurn,
594
+ });
595
+ if (turnsExtension) {
596
+ this.bus.emitNow({
597
+ type: 'recovery-fired',
598
+ runId,
599
+ turn: i,
600
+ strategy: 'max-turns-extension',
601
+ feedback: `Granted +${turnsExtension.extra} extra turns (cap ${maxTurns} → ${turnsExtension.extendedMax}); progress at turn ${runState.lastProgressTurn}.`,
602
+ });
603
+ maxTurns = turnsExtension.extendedMax;
604
+ extensionGranted = true;
696
605
  }
697
606
  // Honor user-driven pause or abort from the interrupt controller.
698
607
  if (this.beforeTurn) {
@@ -810,6 +719,7 @@ export class BrowserAgent {
810
719
  }, scenario.signal);
811
720
  this.cachedPostState = undefined;
812
721
  const observeDurationMs = Date.now() - observeStartedAt;
722
+ phaseTimings.totalObserveMs = (phaseTimings.totalObserveMs ?? 0) + observeDurationMs;
813
723
  if (phaseTimings.firstObserveMs === undefined) {
814
724
  phaseTimings.firstObserveMs = observeDurationMs;
815
725
  this.onPhaseTiming?.('observe', phaseTimings.firstObserveMs);
@@ -1294,6 +1204,15 @@ export class BrowserAgent {
1294
1204
  }
1295
1205
  }
1296
1206
  const decideDurationMs = Date.now() - decideStartedAt;
1207
+ phaseTimings.totalDecideMs = (phaseTimings.totalDecideMs ?? 0) + decideDurationMs;
1208
+ // A pattern-match or cache-hit synthesised the decision with no LLM call;
1209
+ // anything else round-tripped the model. Tracks the lazy-decision hit rate.
1210
+ if (patternMatch || cached) {
1211
+ phaseTimings.decideSkips = (phaseTimings.decideSkips ?? 0) + 1;
1212
+ }
1213
+ else {
1214
+ phaseTimings.decideLlmCalls = (phaseTimings.decideLlmCalls ?? 0) + 1;
1215
+ }
1297
1216
  if (phaseTimings.firstDecideMs === undefined) {
1298
1217
  phaseTimings.firstDecideMs = decideDurationMs;
1299
1218
  this.onPhaseTiming?.('decide', phaseTimings.firstDecideMs);
@@ -1887,8 +1806,10 @@ export class BrowserAgent {
1887
1806
  }, scenario.signal);
1888
1807
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Execute wall-clock timeout after ${executeWallClockMs}ms`)), executeWallClockMs));
1889
1808
  execResult = await Promise.race([executePromise, timeoutPromise]);
1809
+ const executeDurationMs = Date.now() - executeStartedAt;
1810
+ phaseTimings.totalExecuteMs = (phaseTimings.totalExecuteMs ?? 0) + executeDurationMs;
1890
1811
  if (phaseTimings.firstExecuteMs === undefined) {
1891
- phaseTimings.firstExecuteMs = Date.now() - executeStartedAt;
1812
+ phaseTimings.firstExecuteMs = executeDurationMs;
1892
1813
  this.onPhaseTiming?.('execute', phaseTimings.firstExecuteMs);
1893
1814
  }
1894
1815
  this.bus.emitNow({
@@ -2156,27 +2077,37 @@ export class BrowserAgent {
2156
2077
  });
2157
2078
  }
2158
2079
  selectFollowUpActions(primaryAction, nextActions) {
2159
- const microPlanConfig = this.config.microPlan;
2160
- if (microPlanConfig?.enabled !== true || !Array.isArray(nextActions) || nextActions.length === 0) {
2161
- return [];
2162
- }
2163
- // Never chain follow-up actions behind terminal/meta actions.
2164
- if (!SAFE_MICRO_ACTIONS.has(primaryAction.action)) {
2165
- return [];
2166
- }
2167
- const limit = Math.max(1, Math.min(4, microPlanConfig.maxActionsPerTurn ?? DEFAULT_MICRO_PLAN_ACTIONS));
2168
- const remainingSlots = Math.max(0, limit - 1);
2169
- if (remainingSlots === 0)
2170
- return [];
2171
- const selected = [];
2172
- for (const action of nextActions) {
2173
- if (!SAFE_MICRO_ACTIONS.has(action.action))
2174
- continue;
2175
- selected.push(action);
2176
- if (selected.length >= remainingSlots)
2080
+ return selectMicroPlanFollowUps(primaryAction, nextActions, this.config.microPlan);
2081
+ }
2082
+ /**
2083
+ * Surface replay lifecycle events. The structured TurnEventBus union is closed
2084
+ * and replay is opt-in, so rather than widen it we log under debug — the
2085
+ * self-heal decision is never silent (the controller also logs + returns a
2086
+ * reason, and the runner logs the fall-through). `runId` is accepted for
2087
+ * symmetry with bus emits and future wiring.
2088
+ */
2089
+ handleReplayEvent(_runId, event) {
2090
+ if (!this.config.debug)
2091
+ return;
2092
+ switch (event.type) {
2093
+ case 'replay-started':
2094
+ console.log(`[Replay] start: ${event.totalSteps} step(s), goalSimilarity=${event.goalSimilarity.toFixed(2)}`);
2095
+ break;
2096
+ case 'replay-guard-abort':
2097
+ console.log(`[Replay] guard abort at step ${event.index + 1}: ${event.reason}`);
2098
+ break;
2099
+ case 'replay-effect-failed':
2100
+ console.log(`[Replay] effect failed at step ${event.index + 1}: ${event.reason}`);
2101
+ break;
2102
+ case 'replay-healed':
2103
+ console.log(`[Replay] healed ${event.completedSteps}/${event.totalSteps}: ${event.reason}`);
2104
+ break;
2105
+ case 'replay-completed':
2106
+ console.log(`[Replay] completed ${event.completedSteps} step(s), achieved=${event.achieved}`);
2107
+ break;
2108
+ default:
2177
2109
  break;
2178
2110
  }
2179
- return selected;
2180
2111
  }
2181
2112
  /** Persist knowledge, selector cache, and session history to disk */
2182
2113
  saveMemory(scenario, result, turns) {
@@ -2243,659 +2174,32 @@ export class BrowserAgent {
2243
2174
  * unified timeline. The Turn's `reasoning` field carries the plan
2244
2175
  * step's rationale, and `verified` carries the verification result.
2245
2176
  */
2246
- async executePlan(plan, scenario, runId, turns, runState, startingTurnIndex,
2247
- /**
2248
- * Token usage from the Brain.plan() LLM call that produced this plan.
2249
- * The plan call's tokens are NOT attached to any per-step turn — there's
2250
- * one plan call per N steps. To make the run-level cost tally honest,
2251
- * we attribute the plan call to the FIRST step's Turn artifact so the
2252
- * downstream sum (in baseline-summary.json / report.json) reflects the
2253
- * real LLM spend.
2254
- */
2255
- planCallTokens) {
2256
- let currentTurnIndex = startingTurnIndex;
2257
- let lastState = turns[turns.length - 1]?.state
2258
- ?? { url: '', title: '', snapshot: '' };
2259
- // Track the last successful runScript output so placeholder complete
2260
- // results can be substituted with the real script output.
2261
- let lastRunScriptOutput = null;
2262
- // Track extractWithIndex matches for per-action fallback; the LLM must
2263
- // read the list and pick by index.
2264
- let lastExtractOutput = null;
2265
- for (let stepIdx = 0; stepIdx < plan.steps.length; stepIdx++) {
2266
- if (scenario.signal?.aborted) {
2267
- return {
2268
- kind: 'deviated',
2269
- lastState,
2270
- failedStepIndex: stepIdx,
2271
- reason: scenario.signal.reason || 'Cancelled',
2272
- turnsConsumed: stepIdx,
2273
- };
2274
- }
2275
- const step = plan.steps[stepIdx];
2276
- const stepStartedAt = Date.now();
2277
- const turnNumber = currentTurnIndex + 1;
2278
- currentTurnIndex++;
2279
- // Refresh the snapshot before EVERY step. The plan was built from a
2280
- // single observe() call at turn 1; later steps may target a different
2281
- // page entirely (after navigate / click "Next"). The first observe
2282
- // here is also what verify-against will eventually consume.
2283
- const preStepState = await this.driver.observe().catch(() => lastState);
2284
- lastState = preStepState;
2285
- // Wrap each plan step in a Turn artifact so post-run analysis (the
2286
- // viewer, the events.jsonl persistence, the metrics) sees a unified
2287
- // timeline regardless of whether the runner used the planner or the
2288
- // per-action loop.
2289
- //
2290
- // Token attribution: the first step carries the Brain.plan() LLM call's
2291
- // token usage so run-level cost includes planning.
2292
- const isFirstStep = stepIdx === 0;
2293
- const turn = {
2294
- turn: turnNumber,
2295
- state: preStepState,
2296
- action: step.action,
2297
- reasoning: step.rationale ?? `Plan step ${stepIdx + 1}/${plan.steps.length}`,
2298
- expectedEffect: step.expectedEffect,
2299
- plan: plan.steps.map((s) => s.rationale ?? s.action.action),
2300
- currentStep: stepIdx,
2301
- durationMs: 0,
2302
- ...(isFirstStep && planCallTokens?.tokensUsed !== undefined ? { tokensUsed: planCallTokens.tokensUsed } : {}),
2303
- ...(isFirstStep && planCallTokens?.inputTokens !== undefined ? { inputTokens: planCallTokens.inputTokens } : {}),
2304
- ...(isFirstStep && planCallTokens?.outputTokens !== undefined ? { outputTokens: planCallTokens.outputTokens } : {}),
2305
- ...(isFirstStep && planCallTokens?.cacheReadInputTokens !== undefined ? { cacheReadInputTokens: planCallTokens.cacheReadInputTokens } : {}),
2306
- ...(isFirstStep && planCallTokens?.cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens: planCallTokens.cacheCreationInputTokens } : {}),
2307
- };
2308
- // Terminal actions: complete and abort don't go through driver.execute
2309
- // — the runner handles them as the end of the plan.
2310
- if (step.action.action === 'complete') {
2311
- // Substitute placeholder complete results with prior runScript output.
2312
- let resolvedResult = step.action.result;
2313
- if (lastRunScriptOutput
2314
- && typeof resolvedResult === 'string'
2315
- && hasPlaceholderPattern(resolvedResult)) {
2316
- if (this.config.debug) {
2317
- console.log(`[Runner] Substituting placeholder complete.result with runScript output (${lastRunScriptOutput.length} chars)`);
2318
- }
2319
- resolvedResult = lastRunScriptOutput;
2320
- turn.reasoning = `${turn.reasoning ?? ''} [substituted runScript output]`.trim();
2321
- }
2322
- turn.durationMs = Date.now() - stepStartedAt;
2323
- turns.push(turn);
2324
- this.onTurn?.(turn);
2325
- this.bus.emitNow({
2326
- type: 'plan-step-executed',
2327
- runId,
2328
- turn: turnNumber,
2329
- stepIndex: stepIdx + 1,
2330
- totalSteps: plan.steps.length,
2331
- action: step.action,
2332
- executeSuccess: true,
2333
- verified: true,
2334
- durationMs: turn.durationMs,
2335
- });
2336
- return {
2337
- kind: 'completed',
2338
- lastState,
2339
- finalResult: resolvedResult,
2340
- turnsConsumed: stepIdx + 1,
2341
- };
2342
- }
2343
- if (step.action.action === 'abort') {
2344
- turn.durationMs = Date.now() - stepStartedAt;
2345
- turns.push(turn);
2346
- this.onTurn?.(turn);
2347
- this.bus.emitNow({
2348
- type: 'plan-deviated',
2349
- runId,
2350
- turn: turnNumber,
2351
- stepIndex: stepIdx + 1,
2352
- totalSteps: plan.steps.length,
2353
- reason: `plan aborted: ${step.action.reason}`,
2354
- });
2355
- return {
2356
- kind: 'deviated',
2357
- lastState,
2358
- failedStepIndex: stepIdx,
2359
- reason: `plan aborted: ${step.action.reason}`,
2360
- turnsConsumed: stepIdx + 1,
2361
- };
2362
- }
2363
- // Execute the action via the existing driver path. This emits
2364
- // execute-started / execute-completed events on the bus exactly
2365
- // like the per-action loop does.
2366
- //
2367
- // Cap each plan step at 10s so missing selectors fail quickly and hand
2368
- // control back to per-action mode.
2369
- this.bus.emitNow({ type: 'execute-started', runId, turn: turnNumber, action: step.action });
2370
- const execStartedAt = Date.now();
2371
- const planStepTimeoutMs = 10_000;
2372
- let execResult;
2373
- try {
2374
- execResult = await Promise.race([
2375
- this.driver.execute(step.action),
2376
- new Promise((resolve) => setTimeout(() => resolve({ success: false, error: `plan step wall-clock timeout after ${planStepTimeoutMs}ms` }), planStepTimeoutMs)),
2377
- ]);
2378
- }
2379
- catch (err) {
2380
- const message = err instanceof Error ? err.message : String(err);
2381
- execResult = { success: false, error: message };
2382
- }
2383
- const execDurationMs = Date.now() - execStartedAt;
2384
- this.bus.emitNow({
2385
- type: 'execute-completed',
2386
- runId,
2387
- turn: turnNumber,
2388
- action: step.action,
2389
- success: execResult.success,
2390
- ...(execResult.error ? { error: execResult.error } : {}),
2391
- ...(execResult.bounds ? { bounds: execResult.bounds } : {}),
2392
- durationMs: execDurationMs,
2393
- });
2394
- if (!execResult.success) {
2395
- turn.error = execResult.error;
2396
- turn.durationMs = Date.now() - stepStartedAt;
2397
- turn.verified = false;
2398
- turn.verificationFailure = `execute failed: ${execResult.error}`;
2399
- turns.push(turn);
2400
- this.onTurn?.(turn);
2401
- runState.recordError();
2402
- this.bus.emitNow({
2403
- type: 'plan-step-executed',
2404
- runId,
2405
- turn: turnNumber,
2406
- stepIndex: stepIdx + 1,
2407
- totalSteps: plan.steps.length,
2408
- action: step.action,
2409
- executeSuccess: false,
2410
- verified: false,
2411
- durationMs: Date.now() - stepStartedAt,
2412
- ...(execResult.error ? { verifyReason: execResult.error } : {}),
2413
- });
2414
- this.bus.emitNow({
2415
- type: 'plan-deviated',
2416
- runId,
2417
- turn: turnNumber,
2418
- stepIndex: stepIdx + 1,
2419
- totalSteps: plan.steps.length,
2420
- reason: `execute failed: ${execResult.error}`,
2421
- });
2422
- return {
2423
- kind: 'deviated',
2424
- lastState,
2425
- failedStepIndex: stepIdx,
2426
- reason: `execute failed at step ${stepIdx + 1}: ${execResult.error}`,
2427
- turnsConsumed: stepIdx + 1,
2428
- };
2429
- }
2430
- runState.clearConsecutiveErrors();
2431
- if (execResult.bounds)
2432
- turn.actionBounds = execResult.bounds;
2433
- // Capture runScript output for placeholder substitution and evidence.
2434
- if (step.action.action === 'runScript' && typeof execResult.data === 'string' && execResult.data.length > 0) {
2435
- lastRunScriptOutput = execResult.data;
2436
- if (execResult.data.length > 10) {
2437
- runState.recordEvidence(`EXTRACTED (turn ${currentTurnIndex}): ${execResult.data.slice(0, 500)}`);
2438
- }
2439
- }
2440
- // Capture extractWithIndex output for per-action fallback.
2441
- if (step.action.action === 'extractWithIndex' && typeof execResult.data === 'string' && execResult.data.length > 0) {
2442
- lastExtractOutput = execResult.data;
2443
- // Also push as goal verification evidence so the verifier sees what
2444
- // the agent extracted.
2445
- runState.firstSufficientEvidenceTurn ??= currentTurnIndex;
2446
- pushGoalVerificationEvidence(runState.goalVerificationEvidence, `EXTRACT RESULT:\n${execResult.data}`);
2447
- if (execResult.data.length > 10) {
2448
- runState.recordEvidence(`EXTRACTED (turn ${currentTurnIndex}): ${execResult.data.slice(0, 500)}`);
2449
- }
2450
- }
2451
- // Verify the post-condition. We re-observe to get the post-action
2452
- // state, then run the same verifyExpectedEffect helper the per-action
2453
- // loop uses. The fresh observe is also stashed in cachedPostState so
2454
- // the next step's pre-step observe can reuse it.
2455
- this.bus.emitNow({
2456
- type: 'verify-started',
2457
- runId,
2458
- turn: turnNumber,
2459
- expectedEffect: step.expectedEffect,
2460
- });
2461
- const verifyStartedAt = Date.now();
2462
- // Auto-pass list — these actions either don't observably mutate
2463
- // the page state OR they're self-verifying (the underlying Playwright
2464
- // call throws on real failure, so a successful return means the
2465
- // mutation actually happened). Strict expectedEffect verification
2466
- // would generate false negatives on the per-action loop fallback for
2467
- // these. Plan execution trusts the execute result.
2468
- //
2469
- // - wait / scroll / hover: don't mutate observable snapshot state
2470
- // - runScript / evaluate / verifyPreview: meta actions
2471
- // - fill / clickSequence: self-verifying (Playwright throws on miss),
2472
- // AND input values don't always reflect in the ARIA snapshot, so
2473
- // the permissive "did state change?" check would also miss them
2474
- const isAutoPass = step.action.action === 'wait'
2475
- || step.action.action === 'scroll'
2476
- || step.action.action === 'hover'
2477
- || step.action.action === 'runScript'
2478
- || step.action.action === 'extractWithIndex'
2479
- || step.action.action === 'evaluate'
2480
- || step.action.action === 'verifyPreview'
2481
- || step.action.action === 'fill'
2482
- || step.action.action === 'clickSequence';
2483
- // Settle wait for mutating actions, mirroring verifyEffect's logic
2484
- const needsSettleWait = step.action.action === 'click'
2485
- || step.action.action === 'navigate'
2486
- || step.action.action === 'press'
2487
- || step.action.action === 'select'
2488
- || step.action.action === 'fill'
2489
- || step.action.action === 'clickSequence';
2490
- const observePromise = this.driver.observe().catch(() => preStepState);
2491
- if (needsSettleWait) {
2492
- await Promise.all([
2493
- observePromise,
2494
- new Promise((r) => setTimeout(r, 50)),
2495
- ]);
2496
- }
2497
- const postStepState = await observePromise;
2498
- this.cachedPostState = postStepState;
2499
- lastState = postStepState;
2500
- // Plan verification is more permissive than per-action verification:
2501
- // a step passes if (a) it's a non-mutating action, OR (b) the strict
2502
- // verifier passes, OR (c) the snapshot/url changed in any meaningful
2503
- // way (the action did SOMETHING). Strict failure-on-no-change is
2504
- // appropriate for the per-action loop where the agent can recover,
2505
- // but plan execution needs to push forward unless there's positive
2506
- // evidence of failure.
2507
- let verifyResult;
2508
- if (isAutoPass) {
2509
- verifyResult = { verified: true };
2510
- }
2511
- else {
2512
- const strictResult = verifyExpectedEffect({
2513
- expectedEffect: step.expectedEffect,
2514
- preActionState: preStepState,
2515
- postActionState: postStepState,
2516
- });
2517
- if (strictResult.verified) {
2518
- verifyResult = strictResult;
2519
- }
2520
- else {
2521
- // Permissive fallback: did the page change at all?
2522
- const stateChanged = preStepState.url !== postStepState.url
2523
- || preStepState.title !== postStepState.title
2524
- || preStepState.snapshot !== postStepState.snapshot;
2525
- if (stateChanged) {
2526
- verifyResult = { verified: true };
2527
- }
2528
- else {
2529
- verifyResult = strictResult;
2530
- }
2531
- }
2532
- }
2533
- turn.verified = verifyResult.verified;
2534
- if (!verifyResult.verified) {
2535
- turn.verificationFailure = verifyResult.reason;
2536
- }
2537
- turn.durationMs = Date.now() - stepStartedAt;
2538
- turns.push(turn);
2539
- this.onTurn?.(turn);
2540
- this.bus.emitNow({
2541
- type: 'verify-completed',
2542
- runId,
2543
- turn: turnNumber,
2544
- verified: verifyResult.verified,
2545
- ...(verifyResult.reason ? { reason: verifyResult.reason } : {}),
2546
- durationMs: Date.now() - verifyStartedAt,
2547
- });
2548
- this.bus.emitNow({
2549
- type: 'plan-step-executed',
2550
- runId,
2551
- turn: turnNumber,
2552
- stepIndex: stepIdx + 1,
2553
- totalSteps: plan.steps.length,
2554
- action: step.action,
2555
- executeSuccess: true,
2556
- verified: verifyResult.verified,
2557
- durationMs: Date.now() - stepStartedAt,
2558
- ...(verifyResult.reason ? { verifyReason: verifyResult.reason } : {}),
2559
- });
2560
- if (!verifyResult.verified) {
2561
- this.bus.emitNow({
2562
- type: 'plan-deviated',
2563
- runId,
2564
- turn: turnNumber,
2565
- stepIndex: stepIdx + 1,
2566
- totalSteps: plan.steps.length,
2567
- reason: `verification failed at step ${stepIdx + 1}: ${verifyResult.reason ?? 'expected effect not observed'}`,
2568
- });
2569
- return {
2570
- kind: 'deviated',
2571
- lastState,
2572
- failedStepIndex: stepIdx,
2573
- reason: `verification failed at step ${stepIdx + 1}: ${verifyResult.reason ?? 'expected effect not observed'}`,
2574
- turnsConsumed: stepIdx + 1,
2575
- };
2576
- }
2577
- }
2578
- // Auto-complete when the plan ends with meaningful runScript output.
2579
- const lastStep = plan.steps[plan.steps.length - 1];
2580
- if (lastStep
2581
- && lastStep.action.action === 'runScript'
2582
- && isMeaningfulRunScriptOutput(lastRunScriptOutput)) {
2583
- const synthTurnNumber = currentTurnIndex + 1;
2584
- const synthTurn = {
2585
- turn: synthTurnNumber,
2586
- state: lastState,
2587
- action: { action: 'complete', result: lastRunScriptOutput },
2588
- reasoning: 'Auto-complete: plan ended after runScript, runner emitted complete with the runScript output',
2589
- durationMs: 0,
2590
- };
2591
- turns.push(synthTurn);
2592
- this.onTurn?.(synthTurn);
2593
- this.bus.emitNow({
2594
- type: 'plan-step-executed',
2595
- runId,
2596
- turn: synthTurnNumber,
2597
- stepIndex: plan.steps.length + 1,
2598
- totalSteps: plan.steps.length + 1,
2599
- action: synthTurn.action,
2600
- executeSuccess: true,
2601
- verified: true,
2602
- durationMs: 0,
2603
- });
2604
- if (this.config.debug) {
2605
- console.log(`[Runner] Auto-emitted complete with runScript output (${lastRunScriptOutput.length} chars) after plan exhausted`);
2606
- }
2607
- return {
2608
- kind: 'completed',
2609
- lastState,
2610
- finalResult: lastRunScriptOutput,
2611
- turnsConsumed: plan.steps.length + 1,
2612
- };
2613
- }
2614
- // If the plan produced extractWithIndex matches, fall through with the
2615
- // match list so the LLM can choose the correct index.
2616
- if (lastExtractOutput) {
2617
- return {
2618
- kind: 'deviated',
2619
- lastState,
2620
- failedStepIndex: plan.steps.length,
2621
- reason: `plan completed extractWithIndex but the LLM must read the matches and pick by index. Match list:\n${lastExtractOutput.slice(0, 4000)}\n\nPick the index whose text matches the goal, then emit complete with result: <picked text>`,
2622
- turnsConsumed: plan.steps.length,
2623
- };
2624
- }
2625
- // If the plan ends with runScript but output is empty or placeholder-like,
2626
- // fall through to per-action mode instead of completing with bad data.
2627
- if (lastStep
2628
- && lastStep.action.action === 'runScript'
2629
- && !isMeaningfulRunScriptOutput(lastRunScriptOutput)) {
2630
- if (this.config.debug) {
2631
- console.log(`[Runner] runScript returned no meaningful output (${JSON.stringify(lastRunScriptOutput).slice(0, 100)}); falling through to per-action loop for two-pass extraction`);
2632
- }
2633
- return {
2634
- kind: 'deviated',
2635
- lastState,
2636
- failedStepIndex: plan.steps.length - 1,
2637
- reason: `runScript returned no meaningful output (got: ${JSON.stringify(lastRunScriptOutput).slice(0, 200)}). The first-pass extraction failed — re-observe the page and try extractWithIndex with a wide query (e.g. 'p, span, dd, code') and a contains filter naming the expected text fragment. Pick-by-content beats pick-by-selector when the planner couldn't see the data at plan time.`,
2638
- turnsConsumed: plan.steps.length,
2639
- };
2640
- }
2641
- // All steps verified BUT the plan ended without an explicit complete/abort.
2642
- // This means the planner emitted a finite sequence of "work" steps and
2643
- // didn't terminate. The right behavior is NOT to fabricate a complete —
2644
- // we treat plan exhaustion as a deviation that triggers fallback to the
2645
- // per-action loop. The per-action loop will continue from the current
2646
- // state and emit `complete` when the goal is genuinely met.
2647
- return {
2648
- kind: 'deviated',
2649
- lastState,
2650
- failedStepIndex: plan.steps.length,
2651
- reason: 'plan exhausted without an explicit complete or abort step — falling through to per-action loop to finish the task',
2652
- turnsConsumed: plan.steps.length,
2653
- };
2177
+ async executePlan(plan, scenario, runId, turns, runState, startingTurnIndex, planCallTokens) {
2178
+ return executePlanImpl(this, plan, scenario, runId, turns, runState, startingTurnIndex, planCallTokens);
2654
2179
  }
2655
2180
  async verifyEffect(expectedEffect, preActionState, actionType) {
2656
- // Only pause for actions that mutate the page in flight (navigation,
2657
- // clicks that may trigger XHR/route transitions, form submits). For
2658
- // pure reads, scrolls, hovers, and waits the page state is already
2659
- // settled by the time execute returns. The previous unconditional
2660
- // 100ms wait was pure dead time on every turn.
2661
- const needsSettleWait = actionType === 'click'
2662
- || actionType === 'navigate'
2663
- || actionType === 'press'
2664
- || actionType === 'select';
2665
- // Kick observe off immediately and let the settle wait race against it.
2666
- // observe() polls waitForLoadState internally, so the 50ms settle is
2667
- // really only there to let click handlers schedule their first XHR; we
2668
- // don't need to *block* on it before starting observe.
2669
- const observePromise = this.driver.observe().catch(() => preActionState);
2670
- if (needsSettleWait) {
2671
- await Promise.all([
2672
- observePromise,
2673
- new Promise(r => setTimeout(r, 50)),
2674
- ]);
2675
- }
2676
- const postState = await observePromise;
2677
- this.cachedPostState = postState;
2678
- return verifyExpectedEffect({
2679
- expectedEffect,
2680
- preActionState,
2681
- postActionState: postState,
2682
- });
2181
+ return verifyEffectImpl(this, expectedEffect, preActionState, actionType);
2683
2182
  }
2684
2183
  async buildSearchResultsScoutFeedback(state, goal, allowedDomains, seenUrls) {
2685
- if (!buildSearchResultsGuidance(state, goal, allowedDomains))
2686
- return '';
2687
- if (seenUrls.has(state.url))
2688
- return '';
2689
- const page = this.driver.getPage?.();
2690
- if (!page)
2691
- return '';
2692
- try {
2693
- const candidates = await page.evaluate(() => {
2694
- const items = [];
2695
- const seen = new Set();
2696
- const selectors = [
2697
- 'main a[href]',
2698
- '[role="main"] a[href]',
2699
- 'article a[href]',
2700
- '.search-results a[href]',
2701
- '#search-results a[href]',
2702
- '.results a[href]',
2703
- 'ol a[href]',
2704
- ];
2705
- const fallbackLinks = Array.from(document.querySelectorAll('a[href]'));
2706
- const links = selectors
2707
- .flatMap((selector) => Array.from(document.querySelectorAll(selector)))
2708
- .concat(fallbackLinks);
2709
- const noisePattern = /\b(next|previous|page \d+|show more|show fewer|filter|sort|home|contact|privacy|accessibility|search)\b/i;
2710
- for (const node of links) {
2711
- const anchor = node;
2712
- const href = anchor.href?.trim();
2713
- const title = (anchor.innerText || anchor.textContent || '').replace(/\s+/g, ' ').trim();
2714
- if (!href || !title || title.length < 12 || title.length > 220)
2715
- continue;
2716
- if (href.startsWith('javascript:') || href.startsWith('mailto:') || href.includes('#'))
2717
- continue;
2718
- if (href.includes('results.aspx') && !/open government|dataset|data|news release|press release/i.test(title))
2719
- continue;
2720
- if (noisePattern.test(title) && !/open government|dataset|data|news release|press release/i.test(title))
2721
- continue;
2722
- const key = `${title}::${href}`;
2723
- if (seen.has(key))
2724
- continue;
2725
- seen.add(key);
2726
- items.push({ title, href });
2727
- if (items.length >= 24)
2728
- break;
2729
- }
2730
- return items;
2731
- });
2732
- if (!Array.isArray(candidates) || candidates.length === 0)
2733
- return '';
2734
- const ranked = rankSearchCandidates(goal, candidates, allowedDomains);
2735
- const recommendation = ranked[0];
2736
- seenUrls.add(state.url);
2737
- return [
2738
- 'SEARCH RESULTS CANDIDATES:',
2739
- ...ranked.map((candidate, index) => `${index + 1}. ${candidate.title} — ${candidate.href} (score ${candidate.score})`),
2740
- recommendation
2741
- ? `BEST MATCH RECOMMENDATION: prefer "${recommendation.title}" because it best matches the requested entity, content type, and host constraints.`
2742
- : '',
2743
- ].join('\n');
2744
- }
2745
- catch {
2746
- return '';
2747
- }
2184
+ return buildSearchResultsScoutFeedbackImpl(this, state, goal, allowedDomains, seenUrls);
2748
2185
  }
2749
2186
  async buildVisibleLinkScoutRecommendation(state, goal, allowedDomains) {
2750
- const scoutConfig = this.config.scout;
2751
- if (!scoutConfig?.enabled)
2752
- return undefined;
2753
- if (!shouldUseVisibleLinkScoutPage(state, goal, allowedDomains))
2754
- return undefined;
2755
- const ranked = getRankedVisibleLinkCandidates(state, goal, allowedDomains);
2756
- const maxCandidates = Math.max(2, Math.min(scoutConfig.maxCandidates ?? 3, 5));
2757
- const candidates = await this.filterScoutCandidatesByAllowedDomains(ranked.slice(0, maxCandidates), allowedDomains);
2758
- if (!shouldUseVisibleLinkScout(candidates, scoutConfig))
2759
- return undefined;
2760
- const scoutState = scoutConfig.useVision
2761
- ? await this.attachDecisionScreenshot(state)
2762
- : state;
2763
- const extraContext = allowedDomains && allowedDomains.length > 0
2764
- ? `Host constraint: prefer only ${allowedDomains.join(', ')}.`
2765
- : undefined;
2766
- const recommendation = await this.brain.recommendLinkCandidate(goal, scoutState, candidates, extraContext);
2767
- const matched = candidates.find((candidate) => candidate.ref === recommendation.selector);
2768
- if (!matched)
2769
- return undefined;
2770
- return {
2771
- ref: matched.ref,
2772
- text: matched.text,
2773
- confidence: recommendation.confidence,
2774
- reasoning: recommendation.reasoning,
2775
- };
2187
+ return buildVisibleLinkScoutRecommendationImpl(this, state, goal, allowedDomains);
2776
2188
  }
2777
2189
  async buildBranchLinkRecommendation(state, goal, allowedDomains) {
2778
- const scoutConfig = this.config.scout;
2779
- if (!scoutConfig?.enabled || !scoutConfig.readOnlyTop2Challenger)
2780
- return undefined;
2781
- if (!shouldUseVisibleLinkScoutPage(state, goal, allowedDomains))
2782
- return undefined;
2783
- const ranked = getRankedVisibleLinkCandidates(state, goal, allowedDomains);
2784
- const candidates = ranked.slice(0, 2);
2785
- if (!shouldUseBoundedBranchExplorer(candidates, scoutConfig))
2786
- return undefined;
2787
- if (!this.driver.inspectSelectorHref || !this.driver.getPage)
2788
- return undefined;
2789
- const page = this.driver.getPage();
2790
- if (!page)
2791
- return undefined;
2792
- const settled = await Promise.all(candidates.map(async (candidate) => {
2793
- const href = await this.driver.inspectSelectorHref(candidate.ref).catch(() => undefined);
2794
- if (!href)
2795
- return undefined;
2796
- const preview = await inspectBranchPreview(page, href, 8000);
2797
- if (!preview)
2798
- return undefined;
2799
- return { ref: candidate.ref, text: candidate.text, href, score: scoreBranchPreview(goal, preview, allowedDomains), preview };
2800
- }));
2801
- const previews = settled.filter((r) => r !== undefined);
2802
- if (previews.length < 2)
2803
- return undefined;
2804
- previews.sort((a, b) => b.score - a.score);
2805
- const [top, second] = previews;
2806
- if (top.score <= second.score)
2807
- return undefined;
2808
- if (top.score < 4)
2809
- return undefined;
2810
- return {
2811
- ref: top.ref,
2812
- text: top.text,
2813
- confidence: Math.min(0.95, 0.7 + Math.max(0, top.score - second.score) / 20),
2814
- reasoning: `Branch preview favored ${top.href} (${top.preview.title || top.preview.finalUrl}) over ${second.href} based on content-type and goal-match signals.`,
2815
- };
2190
+ return buildBranchLinkRecommendationImpl(this, state, goal, allowedDomains);
2816
2191
  }
2817
2192
  async filterScoutCandidatesByAllowedDomains(candidates, allowedDomains) {
2818
- if (!allowedDomains || allowedDomains.length === 0 || !this.driver.inspectSelectorHref) {
2819
- return candidates;
2820
- }
2821
- const allowedHosts = new Set(allowedDomains.map((domain) => domain.toLowerCase()));
2822
- const resolved = await Promise.all(candidates.map(async (candidate) => {
2823
- const href = await this.driver.inspectSelectorHref(candidate.ref).catch(() => undefined);
2824
- const host = href ? safeHostname(href) : undefined;
2825
- return { candidate, host };
2826
- }));
2827
- return resolved
2828
- .filter(({ host }) => !host || allowedHosts.has(host))
2829
- .map(({ candidate }) => candidate);
2193
+ return filterScoutCandidatesByAllowedDomainsImpl(this, candidates, allowedDomains);
2830
2194
  }
2831
2195
  async inspectDisallowedSearchClick(state, scenario, action) {
2832
- if (action.action !== 'click')
2833
- return undefined;
2834
- if (!scenario.allowedDomains || scenario.allowedDomains.length === 0)
2835
- return undefined;
2836
- if (!buildSearchResultsGuidance(state, scenario.goal, scenario.allowedDomains))
2837
- return undefined;
2838
- if (!this.driver.inspectSelectorHref)
2839
- return undefined;
2840
- const href = await this.driver.inspectSelectorHref(action.selector);
2841
- const host = href ? safeHostname(href) : undefined;
2842
- if (!href || !host)
2843
- return undefined;
2844
- const allowedLower = scenario.allowedDomains.map((domain) => domain.toLowerCase());
2845
- if (allowedLower.includes(host))
2846
- return undefined;
2847
- // First-party subdomain tolerance
2848
- const toRoot = (h) => {
2849
- const parts = h.split('.').filter(Boolean);
2850
- return parts.length <= 2 ? h : parts.slice(-2).join('.');
2851
- };
2852
- if (allowedLower.some((h) => toRoot(h) === toRoot(host)))
2853
- return undefined;
2854
- return [
2855
- `Blocked action: selector ${action.selector} resolves to ${href}, which is outside the allowed host set: ${scenario.allowedDomains.join(', ')}.`,
2856
- 'Choose a result from an allowed host instead, even if the snippet text looks relevant.',
2857
- ].join(' ');
2196
+ return inspectDisallowedSearchClickImpl(this, state, scenario, action);
2858
2197
  }
2859
2198
  async enforceAllowedDomainBoundary(preActionState, scenario) {
2860
- if (!scenario.allowedDomains || scenario.allowedDomains.length === 0)
2861
- return undefined;
2862
- const currentUrl = this.driver.getUrl?.() ?? preActionState.url;
2863
- const currentHost = safeHostname(currentUrl);
2864
- if (!currentHost)
2865
- return undefined;
2866
- const allowedHosts = scenario.allowedDomains.map((domain) => domain.toLowerCase());
2867
- if (allowedHosts.includes(currentHost))
2868
- return undefined;
2869
- // First-party subdomain tolerance: allow navigation within the same registrable domain
2870
- const toRoot = (h) => {
2871
- const parts = h.split('.').filter(Boolean);
2872
- return parts.length <= 2 ? h : parts.slice(-2).join('.');
2873
- };
2874
- const currentRoot = toRoot(currentHost);
2875
- if (allowedHosts.some((h) => toRoot(h) === currentRoot))
2876
- return undefined;
2877
- const previousHost = safeHostname(preActionState.url);
2878
- if (previousHost && allowedHosts.includes(previousHost)) {
2879
- await this.driver.execute({ action: 'navigate', url: preActionState.url }).catch(() => { });
2880
- }
2881
- else if (scenario.startUrl) {
2882
- await this.driver.execute({ action: 'navigate', url: scenario.startUrl }).catch(() => { });
2883
- }
2884
- return [
2885
- `Boundary violation: landed on ${currentUrl}, but the allowed host set is ${allowedHosts.join(', ')}.`,
2886
- 'Return to an allowed host and continue from there; do not rely on disallowed subdomains even if their snippet looks relevant.',
2887
- ].join(' ');
2199
+ return enforceAllowedDomainBoundaryImpl(this, preActionState, scenario);
2888
2200
  }
2889
2201
  async attachDecisionScreenshot(state) {
2890
- if (state.screenshot || !this.driver.screenshot)
2891
- return state;
2892
- try {
2893
- const screenshot = await this.driver.screenshot();
2894
- return { ...state, screenshot: screenshot.toString('base64') };
2895
- }
2896
- catch {
2897
- return state;
2898
- }
2202
+ return attachDecisionScreenshotImpl(this, state);
2899
2203
  }
2900
2204
  }
2901
2205
  /** Convenience function */