jerob 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.
- package/CLI/cli.ts +42 -0
- package/README.md +137 -0
- package/SETUP.md +584 -0
- package/agent/action-tracker.ts +45 -0
- package/agent/agent-tools.ts +111 -0
- package/agent/approval.ts +137 -0
- package/agent/diff-view.ts +26 -0
- package/agent/orchestrator.ts +186 -0
- package/agent/tool-executor.ts +463 -0
- package/agent/types.ts +69 -0
- package/ask/orchestrator.ts +244 -0
- package/auth/auth.ts +567 -0
- package/auth/config-store.ts +77 -0
- package/auth/crypto.ts +51 -0
- package/auth/env-writer.ts +82 -0
- package/bin/jerob.js +28 -0
- package/config/ai.config.ts +163 -0
- package/email_ops/email-tools.ts +178 -0
- package/email_ops/email_functions.ts +443 -0
- package/email_ops/email_init.ts +92 -0
- package/email_ops/email_pass_store.ts +61 -0
- package/email_ops/email_server.ts +29 -0
- package/email_ops/types.ts +88 -0
- package/index.ts +176 -0
- package/package.json +88 -0
- package/plan/browser-agent/README.md +118 -0
- package/plan/browser-agent/USAGE.md +308 -0
- package/plan/browser-agent/evaluator.ts +353 -0
- package/plan/browser-agent/executor.ts +372 -0
- package/plan/browser-agent/index.ts +13 -0
- package/plan/browser-agent/orchestrator.ts +323 -0
- package/plan/browser-agent/planner.ts +200 -0
- package/plan/browser-agent/types.ts +62 -0
- package/plan/browser-tool.ts +128 -0
- package/plan/index.ts +12 -0
- package/plan/orchestrator.ts +214 -0
- package/plan/planner.ts +183 -0
- package/plan/selection.ts +50 -0
- package/plan/types.ts +13 -0
- package/plan/web-tools.ts +119 -0
- package/scheduler/ARCHITECTURE.md +263 -0
- package/scheduler/README.md +200 -0
- package/scheduler/SETUP-READY.sql +84 -0
- package/scheduler/check-status.sql +124 -0
- package/scheduler/config-sync.ts +91 -0
- package/scheduler/db-migrate.ts +271 -0
- package/scheduler/db.ts +162 -0
- package/scheduler/debug.ts +184 -0
- package/scheduler/orchestrator.ts +438 -0
- package/scheduler/planner.ts +170 -0
- package/scheduler/update-task-email.ts +70 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/linked-project.json +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/deploy.ps1 +50 -0
- package/supabase/functions/scheduler-tick/index.ts +496 -0
- package/supabase/supabase/.temp/linked-project.json +1 -0
- package/tsconfig.json +33 -0
- package/tui/spinner.ts +33 -0
- package/tui/spinup.ts +67 -0
- package/tui/terminal-render.ts +16 -0
- package/utils/llm-error.ts +185 -0
- package/utils/model-validator.ts +247 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { Stagehand } from "@browserbasehq/stagehand";
|
|
2
|
+
import { existsSync, mkdirSync } from "fs";
|
|
3
|
+
import path, { resolve } from "path";
|
|
4
|
+
import type { BrowserPlan, ExecutionResult } from "./types";
|
|
5
|
+
|
|
6
|
+
const DOWNLOADS_PATH = resolve("C:/Users/SWEETY/Downloads");
|
|
7
|
+
|
|
8
|
+
let globalStagehand: InstanceType<typeof Stagehand> | null = null;
|
|
9
|
+
|
|
10
|
+
function getStagehandClient(): InstanceType<typeof Stagehand> {
|
|
11
|
+
if (!globalStagehand) {
|
|
12
|
+
globalStagehand = new Stagehand({
|
|
13
|
+
env: "LOCAL",
|
|
14
|
+
localBrowserLaunchOptions: {
|
|
15
|
+
headless: false, // Show browser window
|
|
16
|
+
// devtools: true, // Open developer tools
|
|
17
|
+
port: 9222, // Fixed CDP debugging port
|
|
18
|
+
args: [
|
|
19
|
+
'--no-sandbox',
|
|
20
|
+
'--disable-setuid-sandbox',
|
|
21
|
+
'--disable-web-security',
|
|
22
|
+
'--allow-running-insecure-content',
|
|
23
|
+
],
|
|
24
|
+
chromiumSandbox: false, // Disable sandbox (adds --no-sandbox)
|
|
25
|
+
ignoreHTTPSErrors: true, // Ignore certificate errors
|
|
26
|
+
locale: 'en-US', // Set browser language
|
|
27
|
+
deviceScaleFactor: 1.0, // Display scaling
|
|
28
|
+
downloadsPath: './downloads', // Download directory
|
|
29
|
+
acceptDownloads: true, // Allow downloads
|
|
30
|
+
connectTimeoutMs: 30000, // Connection timeout
|
|
31
|
+
executablePath:"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
|
|
32
|
+
},
|
|
33
|
+
model: {
|
|
34
|
+
modelName: "google/gemini-3.1-flash-lite-preview",
|
|
35
|
+
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return globalStagehand;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Tools whose output is giant DOM/accessibility blobs — skip entirely
|
|
43
|
+
const SKIP_TOOL_OUTPUT = new Set(["ariaTree", "screenshot", "cdpSnapshot"]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parses stagehand agent messages into a compact, evaluator-friendly transcript.
|
|
47
|
+
*
|
|
48
|
+
* Stagehand message shapes (confirmed from live logs):
|
|
49
|
+
*
|
|
50
|
+
* assistant: { role:"assistant", content: [
|
|
51
|
+
* { type:"tool-call", toolName, input, ... } <- agent action intent
|
|
52
|
+
* { type:"text", text: "..." } <- agent's SUMMARY / reasoning text
|
|
53
|
+
* ]}
|
|
54
|
+
*
|
|
55
|
+
* tool: { role:"tool", content: [
|
|
56
|
+
* { type:"tool-result", toolName, output: { type:"json"|"content", value:... } }
|
|
57
|
+
* ]}
|
|
58
|
+
*
|
|
59
|
+
* Strategy:
|
|
60
|
+
* - assistant[type=text] → the agent's written summary/reasoning — MOST VALUABLE
|
|
61
|
+
* - done[input.reasoning] → agent's stated reason for finishing
|
|
62
|
+
* - extract[output] → structured data
|
|
63
|
+
* - navigate/act/wait/goto → one-line confirmation (success + brief detail)
|
|
64
|
+
* - ariaTree/screenshot → SKIPPED (too large, no signal for evaluator)
|
|
65
|
+
*/
|
|
66
|
+
function parseAgentMessages(messages: unknown[]): {
|
|
67
|
+
transcript: string;
|
|
68
|
+
extractedData: unknown;
|
|
69
|
+
} {
|
|
70
|
+
if (!Array.isArray(messages)) return { transcript: "", extractedData: null };
|
|
71
|
+
|
|
72
|
+
const summaryTexts: string[] = []; // agent's written text — highest priority
|
|
73
|
+
const actionLines: string[] = []; // tool confirmations — supporting context
|
|
74
|
+
const allExtracts: unknown[] = [];
|
|
75
|
+
|
|
76
|
+
for (const msg of messages) {
|
|
77
|
+
if (typeof msg !== "object" || msg === null) continue;
|
|
78
|
+
const role: string = (msg as any).role ?? "";
|
|
79
|
+
const content: unknown = (msg as any).content;
|
|
80
|
+
const items: unknown[] = Array.isArray(content) ? content : [];
|
|
81
|
+
|
|
82
|
+
// ── assistant messages ────────────────────────────────────────────
|
|
83
|
+
if (role === "assistant") {
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
if (typeof item !== "object" || item === null) continue;
|
|
86
|
+
const type: string = (item as any).type ?? "";
|
|
87
|
+
|
|
88
|
+
// {type:"text"} — this is the agent's written summary/answer
|
|
89
|
+
if (type === "text") {
|
|
90
|
+
const text = String((item as any).text ?? "").trim();
|
|
91
|
+
if (text) summaryTexts.push(text);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// {type:"tool-call", toolName:"done"} — capture reasoning from input
|
|
95
|
+
if (type === "tool-call" && (item as any).toolName === "done") {
|
|
96
|
+
const reasoning = (item as any).input?.reasoning;
|
|
97
|
+
if (typeof reasoning === "string" && reasoning.trim()) {
|
|
98
|
+
summaryTexts.push(`[DONE REASON] ${reasoning.trim()}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── tool result messages ──────────────────────────────────────────
|
|
106
|
+
if (role === "tool") {
|
|
107
|
+
for (const item of items) {
|
|
108
|
+
if (typeof item !== "object" || item === null) continue;
|
|
109
|
+
const toolName: string = (item as any).toolName ?? (item as any).name ?? "tool";
|
|
110
|
+
const output: unknown = (item as any).output;
|
|
111
|
+
|
|
112
|
+
// Skip large DOM blobs — useless to evaluator
|
|
113
|
+
if (SKIP_TOOL_OUTPUT.has(toolName)) continue;
|
|
114
|
+
|
|
115
|
+
// extract — capture structured data
|
|
116
|
+
if (toolName === "extract") {
|
|
117
|
+
const structured = extractStructured(output);
|
|
118
|
+
if (structured != null) allExtracts.push(structured);
|
|
119
|
+
actionLines.push(`[EXTRACT] ${JSON.stringify(structured ?? output)}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// done tool result — just note success
|
|
124
|
+
if (toolName === "done") {
|
|
125
|
+
const val = getOutputValue(output);
|
|
126
|
+
const ok = (val as any)?.success ?? (val as any)?.taskComplete ?? true;
|
|
127
|
+
actionLines.push(`[DONE] taskComplete=${ok}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// navigate / goto / act / wait / scroll / click / type
|
|
132
|
+
// Pull only the minimal success+detail line — not the whole DOM
|
|
133
|
+
const val = getOutputValue(output);
|
|
134
|
+
if (val != null) {
|
|
135
|
+
const success = (val as any)?.success;
|
|
136
|
+
const url = (val as any)?.url;
|
|
137
|
+
const action = (val as any)?.action;
|
|
138
|
+
const waited = (val as any)?.waited;
|
|
139
|
+
const detail = url ?? action ?? waited ?? "";
|
|
140
|
+
const line = [
|
|
141
|
+
`success=${success ?? "?"}`,
|
|
142
|
+
detail ? String(detail) : "",
|
|
143
|
+
]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join(" ");
|
|
146
|
+
actionLines.push(`[${toolName.toUpperCase()}] ${line}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const extractedData =
|
|
153
|
+
allExtracts.length === 1
|
|
154
|
+
? allExtracts[0]
|
|
155
|
+
: allExtracts.length > 1
|
|
156
|
+
? allExtracts
|
|
157
|
+
: null;
|
|
158
|
+
|
|
159
|
+
// Build compact transcript: summaries first (most signal), then action log
|
|
160
|
+
const parts: string[] = [];
|
|
161
|
+
if (summaryTexts.length > 0) {
|
|
162
|
+
parts.push("=== AGENT OUTPUT ===");
|
|
163
|
+
parts.push(summaryTexts.join("\n\n"));
|
|
164
|
+
}
|
|
165
|
+
if (actionLines.length > 0) {
|
|
166
|
+
parts.push("=== ACTION LOG ===");
|
|
167
|
+
parts.push(actionLines.join("\n"));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { transcript: parts.join("\n"), extractedData };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getOutputValue(output: unknown): unknown {
|
|
174
|
+
if (typeof output !== "object" || output === null) return output;
|
|
175
|
+
const o = output as Record<string, unknown>;
|
|
176
|
+
// { type: "json", value: {...} } or { type: "content", value: [...] }
|
|
177
|
+
if ("value" in o) {
|
|
178
|
+
const v = o.value;
|
|
179
|
+
// content arrays: [{type:"text", text:"..."}]
|
|
180
|
+
if (Array.isArray(v)) {
|
|
181
|
+
const texts = v
|
|
182
|
+
.filter((c: any) => c?.type === "text" && typeof c?.text === "string")
|
|
183
|
+
.map((c: any) => c.text)
|
|
184
|
+
.join(" ");
|
|
185
|
+
return texts || v;
|
|
186
|
+
}
|
|
187
|
+
return v;
|
|
188
|
+
}
|
|
189
|
+
return o;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Pull structured data out of an extract tool output */
|
|
193
|
+
function extractStructured(output: unknown): unknown {
|
|
194
|
+
if (output == null) return null;
|
|
195
|
+
const val = getOutputValue(output);
|
|
196
|
+
if (typeof val !== "object" || val === null) return val;
|
|
197
|
+
const v = val as Record<string, unknown>;
|
|
198
|
+
if (v.success && v.result != null) {
|
|
199
|
+
const r = v.result as Record<string, unknown>;
|
|
200
|
+
return r.extraction ?? r;
|
|
201
|
+
}
|
|
202
|
+
if (v.extraction != null) return v.extraction;
|
|
203
|
+
if (v.result != null) return v.result;
|
|
204
|
+
return val;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Executes a browser task using Stagehand's agent() function.
|
|
209
|
+
* The agent autonomously navigates, clicks, types, and extracts data
|
|
210
|
+
* based on the plan's goal — no manual step-by-step execution.
|
|
211
|
+
*/
|
|
212
|
+
export async function executeBrowserPlan(
|
|
213
|
+
plan: BrowserPlan,
|
|
214
|
+
previousFeedback?: string
|
|
215
|
+
): Promise<ExecutionResult[]> {
|
|
216
|
+
let stagehand: InstanceType<typeof Stagehand>;
|
|
217
|
+
try {
|
|
218
|
+
stagehand = getStagehandClient();
|
|
219
|
+
await stagehand.init();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
222
|
+
const hint = msg.toLowerCase().includes("executable")
|
|
223
|
+
? " Make sure Brave browser is installed at the configured path in executor.ts."
|
|
224
|
+
: msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("timeout")
|
|
225
|
+
? " Check that no other browser instance is blocking port 9222."
|
|
226
|
+
: "";
|
|
227
|
+
return [{
|
|
228
|
+
success: false,
|
|
229
|
+
error: `Browser failed to start: ${msg}${hint}`,
|
|
230
|
+
message: "Browser initialization failed",
|
|
231
|
+
stepNumber: 1,
|
|
232
|
+
action: "agent",
|
|
233
|
+
}];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Build the instruction from the plan goal + feedback context
|
|
237
|
+
const instruction = previousFeedback
|
|
238
|
+
? `${plan.goal}\n\nPrevious attempt feedback to address: ${previousFeedback}`
|
|
239
|
+
: plan.goal;
|
|
240
|
+
|
|
241
|
+
const systemPrompt = `You are a browser automation agent.
|
|
242
|
+
|
|
243
|
+
Rules:
|
|
244
|
+
- Execute the task efficiently and completely.
|
|
245
|
+
- Avoid unnecessary navigation.
|
|
246
|
+
- Extract all requested structured data.
|
|
247
|
+
- Stop only when the task is fully complete.
|
|
248
|
+
- Extract details in deep provide complete comprehensive details about the topic then call it done
|
|
249
|
+
|
|
250
|
+
Before calling done:
|
|
251
|
+
1. Re-read the original user request.
|
|
252
|
+
2. Verify every requested piece of information is collected.
|
|
253
|
+
3. If anything is missing, continue browsing.
|
|
254
|
+
4. Only call done when all requirements are satisfied.
|
|
255
|
+
|
|
256
|
+
User asks:
|
|
257
|
+
"Get top 5 jobs and detailed descriptions."
|
|
258
|
+
|
|
259
|
+
Required:
|
|
260
|
+
✓ 5 jobs found
|
|
261
|
+
✓ description for job 1
|
|
262
|
+
✓ description for job 2
|
|
263
|
+
✓ description for job 3
|
|
264
|
+
✓ description for job 4
|
|
265
|
+
✓ description for job 5
|
|
266
|
+
|
|
267
|
+
Only call done when all 6 checks pass.
|
|
268
|
+
`;
|
|
269
|
+
|
|
270
|
+
// Ensure downloads folder exists
|
|
271
|
+
if (!existsSync(DOWNLOADS_PATH)) mkdirSync(DOWNLOADS_PATH, { recursive: true });
|
|
272
|
+
|
|
273
|
+
// Use CDP to intercept downloads — may not be available on all setups
|
|
274
|
+
const conn = (stagehand.context as any)?.conn;
|
|
275
|
+
const downloadedFiles: string[] = [];
|
|
276
|
+
|
|
277
|
+
if (conn && typeof conn.send === "function") {
|
|
278
|
+
try {
|
|
279
|
+
await conn.send("Browser.setDownloadBehavior", {
|
|
280
|
+
behavior: "allow",
|
|
281
|
+
downloadPath: DOWNLOADS_PATH,
|
|
282
|
+
eventsEnabled: true,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const pendingDownloads = new Map<string, string>();
|
|
286
|
+
conn.on("Browser.downloadWillBegin", (params: any) => {
|
|
287
|
+
const filename = params.suggestedFilename || `download-${params.guid}`;
|
|
288
|
+
const savePath = resolve(DOWNLOADS_PATH, filename);
|
|
289
|
+
pendingDownloads.set(params.guid, savePath);
|
|
290
|
+
console.log(`[download] starting → ${savePath}`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
conn.on("Browser.downloadProgress", (params: any) => {
|
|
294
|
+
if (params.state === "completed") {
|
|
295
|
+
const savePath = pendingDownloads.get(params.guid);
|
|
296
|
+
if (savePath) {
|
|
297
|
+
downloadedFiles.push(savePath);
|
|
298
|
+
pendingDownloads.delete(params.guid);
|
|
299
|
+
console.log(`[download] saved → ${savePath}`);
|
|
300
|
+
}
|
|
301
|
+
} else if (params.state === "canceled") {
|
|
302
|
+
pendingDownloads.delete(params.guid);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
} catch {
|
|
306
|
+
// CDP download tracking unavailable — continue without it
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const agent = stagehand.agent({
|
|
312
|
+
mode: "dom",
|
|
313
|
+
systemPrompt,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const res = await agent.execute({
|
|
317
|
+
instruction,
|
|
318
|
+
maxSteps: 10,
|
|
319
|
+
highlightCursor: true,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const messages = res.messages ?? [];
|
|
323
|
+
const { transcript, extractedData } = parseAgentMessages(messages);
|
|
324
|
+
const completed = res.completed ?? false;
|
|
325
|
+
|
|
326
|
+
// Capture any top-level text stagehand puts on the response object
|
|
327
|
+
const topLevelText = [
|
|
328
|
+
(res as any).output,
|
|
329
|
+
(res as any).result,
|
|
330
|
+
(res as any).message,
|
|
331
|
+
(res as any).text,
|
|
332
|
+
]
|
|
333
|
+
.filter((v) => typeof v === "string" && v.trim().length > 0)
|
|
334
|
+
.join("\n");
|
|
335
|
+
|
|
336
|
+
const agentOutput = transcript || topLevelText;
|
|
337
|
+
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
success: completed,
|
|
341
|
+
message: completed
|
|
342
|
+
? "Agent completed the task successfully"
|
|
343
|
+
: "Agent did not complete the task",
|
|
344
|
+
stepNumber: 1,
|
|
345
|
+
action: "agent",
|
|
346
|
+
agentOutput,
|
|
347
|
+
agentMessages: messages,
|
|
348
|
+
data:
|
|
349
|
+
extractedData ??
|
|
350
|
+
(topLevelText ? { output: topLevelText } : undefined),
|
|
351
|
+
...(downloadedFiles.length > 0 && { downloadedFiles }),
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
} catch (error) {
|
|
355
|
+
return [
|
|
356
|
+
{
|
|
357
|
+
success: false,
|
|
358
|
+
error: error instanceof Error ? error.message : String(error),
|
|
359
|
+
message: "Agent execution failed",
|
|
360
|
+
stepNumber: 1,
|
|
361
|
+
action: "agent",
|
|
362
|
+
},
|
|
363
|
+
];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function closeStagehand(): Promise<void> {
|
|
368
|
+
if (globalStagehand) {
|
|
369
|
+
await globalStagehand.close();
|
|
370
|
+
globalStagehand = null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { runBrowserAgentMode } from './orchestrator';
|
|
2
|
+
export type {
|
|
3
|
+
BrowserPlan,
|
|
4
|
+
BrowserStep,
|
|
5
|
+
ExecutionResult,
|
|
6
|
+
IterationResult,
|
|
7
|
+
EvaluationResult,
|
|
8
|
+
BrowserAgentConfig,
|
|
9
|
+
BrowserAgentResult,
|
|
10
|
+
} from './types';
|
|
11
|
+
export { generateBrowserPlan } from './planner';
|
|
12
|
+
export { executeBrowserPlan, closeStagehand } from './executor';
|
|
13
|
+
export { evaluateExecutionResults, shouldContinueIterating } from './evaluator';
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { text, confirm } from "@clack/prompts";
|
|
3
|
+
import { withSpinner } from "../../tui/spinner";
|
|
4
|
+
import { renderHTMLMarkdown } from "../../tui/terminal-render.ts";
|
|
5
|
+
import { generateBrowserPlan } from "./planner";
|
|
6
|
+
import { executeBrowserPlan, closeStagehand } from "./executor";
|
|
7
|
+
import {
|
|
8
|
+
evaluateExecutionResults,
|
|
9
|
+
extractFeedbackForNextIteration,
|
|
10
|
+
} from "./evaluator";
|
|
11
|
+
import { sendMail } from "../../email_ops/email_functions";
|
|
12
|
+
import type {
|
|
13
|
+
BrowserAgentResult,
|
|
14
|
+
IterationResult,
|
|
15
|
+
BrowserAgentConfig,
|
|
16
|
+
} from "./types";
|
|
17
|
+
import { printLLMError } from "../../utils/llm-error";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CONFIG: BrowserAgentConfig = {
|
|
20
|
+
maxIterations: 5,
|
|
21
|
+
timeout: 120000,
|
|
22
|
+
model: "google/gemini-3.1-flash-lite-preview",
|
|
23
|
+
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY || "",
|
|
24
|
+
evaluationThreshold: 80,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function runBrowserAgentMode(): Promise<void> {
|
|
28
|
+
console.log(chalk.bold("\n🌐 Browser Agent Mode\n"));
|
|
29
|
+
|
|
30
|
+
const queryInput = await text({
|
|
31
|
+
message: "What would you like the browser agent to do?",
|
|
32
|
+
placeholder: "e.g., Find top 5 AI jobs and get their descriptions...",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const query = typeof queryInput === "string" ? queryInput : "";
|
|
36
|
+
|
|
37
|
+
if (!query || query.trim() === "") {
|
|
38
|
+
console.log(chalk.yellow("\nOperation cancelled.\n"));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const liveOutput = await confirm({
|
|
43
|
+
message: "Show live iteration output?",
|
|
44
|
+
initialValue: false,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const maybeLog = (...args: any[]) => {
|
|
48
|
+
if (liveOutput) console.log(...args);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const config = { ...DEFAULT_CONFIG };
|
|
52
|
+
const iterations: IterationResult[] = [];
|
|
53
|
+
let iteration = 0;
|
|
54
|
+
let previousFeedback: string | undefined;
|
|
55
|
+
let finalData: unknown = null;
|
|
56
|
+
let lastEvaluation = null;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
while (iteration < config.maxIterations) {
|
|
60
|
+
iteration++;
|
|
61
|
+
maybeLog(chalk.bold(`\n📋 Iteration ${iteration}/${config.maxIterations}`));
|
|
62
|
+
|
|
63
|
+
// PLAN PHASE
|
|
64
|
+
let plan;
|
|
65
|
+
try {
|
|
66
|
+
plan = await withSpinner(
|
|
67
|
+
`[${iteration}/${config.maxIterations}] Planning...`,
|
|
68
|
+
() => generateBrowserPlan(query, previousFeedback)
|
|
69
|
+
);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
printLLMError(error, "Browser planner");
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
maybeLog(chalk.dim(`Goal: ${plan.goal}`));
|
|
76
|
+
maybeLog(chalk.dim(`Reasoning: ${plan.reasoning}`));
|
|
77
|
+
|
|
78
|
+
// EXECUTE PHASE — Stagehand agent() runs the task autonomously
|
|
79
|
+
let execution;
|
|
80
|
+
try {
|
|
81
|
+
execution = await withSpinner(
|
|
82
|
+
`[${iteration}/${config.maxIterations}] Browser agent running...`,
|
|
83
|
+
() => executeBrowserPlan(plan, previousFeedback)
|
|
84
|
+
);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.log(chalk.red(`✖ Browser execution failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Collect extracted data from agent
|
|
91
|
+
for (const result of execution) {
|
|
92
|
+
if (result.data != null) {
|
|
93
|
+
finalData = result.data;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const agentResult = execution[0];
|
|
98
|
+
maybeLog(
|
|
99
|
+
agentResult?.success
|
|
100
|
+
? chalk.green(`✓ Agent completed task`)
|
|
101
|
+
: chalk.red(`✗ Agent did not complete task`)
|
|
102
|
+
);
|
|
103
|
+
if (agentResult?.agentOutput) {
|
|
104
|
+
maybeLog(chalk.dim(`Output: ${agentResult.agentOutput}`));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// EVALUATE PHASE
|
|
108
|
+
let evaluation;
|
|
109
|
+
try {
|
|
110
|
+
evaluation = await withSpinner(
|
|
111
|
+
`[${iteration}/${config.maxIterations}] Evaluating...`,
|
|
112
|
+
() => evaluateExecutionResults(query, plan, execution, config.evaluationThreshold)
|
|
113
|
+
);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
printLLMError(error, "Evaluator");
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
maybeLog(chalk.cyan(`Score: ${evaluation.score}/100 | Completeness: ${evaluation.completeness}% | Accuracy: ${evaluation.accuracy}%`));
|
|
120
|
+
maybeLog(chalk.dim(`Feedback: ${evaluation.feedback}`));
|
|
121
|
+
|
|
122
|
+
lastEvaluation = evaluation;
|
|
123
|
+
|
|
124
|
+
iterations.push({
|
|
125
|
+
iteration,
|
|
126
|
+
plan,
|
|
127
|
+
execution,
|
|
128
|
+
evaluation,
|
|
129
|
+
shouldContinue: !evaluation.satisfied && iteration < config.maxIterations,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (evaluation.satisfied) {
|
|
133
|
+
maybeLog(chalk.green.bold(`\n✓ Task satisfied!\n`));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (iteration >= config.maxIterations) {
|
|
138
|
+
maybeLog(chalk.yellow(`\n⚠ Max iterations reached.\n`));
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
previousFeedback = extractFeedbackForNextIteration(evaluation);
|
|
143
|
+
maybeLog(chalk.yellow(`Retrying with feedback...`));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// BUILD CONSOLIDATED RESULT
|
|
147
|
+
const result: BrowserAgentResult = {
|
|
148
|
+
success: lastEvaluation?.satisfied || false,
|
|
149
|
+
query,
|
|
150
|
+
finalData,
|
|
151
|
+
iterations,
|
|
152
|
+
totalIterations: iteration,
|
|
153
|
+
completedAt: new Date().toISOString(),
|
|
154
|
+
error: lastEvaluation?.satisfied ? undefined : "Max iterations reached without full satisfaction",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// CONSOLIDATED OUTPUT
|
|
158
|
+
console.log(chalk.bold("\n═══════════════════════════════════════════"));
|
|
159
|
+
console.log(chalk.bold("📊 Browser Agent Result"));
|
|
160
|
+
console.log(chalk.bold("═══════════════════════════════════════════\n"));
|
|
161
|
+
|
|
162
|
+
console.log(`Query: ${chalk.cyan(query)}`);
|
|
163
|
+
console.log(`Status: ${result.success ? chalk.green("✓ Succeeded") : chalk.yellow("⚠ Partial")}`);
|
|
164
|
+
console.log(`Iterations: ${iteration}/${config.maxIterations}`);
|
|
165
|
+
|
|
166
|
+
if (lastEvaluation) {
|
|
167
|
+
console.log(`Score: ${chalk.cyan(`${lastEvaluation.score}/100`)}`);
|
|
168
|
+
console.log(`Complete: ${chalk.cyan(`${lastEvaluation.completeness}%`)}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Show final agent output if available
|
|
172
|
+
const lastExecution = iterations[iterations.length - 1]?.execution[0];
|
|
173
|
+
if (lastExecution?.agentOutput) {
|
|
174
|
+
console.log(chalk.bold("\n🤖 Agent Output:"));
|
|
175
|
+
console.log(chalk.white(lastExecution.agentOutput));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (finalData) {
|
|
179
|
+
console.log(chalk.bold("\n📦 Extracted Data:"));
|
|
180
|
+
console.log(chalk.dim(JSON.stringify(finalData, null, 2)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (lastEvaluation?.feedback) {
|
|
184
|
+
console.log(chalk.bold("\n💬 Final Feedback:"));
|
|
185
|
+
console.log(chalk.dim(lastEvaluation.feedback));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(chalk.bold("\n═══════════════════════════════════════════\n"));
|
|
189
|
+
|
|
190
|
+
// Render markdown report
|
|
191
|
+
const reportMarkdown = buildBrowserAgentMarkdownReport(result);
|
|
192
|
+
console.log(chalk.bold("📄 Report\n"));
|
|
193
|
+
console.log(renderHTMLMarkdown(reportMarkdown));
|
|
194
|
+
console.log(chalk.bold("\n═══════════════════════════════════════════\n"));
|
|
195
|
+
|
|
196
|
+
// Save options
|
|
197
|
+
const shouldSaveJson = await confirm({
|
|
198
|
+
message: "Save results to JSON file?",
|
|
199
|
+
initialValue: false,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (shouldSaveJson) {
|
|
203
|
+
const fs = await import("fs");
|
|
204
|
+
const path = await import("path");
|
|
205
|
+
const filename = `browser-agent-result-${Date.now()}.json`;
|
|
206
|
+
const filepath = path.resolve(process.cwd(), filename);
|
|
207
|
+
fs.writeFileSync(filepath, JSON.stringify(result, null, 2), "utf8");
|
|
208
|
+
console.log(chalk.green(`✓ Saved to ${filename}\n`));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const shouldSaveMd = await confirm({
|
|
212
|
+
message: "Save Markdown report to file?",
|
|
213
|
+
initialValue: false,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (shouldSaveMd) {
|
|
217
|
+
const fs = await import("fs");
|
|
218
|
+
const path = await import("path");
|
|
219
|
+
const filename = `browser-agent-report-${Date.now()}.md`;
|
|
220
|
+
const filepath = path.resolve(process.cwd(), filename);
|
|
221
|
+
fs.writeFileSync(filepath, reportMarkdown, "utf8");
|
|
222
|
+
console.log(chalk.green(`✓ Saved to ${filename}\n`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Email option ──────────────────────────────────────────────────
|
|
226
|
+
const shouldEmail = await confirm({
|
|
227
|
+
message: "Send a summary to your email?",
|
|
228
|
+
initialValue: false,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (shouldEmail) {
|
|
232
|
+
const emailTo = await text({ message: "Send to (email address)?" });
|
|
233
|
+
if (typeof emailTo === "string" && emailTo.trim()) {
|
|
234
|
+
// Build a concise email body from the agent output + score
|
|
235
|
+
const agentOut = lastExecution?.agentOutput ?? "";
|
|
236
|
+
const scoreInfo = lastEvaluation
|
|
237
|
+
? `Score: ${lastEvaluation.score}/100 | Completeness: ${lastEvaluation.completeness}%`
|
|
238
|
+
: "";
|
|
239
|
+
const emailBody = [
|
|
240
|
+
`Query: ${query}`,
|
|
241
|
+
scoreInfo,
|
|
242
|
+
"",
|
|
243
|
+
agentOut || JSON.stringify(finalData, null, 2) || "(no output)",
|
|
244
|
+
]
|
|
245
|
+
.filter(Boolean)
|
|
246
|
+
.join("\n");
|
|
247
|
+
|
|
248
|
+
await withSpinner("Sending email…", () =>
|
|
249
|
+
sendMail({
|
|
250
|
+
to: emailTo.trim(),
|
|
251
|
+
subject: `Browser Agent: ${query.slice(0, 60)}`,
|
|
252
|
+
body: emailBody,
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
console.log(chalk.green("✓ Email sent\n"));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
printLLMError(error, "Browser Agent");
|
|
260
|
+
} finally {
|
|
261
|
+
try {
|
|
262
|
+
await closeStagehand();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error("Error closing browser:", err);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildBrowserAgentMarkdownReport(result: BrowserAgentResult): string {
|
|
270
|
+
const lines: string[] = [];
|
|
271
|
+
|
|
272
|
+
lines.push("# Browser Agent Report\n");
|
|
273
|
+
lines.push(`**Query:** ${result.query}`);
|
|
274
|
+
lines.push(`**Status:** ${result.success ? "✅ Succeeded" : "⚠️ Partial"}`);
|
|
275
|
+
lines.push(`**Total Iterations:** ${result.totalIterations}`);
|
|
276
|
+
lines.push(`**Completed At:** ${result.completedAt}`);
|
|
277
|
+
if (result.error) {
|
|
278
|
+
lines.push(`**Note:** ${result.error}`);
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
|
|
282
|
+
// Only show the last (best) iteration's detail in consolidated mode
|
|
283
|
+
const lastIteration = result.iterations[result.iterations.length - 1];
|
|
284
|
+
if (lastIteration) {
|
|
285
|
+
lines.push("## Final Iteration Result\n");
|
|
286
|
+
|
|
287
|
+
const agentResult = lastIteration.execution[0];
|
|
288
|
+
if (agentResult?.agentOutput) {
|
|
289
|
+
lines.push("### Agent Output");
|
|
290
|
+
lines.push(agentResult.agentOutput);
|
|
291
|
+
lines.push("");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
lines.push("### Evaluation");
|
|
295
|
+
lines.push(`- **Score:** ${lastIteration.evaluation.score}/100`);
|
|
296
|
+
lines.push(`- **Completeness:** ${lastIteration.evaluation.completeness}%`);
|
|
297
|
+
lines.push(`- **Accuracy:** ${lastIteration.evaluation.accuracy}%`);
|
|
298
|
+
lines.push(`- **Feedback:** ${lastIteration.evaluation.feedback}`);
|
|
299
|
+
if (lastIteration.evaluation.issues.length > 0) {
|
|
300
|
+
lines.push("- **Issues:**");
|
|
301
|
+
lastIteration.evaluation.issues.forEach((issue) => {
|
|
302
|
+
lines.push(` - ${issue}`);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
lines.push("");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
lines.push("## Extracted Data\n");
|
|
309
|
+
lines.push("```json");
|
|
310
|
+
lines.push(JSON.stringify(result.finalData ?? {}, null, 2));
|
|
311
|
+
lines.push("```\n");
|
|
312
|
+
|
|
313
|
+
if (result.iterations.length > 1) {
|
|
314
|
+
lines.push("## Iteration Summary\n");
|
|
315
|
+
result.iterations.forEach((it) => {
|
|
316
|
+
lines.push(`- Iteration ${it.iteration}: Score ${it.evaluation.score}/100 — ${it.evaluation.satisfied ? "✅ Satisfied" : "🔄 Continued"}`);
|
|
317
|
+
});
|
|
318
|
+
lines.push("");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
lines.push("*Generated by Browser Agent*");
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|