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