browser-use 0.0.1 → 0.1.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 (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +761 -0
  3. package/dist/agent/cloud-events.d.ts +264 -0
  4. package/dist/agent/cloud-events.js +318 -0
  5. package/dist/agent/gif.d.ts +15 -0
  6. package/dist/agent/gif.js +215 -0
  7. package/dist/agent/index.d.ts +8 -0
  8. package/dist/agent/index.js +8 -0
  9. package/dist/agent/message-manager/service.d.ts +30 -0
  10. package/dist/agent/message-manager/service.js +208 -0
  11. package/dist/agent/message-manager/utils.d.ts +2 -0
  12. package/dist/agent/message-manager/utils.js +41 -0
  13. package/dist/agent/message-manager/views.d.ts +26 -0
  14. package/dist/agent/message-manager/views.js +73 -0
  15. package/dist/agent/prompts.d.ts +52 -0
  16. package/dist/agent/prompts.js +259 -0
  17. package/dist/agent/service.d.ts +290 -0
  18. package/dist/agent/service.js +2200 -0
  19. package/dist/agent/views.d.ts +741 -0
  20. package/dist/agent/views.js +537 -0
  21. package/dist/browser/browser.d.ts +7 -0
  22. package/dist/browser/browser.js +5 -0
  23. package/dist/browser/context.d.ts +8 -0
  24. package/dist/browser/context.js +4 -0
  25. package/dist/browser/dvd-screensaver.d.ts +101 -0
  26. package/dist/browser/dvd-screensaver.js +270 -0
  27. package/dist/browser/extensions.d.ts +63 -0
  28. package/dist/browser/extensions.js +359 -0
  29. package/dist/browser/index.d.ts +10 -0
  30. package/dist/browser/index.js +9 -0
  31. package/dist/browser/playwright-manager.d.ts +47 -0
  32. package/dist/browser/playwright-manager.js +146 -0
  33. package/dist/browser/profile.d.ts +196 -0
  34. package/dist/browser/profile.js +815 -0
  35. package/dist/browser/session.d.ts +505 -0
  36. package/dist/browser/session.js +3409 -0
  37. package/dist/browser/types.d.ts +1184 -0
  38. package/dist/browser/types.js +1 -0
  39. package/dist/browser/utils.d.ts +1 -0
  40. package/dist/browser/utils.js +19 -0
  41. package/dist/browser/views.d.ts +78 -0
  42. package/dist/browser/views.js +72 -0
  43. package/dist/cli.d.ts +2 -0
  44. package/dist/cli.js +44 -0
  45. package/dist/config.d.ts +108 -0
  46. package/dist/config.js +430 -0
  47. package/dist/controller/index.d.ts +3 -0
  48. package/dist/controller/index.js +3 -0
  49. package/dist/controller/registry/index.d.ts +2 -0
  50. package/dist/controller/registry/index.js +2 -0
  51. package/dist/controller/registry/service.d.ts +45 -0
  52. package/dist/controller/registry/service.js +184 -0
  53. package/dist/controller/registry/views.d.ts +55 -0
  54. package/dist/controller/registry/views.js +174 -0
  55. package/dist/controller/service.d.ts +49 -0
  56. package/dist/controller/service.js +1176 -0
  57. package/dist/controller/views.d.ts +241 -0
  58. package/dist/controller/views.js +88 -0
  59. package/dist/dom/clickable-element-processor/service.d.ts +11 -0
  60. package/dist/dom/clickable-element-processor/service.js +60 -0
  61. package/dist/dom/dom_tree/index.js +1400 -0
  62. package/dist/dom/history-tree-processor/service.d.ts +14 -0
  63. package/dist/dom/history-tree-processor/service.js +75 -0
  64. package/dist/dom/history-tree-processor/view.d.ts +54 -0
  65. package/dist/dom/history-tree-processor/view.js +56 -0
  66. package/dist/dom/playground/extraction.d.ts +19 -0
  67. package/dist/dom/playground/extraction.js +187 -0
  68. package/dist/dom/playground/process-dom.d.ts +1 -0
  69. package/dist/dom/playground/process-dom.js +5 -0
  70. package/dist/dom/playground/test-accessibility.d.ts +44 -0
  71. package/dist/dom/playground/test-accessibility.js +111 -0
  72. package/dist/dom/service.d.ts +19 -0
  73. package/dist/dom/service.js +227 -0
  74. package/dist/dom/utils.d.ts +1 -0
  75. package/dist/dom/utils.js +6 -0
  76. package/dist/dom/views.d.ts +61 -0
  77. package/dist/dom/views.js +247 -0
  78. package/dist/event-bus.d.ts +11 -0
  79. package/dist/event-bus.js +19 -0
  80. package/dist/exceptions.d.ts +10 -0
  81. package/dist/exceptions.js +22 -0
  82. package/dist/filesystem/file-system.d.ts +68 -0
  83. package/dist/filesystem/file-system.js +412 -0
  84. package/dist/filesystem/index.d.ts +1 -0
  85. package/dist/filesystem/index.js +1 -0
  86. package/dist/index.d.ts +31 -0
  87. package/dist/index.js +33 -0
  88. package/dist/integrations/gmail/actions.d.ts +12 -0
  89. package/dist/integrations/gmail/actions.js +113 -0
  90. package/dist/integrations/gmail/index.d.ts +2 -0
  91. package/dist/integrations/gmail/index.js +2 -0
  92. package/dist/integrations/gmail/service.d.ts +61 -0
  93. package/dist/integrations/gmail/service.js +260 -0
  94. package/dist/llm/anthropic/chat.d.ts +28 -0
  95. package/dist/llm/anthropic/chat.js +126 -0
  96. package/dist/llm/anthropic/index.d.ts +2 -0
  97. package/dist/llm/anthropic/index.js +2 -0
  98. package/dist/llm/anthropic/serializer.d.ts +68 -0
  99. package/dist/llm/anthropic/serializer.js +285 -0
  100. package/dist/llm/aws/chat-anthropic.d.ts +61 -0
  101. package/dist/llm/aws/chat-anthropic.js +176 -0
  102. package/dist/llm/aws/chat-bedrock.d.ts +15 -0
  103. package/dist/llm/aws/chat-bedrock.js +80 -0
  104. package/dist/llm/aws/index.d.ts +3 -0
  105. package/dist/llm/aws/index.js +3 -0
  106. package/dist/llm/aws/serializer.d.ts +5 -0
  107. package/dist/llm/aws/serializer.js +68 -0
  108. package/dist/llm/azure/chat.d.ts +15 -0
  109. package/dist/llm/azure/chat.js +83 -0
  110. package/dist/llm/azure/index.d.ts +1 -0
  111. package/dist/llm/azure/index.js +1 -0
  112. package/dist/llm/base.d.ts +16 -0
  113. package/dist/llm/base.js +1 -0
  114. package/dist/llm/deepseek/chat.d.ts +15 -0
  115. package/dist/llm/deepseek/chat.js +51 -0
  116. package/dist/llm/deepseek/index.d.ts +2 -0
  117. package/dist/llm/deepseek/index.js +2 -0
  118. package/dist/llm/deepseek/serializer.d.ts +6 -0
  119. package/dist/llm/deepseek/serializer.js +57 -0
  120. package/dist/llm/exceptions.d.ts +10 -0
  121. package/dist/llm/exceptions.js +18 -0
  122. package/dist/llm/google/chat.d.ts +20 -0
  123. package/dist/llm/google/chat.js +144 -0
  124. package/dist/llm/google/index.d.ts +2 -0
  125. package/dist/llm/google/index.js +2 -0
  126. package/dist/llm/google/serializer.d.ts +6 -0
  127. package/dist/llm/google/serializer.js +64 -0
  128. package/dist/llm/groq/chat.d.ts +15 -0
  129. package/dist/llm/groq/chat.js +52 -0
  130. package/dist/llm/groq/index.d.ts +3 -0
  131. package/dist/llm/groq/index.js +3 -0
  132. package/dist/llm/groq/parser.d.ts +32 -0
  133. package/dist/llm/groq/parser.js +189 -0
  134. package/dist/llm/groq/serializer.d.ts +6 -0
  135. package/dist/llm/groq/serializer.js +56 -0
  136. package/dist/llm/messages.d.ts +77 -0
  137. package/dist/llm/messages.js +157 -0
  138. package/dist/llm/ollama/chat.d.ts +15 -0
  139. package/dist/llm/ollama/chat.js +77 -0
  140. package/dist/llm/ollama/index.d.ts +2 -0
  141. package/dist/llm/ollama/index.js +2 -0
  142. package/dist/llm/ollama/serializer.d.ts +6 -0
  143. package/dist/llm/ollama/serializer.js +53 -0
  144. package/dist/llm/openai/chat.d.ts +38 -0
  145. package/dist/llm/openai/chat.js +174 -0
  146. package/dist/llm/openai/index.d.ts +3 -0
  147. package/dist/llm/openai/index.js +3 -0
  148. package/dist/llm/openai/like.d.ts +17 -0
  149. package/dist/llm/openai/like.js +19 -0
  150. package/dist/llm/openai/serializer.d.ts +6 -0
  151. package/dist/llm/openai/serializer.js +57 -0
  152. package/dist/llm/openrouter/chat.d.ts +15 -0
  153. package/dist/llm/openrouter/chat.js +74 -0
  154. package/dist/llm/openrouter/index.d.ts +2 -0
  155. package/dist/llm/openrouter/index.js +2 -0
  156. package/dist/llm/openrouter/serializer.d.ts +3 -0
  157. package/dist/llm/openrouter/serializer.js +3 -0
  158. package/dist/llm/schema.d.ts +6 -0
  159. package/dist/llm/schema.js +77 -0
  160. package/dist/llm/views.d.ts +15 -0
  161. package/dist/llm/views.js +12 -0
  162. package/dist/logging-config.d.ts +25 -0
  163. package/dist/logging-config.js +89 -0
  164. package/dist/mcp/client.d.ts +142 -0
  165. package/dist/mcp/client.js +638 -0
  166. package/dist/mcp/controller.d.ts +6 -0
  167. package/dist/mcp/controller.js +38 -0
  168. package/dist/mcp/index.d.ts +3 -0
  169. package/dist/mcp/index.js +3 -0
  170. package/dist/mcp/server.d.ts +134 -0
  171. package/dist/mcp/server.js +759 -0
  172. package/dist/observability-decorators.d.ts +158 -0
  173. package/dist/observability-decorators.js +286 -0
  174. package/dist/observability.d.ts +23 -0
  175. package/dist/observability.js +58 -0
  176. package/dist/screenshots/index.d.ts +1 -0
  177. package/dist/screenshots/index.js +1 -0
  178. package/dist/screenshots/service.d.ts +6 -0
  179. package/dist/screenshots/service.js +28 -0
  180. package/dist/sync/auth.d.ts +27 -0
  181. package/dist/sync/auth.js +205 -0
  182. package/dist/sync/index.d.ts +2 -0
  183. package/dist/sync/index.js +2 -0
  184. package/dist/sync/service.d.ts +21 -0
  185. package/dist/sync/service.js +146 -0
  186. package/dist/telemetry/index.d.ts +2 -0
  187. package/dist/telemetry/index.js +2 -0
  188. package/dist/telemetry/service.d.ts +12 -0
  189. package/dist/telemetry/service.js +85 -0
  190. package/dist/telemetry/views.d.ts +112 -0
  191. package/dist/telemetry/views.js +112 -0
  192. package/dist/tokens/index.d.ts +2 -0
  193. package/dist/tokens/index.js +2 -0
  194. package/dist/tokens/service.d.ts +35 -0
  195. package/dist/tokens/service.js +423 -0
  196. package/dist/tokens/views.d.ts +58 -0
  197. package/dist/tokens/views.js +1 -0
  198. package/dist/utils.d.ts +128 -0
  199. package/dist/utils.js +529 -0
  200. package/package.json +94 -5
@@ -0,0 +1,2200 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import process from 'node:process';
5
+ import { config as loadEnv } from 'dotenv';
6
+ import { z } from 'zod';
7
+ import { createLogger } from '../logging-config.js';
8
+ import { CONFIG } from '../config.js';
9
+ import { EventBus } from '../event-bus.js';
10
+ import { uuid7str, SignalHandler, get_browser_use_version, } from '../utils.js';
11
+ import { Controller as DefaultController } from '../controller/service.js';
12
+ import { FileSystem as AgentFileSystem, DEFAULT_FILE_SYSTEM_PATH, } from '../filesystem/file-system.js';
13
+ import { SystemPrompt } from './prompts.js';
14
+ import { MessageManager } from './message-manager/service.js';
15
+ import { BrowserStateHistory } from '../browser/views.js';
16
+ import { BrowserSession } from '../browser/session.js';
17
+ import { BrowserProfile, DEFAULT_BROWSER_PROFILE, } from '../browser/profile.js';
18
+ import { HistoryTreeProcessor } from '../dom/history-tree-processor/service.js';
19
+ import { DOMHistoryElement } from '../dom/history-tree-processor/view.js';
20
+ import { UserMessage } from '../llm/messages.js';
21
+ import { ActionResult, AgentHistory, AgentHistoryList, AgentOutput, AgentState, AgentStepInfo, AgentError, StepMetadata, ActionModel, } from './views.js';
22
+ import { CreateAgentOutputFileEvent, CreateAgentSessionEvent, CreateAgentTaskEvent, CreateAgentStepEvent, UpdateAgentTaskEvent, } from './cloud-events.js';
23
+ import { create_history_gif } from './gif.js';
24
+ import { ScreenshotService } from '../screenshots/service.js';
25
+ import { productTelemetry } from '../telemetry/service.js';
26
+ import { AgentTelemetryEvent } from '../telemetry/views.js';
27
+ import { TokenCost } from '../tokens/service.js';
28
+ loadEnv();
29
+ const logger = createLogger('browser_use.agent');
30
+ export const log_response = (response, registry, logInstance = logger) => {
31
+ if (response.current_state.thinking) {
32
+ logInstance.info(`💡 Thinking:\n${response.current_state.thinking}`);
33
+ }
34
+ const evalGoal = response.current_state.evaluation_previous_goal;
35
+ if (evalGoal) {
36
+ let emoji = '❔';
37
+ if (evalGoal.toLowerCase().includes('success'))
38
+ emoji = '👍';
39
+ else if (evalGoal.toLowerCase().includes('failure'))
40
+ emoji = '⚠️';
41
+ logInstance.info(`${emoji} Eval: ${evalGoal}`);
42
+ }
43
+ if (response.current_state.memory) {
44
+ logInstance.info(`🧠 Memory: ${response.current_state.memory}`);
45
+ }
46
+ const nextGoal = response.current_state.next_goal;
47
+ if (nextGoal) {
48
+ logInstance.info(`🎯 Next goal: ${nextGoal}\n`);
49
+ }
50
+ else {
51
+ logInstance.info('');
52
+ }
53
+ };
54
+ class AsyncMutex {
55
+ locked = false;
56
+ waiters = [];
57
+ async acquire() {
58
+ if (!this.locked) {
59
+ this.locked = true;
60
+ let released = false;
61
+ return () => {
62
+ if (released) {
63
+ return;
64
+ }
65
+ released = true;
66
+ this.release();
67
+ };
68
+ }
69
+ await new Promise((resolve) => this.waiters.push(resolve));
70
+ this.locked = true;
71
+ let released = false;
72
+ return () => {
73
+ if (released) {
74
+ return;
75
+ }
76
+ released = true;
77
+ this.release();
78
+ };
79
+ }
80
+ release() {
81
+ const next = this.waiters.shift();
82
+ if (next) {
83
+ next();
84
+ return;
85
+ }
86
+ this.locked = false;
87
+ }
88
+ }
89
+ class ExecutionTimeoutError extends Error {
90
+ constructor() {
91
+ super('Operation timed out');
92
+ this.name = 'ExecutionTimeoutError';
93
+ }
94
+ }
95
+ const ensureDir = (target) => {
96
+ if (!fs.existsSync(target)) {
97
+ fs.mkdirSync(target, { recursive: true });
98
+ }
99
+ };
100
+ const defaultAgentOptions = () => ({
101
+ use_vision: true,
102
+ use_vision_for_planner: false,
103
+ save_conversation_path: null,
104
+ save_conversation_path_encoding: 'utf-8',
105
+ max_failures: 3,
106
+ retry_delay: 10,
107
+ override_system_message: null,
108
+ extend_system_message: null,
109
+ validate_output: false,
110
+ generate_gif: false,
111
+ available_file_paths: [],
112
+ include_attributes: undefined,
113
+ max_actions_per_step: 10,
114
+ use_thinking: true,
115
+ flash_mode: false,
116
+ max_history_items: null,
117
+ page_extraction_llm: null,
118
+ planner_llm: null,
119
+ planner_interval: 1,
120
+ is_planner_reasoning: false,
121
+ extend_planner_system_message: null,
122
+ context: null,
123
+ source: null,
124
+ file_system_path: null,
125
+ task_id: null,
126
+ cloud_sync: null,
127
+ calculate_cost: false,
128
+ display_files_in_done_text: true,
129
+ include_tool_call_examples: false,
130
+ session_attachment_mode: 'copy',
131
+ vision_detail_level: 'auto',
132
+ llm_timeout: 60,
133
+ step_timeout: 180,
134
+ });
135
+ const AgentLLMOutputSchema = z.object({
136
+ thinking: z.string().optional().nullable(),
137
+ evaluation_previous_goal: z.string().optional().nullable(),
138
+ memory: z.string().optional().nullable(),
139
+ next_goal: z.string().optional().nullable(),
140
+ action: z.array(z.record(z.string(), z.any())).optional().nullable().default([]),
141
+ });
142
+ const AgentLLMOutputFormat = AgentLLMOutputSchema;
143
+ AgentLLMOutputFormat.schema = AgentLLMOutputSchema;
144
+ export class Agent {
145
+ static _sharedSessionStepLocks = new Map();
146
+ static DEFAULT_AGENT_DATA_DIR = path.join(process.cwd(), DEFAULT_FILE_SYSTEM_PATH);
147
+ browser_session = null;
148
+ llm;
149
+ unfiltered_actions;
150
+ initial_actions;
151
+ register_new_step_callback;
152
+ register_done_callback;
153
+ register_external_agent_status_raise_error_callback;
154
+ context;
155
+ telemetry;
156
+ eventbus;
157
+ enable_cloud_sync;
158
+ cloud_sync = null;
159
+ file_system = null;
160
+ screenshot_service = null;
161
+ agent_directory;
162
+ _current_screenshot_path = null;
163
+ has_downloads_path = false;
164
+ _last_known_downloads = [];
165
+ version = 'unknown';
166
+ source = 'unknown';
167
+ step_start_time = 0;
168
+ _external_pause_event = {
169
+ resolve: null,
170
+ promise: Promise.resolve(),
171
+ };
172
+ output_model_schema;
173
+ id;
174
+ task_id;
175
+ session_id;
176
+ task;
177
+ controller;
178
+ settings;
179
+ token_cost_service;
180
+ state;
181
+ history;
182
+ _message_manager;
183
+ available_file_paths = [];
184
+ sensitive_data;
185
+ _logger = null;
186
+ _file_system_path = null;
187
+ agent_current_page = null;
188
+ _session_start_time = 0;
189
+ _task_start_time = 0;
190
+ _force_exit_telemetry_logged = false;
191
+ _closePromise = null;
192
+ _hasBrowserSessionClaim = false;
193
+ _sharedPinnedTabId = null;
194
+ _enforceDoneOnlyForCurrentStep = false;
195
+ system_prompt_class;
196
+ ActionModel = ActionModel;
197
+ AgentOutput = AgentOutput;
198
+ DoneActionModel = ActionModel;
199
+ DoneAgentOutput = AgentOutput;
200
+ constructor(params) {
201
+ 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', llm_timeout = 60, step_timeout = 180, } = { ...defaultAgentOptions(), ...params };
202
+ if (!llm) {
203
+ throw new Error('Invalid llm, must be provided');
204
+ }
205
+ const effectivePageExtractionLlm = page_extraction_llm ?? llm;
206
+ this.llm = llm;
207
+ this.id = task_id || uuid7str();
208
+ this.task_id = this.id;
209
+ this.session_id = uuid7str();
210
+ this.task = task;
211
+ this.output_model_schema = output_model_schema ?? null;
212
+ this.sensitive_data = sensitive_data;
213
+ this.available_file_paths = available_file_paths || [];
214
+ this.controller = (controller ??
215
+ new DefaultController({
216
+ display_files_in_done_text,
217
+ }));
218
+ this.initial_actions = initial_actions
219
+ ? this._convertInitialActions(initial_actions)
220
+ : null;
221
+ this.register_new_step_callback = register_new_step_callback;
222
+ this.register_done_callback = register_done_callback;
223
+ this.register_external_agent_status_raise_error_callback =
224
+ register_external_agent_status_raise_error_callback;
225
+ this.context = context;
226
+ this.agent_directory = Agent.DEFAULT_AGENT_DATA_DIR;
227
+ this.settings = {
228
+ use_vision,
229
+ vision_detail_level,
230
+ use_vision_for_planner: false,
231
+ save_conversation_path,
232
+ save_conversation_path_encoding,
233
+ max_failures,
234
+ retry_delay,
235
+ validate_output,
236
+ generate_gif,
237
+ override_system_message,
238
+ extend_system_message,
239
+ include_attributes: include_attributes ?? ['title', 'type', 'name'],
240
+ max_actions_per_step,
241
+ use_thinking,
242
+ flash_mode,
243
+ max_history_items,
244
+ page_extraction_llm: effectivePageExtractionLlm,
245
+ planner_llm: null,
246
+ planner_interval: 1,
247
+ is_planner_reasoning: false,
248
+ extend_planner_system_message: null,
249
+ calculate_cost,
250
+ include_tool_call_examples,
251
+ session_attachment_mode,
252
+ llm_timeout,
253
+ step_timeout,
254
+ };
255
+ this.token_cost_service = new TokenCost(calculate_cost);
256
+ if (calculate_cost) {
257
+ this.token_cost_service.initialize().catch((error) => {
258
+ this.logger.debug(`Failed to initialize token cost service: ${error.message}`);
259
+ });
260
+ }
261
+ this.token_cost_service.register_llm(llm);
262
+ this.token_cost_service.register_llm(effectivePageExtractionLlm);
263
+ this.state = params.injected_agent_state || new AgentState();
264
+ this.history = new AgentHistoryList([], null);
265
+ this.telemetry = productTelemetry;
266
+ this._file_system_path = file_system_path;
267
+ this.file_system = this._initFileSystem(file_system_path);
268
+ this._setScreenshotService();
269
+ this._setup_action_models();
270
+ this._set_browser_use_version_and_source(source);
271
+ this.browser_session = this._init_browser_session({
272
+ page,
273
+ browser,
274
+ browser_context,
275
+ browser_profile,
276
+ browser_session,
277
+ });
278
+ this.has_downloads_path = Boolean(this.browser_session?.browser_profile?.downloads_path);
279
+ if (this.has_downloads_path) {
280
+ this._last_known_downloads = [];
281
+ this.logger.info('📁 Initialized download tracking for agent');
282
+ }
283
+ 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);
284
+ 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);
285
+ this.unfiltered_actions = this.controller.registry.get_prompt_description();
286
+ this.eventbus = new EventBus(`Agent_${String(this.id).slice(-4)}`);
287
+ this.enable_cloud_sync = CONFIG.BROWSER_USE_CLOUD_SYNC;
288
+ if (this.enable_cloud_sync || cloud_sync) {
289
+ this.cloud_sync = cloud_sync ?? null;
290
+ if (this.cloud_sync) {
291
+ this.eventbus.on('*', this.cloud_sync.handle_event?.bind(this.cloud_sync) ?? (() => { }));
292
+ }
293
+ }
294
+ this._external_pause_event = {
295
+ resolve: null,
296
+ promise: Promise.resolve(),
297
+ };
298
+ this._session_start_time = 0;
299
+ this._task_start_time = 0;
300
+ this._force_exit_telemetry_logged = false;
301
+ // Security validation for sensitive_data and allowed_domains
302
+ this._validateSecuritySettings();
303
+ this._capture_shared_pinned_tab();
304
+ // LLM verification and setup
305
+ this._verifyAndSetupLlm();
306
+ // Model-specific vision handling
307
+ this._handleModelSpecificVision();
308
+ }
309
+ _createSessionIdWithAgentSuffix() {
310
+ const suffix = this.id.slice(-4);
311
+ const generated = uuid7str();
312
+ return `${generated.slice(0, -4)}${suffix}`;
313
+ }
314
+ _copyBrowserProfile(profile) {
315
+ const source = profile ?? DEFAULT_BROWSER_PROFILE;
316
+ const clonedConfig = typeof structuredClone === 'function'
317
+ ? structuredClone(source.config)
318
+ : JSON.parse(JSON.stringify(source.config));
319
+ return new BrowserProfile(clonedConfig);
320
+ }
321
+ _getBrowserContextFromPage(page, browser_context) {
322
+ if (!page) {
323
+ return browser_context;
324
+ }
325
+ const contextAttr = page.context;
326
+ if (typeof contextAttr === 'function') {
327
+ try {
328
+ const resolved = contextAttr.call(page);
329
+ return resolved ?? browser_context;
330
+ }
331
+ catch {
332
+ return browser_context;
333
+ }
334
+ }
335
+ return contextAttr ?? browser_context;
336
+ }
337
+ _claim_or_isolate_browser_session(browser_session) {
338
+ const claimMode = this.settings.session_attachment_mode === 'shared'
339
+ ? 'shared'
340
+ : 'exclusive';
341
+ this._hasBrowserSessionClaim = false;
342
+ const claimSession = (session) => {
343
+ const claimFn = session.claim_agent ?? session.claimAgent;
344
+ if (typeof claimFn !== 'function') {
345
+ if (this.settings.session_attachment_mode === 'strict' ||
346
+ this.settings.session_attachment_mode === 'shared') {
347
+ throw new Error(`session_attachment_mode='${this.settings.session_attachment_mode}' requires BrowserSession.claim_agent()/release_agent() support.`);
348
+ }
349
+ return 'noop';
350
+ }
351
+ const claimed = Boolean(claimFn.call(session, this.id, claimMode));
352
+ return claimed ? 'claimed' : 'failed';
353
+ };
354
+ const getAttachedAgentIds = (session) => {
355
+ const pluralGetter = session.get_attached_agent_ids ??
356
+ session.getAttachedAgentIds;
357
+ if (typeof pluralGetter === 'function') {
358
+ const value = pluralGetter.call(session);
359
+ if (Array.isArray(value)) {
360
+ return value.filter((item) => typeof item === 'string');
361
+ }
362
+ }
363
+ const singleGetter = session.get_attached_agent_id ??
364
+ session.getAttachedAgentId;
365
+ if (typeof singleGetter !== 'function') {
366
+ return [];
367
+ }
368
+ const value = singleGetter.call(session);
369
+ return typeof value === 'string' ? [value] : [];
370
+ };
371
+ const claimResult = claimSession(browser_session);
372
+ if (claimResult !== 'failed') {
373
+ this._hasBrowserSessionClaim = claimResult === 'claimed';
374
+ return browser_session;
375
+ }
376
+ const currentOwners = getAttachedAgentIds(browser_session);
377
+ const ownerLabel = currentOwners.length > 0 ? currentOwners.join(', ') : 'unknown';
378
+ if (this.settings.session_attachment_mode === 'strict') {
379
+ throw new Error(`BrowserSession is already attached to Agent ${ownerLabel}. Set session_attachment_mode='copy' to allow automatic isolation.`);
380
+ }
381
+ if (this.settings.session_attachment_mode === 'shared') {
382
+ throw new Error(`BrowserSession is already attached in exclusive mode by Agent ${ownerLabel}. Configure all participating agents with session_attachment_mode='shared' or use session_attachment_mode='copy'.`);
383
+ }
384
+ this.logger.warning(`⚠️ BrowserSession is already attached to Agent ${ownerLabel}. Creating an isolated copy for this Agent.`);
385
+ const modelCopyFn = browser_session.model_copy ?? browser_session.modelCopy;
386
+ if (typeof modelCopyFn !== 'function') {
387
+ throw new Error(`BrowserSession is attached to another Agent (${ownerLabel}) and cannot be safely reused. Provide a separate BrowserSession.`);
388
+ }
389
+ const isolated = modelCopyFn.call(browser_session);
390
+ const isolatedClaimResult = claimSession(isolated);
391
+ if (isolatedClaimResult === 'failed') {
392
+ throw new Error('Failed to claim isolated BrowserSession for current Agent');
393
+ }
394
+ this._hasBrowserSessionClaim = isolatedClaimResult === 'claimed';
395
+ return isolated;
396
+ }
397
+ _release_browser_session_claim(browser_session) {
398
+ if (!browser_session || !this._hasBrowserSessionClaim) {
399
+ return;
400
+ }
401
+ const releaseFn = browser_session.release_agent ??
402
+ browser_session.releaseAgent;
403
+ if (typeof releaseFn !== 'function') {
404
+ return;
405
+ }
406
+ const released = releaseFn.call(browser_session, this.id);
407
+ if (!released) {
408
+ this.logger.warning('⚠️ BrowserSession claim was not released because it is currently attached to another Agent.');
409
+ }
410
+ this._hasBrowserSessionClaim = false;
411
+ }
412
+ _has_any_browser_session_attachments(browser_session) {
413
+ if (!browser_session) {
414
+ return false;
415
+ }
416
+ const pluralGetter = browser_session.get_attached_agent_ids ??
417
+ browser_session.getAttachedAgentIds;
418
+ if (typeof pluralGetter === 'function') {
419
+ const value = pluralGetter.call(browser_session);
420
+ if (Array.isArray(value)) {
421
+ return value.some((item) => typeof item === 'string');
422
+ }
423
+ }
424
+ const singleGetter = browser_session.get_attached_agent_id ??
425
+ browser_session.getAttachedAgentId;
426
+ if (typeof singleGetter !== 'function') {
427
+ return false;
428
+ }
429
+ return typeof singleGetter.call(browser_session) === 'string';
430
+ }
431
+ _is_shared_session_mode() {
432
+ return this.settings.session_attachment_mode === 'shared';
433
+ }
434
+ _capture_shared_pinned_tab() {
435
+ if (!this._is_shared_session_mode() || !this.browser_session) {
436
+ return;
437
+ }
438
+ const activeTab = this.browser_session.active_tab;
439
+ const pageId = activeTab?.page_id;
440
+ if (typeof pageId === 'number') {
441
+ this._sharedPinnedTabId = pageId;
442
+ }
443
+ }
444
+ async _restore_shared_pinned_tab_if_needed() {
445
+ if (!this._is_shared_session_mode() || !this.browser_session) {
446
+ return;
447
+ }
448
+ const switchFn = this.browser_session.switch_to_tab ??
449
+ this.browser_session.switchToTab;
450
+ if (typeof switchFn !== 'function') {
451
+ return;
452
+ }
453
+ if (this._sharedPinnedTabId == null) {
454
+ this._capture_shared_pinned_tab();
455
+ return;
456
+ }
457
+ try {
458
+ await switchFn.call(this.browser_session, this._sharedPinnedTabId);
459
+ }
460
+ catch {
461
+ this._capture_shared_pinned_tab();
462
+ }
463
+ }
464
+ async _run_with_shared_session_step_lock(callback) {
465
+ if (!this._is_shared_session_mode() || !this.browser_session) {
466
+ return callback();
467
+ }
468
+ const sessionId = this.browser_session.id;
469
+ let lock = Agent._sharedSessionStepLocks.get(sessionId);
470
+ if (!lock) {
471
+ lock = new AsyncMutex();
472
+ Agent._sharedSessionStepLocks.set(sessionId, lock);
473
+ }
474
+ const release = await lock.acquire();
475
+ try {
476
+ return await callback();
477
+ }
478
+ finally {
479
+ release();
480
+ }
481
+ }
482
+ _cleanup_shared_session_step_lock_if_unused(browser_session) {
483
+ if (!browser_session) {
484
+ return;
485
+ }
486
+ if (this._has_any_browser_session_attachments(browser_session)) {
487
+ return;
488
+ }
489
+ Agent._sharedSessionStepLocks.delete(browser_session.id);
490
+ }
491
+ _init_browser_session(init) {
492
+ let { page, browser, browser_context, browser_profile, browser_session, } = init;
493
+ if (browser instanceof BrowserSession) {
494
+ browser_session = browser_session ?? browser;
495
+ browser = null;
496
+ }
497
+ if (browser_session) {
498
+ const ownsResources = browser_session._owns_browser_resources;
499
+ if (ownsResources === false &&
500
+ this.settings.session_attachment_mode === 'copy') {
501
+ this.logger.warning("⚠️ Non-owning BrowserSession detected. session_attachment_mode='copy' will isolate this Agent with a cloned BrowserSession.");
502
+ const modelCopyFn = browser_session.model_copy ??
503
+ browser_session.modelCopy;
504
+ if (typeof modelCopyFn === 'function') {
505
+ const isolated = modelCopyFn.call(browser_session);
506
+ return this._claim_or_isolate_browser_session(isolated);
507
+ }
508
+ }
509
+ return this._claim_or_isolate_browser_session(browser_session);
510
+ }
511
+ const resolvedContext = this._getBrowserContextFromPage(page, browser_context);
512
+ const resolvedProfile = this._copyBrowserProfile(browser_profile);
513
+ return this._claim_or_isolate_browser_session(new BrowserSession({
514
+ browser_profile: resolvedProfile,
515
+ browser: browser ?? null,
516
+ browser_context: resolvedContext,
517
+ page,
518
+ id: this._createSessionIdWithAgentSuffix(),
519
+ }));
520
+ }
521
+ _sleep_blocking(ms) {
522
+ if (ms <= 0) {
523
+ return;
524
+ }
525
+ if (typeof SharedArrayBuffer === 'function' && Atomics?.wait) {
526
+ const lock = new Int32Array(new SharedArrayBuffer(4));
527
+ Atomics.wait(lock, 0, 0, ms);
528
+ return;
529
+ }
530
+ const end = Date.now() + ms;
531
+ while (Date.now() < end) {
532
+ // Intentional busy-wait fallback for runtimes without Atomics.wait.
533
+ }
534
+ }
535
+ /**
536
+ * Convert dictionary-based actions to ActionModel instances
537
+ */
538
+ _convertInitialActions(actions) {
539
+ const convertedActions = [];
540
+ for (const actionDict of actions) {
541
+ // Each actionDict should have a single key-value pair
542
+ const actionName = Object.keys(actionDict)[0];
543
+ const params = actionDict[actionName];
544
+ try {
545
+ // Get the parameter model for this action from registry
546
+ const actionInfo = this.controller.registry.get_all_actions().get(actionName) ?? null;
547
+ if (!actionInfo) {
548
+ this.logger.warning(`⚠️ Unknown action "${actionName}" in initial_actions, skipping`);
549
+ continue;
550
+ }
551
+ const paramModel = actionInfo.paramSchema;
552
+ if (!paramModel) {
553
+ this.logger.warning(`⚠️ No parameter model for action "${actionName}", using raw params`);
554
+ convertedActions.push(actionDict);
555
+ continue;
556
+ }
557
+ // Validate parameters using Zod schema
558
+ const validatedParams = paramModel.parse(params);
559
+ // Create action with validated parameters
560
+ convertedActions.push({ [actionName]: validatedParams });
561
+ }
562
+ catch (error) {
563
+ this.logger.error(`❌ Failed to validate initial action "${actionName}": ${error}`);
564
+ // Skip invalid actions
565
+ continue;
566
+ }
567
+ }
568
+ return convertedActions;
569
+ }
570
+ /**
571
+ * Handle model-specific vision capabilities
572
+ * Some models like DeepSeek and Grok don't support vision yet
573
+ */
574
+ _handleModelSpecificVision() {
575
+ const modelName = this.llm.model?.toLowerCase() || '';
576
+ // Handle DeepSeek models
577
+ if (modelName.includes('deepseek') && this.settings.use_vision) {
578
+ this.logger.warning('⚠️ DeepSeek models do not support use_vision=True yet. Setting use_vision=False for now...');
579
+ this.settings.use_vision = false;
580
+ }
581
+ // Handle XAI (Grok) models
582
+ if (modelName.includes('grok') && this.settings.use_vision) {
583
+ this.logger.warning('⚠️ XAI models do not support use_vision=True yet. Setting use_vision=False for now...');
584
+ this.settings.use_vision = false;
585
+ }
586
+ }
587
+ /**
588
+ * Verify that the LLM API keys are setup and the LLM API is responding properly.
589
+ * Also handles model capability detection.
590
+ */
591
+ _verifyAndSetupLlm() {
592
+ // Skip verification if already done or if configured to skip
593
+ if (this.llm._verified_api_keys === true ||
594
+ CONFIG.SKIP_LLM_API_KEY_VERIFICATION) {
595
+ this.llm._verified_api_keys = true;
596
+ return true;
597
+ }
598
+ // Mark as verified
599
+ this.llm._verified_api_keys = true;
600
+ // Log LLM information
601
+ this.logger.debug(`🤖 Using LLM: ${this.llm.model || 'unknown model'}`);
602
+ return true;
603
+ }
604
+ /**
605
+ * Validates security settings when sensitive_data is provided
606
+ * Checks if allowed_domains is properly configured to prevent credential leakage
607
+ */
608
+ _validateSecuritySettings() {
609
+ if (!this.sensitive_data) {
610
+ return;
611
+ }
612
+ // Check if sensitive_data has domain-specific credentials
613
+ const hasDomainSpecificCredentials = Object.values(this.sensitive_data).some((value) => typeof value === 'object' && value !== null);
614
+ const allowedDomainsConfig = this.browser_session?.browser_profile?.config?.allowed_domains;
615
+ const hasAllowedDomains = Array.isArray(allowedDomainsConfig)
616
+ ? allowedDomainsConfig.length > 0
617
+ : Boolean(allowedDomainsConfig);
618
+ // If no allowed_domains are configured, show a security warning
619
+ if (!hasAllowedDomains) {
620
+ this.logger.error('⚠️⚠️⚠️ Agent(sensitive_data=••••••••) was provided but BrowserSession(allowed_domains=[...]) is not locked down! ⚠️⚠️⚠️\n' +
621
+ ' ☠️ If the agent visits a malicious website and encounters a prompt-injection attack, your sensitive_data may be exposed!\n\n' +
622
+ ' https://docs.browser-use.com/customize/browser-settings#restrict-urls\n' +
623
+ 'Waiting 10 seconds before continuing... Press [Ctrl+C] to abort.');
624
+ // Check if we're in an interactive shell (TTY)
625
+ if (process.stdin.isTTY) {
626
+ // Block startup for 10 seconds to match Python warning behavior.
627
+ // User can still abort process with Ctrl+C.
628
+ this._sleep_blocking(10_000);
629
+ }
630
+ this.logger.warning('‼️ Continuing with insecure settings for now... but this will become a hard error in the future!');
631
+ }
632
+ // If we're using domain-specific credentials, validate domain patterns
633
+ else if (hasDomainSpecificCredentials) {
634
+ const allowedDomains = this.browser_session.browser_profile.config.allowed_domains;
635
+ // Get domain patterns from sensitive_data where value is an object
636
+ const domainPatterns = Object.keys(this.sensitive_data).filter((key) => typeof this.sensitive_data[key] === 'object' &&
637
+ this.sensitive_data[key] !== null);
638
+ // Validate each domain pattern against allowed_domains
639
+ for (const domainPattern of domainPatterns) {
640
+ let isAllowed = false;
641
+ for (const allowedDomain of allowedDomains) {
642
+ // Special cases that don't require URL matching
643
+ if (domainPattern === allowedDomain || allowedDomain === '*') {
644
+ isAllowed = true;
645
+ break;
646
+ }
647
+ // Extract the domain parts, ignoring scheme
648
+ const patternDomain = domainPattern.includes('://')
649
+ ? domainPattern.split('://')[1]
650
+ : domainPattern;
651
+ const allowedDomainPart = allowedDomain.includes('://')
652
+ ? allowedDomain.split('://')[1]
653
+ : allowedDomain;
654
+ // Check if pattern is covered by an allowed domain
655
+ // Example: "google.com" is covered by "*.google.com"
656
+ if (patternDomain === allowedDomainPart ||
657
+ (allowedDomainPart.startsWith('*.') &&
658
+ (patternDomain === allowedDomainPart.slice(2) ||
659
+ patternDomain.endsWith('.' + allowedDomainPart.slice(2))))) {
660
+ isAllowed = true;
661
+ break;
662
+ }
663
+ }
664
+ if (!isAllowed) {
665
+ this.logger.warning(`⚠️ Domain pattern "${domainPattern}" in sensitive_data is not covered by any pattern in allowed_domains=${JSON.stringify(allowedDomains)}\n` +
666
+ ` This may be a security risk as credentials could be used on unintended domains.`);
667
+ }
668
+ }
669
+ }
670
+ }
671
+ _initFileSystem(file_system_path) {
672
+ if (this.state.file_system_state && file_system_path) {
673
+ throw new Error('Cannot provide both file_system_state (from agent state) and file_system_path. Restore from state or create new file system, not both.');
674
+ }
675
+ if (this.state.file_system_state) {
676
+ try {
677
+ this.file_system = AgentFileSystem.from_state_sync(this.state.file_system_state);
678
+ this._file_system_path = this.state.file_system_state.base_dir;
679
+ this.logger.info(`💾 File system restored from state to: ${this._file_system_path}`);
680
+ const timestamp = Date.now();
681
+ this.agent_directory = path.join(os.tmpdir(), `browser_use_agent_${this.id}_${timestamp}`);
682
+ ensureDir(this.agent_directory);
683
+ return this.file_system;
684
+ }
685
+ catch (error) {
686
+ const message = error instanceof Error ? error.message : String(error);
687
+ this.logger.error(`💾 Failed to restore file system from state: ${message}`);
688
+ throw error;
689
+ }
690
+ }
691
+ const baseDir = file_system_path ?? path.join(Agent.DEFAULT_AGENT_DATA_DIR, this.task_id);
692
+ ensureDir(baseDir);
693
+ try {
694
+ this.file_system = new AgentFileSystem(baseDir);
695
+ this._file_system_path = baseDir;
696
+ }
697
+ catch (error) {
698
+ const message = error instanceof Error ? error.message : String(error);
699
+ this.logger.error(`💾 Failed to initialize file system: ${message}`);
700
+ throw error;
701
+ }
702
+ const timestamp = Date.now();
703
+ this.agent_directory = path.join(os.tmpdir(), `browser_use_agent_${this.id}_${timestamp}`);
704
+ ensureDir(this.agent_directory);
705
+ this.state.file_system_state = this.file_system.get_state();
706
+ this.logger.info(`💾 File system path: ${this._file_system_path}`);
707
+ return this.file_system;
708
+ }
709
+ _setScreenshotService() {
710
+ try {
711
+ this.screenshot_service = new ScreenshotService(this.agent_directory);
712
+ this.logger.info(`📸 Screenshot service initialized in: ${path.join(this.agent_directory, 'screenshots')}`);
713
+ }
714
+ catch (error) {
715
+ const message = error instanceof Error ? error.message : String(error);
716
+ this.logger.error(`📸 Failed to initialize screenshot service: ${message}`);
717
+ throw error;
718
+ }
719
+ }
720
+ get logger() {
721
+ if (!this._logger) {
722
+ const browserSessionId = (this.browser_session && this.browser_session.id) || this.id;
723
+ this._logger = createLogger(`browser_use.Agent🅰 ${this.task_id.slice(-4)} on 🆂 ${String(browserSessionId).slice(-4)}`);
724
+ }
725
+ return this._logger;
726
+ }
727
+ get message_manager() {
728
+ return this._message_manager;
729
+ }
730
+ /**
731
+ * Get the browser instance from the browser session
732
+ */
733
+ get browser() {
734
+ if (!this.browser_session) {
735
+ throw new Error('BrowserSession is not set up');
736
+ }
737
+ if (!this.browser_session.browser) {
738
+ throw new Error('Browser is not set up');
739
+ }
740
+ return this.browser_session.browser;
741
+ }
742
+ /**
743
+ * Get the browser context from the browser session
744
+ */
745
+ get browserContext() {
746
+ if (!this.browser_session) {
747
+ throw new Error('BrowserSession is not set up');
748
+ }
749
+ if (!this.browser_session.browser_context) {
750
+ throw new Error('BrowserContext is not set up');
751
+ }
752
+ return this.browser_session.browser_context;
753
+ }
754
+ /**
755
+ * Get the browser profile from the browser session
756
+ */
757
+ get browserProfile() {
758
+ if (!this.browser_session) {
759
+ throw new Error('BrowserSession is not set up');
760
+ }
761
+ return this.browser_session.browser_profile;
762
+ }
763
+ /**
764
+ * Add a new task to the agent, keeping the same task_id as tasks are continuous
765
+ */
766
+ addNewTask(newTask) {
767
+ // Simply delegate to message manager - no need for new task_id or events
768
+ // The task continues with new instructions, it doesn't end and start a new one
769
+ this.task = newTask;
770
+ this._message_manager.add_new_task(newTask);
771
+ }
772
+ /**
773
+ * Take a step and return whether the task is done and valid
774
+ * @returns Tuple of [is_done, is_valid]
775
+ */
776
+ async takeStep(stepInfo) {
777
+ await this._step(stepInfo ?? null);
778
+ if (this.history.is_done()) {
779
+ await this.log_completion();
780
+ if (this.register_done_callback) {
781
+ await this.register_done_callback(this.history);
782
+ }
783
+ return [true, true];
784
+ }
785
+ return [false, false];
786
+ }
787
+ /**
788
+ * Remove think tags from text
789
+ */
790
+ _removeThinkTags(text) {
791
+ const THINK_TAGS = /<think>.*?<\/think>/gs;
792
+ const STRAY_CLOSE_TAG = /.*?<\/think>/gs;
793
+ // Step 1: Remove well-formed <think>...</think>
794
+ text = text.replace(THINK_TAGS, '');
795
+ // Step 2: If there's an unmatched closing tag </think>,
796
+ // remove everything up to and including that.
797
+ text = text.replace(STRAY_CLOSE_TAG, '');
798
+ return text.trim();
799
+ }
800
+ /**
801
+ * Log a comprehensive summary of the next action(s)
802
+ */
803
+ _logNextActionSummary(parsed) {
804
+ if (!parsed.action || parsed.action.length === 0) {
805
+ return;
806
+ }
807
+ const actionCount = parsed.action.length;
808
+ // Collect action details
809
+ const actionDetails = [];
810
+ let lastActionName = 'unknown';
811
+ let lastParamStr = '';
812
+ for (const action of parsed.action) {
813
+ const actionData = action.model_dump();
814
+ const actionName = Object.keys(actionData)[0] || 'unknown';
815
+ const actionParams = actionData[actionName] || {};
816
+ // Format key parameters concisely
817
+ const paramSummary = [];
818
+ if (typeof actionParams === 'object' && actionParams !== null) {
819
+ for (const [key, value] of Object.entries(actionParams)) {
820
+ if (key === 'index') {
821
+ paramSummary.push(`#${value}`);
822
+ }
823
+ else if (key === 'text' && typeof value === 'string') {
824
+ const textPreview = value.length > 30 ? value.slice(0, 30) + '...' : value;
825
+ paramSummary.push(`text="${textPreview}"`);
826
+ }
827
+ else if (key === 'url') {
828
+ paramSummary.push(`url="${value}"`);
829
+ }
830
+ else if (key === 'success') {
831
+ paramSummary.push(`success=${value}`);
832
+ }
833
+ else if (typeof value === 'string' ||
834
+ typeof value === 'number' ||
835
+ typeof value === 'boolean') {
836
+ const valStr = String(value);
837
+ const truncatedVal = valStr.length > 30 ? valStr.slice(0, 30) + '...' : valStr;
838
+ paramSummary.push(`${key}=${truncatedVal}`);
839
+ }
840
+ }
841
+ }
842
+ const paramStr = paramSummary.length > 0 ? `(${paramSummary.join(', ')})` : '';
843
+ actionDetails.push(`${actionName}${paramStr}`);
844
+ lastActionName = actionName;
845
+ lastParamStr = paramStr;
846
+ }
847
+ // Create summary based on single vs multi-action
848
+ if (actionCount === 1) {
849
+ this.logger.info(`☝️ Decided next action: ${lastActionName}${lastParamStr}`);
850
+ }
851
+ else {
852
+ const summaryLines = [`✌️ Decided next ${actionCount} multi-actions:`];
853
+ for (let i = 0; i < actionDetails.length; i++) {
854
+ summaryLines.push(` ${i + 1}. ${actionDetails[i]}`);
855
+ }
856
+ this.logger.info(summaryLines.join('\n'));
857
+ }
858
+ }
859
+ _set_browser_use_version_and_source(sourceOverride) {
860
+ const version = get_browser_use_version();
861
+ let source = 'npm';
862
+ try {
863
+ const projectRoot = process.cwd();
864
+ const repoIndicators = ['.git', 'README.md', 'docs', 'examples'];
865
+ if (repoIndicators.every((indicator) => fs.existsSync(path.join(projectRoot, indicator)))) {
866
+ source = 'git';
867
+ }
868
+ }
869
+ catch (error) {
870
+ this.logger.debug(`Error determining browser-use source: ${error.message}`);
871
+ source = 'unknown';
872
+ }
873
+ if (sourceOverride) {
874
+ source = sourceOverride;
875
+ }
876
+ this.version = version;
877
+ this.source = source;
878
+ }
879
+ /**
880
+ * Setup dynamic action models from controller's registry
881
+ * Initially only include actions with no filters
882
+ */
883
+ _setup_action_models() {
884
+ // Initially only include actions with no filters
885
+ this.ActionModel = this.controller.registry.create_action_model();
886
+ // Create output model with the dynamic actions
887
+ if (this.settings.flash_mode) {
888
+ this.AgentOutput = AgentOutput.type_with_custom_actions_flash_mode(this.ActionModel);
889
+ }
890
+ else if (this.settings.use_thinking) {
891
+ this.AgentOutput = AgentOutput.type_with_custom_actions(this.ActionModel);
892
+ }
893
+ else {
894
+ this.AgentOutput = AgentOutput.type_with_custom_actions_no_thinking(this.ActionModel);
895
+ }
896
+ // Used to force the done action when max_steps is reached
897
+ this.DoneActionModel = this.controller.registry.create_action_model({
898
+ include_actions: ['done'],
899
+ });
900
+ if (this.settings.flash_mode) {
901
+ this.DoneAgentOutput = AgentOutput.type_with_custom_actions_flash_mode(this.DoneActionModel);
902
+ }
903
+ else if (this.settings.use_thinking) {
904
+ this.DoneAgentOutput = AgentOutput.type_with_custom_actions(this.DoneActionModel);
905
+ }
906
+ else {
907
+ this.DoneAgentOutput = AgentOutput.type_with_custom_actions_no_thinking(this.DoneActionModel);
908
+ }
909
+ }
910
+ /**
911
+ * Update action models with page-specific actions
912
+ * Called during each step to filter actions based on current page context
913
+ */
914
+ async _updateActionModelsForPage(page) {
915
+ // Create new action model with current page's filtered actions
916
+ this.ActionModel = this.controller.registry.create_action_model({ page });
917
+ // Update output model with the new actions
918
+ if (this.settings.flash_mode) {
919
+ this.AgentOutput = AgentOutput.type_with_custom_actions_flash_mode(this.ActionModel);
920
+ }
921
+ else if (this.settings.use_thinking) {
922
+ this.AgentOutput = AgentOutput.type_with_custom_actions(this.ActionModel);
923
+ }
924
+ else {
925
+ this.AgentOutput = AgentOutput.type_with_custom_actions_no_thinking(this.ActionModel);
926
+ }
927
+ // Update done action model too
928
+ this.DoneActionModel = this.controller.registry.create_action_model({
929
+ include_actions: ['done'],
930
+ page,
931
+ });
932
+ if (this.settings.flash_mode) {
933
+ this.DoneAgentOutput = AgentOutput.type_with_custom_actions_flash_mode(this.DoneActionModel);
934
+ }
935
+ else if (this.settings.use_thinking) {
936
+ this.DoneAgentOutput = AgentOutput.type_with_custom_actions(this.DoneActionModel);
937
+ }
938
+ else {
939
+ this.DoneAgentOutput = AgentOutput.type_with_custom_actions_no_thinking(this.DoneActionModel);
940
+ }
941
+ }
942
+ async run(max_steps = 100, on_step_start = null, on_step_end = null) {
943
+ let agent_run_error = null;
944
+ this._force_exit_telemetry_logged = false;
945
+ const signal_handler = new SignalHandler({
946
+ pause_callback: this.pause.bind(this),
947
+ resume_callback: this.resume.bind(this),
948
+ custom_exit_callback: () => {
949
+ this._log_agent_event(max_steps, 'SIGINT: Cancelled by user');
950
+ this.telemetry?.flush?.();
951
+ this._force_exit_telemetry_logged = true;
952
+ },
953
+ exit_on_second_int: true,
954
+ });
955
+ signal_handler.register();
956
+ try {
957
+ this._log_agent_run();
958
+ 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'}`);
959
+ this._session_start_time = Date.now() / 1000;
960
+ this._task_start_time = this._session_start_time;
961
+ this.logger.debug('📡 Dispatching CreateAgentSessionEvent...');
962
+ this.eventbus.dispatch(CreateAgentSessionEvent.fromAgent(this));
963
+ this.logger.debug('📡 Dispatching CreateAgentTaskEvent...');
964
+ this.eventbus.dispatch(CreateAgentTaskEvent.fromAgent(this));
965
+ if (this.initial_actions?.length) {
966
+ this.logger.debug(`⚡ Executing ${this.initial_actions.length} initial actions...`);
967
+ const result = await this.multi_act(this.initial_actions, {
968
+ check_for_new_elements: false,
969
+ });
970
+ this.state.last_result = result;
971
+ this.logger.debug('✅ Initial actions completed');
972
+ }
973
+ this.logger.debug(`🔄 Starting main execution loop with max ${max_steps} steps...`);
974
+ for (let step = 0; step < max_steps; step += 1) {
975
+ if (this.state.paused) {
976
+ this.logger.debug(`⏸️ Step ${step}: Agent paused, waiting to resume...`);
977
+ await this.wait_until_resumed();
978
+ signal_handler.reset();
979
+ }
980
+ if (this.state.consecutive_failures >= this.settings.max_failures) {
981
+ this.logger.error(`❌ Stopping due to ${this.settings.max_failures} consecutive failures`);
982
+ agent_run_error = `Stopped due to ${this.settings.max_failures} consecutive failures`;
983
+ break;
984
+ }
985
+ if (this.state.stopped) {
986
+ this.logger.info('🛑 Agent stopped');
987
+ agent_run_error = 'Agent stopped programmatically';
988
+ break;
989
+ }
990
+ if (this.register_external_agent_status_raise_error_callback) {
991
+ const shouldRaise = await this.register_external_agent_status_raise_error_callback();
992
+ if (shouldRaise) {
993
+ agent_run_error = 'Agent stopped due to external request';
994
+ break;
995
+ }
996
+ }
997
+ if (on_step_start) {
998
+ await on_step_start(this);
999
+ }
1000
+ this.logger.debug(`🚶 Starting step ${step + 1}/${max_steps}...`);
1001
+ const step_info = new AgentStepInfo(step, max_steps);
1002
+ const stepAbortController = new AbortController();
1003
+ try {
1004
+ await this._executeWithTimeout(this._step(step_info, stepAbortController.signal), this.settings.step_timeout ?? 0, () => stepAbortController.abort());
1005
+ this.logger.debug(`✅ Completed step ${step + 1}/${max_steps}`);
1006
+ }
1007
+ catch (error) {
1008
+ const message = error instanceof Error ? error.message : String(error);
1009
+ const isTimeout = error instanceof ExecutionTimeoutError;
1010
+ if (isTimeout) {
1011
+ const timeoutMessage = `Step ${step + 1} timed out after ${this.settings.step_timeout} seconds`;
1012
+ this.logger.error(`⏰ ${timeoutMessage}`);
1013
+ this.state.consecutive_failures += 1;
1014
+ this.state.last_result = [
1015
+ new ActionResult({ error: timeoutMessage }),
1016
+ ];
1017
+ // JavaScript promises are not force-cancelable; stop the run loop
1018
+ // immediately to avoid overlapping timed-out steps with new steps.
1019
+ this.stop();
1020
+ agent_run_error = timeoutMessage;
1021
+ break;
1022
+ }
1023
+ this.logger.error(`❌ Unhandled step error at step ${step + 1}: ${message}`);
1024
+ this.state.consecutive_failures += 1;
1025
+ this.state.last_result = [
1026
+ new ActionResult({
1027
+ error: message || `Unhandled step error at step ${step + 1}`,
1028
+ }),
1029
+ ];
1030
+ }
1031
+ if (on_step_end) {
1032
+ await on_step_end(this);
1033
+ }
1034
+ if (this.history.is_done()) {
1035
+ this.logger.debug(`🎯 Task completed after ${step + 1} steps!`);
1036
+ await this.log_completion();
1037
+ if (this.register_done_callback) {
1038
+ const maybePromise = this.register_done_callback(this.history);
1039
+ if (maybePromise &&
1040
+ typeof maybePromise.then === 'function') {
1041
+ await maybePromise;
1042
+ }
1043
+ }
1044
+ break;
1045
+ }
1046
+ if (step === max_steps - 1) {
1047
+ agent_run_error = 'Failed to complete task in maximum steps';
1048
+ this.history.add_item(new AgentHistory(null, [
1049
+ new ActionResult({
1050
+ error: agent_run_error,
1051
+ include_in_memory: true,
1052
+ }),
1053
+ ], new BrowserStateHistory('', '', [], [], null), null));
1054
+ this.logger.info(`❌ ${agent_run_error}`);
1055
+ }
1056
+ }
1057
+ this.logger.debug('📊 Collecting usage summary...');
1058
+ this.history.usage =
1059
+ (await this.token_cost_service.get_usage_summary());
1060
+ if (!this.history._output_model_schema && this.output_model_schema) {
1061
+ this.history._output_model_schema = this.output_model_schema;
1062
+ }
1063
+ this.logger.debug('🏁 Agent.run() completed successfully');
1064
+ return this.history;
1065
+ }
1066
+ catch (error) {
1067
+ agent_run_error = error instanceof Error ? error.message : String(error);
1068
+ this.logger.error(`Agent run failed with exception: ${agent_run_error}`);
1069
+ throw error;
1070
+ }
1071
+ finally {
1072
+ await this.token_cost_service.log_usage_summary();
1073
+ signal_handler.unregister();
1074
+ if (!this._force_exit_telemetry_logged) {
1075
+ try {
1076
+ this._log_agent_event(max_steps, agent_run_error);
1077
+ }
1078
+ catch (logError) {
1079
+ this.logger.error(`Failed to log telemetry event: ${String(logError)}`);
1080
+ }
1081
+ finally {
1082
+ try {
1083
+ this.telemetry?.flush?.();
1084
+ }
1085
+ catch (flushError) {
1086
+ this.logger.error(`Failed to flush telemetry client: ${String(flushError)}`);
1087
+ }
1088
+ }
1089
+ }
1090
+ else {
1091
+ this.logger.info('Telemetry for force exit (SIGINT) already logged.');
1092
+ }
1093
+ this.eventbus.dispatch(UpdateAgentTaskEvent.fromAgent(this));
1094
+ if (this.settings.generate_gif) {
1095
+ let output_path = 'agent_history.gif';
1096
+ if (typeof this.settings.generate_gif === 'string') {
1097
+ output_path = this.settings.generate_gif;
1098
+ }
1099
+ await create_history_gif(this.task, this.history, { output_path });
1100
+ if (fs.existsSync(output_path)) {
1101
+ const output_event = await CreateAgentOutputFileEvent.fromAgentAndFile(this, output_path);
1102
+ this.eventbus.dispatch(output_event);
1103
+ }
1104
+ }
1105
+ await this.eventbus.stop();
1106
+ await this.close();
1107
+ }
1108
+ }
1109
+ async _executeWithTimeout(promise, timeoutSeconds, onTimeout) {
1110
+ if (!timeoutSeconds || timeoutSeconds <= 0) {
1111
+ return promise;
1112
+ }
1113
+ let timeoutHandle = null;
1114
+ const timeoutPromise = new Promise((_, reject) => {
1115
+ timeoutHandle = setTimeout(() => {
1116
+ try {
1117
+ onTimeout?.();
1118
+ }
1119
+ catch {
1120
+ // Ignore timeout callback errors and preserve timeout semantics.
1121
+ }
1122
+ reject(new ExecutionTimeoutError());
1123
+ }, timeoutSeconds * 1000);
1124
+ });
1125
+ try {
1126
+ return await Promise.race([promise, timeoutPromise]);
1127
+ }
1128
+ finally {
1129
+ if (timeoutHandle) {
1130
+ clearTimeout(timeoutHandle);
1131
+ }
1132
+ }
1133
+ }
1134
+ async _step(step_info = null, signal = null) {
1135
+ await this._run_with_shared_session_step_lock(async () => {
1136
+ this._throwIfAborted(signal);
1137
+ this.step_start_time = Date.now() / 1000;
1138
+ let browser_state_summary = null;
1139
+ try {
1140
+ browser_state_summary = await this._prepare_context(step_info, signal);
1141
+ this._throwIfAborted(signal);
1142
+ await this._get_next_action(browser_state_summary, signal);
1143
+ this._throwIfAborted(signal);
1144
+ await this._execute_actions(signal);
1145
+ await this._post_process();
1146
+ }
1147
+ catch (error) {
1148
+ if (signal?.aborted) {
1149
+ const message = error instanceof Error ? error.message : String(error);
1150
+ this.logger.debug(`Step aborted before completion: ${message}`);
1151
+ }
1152
+ else {
1153
+ await this._handle_step_error(error);
1154
+ }
1155
+ }
1156
+ finally {
1157
+ await this._finalize(browser_state_summary);
1158
+ }
1159
+ });
1160
+ }
1161
+ async _prepare_context(step_info = null, signal = null) {
1162
+ if (!this.browser_session) {
1163
+ throw new Error('BrowserSession is not set up');
1164
+ }
1165
+ this._throwIfAborted(signal);
1166
+ await this._restore_shared_pinned_tab_if_needed();
1167
+ this._throwIfAborted(signal);
1168
+ this.logger.debug(`🌐 Step ${this.state.n_steps}: Getting browser state...`);
1169
+ const browser_state_summary = await this.browser_session.get_browser_state_with_recovery?.({
1170
+ cache_clickable_elements_hashes: true,
1171
+ include_screenshot: this.settings.use_vision,
1172
+ signal,
1173
+ });
1174
+ this._throwIfAborted(signal);
1175
+ const current_page = await this.browser_session.get_current_page?.();
1176
+ await this._check_and_update_downloads(`Step ${this.state.n_steps}: after getting browser state`);
1177
+ this._log_step_context(current_page, browser_state_summary);
1178
+ await this._storeScreenshotForStep(browser_state_summary);
1179
+ await this._raise_if_stopped_or_paused();
1180
+ this.logger.debug(`📝 Step ${this.state.n_steps}: Updating action models...`);
1181
+ this._throwIfAborted(signal);
1182
+ await this._updateActionModelsForPage(current_page);
1183
+ const page_filtered_actions = this.controller.registry.get_prompt_description(current_page);
1184
+ this.logger.debug(`💬 Step ${this.state.n_steps}: Creating state messages for context...`);
1185
+ 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);
1186
+ await this._handle_final_step(step_info);
1187
+ return browser_state_summary;
1188
+ }
1189
+ async _storeScreenshotForStep(browser_state_summary) {
1190
+ this._current_screenshot_path = null;
1191
+ if (!this.screenshot_service || !browser_state_summary?.screenshot) {
1192
+ return;
1193
+ }
1194
+ try {
1195
+ this._current_screenshot_path =
1196
+ await this.screenshot_service.store_screenshot(browser_state_summary.screenshot, this.state.n_steps);
1197
+ this.logger.debug(`📸 Step ${this.state.n_steps}: Stored screenshot at ${this._current_screenshot_path}`);
1198
+ }
1199
+ catch (error) {
1200
+ const message = error instanceof Error ? error.message : String(error);
1201
+ this.logger.error(`📸 Failed to store screenshot for step ${this.state.n_steps}: ${message}`);
1202
+ this._current_screenshot_path = null;
1203
+ }
1204
+ }
1205
+ async _get_next_action(browser_state_summary, signal = null) {
1206
+ this._throwIfAborted(signal);
1207
+ const input_messages = this._message_manager.get_messages();
1208
+ this.logger.debug(`🤖 Step ${this.state.n_steps}: Calling LLM with ${input_messages.length} messages (model: ${this.llm.model})...`);
1209
+ let model_output;
1210
+ const llmAbortController = new AbortController();
1211
+ const removeAbortRelay = this._relayAbortSignal(signal, llmAbortController);
1212
+ try {
1213
+ model_output = await this._executeWithTimeout(this._get_model_output_with_retry(input_messages, llmAbortController.signal), this.settings.llm_timeout, () => llmAbortController.abort());
1214
+ }
1215
+ catch (error) {
1216
+ if (error instanceof ExecutionTimeoutError) {
1217
+ throw new Error(`LLM call timed out after ${this.settings.llm_timeout} seconds. Keep your thinking and output short.`);
1218
+ }
1219
+ throw error;
1220
+ }
1221
+ finally {
1222
+ removeAbortRelay();
1223
+ }
1224
+ this._throwIfAborted(signal);
1225
+ this.state.last_model_output = model_output;
1226
+ let actions = [];
1227
+ if (model_output) {
1228
+ this._logNextActionSummary(model_output);
1229
+ actions = model_output.action.map((a) => a.model_dump());
1230
+ }
1231
+ await this._raise_if_stopped_or_paused();
1232
+ await this._handle_post_llm_processing(browser_state_summary, input_messages, actions);
1233
+ await this._raise_if_stopped_or_paused();
1234
+ }
1235
+ async _execute_actions(signal = null) {
1236
+ if (!this.state.last_model_output) {
1237
+ throw new Error('No model output to execute actions from');
1238
+ }
1239
+ this.logger.debug(`⚡ Step ${this.state.n_steps}: Executing ${this.state.last_model_output.action.length} actions...`);
1240
+ const result = await this.multi_act(this.state.last_model_output.action.map((a) => a.model_dump()), { signal });
1241
+ this.logger.debug(`✅ Step ${this.state.n_steps}: Actions completed`);
1242
+ this.state.last_result = result;
1243
+ }
1244
+ async _post_process() {
1245
+ if (!this.browser_session) {
1246
+ throw new Error('BrowserSession is not set up');
1247
+ }
1248
+ await this._check_and_update_downloads('after executing actions');
1249
+ this.state.consecutive_failures = 0;
1250
+ }
1251
+ async multi_act(actions, options = {}) {
1252
+ const { check_for_new_elements = true, signal = null } = options;
1253
+ const results = [];
1254
+ if (!this.browser_session) {
1255
+ throw new Error('BrowserSession is not set up');
1256
+ }
1257
+ await this._restore_shared_pinned_tab_if_needed();
1258
+ // ==================== Selector Map Caching ====================
1259
+ // Check if any action uses an index, if so cache the selector map
1260
+ let cached_selector_map = {};
1261
+ let cached_path_hashes = new Set();
1262
+ for (const action of actions) {
1263
+ const actionName = Object.keys(action)[0];
1264
+ const actionParams = action[actionName];
1265
+ const index = actionParams?.index;
1266
+ if (index !== null && index !== undefined) {
1267
+ cached_selector_map =
1268
+ (await this.browser_session.get_selector_map?.()) || {};
1269
+ cached_path_hashes = new Set(Object.values(cached_selector_map)
1270
+ .map((e) => e?.hash?.branch_path_hash)
1271
+ .filter(Boolean));
1272
+ break;
1273
+ }
1274
+ }
1275
+ // ==================== Execute Actions ====================
1276
+ for (let i = 0; i < actions.length; i++) {
1277
+ this._throwIfAborted(signal);
1278
+ const action = actions[i];
1279
+ const actionName = Object.keys(action)[0];
1280
+ const actionParams = action[actionName];
1281
+ // ==================== Done Action Position Validation ====================
1282
+ // ONLY ALLOW TO CALL `done` IF IT IS A SINGLE ACTION
1283
+ if (i > 0 && actionName === 'done') {
1284
+ const msg = `Done action is allowed only as a single action - stopped after action ${i} / ${actions.length}.`;
1285
+ this.logger.info(msg);
1286
+ break;
1287
+ }
1288
+ // ==================== Index Change & New Element Detection ====================
1289
+ if (i > 0) {
1290
+ const currentIndex = actionParams?.index;
1291
+ if (currentIndex !== null && currentIndex !== undefined) {
1292
+ this._throwIfAborted(signal);
1293
+ // Get new browser state after previous action
1294
+ const new_browser_state_summary = await this.browser_session.get_browser_state_with_recovery?.({
1295
+ cache_clickable_elements_hashes: false,
1296
+ include_screenshot: false,
1297
+ signal,
1298
+ });
1299
+ const new_selector_map = new_browser_state_summary?.selector_map || {};
1300
+ // Detect index change after previous action
1301
+ const orig_target = cached_selector_map[currentIndex];
1302
+ const orig_target_hash = orig_target?.hash?.branch_path_hash || null;
1303
+ const new_target = new_selector_map[currentIndex];
1304
+ const new_target_hash = new_target?.hash?.branch_path_hash || null;
1305
+ if (orig_target_hash !== new_target_hash) {
1306
+ const msg = `Element index changed after action ${i} / ${actions.length}, because page changed.`;
1307
+ this.logger.info(msg);
1308
+ results.push(new ActionResult({
1309
+ extracted_content: msg,
1310
+ include_in_memory: true,
1311
+ long_term_memory: msg,
1312
+ }));
1313
+ break;
1314
+ }
1315
+ // Check for new elements on the page
1316
+ const new_path_hashes = new Set(Object.values(new_selector_map)
1317
+ .map((e) => e?.hash?.branch_path_hash)
1318
+ .filter(Boolean));
1319
+ // Check if new elements appeared (new_path_hashes is not a subset of cached_path_hashes)
1320
+ const has_new_elements = Array.from(new_path_hashes).some((hash) => !cached_path_hashes.has(hash));
1321
+ if (check_for_new_elements && has_new_elements) {
1322
+ const msg = `Something new appeared after action ${i} / ${actions.length}, following actions are NOT executed and should be retried.`;
1323
+ this.logger.info(msg);
1324
+ results.push(new ActionResult({
1325
+ extracted_content: msg,
1326
+ include_in_memory: true,
1327
+ long_term_memory: msg,
1328
+ }));
1329
+ break;
1330
+ }
1331
+ }
1332
+ // Wait between actions
1333
+ const wait_time = this.browser_session?.browser_profile
1334
+ ?.wait_between_actions || 0;
1335
+ if (wait_time > 0) {
1336
+ await this._sleep(wait_time, signal);
1337
+ }
1338
+ }
1339
+ // ==================== Execute Action ====================
1340
+ try {
1341
+ this._throwIfAborted(signal);
1342
+ await this._raise_if_stopped_or_paused();
1343
+ const actResult = await this.controller.registry.execute_action(actionName, actionParams, {
1344
+ browser_session: this.browser_session,
1345
+ page_extraction_llm: this.settings.page_extraction_llm,
1346
+ sensitive_data: this.sensitive_data,
1347
+ available_file_paths: this.available_file_paths,
1348
+ file_system: this.file_system,
1349
+ context: this.context ?? undefined,
1350
+ signal,
1351
+ });
1352
+ results.push(actResult);
1353
+ // Log action execution
1354
+ this.logger.info(`☑️ Executed action ${i + 1}/${actions.length}: ${actionName}(${JSON.stringify(actionParams)})`);
1355
+ // Break early if done, error, or last action
1356
+ if (results[results.length - 1]?.is_done ||
1357
+ results[results.length - 1]?.error ||
1358
+ i === actions.length - 1) {
1359
+ this._capture_shared_pinned_tab();
1360
+ break;
1361
+ }
1362
+ this._capture_shared_pinned_tab();
1363
+ }
1364
+ catch (error) {
1365
+ const message = error instanceof Error ? error.message : String(error);
1366
+ this.logger.error(`❌ Action ${i + 1} failed: ${message}`);
1367
+ this._capture_shared_pinned_tab();
1368
+ throw error;
1369
+ }
1370
+ }
1371
+ return results;
1372
+ }
1373
+ async rerun_history(history, options = {}) {
1374
+ const { max_retries = 3, skip_failures = true, delay_between_actions = 2, signal = null, } = options;
1375
+ this._throwIfAborted(signal);
1376
+ if (this.initial_actions?.length) {
1377
+ const initialResult = await this.multi_act(this.initial_actions, {
1378
+ signal,
1379
+ });
1380
+ this.state.last_result = initialResult;
1381
+ }
1382
+ const results = [];
1383
+ for (let index = 0; index < history.history.length; index++) {
1384
+ this._throwIfAborted(signal);
1385
+ const historyItem = history.history[index];
1386
+ const goal = historyItem.model_output?.current_state?.next_goal ?? '';
1387
+ this.logger.info(`Replaying step ${index + 1}/${history.history.length}: goal: ${goal}`);
1388
+ const actions = historyItem.model_output?.action ?? [];
1389
+ const hasValidAction = actions.length && !actions.every((action) => action == null);
1390
+ if (!historyItem.model_output || !hasValidAction) {
1391
+ this.logger.warning(`Step ${index + 1}: No action to replay, skipping`);
1392
+ results.push(new ActionResult({ error: 'No action to replay' }));
1393
+ continue;
1394
+ }
1395
+ let attempt = 0;
1396
+ while (attempt < max_retries) {
1397
+ this._throwIfAborted(signal);
1398
+ try {
1399
+ const stepResult = await this._execute_history_step(historyItem, delay_between_actions, signal);
1400
+ results.push(...stepResult);
1401
+ break;
1402
+ }
1403
+ catch (error) {
1404
+ if (signal?.aborted ||
1405
+ (error instanceof Error && error.name === 'AbortError')) {
1406
+ throw this._createAbortError();
1407
+ }
1408
+ attempt += 1;
1409
+ if (attempt === max_retries) {
1410
+ const message = `Step ${index + 1} failed after ${max_retries} attempts: ${error.message ?? error}`;
1411
+ this.logger.error(message);
1412
+ const failure = new ActionResult({ error: message });
1413
+ results.push(failure);
1414
+ if (!skip_failures) {
1415
+ throw new Error(message);
1416
+ }
1417
+ }
1418
+ else {
1419
+ this.logger.warning(`Step ${index + 1} failed (attempt ${attempt}/${max_retries}), retrying...`);
1420
+ await this._sleep(delay_between_actions, signal);
1421
+ }
1422
+ }
1423
+ }
1424
+ }
1425
+ return results;
1426
+ }
1427
+ async _execute_history_step(historyItem, delaySeconds, signal = null) {
1428
+ this._throwIfAborted(signal);
1429
+ if (!this.browser_session) {
1430
+ throw new Error('BrowserSession is not set up');
1431
+ }
1432
+ const browser_state_summary = await this.browser_session.get_browser_state_with_recovery?.({
1433
+ cache_clickable_elements_hashes: false,
1434
+ include_screenshot: false,
1435
+ signal,
1436
+ });
1437
+ if (!browser_state_summary || !historyItem.model_output) {
1438
+ throw new Error('Invalid browser state or model output');
1439
+ }
1440
+ const interactedElements = historyItem.state?.interacted_element ?? [];
1441
+ const updatedActions = [];
1442
+ for (let actionIndex = 0; actionIndex < historyItem.model_output.action.length; actionIndex++) {
1443
+ this._throwIfAborted(signal);
1444
+ const originalAction = historyItem.model_output.action[actionIndex];
1445
+ if (!originalAction) {
1446
+ continue;
1447
+ }
1448
+ const updatedAction = await this._update_action_indices(this._coerceHistoryElement(interactedElements[actionIndex]), originalAction, browser_state_summary);
1449
+ if (!updatedAction) {
1450
+ throw new Error(`Could not find matching element ${actionIndex} in current page`);
1451
+ }
1452
+ if (typeof updatedAction?.model_dump === 'function') {
1453
+ updatedActions.push(updatedAction.model_dump({ exclude_unset: true }));
1454
+ }
1455
+ else {
1456
+ updatedActions.push(updatedAction);
1457
+ }
1458
+ }
1459
+ this._throwIfAborted(signal);
1460
+ const result = await this.multi_act(updatedActions, { signal });
1461
+ await this._sleep(delaySeconds, signal);
1462
+ return result;
1463
+ }
1464
+ async _update_action_indices(historicalElement, action, browserStateSummary) {
1465
+ if (!historicalElement || !browserStateSummary?.element_tree) {
1466
+ return action;
1467
+ }
1468
+ const currentNode = HistoryTreeProcessor.find_history_element_in_tree(historicalElement, browserStateSummary.element_tree);
1469
+ if (!currentNode || currentNode.highlight_index == null) {
1470
+ return null;
1471
+ }
1472
+ const currentIndex = typeof action?.get_index === 'function' ? action.get_index() : null;
1473
+ if (currentIndex !== currentNode.highlight_index &&
1474
+ typeof action?.set_index === 'function') {
1475
+ action.set_index(currentNode.highlight_index);
1476
+ this.logger.info(`Element moved in DOM, updated index from ${currentIndex} to ${currentNode.highlight_index}`);
1477
+ }
1478
+ return action;
1479
+ }
1480
+ async load_and_rerun(history_file = null, options = {}) {
1481
+ const target = history_file ?? 'AgentHistory.json';
1482
+ const history = AgentHistoryList.load_from_file(target, this.AgentOutput);
1483
+ return this.rerun_history(history, options);
1484
+ }
1485
+ save_history(file_path = null) {
1486
+ const target = file_path ?? 'AgentHistory.json';
1487
+ this.history.save_to_file(target);
1488
+ }
1489
+ _coerceHistoryElement(element) {
1490
+ if (!element) {
1491
+ return null;
1492
+ }
1493
+ if (element instanceof DOMHistoryElement) {
1494
+ return element;
1495
+ }
1496
+ const payload = element;
1497
+ 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);
1498
+ }
1499
+ _createAbortError() {
1500
+ const error = new Error('Operation aborted');
1501
+ error.name = 'AbortError';
1502
+ return error;
1503
+ }
1504
+ _throwIfAborted(signal = null) {
1505
+ if (signal?.aborted) {
1506
+ throw this._createAbortError();
1507
+ }
1508
+ }
1509
+ _relayAbortSignal(signal, controller) {
1510
+ if (!signal) {
1511
+ return () => { };
1512
+ }
1513
+ if (signal.aborted) {
1514
+ controller.abort(signal.reason);
1515
+ return () => { };
1516
+ }
1517
+ const handleAbort = () => controller.abort(signal.reason);
1518
+ signal.addEventListener('abort', handleAbort, { once: true });
1519
+ return () => signal.removeEventListener('abort', handleAbort);
1520
+ }
1521
+ async _sleep(seconds, signal = null) {
1522
+ if (seconds <= 0) {
1523
+ return;
1524
+ }
1525
+ await new Promise((resolve, reject) => {
1526
+ const timeout = setTimeout(() => {
1527
+ cleanup();
1528
+ resolve();
1529
+ }, seconds * 1000);
1530
+ const onAbort = () => {
1531
+ clearTimeout(timeout);
1532
+ cleanup();
1533
+ reject(this._createAbortError());
1534
+ };
1535
+ const cleanup = () => {
1536
+ signal?.removeEventListener('abort', onAbort);
1537
+ };
1538
+ if (signal) {
1539
+ if (signal.aborted) {
1540
+ onAbort();
1541
+ return;
1542
+ }
1543
+ signal.addEventListener('abort', onAbort, { once: true });
1544
+ }
1545
+ });
1546
+ }
1547
+ async wait_until_resumed() {
1548
+ if (!this.state.paused) {
1549
+ return;
1550
+ }
1551
+ if (!this._external_pause_event.resolve) {
1552
+ this._external_pause_event.promise = new Promise((resolve) => {
1553
+ this._external_pause_event.resolve = resolve;
1554
+ });
1555
+ }
1556
+ await this._external_pause_event.promise;
1557
+ }
1558
+ async log_completion() {
1559
+ this.logger.info('✅ Agent completed task');
1560
+ }
1561
+ pause() {
1562
+ if (this.state.paused) {
1563
+ return;
1564
+ }
1565
+ this.state.paused = true;
1566
+ this._external_pause_event.promise = new Promise((resolve) => {
1567
+ this._external_pause_event.resolve = resolve;
1568
+ });
1569
+ }
1570
+ resume() {
1571
+ if (!this.state.paused) {
1572
+ return;
1573
+ }
1574
+ this.state.paused = false;
1575
+ this._external_pause_event.resolve?.();
1576
+ this._external_pause_event.resolve = null;
1577
+ this._external_pause_event.promise = Promise.resolve();
1578
+ }
1579
+ stop() {
1580
+ this.state.stopped = true;
1581
+ this.resume();
1582
+ }
1583
+ async close() {
1584
+ if (this._closePromise) {
1585
+ await this._closePromise;
1586
+ return;
1587
+ }
1588
+ const browser_session = this.browser_session;
1589
+ if (!browser_session) {
1590
+ return;
1591
+ }
1592
+ this._closePromise = (async () => {
1593
+ this._release_browser_session_claim(browser_session);
1594
+ if (this._has_any_browser_session_attachments(browser_session)) {
1595
+ this.logger.debug('Skipping BrowserSession shutdown because other attached Agents are still active.');
1596
+ return;
1597
+ }
1598
+ this._cleanup_shared_session_step_lock_if_unused(browser_session);
1599
+ try {
1600
+ if (typeof browser_session.stop === 'function') {
1601
+ await browser_session.stop();
1602
+ }
1603
+ else if (typeof browser_session.close === 'function') {
1604
+ await browser_session.close();
1605
+ }
1606
+ }
1607
+ catch (error) {
1608
+ this.logger.error(`Error during agent cleanup: ${error instanceof Error ? error.message : String(error)}`);
1609
+ }
1610
+ })();
1611
+ await this._closePromise;
1612
+ }
1613
+ /**
1614
+ * Get the trace and trace_details objects for the agent
1615
+ * Contains comprehensive metadata about the agent run for debugging and analysis
1616
+ */
1617
+ get_trace_object() {
1618
+ // Helper to extract website from task text
1619
+ const extract_task_website = (task_text) => {
1620
+ const url_pattern = /https?:\/\/[^\s<>"']+|www\.[^\s<>"']+|[^\s<>"']+\.[a-z]{2,}(?:\/[^\s<>"']*)?/i;
1621
+ const match = task_text.match(url_pattern);
1622
+ return match ? match[0] : null;
1623
+ };
1624
+ // Helper to get complete history without screenshots
1625
+ const get_complete_history_without_screenshots = (history_data) => {
1626
+ if (history_data.history) {
1627
+ for (const item of history_data.history) {
1628
+ if (item.state && item.state.screenshot) {
1629
+ item.state.screenshot = null;
1630
+ }
1631
+ }
1632
+ }
1633
+ return JSON.stringify(history_data);
1634
+ };
1635
+ // Generate autogenerated fields
1636
+ const trace_id = uuid7str();
1637
+ const timestamp = new Date().toISOString();
1638
+ // Collect data
1639
+ const structured_output = this.history.structured_output;
1640
+ const structured_output_json = structured_output
1641
+ ? JSON.stringify(structured_output)
1642
+ : null;
1643
+ const final_result = this.history.final_result();
1644
+ const action_history = this.history.action_history();
1645
+ const action_errors = this.history.errors();
1646
+ const urls = this.history.urls();
1647
+ const usage = this.history.usage;
1648
+ // Build trace object
1649
+ const trace = {
1650
+ // Autogenerated fields
1651
+ trace_id,
1652
+ timestamp,
1653
+ browser_use_version: this.version,
1654
+ git_info: null, // Can be enhanced if needed
1655
+ // Direct agent properties
1656
+ model: this.llm.model || 'unknown',
1657
+ settings: this.settings ? JSON.stringify(this.settings) : null,
1658
+ task_id: this.task_id,
1659
+ task_truncated: this.task.length > 20000 ? this.task.slice(0, 20000) : this.task,
1660
+ task_website: extract_task_website(this.task),
1661
+ // AgentHistoryList methods
1662
+ structured_output_truncated: structured_output_json && structured_output_json.length > 20000
1663
+ ? structured_output_json.slice(0, 20000)
1664
+ : structured_output_json,
1665
+ action_history_truncated: action_history
1666
+ ? JSON.stringify(action_history)
1667
+ : null,
1668
+ action_errors: action_errors ? JSON.stringify(action_errors) : null,
1669
+ urls: urls ? JSON.stringify(urls) : null,
1670
+ final_result_response_truncated: final_result && final_result.length > 20000
1671
+ ? final_result.slice(0, 20000)
1672
+ : final_result,
1673
+ self_report_completed: this.history.is_done() ? 1 : 0,
1674
+ self_report_success: this.history.is_successful() ? 1 : 0,
1675
+ duration: this.history.total_duration_seconds(),
1676
+ steps_taken: this.history.number_of_steps(),
1677
+ usage: usage ? JSON.stringify(usage) : null,
1678
+ };
1679
+ // Build trace_details object
1680
+ const trace_details = {
1681
+ // Autogenerated fields (ensure same as trace)
1682
+ trace_id,
1683
+ timestamp,
1684
+ // Direct agent properties
1685
+ task: this.task,
1686
+ // AgentHistoryList methods
1687
+ structured_output: structured_output_json,
1688
+ final_result_response: final_result,
1689
+ complete_history: get_complete_history_without_screenshots(this.history.model_dump?.() || {}),
1690
+ };
1691
+ return { trace, trace_details };
1692
+ }
1693
+ _log_agent_run() {
1694
+ this.logger.info(`🧠 Starting agent for task: ${this.task}`);
1695
+ }
1696
+ _raise_if_stopped_or_paused() {
1697
+ if (this.state.stopped) {
1698
+ throw new Error('Agent stopped');
1699
+ }
1700
+ if (this.state.paused) {
1701
+ throw new Error('Agent paused');
1702
+ }
1703
+ }
1704
+ async _handle_post_llm_processing(browser_state_summary, input_messages, actions = []) {
1705
+ if (this.register_new_step_callback && this.state.last_model_output) {
1706
+ await this.register_new_step_callback(browser_state_summary, this.state.last_model_output, this.state.n_steps);
1707
+ }
1708
+ log_response(this.state.last_model_output, this.controller, this.logger);
1709
+ if (this.settings.save_conversation_path) {
1710
+ const dir = this.settings.save_conversation_path;
1711
+ const filepath = path.join(dir, `step_${this.state.n_steps}.json`);
1712
+ await fs.promises.mkdir(path.dirname(filepath), { recursive: true });
1713
+ await fs.promises.writeFile(filepath, JSON.stringify({
1714
+ messages: input_messages,
1715
+ response: this.state.last_model_output?.model_dump(),
1716
+ }, null, 2), this.settings.save_conversation_path_encoding);
1717
+ }
1718
+ }
1719
+ /**
1720
+ * Handle all types of errors that can occur during a step
1721
+ * Implements comprehensive error categorization with:
1722
+ * - Validation error hints
1723
+ * - Rate limit auto-retry
1724
+ * - Parse error guidance
1725
+ * - Token limit warnings
1726
+ * - Network error detection
1727
+ * - Browser error handling
1728
+ * - LLM-specific errors
1729
+ */
1730
+ async _handle_step_error(error) {
1731
+ const include_trace = this.logger.level === 'debug';
1732
+ let error_msg = AgentError.format_error(error, include_trace);
1733
+ const prefix = `❌ Result failed ${this.state.consecutive_failures + 1}/${this.settings.max_failures} times:\n `;
1734
+ this.state.consecutive_failures += 1;
1735
+ // 1. Handle Validation Errors (Pydantic/Zod)
1736
+ if (error.name === 'ValidationError' ||
1737
+ error.name === 'ZodError' ||
1738
+ error instanceof TypeError) {
1739
+ this.logger.error(`${prefix}${error_msg}`);
1740
+ // Add context hint for validation errors
1741
+ if (error_msg.includes('Max token limit reached') ||
1742
+ error_msg.includes('token')) {
1743
+ error_msg +=
1744
+ '\n\n💡 Hint: Your response was too long. Keep your thinking and output concise.';
1745
+ }
1746
+ else {
1747
+ error_msg +=
1748
+ '\n\n💡 Hint: Your output format was invalid. Please follow the exact schema structure required for actions.';
1749
+ }
1750
+ }
1751
+ // 2. Handle Interrupted Errors
1752
+ else if (error.message.includes('interrupted') ||
1753
+ error.message.includes('abort') ||
1754
+ error.message.includes('InterruptedError')) {
1755
+ error_msg = `The agent was interrupted mid-step${error.message ? ` - ${error.message}` : ''}`;
1756
+ this.logger.error(`${prefix}${error_msg}`);
1757
+ }
1758
+ // 3. Handle Parse Errors
1759
+ else if (error_msg.includes('Could not parse') ||
1760
+ error_msg.includes('tool_use_failed') ||
1761
+ error_msg.includes('Failed to parse')) {
1762
+ this.logger.debug(`Model: ${this.llm.model} failed to parse response`);
1763
+ error_msg +=
1764
+ '\n\n💡 Hint: Return a valid JSON object with the required fields.';
1765
+ this.logger.error(`${prefix}${error_msg}`);
1766
+ }
1767
+ // 4. Handle Rate Limit Errors (OpenAI, Anthropic, Google)
1768
+ else if (this._isRateLimitError(error, error_msg)) {
1769
+ this.logger.warning(`${prefix}${error_msg}`);
1770
+ this.logger.warning(`⏳ Rate limit detected, waiting ${this.settings.retry_delay}s before retrying...`);
1771
+ // Auto-retry: wait before continuing
1772
+ await this._sleep(this.settings.retry_delay);
1773
+ error_msg += `\n\n⏳ Retrying after ${this.settings.retry_delay}s delay...`;
1774
+ }
1775
+ // 5. Handle Network Errors
1776
+ else if (this._isNetworkError(error, error_msg)) {
1777
+ this.logger.error(`${prefix}${error_msg}`);
1778
+ error_msg +=
1779
+ '\n\n🌐 Network error detected. Please check your internet connection and try again.';
1780
+ }
1781
+ // 6. Handle Browser Errors
1782
+ else if (this._isBrowserError(error, error_msg)) {
1783
+ this.logger.error(`${prefix}${error_msg}`);
1784
+ error_msg +=
1785
+ '\n\n🌍 Browser error detected. The page may have crashed or become unresponsive.';
1786
+ }
1787
+ // 7. Handle Timeout Errors
1788
+ else if (this._isTimeoutError(error, error_msg)) {
1789
+ this.logger.error(`${prefix}${error_msg}`);
1790
+ error_msg +=
1791
+ '\n\n⏱️ Timeout error. The operation took too long to complete.';
1792
+ }
1793
+ // 8. Handle All Other Errors
1794
+ else {
1795
+ this.logger.error(`${prefix}${error_msg}`);
1796
+ }
1797
+ this.state.last_result = [new ActionResult({ error: error_msg })];
1798
+ }
1799
+ /**
1800
+ * Check if an error is a network error
1801
+ */
1802
+ _isNetworkError(error, error_msg) {
1803
+ const networkPatterns = [
1804
+ 'ECONNREFUSED',
1805
+ 'ENOTFOUND',
1806
+ 'ETIMEDOUT',
1807
+ 'ECONNRESET',
1808
+ 'network error',
1809
+ 'Network Error',
1810
+ 'fetch failed',
1811
+ 'socket hang up',
1812
+ 'getaddrinfo',
1813
+ ];
1814
+ return networkPatterns.some((pattern) => error_msg.includes(pattern) || error.message.includes(pattern));
1815
+ }
1816
+ /**
1817
+ * Check if an error is a browser/Playwright error
1818
+ */
1819
+ _isBrowserError(error, error_msg) {
1820
+ const browserPatterns = [
1821
+ 'Target page',
1822
+ 'Page crashed',
1823
+ 'Browser closed',
1824
+ 'Context closed',
1825
+ 'Frame detached',
1826
+ 'Execution context',
1827
+ 'Navigation failed',
1828
+ 'Protocol error',
1829
+ ];
1830
+ return browserPatterns.some((pattern) => error_msg.includes(pattern) || error.message.includes(pattern));
1831
+ }
1832
+ /**
1833
+ * Check if an error is a timeout error
1834
+ */
1835
+ _isTimeoutError(error, error_msg) {
1836
+ const timeoutPatterns = [
1837
+ 'timeout',
1838
+ 'Timeout',
1839
+ 'timed out',
1840
+ 'time limit exceeded',
1841
+ 'deadline exceeded',
1842
+ ];
1843
+ return timeoutPatterns.some((pattern) => error_msg.toLowerCase().includes(pattern.toLowerCase()));
1844
+ }
1845
+ /**
1846
+ * Check if an error is a rate limit error from various LLM providers
1847
+ */
1848
+ _isRateLimitError(error, error_msg) {
1849
+ // Check error class name
1850
+ const errorClassName = error.constructor.name;
1851
+ if (errorClassName === 'RateLimitError' ||
1852
+ errorClassName === 'ResourceExhausted') {
1853
+ return true;
1854
+ }
1855
+ // Check error message patterns
1856
+ const rateLimitPatterns = [
1857
+ 'rate_limit_exceeded',
1858
+ 'rate limit exceeded',
1859
+ 'RateLimitError',
1860
+ 'RESOURCE_EXHAUSTED',
1861
+ 'ResourceExhausted',
1862
+ 'tokens per minute',
1863
+ 'TPM',
1864
+ 'requests per minute',
1865
+ 'RPM',
1866
+ 'quota exceeded',
1867
+ 'too many requests',
1868
+ '429',
1869
+ ];
1870
+ return rateLimitPatterns.some((pattern) => error_msg.toLowerCase().includes(pattern.toLowerCase()));
1871
+ }
1872
+ async _finalize(browser_state_summary) {
1873
+ const step_end_time = Date.now() / 1000;
1874
+ this._enforceDoneOnlyForCurrentStep = false;
1875
+ if (!this.state.last_result) {
1876
+ return;
1877
+ }
1878
+ if (browser_state_summary) {
1879
+ const metadata = new StepMetadata(this.step_start_time, step_end_time, this.state.n_steps);
1880
+ await this._make_history_item(this.state.last_model_output, browser_state_summary, this.state.last_result, metadata);
1881
+ }
1882
+ this._log_step_completion_summary(this.step_start_time, this.state.last_result);
1883
+ this.save_file_system_state();
1884
+ if (browser_state_summary && this.state.last_model_output) {
1885
+ const actions_data = this.state.last_model_output.action.map((action) => typeof action?.model_dump === 'function'
1886
+ ? action.model_dump()
1887
+ : action);
1888
+ const step_event = CreateAgentStepEvent.fromAgentStep(this, this.state.last_model_output, this.state.last_result, actions_data, browser_state_summary);
1889
+ this.eventbus.dispatch(step_event);
1890
+ }
1891
+ this.state.n_steps += 1;
1892
+ }
1893
+ async _handle_final_step(step_info = null) {
1894
+ const isLastStep = Boolean(step_info && step_info.is_last_step());
1895
+ this._enforceDoneOnlyForCurrentStep = isLastStep;
1896
+ if (isLastStep) {
1897
+ 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' +
1898
+ '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' +
1899
+ 'If the task is fully finished, set success in "done" to true.\n' +
1900
+ 'Include everything you found out for the ultimate task in the done text.';
1901
+ this._message_manager._add_context_message(new UserMessage(message));
1902
+ this.logger.info('⚠️ Approaching last step. Enforcing done-only action.');
1903
+ }
1904
+ }
1905
+ _parseCompletionPayload(rawCompletion) {
1906
+ let parsedCompletion = rawCompletion;
1907
+ if (typeof parsedCompletion === 'string') {
1908
+ let jsonText = this._removeThinkTags(parsedCompletion.trim());
1909
+ // Handle common markdown wrappers like ```json ... ```
1910
+ const fencedMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/i);
1911
+ if (fencedMatch && fencedMatch[1]) {
1912
+ jsonText = fencedMatch[1].trim();
1913
+ }
1914
+ // If extra text surrounds JSON, try to isolate the first JSON object
1915
+ const firstBrace = jsonText.indexOf('{');
1916
+ const lastBrace = jsonText.lastIndexOf('}');
1917
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
1918
+ jsonText = jsonText.slice(firstBrace, lastBrace + 1);
1919
+ }
1920
+ try {
1921
+ parsedCompletion = JSON.parse(jsonText);
1922
+ }
1923
+ catch (error) {
1924
+ throw new Error(`Failed to parse LLM completion as JSON: ${String(error)}`);
1925
+ }
1926
+ }
1927
+ if (!parsedCompletion || typeof parsedCompletion !== 'object') {
1928
+ throw new Error('Model completion must be a JSON object');
1929
+ }
1930
+ return parsedCompletion;
1931
+ }
1932
+ _isModelActionMissing(actions) {
1933
+ if (actions.length === 0) {
1934
+ return true;
1935
+ }
1936
+ return actions.every((entry) => {
1937
+ const candidate = entry &&
1938
+ typeof entry === 'object' &&
1939
+ typeof entry.model_dump === 'function'
1940
+ ? entry.model_dump()
1941
+ : entry;
1942
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
1943
+ return false;
1944
+ }
1945
+ return Object.keys(candidate).length === 0;
1946
+ });
1947
+ }
1948
+ async _get_model_output_with_retry(messages, signal = null) {
1949
+ const invokeAndParse = async (inputMessages) => {
1950
+ this._throwIfAborted(signal);
1951
+ const completion = await this.llm.ainvoke(inputMessages, AgentLLMOutputFormat, { signal: signal ?? undefined });
1952
+ this._throwIfAborted(signal);
1953
+ return this._parseCompletionPayload(completion.completion);
1954
+ };
1955
+ let parsed_completion = await invokeAndParse(messages);
1956
+ let rawAction = Array.isArray(parsed_completion?.action)
1957
+ ? parsed_completion.action
1958
+ : [];
1959
+ this.logger.debug(`✅ Step ${this.state.n_steps}: Got LLM response with ${rawAction.length} actions`);
1960
+ if (this._isModelActionMissing(rawAction)) {
1961
+ this._throwIfAborted(signal);
1962
+ this.logger.warning('Model returned empty action. Retrying...');
1963
+ const clarificationMessage = new UserMessage('You forgot to return an action. Please respond only with a valid JSON action according to the expected format.');
1964
+ parsed_completion = await invokeAndParse([
1965
+ ...messages,
1966
+ clarificationMessage,
1967
+ ]);
1968
+ rawAction = Array.isArray(parsed_completion?.action)
1969
+ ? parsed_completion.action
1970
+ : [];
1971
+ if (this._isModelActionMissing(rawAction)) {
1972
+ this.logger.warning('Model still returned empty after retry. Inserting safe noop action.');
1973
+ rawAction = [
1974
+ {
1975
+ done: {
1976
+ success: false,
1977
+ text: 'No next action returned by LLM!',
1978
+ },
1979
+ },
1980
+ ];
1981
+ }
1982
+ }
1983
+ const action = this._validateAndNormalizeActions(rawAction);
1984
+ const toNullableString = (value) => typeof value === 'string' ? value : null;
1985
+ const AgentOutputModel = this.AgentOutput ?? AgentOutput;
1986
+ return new AgentOutputModel({
1987
+ thinking: toNullableString(parsed_completion?.thinking),
1988
+ evaluation_previous_goal: toNullableString(parsed_completion?.evaluation_previous_goal),
1989
+ memory: toNullableString(parsed_completion?.memory),
1990
+ next_goal: toNullableString(parsed_completion?.next_goal),
1991
+ action,
1992
+ });
1993
+ }
1994
+ _validateAndNormalizeActions(actions) {
1995
+ const normalizedActions = [];
1996
+ const registryActions = this.controller.registry.get_all_actions();
1997
+ const availableNames = new Set();
1998
+ const modelForStep = this._enforceDoneOnlyForCurrentStep
1999
+ ? this.DoneActionModel
2000
+ : this.ActionModel;
2001
+ const modelAvailableNames = modelForStep?.available_actions;
2002
+ if (Array.isArray(modelAvailableNames) && modelAvailableNames.length > 0) {
2003
+ for (const actionName of modelAvailableNames) {
2004
+ if (typeof actionName === 'string' && actionName.trim()) {
2005
+ availableNames.add(actionName);
2006
+ }
2007
+ }
2008
+ }
2009
+ else {
2010
+ for (const actionName of registryActions.keys()) {
2011
+ availableNames.add(actionName);
2012
+ }
2013
+ }
2014
+ for (let i = 0; i < actions.length; i++) {
2015
+ const entry = actions[i];
2016
+ const candidate = entry &&
2017
+ typeof entry === 'object' &&
2018
+ typeof entry.model_dump === 'function'
2019
+ ? entry.model_dump()
2020
+ : entry;
2021
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
2022
+ throw new Error(`Invalid action at index ${i}: expected an object with exactly one action key`);
2023
+ }
2024
+ const actionObject = candidate;
2025
+ const keys = Object.keys(actionObject);
2026
+ if (keys.length !== 1) {
2027
+ throw new Error(`Invalid action at index ${i}: expected exactly one action key, got ${keys.length}`);
2028
+ }
2029
+ const actionName = keys[0];
2030
+ if (!availableNames.has(actionName)) {
2031
+ const available = Array.from(availableNames).sort().join(', ');
2032
+ throw new Error(`Action '${actionName}' is not available on the current page. Available actions: ${available}`);
2033
+ }
2034
+ const actionInfo = registryActions.get(actionName);
2035
+ if (!actionInfo) {
2036
+ throw new Error(`Action '${actionName}' is not registered`);
2037
+ }
2038
+ const rawParams = (actionObject[actionName] ?? {});
2039
+ const paramsResult = actionInfo.paramSchema.safeParse(rawParams);
2040
+ if (!paramsResult.success) {
2041
+ throw new Error(`Invalid parameters for action '${actionName}': ${paramsResult.error.message}`);
2042
+ }
2043
+ normalizedActions.push(new modelForStep({
2044
+ [actionName]: paramsResult.data,
2045
+ }));
2046
+ }
2047
+ if (normalizedActions.length === 0) {
2048
+ throw new Error('Model output must contain at least one action');
2049
+ }
2050
+ if (normalizedActions.length > this.settings.max_actions_per_step) {
2051
+ this.logger.warning(`Model returned ${normalizedActions.length} actions, trimming to max_actions_per_step=${this.settings.max_actions_per_step}`);
2052
+ return normalizedActions.slice(0, this.settings.max_actions_per_step);
2053
+ }
2054
+ return normalizedActions;
2055
+ }
2056
+ async _update_action_models_for_page(page) {
2057
+ await this._updateActionModelsForPage(page);
2058
+ }
2059
+ async _check_and_update_downloads(context = '') {
2060
+ if (!this.has_downloads_path || !this.browser_session) {
2061
+ return;
2062
+ }
2063
+ try {
2064
+ const current_downloads = Array.isArray(this.browser_session.downloaded_files)
2065
+ ? [...this.browser_session.downloaded_files]
2066
+ : [];
2067
+ const changed = current_downloads.length !== this._last_known_downloads.length ||
2068
+ current_downloads.some((value, index) => value !== this._last_known_downloads[index]);
2069
+ if (changed) {
2070
+ this._update_available_file_paths(current_downloads);
2071
+ this._last_known_downloads = current_downloads;
2072
+ if (context) {
2073
+ this.logger.debug(`📁 ${context}: Updated available files`);
2074
+ }
2075
+ }
2076
+ }
2077
+ catch (error) {
2078
+ const message = error instanceof Error ? error.message : String(error);
2079
+ const errorContext = context ? ` ${context}` : '';
2080
+ this.logger.debug(`📁 Failed to check for downloads${errorContext}: ${message}`);
2081
+ }
2082
+ }
2083
+ _update_available_file_paths(downloads) {
2084
+ if (!this.has_downloads_path) {
2085
+ return;
2086
+ }
2087
+ const existing = this.available_file_paths
2088
+ ? [...this.available_file_paths]
2089
+ : [];
2090
+ const known = new Set(existing);
2091
+ const new_files = downloads.filter((pathValue) => !known.has(pathValue));
2092
+ if (new_files.length) {
2093
+ const updated = existing.concat(new_files);
2094
+ this.available_file_paths = updated;
2095
+ this.logger.info(`📁 Added ${new_files.length} downloaded files to available_file_paths (total: ${updated.length} files)`);
2096
+ for (const file_path of new_files) {
2097
+ this.logger.info(`📄 New file available: ${file_path}`);
2098
+ }
2099
+ }
2100
+ else {
2101
+ this.logger.info(`📁 No new downloads detected (tracking ${existing.length} files)`);
2102
+ }
2103
+ }
2104
+ _log_step_context(current_page, browser_state_summary) {
2105
+ const url = typeof current_page?.url === 'function' ? current_page.url() : '';
2106
+ const url_short = url.length > 50 ? `${url.slice(0, 50)}...` : url;
2107
+ const interactive_count = browser_state_summary?.selector_map
2108
+ ? Object.keys(browser_state_summary.selector_map).length
2109
+ : 0;
2110
+ this.logger.info(`📍 Step ${this.state.n_steps}: Evaluating page with ${interactive_count} interactive elements on: ${url_short}`);
2111
+ }
2112
+ _log_step_completion_summary(step_start_time, result) {
2113
+ if (!result.length) {
2114
+ return;
2115
+ }
2116
+ const step_duration = Date.now() / 1000 - step_start_time;
2117
+ const action_count = result.length;
2118
+ const success_count = result.filter((r) => !r.error).length;
2119
+ const failure_count = action_count - success_count;
2120
+ const success_indicator = success_count ? `✅ ${success_count}` : '';
2121
+ const failure_indicator = failure_count ? `❌ ${failure_count}` : '';
2122
+ const status_parts = [success_indicator, failure_indicator].filter(Boolean);
2123
+ const status_str = status_parts.length ? status_parts.join(' | ') : '✅ 0';
2124
+ this.logger.info(`📍 Step ${this.state.n_steps}: Ran ${action_count} actions in ${step_duration.toFixed(2)}s: ${status_str}`);
2125
+ }
2126
+ _log_agent_event(max_steps, agent_run_error) {
2127
+ if (!this.telemetry) {
2128
+ return;
2129
+ }
2130
+ const token_summary = this.token_cost_service?.get_usage_tokens_for_model?.(this.llm.model) ?? {
2131
+ prompt_tokens: 0,
2132
+ completion_tokens: 0,
2133
+ total_tokens: 0,
2134
+ };
2135
+ const action_history_data = this.history.history.map((historyItem) => {
2136
+ if (!historyItem.model_output) {
2137
+ return null;
2138
+ }
2139
+ return historyItem.model_output.action.map((action) => {
2140
+ if (typeof action?.model_dump === 'function') {
2141
+ return action.model_dump({ exclude_unset: true });
2142
+ }
2143
+ return action;
2144
+ });
2145
+ });
2146
+ const final_result = this.history.final_result();
2147
+ const final_result_str = final_result != null ? JSON.stringify(final_result) : null;
2148
+ let cdpHost = null;
2149
+ const cdpUrl = this.browser_session?.cdp_url;
2150
+ if (typeof cdpUrl === 'string' && cdpUrl) {
2151
+ try {
2152
+ const parsed = new URL(cdpUrl);
2153
+ cdpHost = parsed.hostname || cdpUrl;
2154
+ }
2155
+ catch {
2156
+ cdpHost = cdpUrl;
2157
+ }
2158
+ }
2159
+ const plannerModel = this.settings?.planner_llm &&
2160
+ typeof this.settings.planner_llm === 'object'
2161
+ ? (this.settings.planner_llm.model ?? null)
2162
+ : null;
2163
+ this.telemetry.capture(new AgentTelemetryEvent({
2164
+ task: this.task,
2165
+ model: this.llm.model,
2166
+ model_provider: this.llm.provider ?? 'unknown',
2167
+ planner_llm: plannerModel,
2168
+ max_steps: max_steps,
2169
+ max_actions_per_step: this.settings.max_actions_per_step,
2170
+ use_vision: this.settings.use_vision,
2171
+ use_validation: this.settings.validate_output,
2172
+ version: this.version,
2173
+ source: this.source,
2174
+ cdp_url: cdpHost,
2175
+ action_errors: this.history.errors(),
2176
+ action_history: action_history_data,
2177
+ urls_visited: this.history.urls(),
2178
+ steps: this.state.n_steps,
2179
+ total_input_tokens: token_summary.prompt_tokens ?? 0,
2180
+ total_duration_seconds: this.history.total_duration_seconds(),
2181
+ success: this.history.is_successful(),
2182
+ final_result_response: final_result_str,
2183
+ error_message: agent_run_error,
2184
+ }));
2185
+ }
2186
+ async _make_history_item(model_output, browser_state_summary, result, metadata) {
2187
+ const interacted_elements = model_output
2188
+ ? AgentHistory.get_interacted_element(model_output, browser_state_summary.selector_map)
2189
+ : [];
2190
+ const state = new BrowserStateHistory(browser_state_summary.url, browser_state_summary.title, browser_state_summary.tabs, interacted_elements, this._current_screenshot_path);
2191
+ this.history.add_item(new AgentHistory(model_output, result, state, metadata));
2192
+ }
2193
+ save_file_system_state() {
2194
+ if (!this.file_system) {
2195
+ this.logger.error('💾 File system is not set up. Cannot save state.');
2196
+ throw new Error('File system is not set up. Cannot save state.');
2197
+ }
2198
+ this.state.file_system_state = this.file_system.get_state();
2199
+ }
2200
+ }