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.
- package/LICENSE +21 -0
- package/README.md +761 -0
- package/dist/agent/cloud-events.d.ts +264 -0
- package/dist/agent/cloud-events.js +318 -0
- package/dist/agent/gif.d.ts +15 -0
- package/dist/agent/gif.js +215 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.js +8 -0
- package/dist/agent/message-manager/service.d.ts +30 -0
- package/dist/agent/message-manager/service.js +208 -0
- package/dist/agent/message-manager/utils.d.ts +2 -0
- package/dist/agent/message-manager/utils.js +41 -0
- package/dist/agent/message-manager/views.d.ts +26 -0
- package/dist/agent/message-manager/views.js +73 -0
- package/dist/agent/prompts.d.ts +52 -0
- package/dist/agent/prompts.js +259 -0
- package/dist/agent/service.d.ts +290 -0
- package/dist/agent/service.js +2200 -0
- package/dist/agent/views.d.ts +741 -0
- package/dist/agent/views.js +537 -0
- package/dist/browser/browser.d.ts +7 -0
- package/dist/browser/browser.js +5 -0
- package/dist/browser/context.d.ts +8 -0
- package/dist/browser/context.js +4 -0
- package/dist/browser/dvd-screensaver.d.ts +101 -0
- package/dist/browser/dvd-screensaver.js +270 -0
- package/dist/browser/extensions.d.ts +63 -0
- package/dist/browser/extensions.js +359 -0
- package/dist/browser/index.d.ts +10 -0
- package/dist/browser/index.js +9 -0
- package/dist/browser/playwright-manager.d.ts +47 -0
- package/dist/browser/playwright-manager.js +146 -0
- package/dist/browser/profile.d.ts +196 -0
- package/dist/browser/profile.js +815 -0
- package/dist/browser/session.d.ts +505 -0
- package/dist/browser/session.js +3409 -0
- package/dist/browser/types.d.ts +1184 -0
- package/dist/browser/types.js +1 -0
- package/dist/browser/utils.d.ts +1 -0
- package/dist/browser/utils.js +19 -0
- package/dist/browser/views.d.ts +78 -0
- package/dist/browser/views.js +72 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +44 -0
- package/dist/config.d.ts +108 -0
- package/dist/config.js +430 -0
- package/dist/controller/index.d.ts +3 -0
- package/dist/controller/index.js +3 -0
- package/dist/controller/registry/index.d.ts +2 -0
- package/dist/controller/registry/index.js +2 -0
- package/dist/controller/registry/service.d.ts +45 -0
- package/dist/controller/registry/service.js +184 -0
- package/dist/controller/registry/views.d.ts +55 -0
- package/dist/controller/registry/views.js +174 -0
- package/dist/controller/service.d.ts +49 -0
- package/dist/controller/service.js +1176 -0
- package/dist/controller/views.d.ts +241 -0
- package/dist/controller/views.js +88 -0
- package/dist/dom/clickable-element-processor/service.d.ts +11 -0
- package/dist/dom/clickable-element-processor/service.js +60 -0
- package/dist/dom/dom_tree/index.js +1400 -0
- package/dist/dom/history-tree-processor/service.d.ts +14 -0
- package/dist/dom/history-tree-processor/service.js +75 -0
- package/dist/dom/history-tree-processor/view.d.ts +54 -0
- package/dist/dom/history-tree-processor/view.js +56 -0
- package/dist/dom/playground/extraction.d.ts +19 -0
- package/dist/dom/playground/extraction.js +187 -0
- package/dist/dom/playground/process-dom.d.ts +1 -0
- package/dist/dom/playground/process-dom.js +5 -0
- package/dist/dom/playground/test-accessibility.d.ts +44 -0
- package/dist/dom/playground/test-accessibility.js +111 -0
- package/dist/dom/service.d.ts +19 -0
- package/dist/dom/service.js +227 -0
- package/dist/dom/utils.d.ts +1 -0
- package/dist/dom/utils.js +6 -0
- package/dist/dom/views.d.ts +61 -0
- package/dist/dom/views.js +247 -0
- package/dist/event-bus.d.ts +11 -0
- package/dist/event-bus.js +19 -0
- package/dist/exceptions.d.ts +10 -0
- package/dist/exceptions.js +22 -0
- package/dist/filesystem/file-system.d.ts +68 -0
- package/dist/filesystem/file-system.js +412 -0
- package/dist/filesystem/index.d.ts +1 -0
- package/dist/filesystem/index.js +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +33 -0
- package/dist/integrations/gmail/actions.d.ts +12 -0
- package/dist/integrations/gmail/actions.js +113 -0
- package/dist/integrations/gmail/index.d.ts +2 -0
- package/dist/integrations/gmail/index.js +2 -0
- package/dist/integrations/gmail/service.d.ts +61 -0
- package/dist/integrations/gmail/service.js +260 -0
- package/dist/llm/anthropic/chat.d.ts +28 -0
- package/dist/llm/anthropic/chat.js +126 -0
- package/dist/llm/anthropic/index.d.ts +2 -0
- package/dist/llm/anthropic/index.js +2 -0
- package/dist/llm/anthropic/serializer.d.ts +68 -0
- package/dist/llm/anthropic/serializer.js +285 -0
- package/dist/llm/aws/chat-anthropic.d.ts +61 -0
- package/dist/llm/aws/chat-anthropic.js +176 -0
- package/dist/llm/aws/chat-bedrock.d.ts +15 -0
- package/dist/llm/aws/chat-bedrock.js +80 -0
- package/dist/llm/aws/index.d.ts +3 -0
- package/dist/llm/aws/index.js +3 -0
- package/dist/llm/aws/serializer.d.ts +5 -0
- package/dist/llm/aws/serializer.js +68 -0
- package/dist/llm/azure/chat.d.ts +15 -0
- package/dist/llm/azure/chat.js +83 -0
- package/dist/llm/azure/index.d.ts +1 -0
- package/dist/llm/azure/index.js +1 -0
- package/dist/llm/base.d.ts +16 -0
- package/dist/llm/base.js +1 -0
- package/dist/llm/deepseek/chat.d.ts +15 -0
- package/dist/llm/deepseek/chat.js +51 -0
- package/dist/llm/deepseek/index.d.ts +2 -0
- package/dist/llm/deepseek/index.js +2 -0
- package/dist/llm/deepseek/serializer.d.ts +6 -0
- package/dist/llm/deepseek/serializer.js +57 -0
- package/dist/llm/exceptions.d.ts +10 -0
- package/dist/llm/exceptions.js +18 -0
- package/dist/llm/google/chat.d.ts +20 -0
- package/dist/llm/google/chat.js +144 -0
- package/dist/llm/google/index.d.ts +2 -0
- package/dist/llm/google/index.js +2 -0
- package/dist/llm/google/serializer.d.ts +6 -0
- package/dist/llm/google/serializer.js +64 -0
- package/dist/llm/groq/chat.d.ts +15 -0
- package/dist/llm/groq/chat.js +52 -0
- package/dist/llm/groq/index.d.ts +3 -0
- package/dist/llm/groq/index.js +3 -0
- package/dist/llm/groq/parser.d.ts +32 -0
- package/dist/llm/groq/parser.js +189 -0
- package/dist/llm/groq/serializer.d.ts +6 -0
- package/dist/llm/groq/serializer.js +56 -0
- package/dist/llm/messages.d.ts +77 -0
- package/dist/llm/messages.js +157 -0
- package/dist/llm/ollama/chat.d.ts +15 -0
- package/dist/llm/ollama/chat.js +77 -0
- package/dist/llm/ollama/index.d.ts +2 -0
- package/dist/llm/ollama/index.js +2 -0
- package/dist/llm/ollama/serializer.d.ts +6 -0
- package/dist/llm/ollama/serializer.js +53 -0
- package/dist/llm/openai/chat.d.ts +38 -0
- package/dist/llm/openai/chat.js +174 -0
- package/dist/llm/openai/index.d.ts +3 -0
- package/dist/llm/openai/index.js +3 -0
- package/dist/llm/openai/like.d.ts +17 -0
- package/dist/llm/openai/like.js +19 -0
- package/dist/llm/openai/serializer.d.ts +6 -0
- package/dist/llm/openai/serializer.js +57 -0
- package/dist/llm/openrouter/chat.d.ts +15 -0
- package/dist/llm/openrouter/chat.js +74 -0
- package/dist/llm/openrouter/index.d.ts +2 -0
- package/dist/llm/openrouter/index.js +2 -0
- package/dist/llm/openrouter/serializer.d.ts +3 -0
- package/dist/llm/openrouter/serializer.js +3 -0
- package/dist/llm/schema.d.ts +6 -0
- package/dist/llm/schema.js +77 -0
- package/dist/llm/views.d.ts +15 -0
- package/dist/llm/views.js +12 -0
- package/dist/logging-config.d.ts +25 -0
- package/dist/logging-config.js +89 -0
- package/dist/mcp/client.d.ts +142 -0
- package/dist/mcp/client.js +638 -0
- package/dist/mcp/controller.d.ts +6 -0
- package/dist/mcp/controller.js +38 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/server.d.ts +134 -0
- package/dist/mcp/server.js +759 -0
- package/dist/observability-decorators.d.ts +158 -0
- package/dist/observability-decorators.js +286 -0
- package/dist/observability.d.ts +23 -0
- package/dist/observability.js +58 -0
- package/dist/screenshots/index.d.ts +1 -0
- package/dist/screenshots/index.js +1 -0
- package/dist/screenshots/service.d.ts +6 -0
- package/dist/screenshots/service.js +28 -0
- package/dist/sync/auth.d.ts +27 -0
- package/dist/sync/auth.js +205 -0
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +2 -0
- package/dist/sync/service.d.ts +21 -0
- package/dist/sync/service.js +146 -0
- package/dist/telemetry/index.d.ts +2 -0
- package/dist/telemetry/index.js +2 -0
- package/dist/telemetry/service.d.ts +12 -0
- package/dist/telemetry/service.js +85 -0
- package/dist/telemetry/views.d.ts +112 -0
- package/dist/telemetry/views.js +112 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.js +2 -0
- package/dist/tokens/service.d.ts +35 -0
- package/dist/tokens/service.js +423 -0
- package/dist/tokens/views.d.ts +58 -0
- package/dist/tokens/views.js +1 -0
- package/dist/utils.d.ts +128 -0
- package/dist/utils.js +529 -0
- 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
|
+
}
|