browser-use 0.2.0 → 0.3.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 (259) hide show
  1. package/README.md +295 -686
  2. package/dist/actor/element.d.ts +19 -0
  3. package/dist/actor/element.js +46 -0
  4. package/dist/actor/index.d.ts +4 -0
  5. package/dist/actor/index.js +4 -0
  6. package/dist/actor/mouse.d.ts +19 -0
  7. package/dist/actor/mouse.js +39 -0
  8. package/dist/actor/page.d.ts +29 -0
  9. package/dist/actor/page.js +88 -0
  10. package/dist/actor/utils.d.ts +4 -0
  11. package/dist/actor/utils.js +35 -0
  12. package/dist/agent/cloud-events.d.ts +18 -0
  13. package/dist/agent/cloud-events.js +65 -2
  14. package/dist/agent/gif.d.ts +1 -0
  15. package/dist/agent/gif.js +24 -2
  16. package/dist/agent/judge.d.ts +17 -0
  17. package/dist/agent/judge.js +197 -0
  18. package/dist/agent/message-manager/service.d.ts +12 -4
  19. package/dist/agent/message-manager/service.js +205 -39
  20. package/dist/agent/message-manager/utils.js +0 -1
  21. package/dist/agent/message-manager/views.d.ts +4 -0
  22. package/dist/agent/message-manager/views.js +11 -7
  23. package/dist/agent/prompts.d.ts +24 -3
  24. package/dist/agent/prompts.js +274 -59
  25. package/dist/agent/service.d.ts +99 -41
  26. package/dist/agent/service.js +2266 -472
  27. package/dist/agent/variable-detector.d.ts +12 -0
  28. package/dist/agent/variable-detector.js +211 -0
  29. package/dist/agent/views.d.ts +237 -18
  30. package/dist/agent/views.js +446 -33
  31. package/dist/browser/cloud/cloud.d.ts +20 -0
  32. package/dist/browser/cloud/cloud.js +129 -0
  33. package/dist/browser/cloud/index.d.ts +2 -0
  34. package/dist/browser/cloud/index.js +2 -0
  35. package/dist/browser/cloud/views.d.ts +41 -0
  36. package/dist/browser/cloud/views.js +35 -0
  37. package/dist/browser/events.d.ts +345 -0
  38. package/dist/browser/events.js +566 -0
  39. package/dist/browser/extensions.js +17 -17
  40. package/dist/browser/index.d.ts +4 -0
  41. package/dist/browser/index.js +4 -0
  42. package/dist/browser/profile.d.ts +8 -2
  43. package/dist/browser/profile.js +79 -12
  44. package/dist/browser/session-manager.d.ts +85 -0
  45. package/dist/browser/session-manager.js +208 -0
  46. package/dist/browser/session.d.ts +100 -8
  47. package/dist/browser/session.js +1097 -58
  48. package/dist/browser/types.d.ts +0 -2
  49. package/dist/browser/views.d.ts +39 -0
  50. package/dist/browser/views.js +32 -0
  51. package/dist/browser/watchdogs/aboutblank-watchdog.d.ts +12 -0
  52. package/dist/browser/watchdogs/aboutblank-watchdog.js +131 -0
  53. package/dist/browser/watchdogs/base.d.ts +21 -0
  54. package/dist/browser/watchdogs/base.js +81 -0
  55. package/dist/browser/watchdogs/cdp-session-watchdog.d.ts +14 -0
  56. package/dist/browser/watchdogs/cdp-session-watchdog.js +177 -0
  57. package/dist/browser/watchdogs/crash-watchdog.d.ts +38 -0
  58. package/dist/browser/watchdogs/crash-watchdog.js +296 -0
  59. package/dist/browser/watchdogs/default-action-watchdog.d.ts +49 -0
  60. package/dist/browser/watchdogs/default-action-watchdog.js +212 -0
  61. package/dist/browser/watchdogs/dom-watchdog.d.ts +8 -0
  62. package/dist/browser/watchdogs/dom-watchdog.js +31 -0
  63. package/dist/browser/watchdogs/downloads-watchdog.d.ts +77 -0
  64. package/dist/browser/watchdogs/downloads-watchdog.js +409 -0
  65. package/dist/browser/watchdogs/har-recording-watchdog.d.ts +19 -0
  66. package/dist/browser/watchdogs/har-recording-watchdog.js +317 -0
  67. package/dist/browser/watchdogs/index.d.ts +15 -0
  68. package/dist/browser/watchdogs/index.js +15 -0
  69. package/dist/browser/watchdogs/local-browser-watchdog.d.ts +10 -0
  70. package/dist/browser/watchdogs/local-browser-watchdog.js +32 -0
  71. package/dist/browser/watchdogs/permissions-watchdog.d.ts +8 -0
  72. package/dist/browser/watchdogs/permissions-watchdog.js +73 -0
  73. package/dist/browser/watchdogs/popups-watchdog.d.ts +13 -0
  74. package/dist/browser/watchdogs/popups-watchdog.js +77 -0
  75. package/dist/browser/watchdogs/recording-watchdog.d.ts +27 -0
  76. package/dist/browser/watchdogs/recording-watchdog.js +249 -0
  77. package/dist/browser/watchdogs/screenshot-watchdog.d.ts +6 -0
  78. package/dist/browser/watchdogs/screenshot-watchdog.js +13 -0
  79. package/dist/browser/watchdogs/security-watchdog.d.ts +10 -0
  80. package/dist/browser/watchdogs/security-watchdog.js +84 -0
  81. package/dist/browser/watchdogs/storage-state-watchdog.d.ts +24 -0
  82. package/dist/browser/watchdogs/storage-state-watchdog.js +288 -0
  83. package/dist/cli.d.ts +7 -2
  84. package/dist/cli.js +182 -25
  85. package/dist/code-use/formatting.d.ts +3 -0
  86. package/dist/code-use/formatting.js +18 -0
  87. package/dist/code-use/index.d.ts +6 -0
  88. package/dist/code-use/index.js +6 -0
  89. package/dist/code-use/namespace.d.ts +5 -0
  90. package/dist/code-use/namespace.js +81 -0
  91. package/dist/code-use/notebook-export.d.ts +3 -0
  92. package/dist/code-use/notebook-export.js +56 -0
  93. package/dist/code-use/service.d.ts +24 -0
  94. package/dist/code-use/service.js +104 -0
  95. package/dist/code-use/utils.d.ts +4 -0
  96. package/dist/code-use/utils.js +98 -0
  97. package/dist/code-use/views.d.ts +108 -0
  98. package/dist/code-use/views.js +165 -0
  99. package/dist/config.d.ts +13 -0
  100. package/dist/config.js +69 -3
  101. package/dist/controller/registry/service.d.ts +10 -1
  102. package/dist/controller/registry/service.js +266 -10
  103. package/dist/controller/registry/views.d.ts +4 -1
  104. package/dist/controller/registry/views.js +25 -2
  105. package/dist/controller/service.d.ts +10 -1
  106. package/dist/controller/service.js +1807 -268
  107. package/dist/controller/views.d.ts +78 -155
  108. package/dist/controller/views.js +61 -12
  109. package/dist/dom/history-tree-processor/service.d.ts +5 -0
  110. package/dist/dom/history-tree-processor/service.js +169 -14
  111. package/dist/dom/history-tree-processor/view.d.ts +7 -1
  112. package/dist/dom/history-tree-processor/view.js +10 -1
  113. package/dist/dom/markdown-extractor.d.ts +37 -0
  114. package/dist/dom/markdown-extractor.js +345 -0
  115. package/dist/dom/service.d.ts +3 -1
  116. package/dist/dom/service.js +76 -0
  117. package/dist/dom/views.d.ts +1 -0
  118. package/dist/dom/views.js +45 -0
  119. package/dist/event-bus.d.ts +107 -7
  120. package/dist/event-bus.js +313 -10
  121. package/dist/exceptions.d.ts +0 -3
  122. package/dist/exceptions.js +0 -7
  123. package/dist/filesystem/file-system.d.ts +18 -0
  124. package/dist/filesystem/file-system.js +503 -42
  125. package/dist/index.d.ts +7 -0
  126. package/dist/index.js +6 -0
  127. package/dist/integrations/gmail/actions.d.ts +3 -3
  128. package/dist/integrations/gmail/actions.js +4 -4
  129. package/dist/llm/anthropic/chat.d.ts +18 -1
  130. package/dist/llm/anthropic/chat.js +123 -55
  131. package/dist/llm/anthropic/serializer.d.ts +2 -0
  132. package/dist/llm/anthropic/serializer.js +81 -9
  133. package/dist/llm/aws/chat-anthropic.d.ts +17 -0
  134. package/dist/llm/aws/chat-anthropic.js +126 -26
  135. package/dist/llm/aws/chat-bedrock.d.ts +28 -1
  136. package/dist/llm/aws/chat-bedrock.js +161 -34
  137. package/dist/llm/aws/serializer.d.ts +13 -1
  138. package/dist/llm/aws/serializer.js +56 -17
  139. package/dist/llm/azure/chat.d.ts +53 -2
  140. package/dist/llm/azure/chat.js +366 -54
  141. package/dist/llm/base.d.ts +2 -0
  142. package/dist/llm/browser-use/chat.d.ts +40 -0
  143. package/dist/llm/browser-use/chat.js +305 -0
  144. package/dist/llm/browser-use/index.d.ts +1 -0
  145. package/dist/llm/browser-use/index.js +1 -0
  146. package/dist/llm/cerebras/chat.d.ts +39 -0
  147. package/dist/llm/cerebras/chat.js +178 -0
  148. package/dist/llm/cerebras/index.d.ts +2 -0
  149. package/dist/llm/cerebras/index.js +2 -0
  150. package/dist/llm/cerebras/serializer.d.ts +7 -0
  151. package/dist/llm/cerebras/serializer.js +82 -0
  152. package/dist/llm/deepseek/chat.d.ts +19 -2
  153. package/dist/llm/deepseek/chat.js +138 -25
  154. package/dist/llm/google/chat.d.ts +46 -2
  155. package/dist/llm/google/chat.js +267 -64
  156. package/dist/llm/google/serializer.d.ts +9 -1
  157. package/dist/llm/google/serializer.js +141 -34
  158. package/dist/llm/groq/chat.d.ts +21 -2
  159. package/dist/llm/groq/chat.js +125 -26
  160. package/dist/llm/groq/parser.js +3 -1
  161. package/dist/llm/mistral/chat.d.ts +43 -0
  162. package/dist/llm/mistral/chat.js +154 -0
  163. package/dist/llm/mistral/index.d.ts +2 -0
  164. package/dist/llm/mistral/index.js +2 -0
  165. package/dist/llm/mistral/schema.d.ts +8 -0
  166. package/dist/llm/mistral/schema.js +27 -0
  167. package/dist/llm/models.d.ts +2 -0
  168. package/dist/llm/models.js +317 -0
  169. package/dist/llm/ollama/chat.d.ts +13 -1
  170. package/dist/llm/ollama/chat.js +110 -19
  171. package/dist/llm/ollama/serializer.d.ts +1 -0
  172. package/dist/llm/ollama/serializer.js +34 -12
  173. package/dist/llm/openai/chat.d.ts +16 -0
  174. package/dist/llm/openai/chat.js +94 -44
  175. package/dist/llm/openai/like.d.ts +5 -3
  176. package/dist/llm/openai/like.js +7 -3
  177. package/dist/llm/openai/responses-serializer.d.ts +18 -0
  178. package/dist/llm/openai/responses-serializer.js +72 -0
  179. package/dist/llm/openrouter/chat.d.ts +28 -2
  180. package/dist/llm/openrouter/chat.js +115 -29
  181. package/dist/llm/schema.d.ts +11 -1
  182. package/dist/llm/schema.js +81 -1
  183. package/dist/llm/vercel/chat.d.ts +50 -0
  184. package/dist/llm/vercel/chat.js +276 -0
  185. package/dist/llm/vercel/index.d.ts +1 -0
  186. package/dist/llm/vercel/index.js +1 -0
  187. package/dist/llm/vercel/serializer.d.ts +5 -0
  188. package/dist/llm/vercel/serializer.js +7 -0
  189. package/dist/llm/views.d.ts +2 -1
  190. package/dist/llm/views.js +3 -1
  191. package/dist/logging-config.d.ts +2 -0
  192. package/dist/logging-config.js +82 -29
  193. package/dist/mcp/client.d.ts +10 -5
  194. package/dist/mcp/client.js +14 -9
  195. package/dist/mcp/controller.d.ts +42 -3
  196. package/dist/mcp/controller.js +56 -31
  197. package/dist/mcp/server.d.ts +14 -0
  198. package/dist/mcp/server.js +255 -52
  199. package/dist/observability.js +10 -4
  200. package/dist/sandbox/index.d.ts +2 -0
  201. package/dist/sandbox/index.js +2 -0
  202. package/dist/sandbox/sandbox.d.ts +19 -0
  203. package/dist/sandbox/sandbox.js +140 -0
  204. package/dist/sandbox/views.d.ts +67 -0
  205. package/dist/sandbox/views.js +121 -0
  206. package/dist/skill-cli/index.d.ts +3 -0
  207. package/dist/skill-cli/index.js +3 -0
  208. package/dist/skill-cli/protocol.d.ts +30 -0
  209. package/dist/skill-cli/protocol.js +48 -0
  210. package/dist/skill-cli/server.d.ts +11 -0
  211. package/dist/skill-cli/server.js +85 -0
  212. package/dist/skill-cli/sessions.d.ts +24 -0
  213. package/dist/skill-cli/sessions.js +47 -0
  214. package/dist/skills/index.d.ts +3 -0
  215. package/dist/skills/index.js +3 -0
  216. package/dist/skills/service.d.ts +27 -0
  217. package/dist/skills/service.js +266 -0
  218. package/dist/skills/utils.d.ts +6 -0
  219. package/dist/skills/utils.js +53 -0
  220. package/dist/skills/views.d.ts +40 -0
  221. package/dist/skills/views.js +10 -0
  222. package/dist/sync/auth.js +8 -3
  223. package/dist/sync/service.d.ts +6 -6
  224. package/dist/sync/service.js +54 -89
  225. package/dist/telemetry/views.d.ts +20 -6
  226. package/dist/telemetry/views.js +23 -5
  227. package/dist/tokens/custom-pricing.d.ts +2 -0
  228. package/dist/tokens/custom-pricing.js +22 -0
  229. package/dist/tokens/index.d.ts +2 -0
  230. package/dist/tokens/index.js +2 -0
  231. package/dist/tokens/mappings.d.ts +1 -0
  232. package/dist/tokens/mappings.js +3 -0
  233. package/dist/tokens/service.js +27 -8
  234. package/dist/tools/extraction/index.d.ts +2 -0
  235. package/dist/tools/extraction/index.js +2 -0
  236. package/dist/tools/extraction/schema-utils.d.ts +6 -0
  237. package/dist/tools/extraction/schema-utils.js +237 -0
  238. package/dist/tools/extraction/views.d.ts +7 -0
  239. package/dist/tools/index.d.ts +5 -0
  240. package/dist/tools/index.js +5 -0
  241. package/dist/tools/registry/index.d.ts +2 -0
  242. package/dist/tools/registry/index.js +2 -0
  243. package/dist/tools/registry/service.d.ts +1 -0
  244. package/dist/tools/registry/service.js +1 -0
  245. package/dist/tools/registry/views.d.ts +1 -0
  246. package/dist/tools/registry/views.js +1 -0
  247. package/dist/tools/service.d.ts +2 -0
  248. package/dist/tools/service.js +1 -0
  249. package/dist/tools/utils.d.ts +2 -0
  250. package/dist/tools/utils.js +57 -0
  251. package/dist/tools/views.d.ts +1 -0
  252. package/dist/tools/views.js +1 -0
  253. package/dist/utils.d.ts +10 -1
  254. package/dist/utils.js +70 -3
  255. package/package.json +87 -26
  256. package/dist/dom/playground/process-dom.js +0 -5
  257. package/dist/dom/playground/test-accessibility.d.ts +0 -44
  258. package/dist/dom/playground/test-accessibility.js +0 -111
  259. /package/dist/{dom/playground/process-dom.d.ts → tools/extraction/views.js} +0 -0
@@ -2,54 +2,63 @@ import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import process from 'node:process';
5
+ import { createHash } from 'node:crypto';
5
6
  import { config as loadEnv } from 'dotenv';
6
7
  import { z } from 'zod';
7
8
  import { createLogger } from '../logging-config.js';
8
9
  import { CONFIG } from '../config.js';
9
10
  import { EventBus } from '../event-bus.js';
10
- import { uuid7str, SignalHandler, get_browser_use_version } from '../utils.js';
11
+ import { uuid7str, SignalHandler, get_browser_use_version, check_latest_browser_use_version, sanitize_surrogates, } from '../utils.js';
11
12
  import { Controller as DefaultController } from '../controller/service.js';
12
13
  import { FileSystem as AgentFileSystem, DEFAULT_FILE_SYSTEM_PATH, } from '../filesystem/file-system.js';
13
- import { SystemPrompt } from './prompts.js';
14
+ import { SystemPrompt, get_ai_step_system_prompt, get_ai_step_user_prompt, get_rerun_summary_message, get_rerun_summary_prompt, } from './prompts.js';
14
15
  import { MessageManager } from './message-manager/service.js';
15
16
  import { BrowserStateHistory } from '../browser/views.js';
16
17
  import { BrowserSession } from '../browser/session.js';
17
18
  import { BrowserProfile, DEFAULT_BROWSER_PROFILE } from '../browser/profile.js';
18
- import { InsecureSensitiveDataError } from '../exceptions.js';
19
19
  import { HistoryTreeProcessor } from '../dom/history-tree-processor/service.js';
20
20
  import { DOMHistoryElement } from '../dom/history-tree-processor/view.js';
21
- import { UserMessage } from '../llm/messages.js';
22
- import { ActionResult, AgentHistory, AgentHistoryList, AgentOutput, AgentState, AgentStepInfo, AgentError, StepMetadata, ActionModel, } from './views.js';
21
+ import { DEFAULT_INCLUDE_ATTRIBUTES, } from '../dom/views.js';
22
+ import { extractCleanMarkdownFromHtml } from '../dom/markdown-extractor.js';
23
+ import { ChatBrowserUse } from '../llm/browser-use/chat.js';
24
+ import { ModelProviderError, ModelRateLimitError } from '../llm/exceptions.js';
25
+ import { AssistantMessage, ContentPartTextParam, SystemMessage, UserMessage, } from '../llm/messages.js';
26
+ import { getLlmByName } from '../llm/models.js';
27
+ import { ActionResult, AgentHistory, AgentHistoryList, AgentOutput, AgentState, AgentStepInfo, AgentError, StepMetadata, ActionModel, PlanItem, defaultMessageCompactionSettings, normalizeMessageCompactionSettings, } from './views.js';
28
+ import { detect_variables_in_history, substitute_in_dict, } from './variable-detector.js';
23
29
  import { CreateAgentOutputFileEvent, CreateAgentSessionEvent, CreateAgentTaskEvent, CreateAgentStepEvent, UpdateAgentTaskEvent, } from './cloud-events.js';
24
30
  import { create_history_gif } from './gif.js';
25
31
  import { ScreenshotService } from '../screenshots/service.js';
26
32
  import { productTelemetry } from '../telemetry/service.js';
27
33
  import { AgentTelemetryEvent } from '../telemetry/views.js';
28
34
  import { TokenCost } from '../tokens/service.js';
35
+ import { construct_judge_messages, construct_simple_judge_messages, } from './judge.js';
36
+ import { CloudSkillService, MissingCookieException, build_skill_parameters_schema, get_skill_slug, } from '../skills/index.js';
29
37
  loadEnv();
30
38
  const logger = createLogger('browser_use.agent');
39
+ const URL_PATTERN = /https?:\/\/[^\s<>"']+|www\.[^\s<>"']+|[^\s<>"']+\.[a-z]{2,}(?:\/[^\s<>"']*)?/gi;
31
40
  export const log_response = (response, registry, logInstance = logger) => {
32
41
  if (response.current_state.thinking) {
33
- logInstance.info(`💡 Thinking:\n${response.current_state.thinking}`);
42
+ logInstance.debug(`💡 Thinking:\n${response.current_state.thinking}`);
34
43
  }
35
44
  const evalGoal = response.current_state.evaluation_previous_goal;
36
45
  if (evalGoal) {
37
- let emoji = '';
38
- if (evalGoal.toLowerCase().includes('success'))
39
- emoji = '👍';
40
- else if (evalGoal.toLowerCase().includes('failure'))
41
- emoji = '⚠️';
42
- logInstance.info(`${emoji} Eval: ${evalGoal}`);
46
+ if (evalGoal.toLowerCase().includes('success')) {
47
+ logInstance.info(` \x1b[32m👍 Eval: ${evalGoal}\x1b[0m`);
48
+ }
49
+ else if (evalGoal.toLowerCase().includes('failure')) {
50
+ logInstance.info(` \x1b[31m⚠️ Eval: ${evalGoal}\x1b[0m`);
51
+ }
52
+ else {
53
+ logInstance.info(` ❔ Eval: ${evalGoal}`);
54
+ }
43
55
  }
44
56
  if (response.current_state.memory) {
45
- logInstance.info(`🧠 Memory: ${response.current_state.memory}`);
57
+ logInstance.info(` 🧠 Memory: ${response.current_state.memory}`);
46
58
  }
47
59
  const nextGoal = response.current_state.next_goal;
48
60
  if (nextGoal) {
49
- logInstance.info(`🎯 Next goal: ${nextGoal}\n`);
50
- }
51
- else {
52
- logInstance.info('');
61
+ logInstance.info(` \x1b[34m🎯 Next goal: ${nextGoal}\x1b[0m`);
53
62
  }
54
63
  };
55
64
  class AsyncMutex {
@@ -98,28 +107,64 @@ const ensureDir = (target) => {
98
107
  fs.mkdirSync(target, { recursive: true });
99
108
  }
100
109
  };
110
+ const resolve_agent_llm = (llm) => {
111
+ if (llm) {
112
+ return llm;
113
+ }
114
+ const defaultLlmName = CONFIG.DEFAULT_LLM.trim();
115
+ if (defaultLlmName) {
116
+ return getLlmByName(defaultLlmName);
117
+ }
118
+ return new ChatBrowserUse();
119
+ };
120
+ const get_model_timeout = (llm) => {
121
+ const modelName = String(llm?.model ?? '').toLowerCase();
122
+ if (modelName.includes('gemini')) {
123
+ if (modelName.includes('3-pro')) {
124
+ return 90;
125
+ }
126
+ return 75;
127
+ }
128
+ if (modelName.includes('groq')) {
129
+ return 30;
130
+ }
131
+ if (modelName.includes('o3') ||
132
+ modelName.includes('claude') ||
133
+ modelName.includes('sonnet') ||
134
+ modelName.includes('deepseek')) {
135
+ return 90;
136
+ }
137
+ return 75;
138
+ };
101
139
  const defaultAgentOptions = () => ({
102
140
  use_vision: true,
103
- use_vision_for_planner: false,
141
+ include_recent_events: false,
142
+ sample_images: null,
143
+ llm_screenshot_size: null,
104
144
  save_conversation_path: null,
105
145
  save_conversation_path_encoding: 'utf-8',
106
146
  max_failures: 3,
107
- retry_delay: 10,
147
+ directly_open_url: true,
108
148
  override_system_message: null,
109
149
  extend_system_message: null,
110
- validate_output: false,
111
150
  generate_gif: false,
112
151
  available_file_paths: [],
113
152
  include_attributes: undefined,
114
- max_actions_per_step: 10,
153
+ max_actions_per_step: 5,
115
154
  use_thinking: true,
116
155
  flash_mode: false,
156
+ use_judge: true,
157
+ ground_truth: null,
117
158
  max_history_items: null,
118
159
  page_extraction_llm: null,
119
- planner_llm: null,
120
- planner_interval: 1,
121
- is_planner_reasoning: false,
122
- extend_planner_system_message: null,
160
+ fallback_llm: null,
161
+ judge_llm: null,
162
+ skill_ids: null,
163
+ skills: null,
164
+ skill_service: null,
165
+ enable_planning: true,
166
+ planning_replan_on_stall: 3,
167
+ planning_exploration_limit: 5,
123
168
  context: null,
124
169
  source: null,
125
170
  file_system_path: null,
@@ -129,33 +174,68 @@ const defaultAgentOptions = () => ({
129
174
  display_files_in_done_text: true,
130
175
  include_tool_call_examples: false,
131
176
  session_attachment_mode: 'copy',
132
- allow_insecure_sensitive_data: false,
133
177
  vision_detail_level: 'auto',
134
- llm_timeout: 60,
178
+ llm_timeout: null,
135
179
  step_timeout: 180,
180
+ final_response_after_failure: true,
181
+ message_compaction: true,
182
+ loop_detection_window: 20,
183
+ loop_detection_enabled: true,
184
+ _url_shortening_limit: 25,
136
185
  });
137
186
  const AgentLLMOutputSchema = z.object({
138
187
  thinking: z.string().optional().nullable(),
139
188
  evaluation_previous_goal: z.string().optional().nullable(),
140
189
  memory: z.string().optional().nullable(),
141
190
  next_goal: z.string().optional().nullable(),
191
+ current_plan_item: z.number().int().optional().nullable(),
192
+ plan_update: z.array(z.string()).optional().nullable(),
142
193
  action: z
143
194
  .array(z.record(z.string(), z.any()))
144
195
  .optional()
145
196
  .nullable()
146
197
  .default([]),
147
198
  });
199
+ const DoneOnlyLLMOutputSchema = AgentLLMOutputSchema.extend({
200
+ action: z
201
+ .array(z.object({
202
+ done: z.object({}).passthrough(),
203
+ }))
204
+ .optional()
205
+ .nullable()
206
+ .default([]),
207
+ });
208
+ const SimpleJudgeSchema = z.object({
209
+ is_correct: z.boolean(),
210
+ reason: z.string().optional().default(''),
211
+ });
212
+ const JudgeSchema = z.object({
213
+ reasoning: z.string().optional().nullable().default(''),
214
+ verdict: z.boolean(),
215
+ failure_reason: z.string().optional().nullable().default(''),
216
+ impossible_task: z.boolean().optional().default(false),
217
+ reached_captcha: z.boolean().optional().default(false),
218
+ });
148
219
  const AgentLLMOutputFormat = AgentLLMOutputSchema;
149
220
  AgentLLMOutputFormat.schema = AgentLLMOutputSchema;
221
+ const DoneOnlyLLMOutputFormat = DoneOnlyLLMOutputSchema;
222
+ DoneOnlyLLMOutputFormat.schema = DoneOnlyLLMOutputSchema;
223
+ const SimpleJudgeOutputFormat = SimpleJudgeSchema;
224
+ SimpleJudgeOutputFormat.schema = SimpleJudgeSchema;
225
+ const JudgeOutputFormat = JudgeSchema;
226
+ JudgeOutputFormat.schema = JudgeSchema;
150
227
  export class Agent {
151
228
  static _sharedSessionStepLocks = new Map();
152
229
  static DEFAULT_AGENT_DATA_DIR = path.join(process.cwd(), DEFAULT_FILE_SYSTEM_PATH);
153
230
  browser_session = null;
154
231
  llm;
232
+ judge_llm;
155
233
  unfiltered_actions;
156
234
  initial_actions;
235
+ initial_url = null;
157
236
  register_new_step_callback;
158
237
  register_done_callback;
238
+ register_should_stop_callback;
159
239
  register_external_agent_status_raise_error_callback;
160
240
  context;
161
241
  telemetry;
@@ -176,6 +256,7 @@ export class Agent {
176
256
  promise: Promise.resolve(),
177
257
  };
178
258
  output_model_schema;
259
+ extraction_schema;
179
260
  id;
180
261
  task_id;
181
262
  session_id;
@@ -203,61 +284,176 @@ export class Agent {
203
284
  AgentOutput = AgentOutput;
204
285
  DoneActionModel = ActionModel;
205
286
  DoneAgentOutput = AgentOutput;
287
+ _fallback_llm = null;
288
+ _using_fallback_llm = false;
289
+ _original_llm = null;
290
+ _url_shortening_limit = 25;
291
+ skill_service = null;
292
+ _skills_registered = false;
206
293
  constructor(params) {
207
- const { task, llm, page = null, browser = null, browser_context = null, browser_profile = null, browser_session = null, controller = null, sensitive_data = null, initial_actions = null, register_new_step_callback = null, register_done_callback = null, register_external_agent_status_raise_error_callback = null, output_model_schema = null, use_vision = true, save_conversation_path = null, save_conversation_path_encoding = 'utf-8', max_failures = 3, retry_delay = 10, override_system_message = null, extend_system_message = null, validate_output = false, generate_gif = false, available_file_paths = [], include_attributes, max_actions_per_step = 10, use_thinking = true, flash_mode = false, max_history_items = null, page_extraction_llm = null, context = null, source = null, file_system_path = null, task_id = null, cloud_sync = null, calculate_cost = false, display_files_in_done_text = true, include_tool_call_examples = false, vision_detail_level = 'auto', session_attachment_mode = 'copy', allow_insecure_sensitive_data = false, llm_timeout = 60, step_timeout = 180, } = { ...defaultAgentOptions(), ...params };
208
- if (!llm) {
209
- throw new Error('Invalid llm, must be provided');
210
- }
211
- const effectivePageExtractionLlm = page_extraction_llm ?? llm;
212
- this.llm = llm;
294
+ const { task, llm, page = null, browser = null, browser_context = null, browser_profile = null, browser_session = null, tools = null, controller = null, sensitive_data = null, initial_actions = null, directly_open_url = true, register_new_step_callback = null, register_done_callback = null, register_should_stop_callback = null, register_external_agent_status_raise_error_callback = null, output_model_schema = null, extraction_schema = null, use_vision = true, include_recent_events = false, sample_images = null, llm_screenshot_size = null, save_conversation_path = null, save_conversation_path_encoding = 'utf-8', max_failures = 3, override_system_message = null, extend_system_message = null, generate_gif = false, available_file_paths = [], include_attributes, max_actions_per_step = 5, use_thinking = true, flash_mode = false, use_judge = true, ground_truth = null, max_history_items = null, page_extraction_llm = null, fallback_llm = null, judge_llm = null, skill_ids = null, skills = null, skill_service = null, enable_planning = true, planning_replan_on_stall = 3, planning_exploration_limit = 5, context = null, source = null, file_system_path = null, task_id = null, cloud_sync = null, calculate_cost = false, display_files_in_done_text = true, include_tool_call_examples = false, vision_detail_level = 'auto', session_attachment_mode = 'copy', llm_timeout = null, step_timeout = 180, final_response_after_failure = true, message_compaction = true, loop_detection_window = 20, loop_detection_enabled = true, _url_shortening_limit = 25, } = { ...defaultAgentOptions(), ...params };
295
+ const resolvedLlm = resolve_agent_llm(llm);
296
+ const effectivePageExtractionLlm = page_extraction_llm ?? resolvedLlm;
297
+ const effectiveJudgeLlm = judge_llm ?? resolvedLlm;
298
+ const effectiveFlashMode = flash_mode || resolvedLlm?.provider === 'browser-use';
299
+ const effectiveEnablePlanning = effectiveFlashMode
300
+ ? false
301
+ : enable_planning;
302
+ const effectiveLlmTimeout = typeof llm_timeout === 'number'
303
+ ? llm_timeout
304
+ : get_model_timeout(resolvedLlm);
305
+ const normalizedMessageCompaction = this._normalizeMessageCompactionSetting(message_compaction);
306
+ let resolvedLlmScreenshotSize = llm_screenshot_size ?? null;
307
+ if (resolvedLlmScreenshotSize !== null) {
308
+ if (!Array.isArray(resolvedLlmScreenshotSize) ||
309
+ resolvedLlmScreenshotSize.length !== 2) {
310
+ throw new Error('llm_screenshot_size must be a tuple of [width, height]');
311
+ }
312
+ const [width, height] = resolvedLlmScreenshotSize;
313
+ if (!Number.isInteger(width) || !Number.isInteger(height)) {
314
+ throw new Error('llm_screenshot_size dimensions must be integers');
315
+ }
316
+ if (width < 100 || height < 100) {
317
+ throw new Error('llm_screenshot_size dimensions must be at least 100 pixels');
318
+ }
319
+ logger.info(`LLM screenshot resizing enabled: ${width}x${height}`);
320
+ }
321
+ if (resolvedLlmScreenshotSize == null) {
322
+ const modelName = String(resolvedLlm?.model ?? '');
323
+ if (modelName.startsWith('claude-sonnet')) {
324
+ resolvedLlmScreenshotSize = [1400, 850];
325
+ logger.info('Auto-configured LLM screenshot size for Claude Sonnet: 1400x850');
326
+ }
327
+ }
328
+ this.llm = resolvedLlm;
329
+ this.judge_llm = effectiveJudgeLlm;
330
+ this._fallback_llm = fallback_llm;
331
+ this._using_fallback_llm = false;
332
+ this._original_llm = resolvedLlm;
333
+ this._url_shortening_limit = Math.max(0, Math.trunc(_url_shortening_limit));
213
334
  this.id = task_id || uuid7str();
214
335
  this.task_id = this.id;
215
336
  this.session_id = uuid7str();
216
- this.task = task;
217
- this.output_model_schema = output_model_schema ?? null;
218
- this.sensitive_data = sensitive_data;
219
337
  this.available_file_paths = available_file_paths || [];
220
- this.controller = (controller ??
338
+ if (tools && controller) {
339
+ throw new Error('Cannot specify both "tools" and "controller". Use "tools" only.');
340
+ }
341
+ const resolvedController = (tools ??
342
+ controller ??
221
343
  new DefaultController({
344
+ exclude_actions: use_vision !== 'auto' ? ['screenshot'] : [],
222
345
  display_files_in_done_text,
223
346
  }));
224
- this.initial_actions = initial_actions
225
- ? this._convertInitialActions(initial_actions)
347
+ const toolsOutputModel = this._getToolsOutputModelSchema(resolvedController);
348
+ let resolvedOutputModelSchema = output_model_schema ?? null;
349
+ if (resolvedOutputModelSchema &&
350
+ toolsOutputModel &&
351
+ resolvedOutputModelSchema !== toolsOutputModel) {
352
+ this.logger.warning(`output_model_schema (${this._getOutputModelSchemaName(resolvedOutputModelSchema)}) differs from Tools output_model (${this._getOutputModelSchemaName(toolsOutputModel)}). Using Agent output_model_schema.`);
353
+ }
354
+ else if (!resolvedOutputModelSchema && toolsOutputModel) {
355
+ resolvedOutputModelSchema = toolsOutputModel;
356
+ }
357
+ this.output_model_schema = resolvedOutputModelSchema;
358
+ this.extraction_schema = extraction_schema ?? null;
359
+ if (!this.extraction_schema && this.output_model_schema) {
360
+ this.extraction_schema =
361
+ this._getOutputModelSchemaPayload(this.output_model_schema) ?? null;
362
+ }
363
+ this.task = this._enhanceTaskWithSchema(task, this.output_model_schema);
364
+ this.sensitive_data = sensitive_data;
365
+ this.controller = resolvedController;
366
+ const setCoordinateClicking = this.controller
367
+ ?.set_coordinate_clicking;
368
+ if (typeof setCoordinateClicking === 'function') {
369
+ const modelName = String(this.llm?.model ?? '').toLowerCase();
370
+ const supportsCoordinateClicking = [
371
+ 'claude-sonnet-4',
372
+ 'claude-opus-4',
373
+ 'gemini-3-pro',
374
+ 'browser-use/',
375
+ ].some((pattern) => modelName.includes(pattern));
376
+ setCoordinateClicking.call(this.controller, supportsCoordinateClicking);
377
+ }
378
+ const structuredOutputActionSchema = this._resolveStructuredOutputActionSchema(this.output_model_schema);
379
+ if (structuredOutputActionSchema) {
380
+ this.controller.use_structured_output_action(structuredOutputActionSchema);
381
+ }
382
+ if (skills && skill_ids) {
383
+ throw new Error('Cannot specify both "skills" and "skill_ids". Use "skills" only.');
384
+ }
385
+ const resolvedSkillIds = skills ?? skill_ids;
386
+ if (skill_service) {
387
+ this.skill_service = skill_service;
388
+ }
389
+ else if (resolvedSkillIds && resolvedSkillIds.length > 0) {
390
+ this.skill_service = new CloudSkillService({
391
+ skill_ids: resolvedSkillIds,
392
+ });
393
+ }
394
+ if (use_vision !== 'auto') {
395
+ const excludeAction = this.controller?.exclude_action;
396
+ if (typeof excludeAction === 'function') {
397
+ excludeAction.call(this.controller, 'screenshot');
398
+ }
399
+ else {
400
+ this.controller.registry.exclude_action?.('screenshot');
401
+ }
402
+ }
403
+ let resolvedInitialActions = initial_actions;
404
+ const hasFollowUpState = Boolean(params.injected_agent_state?.follow_up_task);
405
+ if (directly_open_url &&
406
+ !hasFollowUpState &&
407
+ !resolvedInitialActions?.length) {
408
+ const extractedUrl = this._extract_start_url(task);
409
+ if (extractedUrl) {
410
+ this.initial_url = extractedUrl;
411
+ this.logger.info(`🔗 Found URL in task: ${extractedUrl}, adding as initial action...`);
412
+ resolvedInitialActions = [
413
+ { go_to_url: { url: extractedUrl, new_tab: false } },
414
+ ];
415
+ }
416
+ }
417
+ this.initial_actions = resolvedInitialActions
418
+ ? this._convertInitialActions(resolvedInitialActions)
226
419
  : null;
227
420
  this.register_new_step_callback = register_new_step_callback;
228
421
  this.register_done_callback = register_done_callback;
422
+ this.register_should_stop_callback = register_should_stop_callback;
229
423
  this.register_external_agent_status_raise_error_callback =
230
424
  register_external_agent_status_raise_error_callback;
231
425
  this.context = context;
232
426
  this.agent_directory = Agent.DEFAULT_AGENT_DATA_DIR;
233
427
  this.settings = {
234
428
  use_vision,
429
+ include_recent_events,
235
430
  vision_detail_level,
236
- use_vision_for_planner: false,
237
431
  save_conversation_path,
238
432
  save_conversation_path_encoding,
239
433
  max_failures,
240
- retry_delay,
241
- validate_output,
242
434
  generate_gif,
243
435
  override_system_message,
244
436
  extend_system_message,
245
- include_attributes: include_attributes ?? ['title', 'type', 'name'],
437
+ include_attributes: include_attributes ?? [...DEFAULT_INCLUDE_ATTRIBUTES],
246
438
  max_actions_per_step,
247
439
  use_thinking,
248
- flash_mode,
440
+ flash_mode: effectiveFlashMode,
441
+ use_judge,
442
+ ground_truth,
249
443
  max_history_items,
250
444
  page_extraction_llm: effectivePageExtractionLlm,
251
- planner_llm: null,
252
- planner_interval: 1,
253
- is_planner_reasoning: false,
254
- extend_planner_system_message: null,
445
+ enable_planning: effectiveEnablePlanning,
446
+ planning_replan_on_stall,
447
+ planning_exploration_limit,
255
448
  calculate_cost,
256
449
  include_tool_call_examples,
257
450
  session_attachment_mode,
258
- allow_insecure_sensitive_data,
259
- llm_timeout,
451
+ llm_timeout: effectiveLlmTimeout,
260
452
  step_timeout,
453
+ final_response_after_failure,
454
+ message_compaction: normalizedMessageCompaction,
455
+ loop_detection_window,
456
+ loop_detection_enabled,
261
457
  };
262
458
  this.token_cost_service = new TokenCost(calculate_cost);
263
459
  if (calculate_cost) {
@@ -265,9 +461,14 @@ export class Agent {
265
461
  this.logger.debug(`Failed to initialize token cost service: ${error.message}`);
266
462
  });
267
463
  }
268
- this.token_cost_service.register_llm(llm);
464
+ this.token_cost_service.register_llm(resolvedLlm);
269
465
  this.token_cost_service.register_llm(effectivePageExtractionLlm);
466
+ this.token_cost_service.register_llm(effectiveJudgeLlm);
467
+ if (normalizedMessageCompaction?.compaction_llm) {
468
+ this.token_cost_service.register_llm(normalizedMessageCompaction.compaction_llm);
469
+ }
270
470
  this.state = params.injected_agent_state || new AgentState();
471
+ this.state.loop_detector.window_size = this.settings.loop_detection_window;
271
472
  this.history = new AgentHistoryList([], null);
272
473
  this.telemetry = productTelemetry;
273
474
  this._file_system_path = file_system_path;
@@ -282,15 +483,20 @@ export class Agent {
282
483
  browser_profile,
283
484
  browser_session,
284
485
  });
486
+ if (this.browser_session) {
487
+ this.browser_session.llm_screenshot_size = resolvedLlmScreenshotSize;
488
+ }
285
489
  this.has_downloads_path = Boolean(this.browser_session?.browser_profile?.downloads_path);
286
490
  if (this.has_downloads_path) {
287
491
  this._last_known_downloads = [];
288
- this.logger.info('📁 Initialized download tracking for agent');
492
+ this.logger.debug('📁 Initialized download tracking for agent');
289
493
  }
290
- this.system_prompt_class = new SystemPrompt(this.controller.registry.get_prompt_description(), this.settings.max_actions_per_step, this.settings.override_system_message, this.settings.extend_system_message, this.settings.use_thinking, this.settings.flash_mode);
291
- this._message_manager = new MessageManager(task, this.system_prompt_class.get_system_message(), this.file_system, this.state.message_manager_state, this.settings.use_thinking, this.settings.include_attributes, sensitive_data ?? undefined, this.settings.max_history_items, this.settings.vision_detail_level, this.settings.include_tool_call_examples);
494
+ this.system_prompt_class = new SystemPrompt(this.settings.max_actions_per_step, this.settings.override_system_message, this.settings.extend_system_message, this.settings.use_thinking, this.settings.flash_mode, String(this.llm?.provider ?? '').toLowerCase() === 'anthropic', String(this.llm?.model ?? '')
495
+ .toLowerCase()
496
+ .includes('browser-use/'), String(this.llm?.model ?? ''));
497
+ this._message_manager = new MessageManager(this.task, this.system_prompt_class.get_system_message(), this.file_system, this.state.message_manager_state, this.settings.use_thinking, this.settings.include_attributes, sensitive_data ?? undefined, this.settings.max_history_items, this.settings.vision_detail_level, this.settings.include_tool_call_examples, this.settings.include_recent_events, sample_images ?? null, resolvedLlmScreenshotSize);
292
498
  this.unfiltered_actions = this.controller.registry.get_prompt_description();
293
- this.eventbus = new EventBus(`Agent_${String(this.id).slice(-4)}`);
499
+ this.eventbus = new EventBus(this._buildEventBusName());
294
500
  this.enable_cloud_sync = CONFIG.BROWSER_USE_CLOUD_SYNC;
295
501
  if (this.enable_cloud_sync || cloud_sync) {
296
502
  this.cloud_sync = cloud_sync ?? null;
@@ -313,11 +519,33 @@ export class Agent {
313
519
  // Model-specific vision handling
314
520
  this._handleModelSpecificVision();
315
521
  }
522
+ _normalizeMessageCompactionSetting(messageCompaction) {
523
+ if (messageCompaction == null) {
524
+ return null;
525
+ }
526
+ if (typeof messageCompaction === 'boolean') {
527
+ return normalizeMessageCompactionSettings({
528
+ ...defaultMessageCompactionSettings(),
529
+ enabled: messageCompaction,
530
+ });
531
+ }
532
+ return normalizeMessageCompactionSettings({
533
+ ...defaultMessageCompactionSettings(),
534
+ ...messageCompaction,
535
+ });
536
+ }
316
537
  _createSessionIdWithAgentSuffix() {
317
538
  const suffix = this.id.slice(-4);
318
539
  const generated = uuid7str();
319
540
  return `${generated.slice(0, -4)}${suffix}`;
320
541
  }
542
+ _buildEventBusName() {
543
+ let agentIdSuffix = String(this.id).slice(-4).replace(/-/g, '_');
544
+ if (agentIdSuffix && /^\d/.test(agentIdSuffix)) {
545
+ agentIdSuffix = `a${agentIdSuffix}`;
546
+ }
547
+ return `Agent_${agentIdSuffix}`;
548
+ }
321
549
  _copyBrowserProfile(profile) {
322
550
  const source = profile ?? DEFAULT_BROWSER_PROFILE;
323
551
  const clonedConfig = typeof structuredClone === 'function'
@@ -525,20 +753,6 @@ export class Agent {
525
753
  id: this._createSessionIdWithAgentSuffix(),
526
754
  }));
527
755
  }
528
- _sleep_blocking(ms) {
529
- if (ms <= 0) {
530
- return;
531
- }
532
- if (typeof SharedArrayBuffer === 'function' && Atomics?.wait) {
533
- const lock = new Int32Array(new SharedArrayBuffer(4));
534
- Atomics.wait(lock, 0, 0, ms);
535
- return;
536
- }
537
- const end = Date.now() + ms;
538
- while (Date.now() < end) {
539
- // Intentional busy-wait fallback for runtimes without Atomics.wait.
540
- }
541
- }
542
756
  /**
543
757
  * Convert dictionary-based actions to ActionModel instances
544
758
  */
@@ -563,8 +777,16 @@ export class Agent {
563
777
  }
564
778
  // Validate parameters using Zod schema
565
779
  const validatedParams = paramModel.parse(params);
780
+ if (!validatedParams ||
781
+ typeof validatedParams !== 'object' ||
782
+ Array.isArray(validatedParams)) {
783
+ this.logger.warning(`⚠️ Parsed params for action "${actionName}" are not an object, skipping`);
784
+ continue;
785
+ }
566
786
  // Create action with validated parameters
567
- convertedActions.push({ [actionName]: validatedParams });
787
+ convertedActions.push({
788
+ [actionName]: validatedParams,
789
+ });
568
790
  }
569
791
  catch (error) {
570
792
  this.logger.error(`❌ Failed to validate initial action "${actionName}": ${error}`);
@@ -585,9 +807,10 @@ export class Agent {
585
807
  this.logger.warning('⚠️ DeepSeek models do not support use_vision=True yet. Setting use_vision=False for now...');
586
808
  this.settings.use_vision = false;
587
809
  }
588
- // Handle XAI (Grok) models
589
- if (modelName.includes('grok') && this.settings.use_vision) {
590
- this.logger.warning('⚠️ XAI models do not support use_vision=True yet. Setting use_vision=False for now...');
810
+ // Handle XAI models that currently do not support vision
811
+ if ((modelName.includes('grok-3') || modelName.includes('grok-code')) &&
812
+ this.settings.use_vision) {
813
+ this.logger.warning('⚠️ This XAI model does not support use_vision=True yet. Setting use_vision=False for now...');
591
814
  this.settings.use_vision = false;
592
815
  }
593
816
  }
@@ -624,20 +847,9 @@ export class Agent {
624
847
  : Boolean(allowedDomainsConfig);
625
848
  // If no allowed_domains are configured, show a security warning
626
849
  if (!hasAllowedDomains) {
627
- if (!this.settings.allow_insecure_sensitive_data) {
628
- throw new InsecureSensitiveDataError();
629
- }
630
- this.logger.error('⚠️⚠️⚠️ Agent(sensitive_data=••••••••) was provided but BrowserSession(allowed_domains=[...]) is not locked down! ⚠️⚠️⚠️\n' +
850
+ this.logger.warning('⚠️ Agent(sensitive_data=••••••••) was provided but Browser(allowed_domains=[...]) is not locked down! ⚠️\n' +
631
851
  ' ☠️ If the agent visits a malicious website and encounters a prompt-injection attack, your sensitive_data may be exposed!\n\n' +
632
- ' https://docs.browser-use.com/customize/browser-settings#restrict-urls\n' +
633
- 'Waiting 10 seconds before continuing... Press [Ctrl+C] to abort.');
634
- // Check if we're in an interactive shell (TTY)
635
- if (process.stdin.isTTY) {
636
- // Block startup for 10 seconds to match Python warning behavior.
637
- // User can still abort process with Ctrl+C.
638
- this._sleep_blocking(10_000);
639
- }
640
- this.logger.warning('‼️ Continuing with insecure settings because allow_insecure_sensitive_data=true is enabled.');
852
+ ' \n');
641
853
  }
642
854
  // If we're using domain-specific credentials, validate domain patterns
643
855
  else if (hasDomainSpecificCredentials) {
@@ -686,7 +898,7 @@ export class Agent {
686
898
  try {
687
899
  this.file_system = AgentFileSystem.from_state_sync(this.state.file_system_state);
688
900
  this._file_system_path = this.state.file_system_state.base_dir;
689
- this.logger.info(`💾 File system restored from state to: ${this._file_system_path}`);
901
+ this.logger.debug(`💾 File system restored from state to: ${this._file_system_path}`);
690
902
  const timestamp = Date.now();
691
903
  this.agent_directory = path.join(os.tmpdir(), `browser_use_agent_${this.id}_${timestamp}`);
692
904
  ensureDir(this.agent_directory);
@@ -698,7 +910,10 @@ export class Agent {
698
910
  throw error;
699
911
  }
700
912
  }
701
- const baseDir = file_system_path ?? path.join(Agent.DEFAULT_AGENT_DATA_DIR, this.task_id);
913
+ const timestamp = Date.now();
914
+ this.agent_directory = path.join(os.tmpdir(), `browser_use_agent_${this.id}_${timestamp}`);
915
+ ensureDir(this.agent_directory);
916
+ const baseDir = file_system_path ?? this.agent_directory;
702
917
  ensureDir(baseDir);
703
918
  try {
704
919
  this.file_system = new AgentFileSystem(baseDir);
@@ -709,17 +924,14 @@ export class Agent {
709
924
  this.logger.error(`💾 Failed to initialize file system: ${message}`);
710
925
  throw error;
711
926
  }
712
- const timestamp = Date.now();
713
- this.agent_directory = path.join(os.tmpdir(), `browser_use_agent_${this.id}_${timestamp}`);
714
- ensureDir(this.agent_directory);
715
927
  this.state.file_system_state = this.file_system.get_state();
716
- this.logger.info(`💾 File system path: ${this._file_system_path}`);
928
+ this.logger.debug(`💾 File system path: ${this._file_system_path}`);
717
929
  return this.file_system;
718
930
  }
719
931
  _setScreenshotService() {
720
932
  try {
721
933
  this.screenshot_service = new ScreenshotService(this.agent_directory);
722
- this.logger.info(`📸 Screenshot service initialized in: ${path.join(this.agent_directory, 'screenshots')}`);
934
+ this.logger.debug(`📸 Screenshot service initialized in: ${path.join(this.agent_directory, 'screenshots')}`);
723
935
  }
724
936
  catch (error) {
725
937
  const message = error instanceof Error ? error.message : String(error);
@@ -729,8 +941,20 @@ export class Agent {
729
941
  }
730
942
  get logger() {
731
943
  if (!this._logger) {
732
- const browserSessionId = (this.browser_session && this.browser_session.id) || this.id;
733
- this._logger = createLogger(`browser_use.Agent🅰 ${this.task_id.slice(-4)} on 🆂 ${String(browserSessionId).slice(-4)}`);
944
+ const taskIdSuffix = typeof this.task_id === 'string' && this.task_id.length
945
+ ? this.task_id.slice(-4)
946
+ : '----';
947
+ const browserSessionSuffix = typeof this.browser_session?.id === 'string'
948
+ ? this.browser_session.id.slice(-4)
949
+ : typeof this.id === 'string'
950
+ ? this.id.slice(-4)
951
+ : '----';
952
+ const focusTargetSuffixRaw = this.browser_session
953
+ ?.agent_focus_target_id;
954
+ const focusTargetSuffix = typeof focusTargetSuffixRaw === 'string' && focusTargetSuffixRaw.length
955
+ ? focusTargetSuffixRaw.slice(-2)
956
+ : '--';
957
+ this._logger = createLogger(`browser_use.Agent🅰 ${taskIdSuffix} ⇢ 🅑 ${browserSessionSuffix} 🅣 ${focusTargetSuffix}`);
734
958
  }
735
959
  return this._logger;
736
960
  }
@@ -770,6 +994,12 @@ export class Agent {
770
994
  }
771
995
  return this.browser_session.browser_profile;
772
996
  }
997
+ get is_using_fallback_llm() {
998
+ return this._using_fallback_llm;
999
+ }
1000
+ get current_llm_model() {
1001
+ return typeof this.llm?.model === 'string' ? this.llm.model : 'unknown';
1002
+ }
773
1003
  /**
774
1004
  * Add a new task to the agent, keeping the same task_id as tasks are continuous
775
1005
  */
@@ -778,6 +1008,210 @@ export class Agent {
778
1008
  // The task continues with new instructions, it doesn't end and start a new one
779
1009
  this.task = newTask;
780
1010
  this._message_manager.add_new_task(newTask);
1011
+ this.state.follow_up_task = true;
1012
+ this.state.stopped = false;
1013
+ this.state.paused = false;
1014
+ this.eventbus = new EventBus(this._buildEventBusName());
1015
+ }
1016
+ _enhanceTaskWithSchema(task, outputModelSchema) {
1017
+ if (!outputModelSchema) {
1018
+ return task;
1019
+ }
1020
+ try {
1021
+ const schemaPayload = this._getOutputModelSchemaPayload(outputModelSchema);
1022
+ if (schemaPayload == null) {
1023
+ return task;
1024
+ }
1025
+ const schemaJson = JSON.stringify(schemaPayload, null, 2);
1026
+ if (!schemaJson) {
1027
+ return task;
1028
+ }
1029
+ const schemaName = typeof outputModelSchema?.name === 'string'
1030
+ ? outputModelSchema.name
1031
+ : 'StructuredOutput';
1032
+ return `${task}\nExpected output format: ${schemaName}\n${schemaJson}`;
1033
+ }
1034
+ catch (error) {
1035
+ this.logger.debug(`Could not parse output schema for task enhancement: ${error instanceof Error ? error.message : String(error)}`);
1036
+ return task;
1037
+ }
1038
+ }
1039
+ _getOutputModelSchemaPayload(outputModelSchema) {
1040
+ if (outputModelSchema instanceof z.ZodType) {
1041
+ try {
1042
+ const schema = z.toJSONSchema(outputModelSchema);
1043
+ return schema && typeof schema === 'object'
1044
+ ? schema
1045
+ : null;
1046
+ }
1047
+ catch {
1048
+ return null;
1049
+ }
1050
+ }
1051
+ if (typeof outputModelSchema.model_json_schema === 'function') {
1052
+ const schema = outputModelSchema.model_json_schema();
1053
+ return schema && typeof schema === 'object'
1054
+ ? schema
1055
+ : null;
1056
+ }
1057
+ if (outputModelSchema.schema != null) {
1058
+ const schemaCandidate = outputModelSchema.schema;
1059
+ const schema = (() => {
1060
+ if (schemaCandidate instanceof z.ZodType) {
1061
+ return z.toJSONSchema(schemaCandidate);
1062
+ }
1063
+ if (typeof schemaCandidate?.toJSON === 'function') {
1064
+ return schemaCandidate.toJSON();
1065
+ }
1066
+ return schemaCandidate;
1067
+ })();
1068
+ return schema && typeof schema === 'object'
1069
+ ? schema
1070
+ : null;
1071
+ }
1072
+ return null;
1073
+ }
1074
+ _getToolsOutputModelSchema(tools) {
1075
+ const getOutputModel = tools?.get_output_model;
1076
+ if (typeof getOutputModel !== 'function') {
1077
+ return null;
1078
+ }
1079
+ const outputModel = getOutputModel.call(tools);
1080
+ return outputModel == null
1081
+ ? null
1082
+ : outputModel;
1083
+ }
1084
+ _getOutputModelSchemaName(outputModelSchema) {
1085
+ const explicitName = typeof outputModelSchema?.name === 'string'
1086
+ ? outputModelSchema.name
1087
+ : null;
1088
+ if (explicitName && explicitName.trim().length > 0) {
1089
+ return explicitName;
1090
+ }
1091
+ const ctorName = outputModelSchema?.constructor?.name;
1092
+ return typeof ctorName === 'string' && ctorName.trim().length > 0
1093
+ ? ctorName
1094
+ : 'StructuredOutput';
1095
+ }
1096
+ _resolveStructuredOutputActionSchema(outputModelSchema) {
1097
+ if (!outputModelSchema) {
1098
+ return null;
1099
+ }
1100
+ if (outputModelSchema instanceof z.ZodType) {
1101
+ return outputModelSchema;
1102
+ }
1103
+ const schemaCandidate = outputModelSchema?.schema;
1104
+ if (schemaCandidate instanceof z.ZodType) {
1105
+ return schemaCandidate;
1106
+ }
1107
+ return null;
1108
+ }
1109
+ _extract_start_url(taskText) {
1110
+ const taskWithoutEmails = taskText.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, '');
1111
+ const urlPatterns = [
1112
+ /https?:\/\/[^\s<>"']+/g,
1113
+ /(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}(?:\/[^\s<>"']*)?/g,
1114
+ ];
1115
+ const excludedExtensions = new Set([
1116
+ 'pdf',
1117
+ 'doc',
1118
+ 'docx',
1119
+ 'xls',
1120
+ 'xlsx',
1121
+ 'ppt',
1122
+ 'pptx',
1123
+ 'odt',
1124
+ 'ods',
1125
+ 'odp',
1126
+ 'txt',
1127
+ 'md',
1128
+ 'csv',
1129
+ 'json',
1130
+ 'xml',
1131
+ 'yaml',
1132
+ 'yml',
1133
+ 'zip',
1134
+ 'rar',
1135
+ '7z',
1136
+ 'tar',
1137
+ 'gz',
1138
+ 'bz2',
1139
+ 'xz',
1140
+ 'jpg',
1141
+ 'jpeg',
1142
+ 'png',
1143
+ 'gif',
1144
+ 'bmp',
1145
+ 'svg',
1146
+ 'webp',
1147
+ 'ico',
1148
+ 'mp3',
1149
+ 'mp4',
1150
+ 'avi',
1151
+ 'mkv',
1152
+ 'mov',
1153
+ 'wav',
1154
+ 'flac',
1155
+ 'ogg',
1156
+ 'py',
1157
+ 'js',
1158
+ 'css',
1159
+ 'java',
1160
+ 'cpp',
1161
+ 'bib',
1162
+ 'bibtex',
1163
+ 'tex',
1164
+ 'latex',
1165
+ 'cls',
1166
+ 'sty',
1167
+ 'exe',
1168
+ 'msi',
1169
+ 'dmg',
1170
+ 'pkg',
1171
+ 'deb',
1172
+ 'rpm',
1173
+ 'iso',
1174
+ 'polynomial',
1175
+ ]);
1176
+ const excludedWords = ['never', 'dont', "don't", 'not'];
1177
+ const foundUrls = [];
1178
+ for (const pattern of urlPatterns) {
1179
+ for (const match of taskWithoutEmails.matchAll(pattern)) {
1180
+ const original = match[0];
1181
+ const startIndex = match.index ?? 0;
1182
+ let url = original.replace(/[.,;:!?()[\]]+$/g, '');
1183
+ const lowerUrl = url.toLowerCase();
1184
+ let shouldExclude = false;
1185
+ for (const ext of excludedExtensions) {
1186
+ if (lowerUrl.includes(`.${ext}`)) {
1187
+ shouldExclude = true;
1188
+ break;
1189
+ }
1190
+ }
1191
+ if (shouldExclude) {
1192
+ this.logger.debug(`Excluding URL with file extension from auto-navigation: ${url}`);
1193
+ continue;
1194
+ }
1195
+ const contextStart = Math.max(0, startIndex - 20);
1196
+ const contextText = taskWithoutEmails
1197
+ .slice(contextStart, startIndex)
1198
+ .toLowerCase();
1199
+ if (excludedWords.some((word) => contextText.includes(word))) {
1200
+ this.logger.debug(`Excluding URL with word in excluded words from auto-navigation: ${url} (context: "${contextText.trim()}")`);
1201
+ continue;
1202
+ }
1203
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
1204
+ url = `https://${url}`;
1205
+ }
1206
+ foundUrls.push(url);
1207
+ }
1208
+ }
1209
+ const uniqueUrls = Array.from(new Set(foundUrls));
1210
+ if (uniqueUrls.length > 1) {
1211
+ this.logger.debug(`Multiple URLs found (${foundUrls.length}), skipping directly_open_url to avoid ambiguity`);
1212
+ return null;
1213
+ }
1214
+ return uniqueUrls.length === 1 ? uniqueUrls[0] : null;
781
1215
  }
782
1216
  /**
783
1217
  * Take a step and return whether the task is done and valid
@@ -786,7 +1220,11 @@ export class Agent {
786
1220
  async takeStep(stepInfo) {
787
1221
  await this._step(stepInfo ?? null);
788
1222
  if (this.history.is_done()) {
1223
+ await this._run_simple_judge();
789
1224
  await this.log_completion();
1225
+ if (this.settings.use_judge) {
1226
+ await this._judge_and_log();
1227
+ }
790
1228
  if (this.register_done_callback) {
791
1229
  await this.register_done_callback(this.history);
792
1230
  }
@@ -917,6 +1355,173 @@ export class Agent {
917
1355
  this.DoneAgentOutput = AgentOutput.type_with_custom_actions_no_thinking(this.DoneActionModel);
918
1356
  }
919
1357
  }
1358
+ async _register_skills_as_actions() {
1359
+ if (!this.skill_service || this._skills_registered) {
1360
+ return;
1361
+ }
1362
+ const skills = await this.skill_service.get_all_skills();
1363
+ if (!skills.length) {
1364
+ this.logger.warning('No skills loaded from SkillService');
1365
+ return;
1366
+ }
1367
+ this.logger.info(`🔧 Registering ${skills.length} skill action(s)...`);
1368
+ for (const skill of skills) {
1369
+ const slug = get_skill_slug(skill, skills);
1370
+ const paramSchema = build_skill_parameters_schema(skill.parameters, {
1371
+ exclude_cookies: true,
1372
+ });
1373
+ const description = `${skill.description} (Skill: "${skill.title}")`;
1374
+ this.controller.registry.action(description, {
1375
+ param_model: paramSchema,
1376
+ action_name: slug,
1377
+ })(async (params, { browser_session }) => {
1378
+ if (!this.skill_service) {
1379
+ return new ActionResult({ error: 'SkillService not initialized' });
1380
+ }
1381
+ if (!browser_session ||
1382
+ typeof browser_session.get_cookies !== 'function') {
1383
+ return new ActionResult({
1384
+ error: 'Skill execution requires an active BrowserSession.',
1385
+ });
1386
+ }
1387
+ try {
1388
+ const cookiesRaw = await browser_session.get_cookies();
1389
+ const cookies = Array.isArray(cookiesRaw)
1390
+ ? cookiesRaw
1391
+ .map((cookie) => {
1392
+ const record = cookie && typeof cookie === 'object'
1393
+ ? cookie
1394
+ : null;
1395
+ const name = record && typeof record.name === 'string'
1396
+ ? record.name
1397
+ : null;
1398
+ const value = record && typeof record.value === 'string'
1399
+ ? record.value
1400
+ : '';
1401
+ return name ? { name, value } : null;
1402
+ })
1403
+ .filter((cookie) => cookie != null)
1404
+ : [];
1405
+ const result = await this.skill_service.execute_skill({
1406
+ skill_id: skill.id,
1407
+ parameters: params ?? {},
1408
+ cookies,
1409
+ });
1410
+ if (!result.success) {
1411
+ return new ActionResult({
1412
+ error: result.error ?? 'Skill execution failed',
1413
+ });
1414
+ }
1415
+ const rendered = typeof result.result === 'string'
1416
+ ? result.result
1417
+ : JSON.stringify(result.result ?? {});
1418
+ return new ActionResult({
1419
+ extracted_content: rendered,
1420
+ long_term_memory: rendered,
1421
+ });
1422
+ }
1423
+ catch (error) {
1424
+ if (error instanceof MissingCookieException) {
1425
+ return new ActionResult({
1426
+ error: `Missing cookies (${error.cookie_name}): ${error.cookie_description}`,
1427
+ });
1428
+ }
1429
+ const message = error instanceof Error
1430
+ ? `${error.name}: ${error.message}`
1431
+ : String(error);
1432
+ return new ActionResult({
1433
+ error: `Skill execution error: ${message}`,
1434
+ });
1435
+ }
1436
+ });
1437
+ }
1438
+ this._skills_registered = true;
1439
+ this._setup_action_models();
1440
+ if (this.initial_actions?.length) {
1441
+ const actionDicts = this.initial_actions.map((action) => typeof action?.model_dump === 'function'
1442
+ ? action.model_dump({ exclude_unset: true })
1443
+ : action);
1444
+ this.initial_actions = this._convertInitialActions(actionDicts);
1445
+ }
1446
+ this.logger.info(`✓ Registered ${skills.length} skill actions`);
1447
+ }
1448
+ async _get_unavailable_skills_info() {
1449
+ if (!this.skill_service || !this.browser_session) {
1450
+ return '';
1451
+ }
1452
+ try {
1453
+ const skills = await this.skill_service.get_all_skills();
1454
+ if (!skills.length) {
1455
+ return '';
1456
+ }
1457
+ const currentCookies = await this.browser_session.get_cookies();
1458
+ const cookieNames = new Set();
1459
+ if (Array.isArray(currentCookies)) {
1460
+ for (const cookie of currentCookies) {
1461
+ if (!cookie || typeof cookie !== 'object') {
1462
+ continue;
1463
+ }
1464
+ const name = typeof cookie.name === 'string'
1465
+ ? String(cookie.name)
1466
+ : '';
1467
+ if (name) {
1468
+ cookieNames.add(name);
1469
+ }
1470
+ }
1471
+ }
1472
+ const unavailableSkills = [];
1473
+ for (const skill of skills) {
1474
+ const cookieParams = skill.parameters.filter((param) => param.type === 'cookie');
1475
+ if (!cookieParams.length) {
1476
+ continue;
1477
+ }
1478
+ const missingCookies = [];
1479
+ for (const cookieParam of cookieParams) {
1480
+ const isRequired = cookieParam.required !== false;
1481
+ if (isRequired && !cookieNames.has(cookieParam.name)) {
1482
+ missingCookies.push({
1483
+ name: cookieParam.name,
1484
+ description: cookieParam.description || 'No description provided',
1485
+ });
1486
+ }
1487
+ }
1488
+ if (missingCookies.length) {
1489
+ unavailableSkills.push({
1490
+ id: skill.id,
1491
+ title: skill.title,
1492
+ description: skill.description,
1493
+ missing_cookies: missingCookies,
1494
+ });
1495
+ }
1496
+ }
1497
+ if (!unavailableSkills.length) {
1498
+ return '';
1499
+ }
1500
+ const lines = [
1501
+ 'Unavailable Skills (missing required cookies):',
1502
+ ];
1503
+ for (const skillInfo of unavailableSkills) {
1504
+ const skillObj = skills.find((entry) => entry.id === skillInfo.id);
1505
+ const slug = skillObj
1506
+ ? get_skill_slug(skillObj, skills)
1507
+ : skillInfo.title;
1508
+ lines.push('');
1509
+ lines.push(` • ${slug} ("${skillInfo.title}")`);
1510
+ lines.push(` Description: ${skillInfo.description}`);
1511
+ lines.push(' Missing cookies:');
1512
+ for (const cookie of skillInfo.missing_cookies) {
1513
+ lines.push(` - ${cookie.name}: ${cookie.description}`);
1514
+ }
1515
+ }
1516
+ return lines.join('\n');
1517
+ }
1518
+ catch (error) {
1519
+ this.logger.error(`Error getting unavailable skills info: ${error instanceof Error
1520
+ ? `${error.name}: ${error.message}`
1521
+ : String(error)}`);
1522
+ return '';
1523
+ }
1524
+ }
920
1525
  /**
921
1526
  * Update action models with page-specific actions
922
1527
  * Called during each step to filter actions based on current page context
@@ -949,7 +1554,37 @@ export class Agent {
949
1554
  this.DoneAgentOutput = AgentOutput.type_with_custom_actions_no_thinking(this.DoneActionModel);
950
1555
  }
951
1556
  }
952
- async run(max_steps = 100, on_step_start = null, on_step_end = null) {
1557
+ async _execute_initial_actions() {
1558
+ if (!this.initial_actions?.length || this.state.follow_up_task) {
1559
+ return;
1560
+ }
1561
+ this.logger.debug(`⚡ Executing ${this.initial_actions.length} initial actions...`);
1562
+ const result = await this.multi_act(this.initial_actions);
1563
+ if (result.length > 0 && this.initial_url && result[0]?.long_term_memory) {
1564
+ result[0].long_term_memory = `Found initial url and automatically loaded it. ${result[0].long_term_memory}`;
1565
+ }
1566
+ this.state.last_result = result;
1567
+ const modelOutput = this.settings.flash_mode
1568
+ ? new this.AgentOutput({
1569
+ evaluation_previous_goal: null,
1570
+ memory: 'Initial navigation',
1571
+ next_goal: null,
1572
+ action: this.initial_actions,
1573
+ })
1574
+ : new this.AgentOutput({
1575
+ evaluation_previous_goal: 'Start',
1576
+ memory: null,
1577
+ next_goal: 'Initial navigation',
1578
+ action: this.initial_actions,
1579
+ });
1580
+ const timestamp = Date.now() / 1000;
1581
+ const metadata = new StepMetadata(timestamp, timestamp, 0, null);
1582
+ const stateHistory = new BrowserStateHistory(this.initial_url ?? '', 'Initial Actions', [], Array(this.initial_actions.length).fill(null), null);
1583
+ this.history.add_item(new AgentHistory(modelOutput, result, stateHistory, metadata, null));
1584
+ this.logger.debug('📝 Saved initial actions to history as step 0');
1585
+ this.logger.debug('✅ Initial actions completed');
1586
+ }
1587
+ async run(max_steps = 500, on_step_start = null, on_step_end = null) {
953
1588
  let agent_run_error = null;
954
1589
  this._force_exit_telemetry_logged = false;
955
1590
  const signal_handler = new SignalHandler({
@@ -964,61 +1599,76 @@ export class Agent {
964
1599
  });
965
1600
  signal_handler.register();
966
1601
  try {
967
- this._log_agent_run();
1602
+ await this._log_agent_run();
968
1603
  this.logger.debug(`🔧 Agent setup: Task ID ${this.task_id.slice(-4)}, Session ID ${this.session_id.slice(-4)}, Browser Session ID ${this.browser_session?.id?.slice?.(-4) ?? 'None'}`);
969
1604
  this._session_start_time = Date.now() / 1000;
970
1605
  this._task_start_time = this._session_start_time;
971
- this.logger.debug('📡 Dispatching CreateAgentSessionEvent...');
972
- this.eventbus.dispatch(CreateAgentSessionEvent.fromAgent(this));
1606
+ if (!this.state.session_initialized) {
1607
+ this.logger.debug('📡 Dispatching CreateAgentSessionEvent...');
1608
+ this.eventbus.dispatch(CreateAgentSessionEvent.fromAgent(this));
1609
+ this.state.session_initialized = true;
1610
+ }
973
1611
  this.logger.debug('📡 Dispatching CreateAgentTaskEvent...');
974
1612
  this.eventbus.dispatch(CreateAgentTaskEvent.fromAgent(this));
975
- if (this.initial_actions?.length) {
976
- this.logger.debug(`⚡ Executing ${this.initial_actions.length} initial actions...`);
977
- const result = await this.multi_act(this.initial_actions, {
978
- check_for_new_elements: false,
979
- });
980
- this.state.last_result = result;
981
- this.logger.debug('✅ Initial actions completed');
1613
+ if (!this.state.stopped) {
1614
+ await this.browser_session?.start();
1615
+ }
1616
+ await this._register_skills_as_actions();
1617
+ try {
1618
+ await this._execute_initial_actions();
1619
+ }
1620
+ catch (error) {
1621
+ if (error?.name !== 'InterruptedError') {
1622
+ throw error;
1623
+ }
982
1624
  }
983
- this.logger.debug(`🔄 Starting main execution loop with max ${max_steps} steps...`);
984
- for (let step = 0; step < max_steps; step += 1) {
1625
+ this.logger.debug(`🔄 Starting main execution loop with max ${max_steps} steps (currently at step ${this.state.n_steps})...`);
1626
+ while (this.state.n_steps <= max_steps) {
1627
+ const currentStep = this.state.n_steps - 1;
985
1628
  if (this.state.paused) {
986
- this.logger.debug(`⏸️ Step ${step}: Agent paused, waiting to resume...`);
1629
+ this.logger.debug(`⏸️ Step ${this.state.n_steps}: Agent paused, waiting to resume...`);
987
1630
  await this.wait_until_resumed();
988
1631
  signal_handler.reset();
989
1632
  }
990
- if (this.state.consecutive_failures >= this.settings.max_failures) {
1633
+ if (this.state.consecutive_failures >= this._max_total_failures()) {
991
1634
  this.logger.error(`❌ Stopping due to ${this.settings.max_failures} consecutive failures`);
992
1635
  agent_run_error = `Stopped due to ${this.settings.max_failures} consecutive failures`;
993
1636
  break;
994
1637
  }
995
- if (this.state.stopped) {
996
- this.logger.info('🛑 Agent stopped');
997
- agent_run_error = 'Agent stopped programmatically';
998
- break;
1638
+ try {
1639
+ await this._raise_if_stopped_or_paused();
999
1640
  }
1000
- if (this.register_external_agent_status_raise_error_callback) {
1001
- const shouldRaise = await this.register_external_agent_status_raise_error_callback();
1002
- if (shouldRaise) {
1003
- agent_run_error = 'Agent stopped due to external request';
1641
+ catch (error) {
1642
+ if (error?.name === 'InterruptedError') {
1643
+ if (this.state.paused) {
1644
+ continue;
1645
+ }
1646
+ if (this.state.stopped) {
1647
+ this.logger.info('🛑 Agent stopped');
1648
+ agent_run_error = 'Agent stopped programmatically';
1649
+ }
1650
+ else {
1651
+ agent_run_error = 'Agent stopped due to external request';
1652
+ }
1004
1653
  break;
1005
1654
  }
1655
+ throw error;
1006
1656
  }
1007
1657
  if (on_step_start) {
1008
1658
  await on_step_start(this);
1009
1659
  }
1010
- this.logger.debug(`🚶 Starting step ${step + 1}/${max_steps}...`);
1011
- const step_info = new AgentStepInfo(step, max_steps);
1660
+ this.logger.debug(`🚶 Starting step ${currentStep + 1}/${max_steps}...`);
1661
+ const step_info = new AgentStepInfo(currentStep, max_steps);
1012
1662
  const stepAbortController = new AbortController();
1013
1663
  try {
1014
1664
  await this._executeWithTimeout(this._step(step_info, stepAbortController.signal), this.settings.step_timeout ?? 0, () => stepAbortController.abort());
1015
- this.logger.debug(`✅ Completed step ${step + 1}/${max_steps}`);
1665
+ this.logger.debug(`✅ Completed step ${currentStep + 1}/${max_steps}`);
1016
1666
  }
1017
1667
  catch (error) {
1018
1668
  const message = error instanceof Error ? error.message : String(error);
1019
1669
  const isTimeout = error instanceof ExecutionTimeoutError;
1020
1670
  if (isTimeout) {
1021
- const timeoutMessage = `Step ${step + 1} timed out after ${this.settings.step_timeout} seconds`;
1671
+ const timeoutMessage = `Step ${currentStep + 1} timed out after ${this.settings.step_timeout} seconds`;
1022
1672
  this.logger.error(`⏰ ${timeoutMessage}`);
1023
1673
  this.state.consecutive_failures += 1;
1024
1674
  this.state.last_result = [
@@ -1030,11 +1680,11 @@ export class Agent {
1030
1680
  agent_run_error = timeoutMessage;
1031
1681
  break;
1032
1682
  }
1033
- this.logger.error(`❌ Unhandled step error at step ${step + 1}: ${message}`);
1683
+ this.logger.error(`❌ Unhandled step error at step ${currentStep + 1}: ${message}`);
1034
1684
  this.state.consecutive_failures += 1;
1035
1685
  this.state.last_result = [
1036
1686
  new ActionResult({
1037
- error: message || `Unhandled step error at step ${step + 1}`,
1687
+ error: message || `Unhandled step error at step ${currentStep + 1}`,
1038
1688
  }),
1039
1689
  ];
1040
1690
  }
@@ -1042,8 +1692,12 @@ export class Agent {
1042
1692
  await on_step_end(this);
1043
1693
  }
1044
1694
  if (this.history.is_done()) {
1045
- this.logger.debug(`🎯 Task completed after ${step + 1} steps!`);
1695
+ this.logger.debug(`🎯 Task completed after ${currentStep + 1} steps!`);
1696
+ await this._run_simple_judge();
1046
1697
  await this.log_completion();
1698
+ if (this.settings.use_judge) {
1699
+ await this._judge_and_log();
1700
+ }
1047
1701
  if (this.register_done_callback) {
1048
1702
  const maybePromise = this.register_done_callback(this.history);
1049
1703
  if (maybePromise &&
@@ -1053,16 +1707,18 @@ export class Agent {
1053
1707
  }
1054
1708
  break;
1055
1709
  }
1056
- if (step === max_steps - 1) {
1057
- agent_run_error = 'Failed to complete task in maximum steps';
1058
- this.history.add_item(new AgentHistory(null, [
1059
- new ActionResult({
1060
- error: agent_run_error,
1061
- include_in_memory: true,
1062
- }),
1063
- ], new BrowserStateHistory('', '', [], [], null), null));
1064
- this.logger.info(`❌ ${agent_run_error}`);
1065
- }
1710
+ }
1711
+ if (this.state.n_steps > max_steps &&
1712
+ !this.history.is_done() &&
1713
+ !agent_run_error) {
1714
+ agent_run_error = 'Failed to complete task in maximum steps';
1715
+ this.history.add_item(new AgentHistory(null, [
1716
+ new ActionResult({
1717
+ error: agent_run_error,
1718
+ include_in_memory: true,
1719
+ }),
1720
+ ], new BrowserStateHistory('', '', [], [], null), null));
1721
+ this.logger.info(`❌ ${agent_run_error}`);
1066
1722
  }
1067
1723
  this.logger.debug('📊 Collecting usage summary...');
1068
1724
  this.history.usage =
@@ -1175,10 +1831,12 @@ export class Agent {
1175
1831
  this._throwIfAborted(signal);
1176
1832
  await this._restore_shared_pinned_tab_if_needed();
1177
1833
  this._throwIfAborted(signal);
1834
+ this._log_first_step_startup();
1178
1835
  this.logger.debug(`🌐 Step ${this.state.n_steps}: Getting browser state...`);
1179
1836
  const browser_state_summary = await this.browser_session.get_browser_state_with_recovery?.({
1180
1837
  cache_clickable_elements_hashes: true,
1181
- include_screenshot: this.settings.use_vision,
1838
+ include_screenshot: true,
1839
+ include_recent_events: this.settings.include_recent_events,
1182
1840
  signal,
1183
1841
  });
1184
1842
  this._throwIfAborted(signal);
@@ -1191,11 +1849,33 @@ export class Agent {
1191
1849
  this._throwIfAborted(signal);
1192
1850
  await this._updateActionModelsForPage(current_page);
1193
1851
  const page_filtered_actions = this.controller.registry.get_prompt_description(current_page);
1852
+ let unavailable_skills_info = null;
1853
+ if (this.skill_service) {
1854
+ unavailable_skills_info = await this._get_unavailable_skills_info();
1855
+ }
1194
1856
  this.logger.debug(`💬 Step ${this.state.n_steps}: Creating state messages for context...`);
1195
- this._message_manager.create_state_messages(browser_state_summary, this.state.last_model_output, this.state.last_result, step_info, this.settings.use_vision, page_filtered_actions || null, this.sensitive_data ?? null, this.available_file_paths);
1857
+ this._message_manager.prepare_step_state(browser_state_summary, this.state.last_model_output, this.state.last_result, step_info, this.sensitive_data ?? null);
1858
+ await this._maybe_compact_messages(step_info);
1859
+ this._message_manager.create_state_messages(browser_state_summary, this.state.last_model_output, this.state.last_result, step_info, this.settings.use_vision, page_filtered_actions || null, this.sensitive_data ?? null, this.available_file_paths, this.settings.include_recent_events, this._render_plan_description(), unavailable_skills_info, true);
1860
+ this._inject_budget_warning(step_info);
1861
+ this._inject_replan_nudge();
1862
+ this._inject_exploration_nudge();
1863
+ this._update_loop_detector_page_state(browser_state_summary);
1864
+ this._inject_loop_detection_nudge();
1196
1865
  await this._handle_final_step(step_info);
1866
+ await this._handle_failure_limit_recovery();
1197
1867
  return browser_state_summary;
1198
1868
  }
1869
+ async _maybe_compact_messages(step_info = null) {
1870
+ const settings = this.settings.message_compaction;
1871
+ if (!settings || !settings.enabled) {
1872
+ return;
1873
+ }
1874
+ const compactionLlm = settings.compaction_llm ??
1875
+ this.settings.page_extraction_llm ??
1876
+ this.llm;
1877
+ await this._message_manager.maybe_compact_messages(compactionLlm, settings, step_info);
1878
+ }
1199
1879
  async _storeScreenshotForStep(browser_state_summary) {
1200
1880
  this._current_screenshot_path = null;
1201
1881
  if (!this.screenshot_service || !browser_state_summary?.screenshot) {
@@ -1224,7 +1904,7 @@ export class Agent {
1224
1904
  }
1225
1905
  catch (error) {
1226
1906
  if (error instanceof ExecutionTimeoutError) {
1227
- throw new Error(`LLM call timed out after ${this.settings.llm_timeout} seconds. Keep your thinking and output short.`);
1907
+ throw new Error(`LLM call timed out after ${this.settings.llm_timeout} seconds. Keep your thinking and output short.`, { cause: error });
1228
1908
  }
1229
1909
  throw error;
1230
1910
  }
@@ -1256,32 +1936,51 @@ export class Agent {
1256
1936
  throw new Error('BrowserSession is not set up');
1257
1937
  }
1258
1938
  await this._check_and_update_downloads('after executing actions');
1259
- this.state.consecutive_failures = 0;
1939
+ if (this.state.last_model_output) {
1940
+ this._update_plan_from_model_output(this.state.last_model_output);
1941
+ }
1942
+ this._update_loop_detector_actions();
1943
+ const lastResult = this.state.last_result;
1944
+ if (lastResult && lastResult.length === 1 && lastResult[0]?.error) {
1945
+ this.state.consecutive_failures += 1;
1946
+ this.logger.debug(`🔄 Step ${this.state.n_steps}: Consecutive failures: ${this.state.consecutive_failures}`);
1947
+ return;
1948
+ }
1949
+ if (this.state.consecutive_failures > 0) {
1950
+ this.state.consecutive_failures = 0;
1951
+ this.logger.debug(`🔄 Step ${this.state.n_steps}: Consecutive failures reset to: ${this.state.consecutive_failures}`);
1952
+ }
1953
+ if (lastResult &&
1954
+ lastResult.length > 0 &&
1955
+ lastResult[lastResult.length - 1]?.is_done) {
1956
+ const finalResult = lastResult[lastResult.length - 1];
1957
+ const success = Boolean(finalResult.success);
1958
+ const renderedContent = typeof finalResult.extracted_content === 'string'
1959
+ ? finalResult.extracted_content
1960
+ : String(finalResult.extracted_content ?? '');
1961
+ if (success) {
1962
+ this.logger.info(`\n📄 \x1b[32m Final Result:\x1b[0m \n${renderedContent}\n\n`);
1963
+ }
1964
+ else {
1965
+ this.logger.info(`\n📄 \x1b[31m Final Result:\x1b[0m \n${renderedContent}\n\n`);
1966
+ }
1967
+ const attachments = Array.isArray(finalResult.attachments)
1968
+ ? finalResult.attachments
1969
+ : [];
1970
+ const totalAttachments = attachments.length;
1971
+ for (let i = 0; i < attachments.length; i++) {
1972
+ const suffix = totalAttachments > 1 ? String(i + 1) : '';
1973
+ this.logger.info(`👉 Attachment${suffix ? ` ${suffix}` : ''}: ${attachments[i]}`);
1974
+ }
1975
+ }
1260
1976
  }
1261
1977
  async multi_act(actions, options = {}) {
1262
- const { check_for_new_elements = true, signal = null } = options;
1978
+ const { signal = null } = options;
1263
1979
  const results = [];
1264
1980
  if (!this.browser_session) {
1265
1981
  throw new Error('BrowserSession is not set up');
1266
1982
  }
1267
1983
  await this._restore_shared_pinned_tab_if_needed();
1268
- // ==================== Selector Map Caching ====================
1269
- // Check if any action uses an index, if so cache the selector map
1270
- let cached_selector_map = {};
1271
- let cached_path_hashes = new Set();
1272
- for (const action of actions) {
1273
- const actionName = Object.keys(action)[0];
1274
- const actionParams = action[actionName];
1275
- const index = actionParams?.index;
1276
- if (index !== null && index !== undefined) {
1277
- cached_selector_map =
1278
- (await this.browser_session.get_selector_map?.()) || {};
1279
- cached_path_hashes = new Set(Object.values(cached_selector_map)
1280
- .map((e) => e?.hash?.branch_path_hash)
1281
- .filter(Boolean));
1282
- break;
1283
- }
1284
- }
1285
1984
  // ==================== Execute Actions ====================
1286
1985
  for (let i = 0; i < actions.length; i++) {
1287
1986
  this._throwIfAborted(signal);
@@ -1295,50 +1994,8 @@ export class Agent {
1295
1994
  this.logger.info(msg);
1296
1995
  break;
1297
1996
  }
1298
- // ==================== Index Change & New Element Detection ====================
1997
+ // ==================== Wait Between Actions ====================
1299
1998
  if (i > 0) {
1300
- const currentIndex = actionParams?.index;
1301
- if (currentIndex !== null && currentIndex !== undefined) {
1302
- this._throwIfAborted(signal);
1303
- // Get new browser state after previous action
1304
- const new_browser_state_summary = await this.browser_session.get_browser_state_with_recovery?.({
1305
- cache_clickable_elements_hashes: false,
1306
- include_screenshot: false,
1307
- signal,
1308
- });
1309
- const new_selector_map = new_browser_state_summary?.selector_map || {};
1310
- // Detect index change after previous action
1311
- const orig_target = cached_selector_map[currentIndex];
1312
- const orig_target_hash = orig_target?.hash?.branch_path_hash || null;
1313
- const new_target = new_selector_map[currentIndex];
1314
- const new_target_hash = new_target?.hash?.branch_path_hash || null;
1315
- if (orig_target_hash !== new_target_hash) {
1316
- const msg = `Element index changed after action ${i} / ${actions.length}, because page changed.`;
1317
- this.logger.info(msg);
1318
- results.push(new ActionResult({
1319
- extracted_content: msg,
1320
- include_in_memory: true,
1321
- long_term_memory: msg,
1322
- }));
1323
- break;
1324
- }
1325
- // Check for new elements on the page
1326
- const new_path_hashes = new Set(Object.values(new_selector_map)
1327
- .map((e) => e?.hash?.branch_path_hash)
1328
- .filter(Boolean));
1329
- // Check if new elements appeared (new_path_hashes is not a subset of cached_path_hashes)
1330
- const has_new_elements = Array.from(new_path_hashes).some((hash) => !cached_path_hashes.has(hash));
1331
- if (check_for_new_elements && has_new_elements) {
1332
- const msg = `Something new appeared after action ${i} / ${actions.length}, following actions are NOT executed and should be retried.`;
1333
- this.logger.info(msg);
1334
- results.push(new ActionResult({
1335
- extracted_content: msg,
1336
- include_in_memory: true,
1337
- long_term_memory: msg,
1338
- }));
1339
- break;
1340
- }
1341
- }
1342
1999
  // Wait between actions
1343
2000
  const wait_time = this.browser_session?.browser_profile
1344
2001
  ?.wait_between_actions || 0;
@@ -1350,9 +2007,15 @@ export class Agent {
1350
2007
  try {
1351
2008
  this._throwIfAborted(signal);
1352
2009
  await this._raise_if_stopped_or_paused();
2010
+ const preActionPage = await this.browser_session.get_current_page?.();
2011
+ const preActionUrl = typeof preActionPage?.url === 'function' ? preActionPage.url() : '';
2012
+ const preActionFocusTargetId = this.browser_session.agent_focus_target_id ??
2013
+ this.browser_session.active_tab?.page_id ??
2014
+ null;
1353
2015
  const actResult = await this.controller.registry.execute_action(actionName, actionParams, {
1354
2016
  browser_session: this.browser_session,
1355
2017
  page_extraction_llm: this.settings.page_extraction_llm,
2018
+ extraction_schema: this.extraction_schema,
1356
2019
  sensitive_data: this.sensitive_data,
1357
2020
  available_file_paths: this.available_file_paths,
1358
2021
  file_system: this.file_system,
@@ -1369,6 +2032,24 @@ export class Agent {
1369
2032
  this._capture_shared_pinned_tab();
1370
2033
  break;
1371
2034
  }
2035
+ const registeredAction = this.controller.registry.get_action?.(actionName);
2036
+ const terminatesSequence = Boolean(registeredAction?.terminates_sequence);
2037
+ if (terminatesSequence) {
2038
+ this.logger.info(`Action "${actionName}" terminates sequence - skipping ${actions.length - i - 1} remaining action(s)`);
2039
+ this._capture_shared_pinned_tab();
2040
+ break;
2041
+ }
2042
+ const postActionPage = await this.browser_session.get_current_page?.();
2043
+ const postActionUrl = typeof postActionPage?.url === 'function' ? postActionPage.url() : '';
2044
+ const postActionFocusTargetId = this.browser_session.agent_focus_target_id ??
2045
+ this.browser_session.active_tab?.page_id ??
2046
+ null;
2047
+ if (postActionUrl !== preActionUrl ||
2048
+ postActionFocusTargetId !== preActionFocusTargetId) {
2049
+ this.logger.info(`Page changed after "${actionName}" - skipping ${actions.length - i - 1} remaining action(s)`);
2050
+ this._capture_shared_pinned_tab();
2051
+ break;
2052
+ }
1372
2053
  this._capture_shared_pinned_tab();
1373
2054
  }
1374
2055
  catch (error) {
@@ -1380,102 +2061,723 @@ export class Agent {
1380
2061
  }
1381
2062
  return results;
1382
2063
  }
1383
- async rerun_history(history, options = {}) {
1384
- const { max_retries = 3, skip_failures = true, delay_between_actions = 2, signal = null, } = options;
1385
- this._throwIfAborted(signal);
1386
- if (this.initial_actions?.length) {
1387
- const initialResult = await this.multi_act(this.initial_actions, {
1388
- signal,
2064
+ async _generate_rerun_summary(originalTask, results, summaryLlm = null, signal = null) {
2065
+ if (!this.browser_session) {
2066
+ return new ActionResult({
2067
+ is_done: true,
2068
+ success: false,
2069
+ extracted_content: 'Rerun completed without an active browser session.',
2070
+ long_term_memory: 'Rerun completed without an active browser session.',
1389
2071
  });
1390
- this.state.last_result = initialResult;
1391
2072
  }
1392
- const results = [];
1393
- for (let index = 0; index < history.history.length; index++) {
1394
- this._throwIfAborted(signal);
1395
- const historyItem = history.history[index];
1396
- const goal = historyItem.model_output?.current_state?.next_goal ?? '';
1397
- this.logger.info(`Replaying step ${index + 1}/${history.history.length}: goal: ${goal}`);
1398
- const actions = historyItem.model_output?.action ?? [];
1399
- const hasValidAction = actions.length && !actions.every((action) => action == null);
1400
- if (!historyItem.model_output || !hasValidAction) {
1401
- this.logger.warning(`Step ${index + 1}: No action to replay, skipping`);
1402
- results.push(new ActionResult({ error: 'No action to replay' }));
1403
- continue;
1404
- }
1405
- let attempt = 0;
1406
- while (attempt < max_retries) {
1407
- this._throwIfAborted(signal);
1408
- try {
1409
- const stepResult = await this._execute_history_step(historyItem, delay_between_actions, signal);
1410
- results.push(...stepResult);
1411
- break;
1412
- }
1413
- catch (error) {
1414
- if (signal?.aborted ||
1415
- (error instanceof Error && error.name === 'AbortError')) {
1416
- throw this._createAbortError();
1417
- }
1418
- attempt += 1;
1419
- if (attempt === max_retries) {
1420
- const message = `Step ${index + 1} failed after ${max_retries} attempts: ${error.message ?? error}`;
1421
- this.logger.error(message);
1422
- const failure = new ActionResult({ error: message });
1423
- results.push(failure);
1424
- if (!skip_failures) {
1425
- throw new Error(message);
1426
- }
1427
- }
1428
- else {
1429
- this.logger.warning(`Step ${index + 1} failed (attempt ${attempt}/${max_retries}), retrying...`);
1430
- await this._sleep(delay_between_actions, signal);
1431
- }
1432
- }
1433
- }
2073
+ let screenshotB64 = null;
2074
+ try {
2075
+ screenshotB64 = await this.browser_session.take_screenshot(false);
2076
+ }
2077
+ catch (error) {
2078
+ this.logger.warning(`Failed to capture screenshot for rerun summary: ${error instanceof Error ? error.message : String(error)}`);
2079
+ }
2080
+ const errorCount = results.filter((result) => Boolean(result.error)).length;
2081
+ const successCount = results.length - errorCount;
2082
+ const prompt = get_rerun_summary_prompt(originalTask, results.length, successCount, errorCount);
2083
+ const message = get_rerun_summary_message(prompt, screenshotB64);
2084
+ const llm = summaryLlm ?? this.llm;
2085
+ const parser = {
2086
+ parse: (input) => z
2087
+ .object({
2088
+ summary: z.string(),
2089
+ success: z.boolean(),
2090
+ completion_status: z.enum(['complete', 'partial', 'failed']),
2091
+ })
2092
+ .parse(JSON.parse(input)),
2093
+ };
2094
+ try {
2095
+ const response = await llm.ainvoke([message], parser, {
2096
+ signal: signal ?? undefined,
2097
+ });
2098
+ const summary = response.completion;
2099
+ if (!summary ||
2100
+ typeof summary !== 'object' ||
2101
+ typeof summary.summary !== 'string' ||
2102
+ typeof summary.success !== 'boolean' ||
2103
+ !['complete', 'partial', 'failed'].includes(String(summary.completion_status))) {
2104
+ throw new Error('Structured rerun summary response did not match expected schema');
2105
+ }
2106
+ this.logger.info(`Rerun Summary: ${summary.summary}`);
2107
+ this.logger.info(`Rerun Status: ${summary.completion_status} (success=${summary.success})`);
2108
+ return new ActionResult({
2109
+ is_done: true,
2110
+ success: summary.success,
2111
+ extracted_content: summary.summary,
2112
+ long_term_memory: `Rerun completed with status: ${summary.completion_status}. ${summary.summary.slice(0, 100)}`,
2113
+ });
2114
+ }
2115
+ catch (structuredError) {
2116
+ this.logger.debug(`Structured rerun summary failed: ${structuredError instanceof Error
2117
+ ? structuredError.message
2118
+ : String(structuredError)}, falling back to text response`);
2119
+ }
2120
+ try {
2121
+ const response = await llm.ainvoke([message], undefined, {
2122
+ signal: signal ?? undefined,
2123
+ });
2124
+ const summaryText = typeof response.completion === 'string'
2125
+ ? response.completion
2126
+ : JSON.stringify(response.completion);
2127
+ const completionStatus = errorCount === 0 ? 'complete' : successCount > 0 ? 'partial' : 'failed';
2128
+ return new ActionResult({
2129
+ is_done: true,
2130
+ success: errorCount === 0,
2131
+ extracted_content: summaryText,
2132
+ long_term_memory: `Rerun completed with status: ${completionStatus}. ${summaryText.slice(0, 100)}`,
2133
+ });
2134
+ }
2135
+ catch (error) {
2136
+ this.logger.warning(`Failed to generate rerun summary: ${error instanceof Error ? error.message : String(error)}`);
2137
+ return new ActionResult({
2138
+ is_done: true,
2139
+ success: errorCount === 0,
2140
+ extracted_content: `Rerun completed: ${successCount}/${results.length} steps succeeded`,
2141
+ long_term_memory: `Rerun completed: ${successCount} steps succeeded, ${errorCount} errors`,
2142
+ });
1434
2143
  }
1435
- return results;
1436
2144
  }
1437
- async _execute_history_step(historyItem, delaySeconds, signal = null) {
1438
- this._throwIfAborted(signal);
2145
+ async _execute_ai_step(query, includeScreenshot = false, extractLinks = false, aiStepLlm = null, signal = null) {
1439
2146
  if (!this.browser_session) {
1440
- throw new Error('BrowserSession is not set up');
1441
- }
1442
- const browser_state_summary = await this.browser_session.get_browser_state_with_recovery?.({
1443
- cache_clickable_elements_hashes: false,
1444
- include_screenshot: false,
1445
- signal,
1446
- });
1447
- if (!browser_state_summary || !historyItem.model_output) {
1448
- throw new Error('Invalid browser state or model output');
2147
+ return new ActionResult({
2148
+ error: 'AI step failed: BrowserSession missing',
2149
+ });
1449
2150
  }
1450
- const interactedElements = historyItem.state?.interacted_element ?? [];
1451
- const updatedActions = [];
1452
- for (let actionIndex = 0; actionIndex < historyItem.model_output.action.length; actionIndex++) {
1453
- this._throwIfAborted(signal);
1454
- const originalAction = historyItem.model_output.action[actionIndex];
1455
- if (!originalAction) {
1456
- continue;
2151
+ const llm = aiStepLlm ?? this.llm;
2152
+ let content;
2153
+ let statsSummary;
2154
+ let currentUrl = '';
2155
+ try {
2156
+ const page = await this.browser_session.get_current_page?.();
2157
+ if (!page || typeof page.content !== 'function') {
2158
+ throw new Error('No page available for markdown extraction');
1457
2159
  }
1458
- const updatedAction = await this._update_action_indices(this._coerceHistoryElement(interactedElements[actionIndex]), originalAction, browser_state_summary);
2160
+ if (typeof page.url === 'function') {
2161
+ currentUrl = page.url();
2162
+ }
2163
+ const html = (await page.content()) || '';
2164
+ const extracted = extractCleanMarkdownFromHtml(html, {
2165
+ extract_links: extractLinks,
2166
+ });
2167
+ content = extracted.content;
2168
+ const contentStats = extracted.stats;
2169
+ statsSummary = `Content processed: ${contentStats.original_html_chars.toLocaleString()} HTML chars -> ${contentStats.initial_markdown_chars.toLocaleString()} initial markdown -> ${contentStats.final_filtered_chars.toLocaleString()} filtered markdown`;
2170
+ if (contentStats.filtered_chars_removed > 0) {
2171
+ statsSummary += ` (filtered ${contentStats.filtered_chars_removed.toLocaleString()} chars of noise)`;
2172
+ }
2173
+ }
2174
+ catch (error) {
2175
+ const name = error instanceof Error ? error.name : 'Error';
2176
+ const message = error instanceof Error ? error.message : String(error);
2177
+ return new ActionResult({
2178
+ error: `Could not extract clean markdown: ${name}: ${message}`,
2179
+ });
2180
+ }
2181
+ const safeContent = sanitize_surrogates(content);
2182
+ const safeQuery = sanitize_surrogates(query);
2183
+ const systemPrompt = get_ai_step_system_prompt();
2184
+ const userPrompt = get_ai_step_user_prompt(safeQuery, statsSummary, safeContent);
2185
+ let screenshotB64 = null;
2186
+ if (includeScreenshot) {
2187
+ try {
2188
+ screenshotB64 =
2189
+ (await this.browser_session.take_screenshot?.(false)) ?? null;
2190
+ }
2191
+ catch (error) {
2192
+ this.logger.warning(`Failed to capture screenshot for ai_step: ${error instanceof Error ? error.message : String(error)}`);
2193
+ }
2194
+ }
2195
+ const userMessage = screenshotB64
2196
+ ? get_rerun_summary_message(userPrompt, screenshotB64)
2197
+ : new UserMessage(userPrompt);
2198
+ try {
2199
+ const response = await llm.ainvoke([new SystemMessage(systemPrompt), userMessage], undefined, { signal: signal ?? undefined });
2200
+ const completion = typeof response.completion === 'string'
2201
+ ? response.completion
2202
+ : JSON.stringify(response.completion);
2203
+ const extractedContent = `<url>\n${currentUrl}\n</url>\n<query>\n${safeQuery}\n</query>\n<result>\n${completion}\n</result>`;
2204
+ const maxMemoryLength = 1000;
2205
+ if (extractedContent.length < maxMemoryLength) {
2206
+ return new ActionResult({
2207
+ extracted_content: extractedContent,
2208
+ include_extracted_content_only_once: false,
2209
+ long_term_memory: extractedContent,
2210
+ });
2211
+ }
2212
+ if (!this.file_system) {
2213
+ return new ActionResult({
2214
+ extracted_content: extractedContent,
2215
+ include_extracted_content_only_once: false,
2216
+ long_term_memory: extractedContent.slice(0, maxMemoryLength),
2217
+ });
2218
+ }
2219
+ const fileName = await this.file_system.save_extracted_content(extractedContent);
2220
+ return new ActionResult({
2221
+ extracted_content: extractedContent,
2222
+ include_extracted_content_only_once: true,
2223
+ long_term_memory: `Query: ${query}\nContent in ${fileName} and once in <read_state>.`,
2224
+ });
2225
+ }
2226
+ catch (error) {
2227
+ this.logger.warning(`Failed to execute AI step: ${error instanceof Error ? error.message : String(error)}`);
2228
+ return new ActionResult({
2229
+ error: `AI step failed: ${error instanceof Error ? error.message : String(error)}`,
2230
+ });
2231
+ }
2232
+ }
2233
+ async rerun_history(history, options = {}) {
2234
+ const { max_retries = 3, skip_failures = false, delay_between_actions = 2, max_step_interval = 45, wait_for_elements = false, summary_llm = null, ai_step_llm = null, signal = null, } = options;
2235
+ this._throwIfAborted(signal);
2236
+ // Mirror python c011 behavior: rerun should not emit create-session events.
2237
+ this.state.session_initialized = true;
2238
+ const results = [];
2239
+ let previousItem = null;
2240
+ let previousStepSucceeded = false;
2241
+ try {
2242
+ await this.browser_session?.start();
2243
+ for (let index = 0; index < history.history.length; index++) {
2244
+ this._throwIfAborted(signal);
2245
+ const historyItem = history.history[index];
2246
+ const goal = historyItem.model_output?.current_state?.next_goal ?? '';
2247
+ const stepNumber = historyItem.metadata?.step_number ?? index;
2248
+ const stepName = stepNumber === 0 ? 'Initial actions' : `Step ${stepNumber}`;
2249
+ const savedInterval = historyItem.metadata?.step_interval;
2250
+ let stepDelay = delay_between_actions;
2251
+ let delaySource = `using default delay=${this._formatDelaySeconds(stepDelay)}`;
2252
+ if (typeof savedInterval === 'number' &&
2253
+ Number.isFinite(savedInterval)) {
2254
+ stepDelay = Math.min(savedInterval, max_step_interval);
2255
+ if (savedInterval > max_step_interval) {
2256
+ delaySource = `capped to ${this._formatDelaySeconds(stepDelay)} (saved was ${savedInterval.toFixed(1)}s)`;
2257
+ }
2258
+ else {
2259
+ delaySource = `using saved step_interval=${this._formatDelaySeconds(stepDelay)}`;
2260
+ }
2261
+ }
2262
+ this.logger.info(`Replaying ${stepName} (${index + 1}/${history.history.length}) [${delaySource}]: ${goal}`);
2263
+ const actions = historyItem.model_output?.action ?? [];
2264
+ const hasValidAction = actions.length && !actions.every((action) => action == null);
2265
+ if (!historyItem.model_output || !hasValidAction) {
2266
+ this.logger.warning(`${stepName}: No action to replay, skipping`);
2267
+ results.push(new ActionResult({ error: 'No action to replay' }));
2268
+ continue;
2269
+ }
2270
+ const originalErrors = Array.isArray(historyItem.result)
2271
+ ? historyItem.result
2272
+ .map((result) => result?.error)
2273
+ .filter((error) => typeof error === 'string')
2274
+ : [];
2275
+ if (originalErrors.length && skip_failures) {
2276
+ const firstError = originalErrors[0] ?? 'unknown';
2277
+ const preview = firstError.length > 100
2278
+ ? `${firstError.slice(0, 100)}...`
2279
+ : firstError;
2280
+ this.logger.warning(`${stepName}: Original step had error(s), skipping (skip_failures=true): ${preview}`);
2281
+ results.push(new ActionResult({
2282
+ error: `Skipped - original step had error: ${preview}`,
2283
+ }));
2284
+ continue;
2285
+ }
2286
+ if (this._is_redundant_retry_step(historyItem, previousItem, previousStepSucceeded)) {
2287
+ this.logger.info(`${stepName}: Skipping redundant retry (previous step already succeeded with same element)`);
2288
+ results.push(new ActionResult({
2289
+ extracted_content: 'Skipped - redundant retry of previous step',
2290
+ include_in_memory: false,
2291
+ }));
2292
+ continue;
2293
+ }
2294
+ let attempt = 0;
2295
+ let stepSucceeded = false;
2296
+ let menuReopened = false;
2297
+ while (attempt < max_retries) {
2298
+ this._throwIfAborted(signal);
2299
+ try {
2300
+ const stepResult = ai_step_llm != null
2301
+ ? await this._execute_history_step(historyItem, stepDelay, signal, wait_for_elements, ai_step_llm)
2302
+ : await this._execute_history_step(historyItem, stepDelay, signal, wait_for_elements);
2303
+ results.push(...stepResult);
2304
+ stepSucceeded = true;
2305
+ break;
2306
+ }
2307
+ catch (error) {
2308
+ const errorMessage = error instanceof Error ? error.message : String(error);
2309
+ if (signal?.aborted ||
2310
+ (error instanceof Error && error.name === 'AbortError')) {
2311
+ throw this._createAbortError();
2312
+ }
2313
+ attempt += 1;
2314
+ if (!menuReopened &&
2315
+ errorMessage.includes('Could not find matching element') &&
2316
+ previousItem &&
2317
+ this._is_menu_opener_step(previousItem)) {
2318
+ const currentElement = this._coerceHistoryElement(historyItem.state?.interacted_element?.[0]);
2319
+ if (this._is_menu_item_element(currentElement)) {
2320
+ this.logger.info('Dropdown may have closed. Attempting to re-open by re-executing previous step...');
2321
+ const reopened = await this._reexecute_menu_opener(previousItem, signal, ai_step_llm);
2322
+ if (reopened) {
2323
+ menuReopened = true;
2324
+ attempt -= 1;
2325
+ stepDelay = 0.5;
2326
+ this.logger.info('Dropdown re-opened, retrying element match...');
2327
+ continue;
2328
+ }
2329
+ }
2330
+ }
2331
+ if (attempt === max_retries) {
2332
+ const message = `${stepName} failed after ${max_retries} attempts: ${errorMessage}`;
2333
+ this.logger.error(message);
2334
+ const failure = new ActionResult({ error: message });
2335
+ results.push(failure);
2336
+ if (!skip_failures) {
2337
+ throw new Error(message, { cause: error });
2338
+ }
2339
+ }
2340
+ else {
2341
+ const retryDelay = Math.min(5 * 2 ** Math.max(attempt - 1, 0), 30);
2342
+ this.logger.warning(`${stepName} failed (attempt ${attempt}/${max_retries}), retrying in ${retryDelay}s...`);
2343
+ await this._sleep(retryDelay, signal);
2344
+ }
2345
+ }
2346
+ }
2347
+ previousItem = historyItem;
2348
+ previousStepSucceeded = stepSucceeded;
2349
+ }
2350
+ const summaryResult = await this._generate_rerun_summary(this.task, results, summary_llm, signal);
2351
+ results.push(summaryResult);
2352
+ return results;
2353
+ }
2354
+ finally {
2355
+ await this.close();
2356
+ }
2357
+ }
2358
+ async _execute_history_step(historyItem, delaySeconds, signal = null, wait_for_elements = false, ai_step_llm = null) {
2359
+ this._throwIfAborted(signal);
2360
+ if (!this.browser_session) {
2361
+ throw new Error('BrowserSession is not set up');
2362
+ }
2363
+ await this._sleep(delaySeconds, signal);
2364
+ const interactedElements = historyItem.state?.interacted_element ?? [];
2365
+ let browser_state_summary = null;
2366
+ if (wait_for_elements) {
2367
+ const needsElementMatching = this._historyStepNeedsElementMatching(historyItem, interactedElements);
2368
+ if (needsElementMatching) {
2369
+ const minElements = this._countExpectedElementsFromHistory(historyItem);
2370
+ if (minElements > 0) {
2371
+ browser_state_summary = await this._waitForMinimumElements(minElements, 15, 1, signal);
2372
+ }
2373
+ }
2374
+ }
2375
+ if (!browser_state_summary) {
2376
+ browser_state_summary =
2377
+ await this.browser_session.get_browser_state_with_recovery?.({
2378
+ cache_clickable_elements_hashes: false,
2379
+ include_screenshot: false,
2380
+ signal,
2381
+ });
2382
+ }
2383
+ if (!browser_state_summary || !historyItem.model_output) {
2384
+ throw new Error('Invalid browser state or model output');
2385
+ }
2386
+ const results = [];
2387
+ const pendingActions = [];
2388
+ for (let actionIndex = 0; actionIndex < historyItem.model_output.action.length; actionIndex++) {
2389
+ this._throwIfAborted(signal);
2390
+ const originalAction = historyItem.model_output.action[actionIndex];
2391
+ if (!originalAction) {
2392
+ continue;
2393
+ }
2394
+ const actionPayload = typeof originalAction?.model_dump === 'function'
2395
+ ? originalAction.model_dump({ exclude_unset: true })
2396
+ : originalAction;
2397
+ const actionName = Object.keys(actionPayload ?? {})[0] ?? null;
2398
+ if (actionName &&
2399
+ ['extract', 'extract_structured_data', 'extract_content'].includes(actionName)) {
2400
+ if (pendingActions.length > 0) {
2401
+ this._throwIfAborted(signal);
2402
+ const batchActions = [...pendingActions];
2403
+ pendingActions.length = 0;
2404
+ const batchResults = await this.multi_act(batchActions, { signal });
2405
+ results.push(...batchResults);
2406
+ }
2407
+ const params = actionPayload[actionName] ?? {};
2408
+ const query = typeof params.query === 'string' ? params.query : '';
2409
+ const extractLinks = Boolean(params.extract_links);
2410
+ this.logger.info(`Using AI step for extract action: ${query.slice(0, 50)}...`);
2411
+ const aiResult = await this._execute_ai_step(query, false, extractLinks, ai_step_llm, signal);
2412
+ results.push(aiResult);
2413
+ continue;
2414
+ }
2415
+ const updatedAction = await this._update_action_indices(this._coerceHistoryElement(interactedElements[actionIndex]), originalAction, browser_state_summary);
1459
2416
  if (!updatedAction) {
1460
- throw new Error(`Could not find matching element ${actionIndex} in current page`);
2417
+ const historicalElement = this._coerceHistoryElement(interactedElements[actionIndex]);
2418
+ const selectorCount = Object.keys(browser_state_summary.selector_map ?? {}).length;
2419
+ throw new Error(`Could not find matching element for action ${actionIndex} in current page.\n` +
2420
+ ` Looking for: ${this._formatHistoryElementForError(historicalElement)}\n` +
2421
+ ` Page has ${selectorCount} interactive elements.\n` +
2422
+ ' Tried: EXACT hash → STABLE hash → XPATH → AX_NAME → ATTRIBUTE matching');
1461
2423
  }
1462
2424
  if (typeof updatedAction?.model_dump === 'function') {
1463
- updatedActions.push(updatedAction.model_dump({ exclude_unset: true }));
2425
+ pendingActions.push(updatedAction.model_dump({ exclude_unset: true }));
1464
2426
  }
1465
2427
  else {
1466
- updatedActions.push(updatedAction);
2428
+ pendingActions.push(updatedAction);
1467
2429
  }
1468
2430
  }
1469
- this._throwIfAborted(signal);
1470
- const result = await this.multi_act(updatedActions, { signal });
1471
- await this._sleep(delaySeconds, signal);
1472
- return result;
2431
+ if (pendingActions.length > 0) {
2432
+ this._throwIfAborted(signal);
2433
+ const batchActions = [...pendingActions];
2434
+ pendingActions.length = 0;
2435
+ const batchResults = await this.multi_act(batchActions, { signal });
2436
+ results.push(...batchResults);
2437
+ }
2438
+ return results;
2439
+ }
2440
+ _historyStepNeedsElementMatching(historyItem, interactedElements) {
2441
+ const actions = historyItem.model_output?.action ?? [];
2442
+ for (let index = 0; index < actions.length; index++) {
2443
+ const action = actions[index];
2444
+ if (!action) {
2445
+ continue;
2446
+ }
2447
+ const payload = typeof action.model_dump === 'function'
2448
+ ? action.model_dump({ exclude_unset: true })
2449
+ : action;
2450
+ const actionName = Object.keys(payload ?? {})[0] ?? null;
2451
+ if (!actionName) {
2452
+ continue;
2453
+ }
2454
+ if ([
2455
+ 'click',
2456
+ 'input',
2457
+ 'input_text',
2458
+ 'hover',
2459
+ 'select_option',
2460
+ 'select_dropdown_option',
2461
+ 'drag_and_drop',
2462
+ ].includes(actionName)) {
2463
+ const historicalElement = this._coerceHistoryElement(interactedElements[index] ?? null);
2464
+ if (historicalElement) {
2465
+ return true;
2466
+ }
2467
+ }
2468
+ }
2469
+ return false;
2470
+ }
2471
+ _countExpectedElementsFromHistory(historyItem) {
2472
+ if (!historyItem.model_output?.action?.length) {
2473
+ return 0;
2474
+ }
2475
+ let maxIndex = -1;
2476
+ for (const action of historyItem.model_output.action) {
2477
+ const index = this._extractActionIndex(action);
2478
+ if (index != null) {
2479
+ maxIndex = Math.max(maxIndex, index);
2480
+ }
2481
+ }
2482
+ if (maxIndex < 0) {
2483
+ return 0;
2484
+ }
2485
+ return Math.min(maxIndex + 1, 50);
2486
+ }
2487
+ async _waitForMinimumElements(minElements, timeoutSeconds = 30, pollIntervalSeconds = 1, signal = null) {
2488
+ if (!this.browser_session) {
2489
+ return null;
2490
+ }
2491
+ const start = Date.now();
2492
+ let lastCount = 0;
2493
+ let lastState = null;
2494
+ while ((Date.now() - start) / 1000 < timeoutSeconds) {
2495
+ this._throwIfAborted(signal);
2496
+ const state = await this.browser_session.get_browser_state_with_recovery?.({
2497
+ cache_clickable_elements_hashes: false,
2498
+ include_screenshot: false,
2499
+ signal,
2500
+ });
2501
+ lastState = state ?? null;
2502
+ const currentCount = Object.keys(state?.selector_map ?? {}).length;
2503
+ if (currentCount >= minElements) {
2504
+ this.logger.debug(`Page has ${currentCount} interactive elements (needed ${minElements}), proceeding`);
2505
+ return state;
2506
+ }
2507
+ if (currentCount !== lastCount) {
2508
+ const remaining = Math.max(0, timeoutSeconds - (Date.now() - start) / 1000);
2509
+ this.logger.debug(`Waiting for elements: ${currentCount}/${minElements} (timeout in ${remaining.toFixed(1)}s)`);
2510
+ lastCount = currentCount;
2511
+ }
2512
+ await this._sleep(pollIntervalSeconds, signal);
2513
+ }
2514
+ this.logger.warning(`Timeout waiting for ${minElements} elements, proceeding with ${lastCount} elements`);
2515
+ return lastState;
2516
+ }
2517
+ _extractActionIndex(action) {
2518
+ if (action && typeof action.get_index === 'function') {
2519
+ const index = action.get_index();
2520
+ if (typeof index === 'number' && Number.isFinite(index)) {
2521
+ return index;
2522
+ }
2523
+ }
2524
+ if (!action || typeof action !== 'object') {
2525
+ return null;
2526
+ }
2527
+ const modelDump = typeof action.model_dump === 'function'
2528
+ ? action.model_dump()
2529
+ : action;
2530
+ if (!modelDump ||
2531
+ typeof modelDump !== 'object' ||
2532
+ Array.isArray(modelDump)) {
2533
+ return null;
2534
+ }
2535
+ const actionName = Object.keys(modelDump)[0];
2536
+ if (!actionName) {
2537
+ return null;
2538
+ }
2539
+ const params = modelDump[actionName];
2540
+ const index = params?.index;
2541
+ return typeof index === 'number' && Number.isFinite(index) ? index : null;
2542
+ }
2543
+ _extractActionType(action) {
2544
+ if (!action || typeof action !== 'object') {
2545
+ return null;
2546
+ }
2547
+ const modelDump = typeof action.model_dump === 'function'
2548
+ ? action.model_dump()
2549
+ : action;
2550
+ if (!modelDump ||
2551
+ typeof modelDump !== 'object' ||
2552
+ Array.isArray(modelDump)) {
2553
+ return null;
2554
+ }
2555
+ const actionName = Object.keys(modelDump)[0];
2556
+ return actionName ?? null;
2557
+ }
2558
+ _sameHistoryElement(current, previous) {
2559
+ if (!current || !previous) {
2560
+ return false;
2561
+ }
2562
+ if (current.element_hash &&
2563
+ previous.element_hash &&
2564
+ current.element_hash === previous.element_hash) {
2565
+ return true;
2566
+ }
2567
+ if (current.stable_hash &&
2568
+ previous.stable_hash &&
2569
+ current.stable_hash === previous.stable_hash) {
2570
+ return true;
2571
+ }
2572
+ if (current.xpath && previous.xpath && current.xpath === previous.xpath) {
2573
+ return true;
2574
+ }
2575
+ if (current.tag_name &&
2576
+ previous.tag_name &&
2577
+ current.tag_name === previous.tag_name) {
2578
+ for (const key of ['name', 'id', 'aria-label']) {
2579
+ const currentValue = current.attributes?.[key];
2580
+ const previousValue = previous.attributes?.[key];
2581
+ if (currentValue &&
2582
+ previousValue &&
2583
+ String(currentValue) === String(previousValue)) {
2584
+ return true;
2585
+ }
2586
+ }
2587
+ }
2588
+ return false;
2589
+ }
2590
+ _is_redundant_retry_step(currentItem, previousItem, previousStepSucceeded) {
2591
+ if (!previousItem || !previousStepSucceeded) {
2592
+ return false;
2593
+ }
2594
+ const currentActions = currentItem.model_output?.action ?? [];
2595
+ const previousActions = previousItem.model_output?.action ?? [];
2596
+ if (!currentActions.length || !previousActions.length) {
2597
+ return false;
2598
+ }
2599
+ const currentActionType = this._extractActionType(currentActions[0]);
2600
+ const previousActionType = this._extractActionType(previousActions[0]);
2601
+ if (!currentActionType || currentActionType !== previousActionType) {
2602
+ return false;
2603
+ }
2604
+ const currentElement = this._coerceHistoryElement(currentItem.state?.interacted_element?.[0]);
2605
+ const previousElement = this._coerceHistoryElement(previousItem.state?.interacted_element?.[0]);
2606
+ if (!this._sameHistoryElement(currentElement, previousElement)) {
2607
+ return false;
2608
+ }
2609
+ this.logger.debug(`Detected redundant retry on same element with action "${currentActionType}"`);
2610
+ return true;
2611
+ }
2612
+ _is_menu_opener_step(historyItem) {
2613
+ const element = this._coerceHistoryElement(historyItem?.state?.interacted_element?.[0]);
2614
+ if (!element) {
2615
+ return false;
2616
+ }
2617
+ const attrs = element.attributes ?? {};
2618
+ if (['true', 'menu', 'listbox'].includes(String(attrs['aria-haspopup']))) {
2619
+ return true;
2620
+ }
2621
+ if (attrs['data-gw-click'] === 'toggleSubMenu') {
2622
+ return true;
2623
+ }
2624
+ if (String(attrs.class ?? '').includes('expand-button')) {
2625
+ return true;
2626
+ }
2627
+ if (attrs.role === 'menuitem' &&
2628
+ ['false', 'true'].includes(String(attrs['aria-expanded']))) {
2629
+ return true;
2630
+ }
2631
+ if (attrs.role === 'button' &&
2632
+ ['false', 'true'].includes(String(attrs['aria-expanded']))) {
2633
+ return true;
2634
+ }
2635
+ return false;
2636
+ }
2637
+ _is_menu_item_element(element) {
2638
+ if (!element) {
2639
+ return false;
2640
+ }
2641
+ const attrs = element.attributes ?? {};
2642
+ const role = String(attrs.role ?? '');
2643
+ if ([
2644
+ 'menuitem',
2645
+ 'option',
2646
+ 'menuitemcheckbox',
2647
+ 'menuitemradio',
2648
+ 'treeitem',
2649
+ ].includes(role)) {
2650
+ return true;
2651
+ }
2652
+ const className = String(attrs.class ?? '');
2653
+ if (className.includes('gw-action--inner')) {
2654
+ return true;
2655
+ }
2656
+ if (className.toLowerCase().includes('menuitem')) {
2657
+ return true;
2658
+ }
2659
+ if (element.ax_name && element.ax_name.trim()) {
2660
+ const lowered = className.toLowerCase();
2661
+ if (['dropdown', 'popup', 'menu', 'submenu', 'action'].some((needle) => lowered.includes(needle))) {
2662
+ return true;
2663
+ }
2664
+ }
2665
+ return false;
2666
+ }
2667
+ async _reexecute_menu_opener(openerItem, signal = null, aiStepLlm = null) {
2668
+ try {
2669
+ this.logger.info('Re-opening dropdown/menu by re-executing previous step...');
2670
+ await this._execute_history_step(openerItem, 0.5, signal, false, aiStepLlm);
2671
+ await this._sleep(0.3, signal);
2672
+ return true;
2673
+ }
2674
+ catch (error) {
2675
+ this.logger.warning(`Failed to re-open dropdown: ${error instanceof Error ? error.message : String(error)}`);
2676
+ return false;
2677
+ }
2678
+ }
2679
+ _formatHistoryElementForError(element) {
2680
+ if (!element) {
2681
+ return '<no element recorded>';
2682
+ }
2683
+ const parts = [`<${element.tag_name || 'unknown'}>`];
2684
+ for (const key of ['name', 'id', 'aria-label', 'type']) {
2685
+ const value = element.attributes?.[key];
2686
+ if (typeof value === 'string' && value.trim()) {
2687
+ parts.push(`${key}="${value}"`);
2688
+ }
2689
+ }
2690
+ if (element.xpath) {
2691
+ const xpath = element.xpath.length > 60
2692
+ ? `...${element.xpath.slice(-57)}`
2693
+ : element.xpath;
2694
+ parts.push(`xpath="${xpath}"`);
2695
+ }
2696
+ if (element.element_hash) {
2697
+ parts.push(`hash=${element.element_hash}`);
2698
+ }
2699
+ if (element.stable_hash) {
2700
+ parts.push(`stable_hash=${element.stable_hash}`);
2701
+ }
2702
+ return parts.join(' ');
1473
2703
  }
1474
2704
  async _update_action_indices(historicalElement, action, browserStateSummary) {
1475
- if (!historicalElement || !browserStateSummary?.element_tree) {
2705
+ if (!historicalElement || !browserStateSummary?.selector_map) {
1476
2706
  return action;
1477
2707
  }
1478
- const currentNode = HistoryTreeProcessor.find_history_element_in_tree(historicalElement, browserStateSummary.element_tree);
2708
+ const selectorMap = browserStateSummary.selector_map ?? {};
2709
+ if (!Object.keys(selectorMap).length) {
2710
+ return action;
2711
+ }
2712
+ let matchLevel = null;
2713
+ let currentNode = null;
2714
+ if (historicalElement.element_hash) {
2715
+ for (const node of Object.values(selectorMap)) {
2716
+ const nodeHash = HistoryTreeProcessor.compute_element_hash(node);
2717
+ if (nodeHash === historicalElement.element_hash) {
2718
+ currentNode = node;
2719
+ matchLevel = 'EXACT';
2720
+ break;
2721
+ }
2722
+ }
2723
+ }
2724
+ if (!currentNode && historicalElement.stable_hash) {
2725
+ for (const node of Object.values(selectorMap)) {
2726
+ const stableHash = HistoryTreeProcessor.compute_stable_hash(node);
2727
+ if (stableHash === historicalElement.stable_hash) {
2728
+ currentNode = node;
2729
+ matchLevel = 'STABLE';
2730
+ this.logger.info('Element matched at STABLE hash fallback');
2731
+ break;
2732
+ }
2733
+ }
2734
+ }
2735
+ if (!currentNode && historicalElement.xpath) {
2736
+ for (const node of Object.values(selectorMap)) {
2737
+ if (node?.xpath === historicalElement.xpath) {
2738
+ currentNode = node;
2739
+ matchLevel = 'XPATH';
2740
+ this.logger.info(`Element matched at XPATH fallback: ${historicalElement.xpath}`);
2741
+ break;
2742
+ }
2743
+ }
2744
+ }
2745
+ if (!currentNode && historicalElement.ax_name) {
2746
+ const tagName = historicalElement.tag_name?.toLowerCase();
2747
+ const targetAxName = historicalElement.ax_name;
2748
+ for (const node of Object.values(selectorMap)) {
2749
+ const nodeAxName = HistoryTreeProcessor.get_accessible_name(node);
2750
+ if (node?.tag_name?.toLowerCase() === tagName &&
2751
+ typeof nodeAxName === 'string' &&
2752
+ nodeAxName === targetAxName) {
2753
+ currentNode = node;
2754
+ matchLevel = 'AX_NAME';
2755
+ this.logger.info(`Element matched at AX_NAME fallback: ${targetAxName}`);
2756
+ break;
2757
+ }
2758
+ }
2759
+ }
2760
+ if (!currentNode && historicalElement.attributes) {
2761
+ const tagName = historicalElement.tag_name?.toLowerCase();
2762
+ for (const attrKey of ['name', 'id', 'aria-label']) {
2763
+ const attrValue = historicalElement.attributes[attrKey];
2764
+ if (!attrValue) {
2765
+ continue;
2766
+ }
2767
+ for (const node of Object.values(selectorMap)) {
2768
+ if (node?.tag_name?.toLowerCase() === tagName &&
2769
+ node?.attributes?.[attrKey] === attrValue) {
2770
+ currentNode = node;
2771
+ matchLevel = 'ATTRIBUTE';
2772
+ this.logger.info(`Element matched via ${attrKey} attribute fallback: ${attrValue}`);
2773
+ break;
2774
+ }
2775
+ }
2776
+ if (currentNode) {
2777
+ break;
2778
+ }
2779
+ }
2780
+ }
1479
2781
  if (!currentNode || currentNode.highlight_index == null) {
1480
2782
  return null;
1481
2783
  }
@@ -1483,18 +2785,25 @@ export class Agent {
1483
2785
  if (currentIndex !== currentNode.highlight_index &&
1484
2786
  typeof action?.set_index === 'function') {
1485
2787
  action.set_index(currentNode.highlight_index);
1486
- this.logger.info(`Element moved in DOM, updated index from ${currentIndex} to ${currentNode.highlight_index}`);
2788
+ this.logger.info(`Element moved in DOM, updated index from ${currentIndex} to ${currentNode.highlight_index} (matched at ${matchLevel ?? 'UNKNOWN'} level)`);
1487
2789
  }
1488
2790
  return action;
1489
2791
  }
1490
2792
  async load_and_rerun(history_file = null, options = {}) {
2793
+ const { variables = null, ...rerunOptions } = options;
1491
2794
  const target = history_file ?? 'AgentHistory.json';
1492
2795
  const history = AgentHistoryList.load_from_file(target, this.AgentOutput);
1493
- return this.rerun_history(history, options);
2796
+ const substitutedHistory = variables
2797
+ ? this._substitute_variables_in_history(history, variables)
2798
+ : history;
2799
+ return this.rerun_history(substitutedHistory, rerunOptions);
2800
+ }
2801
+ detect_variables() {
2802
+ return detect_variables_in_history(this.history);
1494
2803
  }
1495
2804
  save_history(file_path = null) {
1496
2805
  const target = file_path ?? 'AgentHistory.json';
1497
- this.history.save_to_file(target);
2806
+ this.history.save_to_file(target, this.sensitive_data ?? null);
1498
2807
  }
1499
2808
  _coerceHistoryElement(element) {
1500
2809
  if (!element) {
@@ -1504,7 +2813,69 @@ export class Agent {
1504
2813
  return element;
1505
2814
  }
1506
2815
  const payload = element;
1507
- return new DOMHistoryElement(payload.tag_name ?? '', payload.xpath ?? '', payload.highlight_index ?? null, payload.entire_parent_branch_path ?? [], payload.attributes ?? {}, payload.shadow_root ?? false, payload.css_selector ?? null, payload.page_coordinates ?? null, payload.viewport_coordinates ?? null, payload.viewport_info ?? null);
2816
+ return new DOMHistoryElement(payload.tag_name ?? '', payload.xpath ?? '', payload.highlight_index ?? null, payload.entire_parent_branch_path ?? [], payload.attributes ?? {}, payload.shadow_root ?? false, payload.css_selector ?? null, payload.page_coordinates ?? null, payload.viewport_coordinates ?? null, payload.viewport_info ?? null, payload.element_hash != null ? String(payload.element_hash) : null, payload.stable_hash != null ? String(payload.stable_hash) : null, payload.ax_name != null ? String(payload.ax_name) : null);
2817
+ }
2818
+ _substitute_variables_in_history(history, variables) {
2819
+ const detectedVars = detect_variables_in_history(history);
2820
+ const valueReplacements = {};
2821
+ for (const [varName, newValue] of Object.entries(variables)) {
2822
+ const detected = detectedVars[varName];
2823
+ if (!detected) {
2824
+ this.logger.warning(`Variable "${varName}" not found in history, skipping substitution`);
2825
+ continue;
2826
+ }
2827
+ valueReplacements[detected.original_value] = newValue;
2828
+ }
2829
+ if (!Object.keys(valueReplacements).length) {
2830
+ this.logger.info('No variables to substitute');
2831
+ return history;
2832
+ }
2833
+ const clonedHistory = this._clone_history_for_substitution(history);
2834
+ let substitutionCount = 0;
2835
+ for (const historyItem of clonedHistory.history) {
2836
+ if (!historyItem.model_output?.action?.length) {
2837
+ continue;
2838
+ }
2839
+ for (let actionIndex = 0; actionIndex < historyItem.model_output.action.length; actionIndex += 1) {
2840
+ const action = historyItem.model_output.action[actionIndex];
2841
+ const actionPayload = typeof action.model_dump === 'function'
2842
+ ? action.model_dump()
2843
+ : action;
2844
+ if (!actionPayload ||
2845
+ typeof actionPayload !== 'object' ||
2846
+ Array.isArray(actionPayload)) {
2847
+ continue;
2848
+ }
2849
+ substitutionCount += substitute_in_dict(actionPayload, valueReplacements);
2850
+ const ActionCtor = action?.constructor;
2851
+ if (typeof ActionCtor === 'function') {
2852
+ historyItem.model_output.action[actionIndex] = new ActionCtor(actionPayload);
2853
+ }
2854
+ else {
2855
+ historyItem.model_output.action[actionIndex] = actionPayload;
2856
+ }
2857
+ }
2858
+ }
2859
+ this.logger.info(`Substituted ${substitutionCount} value(s) in ${Object.keys(valueReplacements).length} variable type(s) in history`);
2860
+ return clonedHistory;
2861
+ }
2862
+ _clone_history_for_substitution(history) {
2863
+ const payload = history.toJSON();
2864
+ const historyItems = (payload.history ?? []).map((entry) => {
2865
+ const modelOutput = entry.model_output
2866
+ ? this.AgentOutput.fromJSON(entry.model_output)
2867
+ : null;
2868
+ const result = (entry.result ?? []).map((item) => new ActionResult(item));
2869
+ const interacted = Array.isArray(entry.state?.interacted_element)
2870
+ ? entry.state.interacted_element.map((element) => this._coerceHistoryElement(element))
2871
+ : [];
2872
+ const state = new BrowserStateHistory(entry.state?.url ?? '', entry.state?.title ?? '', entry.state?.tabs ?? [], interacted, entry.state?.screenshot_path ?? null);
2873
+ const metadata = entry.metadata
2874
+ ? new StepMetadata(entry.metadata.step_start_time, entry.metadata.step_end_time, entry.metadata.step_number, entry.metadata.step_interval ?? null)
2875
+ : null;
2876
+ return new AgentHistory(modelOutput, result, state, metadata, entry.state_message ?? null);
2877
+ });
2878
+ return new AgentHistoryList(historyItems, history.usage ?? null);
1508
2879
  }
1509
2880
  _createAbortError() {
1510
2881
  const error = new Error('Operation aborted');
@@ -1528,6 +2899,12 @@ export class Agent {
1528
2899
  signal.addEventListener('abort', handleAbort, { once: true });
1529
2900
  return () => signal.removeEventListener('abort', handleAbort);
1530
2901
  }
2902
+ _formatDelaySeconds(delaySeconds) {
2903
+ if (delaySeconds < 1) {
2904
+ return `${Math.round(delaySeconds * 1000)}ms`;
2905
+ }
2906
+ return `${delaySeconds.toFixed(1)}s`;
2907
+ }
1531
2908
  async _sleep(seconds, signal = null) {
1532
2909
  if (seconds <= 0) {
1533
2910
  return;
@@ -1595,28 +2972,37 @@ export class Agent {
1595
2972
  await this._closePromise;
1596
2973
  return;
1597
2974
  }
1598
- const browser_session = this.browser_session;
1599
- if (!browser_session) {
1600
- return;
1601
- }
1602
2975
  this._closePromise = (async () => {
1603
- this._release_browser_session_claim(browser_session);
1604
- if (this._has_any_browser_session_attachments(browser_session)) {
1605
- this.logger.debug('Skipping BrowserSession shutdown because other attached Agents are still active.');
1606
- return;
1607
- }
1608
- this._cleanup_shared_session_step_lock_if_unused(browser_session);
2976
+ const browser_session = this.browser_session;
1609
2977
  try {
1610
- if (typeof browser_session.stop === 'function') {
1611
- await browser_session.stop();
1612
- }
1613
- else if (typeof browser_session.close === 'function') {
1614
- await browser_session.close();
2978
+ if (browser_session) {
2979
+ this._release_browser_session_claim(browser_session);
2980
+ if (this._has_any_browser_session_attachments(browser_session)) {
2981
+ this.logger.debug('Skipping BrowserSession shutdown because other attached Agents are still active.');
2982
+ }
2983
+ else {
2984
+ this._cleanup_shared_session_step_lock_if_unused(browser_session);
2985
+ if (typeof browser_session.stop === 'function') {
2986
+ await browser_session.stop();
2987
+ }
2988
+ else if (typeof browser_session.close === 'function') {
2989
+ await browser_session.close();
2990
+ }
2991
+ }
1615
2992
  }
1616
2993
  }
1617
2994
  catch (error) {
1618
2995
  this.logger.error(`Error during agent cleanup: ${error instanceof Error ? error.message : String(error)}`);
1619
2996
  }
2997
+ if (this.skill_service &&
2998
+ typeof this.skill_service.close === 'function') {
2999
+ try {
3000
+ await this.skill_service.close();
3001
+ }
3002
+ catch (error) {
3003
+ this.logger.error(`Error during skill service cleanup: ${error instanceof Error ? error.message : String(error)}`);
3004
+ }
3005
+ }
1620
3006
  })();
1621
3007
  await this._closePromise;
1622
3008
  }
@@ -1700,15 +3086,43 @@ export class Agent {
1700
3086
  };
1701
3087
  return { trace, trace_details };
1702
3088
  }
1703
- _log_agent_run() {
1704
- this.logger.info(`🧠 Starting agent for task: ${this.task}`);
3089
+ async _log_agent_run() {
3090
+ this.logger.info(`\x1b[34m🎯 Task: ${this.task}\x1b[0m`);
3091
+ this.logger.debug(`🤖 Browser-Use Library Version ${this.version} (${this.source})`);
3092
+ if (CONFIG.BROWSER_USE_VERSION_CHECK &&
3093
+ process.env.NODE_ENV !== 'test' &&
3094
+ !process.env.VITEST) {
3095
+ const latestVersion = await check_latest_browser_use_version();
3096
+ if (latestVersion && latestVersion !== this.version) {
3097
+ this.logger.info(`📦 Newer version available: ${latestVersion} (current: ${this.version}). Upgrade with: npm install browser-use@${latestVersion}`);
3098
+ }
3099
+ }
1705
3100
  }
1706
- _raise_if_stopped_or_paused() {
3101
+ _createInterruptedError(message = '') {
3102
+ const interruptedError = new Error(message);
3103
+ interruptedError.name = 'InterruptedError';
3104
+ return interruptedError;
3105
+ }
3106
+ async _raise_if_stopped_or_paused() {
3107
+ if (this.register_should_stop_callback) {
3108
+ const shouldStop = await this.register_should_stop_callback();
3109
+ if (shouldStop) {
3110
+ this.logger.info('External callback requested stop');
3111
+ this.state.stopped = true;
3112
+ throw this._createInterruptedError();
3113
+ }
3114
+ }
3115
+ if (this.register_external_agent_status_raise_error_callback) {
3116
+ const shouldRaise = await this.register_external_agent_status_raise_error_callback();
3117
+ if (shouldRaise) {
3118
+ throw this._createInterruptedError();
3119
+ }
3120
+ }
1707
3121
  if (this.state.stopped) {
1708
- throw new Error('Agent stopped');
3122
+ throw this._createInterruptedError('Agent stopped');
1709
3123
  }
1710
3124
  if (this.state.paused) {
1711
- throw new Error('Agent paused');
3125
+ throw this._createInterruptedError('Agent paused');
1712
3126
  }
1713
3127
  }
1714
3128
  async _handle_post_llm_processing(browser_state_summary, input_messages, _actions = []) {
@@ -1726,159 +3140,40 @@ export class Agent {
1726
3140
  }, null, 2), this.settings.save_conversation_path_encoding);
1727
3141
  }
1728
3142
  }
1729
- /**
1730
- * Handle all types of errors that can occur during a step
1731
- * Implements comprehensive error categorization with:
1732
- * - Validation error hints
1733
- * - Rate limit auto-retry
1734
- * - Parse error guidance
1735
- * - Token limit warnings
1736
- * - Network error detection
1737
- * - Browser error handling
1738
- * - LLM-specific errors
1739
- */
3143
+ /** Handle all types of errors that can occur during a step (python c011 parity). */
1740
3144
  async _handle_step_error(error) {
3145
+ if (error?.name === 'InterruptedError') {
3146
+ const message = error.message
3147
+ ? `The agent was interrupted mid-step - ${error.message}`
3148
+ : 'The agent was interrupted mid-step';
3149
+ this.logger.warning(message);
3150
+ return;
3151
+ }
1741
3152
  const include_trace = this.logger.level === 'debug';
1742
- let error_msg = AgentError.format_error(error, include_trace);
1743
- const prefix = `❌ Result failed ${this.state.consecutive_failures + 1}/${this.settings.max_failures} times:\n `;
3153
+ const error_msg = AgentError.format_error(error, include_trace);
3154
+ const maxTotalFailures = this._max_total_failures();
3155
+ const prefix = `❌ Result failed ${this.state.consecutive_failures + 1}/${maxTotalFailures} times: `;
1744
3156
  this.state.consecutive_failures += 1;
1745
- // 1. Handle Validation Errors (Pydantic/Zod)
1746
- if (error.name === 'ValidationError' ||
1747
- error.name === 'ZodError' ||
1748
- error instanceof TypeError) {
1749
- this.logger.error(`${prefix}${error_msg}`);
1750
- // Add context hint for validation errors
1751
- if (error_msg.includes('Max token limit reached') ||
1752
- error_msg.includes('token')) {
1753
- error_msg +=
1754
- '\n\n💡 Hint: Your response was too long. Keep your thinking and output concise.';
3157
+ const isFinalFailure = this.state.consecutive_failures >= maxTotalFailures;
3158
+ const isParseError = error_msg.includes('Could not parse response') ||
3159
+ error_msg.includes('tool_use_failed');
3160
+ if (isParseError) {
3161
+ const parseLog = `Model: ${this.llm.model} failed`;
3162
+ if (isFinalFailure) {
3163
+ this.logger.error(parseLog);
1755
3164
  }
1756
3165
  else {
1757
- error_msg +=
1758
- '\n\n💡 Hint: Your output format was invalid. Please follow the exact schema structure required for actions.';
3166
+ this.logger.warning(parseLog);
1759
3167
  }
1760
3168
  }
1761
- // 2. Handle Interrupted Errors
1762
- else if (error.message.includes('interrupted') ||
1763
- error.message.includes('abort') ||
1764
- error.message.includes('InterruptedError')) {
1765
- error_msg = `The agent was interrupted mid-step${error.message ? ` - ${error.message}` : ''}`;
1766
- this.logger.error(`${prefix}${error_msg}`);
1767
- }
1768
- // 3. Handle Parse Errors
1769
- else if (error_msg.includes('Could not parse') ||
1770
- error_msg.includes('tool_use_failed') ||
1771
- error_msg.includes('Failed to parse')) {
1772
- this.logger.debug(`Model: ${this.llm.model} failed to parse response`);
1773
- error_msg +=
1774
- '\n\n💡 Hint: Return a valid JSON object with the required fields.';
1775
- this.logger.error(`${prefix}${error_msg}`);
1776
- }
1777
- // 4. Handle Rate Limit Errors (OpenAI, Anthropic, Google)
1778
- else if (this._isRateLimitError(error, error_msg)) {
1779
- this.logger.warning(`${prefix}${error_msg}`);
1780
- this.logger.warning(`⏳ Rate limit detected, waiting ${this.settings.retry_delay}s before retrying...`);
1781
- // Auto-retry: wait before continuing
1782
- await this._sleep(this.settings.retry_delay);
1783
- error_msg += `\n\n⏳ Retrying after ${this.settings.retry_delay}s delay...`;
1784
- }
1785
- // 5. Handle Network Errors
1786
- else if (this._isNetworkError(error, error_msg)) {
1787
- this.logger.error(`${prefix}${error_msg}`);
1788
- error_msg +=
1789
- '\n\n🌐 Network error detected. Please check your internet connection and try again.';
1790
- }
1791
- // 6. Handle Browser Errors
1792
- else if (this._isBrowserError(error, error_msg)) {
1793
- this.logger.error(`${prefix}${error_msg}`);
1794
- error_msg +=
1795
- '\n\n🌍 Browser error detected. The page may have crashed or become unresponsive.';
1796
- }
1797
- // 7. Handle Timeout Errors
1798
- else if (this._isTimeoutError(error, error_msg)) {
3169
+ if (isFinalFailure) {
1799
3170
  this.logger.error(`${prefix}${error_msg}`);
1800
- error_msg +=
1801
- '\n\n⏱️ Timeout error. The operation took too long to complete.';
1802
3171
  }
1803
- // 8. Handle All Other Errors
1804
3172
  else {
1805
- this.logger.error(`${prefix}${error_msg}`);
3173
+ this.logger.warning(`${prefix}${error_msg}`);
1806
3174
  }
1807
3175
  this.state.last_result = [new ActionResult({ error: error_msg })];
1808
3176
  }
1809
- /**
1810
- * Check if an error is a network error
1811
- */
1812
- _isNetworkError(error, error_msg) {
1813
- const networkPatterns = [
1814
- 'ECONNREFUSED',
1815
- 'ENOTFOUND',
1816
- 'ETIMEDOUT',
1817
- 'ECONNRESET',
1818
- 'network error',
1819
- 'Network Error',
1820
- 'fetch failed',
1821
- 'socket hang up',
1822
- 'getaddrinfo',
1823
- ];
1824
- return networkPatterns.some((pattern) => error_msg.includes(pattern) || error.message.includes(pattern));
1825
- }
1826
- /**
1827
- * Check if an error is a browser/Playwright error
1828
- */
1829
- _isBrowserError(error, error_msg) {
1830
- const browserPatterns = [
1831
- 'Target page',
1832
- 'Page crashed',
1833
- 'Browser closed',
1834
- 'Context closed',
1835
- 'Frame detached',
1836
- 'Execution context',
1837
- 'Navigation failed',
1838
- 'Protocol error',
1839
- ];
1840
- return browserPatterns.some((pattern) => error_msg.includes(pattern) || error.message.includes(pattern));
1841
- }
1842
- /**
1843
- * Check if an error is a timeout error
1844
- */
1845
- _isTimeoutError(error, error_msg) {
1846
- const timeoutPatterns = [
1847
- 'timeout',
1848
- 'Timeout',
1849
- 'timed out',
1850
- 'time limit exceeded',
1851
- 'deadline exceeded',
1852
- ];
1853
- return timeoutPatterns.some((pattern) => error_msg.toLowerCase().includes(pattern.toLowerCase()));
1854
- }
1855
- /**
1856
- * Check if an error is a rate limit error from various LLM providers
1857
- */
1858
- _isRateLimitError(error, error_msg) {
1859
- // Check error class name
1860
- const errorClassName = error.constructor.name;
1861
- if (errorClassName === 'RateLimitError' ||
1862
- errorClassName === 'ResourceExhausted') {
1863
- return true;
1864
- }
1865
- // Check error message patterns
1866
- const rateLimitPatterns = [
1867
- 'rate_limit_exceeded',
1868
- 'rate limit exceeded',
1869
- 'RateLimitError',
1870
- 'RESOURCE_EXHAUSTED',
1871
- 'ResourceExhausted',
1872
- 'tokens per minute',
1873
- 'TPM',
1874
- 'requests per minute',
1875
- 'RPM',
1876
- 'quota exceeded',
1877
- 'too many requests',
1878
- '429',
1879
- ];
1880
- return rateLimitPatterns.some((pattern) => error_msg.toLowerCase().includes(pattern.toLowerCase()));
1881
- }
1882
3177
  async _finalize(browser_state_summary) {
1883
3178
  const step_end_time = Date.now() / 1000;
1884
3179
  this._enforceDoneOnlyForCurrentStep = false;
@@ -1886,8 +3181,15 @@ export class Agent {
1886
3181
  return;
1887
3182
  }
1888
3183
  if (browser_state_summary) {
1889
- const metadata = new StepMetadata(this.step_start_time, step_end_time, this.state.n_steps);
1890
- await this._make_history_item(this.state.last_model_output, browser_state_summary, this.state.last_result, metadata);
3184
+ let stepInterval = null;
3185
+ if (this.history.history.length > 0) {
3186
+ const lastMetadata = this.history.history.at(-1)?.metadata;
3187
+ if (lastMetadata) {
3188
+ stepInterval = Math.max(0, lastMetadata.step_end_time - lastMetadata.step_start_time);
3189
+ }
3190
+ }
3191
+ const metadata = new StepMetadata(this.step_start_time, step_end_time, this.state.n_steps, stepInterval);
3192
+ await this._make_history_item(this.state.last_model_output, browser_state_summary, this.state.last_result, metadata, this._message_manager.last_state_message_text);
1891
3193
  }
1892
3194
  this._log_step_completion_summary(this.step_start_time, this.state.last_result);
1893
3195
  this.save_file_system_state();
@@ -1904,14 +3206,388 @@ export class Agent {
1904
3206
  const isLastStep = Boolean(step_info && step_info.is_last_step());
1905
3207
  this._enforceDoneOnlyForCurrentStep = isLastStep;
1906
3208
  if (isLastStep) {
1907
- const message = 'Now comes your last step. Use only the "done" action now. No other actions - so here your action sequence must have length 1.\n' +
1908
- 'If the task is not yet fully finished as requested by the user, set success in "done" to false! E.g. if not all steps are fully completed.\n' +
1909
- 'If the task is fully finished, set success in "done" to true.\n' +
3209
+ const message = 'You reached max_steps - this is your last step. Your only tool available is the "done" tool. No other tool is available. All other tools which you see in history or examples are not available.\n' +
3210
+ 'If the task is not yet fully finished as requested by the user, set success in "done" to false! E.g. if not all steps are fully completed. Else success to true.\n' +
1910
3211
  'Include everything you found out for the ultimate task in the done text.';
1911
3212
  this._message_manager._add_context_message(new UserMessage(message));
1912
- this.logger.info('⚠️ Approaching last step. Enforcing done-only action.');
3213
+ this.logger.debug('Last step finishing up');
3214
+ }
3215
+ }
3216
+ _max_total_failures() {
3217
+ return (this.settings.max_failures +
3218
+ Number(this.settings.final_response_after_failure));
3219
+ }
3220
+ async _handle_failure_limit_recovery() {
3221
+ if (!this.settings.final_response_after_failure ||
3222
+ this.state.consecutive_failures < this.settings.max_failures) {
3223
+ return;
3224
+ }
3225
+ const message = `You failed ${this.settings.max_failures} times. Therefore we terminate the agent.\n` +
3226
+ 'Your only tool available is the "done" tool. No other tool is available. All other tools which you see in history or examples are not available.\n' +
3227
+ 'If the task is not yet fully finished as requested by the user, set success in "done" to false! E.g. if not all steps are fully completed. Else success to true.\n' +
3228
+ 'Include everything you found out for the ultimate task in the done text.';
3229
+ this._message_manager._add_context_message(new UserMessage(message));
3230
+ this._enforceDoneOnlyForCurrentStep = true;
3231
+ this.logger.debug('Force done action, because we reached max_failures.');
3232
+ }
3233
+ _update_plan_from_model_output(modelOutput) {
3234
+ if (!this.settings.enable_planning) {
3235
+ return;
3236
+ }
3237
+ if (Array.isArray(modelOutput.plan_update)) {
3238
+ this.state.plan = modelOutput.plan_update.map((stepText) => new PlanItem({
3239
+ text: stepText,
3240
+ status: 'pending',
3241
+ }));
3242
+ this.state.current_plan_item_index = 0;
3243
+ this.state.plan_generation_step = this.state.n_steps;
3244
+ if (this.state.plan.length > 0) {
3245
+ this.state.plan[0].status = 'current';
3246
+ }
3247
+ this.logger.info(`📋 Plan updated with ${this.state.plan.length} steps`);
3248
+ return;
3249
+ }
3250
+ if (typeof modelOutput.current_plan_item !== 'number' ||
3251
+ !this.state.plan ||
3252
+ this.state.plan.length === 0) {
3253
+ return;
3254
+ }
3255
+ const oldIndex = this.state.current_plan_item_index;
3256
+ const newIndex = Math.max(0, Math.min(modelOutput.current_plan_item, this.state.plan.length - 1));
3257
+ for (let i = oldIndex; i < newIndex; i += 1) {
3258
+ if (this.state.plan[i] &&
3259
+ (this.state.plan[i].status === 'current' ||
3260
+ this.state.plan[i].status === 'pending')) {
3261
+ this.state.plan[i].status = 'done';
3262
+ }
3263
+ }
3264
+ if (this.state.plan[newIndex]) {
3265
+ this.state.plan[newIndex].status = 'current';
3266
+ }
3267
+ this.state.current_plan_item_index = newIndex;
3268
+ }
3269
+ _render_plan_description() {
3270
+ if (!this.settings.enable_planning || !this.state.plan) {
3271
+ return null;
3272
+ }
3273
+ const markers = {
3274
+ done: '[x]',
3275
+ current: '[>]',
3276
+ pending: '[ ]',
3277
+ skipped: '[-]',
3278
+ };
3279
+ return this.state.plan
3280
+ .map((step, index) => `${markers[step.status] ?? '[ ]'} ${index}: ${step.text}`)
3281
+ .join('\n');
3282
+ }
3283
+ _inject_replan_nudge() {
3284
+ if (!this.settings.enable_planning || !this.state.plan) {
3285
+ return;
3286
+ }
3287
+ if (this.settings.planning_replan_on_stall <= 0) {
3288
+ return;
3289
+ }
3290
+ if (this.state.consecutive_failures < this.settings.planning_replan_on_stall) {
3291
+ return;
3292
+ }
3293
+ const message = 'REPLAN SUGGESTED: You have failed ' +
3294
+ `${this.state.consecutive_failures} consecutive times. ` +
3295
+ 'Your current plan may need revision. ' +
3296
+ 'Output a new `plan_update` with revised steps to recover.';
3297
+ this.logger.info(`📋 Replan nudge injected after ${this.state.consecutive_failures} consecutive failures`);
3298
+ this._message_manager._add_context_message(new UserMessage(message));
3299
+ }
3300
+ _inject_exploration_nudge() {
3301
+ if (!this.settings.enable_planning || this.state.plan) {
3302
+ return;
3303
+ }
3304
+ if (this.settings.planning_exploration_limit <= 0) {
3305
+ return;
3306
+ }
3307
+ if (this.state.n_steps < this.settings.planning_exploration_limit) {
3308
+ return;
3309
+ }
3310
+ const message = 'PLANNING NUDGE: You have taken ' +
3311
+ `${this.state.n_steps} steps without creating a plan. ` +
3312
+ 'If the task is complex, output a `plan_update` with clear todo items now. ' +
3313
+ 'If the task is already done or nearly done, call `done` instead.';
3314
+ this.logger.info(`📋 Exploration nudge injected after ${this.state.n_steps} steps without a plan`);
3315
+ this._message_manager._add_context_message(new UserMessage(message));
3316
+ }
3317
+ _inject_loop_detection_nudge() {
3318
+ if (!this.settings.loop_detection_enabled) {
3319
+ return;
3320
+ }
3321
+ const nudge = this.state.loop_detector.get_nudge_message();
3322
+ if (!nudge) {
3323
+ return;
3324
+ }
3325
+ this.logger.info(`🔁 Loop detection nudge injected (repetition=${this.state.loop_detector.max_repetition_count}, stagnation=${this.state.loop_detector.consecutive_stagnant_pages})`);
3326
+ this._message_manager._add_context_message(new UserMessage(nudge));
3327
+ }
3328
+ _update_loop_detector_actions() {
3329
+ if (!this.settings.loop_detection_enabled ||
3330
+ !this.state.last_model_output) {
3331
+ return;
3332
+ }
3333
+ const exemptActions = new Set(['wait', 'done', 'go_back']);
3334
+ for (const action of this.state.last_model_output.action) {
3335
+ const actionData = typeof action?.model_dump === 'function'
3336
+ ? action.model_dump()
3337
+ : action;
3338
+ if (!actionData || typeof actionData !== 'object') {
3339
+ continue;
3340
+ }
3341
+ const actionName = Object.keys(actionData)[0] ?? 'unknown';
3342
+ if (exemptActions.has(actionName)) {
3343
+ continue;
3344
+ }
3345
+ const rawParams = actionData[actionName];
3346
+ const params = rawParams && typeof rawParams === 'object' && !Array.isArray(rawParams)
3347
+ ? rawParams
3348
+ : {};
3349
+ this.state.loop_detector.record_action(actionName, params);
1913
3350
  }
1914
3351
  }
3352
+ _update_loop_detector_page_state(browser_state_summary) {
3353
+ if (!this.settings.loop_detection_enabled) {
3354
+ return;
3355
+ }
3356
+ const url = browser_state_summary.url ?? '';
3357
+ const elementCount = browser_state_summary.selector_map
3358
+ ? Object.keys(browser_state_summary.selector_map).length
3359
+ : 0;
3360
+ const domText = (() => {
3361
+ try {
3362
+ return (browser_state_summary.element_tree?.clickable_elements_to_string?.() ??
3363
+ '');
3364
+ }
3365
+ catch {
3366
+ return '';
3367
+ }
3368
+ })();
3369
+ this.state.loop_detector.record_page_state(url, domText, elementCount);
3370
+ }
3371
+ _inject_budget_warning(step_info = null) {
3372
+ if (!step_info) {
3373
+ return;
3374
+ }
3375
+ const stepsUsed = step_info.step_number + 1;
3376
+ const budgetRatio = stepsUsed / step_info.max_steps;
3377
+ if (budgetRatio < 0.75 || step_info.is_last_step()) {
3378
+ return;
3379
+ }
3380
+ const stepsRemaining = step_info.max_steps - stepsUsed;
3381
+ const pct = Math.floor(budgetRatio * 100);
3382
+ const message = `BUDGET WARNING: You have used ${stepsUsed}/${step_info.max_steps} steps ` +
3383
+ `(${pct}%). ${stepsRemaining} steps remaining. ` +
3384
+ 'If the task cannot be completed in the remaining steps, prioritize: ' +
3385
+ '(1) consolidate your results (save to files if the file system is in use), ' +
3386
+ '(2) call done with what you have. ' +
3387
+ 'Partial results are far more valuable than exhausting all steps with nothing saved.';
3388
+ this.logger.info(`Step budget warning: ${stepsUsed}/${step_info.max_steps} (${pct}%)`);
3389
+ this._message_manager._add_context_message(new UserMessage(message));
3390
+ }
3391
+ async _run_simple_judge() {
3392
+ const lastHistoryItem = this.history.history[this.history.history.length - 1];
3393
+ if (!lastHistoryItem || !lastHistoryItem.result.length) {
3394
+ return;
3395
+ }
3396
+ const lastResult = lastHistoryItem.result[lastHistoryItem.result.length - 1];
3397
+ if (!lastResult.is_done || !lastResult.success) {
3398
+ return;
3399
+ }
3400
+ const messages = construct_simple_judge_messages({
3401
+ task: this.task,
3402
+ final_result: this.history.final_result() ?? '',
3403
+ });
3404
+ try {
3405
+ const response = await this.llm.ainvoke(messages, SimpleJudgeOutputFormat);
3406
+ const parsed = this._parseCompletionPayload(response.completion);
3407
+ if (typeof parsed?.is_correct !== 'boolean') {
3408
+ this.logger.debug('Simple judge response missing boolean is_correct; skipping override.');
3409
+ return;
3410
+ }
3411
+ const isCorrect = parsed.is_correct;
3412
+ const reason = typeof parsed?.reason === 'string' && parsed.reason.trim()
3413
+ ? parsed.reason.trim()
3414
+ : 'Task requirements not fully met';
3415
+ if (!isCorrect) {
3416
+ this.logger.info(`⚠️ Simple judge overriding success to failure: ${reason}`);
3417
+ lastResult.success = false;
3418
+ const note = `[Simple judge: ${reason}]`;
3419
+ if (lastResult.extracted_content) {
3420
+ lastResult.extracted_content += `\n\n${note}`;
3421
+ }
3422
+ else {
3423
+ lastResult.extracted_content = note;
3424
+ }
3425
+ }
3426
+ }
3427
+ catch (error) {
3428
+ this.logger.warning(`Simple judge failed with error: ${error instanceof Error ? error.message : String(error)}`);
3429
+ }
3430
+ }
3431
+ async _judge_trace() {
3432
+ const messages = construct_judge_messages({
3433
+ task: this.task,
3434
+ final_result: this.history.final_result() ?? '',
3435
+ agent_steps: this.history.agent_steps(),
3436
+ screenshot_paths: this.history
3437
+ .screenshot_paths()
3438
+ .filter((value) => typeof value === 'string'),
3439
+ max_images: 10,
3440
+ ground_truth: this.settings.ground_truth,
3441
+ use_vision: this.settings.use_vision,
3442
+ });
3443
+ try {
3444
+ const invokeOptions = this.judge_llm?.provider === 'browser-use'
3445
+ ? { request_type: 'judge' }
3446
+ : undefined;
3447
+ const response = await this.judge_llm.ainvoke(messages, JudgeOutputFormat, invokeOptions);
3448
+ const parsed = this._parseCompletionPayload(response.completion);
3449
+ const validation = JudgeSchema.safeParse(parsed);
3450
+ if (!validation.success) {
3451
+ this.logger.warning('Judge trace response did not match expected schema; skipping judgement.');
3452
+ return null;
3453
+ }
3454
+ return validation.data;
3455
+ }
3456
+ catch (error) {
3457
+ this.logger.warning(`Judge trace failed: ${error instanceof Error ? error.message : String(error)}`);
3458
+ return null;
3459
+ }
3460
+ }
3461
+ async _judge_and_log() {
3462
+ const lastHistoryItem = this.history.history[this.history.history.length - 1];
3463
+ if (!lastHistoryItem || !lastHistoryItem.result.length) {
3464
+ return;
3465
+ }
3466
+ const lastResult = lastHistoryItem.result[lastHistoryItem.result.length - 1];
3467
+ if (!lastResult.is_done) {
3468
+ return;
3469
+ }
3470
+ const judgement = await this._judge_trace();
3471
+ lastResult.judgement = judgement;
3472
+ if (!judgement) {
3473
+ return;
3474
+ }
3475
+ if (lastResult.success === true && judgement.verdict === true) {
3476
+ return;
3477
+ }
3478
+ let judgeLog = '\n';
3479
+ if (lastResult.success === true && judgement.verdict === false) {
3480
+ judgeLog += '⚠️ Agent reported success but judge thinks task failed\n';
3481
+ }
3482
+ judgeLog += `⚖️ Judge Verdict: ${judgement.verdict ? 'PASS' : 'FAIL'}\n`;
3483
+ if (judgement.failure_reason) {
3484
+ judgeLog += ` Failure Reason: ${judgement.failure_reason}\n`;
3485
+ }
3486
+ if (judgement.reached_captcha) {
3487
+ judgeLog += ' Captcha Detected: Agent encountered captcha challenges\n';
3488
+ judgeLog +=
3489
+ ' Use Browser Use Cloud for stealth browser infra: https://docs.browser-use.com/customize/browser/remote\n';
3490
+ }
3491
+ if (judgement.reasoning) {
3492
+ judgeLog += ` ${judgement.reasoning}\n`;
3493
+ }
3494
+ this.logger.info(judgeLog);
3495
+ }
3496
+ _replace_urls_in_text(text) {
3497
+ const replacedUrls = {};
3498
+ const shortenedText = text.replace(URL_PATTERN, (originalUrl) => {
3499
+ const queryStart = originalUrl.indexOf('?');
3500
+ const fragmentStart = originalUrl.indexOf('#');
3501
+ let afterPathStart = originalUrl.length;
3502
+ if (queryStart !== -1) {
3503
+ afterPathStart = Math.min(afterPathStart, queryStart);
3504
+ }
3505
+ if (fragmentStart !== -1) {
3506
+ afterPathStart = Math.min(afterPathStart, fragmentStart);
3507
+ }
3508
+ const baseUrl = originalUrl.slice(0, afterPathStart);
3509
+ const afterPath = originalUrl.slice(afterPathStart);
3510
+ if (afterPath.length <= this._url_shortening_limit) {
3511
+ return originalUrl;
3512
+ }
3513
+ const truncatedAfterPath = afterPath.slice(0, this._url_shortening_limit);
3514
+ const shortHash = createHash('md5')
3515
+ .update(afterPath, 'utf8')
3516
+ .digest('hex')
3517
+ .slice(0, 7);
3518
+ const shortened = `${baseUrl}${truncatedAfterPath}...${shortHash}`;
3519
+ if (shortened.length >= originalUrl.length) {
3520
+ return originalUrl;
3521
+ }
3522
+ replacedUrls[shortened] = originalUrl;
3523
+ return shortened;
3524
+ });
3525
+ return [shortenedText, replacedUrls];
3526
+ }
3527
+ _process_messages_and_replace_long_urls_shorter_ones(inputMessages) {
3528
+ const urlsReplaced = {};
3529
+ for (const message of inputMessages) {
3530
+ if (!message || typeof message !== 'object') {
3531
+ continue;
3532
+ }
3533
+ const role = message.role;
3534
+ const isUserOrAssistant = message instanceof UserMessage ||
3535
+ message instanceof AssistantMessage ||
3536
+ role === 'user' ||
3537
+ role === 'assistant';
3538
+ if (!isUserOrAssistant) {
3539
+ continue;
3540
+ }
3541
+ if (typeof message.content === 'string') {
3542
+ const [updated, replaced] = this._replace_urls_in_text(message.content);
3543
+ message.content = updated;
3544
+ Object.assign(urlsReplaced, replaced);
3545
+ continue;
3546
+ }
3547
+ if (!Array.isArray(message.content)) {
3548
+ continue;
3549
+ }
3550
+ for (const part of message.content) {
3551
+ if (!part || typeof part !== 'object') {
3552
+ continue;
3553
+ }
3554
+ const isTextPart = part instanceof ContentPartTextParam || part.type === 'text';
3555
+ if (!isTextPart || typeof part.text !== 'string') {
3556
+ continue;
3557
+ }
3558
+ const [updated, replaced] = this._replace_urls_in_text(part.text);
3559
+ part.text = updated;
3560
+ Object.assign(urlsReplaced, replaced);
3561
+ }
3562
+ }
3563
+ return urlsReplaced;
3564
+ }
3565
+ _replace_shortened_urls_in_string(text, urlReplacements) {
3566
+ let result = text;
3567
+ for (const [shortenedUrl, originalUrl] of Object.entries(urlReplacements)) {
3568
+ result = result.split(shortenedUrl).join(originalUrl);
3569
+ }
3570
+ return result;
3571
+ }
3572
+ _replace_shortened_urls_in_value(value, urlReplacements) {
3573
+ if (typeof value === 'string') {
3574
+ return this._replace_shortened_urls_in_string(value, urlReplacements);
3575
+ }
3576
+ if (Array.isArray(value)) {
3577
+ for (let i = 0; i < value.length; i += 1) {
3578
+ value[i] = this._replace_shortened_urls_in_value(value[i], urlReplacements);
3579
+ }
3580
+ return value;
3581
+ }
3582
+ if (!value || typeof value !== 'object') {
3583
+ return value;
3584
+ }
3585
+ for (const [key, nested] of Object.entries(value)) {
3586
+ value[key] =
3587
+ this._replace_shortened_urls_in_value(nested, urlReplacements);
3588
+ }
3589
+ return value;
3590
+ }
1915
3591
  _parseCompletionPayload(rawCompletion) {
1916
3592
  let parsedCompletion = rawCompletion;
1917
3593
  if (typeof parsedCompletion === 'string') {
@@ -1931,7 +3607,7 @@ export class Agent {
1931
3607
  parsedCompletion = JSON.parse(jsonText);
1932
3608
  }
1933
3609
  catch (error) {
1934
- throw new Error(`Failed to parse LLM completion as JSON: ${String(error)}`);
3610
+ throw new Error(`Failed to parse LLM completion as JSON: ${String(error)}`, { cause: error });
1935
3611
  }
1936
3612
  }
1937
3613
  if (!parsedCompletion || typeof parsedCompletion !== 'object') {
@@ -1958,13 +3634,38 @@ export class Agent {
1958
3634
  });
1959
3635
  }
1960
3636
  async _get_model_output_with_retry(messages, signal = null) {
3637
+ const urlReplacements = this._process_messages_and_replace_long_urls_shorter_ones(messages);
1961
3638
  const invokeAndParse = async (inputMessages) => {
1962
3639
  this._throwIfAborted(signal);
1963
- const completion = await this.llm.ainvoke(inputMessages, AgentLLMOutputFormat, { signal: signal ?? undefined });
3640
+ const outputFormat = this._enforceDoneOnlyForCurrentStep
3641
+ ? DoneOnlyLLMOutputFormat
3642
+ : AgentLLMOutputFormat;
3643
+ const completion = await this.llm.ainvoke(inputMessages, outputFormat, {
3644
+ signal: signal ?? undefined,
3645
+ session_id: this.session_id,
3646
+ });
1964
3647
  this._throwIfAborted(signal);
1965
- return this._parseCompletionPayload(completion.completion);
3648
+ const parsed = this._parseCompletionPayload(completion.completion);
3649
+ if (Object.keys(urlReplacements).length) {
3650
+ this._replace_shortened_urls_in_value(parsed, urlReplacements);
3651
+ }
3652
+ return parsed;
3653
+ };
3654
+ const invokeAndParseWithFallback = async (inputMessages) => {
3655
+ try {
3656
+ return await invokeAndParse(inputMessages);
3657
+ }
3658
+ catch (error) {
3659
+ if ((error instanceof ModelRateLimitError ||
3660
+ error instanceof ModelProviderError) &&
3661
+ this._try_switch_to_fallback_llm(error)) {
3662
+ this._throwIfAborted(signal);
3663
+ return await invokeAndParse(inputMessages);
3664
+ }
3665
+ throw error;
3666
+ }
1966
3667
  };
1967
- let parsed_completion = await invokeAndParse(messages);
3668
+ let parsed_completion = await invokeAndParseWithFallback(messages);
1968
3669
  let rawAction = Array.isArray(parsed_completion?.action)
1969
3670
  ? parsed_completion.action
1970
3671
  : [];
@@ -1972,8 +3673,8 @@ export class Agent {
1972
3673
  if (this._isModelActionMissing(rawAction)) {
1973
3674
  this._throwIfAborted(signal);
1974
3675
  this.logger.warning('Model returned empty action. Retrying...');
1975
- const clarificationMessage = new UserMessage('You forgot to return an action. Please respond only with a valid JSON action according to the expected format.');
1976
- parsed_completion = await invokeAndParse([
3676
+ const clarificationMessage = new UserMessage('You forgot to return an action. Please respond with a valid JSON action according to the expected schema with your assessment and next actions.');
3677
+ parsed_completion = await invokeAndParseWithFallback([
1977
3678
  ...messages,
1978
3679
  clarificationMessage,
1979
3680
  ]);
@@ -1994,18 +3695,70 @@ export class Agent {
1994
3695
  }
1995
3696
  const action = this._validateAndNormalizeActions(rawAction);
1996
3697
  const toNullableString = (value) => typeof value === 'string' ? value : null;
1997
- const AgentOutputModel = this.AgentOutput ?? AgentOutput;
3698
+ const toNullableNumber = (value) => typeof value === 'number' && Number.isFinite(value)
3699
+ ? Math.trunc(value)
3700
+ : null;
3701
+ const toNullablePlanUpdate = (value) => Array.isArray(value)
3702
+ ? value.filter((item) => typeof item === 'string')
3703
+ : null;
3704
+ const AgentOutputModel = this._enforceDoneOnlyForCurrentStep
3705
+ ? (this.DoneAgentOutput ?? this.AgentOutput ?? AgentOutput)
3706
+ : (this.AgentOutput ?? AgentOutput);
1998
3707
  return new AgentOutputModel({
1999
3708
  thinking: toNullableString(parsed_completion?.thinking),
2000
3709
  evaluation_previous_goal: toNullableString(parsed_completion?.evaluation_previous_goal),
2001
3710
  memory: toNullableString(parsed_completion?.memory),
2002
3711
  next_goal: toNullableString(parsed_completion?.next_goal),
3712
+ current_plan_item: toNullableNumber(parsed_completion?.current_plan_item),
3713
+ plan_update: toNullablePlanUpdate(parsed_completion?.plan_update),
2003
3714
  action,
2004
3715
  });
2005
3716
  }
3717
+ _try_switch_to_fallback_llm(error) {
3718
+ if (this._using_fallback_llm) {
3719
+ this.logger.warning(`⚠️ Fallback LLM also failed (${error.name}: ${error.message}), no more fallbacks available`);
3720
+ return false;
3721
+ }
3722
+ const retryableStatusCodes = new Set([401, 402, 429, 500, 502, 503, 504]);
3723
+ const statusCode = typeof error.statusCode === 'number' ? error.statusCode : null;
3724
+ const isRetryable = error instanceof ModelRateLimitError ||
3725
+ (statusCode != null && retryableStatusCodes.has(statusCode));
3726
+ if (!isRetryable) {
3727
+ return false;
3728
+ }
3729
+ if (!this._fallback_llm) {
3730
+ this.logger.warning(`⚠️ LLM error (${error.name}: ${error.message}) but no fallback_llm configured`);
3731
+ return false;
3732
+ }
3733
+ this._log_fallback_switch(error, this._fallback_llm);
3734
+ this.llm = this._fallback_llm;
3735
+ this._using_fallback_llm = true;
3736
+ this.token_cost_service.register_llm(this._fallback_llm);
3737
+ return true;
3738
+ }
3739
+ _log_fallback_switch(error, fallback) {
3740
+ const originalModel = typeof this._original_llm?.model === 'string'
3741
+ ? this._original_llm.model
3742
+ : 'unknown';
3743
+ const fallbackModel = typeof fallback?.model === 'string' ? fallback.model : 'unknown';
3744
+ const errorType = error.name || 'Error';
3745
+ const statusCode = typeof error.statusCode === 'number' ? error.statusCode : 'N/A';
3746
+ this.logger.warning(`⚠️ Primary LLM (${originalModel}) failed with ${errorType} (status=${statusCode}), switching to fallback LLM (${fallbackModel})`);
3747
+ }
2006
3748
  _validateAndNormalizeActions(actions) {
2007
3749
  const normalizedActions = [];
2008
3750
  const registryActions = this.controller.registry.get_all_actions();
3751
+ const actionAliases = {
3752
+ navigate: 'go_to_url',
3753
+ input: 'input_text',
3754
+ switch: 'switch_tab',
3755
+ close: 'close_tab',
3756
+ extract: 'extract_structured_data',
3757
+ find_text: 'scroll_to_text',
3758
+ dropdown_options: 'get_dropdown_options',
3759
+ select_dropdown: 'select_dropdown_option',
3760
+ replace_file: 'replace_file_str',
3761
+ };
2009
3762
  const availableNames = new Set();
2010
3763
  const modelForStep = this._enforceDoneOnlyForCurrentStep
2011
3764
  ? this.DoneActionModel
@@ -2040,19 +3793,28 @@ export class Agent {
2040
3793
  if (keys.length !== 1) {
2041
3794
  throw new Error(`Invalid action at index ${i}: expected exactly one action key, got ${keys.length}`);
2042
3795
  }
2043
- const actionName = keys[0];
3796
+ const requestedActionName = keys[0];
3797
+ let actionName = requestedActionName;
3798
+ if (!availableNames.has(actionName)) {
3799
+ const aliasTarget = actionAliases[requestedActionName];
3800
+ if (aliasTarget && availableNames.has(aliasTarget)) {
3801
+ actionName = aliasTarget;
3802
+ }
3803
+ }
2044
3804
  if (!availableNames.has(actionName)) {
2045
3805
  const available = Array.from(availableNames).sort().join(', ');
2046
- throw new Error(`Action '${actionName}' is not available on the current page. Available actions: ${available}`);
3806
+ throw new Error(`Action '${requestedActionName}' is not available on the current page. Available actions: ${available}`);
2047
3807
  }
2048
3808
  const actionInfo = registryActions.get(actionName);
2049
3809
  if (!actionInfo) {
2050
- throw new Error(`Action '${actionName}' is not registered`);
3810
+ throw new Error(`Action '${requestedActionName}' is not registered`);
2051
3811
  }
2052
- const rawParams = (actionObject[actionName] ?? {});
3812
+ const rawParams = (actionObject[requestedActionName] ??
3813
+ actionObject[actionName] ??
3814
+ {});
2053
3815
  const paramsResult = actionInfo.paramSchema.safeParse(rawParams);
2054
3816
  if (!paramsResult.success) {
2055
- throw new Error(`Invalid parameters for action '${actionName}': ${paramsResult.error.message}`);
3817
+ throw new Error(`Invalid parameters for action '${requestedActionName}': ${paramsResult.error.message}`);
2056
3818
  }
2057
3819
  normalizedActions.push(new modelForStep({
2058
3820
  [actionName]: paramsResult.data,
@@ -2089,9 +3851,14 @@ export class Agent {
2089
3851
  }
2090
3852
  }
2091
3853
  catch (error) {
3854
+ const errorType = error instanceof Error
3855
+ ? error.name || 'Error'
3856
+ : typeof error === 'object' && error !== null
3857
+ ? (error.constructor?.name ?? 'Error')
3858
+ : 'Error';
2092
3859
  const message = error instanceof Error ? error.message : String(error);
2093
3860
  const errorContext = context ? ` ${context}` : '';
2094
- this.logger.debug(`📁 Failed to check for downloads${errorContext}: ${message}`);
3861
+ this.logger.debug(`📁 Failed to check for downloads${errorContext}: ${errorType}: ${message}`);
2095
3862
  }
2096
3863
  }
2097
3864
  _update_available_file_paths(downloads) {
@@ -2112,16 +3879,24 @@ export class Agent {
2112
3879
  }
2113
3880
  }
2114
3881
  else {
2115
- this.logger.info(`📁 No new downloads detected (tracking ${existing.length} files)`);
3882
+ this.logger.debug(`📁 No new downloads detected (tracking ${existing.length} files)`);
2116
3883
  }
2117
3884
  }
2118
3885
  _log_step_context(current_page, browser_state_summary) {
2119
- const url = typeof current_page?.url === 'function' ? current_page.url() : '';
3886
+ const url = browser_state_summary?.url ?? '';
2120
3887
  const url_short = url.length > 50 ? `${url.slice(0, 50)}...` : url;
2121
3888
  const interactive_count = browser_state_summary?.selector_map
2122
3889
  ? Object.keys(browser_state_summary.selector_map).length
2123
3890
  : 0;
2124
- this.logger.info(`📍 Step ${this.state.n_steps}: Evaluating page with ${interactive_count} interactive elements on: ${url_short}`);
3891
+ this.logger.info('\n');
3892
+ this.logger.info(`📍 Step ${this.state.n_steps}:`);
3893
+ this.logger.debug(`Evaluating page with ${interactive_count} interactive elements on: ${url_short}`);
3894
+ }
3895
+ _log_first_step_startup() {
3896
+ if (this.history.history.length !== 0) {
3897
+ return;
3898
+ }
3899
+ this.logger.info(`Starting a browser-use agent with version ${this.version}, with provider=${this.llm.provider ?? 'unknown'} and model=${this.llm.model}`);
2125
3900
  }
2126
3901
  _log_step_completion_summary(step_start_time, result) {
2127
3902
  if (!result.length) {
@@ -2159,6 +3934,22 @@ export class Agent {
2159
3934
  });
2160
3935
  const final_result = this.history.final_result();
2161
3936
  const final_result_str = final_result != null ? JSON.stringify(final_result) : null;
3937
+ const judgement_data = this.history.judgement();
3938
+ const judge_verdict = judgement_data && typeof judgement_data.verdict === 'boolean'
3939
+ ? judgement_data.verdict
3940
+ : null;
3941
+ const judge_reasoning = judgement_data && typeof judgement_data.reasoning === 'string'
3942
+ ? judgement_data.reasoning
3943
+ : null;
3944
+ const judge_failure_reason = judgement_data && typeof judgement_data.failure_reason === 'string'
3945
+ ? judgement_data.failure_reason
3946
+ : null;
3947
+ const judge_reached_captcha = judgement_data && typeof judgement_data.reached_captcha === 'boolean'
3948
+ ? judgement_data.reached_captcha
3949
+ : null;
3950
+ const judge_impossible_task = judgement_data && typeof judgement_data.impossible_task === 'boolean'
3951
+ ? judgement_data.impossible_task
3952
+ : null;
2162
3953
  let cdpHost = null;
2163
3954
  const cdpUrl = this.browser_session?.cdp_url;
2164
3955
  if (typeof cdpUrl === 'string' && cdpUrl) {
@@ -2170,39 +3961,42 @@ export class Agent {
2170
3961
  cdpHost = cdpUrl;
2171
3962
  }
2172
3963
  }
2173
- const plannerModel = this.settings?.planner_llm &&
2174
- typeof this.settings.planner_llm === 'object'
2175
- ? (this.settings.planner_llm.model ?? null)
2176
- : null;
2177
3964
  this.telemetry.capture(new AgentTelemetryEvent({
2178
3965
  task: this.task,
2179
3966
  model: this.llm.model,
2180
3967
  model_provider: this.llm.provider ?? 'unknown',
2181
- planner_llm: plannerModel,
2182
3968
  max_steps: max_steps,
2183
3969
  max_actions_per_step: this.settings.max_actions_per_step,
2184
3970
  use_vision: this.settings.use_vision,
2185
- use_validation: this.settings.validate_output,
2186
3971
  version: this.version,
2187
3972
  source: this.source,
2188
3973
  cdp_url: cdpHost,
3974
+ agent_type: null,
2189
3975
  action_errors: this.history.errors(),
2190
3976
  action_history: action_history_data,
2191
3977
  urls_visited: this.history.urls(),
2192
3978
  steps: this.state.n_steps,
2193
3979
  total_input_tokens: token_summary.prompt_tokens ?? 0,
3980
+ total_output_tokens: token_summary.completion_tokens ?? 0,
3981
+ prompt_cached_tokens: token_summary.prompt_cached_tokens ?? 0,
3982
+ total_tokens: token_summary.total_tokens ?? 0,
2194
3983
  total_duration_seconds: this.history.total_duration_seconds(),
2195
3984
  success: this.history.is_successful(),
2196
3985
  final_result_response: final_result_str,
2197
3986
  error_message: agent_run_error,
3987
+ judge_verdict,
3988
+ judge_reasoning,
3989
+ judge_failure_reason,
3990
+ judge_reached_captcha,
3991
+ judge_impossible_task,
2198
3992
  }));
2199
3993
  }
2200
- async _make_history_item(model_output, browser_state_summary, result, metadata) {
3994
+ async _make_history_item(model_output, browser_state_summary, result, metadata, state_message = null) {
2201
3995
  const interacted_elements = model_output
2202
3996
  ? AgentHistory.get_interacted_element(model_output, browser_state_summary.selector_map)
2203
3997
  : [];
2204
3998
  const state = new BrowserStateHistory(browser_state_summary.url, browser_state_summary.title, browser_state_summary.tabs, interacted_elements, this._current_screenshot_path);
2205
- this.history.add_item(new AgentHistory(model_output, result, state, metadata));
3999
+ this.history.add_item(new AgentHistory(model_output, result, state, metadata, state_message));
2206
4000
  }
2207
4001
  save_file_system_state() {
2208
4002
  if (!this.file_system) {