browserclaw-agent 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/advisor.d.ts +8 -0
- package/dist/advisor.d.ts.map +1 -0
- package/dist/{judge.js → advisor.js} +14 -14
- package/dist/advisor.js.map +1 -0
- package/dist/agent-loop.d.ts +2 -1
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +57 -45
- package/dist/agent-loop.js.map +1 -1
- package/dist/answer-validator.d.ts +15 -0
- package/dist/answer-validator.d.ts.map +1 -0
- package/dist/answer-validator.js +94 -0
- package/dist/answer-validator.js.map +1 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +39 -24
- package/dist/session-manager.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -1
- package/dist/judge.d.ts +0 -8
- package/dist/judge.d.ts.map +0 -1
- package/dist/judge.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Idan Rubin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# browserclaw-agent
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://browserclaw.org"><img src="https://img.shields.io/badge/Live-browserclaw.org-orange" alt="Live" /></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/browserclaw-agent"><img src="https://img.shields.io/npm/v/browserclaw-agent.svg" alt="npm" /></a>
|
|
6
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /></a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
The AI agent for [browserclaw](https://github.com/idan-rubin/browserclaw).
|
|
10
|
+
|
|
11
|
+
## Layered, not bundled
|
|
12
|
+
|
|
13
|
+
Three separate layers, not one monolith:
|
|
14
|
+
|
|
15
|
+
- ⚡ **LLM** — the electricity. Your choice: Claude, GPT, Gemini, local. No lock-in.
|
|
16
|
+
- 😎 **The agent** — the driver. Reasoning, obstacle recovery, learned skills. _That's this project._
|
|
17
|
+
- 🏎️ **[BrowserClaw](https://github.com/idan-rubin/browserclaw)** — the vehicle. Snapshots, element refs, browser control. Standalone npm library.
|
|
18
|
+
|
|
19
|
+
browser-use welds these into one package. We keep them as separate, swappable layers — drop the library into your own agent, pair this agent with any LLM, or run the whole stack as-is.
|
|
20
|
+
|
|
21
|
+
## vs browser-use
|
|
22
|
+
|
|
23
|
+
**Different lineage, different design.** [OpenClaw](https://openclaw.ai) took the [Playwright MCP](https://github.com/microsoft/playwright-mcp) approach — Microsoft's snapshot-and-ref pattern — implemented it locally, and refined it into [browserclaw](https://github.com/idan-rubin/browserclaw), a standalone npm library. This agent rides on that library. browser-use rolled its own Python stack as one bundled package.
|
|
24
|
+
|
|
25
|
+
| | browserclaw | browser-use |
|
|
26
|
+
| :-------------------------- | :----------------: | :----------------: |
|
|
27
|
+
| Standalone browser library | :white_check_mark: | :x: |
|
|
28
|
+
| Works with any agent | :white_check_mark: | :heavy_minus_sign: |
|
|
29
|
+
| Auto-learns skills per site | :white_check_mark: | :x: |
|
|
30
|
+
| Solves anti-bot in OSS | :white_check_mark: | :heavy_minus_sign: |
|
|
31
|
+
| TypeScript / Node | :white_check_mark: | :x: |
|
|
32
|
+
|
|
33
|
+
:white_check_mark: = Yes  :heavy_minus_sign: = Partial  :x: = No
|
|
34
|
+
|
|
35
|
+
## What the agent does
|
|
36
|
+
|
|
37
|
+
The agent reads an accessibility snapshot of the page, decides what to do next, and executes the action. Up to 100 steps per run. It maintains a memory scratchpad across steps and evaluates whether each action succeeded before deciding the next move.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
snapshot → LLM → action → repeat
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Built-in skills
|
|
44
|
+
|
|
45
|
+
When the agent hits common obstacles, built-in skills take over automatically — no prompting needed:
|
|
46
|
+
|
|
47
|
+
- **Anti-bot bypass** — Detects and solves "hold to verify" overlays and press-and-hold challenges via CDP
|
|
48
|
+
- **Cloudflare Turnstile** — Solves "Verify you are human" checkboxes by locating and clicking the Turnstile iframe via CDP
|
|
49
|
+
- **Popup dismissal** — Closes cookie banners, consent dialogs, and modals using multi-strategy detection
|
|
50
|
+
- **Loop detection** — Detects when the agent is stuck repeating the same action and nudges it toward a different approach
|
|
51
|
+
- **Tab manager** — Detects and switches to new tabs opened during automation
|
|
52
|
+
|
|
53
|
+
### Skill catalog
|
|
54
|
+
|
|
55
|
+
Every successful run generates a skill file — steps and tips for that domain, stored in MinIO. On the next run against the same domain, the agent loads the skill as a playbook instead of exploring from scratch. If the new run completes in fewer steps, the skill is replaced. One domain, one skill, always improving.
|
|
56
|
+
|
|
57
|
+
The first user to automate a domain pays the exploration cost. Every subsequent run benefits from the learned playbook — and refines it further.
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
### Local (dev mode)
|
|
62
|
+
|
|
63
|
+
Chrome opens on your desktop. No containers, no VNC.
|
|
64
|
+
|
|
65
|
+
**Requires:** Node.js 22+, Chrome installed
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd src/Services/Browser
|
|
69
|
+
cp .env.example .env.local
|
|
70
|
+
# Set LLM_PROVIDER and at least one API key (see LLM providers below)
|
|
71
|
+
npm install
|
|
72
|
+
npm run dev
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Start a run:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
curl -X POST http://localhost:5040/api/v1/sessions \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-d '{"prompt": "Find apartments in NYC under $3000"}'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Stream progress:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl http://localhost:5040/api/v1/sessions/{id}/stream
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Docker (full stack)
|
|
90
|
+
|
|
91
|
+
Runs the frontend, browser service (headless Chrome + VNC), MinIO (skill storage), and Traefik. Same setup as [browserclaw.org](https://browserclaw.org).
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/idan-rubin/browserclaw-agent.git
|
|
95
|
+
cd browserclaw-agent
|
|
96
|
+
cp src/Services/Browser/.env.example src/Services/Browser/.env.local
|
|
97
|
+
# Set LLM_PROVIDER and at least one API key
|
|
98
|
+
# Set BROWSER_INTERNAL_TOKEN to a random secret (used for frontend → browser service auth):
|
|
99
|
+
echo "BROWSER_INTERNAL_TOKEN=$(openssl rand -hex 32)" >> .env
|
|
100
|
+
docker compose up
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Open [localhost](http://localhost).
|
|
104
|
+
|
|
105
|
+
### LLM providers
|
|
106
|
+
|
|
107
|
+
Add at least one API key to `.env.local` and set `LLM_PROVIDER`:
|
|
108
|
+
|
|
109
|
+
| Provider | Env var | `LLM_PROVIDER` | Free tier |
|
|
110
|
+
| ----------------------------- | -------------------- | -------------- | ----------------- |
|
|
111
|
+
| Groq | `GROQ_API_KEY` | `groq` | Yes |
|
|
112
|
+
| Google Gemini | `GEMINI_API_KEY` | `gemini` | Yes |
|
|
113
|
+
| OpenAI | `OPENAI_API_KEY` | `openai` | No |
|
|
114
|
+
| OpenAI (ChatGPT subscription) | `OPENAI_OAUTH_TOKEN` | `openai-oauth` | No (subscription) |
|
|
115
|
+
| Anthropic | `ANTHROPIC_API_KEY` | `anthropic` | No |
|
|
116
|
+
|
|
117
|
+
Set `LLM_MODEL` to override the default model for your provider.
|
|
118
|
+
|
|
119
|
+
### Other features
|
|
120
|
+
|
|
121
|
+
- **BYOK** — Users can pass their own LLM API key per session for multi-tenant deployments
|
|
122
|
+
- **User interaction** — The agent can pause mid-run to ask for information (MFA codes, credentials)
|
|
123
|
+
- **SSE streaming** — Real-time step-by-step progress events
|
|
124
|
+
- **Content moderation** — Rejects harmful prompts before execution
|
|
125
|
+
- **SSRF protection** — Private network access blocked by default
|
|
126
|
+
|
|
127
|
+
## Bring your own agent
|
|
128
|
+
|
|
129
|
+
Don't want this agent? Use the [browserclaw](https://github.com/idan-rubin/browserclaw) library directly with any LLM.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm install browserclaw
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Requires Chrome, Brave, Edge, or Chromium installed on your machine.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { BrowserClaw } from "browserclaw";
|
|
139
|
+
|
|
140
|
+
const browser = await BrowserClaw.launch({ headless: false });
|
|
141
|
+
const page = await browser.open("https://example.com");
|
|
142
|
+
|
|
143
|
+
const { snapshot, refs } = await page.snapshot();
|
|
144
|
+
// snapshot: text tree of the page
|
|
145
|
+
// refs: { "e1": { role: "link", name: "More info" }, ... }
|
|
146
|
+
|
|
147
|
+
await page.click("e1");
|
|
148
|
+
await page.type("e3", "hello");
|
|
149
|
+
await browser.stop();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`snapshot()` returns a text representation of the page with numbered refs. Pass it to any LLM, get back a ref, call the action. Here's a minimal agent loop:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { BrowserClaw } from "browserclaw";
|
|
156
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
157
|
+
|
|
158
|
+
const client = new Anthropic();
|
|
159
|
+
const browser = await BrowserClaw.launch({ headless: false });
|
|
160
|
+
const page = await browser.open("https://news.ycombinator.com");
|
|
161
|
+
const history = [];
|
|
162
|
+
|
|
163
|
+
for (let step = 0; step < 20; step++) {
|
|
164
|
+
const { snapshot } = await page.snapshot();
|
|
165
|
+
const response = await client.messages.create({
|
|
166
|
+
model: "claude-sonnet-4-20250514",
|
|
167
|
+
max_tokens: 1024,
|
|
168
|
+
system:
|
|
169
|
+
"You control a browser. Given a page snapshot, return JSON: { action, ref?, text?, url?, reasoning }. Actions: click, type, navigate, done.",
|
|
170
|
+
messages: [
|
|
171
|
+
...history,
|
|
172
|
+
{
|
|
173
|
+
role: "user",
|
|
174
|
+
content: `Task: Find the top 3 AI posts.\n\nPage:\n${snapshot}`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const action = JSON.parse(response.content[0].text);
|
|
180
|
+
history.push(
|
|
181
|
+
{ role: "user", content: `Page:\n${snapshot}` },
|
|
182
|
+
{ role: "assistant", content: JSON.stringify(action) },
|
|
183
|
+
);
|
|
184
|
+
if (action.action === "done") break;
|
|
185
|
+
|
|
186
|
+
switch (action.action) {
|
|
187
|
+
case "click":
|
|
188
|
+
await page.click(action.ref);
|
|
189
|
+
break;
|
|
190
|
+
case "type":
|
|
191
|
+
await page.type(action.ref, action.text);
|
|
192
|
+
break;
|
|
193
|
+
case "navigate":
|
|
194
|
+
await page.goto(action.url);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
await browser.stop();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Swap Anthropic for OpenAI, Groq, Gemini, or a local model. See the full [browserclaw API docs](https://github.com/idan-rubin/browserclaw) for `fill()`, `select()`, `drag()`, `screenshot()`, `pdf()`, `waitFor()`, and more.
|
|
202
|
+
|
|
203
|
+
## Why browserclaw?
|
|
204
|
+
|
|
205
|
+
- **Built for TypeScript** — native to the JS ecosystem. First-class Node.js support, not a Python port.
|
|
206
|
+
- **Accessibility tree, not DOM** — snapshots use the browser's accessibility tree — the same structure screen readers use. Semantic roles, names, and states instead of raw tags and attributes. Cleaner, smaller, and more meaningful to an LLM.
|
|
207
|
+
- **Layered, not bundled** — the engine, the agent, and the LLM are separate, swappable pieces. See the [comparison above](#vs-browser-use).
|
|
208
|
+
- **Gets smarter with use** — the skill catalog learns from every successful run. Other browser agents start from scratch each time. browserclaw-agent builds a playbook per domain and improves it on every run.
|
|
209
|
+
- **Handles the real world** — Cloudflare Turnstile, press-and-hold anti-bot overlays, cookie banners, tab management — handled automatically via CDP. These are the things that make browser agents fail in production.
|
|
210
|
+
|
|
211
|
+
## Read more
|
|
212
|
+
|
|
213
|
+
- [The Intelligence Gap](https://mrrubin.substack.com/p/the-knowledge-gap-why-ai-browser) — why AI browser agents keep failing, and what we're doing about it
|
|
214
|
+
|
|
215
|
+
## Built with
|
|
216
|
+
|
|
217
|
+
- [BrowserClaw](https://github.com/idan-rubin/browserclaw) — the browser automation library
|
|
218
|
+
- [OpenClaw](https://github.com/openclaw/openclaw) — the community behind it
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentLoopResult } from './types.js';
|
|
2
|
+
interface AdvisorReview {
|
|
3
|
+
success: boolean;
|
|
4
|
+
reasoning: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function advisorReview(prompt: string, result: AgentLoopResult): Promise<AdvisorReview>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=advisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advisor.d.ts","sourceRoot":"","sources":["../src/advisor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD,UAAU,aAAa;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AA2CD,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CAanG"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { llmJson } from './llm.js';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
|
-
const
|
|
3
|
+
const ADVISOR_PROMPT = `You are a quality advisor for browser automation runs. You review the agent's completed trace and flag quality issues as advisory notes — you do NOT gate success.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Review for:
|
|
6
6
|
1. Did the agent's actions actually achieve the goal, or did it just claim success?
|
|
7
7
|
2. Is the answer grounded in data from actual page snapshots, or does it contain fabricated/hallucinated data?
|
|
8
8
|
3. Did the agent get blocked by a login wall, paywall, CAPTCHA, or error page and report success anyway?
|
|
@@ -12,11 +12,11 @@ Be initially doubtful of the agent's self-reported success. Check for:
|
|
|
12
12
|
Respond with JSON:
|
|
13
13
|
{
|
|
14
14
|
"success": true/false,
|
|
15
|
-
"reasoning": "
|
|
15
|
+
"reasoning": "one-paragraph quality note — what you'd tell a reviewer to check"
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
Set success=false
|
|
19
|
-
function
|
|
18
|
+
Set success=false when you'd want a human reviewer to double-check the answer. Callers treat this as advisory metadata, not a blocking verdict.`;
|
|
19
|
+
function buildAdvisorMessage(prompt, result) {
|
|
20
20
|
let message = `Task: ${prompt}\n`;
|
|
21
21
|
message += `Agent reported: ${result.success ? 'SUCCESS' : 'FAILURE'}\n`;
|
|
22
22
|
if (result.answer !== undefined) {
|
|
@@ -39,19 +39,19 @@ function buildJudgeMessage(prompt, result) {
|
|
|
39
39
|
}
|
|
40
40
|
return message;
|
|
41
41
|
}
|
|
42
|
-
export async function
|
|
42
|
+
export async function advisorReview(prompt, result) {
|
|
43
43
|
try {
|
|
44
|
-
const
|
|
45
|
-
system:
|
|
46
|
-
message:
|
|
44
|
+
const review = await llmJson({
|
|
45
|
+
system: ADVISOR_PROMPT,
|
|
46
|
+
message: buildAdvisorMessage(prompt, result),
|
|
47
47
|
maxTokens: 256,
|
|
48
48
|
});
|
|
49
|
-
logger.info({
|
|
50
|
-
return
|
|
49
|
+
logger.info({ advisor_ran: true, quality_ok: review.success }, 'Advisor review');
|
|
50
|
+
return review;
|
|
51
51
|
}
|
|
52
52
|
catch {
|
|
53
|
-
logger.warn({
|
|
54
|
-
return { success:
|
|
53
|
+
logger.warn({ advisor_failed: true }, 'Advisor review failed — treating run as clean');
|
|
54
|
+
return { success: true, reasoning: 'Advisor review unavailable' };
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
//# sourceMappingURL=
|
|
57
|
+
//# sourceMappingURL=advisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advisor.js","sourceRoot":"","sources":["../src/advisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAOrC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;gJAeyH,CAAC;AAEjJ,SAAS,mBAAmB,CAAC,MAAc,EAAE,MAAuB;IAClE,IAAI,OAAO,GAAG,SAAS,MAAM,IAAI,CAAC;IAClC,OAAO,IAAI,mBAAmB,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC;IACzE,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,IAAI,iBAAiB,MAAM,CAAC,MAAM,IAAI,CAAC;IAChD,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,gBAAgB,MAAM,CAAC,KAAK,IAAI,CAAC;IAC9C,CAAC;IACD,OAAO,IAAI,sBAAsB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC;IAEzE,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,IAAI,IAAI,GAAG,UAAU,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAC3F,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;YAAE,IAAI,IAAI,KAAK,IAAI,CAAC,GAAG,GAAG,CAAC;QACrD,IAAI,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,SAAS;YAAE,IAAI,IAAI,cAAc,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QACjG,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;YAClE,IAAI,IAAI,iBAAiB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QAClE,CAAC;QACD,OAAO,IAAI,GAAG,IAAI,IAAI,CAAC;IACzB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,MAAuB;IACzE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAgB;YAC1C,MAAM,EAAE,cAAc;YACtB,OAAO,EAAE,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC;YAC5C,SAAS,EAAE,GAAG;SACf,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,EAAE,gBAAgB,CAAC,CAAC;QACjF,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,+CAA+C,CAAC,CAAC;QACvF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,4BAA4B,EAAE,CAAC;IACpE,CAAC;AACH,CAAC"}
|
package/dist/agent-loop.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export interface AgentLoopOptions {
|
|
|
45
45
|
} | undefined>>;
|
|
46
46
|
buildTask?: (prompt: string) => string | Promise<string>;
|
|
47
47
|
getLesson?: (prompt: string) => Promise<TaskLesson | null>;
|
|
48
|
+
getSkill?: (domain: string) => Promise<CatalogSkill | null>;
|
|
48
49
|
config?: Partial<AgentConfig>;
|
|
49
50
|
}
|
|
50
51
|
export interface UserChatHooks {
|
|
@@ -53,5 +54,5 @@ export interface UserChatHooks {
|
|
|
53
54
|
/** Per-session random token used to wrap user-interjection content so the LLM can't confuse user data for system instructions. */
|
|
54
55
|
nonce: string;
|
|
55
56
|
}
|
|
56
|
-
export declare function runAgentLoop(prompt: string, pageOrHolder: CrawlPage | PageHolder, emit: (event: string, data: unknown) => void, signal: AbortSignal, waitForUser?: () => Promise<string>, browser?: BrowserClaw,
|
|
57
|
+
export declare function runAgentLoop(prompt: string, pageOrHolder: CrawlPage | PageHolder, emit: (event: string, data: unknown) => void, signal: AbortSignal, waitForUser?: () => Promise<string>, browser?: BrowserClaw, initialDomainSkill?: CatalogSkill | null, maxSteps?: number, userChat?: UserChatHooks, options?: AgentLoopOptions): Promise<AgentLoopResult>;
|
|
57
58
|
//# sourceMappingURL=agent-loop.d.ts.map
|
package/dist/agent-loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAa1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,KAAK,EAAE,WAAW,EAAa,eAAe,EAAiB,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAY/C;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiC5F;AAgrBD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAsB1D;AAMD,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAIvE;AA8QD,MAAM,MAAM,mBAAmB,GAAG;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtG;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG5D;AA2CD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,aAAa,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAC3G,CAAC;IACF,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,aAAa,EAAE,MAAM,WAAW,EAAE,CAAC;IACnC,kIAAkI;IAClI,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,SAAS,GAAG,UAAU,EACpC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,EAC5C,MAAM,EAAE,WAAW,EACnB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EACnC,OAAO,CAAC,EAAE,WAAW,EACrB,kBAAkB,CAAC,EAAE,YAAY,GAAG,IAAI,EACxC,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,aAAa,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAw/B1B"}
|
package/dist/agent-loop.js
CHANGED
|
@@ -7,6 +7,8 @@ import { diagnoseStuckAgent, formatRecovery } from './skills/recovery.js';
|
|
|
7
7
|
import { TabManager } from './skills/tab-manager.js';
|
|
8
8
|
import { getCdpBaseUrl, activateCdpTarget } from './skills/cdp-utils.js';
|
|
9
9
|
import { extractItems, extractItemsFromUrls } from './skills/extract-items.js';
|
|
10
|
+
import { extractDomain } from './skill-store.js';
|
|
11
|
+
import { extractSchema, validateAnswer } from './answer-validator.js';
|
|
10
12
|
import { webSearch } from './web-search.js';
|
|
11
13
|
import { llmJson, llmVision, sanitizeErrorText, getTokenUsage } from './llm.js';
|
|
12
14
|
import { LlmParseError } from './types.js';
|
|
@@ -377,7 +379,6 @@ async function isBrowserAlive(page) {
|
|
|
377
379
|
return false;
|
|
378
380
|
}
|
|
379
381
|
}
|
|
380
|
-
const SKILL_INJECT_MAX_STEP = 2;
|
|
381
382
|
const PLAN_INJECT_MAX_STEP = 8;
|
|
382
383
|
const HISTORY_RECENT_WINDOW = 8;
|
|
383
384
|
const MAX_ACTIONS_PER_STEP = 4;
|
|
@@ -789,6 +790,24 @@ function computeStateSig(url, snapshotText) {
|
|
|
789
790
|
}
|
|
790
791
|
return `${String(payload.length)}:${String(h)}`;
|
|
791
792
|
}
|
|
793
|
+
const REF_FAILURE_KEYWORDS = [
|
|
794
|
+
'not found',
|
|
795
|
+
'no element',
|
|
796
|
+
'intercept',
|
|
797
|
+
'covered',
|
|
798
|
+
'obscured',
|
|
799
|
+
'detach',
|
|
800
|
+
'stale',
|
|
801
|
+
'disabled',
|
|
802
|
+
'not interactable',
|
|
803
|
+
];
|
|
804
|
+
function isRefFailure(action, err) {
|
|
805
|
+
if (action.action !== 'click' && action.action !== 'type' && action.action !== 'select')
|
|
806
|
+
return false;
|
|
807
|
+
const raw = err instanceof Error ? err.message : '';
|
|
808
|
+
const lower = raw.toLowerCase();
|
|
809
|
+
return REF_FAILURE_KEYWORDS.some((kw) => lower.includes(kw));
|
|
810
|
+
}
|
|
792
811
|
function describeActionError(action, err) {
|
|
793
812
|
const raw = err instanceof Error ? err.message : 'Action execution failed';
|
|
794
813
|
const lower = raw.toLowerCase();
|
|
@@ -943,7 +962,7 @@ Use ONLY data that appears in the memory or steps below — never invent values.
|
|
|
943
962
|
return { ready: false, missing: 'judgment unavailable' };
|
|
944
963
|
}
|
|
945
964
|
}
|
|
946
|
-
export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUser, browser,
|
|
965
|
+
export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUser, browser, initialDomainSkill, maxSteps, userChat, options) {
|
|
947
966
|
const cfg = defaultAgentConfig(options?.config);
|
|
948
967
|
maxSteps ??= cfg.maxSteps;
|
|
949
968
|
// Accept either a bare CrawlPage or a PageHolder. When a PageHolder is
|
|
@@ -997,7 +1016,8 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
|
|
|
997
1016
|
final_url: currentUrl,
|
|
998
1017
|
};
|
|
999
1018
|
};
|
|
1000
|
-
|
|
1019
|
+
let domainSkill = initialDomainSkill ?? null;
|
|
1020
|
+
let answerSchema = null;
|
|
1001
1021
|
let taskLesson = null;
|
|
1002
1022
|
if (options?.getLesson !== undefined) {
|
|
1003
1023
|
try {
|
|
@@ -1012,7 +1032,6 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
|
|
|
1012
1032
|
logger.error({ err }, 'Failed to load task lessons');
|
|
1013
1033
|
}
|
|
1014
1034
|
}
|
|
1015
|
-
// ── Planning + goal refinement (single LLM call) ──────────────────────────
|
|
1016
1035
|
// If the prompt is vague (e.g. "find apartments in Chelsea"), the planner
|
|
1017
1036
|
// also produces a SMART task with clear scope and stopping criteria.
|
|
1018
1037
|
let refinedPrompt = prompt;
|
|
@@ -1020,7 +1039,7 @@ export async function runAgentLoop(prompt, pageOrHolder, emit, signal, waitForUs
|
|
|
1020
1039
|
try {
|
|
1021
1040
|
const taskInput = options?.buildTask !== undefined ? await options.buildTask(prompt) : prompt;
|
|
1022
1041
|
let planMessage = `User prompt: ${taskInput}`;
|
|
1023
|
-
if (domainSkill !==
|
|
1042
|
+
if (domainSkill !== null) {
|
|
1024
1043
|
planMessage += `\n\nWe have a proven skill for this site: "${domainSkill.skill.title}" — ${domainSkill.skill.description}`;
|
|
1025
1044
|
planMessage += '\nLeverage it — no need to rediscover what already works.';
|
|
1026
1045
|
}
|
|
@@ -1079,10 +1098,8 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1079
1098
|
let lastStateSig = null;
|
|
1080
1099
|
let noOpStreak = 0;
|
|
1081
1100
|
let baselineModalSigs = null;
|
|
1101
|
+
let lastLoadedSkillDomain = domainSkill?.domain ?? null;
|
|
1082
1102
|
const seenFilterTokens = new Set();
|
|
1083
|
-
const visitedListingUrls = new Set();
|
|
1084
|
-
let lastListExtractStep = -1;
|
|
1085
|
-
let lastListExtractUrls = [];
|
|
1086
1103
|
while (step < maxSteps) {
|
|
1087
1104
|
if (signal.aborted) {
|
|
1088
1105
|
return {
|
|
@@ -1110,6 +1127,27 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1110
1127
|
let snapshot = await safeSnapshot(holder.page);
|
|
1111
1128
|
const url = await holder.page.url();
|
|
1112
1129
|
const title = await holder.page.title();
|
|
1130
|
+
if (options?.getSkill !== undefined) {
|
|
1131
|
+
const currentDomain = extractDomain(url);
|
|
1132
|
+
if (currentDomain !== '' && currentDomain !== lastLoadedSkillDomain) {
|
|
1133
|
+
try {
|
|
1134
|
+
const loaded = await options.getSkill(currentDomain);
|
|
1135
|
+
if (loaded !== null) {
|
|
1136
|
+
domainSkill = loaded;
|
|
1137
|
+
emit('skills_loaded', {
|
|
1138
|
+
count: 1,
|
|
1139
|
+
domain: currentDomain,
|
|
1140
|
+
title: loaded.skill.title,
|
|
1141
|
+
run_count: loaded.run_count,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
logger.warn({ domain: currentDomain, err: err instanceof Error ? err.message : err }, 'Lazy skill fetch failed');
|
|
1147
|
+
}
|
|
1148
|
+
lastLoadedSkillDomain = currentDomain;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1113
1151
|
const domText = await getPageText(holder.page);
|
|
1114
1152
|
const antiBotType = detectAntiBot(domText);
|
|
1115
1153
|
if (antiBotType !== null) {
|
|
@@ -1127,7 +1165,6 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1127
1165
|
snapshot = await screenshotFallback(holder.page, snapshot);
|
|
1128
1166
|
}
|
|
1129
1167
|
emit('thinking', { step, message: `Analyzing page: ${title}` });
|
|
1130
|
-
// --- Context compression ---
|
|
1131
1168
|
if (step > 0 && step % CONTEXT_COMPRESS_INTERVAL === 0) {
|
|
1132
1169
|
contextSummary = await compressContext(refinedPrompt, history, lastProgress);
|
|
1133
1170
|
if (contextSummary !== '') {
|
|
@@ -1135,7 +1172,6 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1135
1172
|
emit('context_compressed', { step, summary_length: contextSummary.length });
|
|
1136
1173
|
}
|
|
1137
1174
|
}
|
|
1138
|
-
// --- Domain change detection (proactive replanning trigger) ---
|
|
1139
1175
|
let domainChanged = false;
|
|
1140
1176
|
try {
|
|
1141
1177
|
const currentDomain = new URL(url).hostname;
|
|
@@ -1147,7 +1183,6 @@ Respond with JSON: {"task": "the SMART task", "plan": "your action plan"}`,
|
|
|
1147
1183
|
catch {
|
|
1148
1184
|
// URL may not be parseable (about:blank etc.)
|
|
1149
1185
|
}
|
|
1150
|
-
// --- Re-planning checkpoint ---
|
|
1151
1186
|
// Uses adaptive intervals: extends when healthy, contracts on failures.
|
|
1152
1187
|
// Also triggers on domain changes (proactive).
|
|
1153
1188
|
const atReplanCheckpoint = step > 0 && step >= nextReplanStep;
|
|
@@ -1194,7 +1229,6 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1194
1229
|
logger.warn('Re-planning failed');
|
|
1195
1230
|
}
|
|
1196
1231
|
}
|
|
1197
|
-
// --- Recovery diagnosis (only when the agent is struggling) ---
|
|
1198
1232
|
let recoveryMessage = null;
|
|
1199
1233
|
if (step > 0 && step % 4 === 0 && recentFailureCount > 0) {
|
|
1200
1234
|
const recovery = diagnoseStuckAgent(history, url);
|
|
@@ -1299,7 +1333,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1299
1333
|
logger.warn({ error: sanitizeErrorText(err instanceof Error ? err.message : 'unknown') }, 'Failed to get tab count');
|
|
1300
1334
|
}
|
|
1301
1335
|
}
|
|
1302
|
-
const skillForStep =
|
|
1336
|
+
const skillForStep = extractDomain(url) === domainSkill?.domain ? domainSkill : undefined;
|
|
1303
1337
|
const lessonForStep = step <= PLAN_INJECT_MAX_STEP ? taskLesson : null;
|
|
1304
1338
|
// Keep plan available for longer — it's cheap context and prevents drift
|
|
1305
1339
|
const planForStep = step <= PLAN_INJECT_MAX_STEP ? planText : null;
|
|
@@ -1485,22 +1519,6 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1485
1519
|
}
|
|
1486
1520
|
continue;
|
|
1487
1521
|
}
|
|
1488
|
-
if (lastListExtractUrls.length >= 5 && step - lastListExtractStep <= 3 && actions[0]?.action === 'click') {
|
|
1489
|
-
const urls = lastListExtractUrls.filter((u) => !visitedListingUrls.has(u)).slice(0, 8);
|
|
1490
|
-
if (urls.length >= 3) {
|
|
1491
|
-
logger.warn({ step, count: urls.length }, 'batch-urls preempt');
|
|
1492
|
-
actions = [
|
|
1493
|
-
{
|
|
1494
|
-
action: 'extract',
|
|
1495
|
-
reasoning: `Batch-fetching ${String(urls.length)} unseen candidate detail pages in parallel.`,
|
|
1496
|
-
urls,
|
|
1497
|
-
},
|
|
1498
|
-
];
|
|
1499
|
-
for (const u of urls)
|
|
1500
|
-
visitedListingUrls.add(u);
|
|
1501
|
-
lastListExtractUrls = [];
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
1522
|
// Execute batch of actions sequentially
|
|
1505
1523
|
for (let actionIdx = 0; actionIdx < actions.length; actionIdx++) {
|
|
1506
1524
|
if (step >= maxSteps)
|
|
@@ -1526,7 +1544,6 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1526
1544
|
page_title: agentStep.page_title,
|
|
1527
1545
|
});
|
|
1528
1546
|
emit('tokens', getTokenUsage());
|
|
1529
|
-
// --- Terminal actions ---
|
|
1530
1547
|
if (action.action === 'done') {
|
|
1531
1548
|
const doneBlockReason = shouldBlockDone(pageState, history.length, action.answer);
|
|
1532
1549
|
if (doneBlockReason !== null) {
|
|
@@ -1535,12 +1552,21 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1535
1552
|
step++;
|
|
1536
1553
|
break;
|
|
1537
1554
|
}
|
|
1555
|
+
const answerText = action.answer ?? '';
|
|
1556
|
+
answerSchema ??= await extractSchema(prompt);
|
|
1557
|
+
const validation = validateAnswer(answerSchema, answerText);
|
|
1558
|
+
const validationWarnings = validation.valid ? undefined : validation.errors;
|
|
1559
|
+
if (validationWarnings !== undefined) {
|
|
1560
|
+
logger.info({ step, warnings: validationWarnings }, 'Done has validation warnings');
|
|
1561
|
+
emit('done_validation_warnings', { step, warnings: validationWarnings });
|
|
1562
|
+
}
|
|
1538
1563
|
return {
|
|
1539
1564
|
success: true,
|
|
1540
1565
|
steps: history,
|
|
1541
1566
|
answer: action.answer,
|
|
1542
1567
|
duration_ms: Date.now() - startTime,
|
|
1543
1568
|
final_url: agentStep.url,
|
|
1569
|
+
validation_warnings: validationWarnings,
|
|
1544
1570
|
};
|
|
1545
1571
|
}
|
|
1546
1572
|
if (action.action === 'fail') {
|
|
@@ -1580,7 +1606,6 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1580
1606
|
step++;
|
|
1581
1607
|
break; // Need new snapshot after user response
|
|
1582
1608
|
}
|
|
1583
|
-
// --- Special actions (break batch — need new snapshot) ---
|
|
1584
1609
|
if (action.action === 'press_and_hold') {
|
|
1585
1610
|
const solved = await pressAndHold(holder.page, { holdMs: action.hold_ms });
|
|
1586
1611
|
if (!solved) {
|
|
@@ -1654,18 +1679,6 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1654
1679
|
logger.info({ step, source: items.source, count: items.count }, 'extract items auto');
|
|
1655
1680
|
if (items.count > 0)
|
|
1656
1681
|
extractProducedData = true;
|
|
1657
|
-
const urls = [];
|
|
1658
|
-
for (const r of items.records) {
|
|
1659
|
-
const u = typeof r.url === 'string' ? r.url : '';
|
|
1660
|
-
if (u !== '' && !urls.includes(u))
|
|
1661
|
-
urls.push(u);
|
|
1662
|
-
if (urls.length >= 10)
|
|
1663
|
-
break;
|
|
1664
|
-
}
|
|
1665
|
-
if (urls.length >= 5) {
|
|
1666
|
-
lastListExtractUrls = urls;
|
|
1667
|
-
lastListExtractStep = step;
|
|
1668
|
-
}
|
|
1669
1682
|
}
|
|
1670
1683
|
if (extractProducedData)
|
|
1671
1684
|
extractsWithDataCount++;
|
|
@@ -1735,7 +1748,6 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1735
1748
|
step++;
|
|
1736
1749
|
break;
|
|
1737
1750
|
}
|
|
1738
|
-
// --- Normal actions ---
|
|
1739
1751
|
const preActionUrl = await holder.page.url();
|
|
1740
1752
|
try {
|
|
1741
1753
|
const customHandler = options?.customActions?.[action.action];
|
|
@@ -1813,7 +1825,7 @@ Respond with JSON: {"plan": "your revised plan here"}`,
|
|
|
1813
1825
|
emit('step_error', { step, action: action.action, error: rawMessage });
|
|
1814
1826
|
agentStep.action.error_feedback = feedback;
|
|
1815
1827
|
recentFailureCount++;
|
|
1816
|
-
if (action.ref !== undefined && action.ref !== '') {
|
|
1828
|
+
if (action.ref !== undefined && action.ref !== '' && isRefFailure(action, err)) {
|
|
1817
1829
|
const entry = bannedRefs.get(action.ref) ?? { action: action.action, failures: 0 };
|
|
1818
1830
|
entry.failures += 1;
|
|
1819
1831
|
entry.action = action.action;
|