@akshayram1/omnibrowser-agent 0.2.2 → 0.2.3
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/.github/workflows/ci.yml +41 -0
- package/dist/content.js +51 -13
- package/dist/content.js.map +3 -3
- package/dist/lib.js +109 -20
- package/dist/lib.js.map +3 -3
- package/dist/manifest.json +4 -4
- package/dist/types/core/executor.d.ts +2 -0
- package/dist/types/core/observer.d.ts +2 -0
- package/dist/types/core/planner.d.ts +2 -0
- package/dist/types/lib/index.d.ts +1 -0
- package/dist/types/shared/contracts.d.ts +6 -0
- package/dist/types/shared/parse-action.d.ts +13 -0
- package/docs/DEPLOYMENT.md +67 -0
- package/index.html +337 -84
- package/package.json +4 -3
- package/styles.css +287 -97
- package/vercel.json +2 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
ci:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '22'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm install
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: npm test
|
|
28
|
+
|
|
29
|
+
- name: Bump patch version
|
|
30
|
+
id: bump
|
|
31
|
+
run: |
|
|
32
|
+
NEW_VERSION=$(npm version patch --no-git-tag-version)
|
|
33
|
+
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
|
34
|
+
|
|
35
|
+
- name: Commit version bump
|
|
36
|
+
run: |
|
|
37
|
+
git config user.name "github-actions[bot]"
|
|
38
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
39
|
+
git add package.json
|
|
40
|
+
git commit -m "chore: bump version to ${{ steps.bump.outputs.version }} [skip ci]"
|
|
41
|
+
git push
|
package/dist/content.js
CHANGED
|
@@ -33,7 +33,7 @@ function assessRisk(action) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// src/
|
|
36
|
+
// src/core/executor.ts
|
|
37
37
|
function mustFind(selector) {
|
|
38
38
|
const node = document.querySelector(selector);
|
|
39
39
|
if (!(node instanceof HTMLElement)) {
|
|
@@ -48,7 +48,11 @@ function dispatchInputEvents(el) {
|
|
|
48
48
|
async function executeAction(action) {
|
|
49
49
|
switch (action.type) {
|
|
50
50
|
case "click": {
|
|
51
|
-
mustFind(action.selector)
|
|
51
|
+
const el = mustFind(action.selector);
|
|
52
|
+
if (el.disabled) {
|
|
53
|
+
throw new Error(`Element is disabled: ${action.selector}`);
|
|
54
|
+
}
|
|
55
|
+
el.click();
|
|
52
56
|
return `Clicked ${action.selector}`;
|
|
53
57
|
}
|
|
54
58
|
case "type": {
|
|
@@ -60,6 +64,9 @@ async function executeAction(action) {
|
|
|
60
64
|
}
|
|
61
65
|
input.value = `${input.value}${action.text}`;
|
|
62
66
|
dispatchInputEvents(input);
|
|
67
|
+
if (input.value.indexOf(action.text) === -1) {
|
|
68
|
+
throw new Error(`Type verification failed: value did not update for ${action.selector}`);
|
|
69
|
+
}
|
|
63
70
|
return `Typed into ${action.selector}`;
|
|
64
71
|
}
|
|
65
72
|
case "navigate": {
|
|
@@ -68,6 +75,9 @@ async function executeAction(action) {
|
|
|
68
75
|
}
|
|
69
76
|
case "extract": {
|
|
70
77
|
const value = mustFind(action.selector).innerText.trim();
|
|
78
|
+
if (!value) {
|
|
79
|
+
throw new Error(`Extract returned empty text from ${action.selector}`);
|
|
80
|
+
}
|
|
71
81
|
return `${action.label}: ${value}`;
|
|
72
82
|
}
|
|
73
83
|
case "scroll": {
|
|
@@ -91,7 +101,7 @@ async function executeAction(action) {
|
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
// src/
|
|
104
|
+
// src/core/observer.ts
|
|
95
105
|
var CANDIDATE_SELECTOR = "a,button,input,textarea,select,[role='button'],[role='link'],[contenteditable='true']";
|
|
96
106
|
var MAX_CANDIDATES = 60;
|
|
97
107
|
function cssPath(element) {
|
|
@@ -122,23 +132,50 @@ function cssPath(element) {
|
|
|
122
132
|
return parts.join(" > ");
|
|
123
133
|
}
|
|
124
134
|
function isVisible(el) {
|
|
125
|
-
if (el.offsetParent === null && el.tagName !== "BODY")
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
135
|
+
if (el.offsetParent === null && el.tagName !== "BODY") return false;
|
|
128
136
|
const style = window.getComputedStyle(el);
|
|
129
|
-
|
|
137
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
138
|
+
const rect = el.getBoundingClientRect();
|
|
139
|
+
return rect.width > 0 || rect.height > 0;
|
|
140
|
+
}
|
|
141
|
+
function isInViewport(el) {
|
|
142
|
+
const rect = el.getBoundingClientRect();
|
|
143
|
+
return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
|
|
144
|
+
}
|
|
145
|
+
function getAssociatedLabel(el) {
|
|
146
|
+
if (el.id) {
|
|
147
|
+
const label = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
148
|
+
if (label) return label.innerText.trim();
|
|
149
|
+
}
|
|
150
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
151
|
+
if (labelledBy) {
|
|
152
|
+
const labelEl = document.getElementById(labelledBy);
|
|
153
|
+
if (labelEl) return labelEl.innerText.trim();
|
|
154
|
+
}
|
|
155
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
156
|
+
if (ariaLabel) return ariaLabel.trim();
|
|
157
|
+
const parentLabel = el.closest("label");
|
|
158
|
+
if (parentLabel) {
|
|
159
|
+
return Array.from(parentLabel.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent?.trim() ?? "").filter(Boolean).join(" ");
|
|
160
|
+
}
|
|
161
|
+
return "";
|
|
130
162
|
}
|
|
131
163
|
function collectSnapshot() {
|
|
132
|
-
const
|
|
164
|
+
const allNodes = Array.from(
|
|
133
165
|
document.querySelectorAll(CANDIDATE_SELECTOR)
|
|
134
|
-
).filter(isVisible)
|
|
166
|
+
).filter(isVisible);
|
|
167
|
+
const inView = allNodes.filter(isInViewport);
|
|
168
|
+
const offScreen = allNodes.filter((el) => !isInViewport(el));
|
|
169
|
+
const nodes = [...inView, ...offScreen].slice(0, MAX_CANDIDATES);
|
|
135
170
|
const candidates = nodes.map((node) => {
|
|
136
171
|
const placeholder = node.placeholder?.trim() || node.getAttribute("placeholder")?.trim();
|
|
172
|
+
const associatedLabel = getAssociatedLabel(node);
|
|
137
173
|
return {
|
|
138
174
|
selector: cssPath(node),
|
|
139
175
|
role: node.getAttribute("role") ?? node.tagName.toLowerCase(),
|
|
140
|
-
text: (node.innerText || node.getAttribute("
|
|
141
|
-
placeholder: placeholder || void 0
|
|
176
|
+
text: (node.innerText || node.getAttribute("name") || "").trim().slice(0, 120),
|
|
177
|
+
placeholder: placeholder || void 0,
|
|
178
|
+
label: associatedLabel || void 0
|
|
142
179
|
};
|
|
143
180
|
});
|
|
144
181
|
const textPreview = document.body.innerText.replace(/\s+/g, " ").trim().slice(0, 1500);
|
|
@@ -150,7 +187,7 @@ function collectSnapshot() {
|
|
|
150
187
|
};
|
|
151
188
|
}
|
|
152
189
|
|
|
153
|
-
// src/
|
|
190
|
+
// src/core/planner.ts
|
|
154
191
|
var URL_PATTERN = /(?:go to|navigate to|open)\s+(https?:\/\/\S+)/i;
|
|
155
192
|
var SEARCH_PATTERN = /search(?:\s+for)?\s+(.+)/i;
|
|
156
193
|
var FILL_PATTERN = /(?:fill|type|enter)\s+"?([^"]+)"?\s+(?:in(?:to)?|for|on)\s+(.+)/i;
|
|
@@ -241,7 +278,8 @@ async function runTick(session) {
|
|
|
241
278
|
const action = await planNextAction(session.planner, {
|
|
242
279
|
goal: session.goal,
|
|
243
280
|
snapshot,
|
|
244
|
-
history: session.history
|
|
281
|
+
history: session.history,
|
|
282
|
+
lastError: session.lastError
|
|
245
283
|
});
|
|
246
284
|
const risk = assessRisk(action);
|
|
247
285
|
if (risk === "blocked") {
|
package/dist/content.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/shared/safety.ts", "../src/
|
|
4
|
-
"sourcesContent": ["import type { AgentAction, RiskLevel } from \"./contracts\";\n\nconst RISKY_KEYWORDS = /\\b(delete|remove|pay|purchase|submit|confirm|checkout|transfer|withdraw|send)\\b/i;\n\nfunction elementTextRisky(text?: string): boolean {\n return text != null && RISKY_KEYWORDS.test(text);\n}\n\nexport function assessRisk(action: AgentAction): RiskLevel {\n switch (action.type) {\n case \"navigate\": {\n try {\n const next = new URL(action.url);\n if (![\"http:\", \"https:\"].includes(next.protocol)) {\n return \"blocked\";\n }\n } catch {\n return \"blocked\";\n }\n return \"safe\";\n }\n case \"click\":\n return elementTextRisky(action.label) ? \"review\" : \"safe\";\n case \"type\":\n return elementTextRisky(action.label) ? \"review\" : \"safe\";\n case \"focus\":\n case \"scroll\":\n case \"wait\":\n return \"safe\";\n case \"extract\":\n return \"review\";\n case \"done\":\n return \"safe\";\n default:\n return \"review\";\n }\n}\n", "import type { AgentAction } from \"../shared/contracts\";\n\nfunction mustFind(selector: string): HTMLElement {\n const node = document.querySelector(selector);\n if (!(node instanceof HTMLElement)) {\n throw new Error(`Selector not found: ${selector}`);\n }\n return node;\n}\n\nfunction dispatchInputEvents(el: HTMLInputElement | HTMLTextAreaElement): void {\n el.dispatchEvent(new InputEvent(\"input\", { bubbles: true, cancelable: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n}\n\nexport async function executeAction(action: AgentAction): Promise<string> {\n switch (action.type) {\n case \"click\": {\n mustFind(action.selector).click();\n return `Clicked ${action.selector}`;\n }\n case \"type\": {\n const input = mustFind(action.selector) as HTMLInputElement | HTMLTextAreaElement;\n input.focus();\n if (action.clearFirst) {\n input.value = \"\";\n dispatchInputEvents(input);\n }\n input.value = `${input.value}${action.text}`;\n dispatchInputEvents(input);\n return `Typed into ${action.selector}`;\n }\n case \"navigate\": {\n window.location.href = action.url;\n return `Navigated to ${action.url}`;\n }\n case \"extract\": {\n const value = mustFind(action.selector).innerText.trim();\n return `${action.label}: ${value}`;\n }\n case \"scroll\": {\n const target = action.selector ? mustFind(action.selector) : document.documentElement;\n target.scrollBy({ top: action.deltaY, behavior: \"smooth\" });\n return `Scrolled ${action.deltaY > 0 ? \"down\" : \"up\"} ${Math.abs(action.deltaY)}px`;\n }\n case \"focus\": {\n mustFind(action.selector).focus();\n return `Focused ${action.selector}`;\n }\n case \"wait\": {\n await new Promise((resolve) => setTimeout(resolve, action.ms));\n return `Waited ${action.ms}ms`;\n }\n case \"done\": {\n return action.reason;\n }\n default:\n return \"No-op\";\n }\n}\n", "import type { CandidateElement, PageSnapshot } from \"../shared/contracts\";\n\nconst CANDIDATE_SELECTOR =\n \"a,button,input,textarea,select,[role='button'],[role='link'],[contenteditable='true']\";\n\nconst MAX_CANDIDATES = 60;\n\nfunction cssPath(element: Element): string {\n if (!(element instanceof HTMLElement)) {\n return element.tagName.toLowerCase();\n }\n\n if (element.id) {\n return `#${CSS.escape(element.id)}`;\n }\n\n const parts: string[] = [];\n let current: HTMLElement | null = element;\n while (current && parts.length < 4) {\n let part = current.tagName.toLowerCase();\n if (current.classList.length > 0) {\n part += `.${Array.from(current.classList).slice(0, 2).map(CSS.escape).join(\".\")}`;\n }\n const parent: HTMLElement | null = current.parentElement;\n if (parent) {\n const siblings = Array.from(parent.children).filter((s: Element) => s.tagName === current!.tagName);\n if (siblings.length > 1) {\n const index = siblings.indexOf(current) + 1;\n part += `:nth-of-type(${index})`;\n }\n }\n parts.unshift(part);\n current = parent;\n }\n return parts.join(\" > \");\n}\n\nfunction isVisible(el: HTMLElement): boolean {\n if (el.offsetParent === null && el.tagName !== \"BODY\") {\n return false;\n }\n const style = window.getComputedStyle(el);\n return style.display !== \"none\" && style.visibility !== \"hidden\" && style.opacity !== \"0\";\n}\n\nexport function collectSnapshot(): PageSnapshot {\n const nodes = Array.from(\n document.querySelectorAll<HTMLElement>(CANDIDATE_SELECTOR)\n )\n .filter(isVisible)\n .slice(0, MAX_CANDIDATES);\n\n const candidates: CandidateElement[] = nodes.map((node) => {\n const placeholder =\n (node as HTMLInputElement).placeholder?.trim() || node.getAttribute(\"placeholder\")?.trim();\n return {\n selector: cssPath(node),\n role: node.getAttribute(\"role\") ?? node.tagName.toLowerCase(),\n text: (node.innerText || node.getAttribute(\"aria-label\") || node.getAttribute(\"name\") || \"\").trim().slice(0, 120),\n placeholder: placeholder || undefined\n };\n });\n\n const textPreview = document.body.innerText.replace(/\\s+/g, \" \").trim().slice(0, 1500);\n\n return {\n url: window.location.href,\n title: document.title,\n textPreview,\n candidates\n };\n}\n", "import type { AgentAction, CandidateElement, PlannerConfig, PlannerInput } from \"../shared/contracts\";\n\ntype WebLLMBridge = {\n plan(input: PlannerInput, modelId?: string): Promise<AgentAction>;\n};\n\ntype PageAgentBridge = {\n plan(input: PlannerInput): Promise<AgentAction>;\n};\n\nconst URL_PATTERN = /(?:go to|navigate to|open)\\s+(https?:\\/\\/\\S+)/i;\nconst SEARCH_PATTERN = /search(?:\\s+for)?\\s+(.+)/i;\nconst FILL_PATTERN = /(?:fill|type|enter)\\s+\"?([^\"]+)\"?\\s+(?:in(?:to)?|for|on)\\s+(.+)/i;\nconst CLICK_PATTERN = /click(?:\\s+(?:on|the))?\\s+(.+)/i;\n\nfunction findByText(candidates: CandidateElement[], text: string): CandidateElement | undefined {\n const lower = text.toLowerCase();\n return candidates.find(\n (c) =>\n c.text.toLowerCase().includes(lower) ||\n (c.placeholder?.toLowerCase().includes(lower) ?? false)\n );\n}\n\nfunction findInput(candidates: CandidateElement[]): CandidateElement | undefined {\n return candidates.find(\n (c) => c.role === \"input\" || c.role === \"textarea\" || c.selector.includes(\"input\") || c.selector.includes(\"textarea\")\n );\n}\n\nfunction findButton(candidates: CandidateElement[]): CandidateElement | undefined {\n return candidates.find(\n (c) => c.role === \"button\" || c.role === \"a\" || c.selector.includes(\"button\") || c.selector.includes(\"a\")\n );\n}\n\nfunction heuristicPlan(input: PlannerInput): AgentAction {\n const { goal, snapshot, history } = input;\n\n const navMatch = goal.match(URL_PATTERN);\n if (navMatch) {\n return { type: \"navigate\", url: navMatch[1] };\n }\n\n const fillMatch = goal.match(FILL_PATTERN);\n if (fillMatch) {\n const [, text, fieldHint] = fillMatch;\n const target = findByText(snapshot.candidates, fieldHint) ?? findInput(snapshot.candidates);\n if (target) {\n return { type: \"type\", selector: target.selector, text, clearFirst: true, label: target.text || target.placeholder };\n }\n }\n\n const searchMatch = goal.match(SEARCH_PATTERN);\n if (searchMatch) {\n const input = findInput(snapshot.candidates);\n if (input) {\n return { type: \"type\", selector: input.selector, text: searchMatch[1].trim(), clearFirst: true, label: input.text || input.placeholder };\n }\n }\n\n const clickMatch = goal.match(CLICK_PATTERN);\n if (clickMatch) {\n const target = findByText(snapshot.candidates, clickMatch[1].trim());\n if (target) {\n return { type: \"click\", selector: target.selector, label: target.text };\n }\n }\n\n const firstInput = findInput(snapshot.candidates);\n const firstButton = findButton(snapshot.candidates);\n\n if (firstInput && !history.some((h) => h.startsWith(\"Typed\"))) {\n const searchTerm = goal.replace(/.*(?:search|find|look up)\\s+/i, \"\").trim();\n return { type: \"type\", selector: firstInput.selector, text: searchTerm, clearFirst: true, label: firstInput.text || firstInput.placeholder };\n }\n\n if (firstButton && !history.some((h) => h.startsWith(\"Clicked\"))) {\n return { type: \"click\", selector: firstButton.selector, label: firstButton.text };\n }\n\n return { type: \"done\", reason: \"No further heuristic actions available\" };\n}\n\nexport async function planNextAction(config: PlannerConfig, input: PlannerInput): Promise<AgentAction> {\n if (config.kind === \"heuristic\") {\n return heuristicPlan(input);\n }\n\n if (config.kind === \"page-agent\") {\n const pageAgentBridge = (window as Window & { __browserAgentPageAgent?: PageAgentBridge }).__browserAgentPageAgent;\n if (!pageAgentBridge) {\n return {\n type: \"done\",\n reason: \"page-agent bridge is not configured. Assign a PageAgentBridge to window.__browserAgentPageAgent.\"\n };\n }\n return pageAgentBridge.plan(input);\n }\n\n const bridge = (window as Window & { __browserAgentWebLLM?: WebLLMBridge }).__browserAgentWebLLM;\n if (!bridge) {\n return {\n type: \"done\",\n reason: \"WebLLM bridge is not configured. Use heuristic mode or wire a local bridge implementation.\"\n };\n }\n\n return bridge.plan(input, config.modelId);\n}\n", "import type { AgentSession, ContentCommand, ContentResult } from \"../shared/contracts\";\nimport { assessRisk } from \"../shared/safety\";\nimport { executeAction } from \"./executor\";\nimport { collectSnapshot } from \"./pageObserver\";\nimport { planNextAction } from \"./planner\";\n\nlet stopped = false;\n\nasync function runTick(session: AgentSession): Promise<ContentResult> {\n const snapshot = collectSnapshot();\n const action = await planNextAction(session.planner, {\n goal: session.goal,\n snapshot,\n history: session.history\n });\n\n const risk = assessRisk(action);\n if (risk === \"blocked\") {\n return { status: \"blocked\", action, message: `Blocked action: ${JSON.stringify(action)}` };\n }\n\n if (session.mode === \"human-approved\" && risk === \"review\") {\n return { status: \"needs_approval\", action, message: `Approval needed for ${action.type}` };\n }\n\n if (action.type === \"done\") {\n return { status: \"done\", action, message: action.reason };\n }\n\n const message = await executeAction(action);\n return { status: \"executed\", action, message };\n}\n\nasync function executePendingAction(session: AgentSession): Promise<ContentResult> {\n if (!session.pendingAction) {\n return { status: \"error\", message: \"No pending action to approve\" };\n }\n\n const message = await executeAction(session.pendingAction);\n return { status: \"executed\", action: session.pendingAction, message };\n}\n\nchrome.runtime.onMessage.addListener((command: ContentCommand, _sender, sendResponse) => {\n if (command.type === \"AGENT_STOP\") {\n stopped = true;\n sendResponse({ status: \"done\", message: \"Stopped by user\" } satisfies ContentResult);\n return true;\n }\n\n if (command.type !== \"AGENT_TICK\") {\n return false;\n }\n\n const session = command.session;\n const exec = session.pendingAction ? executePendingAction(session) : runTick(session);\n\n exec\n .then((result) => {\n if (stopped) {\n sendResponse({ status: \"done\", message: \"Stopped\" } satisfies ContentResult);\n return;\n }\n sendResponse(result);\n })\n .catch((error) => {\n sendResponse({ status: \"error\", message: String(error) } satisfies ContentResult);\n });\n\n return true;\n});\n"],
|
|
5
|
-
"mappings": ";AAEA,IAAM,iBAAiB;AAEvB,SAAS,iBAAiB,MAAwB;AAChD,SAAO,QAAQ,QAAQ,eAAe,KAAK,IAAI;AACjD;AAEO,SAAS,WAAW,QAAgC;AACzD,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK,YAAY;AACf,UAAI;AACF,cAAM,OAAO,IAAI,IAAI,OAAO,GAAG;AAC/B,YAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,KAAK,QAAQ,GAAG;AAChD,iBAAO;AAAA,QACT;AAAA,MACF,QAAQ;AACN,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,IACA,KAAK;AACH,aAAO,iBAAiB,OAAO,KAAK,IAAI,WAAW;AAAA,IACrD,KAAK;AACH,aAAO,iBAAiB,OAAO,KAAK,IAAI,WAAW;AAAA,IACrD,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;;;AClCA,SAAS,SAAS,UAA+B;AAC/C,QAAM,OAAO,SAAS,cAAc,QAAQ;AAC5C,MAAI,EAAE,gBAAgB,cAAc;AAClC,UAAM,IAAI,MAAM,uBAAuB,QAAQ,EAAE;AAAA,EACnD;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,IAAkD;AAC7E,KAAG,cAAc,IAAI,WAAW,SAAS,EAAE,SAAS,MAAM,YAAY,KAAK,CAAC,CAAC;AAC7E,KAAG,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;AACzD;AAEA,eAAsB,cAAc,QAAsC;AACxE,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK,SAAS;AACZ,
|
|
3
|
+
"sources": ["../src/shared/safety.ts", "../src/core/executor.ts", "../src/core/observer.ts", "../src/core/planner.ts", "../src/content/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AgentAction, RiskLevel } from \"./contracts\";\n\nconst RISKY_KEYWORDS = /\\b(delete|remove|pay|purchase|submit|confirm|checkout|transfer|withdraw|send)\\b/i;\n\nfunction elementTextRisky(text?: string): boolean {\n return text != null && RISKY_KEYWORDS.test(text);\n}\n\nexport function assessRisk(action: AgentAction): RiskLevel {\n switch (action.type) {\n case \"navigate\": {\n try {\n const next = new URL(action.url);\n if (![\"http:\", \"https:\"].includes(next.protocol)) {\n return \"blocked\";\n }\n } catch {\n return \"blocked\";\n }\n return \"safe\";\n }\n case \"click\":\n return elementTextRisky(action.label) ? \"review\" : \"safe\";\n case \"type\":\n return elementTextRisky(action.label) ? \"review\" : \"safe\";\n case \"focus\":\n case \"scroll\":\n case \"wait\":\n return \"safe\";\n case \"extract\":\n return \"review\";\n case \"done\":\n return \"safe\";\n default:\n return \"review\";\n }\n}\n", "import type { AgentAction } from \"../shared/contracts\";\n\nfunction mustFind(selector: string): HTMLElement {\n const node = document.querySelector(selector);\n if (!(node instanceof HTMLElement)) {\n throw new Error(`Selector not found: ${selector}`);\n }\n return node;\n}\n\nfunction dispatchInputEvents(el: HTMLInputElement | HTMLTextAreaElement): void {\n el.dispatchEvent(new InputEvent(\"input\", { bubbles: true, cancelable: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n}\n\nexport async function executeAction(action: AgentAction): Promise<string> {\n switch (action.type) {\n case \"click\": {\n const el = mustFind(action.selector);\n if ((el as HTMLButtonElement).disabled) {\n throw new Error(`Element is disabled: ${action.selector}`);\n }\n el.click();\n return `Clicked ${action.selector}`;\n }\n case \"type\": {\n const input = mustFind(action.selector) as HTMLInputElement | HTMLTextAreaElement;\n input.focus();\n if (action.clearFirst) {\n input.value = \"\";\n dispatchInputEvents(input);\n }\n input.value = `${input.value}${action.text}`;\n dispatchInputEvents(input);\n if (input.value.indexOf(action.text) === -1) {\n throw new Error(`Type verification failed: value did not update for ${action.selector}`);\n }\n return `Typed into ${action.selector}`;\n }\n case \"navigate\": {\n window.location.href = action.url;\n return `Navigated to ${action.url}`;\n }\n case \"extract\": {\n const value = mustFind(action.selector).innerText.trim();\n if (!value) {\n throw new Error(`Extract returned empty text from ${action.selector}`);\n }\n return `${action.label}: ${value}`;\n }\n case \"scroll\": {\n const target = action.selector ? mustFind(action.selector) : document.documentElement;\n target.scrollBy({ top: action.deltaY, behavior: \"smooth\" });\n return `Scrolled ${action.deltaY > 0 ? \"down\" : \"up\"} ${Math.abs(action.deltaY)}px`;\n }\n case \"focus\": {\n mustFind(action.selector).focus();\n return `Focused ${action.selector}`;\n }\n case \"wait\": {\n await new Promise((resolve) => setTimeout(resolve, action.ms));\n return `Waited ${action.ms}ms`;\n }\n case \"done\": {\n return action.reason;\n }\n default:\n return \"No-op\";\n }\n}\n", "import type { CandidateElement, PageSnapshot } from \"../shared/contracts\";\n\nconst CANDIDATE_SELECTOR =\n \"a,button,input,textarea,select,[role='button'],[role='link'],[contenteditable='true']\";\n\nconst MAX_CANDIDATES = 60;\n\nfunction cssPath(element: Element): string {\n if (!(element instanceof HTMLElement)) {\n return element.tagName.toLowerCase();\n }\n\n if (element.id) {\n return `#${CSS.escape(element.id)}`;\n }\n\n const parts: string[] = [];\n let current: HTMLElement | null = element;\n while (current && parts.length < 4) {\n let part = current.tagName.toLowerCase();\n if (current.classList.length > 0) {\n part += `.${Array.from(current.classList).slice(0, 2).map(CSS.escape).join(\".\")}`;\n }\n const parent: HTMLElement | null = current.parentElement;\n if (parent) {\n const siblings = Array.from(parent.children).filter((s: Element) => s.tagName === current!.tagName);\n if (siblings.length > 1) {\n const index = siblings.indexOf(current) + 1;\n part += `:nth-of-type(${index})`;\n }\n }\n parts.unshift(part);\n current = parent;\n }\n return parts.join(\" > \");\n}\n\nfunction isVisible(el: HTMLElement): boolean {\n if (el.offsetParent === null && el.tagName !== \"BODY\") return false;\n const style = window.getComputedStyle(el);\n if (style.display === \"none\" || style.visibility === \"hidden\" || style.opacity === \"0\") return false;\n // Zero-dimension elements are functionally hidden\n const rect = el.getBoundingClientRect();\n return rect.width > 0 || rect.height > 0;\n}\n\nfunction isInViewport(el: HTMLElement): boolean {\n const rect = el.getBoundingClientRect();\n return (\n rect.bottom > 0 &&\n rect.top < window.innerHeight &&\n rect.right > 0 &&\n rect.left < window.innerWidth\n );\n}\n\n/** Resolve the visible label text via for/id, aria-labelledby, aria-label, or wrapping <label>. */\nfunction getAssociatedLabel(el: HTMLElement): string {\n if (el.id) {\n const label = document.querySelector<HTMLLabelElement>(`label[for=\"${CSS.escape(el.id)}\"]`);\n if (label) return label.innerText.trim();\n }\n\n const labelledBy = el.getAttribute(\"aria-labelledby\");\n if (labelledBy) {\n const labelEl = document.getElementById(labelledBy);\n if (labelEl) return labelEl.innerText.trim();\n }\n\n const ariaLabel = el.getAttribute(\"aria-label\");\n if (ariaLabel) return ariaLabel.trim();\n\n const parentLabel = el.closest(\"label\");\n if (parentLabel) {\n return Array.from(parentLabel.childNodes)\n .filter((n) => n.nodeType === Node.TEXT_NODE)\n .map((n) => n.textContent?.trim() ?? \"\")\n .filter(Boolean)\n .join(\" \");\n }\n\n return \"\";\n}\n\nexport function collectSnapshot(): PageSnapshot {\n const allNodes = Array.from(\n document.querySelectorAll<HTMLElement>(CANDIDATE_SELECTOR)\n ).filter(isVisible);\n\n // In-viewport elements first so the model sees the most relevant candidates first\n const inView = allNodes.filter(isInViewport);\n const offScreen = allNodes.filter((el) => !isInViewport(el));\n const nodes = [...inView, ...offScreen].slice(0, MAX_CANDIDATES);\n\n const candidates: CandidateElement[] = nodes.map((node) => {\n const placeholder =\n (node as HTMLInputElement).placeholder?.trim() || node.getAttribute(\"placeholder\")?.trim();\n const associatedLabel = getAssociatedLabel(node);\n return {\n selector: cssPath(node),\n role: node.getAttribute(\"role\") ?? node.tagName.toLowerCase(),\n text: (node.innerText || node.getAttribute(\"name\") || \"\").trim().slice(0, 120),\n placeholder: placeholder || undefined,\n label: associatedLabel || undefined,\n };\n });\n\n const textPreview = document.body.innerText.replace(/\\s+/g, \" \").trim().slice(0, 1500);\n\n return {\n url: window.location.href,\n title: document.title,\n textPreview,\n candidates,\n };\n}\n", "import type { AgentAction, CandidateElement, PlannerConfig, PlannerInput } from \"../shared/contracts\";\n\ntype WebLLMBridge = {\n plan(input: PlannerInput, modelId?: string): Promise<AgentAction>;\n};\n\ntype PageAgentBridge = {\n plan(input: PlannerInput): Promise<AgentAction>;\n};\n\nconst URL_PATTERN = /(?:go to|navigate to|open)\\s+(https?:\\/\\/\\S+)/i;\nconst SEARCH_PATTERN = /search(?:\\s+for)?\\s+(.+)/i;\nconst FILL_PATTERN = /(?:fill|type|enter)\\s+\"?([^\"]+)\"?\\s+(?:in(?:to)?|for|on)\\s+(.+)/i;\nconst CLICK_PATTERN = /click(?:\\s+(?:on|the))?\\s+(.+)/i;\n\nfunction findByText(candidates: CandidateElement[], text: string): CandidateElement | undefined {\n const lower = text.toLowerCase();\n return candidates.find(\n (c) =>\n c.text.toLowerCase().includes(lower) ||\n (c.placeholder?.toLowerCase().includes(lower) ?? false)\n );\n}\n\nfunction findInput(candidates: CandidateElement[]): CandidateElement | undefined {\n return candidates.find(\n (c) => c.role === \"input\" || c.role === \"textarea\" || c.selector.includes(\"input\") || c.selector.includes(\"textarea\")\n );\n}\n\nfunction findButton(candidates: CandidateElement[]): CandidateElement | undefined {\n return candidates.find(\n (c) => c.role === \"button\" || c.role === \"a\" || c.selector.includes(\"button\") || c.selector.includes(\"a\")\n );\n}\n\nfunction heuristicPlan(input: PlannerInput): AgentAction {\n const { goal, snapshot, history } = input;\n\n const navMatch = goal.match(URL_PATTERN);\n if (navMatch) {\n return { type: \"navigate\", url: navMatch[1] };\n }\n\n const fillMatch = goal.match(FILL_PATTERN);\n if (fillMatch) {\n const [, text, fieldHint] = fillMatch;\n const target = findByText(snapshot.candidates, fieldHint) ?? findInput(snapshot.candidates);\n if (target) {\n return { type: \"type\", selector: target.selector, text, clearFirst: true, label: target.text || target.placeholder };\n }\n }\n\n const searchMatch = goal.match(SEARCH_PATTERN);\n if (searchMatch) {\n const input = findInput(snapshot.candidates);\n if (input) {\n return { type: \"type\", selector: input.selector, text: searchMatch[1].trim(), clearFirst: true, label: input.text || input.placeholder };\n }\n }\n\n const clickMatch = goal.match(CLICK_PATTERN);\n if (clickMatch) {\n const target = findByText(snapshot.candidates, clickMatch[1].trim());\n if (target) {\n return { type: \"click\", selector: target.selector, label: target.text };\n }\n }\n\n const firstInput = findInput(snapshot.candidates);\n const firstButton = findButton(snapshot.candidates);\n\n if (firstInput && !history.some((h) => h.startsWith(\"Typed\"))) {\n const searchTerm = goal.replace(/.*(?:search|find|look up)\\s+/i, \"\").trim();\n return { type: \"type\", selector: firstInput.selector, text: searchTerm, clearFirst: true, label: firstInput.text || firstInput.placeholder };\n }\n\n if (firstButton && !history.some((h) => h.startsWith(\"Clicked\"))) {\n return { type: \"click\", selector: firstButton.selector, label: firstButton.text };\n }\n\n return { type: \"done\", reason: \"No further heuristic actions available\" };\n}\n\nexport async function planNextAction(config: PlannerConfig, input: PlannerInput): Promise<AgentAction> {\n if (config.kind === \"heuristic\") {\n return heuristicPlan(input);\n }\n\n if (config.kind === \"page-agent\") {\n const pageAgentBridge = (window as Window & { __browserAgentPageAgent?: PageAgentBridge }).__browserAgentPageAgent;\n if (!pageAgentBridge) {\n return {\n type: \"done\",\n reason: \"page-agent bridge is not configured. Assign a PageAgentBridge to window.__browserAgentPageAgent.\"\n };\n }\n return pageAgentBridge.plan(input);\n }\n\n const bridge = (window as Window & { __browserAgentWebLLM?: WebLLMBridge }).__browserAgentWebLLM;\n if (!bridge) {\n return {\n type: \"done\",\n reason: \"WebLLM bridge is not configured. Use heuristic mode or wire a local bridge implementation.\"\n };\n }\n\n return bridge.plan(input, config.modelId);\n}\n", "import type { AgentSession, ContentCommand, ContentResult } from \"../shared/contracts\";\nimport { assessRisk } from \"../shared/safety\";\nimport { executeAction } from \"../core/executor\";\nimport { collectSnapshot } from \"../core/observer\";\nimport { planNextAction } from \"../core/planner\";\n\nlet stopped = false;\n\nasync function runTick(session: AgentSession): Promise<ContentResult> {\n const snapshot = collectSnapshot();\n const action = await planNextAction(session.planner, {\n goal: session.goal,\n snapshot,\n history: session.history,\n lastError: session.lastError\n });\n\n const risk = assessRisk(action);\n if (risk === \"blocked\") {\n return { status: \"blocked\", action, message: `Blocked action: ${JSON.stringify(action)}` };\n }\n\n if (session.mode === \"human-approved\" && risk === \"review\") {\n return { status: \"needs_approval\", action, message: `Approval needed for ${action.type}` };\n }\n\n if (action.type === \"done\") {\n return { status: \"done\", action, message: action.reason };\n }\n\n const message = await executeAction(action);\n return { status: \"executed\", action, message };\n}\n\nasync function executePendingAction(session: AgentSession): Promise<ContentResult> {\n if (!session.pendingAction) {\n return { status: \"error\", message: \"No pending action to approve\" };\n }\n\n const message = await executeAction(session.pendingAction);\n return { status: \"executed\", action: session.pendingAction, message };\n}\n\nchrome.runtime.onMessage.addListener((command: ContentCommand, _sender, sendResponse) => {\n if (command.type === \"AGENT_STOP\") {\n stopped = true;\n sendResponse({ status: \"done\", message: \"Stopped by user\" } satisfies ContentResult);\n return true;\n }\n\n if (command.type !== \"AGENT_TICK\") {\n return false;\n }\n\n const session = command.session;\n const exec = session.pendingAction ? executePendingAction(session) : runTick(session);\n\n exec\n .then((result) => {\n if (stopped) {\n sendResponse({ status: \"done\", message: \"Stopped\" } satisfies ContentResult);\n return;\n }\n sendResponse(result);\n })\n .catch((error) => {\n sendResponse({ status: \"error\", message: String(error) } satisfies ContentResult);\n });\n\n return true;\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["input"]
|
|
7
7
|
}
|
package/dist/lib.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/core/executor.ts
|
|
2
2
|
function mustFind(selector) {
|
|
3
3
|
const node = document.querySelector(selector);
|
|
4
4
|
if (!(node instanceof HTMLElement)) {
|
|
@@ -13,7 +13,11 @@ function dispatchInputEvents(el) {
|
|
|
13
13
|
async function executeAction(action) {
|
|
14
14
|
switch (action.type) {
|
|
15
15
|
case "click": {
|
|
16
|
-
mustFind(action.selector)
|
|
16
|
+
const el = mustFind(action.selector);
|
|
17
|
+
if (el.disabled) {
|
|
18
|
+
throw new Error(`Element is disabled: ${action.selector}`);
|
|
19
|
+
}
|
|
20
|
+
el.click();
|
|
17
21
|
return `Clicked ${action.selector}`;
|
|
18
22
|
}
|
|
19
23
|
case "type": {
|
|
@@ -25,6 +29,9 @@ async function executeAction(action) {
|
|
|
25
29
|
}
|
|
26
30
|
input.value = `${input.value}${action.text}`;
|
|
27
31
|
dispatchInputEvents(input);
|
|
32
|
+
if (input.value.indexOf(action.text) === -1) {
|
|
33
|
+
throw new Error(`Type verification failed: value did not update for ${action.selector}`);
|
|
34
|
+
}
|
|
28
35
|
return `Typed into ${action.selector}`;
|
|
29
36
|
}
|
|
30
37
|
case "navigate": {
|
|
@@ -33,6 +40,9 @@ async function executeAction(action) {
|
|
|
33
40
|
}
|
|
34
41
|
case "extract": {
|
|
35
42
|
const value = mustFind(action.selector).innerText.trim();
|
|
43
|
+
if (!value) {
|
|
44
|
+
throw new Error(`Extract returned empty text from ${action.selector}`);
|
|
45
|
+
}
|
|
36
46
|
return `${action.label}: ${value}`;
|
|
37
47
|
}
|
|
38
48
|
case "scroll": {
|
|
@@ -56,7 +66,7 @@ async function executeAction(action) {
|
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
// src/
|
|
69
|
+
// src/core/observer.ts
|
|
60
70
|
var CANDIDATE_SELECTOR = "a,button,input,textarea,select,[role='button'],[role='link'],[contenteditable='true']";
|
|
61
71
|
var MAX_CANDIDATES = 60;
|
|
62
72
|
function cssPath(element) {
|
|
@@ -87,23 +97,50 @@ function cssPath(element) {
|
|
|
87
97
|
return parts.join(" > ");
|
|
88
98
|
}
|
|
89
99
|
function isVisible(el) {
|
|
90
|
-
if (el.offsetParent === null && el.tagName !== "BODY")
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
100
|
+
if (el.offsetParent === null && el.tagName !== "BODY") return false;
|
|
93
101
|
const style = window.getComputedStyle(el);
|
|
94
|
-
|
|
102
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
103
|
+
const rect = el.getBoundingClientRect();
|
|
104
|
+
return rect.width > 0 || rect.height > 0;
|
|
105
|
+
}
|
|
106
|
+
function isInViewport(el) {
|
|
107
|
+
const rect = el.getBoundingClientRect();
|
|
108
|
+
return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
|
|
109
|
+
}
|
|
110
|
+
function getAssociatedLabel(el) {
|
|
111
|
+
if (el.id) {
|
|
112
|
+
const label = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
113
|
+
if (label) return label.innerText.trim();
|
|
114
|
+
}
|
|
115
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
116
|
+
if (labelledBy) {
|
|
117
|
+
const labelEl = document.getElementById(labelledBy);
|
|
118
|
+
if (labelEl) return labelEl.innerText.trim();
|
|
119
|
+
}
|
|
120
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
121
|
+
if (ariaLabel) return ariaLabel.trim();
|
|
122
|
+
const parentLabel = el.closest("label");
|
|
123
|
+
if (parentLabel) {
|
|
124
|
+
return Array.from(parentLabel.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent?.trim() ?? "").filter(Boolean).join(" ");
|
|
125
|
+
}
|
|
126
|
+
return "";
|
|
95
127
|
}
|
|
96
128
|
function collectSnapshot() {
|
|
97
|
-
const
|
|
129
|
+
const allNodes = Array.from(
|
|
98
130
|
document.querySelectorAll(CANDIDATE_SELECTOR)
|
|
99
|
-
).filter(isVisible)
|
|
131
|
+
).filter(isVisible);
|
|
132
|
+
const inView = allNodes.filter(isInViewport);
|
|
133
|
+
const offScreen = allNodes.filter((el) => !isInViewport(el));
|
|
134
|
+
const nodes = [...inView, ...offScreen].slice(0, MAX_CANDIDATES);
|
|
100
135
|
const candidates = nodes.map((node) => {
|
|
101
136
|
const placeholder = node.placeholder?.trim() || node.getAttribute("placeholder")?.trim();
|
|
137
|
+
const associatedLabel = getAssociatedLabel(node);
|
|
102
138
|
return {
|
|
103
139
|
selector: cssPath(node),
|
|
104
140
|
role: node.getAttribute("role") ?? node.tagName.toLowerCase(),
|
|
105
|
-
text: (node.innerText || node.getAttribute("
|
|
106
|
-
placeholder: placeholder || void 0
|
|
141
|
+
text: (node.innerText || node.getAttribute("name") || "").trim().slice(0, 120),
|
|
142
|
+
placeholder: placeholder || void 0,
|
|
143
|
+
label: associatedLabel || void 0
|
|
107
144
|
};
|
|
108
145
|
});
|
|
109
146
|
const textPreview = document.body.innerText.replace(/\s+/g, " ").trim().slice(0, 1500);
|
|
@@ -115,7 +152,7 @@ function collectSnapshot() {
|
|
|
115
152
|
};
|
|
116
153
|
}
|
|
117
154
|
|
|
118
|
-
// src/
|
|
155
|
+
// src/core/planner.ts
|
|
119
156
|
var URL_PATTERN = /(?:go to|navigate to|open)\s+(https?:\/\/\S+)/i;
|
|
120
157
|
var SEARCH_PATTERN = /search(?:\s+for)?\s+(.+)/i;
|
|
121
158
|
var FILL_PATTERN = /(?:fill|type|enter)\s+"?([^"]+)"?\s+(?:in(?:to)?|for|on)\s+(.+)/i;
|
|
@@ -234,8 +271,43 @@ function assessRisk(action) {
|
|
|
234
271
|
}
|
|
235
272
|
}
|
|
236
273
|
|
|
274
|
+
// src/shared/parse-action.ts
|
|
275
|
+
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
276
|
+
"click",
|
|
277
|
+
"type",
|
|
278
|
+
"navigate",
|
|
279
|
+
"extract",
|
|
280
|
+
"scroll",
|
|
281
|
+
"focus",
|
|
282
|
+
"wait",
|
|
283
|
+
"done"
|
|
284
|
+
]);
|
|
285
|
+
function parseAction(raw) {
|
|
286
|
+
const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
287
|
+
const candidate = fenceMatch ? fenceMatch[1].trim() : raw.trim();
|
|
288
|
+
const objectMatch = candidate.match(/\{[\s\S]*\}/);
|
|
289
|
+
if (!objectMatch) {
|
|
290
|
+
return { type: "done", reason: `No JSON object found in: ${raw.slice(0, 120)}` };
|
|
291
|
+
}
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(objectMatch[0]);
|
|
295
|
+
} catch {
|
|
296
|
+
return { type: "done", reason: `JSON parse error for: ${objectMatch[0].slice(0, 120)}` };
|
|
297
|
+
}
|
|
298
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
299
|
+
return { type: "done", reason: "Parsed value is not an object" };
|
|
300
|
+
}
|
|
301
|
+
const obj = parsed;
|
|
302
|
+
if (typeof obj.type !== "string" || !VALID_TYPES.has(obj.type)) {
|
|
303
|
+
return { type: "done", reason: `Unknown or missing action type: ${String(obj.type)}` };
|
|
304
|
+
}
|
|
305
|
+
return obj;
|
|
306
|
+
}
|
|
307
|
+
|
|
237
308
|
// src/lib/index.ts
|
|
238
309
|
var DEFAULT_PLANNER = { kind: "heuristic" };
|
|
310
|
+
var MAX_CONSECUTIVE_ERRORS = 2;
|
|
239
311
|
var BrowserAgent = class {
|
|
240
312
|
session;
|
|
241
313
|
maxSteps;
|
|
@@ -284,6 +356,8 @@ var BrowserAgent = class {
|
|
|
284
356
|
return this.runLoop();
|
|
285
357
|
}
|
|
286
358
|
async runLoop() {
|
|
359
|
+
let consecutiveErrors = 0;
|
|
360
|
+
let lastError;
|
|
287
361
|
for (let step = 0; step < this.maxSteps; step += 1) {
|
|
288
362
|
if (this.isStopped || !this.session.isRunning) {
|
|
289
363
|
return { status: "done", message: "Stopped" };
|
|
@@ -292,9 +366,24 @@ var BrowserAgent = class {
|
|
|
292
366
|
this.session.isRunning = false;
|
|
293
367
|
return { status: "done", message: "Aborted" };
|
|
294
368
|
}
|
|
295
|
-
const result2 = await this.tick();
|
|
296
|
-
this.session.history.push(result2.message);
|
|
369
|
+
const result2 = await this.tick(lastError);
|
|
297
370
|
this.events.onStep?.(result2, this.getSession());
|
|
371
|
+
if (result2.status === "error") {
|
|
372
|
+
consecutiveErrors += 1;
|
|
373
|
+
lastError = result2.message;
|
|
374
|
+
this.session.history.push(`Error: ${result2.message}`);
|
|
375
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
376
|
+
this.session.isRunning = false;
|
|
377
|
+
this.events.onError?.(new Error(result2.message), this.getSession());
|
|
378
|
+
this.events.onDone?.(result2, this.getSession());
|
|
379
|
+
return result2;
|
|
380
|
+
}
|
|
381
|
+
await this.delay(this.stepDelayMs);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
consecutiveErrors = 0;
|
|
385
|
+
lastError = void 0;
|
|
386
|
+
this.session.history.push(result2.message);
|
|
298
387
|
if (result2.status === "needs_approval") {
|
|
299
388
|
this.session.pendingAction = result2.action;
|
|
300
389
|
this.session.isRunning = false;
|
|
@@ -303,7 +392,7 @@ var BrowserAgent = class {
|
|
|
303
392
|
}
|
|
304
393
|
return result2;
|
|
305
394
|
}
|
|
306
|
-
if (["done", "blocked"
|
|
395
|
+
if (["done", "blocked"].includes(result2.status)) {
|
|
307
396
|
this.session.isRunning = false;
|
|
308
397
|
this.events.onDone?.(result2, this.getSession());
|
|
309
398
|
return result2;
|
|
@@ -343,18 +432,17 @@ var BrowserAgent = class {
|
|
|
343
432
|
this.isStopped = true;
|
|
344
433
|
this.session.isRunning = false;
|
|
345
434
|
}
|
|
346
|
-
async tick() {
|
|
435
|
+
async tick(lastError) {
|
|
347
436
|
try {
|
|
348
437
|
const snapshot = collectSnapshot();
|
|
349
438
|
const action = await planNextAction(this.session.planner, {
|
|
350
439
|
goal: this.session.goal,
|
|
351
440
|
snapshot,
|
|
352
|
-
history: this.session.history
|
|
441
|
+
history: this.session.history,
|
|
442
|
+
lastError
|
|
353
443
|
});
|
|
354
444
|
return this.processAction(action);
|
|
355
445
|
} catch (error) {
|
|
356
|
-
this.session.isRunning = false;
|
|
357
|
-
this.events.onError?.(error, this.getSession());
|
|
358
446
|
return { status: "error", message: String(error) };
|
|
359
447
|
}
|
|
360
448
|
}
|
|
@@ -393,6 +481,7 @@ function createBrowserAgent(config, events) {
|
|
|
393
481
|
}
|
|
394
482
|
export {
|
|
395
483
|
BrowserAgent,
|
|
396
|
-
createBrowserAgent
|
|
484
|
+
createBrowserAgent,
|
|
485
|
+
parseAction
|
|
397
486
|
};
|
|
398
487
|
//# sourceMappingURL=lib.js.map
|