browser-debugging-daemon 1.0.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.
@@ -0,0 +1,382 @@
1
+ function extractTextContent(content) {
2
+ if (typeof content === "string") {
3
+ return content;
4
+ }
5
+
6
+ if (Array.isArray(content)) {
7
+ return content
8
+ .map((item) => {
9
+ if (typeof item === "string") return item;
10
+ if (item?.type === "text") return item.text || "";
11
+ return "";
12
+ })
13
+ .join("");
14
+ }
15
+
16
+ return "";
17
+ }
18
+
19
+ function extractJsonObject(text) {
20
+ const trimmed = text.trim();
21
+ const direct = tryParseJson(trimmed);
22
+ if (direct) return direct;
23
+
24
+ const start = trimmed.indexOf("{");
25
+ const end = trimmed.lastIndexOf("}");
26
+ if (start === -1 || end === -1 || end <= start) {
27
+ return null;
28
+ }
29
+
30
+ return tryParseJson(trimmed.slice(start, end + 1));
31
+ }
32
+
33
+ function tryParseJson(text) {
34
+ try {
35
+ return JSON.parse(text);
36
+ } catch (error) {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function sleep(ms) {
42
+ return new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
+
45
+ function parseCsvList(value) {
46
+ return String(value || "")
47
+ .split(",")
48
+ .map((item) => item.trim())
49
+ .filter(Boolean);
50
+ }
51
+
52
+ const DEFAULT_MODEL_FALLBACKS = [];
53
+ const DEFAULT_BASE_URL = "http://localhost:8045/v1";
54
+ const BASE_URL_ENV_KEYS = [
55
+ "OPENAI_BASE_URL",
56
+ "LLM_BASE_URL",
57
+ "MODEL_BASE_URL",
58
+ "ANTHROPIC_BASE_URL",
59
+ ];
60
+ const MODEL_ENV_KEYS = [
61
+ "OPENAI_MODEL",
62
+ "LLM_MODEL",
63
+ "MODEL_NAME",
64
+ "ANTHROPIC_MODEL",
65
+ "CLAUDE_CODE_MODEL",
66
+ ];
67
+ const API_KEY_ENV_KEYS = [
68
+ "OPENAI_API_KEY",
69
+ "LLM_API_KEY",
70
+ "MODEL_API_KEY",
71
+ "ANTHROPIC_API_KEY",
72
+ ];
73
+
74
+ function firstNonEmpty(...values) {
75
+ for (const value of values) {
76
+ if (typeof value !== "string") continue;
77
+ const trimmed = value.trim();
78
+ if (trimmed) return trimmed;
79
+ }
80
+ return "";
81
+ }
82
+
83
+ function firstEnv(keys = []) {
84
+ for (const key of keys) {
85
+ const value = process.env[key];
86
+ if (typeof value !== "string") continue;
87
+ const trimmed = value.trim();
88
+ if (trimmed) return trimmed;
89
+ }
90
+ return "";
91
+ }
92
+
93
+ export class OpenAIPlanner {
94
+ constructor(options = {}) {
95
+ this.apiKey = firstNonEmpty(options.apiKey, firstEnv(API_KEY_ENV_KEYS), "browser-subagent-local");
96
+ this.baseUrl = firstNonEmpty(options.baseUrl, firstEnv(BASE_URL_ENV_KEYS), DEFAULT_BASE_URL).replace(/\/$/, "");
97
+ this.model = firstNonEmpty(options.model, firstEnv(MODEL_ENV_KEYS));
98
+ this.modelFallbacks = parseCsvList(
99
+ options.modelFallbacks
100
+ || process.env.BROWSER_PLANNER_MODEL_FALLBACKS
101
+ || DEFAULT_MODEL_FALLBACKS.join(",")
102
+ );
103
+ this.requestTimeoutMs = Number.parseInt(process.env.BROWSER_PLANNER_TIMEOUT_MS, 10) || 45000;
104
+ this.maxRetries = Number.parseInt(process.env.BROWSER_PLANNER_MAX_RETRIES, 10) || 2;
105
+ this.baseRetryDelayMs = Number.parseInt(process.env.BROWSER_PLANNER_RETRY_BACKOFF_MS, 10) || 700;
106
+ this.modelDiscoveryTtlMs = Number.parseInt(process.env.BROWSER_PLANNER_MODEL_CACHE_MS, 10) || 60000;
107
+ this.availableModelsCache = [];
108
+ this.availableModelsCacheAt = 0;
109
+ this.lastHealthyModel = null;
110
+ }
111
+
112
+ buildDecisionSystemPrompt() {
113
+ return [
114
+ "You are an autonomous browser subagent controller.",
115
+ "Your job is to complete the user's task by choosing exactly one next browser action at a time.",
116
+ "You must output valid JSON only.",
117
+ "Prefer the simplest action that makes forward progress.",
118
+ "If the task is complete, return status=completed and next_action.type=finish.",
119
+ "If the task is blocked or impossible, return status=failed and next_action.type=fail with a clear reason.",
120
+ "If you need guidance from the main agent or a human operator, return status=needs_input and next_action.type=ask_main_agent.",
121
+ "Be proactive about asking for guidance when you detect a login wall, permission dialog, or unexpected page/runtime error.",
122
+ "Never invent elements that are not present in the observed element list.",
123
+ "Use goto only when you know the exact URL to open.",
124
+ "Available action types: goto, click, type, hover, keypress, scroll, finish, fail, ask_main_agent.",
125
+ ].join(" ");
126
+ }
127
+
128
+ buildDecisionUserPrompt(input) {
129
+ return JSON.stringify({
130
+ task: input.taskInstruction,
131
+ current_page: input.page,
132
+ observed_elements: input.elements,
133
+ history: input.history,
134
+ operator_messages: input.operatorMessages || [],
135
+ debug_state: {
136
+ recentErrors: input.debugState.recentErrors,
137
+ recentConsole: input.debugState.recentConsole,
138
+ recentNetwork: input.debugState.recentNetwork,
139
+ },
140
+ required_output_schema: {
141
+ thinking: "string",
142
+ status: "continue|completed|failed|needs_input",
143
+ summary: "string",
144
+ next_action: {
145
+ type: "goto|click|type|hover|keypress|scroll|finish|fail|ask_main_agent",
146
+ url: "string?",
147
+ id: "number?",
148
+ text: "string?",
149
+ key: "string?",
150
+ direction: "down|up|top|bottom?",
151
+ result: "string?",
152
+ reason: "string?",
153
+ question: "string?",
154
+ details: "string?",
155
+ suggested_reply: "string?",
156
+ },
157
+ },
158
+ }, null, 2);
159
+ }
160
+
161
+ buildVerificationSystemPrompt() {
162
+ return [
163
+ "You are a browser task verifier.",
164
+ "You evaluate whether the browser task is complete after the last action.",
165
+ "You must output valid JSON only.",
166
+ "Be conservative: only mark completed when the page state clearly satisfies the task.",
167
+ "If progress was made but the task is not finished, return goal_status=incomplete.",
168
+ "If the task appears blocked or impossible from the current state, return goal_status=blocked.",
169
+ ].join(" ");
170
+ }
171
+
172
+ buildVerificationUserPrompt(input) {
173
+ return JSON.stringify({
174
+ task: input.taskInstruction,
175
+ page_after_action: input.page,
176
+ observed_elements_after_action: input.elements,
177
+ last_action: input.lastAction,
178
+ last_action_result: input.lastActionResult,
179
+ history: input.history,
180
+ debug_state: {
181
+ recentErrors: input.debugState.recentErrors,
182
+ recentConsole: input.debugState.recentConsole,
183
+ recentNetwork: input.debugState.recentNetwork,
184
+ },
185
+ required_output_schema: {
186
+ goal_status: "incomplete|completed|blocked",
187
+ confidence: "low|medium|high",
188
+ summary: "string",
189
+ evidence: ["string"],
190
+ next_hint: "string",
191
+ },
192
+ }, null, 2);
193
+ }
194
+
195
+ shouldTryAnotherModel(error) {
196
+ const message = String(error?.message || "").toLowerCase();
197
+ return message.includes("all accounts failed")
198
+ || message.includes("unhealthy")
199
+ || message.includes("token error")
200
+ || message.includes("no such model")
201
+ || message.includes("not found")
202
+ || message.includes("model_not_found")
203
+ || message.includes("does not exist")
204
+ || message.includes("unsupported model")
205
+ || message.includes("unsupported value")
206
+ || message.includes("invalid model")
207
+ || message.includes("bad model");
208
+ }
209
+
210
+ async listAvailableModels() {
211
+ const now = Date.now();
212
+ if (
213
+ this.availableModelsCache.length > 0
214
+ && now - this.availableModelsCacheAt < this.modelDiscoveryTtlMs
215
+ ) {
216
+ return this.availableModelsCache;
217
+ }
218
+
219
+ const controller = new AbortController();
220
+ const timeoutMs = Math.max(3000, Math.min(this.requestTimeoutMs, 8000));
221
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
222
+ try {
223
+ const response = await fetch(`${this.baseUrl}/models`, {
224
+ method: "GET",
225
+ signal: controller.signal,
226
+ headers: {
227
+ Authorization: `Bearer ${this.apiKey}`,
228
+ },
229
+ });
230
+ if (!response.ok) {
231
+ return [];
232
+ }
233
+ const data = await response.json().catch(() => ({}));
234
+ const models = Array.isArray(data?.data)
235
+ ? data.data
236
+ .map((item) => (typeof item?.id === "string" ? item.id.trim() : ""))
237
+ .filter(Boolean)
238
+ : [];
239
+ this.availableModelsCache = models;
240
+ this.availableModelsCacheAt = now;
241
+ return models;
242
+ } catch (error) {
243
+ return [];
244
+ } finally {
245
+ clearTimeout(timeoutId);
246
+ }
247
+ }
248
+
249
+ async buildModelCandidates() {
250
+ const availableModels = await this.listAvailableModels();
251
+ const discovered = availableModels.length > 0
252
+ ? [
253
+ this.model && availableModels.includes(this.model) ? this.model : null,
254
+ ...availableModels,
255
+ ].filter(Boolean)
256
+ : [];
257
+ const configured = [this.model, ...this.modelFallbacks].filter(Boolean);
258
+ const candidates = [
259
+ this.lastHealthyModel,
260
+ ...discovered,
261
+ ...configured,
262
+ ].filter(Boolean);
263
+
264
+ const unique = [];
265
+ const seen = new Set();
266
+ for (const modelName of candidates) {
267
+ if (!seen.has(modelName)) {
268
+ seen.add(modelName);
269
+ unique.push(modelName);
270
+ }
271
+ }
272
+ if (unique.length > 0) {
273
+ return unique;
274
+ }
275
+
276
+ throw new Error(
277
+ "Planner model is not configured and no models were discovered. Set OPENAI_MODEL (or LLM_MODEL/ANTHROPIC_MODEL), or ensure /models is reachable."
278
+ );
279
+ }
280
+
281
+ async requestJsonWithModel(systemPrompt, userPrompt, modelName) {
282
+ let lastError = null;
283
+ const attempts = Math.max(1, this.maxRetries + 1);
284
+
285
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
286
+ const controller = new AbortController();
287
+ const timeoutId = setTimeout(() => controller.abort(), this.requestTimeoutMs);
288
+ try {
289
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
290
+ method: "POST",
291
+ signal: controller.signal,
292
+ headers: {
293
+ "Content-Type": "application/json",
294
+ Authorization: `Bearer ${this.apiKey}`,
295
+ },
296
+ body: JSON.stringify({
297
+ model: modelName,
298
+ temperature: 0,
299
+ response_format: { type: "json_object" },
300
+ messages: [
301
+ { role: "system", content: systemPrompt },
302
+ { role: "user", content: userPrompt },
303
+ ],
304
+ }),
305
+ });
306
+
307
+ if (!response.ok) {
308
+ const errorText = await response.text().catch(() => response.statusText);
309
+ const retryable = response.status >= 500 || response.status === 429;
310
+ throw Object.assign(new Error(`Planner request failed (${modelName}): ${response.status} ${errorText}`), {
311
+ retryable,
312
+ });
313
+ }
314
+
315
+ const data = await response.json();
316
+ const message = data?.choices?.[0]?.message;
317
+ const text = extractTextContent(message?.content || "");
318
+ const parsed = extractJsonObject(text);
319
+
320
+ if (!parsed) {
321
+ throw Object.assign(new Error(`Planner returned non-JSON output (${modelName}): ${text}`), {
322
+ retryable: false,
323
+ });
324
+ }
325
+
326
+ this.lastHealthyModel = modelName;
327
+ return parsed;
328
+ } catch (error) {
329
+ const isTimeout = error?.name === "AbortError";
330
+ const retryable = isTimeout || error?.retryable === true || !Object.prototype.hasOwnProperty.call(error || {}, "retryable");
331
+ lastError = isTimeout
332
+ ? new Error(`Planner request timed out (${modelName}) after ${this.requestTimeoutMs}ms.`)
333
+ : error;
334
+
335
+ if (!retryable || attempt >= attempts) {
336
+ throw lastError;
337
+ }
338
+
339
+ const delay = this.baseRetryDelayMs * (2 ** (attempt - 1));
340
+ await sleep(delay);
341
+ } finally {
342
+ clearTimeout(timeoutId);
343
+ }
344
+ }
345
+
346
+ throw lastError || new Error("Planner request failed.");
347
+ }
348
+
349
+ async requestJson(systemPrompt, userPrompt) {
350
+ const candidates = await this.buildModelCandidates();
351
+ let lastError = null;
352
+
353
+ for (let index = 0; index < candidates.length; index += 1) {
354
+ const candidate = candidates[index];
355
+ try {
356
+ return await this.requestJsonWithModel(systemPrompt, userPrompt, candidate);
357
+ } catch (error) {
358
+ lastError = error;
359
+ const hasNext = index < candidates.length - 1;
360
+ if (!hasNext || !this.shouldTryAnotherModel(error)) {
361
+ throw error;
362
+ }
363
+ }
364
+ }
365
+
366
+ throw lastError || new Error("Planner request failed: no healthy model available.");
367
+ }
368
+
369
+ async decide(input) {
370
+ return this.requestJson(
371
+ this.buildDecisionSystemPrompt(),
372
+ this.buildDecisionUserPrompt(input)
373
+ );
374
+ }
375
+
376
+ async verify(input) {
377
+ return this.requestJson(
378
+ this.buildVerificationSystemPrompt(),
379
+ this.buildVerificationUserPrompt(input)
380
+ );
381
+ }
382
+ }