@yibie/pi-jev-browser 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/package.json +3 -2
- package/src/gate.ts +53 -0
- package/src/jev-run.ts +47 -2
- package/test/guard.test.ts +181 -0
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ An isolated Playwright Chromium browser for pi, driven by [Jev](https://typesafe
|
|
|
6
6
|
|
|
7
7
|
Jev does not see screenshots. The plugin hands it a structured DOM observation and one multiple-choice question per step, and Jev answers with a concrete operation plus a probability distribution over the offered options. That removes the screenshot round trip and the reasoning round trip from every browser step.
|
|
8
8
|
|
|
9
|
+
**Demo:** [`docs/demo.mp4`](docs/demo.mp4) — a recorded run that pages to the next listing, opens the first book, and scrolls until the Product Information table is in view. 2× speed, 11 s, with the cursor and click indicators the plugin injects for recordings.
|
|
10
|
+
|
|
9
11
|
**This is not a replacement for `agent_browser`.** It is the other trade: no login state, no extensions, no host environment, every step recorded with its probability, and a much cheaper fast loop. Use it for narrowly scoped goals on public pages. Use a profile-based browser tool when you need the user's session.
|
|
10
12
|
|
|
11
13
|
## Decision policies
|
|
@@ -83,8 +85,8 @@ Statuses are `done_unverified`, `blocked`, `needs_review`, `uncertain`, `step_li
|
|
|
83
85
|
|
|
84
86
|
Three layers exist, and only the first one is enforcement:
|
|
85
87
|
|
|
86
|
-
1. **Mechanical.** Navigation allowlist enforced in the request router; downloads refused and logged; service workers blocked; extensions and file-system access disabled; the browser process gets an empty environment; password, file, and hidden inputs are never observed; the model can only select from server-generated target IDs, so its output can never become a selector, URL, or code
|
|
87
|
-
2. **Model guidance.** Jev is instructed to return `REVIEW` before messages, posts, orders, payments, bookings, deletions, permission changes, sensitive-data entry, CAPTCHAs, or security warnings. This is guidance, not a deterministic boundary.
|
|
88
|
+
1. **Mechanical.** Navigation allowlist enforced in the request router; downloads refused and logged; service workers blocked; extensions and file-system access disabled; the browser process gets an empty environment; password, file, and hidden inputs are never observed; the model can only select from server-generated target IDs, so its output can never become a selector, URL, or code; and a human-verification gate ends the run as `needs_review` **before the decision layer is consulted at all**.
|
|
89
|
+
2. **Model guidance.** Jev is instructed to return `REVIEW` before messages, posts, orders, payments, bookings, deletions, permission changes, sensitive-data entry, CAPTCHAs, or security warnings. This is guidance, not a deterministic boundary — which is why the CAPTCHA case has a mechanical guard above it. The sibling port [measured the difference](https://github.com/laihenyi/pi-Jev-browser): against a real reCAPTCHA the model stopped, but only because that widget lives in an iframe the loop cannot see; given the same gate as plain DOM controls it clicked through and reported success.
|
|
88
90
|
3. **Agent instructions.** The tool guidelines tell pi to treat page content as untrusted, to ask before consequential actions, to never type secrets, and to verify `done_unverified` independently. These are instructions to another model, not guarantees.
|
|
89
91
|
|
|
90
92
|
Page text, visible field values, and the goal are sent to the decision model — the TypeSafe API under policy `typesafe`, your configured provider under policy `pi` — and field values are sent to pi's model to be filled. Password and file fields are excluded, but other sensitive content is **not** automatically redacted. Delegate only narrowly scoped tasks.
|
|
@@ -99,7 +101,9 @@ Page text, visible field values, and the goal are sent to the decision model —
|
|
|
99
101
|
- **Unbenchmarked.** End-to-end speed and live-model reliability have not been measured.
|
|
100
102
|
- **Memory is narrow.** The last ten actions are retained in memory per goal within one browser session, and are dropped when the goal changes. Nothing is persisted.
|
|
101
103
|
|
|
102
|
-
The loop retries only reads invalidated by a document replacement, up to five times. A browser mutation is **never** retried: a failed run may still have applied an action, so inspect the page before continuing.
|
|
104
|
+
The loop retries only reads invalidated by a document replacement, up to five times. A browser mutation is **never** retried: a failed run may still have applied an action, so inspect the page before continuing.
|
|
105
|
+
|
|
106
|
+
Two progress guards end a run as `blocked`. Three non-wait actions without observable progress is the upstream one. The second catches longer cycles, which that guard cannot see: an action that returns the page to a state this run has already visited is counted, and **discovering any new state resets the count**. Three repeats in a row without meeting anything new means the run is going in circles rather than sweeping through pages, and it stops there instead of spending the step budget. A sweep over several items keeps producing unseen states, so it is never cut short.
|
|
103
107
|
|
|
104
108
|
## Porting notes
|
|
105
109
|
|
|
@@ -113,6 +117,8 @@ Ported from [`cline/plugins` → `plugins/jev-browser`](https://github.com/cline
|
|
|
113
117
|
- **Dependencies.** `zod` was unused and was dropped. Upstream reached Jev through Vercel AI Gateway with `ai` and `@ai-sdk/gateway`; both are gone, because TypeSafe's own API takes the same `state` + `questions` body the loop already builds. The validation those packages provided moved into `parseChoiceAnswer`, narrowed to what the loop actually needs: only an answer that names no offered option is fatal, because a doubtful probability distribution must never kill a run that has already clicked things. Its own value is reported when it is a usable number and marked unknown otherwise.
|
|
114
118
|
- **Throttling is named, and failures explain themselves.** Upstream reported any evaluation failure as an unexplained interruption. HTTP 429 and 529 now carry their own failure categories, `rate_limited` and `overloaded`, and every failure records a bounded single-line `detail`. That last part is not cosmetic: an unexplained four-step failure is what prompted this change, and the detail line is what makes the next one diagnosable.
|
|
115
119
|
- **Input validation.** `jev_actions` now validates every action in the batch before executing any of it, so a malformed action can no longer leave earlier actions half-applied.
|
|
120
|
+
- **A mechanical gate for human-verification challenges.** Upstream left CAPTCHAs to the `REVIEW` instruction, and a decision model can talk itself past that. A page that both announces a gate and offers a control that would pass it now ends the run as `needs_review` before the policy is consulted, with a test asserting the policy is never called. Both conditions are required, so an article about CAPTCHAs and a plain "Verify email" button are not gates. Adapted from the Apache-2.0 [`pi-Jev-browser`](https://github.com/laihenyi/pi-Jev-browser) by laihengyi, which measured the failure.
|
|
121
|
+
- **Cycle detection.** Upstream stopped after three actions with no observable progress, which a longer cycle survives: clicking between known pages always changes the page. Revisiting an already-seen state now counts towards a stop of its own, and a new state resets the counter so a sweep over several items is not mistaken for a loop. Measured against a real 20-step oscillation, this stops it around step 7.
|
|
116
122
|
- **Pluggable decision policy.** The decision step became the `JevPolicy` seam the upstream interface hinted at: `policy: "pi"` answers it with the model pi already has configured, so the loop needs no second credential, while `policy: "typesafe"` calls the TypeSafe API directly. Vercel AI Gateway is no longer a dependency of any kind.
|
|
117
123
|
- **Verification without vision.** Upstream handed back screenshots, so a text-only model could not check a `done_unverified` claim at all. Runs now also return the stopped page's URL, title, and visible-text excerpt, produced by the same observation layer, and the image is attached only when the model declares image input. Measured on one goal: the payload dropped from 1.38 MB to 164 KB with a text-only model, while the agent's verification went from "claim is unverified" to naming the book title and price.
|
|
118
124
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yibie/pi-jev-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Isolated Playwright browser for pi, driven by Jev typed decisions through the TypeSafe API or the model pi already has configured.",
|
|
5
5
|
"author": "yibie",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"pi": {
|
|
23
23
|
"extensions": [
|
|
24
24
|
"./extensions/jev-browser.ts"
|
|
25
|
-
]
|
|
25
|
+
],
|
|
26
|
+
"video": "https://cdn.jsdelivr.net/gh/yibie/pi-jev-browser@main/docs/demo.mp4"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"extensions",
|
package/src/gate.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Observation } from "./jev-browser.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Human-verification gates are refused before the policy is consulted.
|
|
5
|
+
*
|
|
6
|
+
* The `REVIEW` instruction already tells the decision layer to stop at a CAPTCHA,
|
|
7
|
+
* and on a real reCAPTCHA demo it does — but only because that widget lives in an
|
|
8
|
+
* iframe the loop cannot see. Given the same gate as plain DOM controls, it
|
|
9
|
+
* clicked through and reported success. Whether to complete a challenge that
|
|
10
|
+
* exists to keep automation out is not a judgement call to leave to a model, so
|
|
11
|
+
* the loop makes it mechanically instead.
|
|
12
|
+
*
|
|
13
|
+
* Adapted from the Apache-2.0 `pi-Jev-browser` project by laihengyi, which
|
|
14
|
+
* measured that failure mode and wrote these phrases.
|
|
15
|
+
*/
|
|
16
|
+
const GATE_TEXT =
|
|
17
|
+
/\b(?:captcha|recaptcha|hcaptcha|turnstile|human verification|verification (?:challenge|required)|verify (?:that )?you(?:'re| are) (?:a )?(?:human|person)|are you (?:a )?(?:human|robot)|i(?:'m| am) not a robot|not a robot|confirm (?:that )?you(?:'re| are) (?:a )?(?:human|person)|prove (?:that )?you(?:'re| are) (?:not a robot|(?:a )?human)|press and hold|slide to verify|checking your browser)\b/i;
|
|
18
|
+
|
|
19
|
+
const GATE_TARGET =
|
|
20
|
+
/verif|robot|human|person|captcha|challenge|press and hold|slide|continue|i am|i'm/i;
|
|
21
|
+
|
|
22
|
+
/** A control that leaves the gate rather than passes it is not evidence of one. */
|
|
23
|
+
const NOT_A_GATE_TARGET =
|
|
24
|
+
/^(?:close|dismiss|cancel|back|go back|help|learn more|privacy|terms)$/i;
|
|
25
|
+
|
|
26
|
+
export interface VerificationGate {
|
|
27
|
+
/** The phrase that announced the gate, as it appeared. */
|
|
28
|
+
phrase: string;
|
|
29
|
+
/** The label of the control that would pass it. */
|
|
30
|
+
target: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Both conditions are required. Text alone would stop on an article about
|
|
35
|
+
* CAPTCHAs; a control alone would stop on every "Verify email" button. The cost
|
|
36
|
+
* of a false positive is a handoff to the user, never a wrong action, so the
|
|
37
|
+
* phrases lean towards recall within that constraint.
|
|
38
|
+
*/
|
|
39
|
+
export function verificationGate(
|
|
40
|
+
observation: Observation,
|
|
41
|
+
): VerificationGate | undefined {
|
|
42
|
+
const phrase = GATE_TEXT.exec(
|
|
43
|
+
`${observation.title}\n${observation.text}`,
|
|
44
|
+
)?.[0];
|
|
45
|
+
if (!phrase) return undefined;
|
|
46
|
+
const target = observation.targets.find(
|
|
47
|
+
(candidate) =>
|
|
48
|
+
GATE_TARGET.test(candidate.label) &&
|
|
49
|
+
!NOT_A_GATE_TARGET.test(candidate.label.trim()),
|
|
50
|
+
);
|
|
51
|
+
if (!target) return undefined;
|
|
52
|
+
return { phrase, target: target.label };
|
|
53
|
+
}
|
package/src/jev-run.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
StaleObservationError,
|
|
7
7
|
} from "./jev-browser.ts";
|
|
8
8
|
import type { JevPolicy } from "./jev-model.ts";
|
|
9
|
+
import { verificationGate } from "./gate.ts";
|
|
9
10
|
|
|
10
11
|
export interface RunInput {
|
|
11
12
|
goal: string;
|
|
@@ -40,8 +41,19 @@ export interface ActionHistory {
|
|
|
40
41
|
export interface RunMemory {
|
|
41
42
|
goal: string;
|
|
42
43
|
actions: ActionHistory[];
|
|
44
|
+
/** Observation keys this run has already visited, newest last. */
|
|
45
|
+
states?: string[];
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* A cycle needs a repeat: a loop that returns to a page it already visited three
|
|
50
|
+
* times in a row is not making progress, while a sweep over several items keeps
|
|
51
|
+
* finding states it has never seen.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_STATE_REVISITS = 3;
|
|
54
|
+
/** Bounded, so a long run forgets the oldest states instead of growing forever. */
|
|
55
|
+
const MAX_VISITED_STATES = 12;
|
|
56
|
+
|
|
45
57
|
export async function runJev(
|
|
46
58
|
input: RunInput,
|
|
47
59
|
options: {
|
|
@@ -81,8 +93,10 @@ export async function runJev(
|
|
|
81
93
|
if (memory.goal !== input.goal) {
|
|
82
94
|
memory.goal = input.goal;
|
|
83
95
|
memory.actions = [];
|
|
96
|
+
memory.states = [];
|
|
84
97
|
}
|
|
85
98
|
let executed = 0;
|
|
99
|
+
let revisits = 0;
|
|
86
100
|
let stage = "observation";
|
|
87
101
|
const textCache = new Map<string, string>();
|
|
88
102
|
const started = performance.now();
|
|
@@ -106,6 +120,22 @@ export async function runJev(
|
|
|
106
120
|
const page = options.page();
|
|
107
121
|
const snapshot = await observe(page, signal);
|
|
108
122
|
try {
|
|
123
|
+
// Mechanical refusal, before the policy can choose to click through it.
|
|
124
|
+
const gate = verificationGate(snapshot.data);
|
|
125
|
+
if (gate) {
|
|
126
|
+
await options.onStep?.({
|
|
127
|
+
step,
|
|
128
|
+
operation: "REVIEW",
|
|
129
|
+
target: gate.target,
|
|
130
|
+
status: "decision",
|
|
131
|
+
reason: "verification_gate",
|
|
132
|
+
latencyMs: 0,
|
|
133
|
+
});
|
|
134
|
+
return finish(
|
|
135
|
+
"needs_review",
|
|
136
|
+
`The page presents a human-verification gate (${JSON.stringify(gate.phrase)}, control ${JSON.stringify(gate.target)}). This loop does not complete verification challenges; hand this step to the user.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
109
139
|
const decisionStarted = performance.now();
|
|
110
140
|
stage = "evaluation";
|
|
111
141
|
const decision = await policy.choose(
|
|
@@ -221,12 +251,12 @@ export async function runJev(
|
|
|
221
251
|
stage = "post_action_observation";
|
|
222
252
|
const after = await observe(options.page(), signal);
|
|
223
253
|
try {
|
|
254
|
+
const stateKey = JSON.stringify(after.data);
|
|
224
255
|
memory.actions.push({
|
|
225
256
|
action: decision.target?.label ?? decision.operation,
|
|
226
257
|
kind: decision.operation,
|
|
227
258
|
text,
|
|
228
|
-
page_changed:
|
|
229
|
-
JSON.stringify(after.data) !== JSON.stringify(snapshot.data),
|
|
259
|
+
page_changed: stateKey !== JSON.stringify(snapshot.data),
|
|
230
260
|
});
|
|
231
261
|
memory.actions.splice(0, Math.max(0, memory.actions.length - 10));
|
|
232
262
|
const recent = memory.actions
|
|
@@ -237,6 +267,21 @@ export async function runJev(
|
|
|
237
267
|
"blocked",
|
|
238
268
|
"Three actions produced no observable progress.",
|
|
239
269
|
);
|
|
270
|
+
// That check only sees consecutive actions that change nothing, so a longer
|
|
271
|
+
// cycle — click A, click B, click A, … — passes it every time: each action
|
|
272
|
+
// does change the page, it just returns to a page already seen.
|
|
273
|
+
const visited = (memory.states ??= []);
|
|
274
|
+
if (visited.includes(stateKey)) revisits++;
|
|
275
|
+
else {
|
|
276
|
+
revisits = 0;
|
|
277
|
+
visited.push(stateKey);
|
|
278
|
+
if (visited.length > MAX_VISITED_STATES) visited.shift();
|
|
279
|
+
}
|
|
280
|
+
if (revisits >= MAX_STATE_REVISITS)
|
|
281
|
+
return finish(
|
|
282
|
+
"blocked",
|
|
283
|
+
`The run came back to a page state it had already visited ${revisits} times in a row without meeting anything new. The goal does not look reachable through the offered actions; continue with jev_actions if it is still worth pursuing.`,
|
|
284
|
+
);
|
|
240
285
|
} finally {
|
|
241
286
|
await after.dispose().catch(() => undefined);
|
|
242
287
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
import { verificationGate } from "../src/gate.ts";
|
|
5
|
+
import type { Observation } from "../src/jev-browser.ts";
|
|
6
|
+
import { type RunStep, runJev } from "../src/jev-run.ts";
|
|
7
|
+
|
|
8
|
+
const base: Observation = {
|
|
9
|
+
url: "https://example.test",
|
|
10
|
+
title: "Example",
|
|
11
|
+
text: "Nothing to see",
|
|
12
|
+
targets: [{ id: "1", operation: "CLICK", label: "Continue", value: "" }],
|
|
13
|
+
scrollUp: false,
|
|
14
|
+
scrollDown: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
test("a verification gate needs the announcement and a control that passes it", () => {
|
|
18
|
+
const gate = (over: Partial<Observation>) =>
|
|
19
|
+
verificationGate({
|
|
20
|
+
...base,
|
|
21
|
+
...over,
|
|
22
|
+
targets: over.targets ?? base.targets,
|
|
23
|
+
});
|
|
24
|
+
// Both present: refused.
|
|
25
|
+
assert.deepEqual(
|
|
26
|
+
gate({
|
|
27
|
+
text: "Please verify that you are human",
|
|
28
|
+
targets: [{ id: "1", operation: "CLICK", label: "Verify", value: "" }],
|
|
29
|
+
}),
|
|
30
|
+
{ phrase: "verify that you are human", target: "Verify" },
|
|
31
|
+
);
|
|
32
|
+
assert.equal(
|
|
33
|
+
gate({
|
|
34
|
+
title: "Checking your browser",
|
|
35
|
+
targets: [
|
|
36
|
+
{ id: "1", operation: "CLICK", label: "I am not a robot", value: "" },
|
|
37
|
+
],
|
|
38
|
+
})?.target,
|
|
39
|
+
"I am not a robot",
|
|
40
|
+
);
|
|
41
|
+
// A widget that lives in an iframe is invisible to the observation, so the
|
|
42
|
+
// control is the only thing that can distinguish a gate from prose in that case.
|
|
43
|
+
assert.equal(
|
|
44
|
+
gate({
|
|
45
|
+
text: "We use a CAPTCHA to keep bots out",
|
|
46
|
+
targets: [{ id: "1", operation: "CLICK", label: "Home", value: "" }],
|
|
47
|
+
}),
|
|
48
|
+
undefined,
|
|
49
|
+
);
|
|
50
|
+
assert.equal(
|
|
51
|
+
gate({
|
|
52
|
+
targets: [{ id: "1", operation: "CLICK", label: "Verify email", value: "" }],
|
|
53
|
+
}),
|
|
54
|
+
undefined,
|
|
55
|
+
);
|
|
56
|
+
// Controls that leave the gate are not evidence of one.
|
|
57
|
+
assert.equal(
|
|
58
|
+
gate({
|
|
59
|
+
text: "captcha",
|
|
60
|
+
targets: [{ id: "1", operation: "CLICK", label: "Dismiss", value: "" }],
|
|
61
|
+
}),
|
|
62
|
+
undefined,
|
|
63
|
+
);
|
|
64
|
+
assert.equal(gate({}), undefined);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("the gate stops the run before the policy is consulted", async () => {
|
|
68
|
+
const browser = await chromium.launch({ headless: true });
|
|
69
|
+
try {
|
|
70
|
+
const page = await browser.newPage();
|
|
71
|
+
await page.setContent(
|
|
72
|
+
"<h1>Verify that you are human</h1><button>Continue</button>",
|
|
73
|
+
);
|
|
74
|
+
let policyCalls = 0;
|
|
75
|
+
const steps: RunStep[] = [];
|
|
76
|
+
const result = await runJev(
|
|
77
|
+
{ goal: "Read the page" },
|
|
78
|
+
{
|
|
79
|
+
page: () => page,
|
|
80
|
+
onStep: async (step) => {
|
|
81
|
+
steps.push(step);
|
|
82
|
+
},
|
|
83
|
+
policy: {
|
|
84
|
+
async choose(observation) {
|
|
85
|
+
policyCalls++;
|
|
86
|
+
return {
|
|
87
|
+
operation: "CLICK",
|
|
88
|
+
probability: 1,
|
|
89
|
+
target: observation.targets.find((t) => t.label === "Continue"),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
async text() {
|
|
93
|
+
return "unused";
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
assert.equal(result.status, "needs_review");
|
|
99
|
+
assert.equal(policyCalls, 0, "the policy must never see this page");
|
|
100
|
+
assert.equal(result.steps.length, 0);
|
|
101
|
+
assert.equal(steps.at(-1)?.reason, "verification_gate");
|
|
102
|
+
assert.match(result.message, /does not complete verification challenges/);
|
|
103
|
+
} finally {
|
|
104
|
+
await browser.close();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a cycle between known states is stopped, a sweep over new ones is not", async () => {
|
|
109
|
+
const browser = await chromium.launch({ headless: true });
|
|
110
|
+
try {
|
|
111
|
+
const page = await browser.newPage();
|
|
112
|
+
// Two controls whose result is a state the run can come back to.
|
|
113
|
+
await page.setContent(
|
|
114
|
+
`<button onclick="document.body.dataset.at='a'">Alpha</button><button onclick="document.body.dataset.at='b'">Beta</button><p id="marker">start</p>`,
|
|
115
|
+
);
|
|
116
|
+
const clickLabel = (observation: Observation, label: string) => ({
|
|
117
|
+
operation: "CLICK",
|
|
118
|
+
probability: 1,
|
|
119
|
+
target: observation.targets.find((t) => t.label === label),
|
|
120
|
+
});
|
|
121
|
+
let turn = 0;
|
|
122
|
+
const cycle = await runJev(
|
|
123
|
+
{ goal: "Do the thing", maxSteps: 20 },
|
|
124
|
+
{
|
|
125
|
+
page: () => page,
|
|
126
|
+
policy: {
|
|
127
|
+
async choose(observation) {
|
|
128
|
+
turn++;
|
|
129
|
+
await page.evaluate(
|
|
130
|
+
(state) => {
|
|
131
|
+
document.getElementById("marker")!.textContent = state;
|
|
132
|
+
},
|
|
133
|
+
turn % 2 === 0 ? "alpha" : "beta",
|
|
134
|
+
);
|
|
135
|
+
return clickLabel(observation, "Alpha");
|
|
136
|
+
},
|
|
137
|
+
async text() {
|
|
138
|
+
return "unused";
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
assert.equal(cycle.status, "blocked");
|
|
144
|
+
assert.match(cycle.message, /already visited/);
|
|
145
|
+
assert.ok(
|
|
146
|
+
cycle.steps.length < 10,
|
|
147
|
+
`stopped early, took ${cycle.steps.length} of 20 steps`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// A policy that keeps producing states it has never seen is a sweep, not a loop.
|
|
151
|
+
await page.setContent("<button>Next</button><p id=\"n\">0</p>");
|
|
152
|
+
let tick = 0;
|
|
153
|
+
const sweep = await runJev(
|
|
154
|
+
{ goal: "Walk through the pages", maxSteps: 20 },
|
|
155
|
+
{
|
|
156
|
+
page: () => page,
|
|
157
|
+
policy: {
|
|
158
|
+
async choose(observation) {
|
|
159
|
+
tick++;
|
|
160
|
+
if (tick > 5) return { operation: "DONE", probability: 1 };
|
|
161
|
+
await page.evaluate(
|
|
162
|
+
(value) => {
|
|
163
|
+
document.getElementById("n")!.textContent = String(value);
|
|
164
|
+
},
|
|
165
|
+
tick,
|
|
166
|
+
);
|
|
167
|
+
return clickLabel(observation, "Next");
|
|
168
|
+
},
|
|
169
|
+
async text() {
|
|
170
|
+
return "unused";
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
assert.equal(sweep.status, "done_unverified");
|
|
176
|
+
assert.equal(sweep.failure, undefined);
|
|
177
|
+
assert.equal(sweep.steps.filter((s) => s.status === "executed").length, 5);
|
|
178
|
+
} finally {
|
|
179
|
+
await browser.close();
|
|
180
|
+
}
|
|
181
|
+
});
|