personal-agent-benchmark 0.0.0-stage → 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 +128 -2
- package/bin/pab.js +5 -0
- package/dist/src/browser.d.ts +29 -0
- package/dist/src/browser.js +37 -0
- package/dist/src/browser.js.map +1 -0
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +331 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/archive.d.ts +49 -0
- package/dist/src/core/archive.js +213 -0
- package/dist/src/core/archive.js.map +1 -0
- package/dist/src/core/audit.d.ts +15 -0
- package/dist/src/core/audit.js +85 -0
- package/dist/src/core/audit.js.map +1 -0
- package/dist/src/core/bundle.d.ts +42 -0
- package/dist/src/core/bundle.js +80 -0
- package/dist/src/core/bundle.js.map +1 -0
- package/dist/src/core/lookup.d.ts +15 -0
- package/dist/src/core/lookup.js +82 -0
- package/dist/src/core/lookup.js.map +1 -0
- package/dist/src/core/normalize.d.ts +45 -0
- package/dist/src/core/normalize.js +143 -0
- package/dist/src/core/normalize.js.map +1 -0
- package/dist/src/core/rules.d.ts +38 -0
- package/dist/src/core/rules.js +187 -0
- package/dist/src/core/rules.js.map +1 -0
- package/dist/src/core/store.d.ts +26 -0
- package/dist/src/core/store.js +78 -0
- package/dist/src/core/store.js.map +1 -0
- package/dist/src/core/types.d.ts +125 -0
- package/dist/src/core/types.js +3 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/core/util.d.ts +21 -0
- package/dist/src/core/util.js +150 -0
- package/dist/src/core/util.js.map +1 -0
- package/dist/src/core/warc.d.ts +55 -0
- package/dist/src/core/warc.js +258 -0
- package/dist/src/core/warc.js.map +1 -0
- package/dist/src/core/writer.d.ts +26 -0
- package/dist/src/core/writer.js +119 -0
- package/dist/src/core/writer.js.map +1 -0
- package/dist/src/grading.d.ts +63 -0
- package/dist/src/grading.js +173 -0
- package/dist/src/grading.js.map +1 -0
- package/dist/src/judge.d.ts +25 -0
- package/dist/src/judge.js +119 -0
- package/dist/src/judge.js.map +1 -0
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +40 -0
- package/dist/src/paths.js.map +1 -0
- package/dist/src/replay/clock.d.ts +19 -0
- package/dist/src/replay/clock.js +60 -0
- package/dist/src/replay/clock.js.map +1 -0
- package/dist/src/replay/engine.d.ts +53 -0
- package/dist/src/replay/engine.js +280 -0
- package/dist/src/replay/engine.js.map +1 -0
- package/dist/src/replay/launcher.d.ts +36 -0
- package/dist/src/replay/launcher.js +163 -0
- package/dist/src/replay/launcher.js.map +1 -0
- package/dist/src/replay/whistle-plugin.d.ts +24 -0
- package/dist/src/replay/whistle-plugin.js +82 -0
- package/dist/src/replay/whistle-plugin.js.map +1 -0
- package/dist/src/report.d.ts +3 -0
- package/dist/src/report.js +143 -0
- package/dist/src/report.js.map +1 -0
- package/dist/src/scoring.d.ts +87 -0
- package/dist/src/scoring.js +232 -0
- package/dist/src/scoring.js.map +1 -0
- package/dist/src/tasks.d.ts +61 -0
- package/dist/src/tasks.js +152 -0
- package/dist/src/tasks.js.map +1 -0
- package/docs/bundle-format.md +114 -0
- package/docs/deploy.md +78 -0
- package/docs/grading.md +81 -0
- package/docs/site-rules.md +114 -0
- package/docs/tasks.md +63 -0
- package/package.json +121 -4
- package/plugins/whistle.bench-replay/index.js +1 -0
- package/plugins/whistle.bench-replay/package.json +7 -0
- package/sites/_default.yaml +80 -0
- package/sites/amazon.com.yaml +125 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.Task = exports.Repo = void 0;
|
|
40
|
+
exports.resolveTarget = resolveTarget;
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
44
|
+
const archive_1 = require("./core/archive");
|
|
45
|
+
const paths_1 = require("./paths");
|
|
46
|
+
class Repo {
|
|
47
|
+
root;
|
|
48
|
+
constructor(root) {
|
|
49
|
+
this.root = root;
|
|
50
|
+
}
|
|
51
|
+
/** $PAB_REPO, or the checkout this code runs from. */
|
|
52
|
+
static default() {
|
|
53
|
+
return new Repo(path.resolve(process.env.PAB_REPO ?? paths_1.PACKAGE_ROOT));
|
|
54
|
+
}
|
|
55
|
+
get sitesDir() { return path.join(this.root, 'sites'); }
|
|
56
|
+
get tasksDir() { return path.join(this.root, 'tasks'); }
|
|
57
|
+
/** Unpacked bundles and replay logs ($PAB_CACHE, default .cache/ in the repository). */
|
|
58
|
+
get cacheDir() { return path.resolve(process.env.PAB_CACHE ?? path.join(this.root, '.cache')); }
|
|
59
|
+
taskIds() {
|
|
60
|
+
if (!fs.existsSync(this.tasksDir))
|
|
61
|
+
return [];
|
|
62
|
+
return fs
|
|
63
|
+
.readdirSync(this.tasksDir)
|
|
64
|
+
.filter((name) => fs.existsSync(path.join(this.tasksDir, name, 'task.yaml')))
|
|
65
|
+
.sort();
|
|
66
|
+
}
|
|
67
|
+
task(id) {
|
|
68
|
+
const dir = path.join(this.tasksDir, id);
|
|
69
|
+
if (!fs.existsSync(path.join(dir, 'task.yaml')))
|
|
70
|
+
throw new Error(`no task "${id}" in ${this.tasksDir}`);
|
|
71
|
+
return new Task(this, dir);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.Repo = Repo;
|
|
75
|
+
class Task {
|
|
76
|
+
repo;
|
|
77
|
+
dir;
|
|
78
|
+
constructor(repo, dir) {
|
|
79
|
+
this.repo = repo;
|
|
80
|
+
this.dir = dir;
|
|
81
|
+
}
|
|
82
|
+
get id() { return path.basename(this.dir); }
|
|
83
|
+
get cardPath() { return path.join(this.dir, 'task.yaml'); }
|
|
84
|
+
get recordingDir() { return path.join(this.dir, 'recording'); }
|
|
85
|
+
get rulesDir() { return path.join(this.dir, 'rules'); }
|
|
86
|
+
/** grading.yaml: how answers are scored (see grading.ts). */
|
|
87
|
+
get gradingPath() { return path.join(this.dir, 'grading.yaml'); }
|
|
88
|
+
card() {
|
|
89
|
+
const card = yaml_1.default.parse(fs.readFileSync(this.cardPath, 'utf8'));
|
|
90
|
+
return { ...card, id: card?.id ?? this.id };
|
|
91
|
+
}
|
|
92
|
+
/** Files of the recording; empty when the task has none yet. */
|
|
93
|
+
recordingFiles() {
|
|
94
|
+
return (0, archive_1.recordingFiles)(this.recordingDir);
|
|
95
|
+
}
|
|
96
|
+
/** Unpacks the recording into the cache (once per recording content) and returns its directory. */
|
|
97
|
+
async bundleDir() {
|
|
98
|
+
const files = this.recordingFiles();
|
|
99
|
+
if (!files.length)
|
|
100
|
+
throw new Error(`task ${this.id} has no recording (${this.recordingDir})`);
|
|
101
|
+
const digest = (0, archive_1.hashParts)(files).slice(0, 16);
|
|
102
|
+
const bundlesDir = path.join(this.repo.cacheDir, 'bundles');
|
|
103
|
+
const dest = path.join(bundlesDir, `${this.id}-${digest}`);
|
|
104
|
+
if (fs.existsSync(path.join(dest, 'manifest.json')))
|
|
105
|
+
return dest;
|
|
106
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
107
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
108
|
+
try {
|
|
109
|
+
await (0, archive_1.unpackRecording)(this.recordingDir, tmp);
|
|
110
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
111
|
+
fs.renameSync(tmp, dest);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
// older unpacked versions of this task
|
|
118
|
+
for (const name of fs.readdirSync(bundlesDir)) {
|
|
119
|
+
if (name.startsWith(`${this.id}-`) && path.join(bundlesDir, name) !== dest && !name.includes('.tmp-')) {
|
|
120
|
+
fs.rmSync(path.join(bundlesDir, name), { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return dest;
|
|
124
|
+
}
|
|
125
|
+
async target() {
|
|
126
|
+
return {
|
|
127
|
+
name: this.id,
|
|
128
|
+
bundleDir: await this.bundleDir(),
|
|
129
|
+
rulesDirs: [this.repo.sitesDir, this.rulesDir],
|
|
130
|
+
task: this,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.Task = Task;
|
|
135
|
+
/**
|
|
136
|
+
* Resolves a command-line argument to a replay target: a task id, a task directory, or a bundle
|
|
137
|
+
* directory (one with manifest.json, replayed with its own rules/).
|
|
138
|
+
*/
|
|
139
|
+
async function resolveTarget(arg, repo = Repo.default()) {
|
|
140
|
+
const asPath = path.resolve(arg);
|
|
141
|
+
if (fs.existsSync(path.join(asPath, 'manifest.json'))) {
|
|
142
|
+
return { name: path.basename(asPath), bundleDir: asPath, rulesDirs: [] };
|
|
143
|
+
}
|
|
144
|
+
if (fs.existsSync(path.join(asPath, 'task.yaml'))) {
|
|
145
|
+
// a task inside a repository uses that repository's sites/; a lone task directory uses ours
|
|
146
|
+
const parent = path.dirname(asPath);
|
|
147
|
+
const owner = path.basename(parent) === 'tasks' && fs.existsSync(path.join(parent, '..', 'sites')) ? new Repo(path.dirname(parent)) : repo;
|
|
148
|
+
return new Task(owner, asPath).target();
|
|
149
|
+
}
|
|
150
|
+
return repo.task(arg).target();
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=tasks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../src/tasks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Bundle format
|
|
2
|
+
|
|
3
|
+
A bundle is one recording of a task (or several merged into one). It holds only what replay
|
|
4
|
+
reads. In this repository it travels as `tasks/<id>/recording/` (the published layout, below),
|
|
5
|
+
which `pab` unpacks into `.cache/bundles/<id>-<hash>/` on first use.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
manifest.json the few facts replay needs (below)
|
|
9
|
+
archive/data.warc every HTTP exchange (WARC 1.1, uncompressed records)
|
|
10
|
+
indexes/entries.jsonl replay index: one line per exchange, with the request body
|
|
11
|
+
pages/pages.jsonl top-level pages visited while recording
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Web-archive tools such as ReplayWeb.page read WACZ files, which also carry a CDXJ index and a
|
|
15
|
+
`datapackage.json`. Replay does not use them, so bundles do not store them; the recording tool
|
|
16
|
+
builds them when it exports a WACZ. Bundles written before this change still contain both files
|
|
17
|
+
and unpack as before.
|
|
18
|
+
|
|
19
|
+
## manifest.json
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"task_id": "amazon-airpods",
|
|
24
|
+
"captured_from": "2026-10-22T10:40:57.458Z",
|
|
25
|
+
"captured_to": "2026-10-22T12:50:41.949Z",
|
|
26
|
+
"not_found": { "amazon": "https://www.amazon.com/rr-not-found-mgxy1k2a" },
|
|
27
|
+
"locale": "en-US",
|
|
28
|
+
"timezone": "America/New_York"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Field | Used for |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `task_id` | which `tasks/<id>/` the recording belongs to |
|
|
35
|
+
| `captured_from` | page clocks start here during replay |
|
|
36
|
+
| `captured_to` | with `captured_from`, tells merged recordings apart |
|
|
37
|
+
| `not_found` | each site's own 404 page, answered for documents replay cannot find |
|
|
38
|
+
| `locale`, `timezone` | the replay browser's language and time zone |
|
|
39
|
+
| `format` | layout version of the published recording (below); only in `recording/manifest.json` |
|
|
40
|
+
|
|
41
|
+
Nothing else is recorded: not who recorded it, where from, with which browser, or request
|
|
42
|
+
counts. `pab audit` fails a manifest with any other field. Recordings made before 2026-10-26
|
|
43
|
+
also have `start_url`; replay ignores it, the audit lets it pass, and the next import of the
|
|
44
|
+
task drops it. Replay opens the first page opened while recording unless given `--url`.
|
|
45
|
+
|
|
46
|
+
Rules are not part of the published bundle; they live next to it in the task
|
|
47
|
+
directory, so they can be reviewed and changed without touching the recording.
|
|
48
|
+
|
|
49
|
+
## Published layout (format 2)
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
tasks/<id>/recording/
|
|
53
|
+
manifest.json the manifest above, plus "format": 2
|
|
54
|
+
pages.jsonl pages/pages.jsonl
|
|
55
|
+
entries.jsonl.gz indexes/entries.jsonl, gzip-compressed
|
|
56
|
+
warc.tar.gz archive/data.warc in a gzip-compressed tar
|
|
57
|
+
(warc.tar.gz.000, .001, ... when larger than 45 MB)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The WARC is almost all of a recording (the Sony task: 74 MB of 78 MB) and follows the WARC
|
|
61
|
+
standard, so it is stored apart from the small files whose layout is ours. The tar is written
|
|
62
|
+
with fixed timestamps, so the same WARC always gives the same bytes. WARC records are stored
|
|
63
|
+
uncompressed inside it: one gzip stream over the whole file compresses far better than
|
|
64
|
+
per-record gzip (the Amazon task: 33 MB instead of 39 MB). Archives larger than 45 MB are split
|
|
65
|
+
into numbered parts (GitHub warns above 50 MB and refuses 100 MB) and read back in order.
|
|
66
|
+
Extraction accepts only `archive/data.warc`, and only as a regular file.
|
|
67
|
+
|
|
68
|
+
### Changing the format
|
|
69
|
+
|
|
70
|
+
`format` is the contract between this repository and the tool that writes recordings into it.
|
|
71
|
+
Replay reads exactly one format, `RECORDING_FORMAT` in `src/core/archive.ts`, and refuses any
|
|
72
|
+
other with an error naming both. A change to the layout or to the files in it bumps the number,
|
|
73
|
+
and the same change rewrites every task to the new format with a script, so replay never keeps
|
|
74
|
+
readers for old formats. When only the small files change, the rewrite leaves `warc.tar.gz` as
|
|
75
|
+
it was and git stores nothing new for it.
|
|
76
|
+
|
|
77
|
+
Format 1 was one `bundle.tar.gz` (split the same way) holding all four files; format 2 replaced
|
|
78
|
+
it on 2026-10-26.
|
|
79
|
+
|
|
80
|
+
## WARC records
|
|
81
|
+
|
|
82
|
+
Each exchange is two records: `response` (or `revisit`) first, then `request`, whose
|
|
83
|
+
`WARC-Concurrent-To` points at the response.
|
|
84
|
+
|
|
85
|
+
- Response bodies are stored **decoded**. `Content-Encoding` and `Transfer-Encoding` are removed
|
|
86
|
+
and `Content-Length` matches the stored body; the original encoding is kept in the WARC header
|
|
87
|
+
`RR-Original-Content-Encoding`.
|
|
88
|
+
- Identical payloads of 1 KB or more are stored once; later copies are `revisit` records
|
|
89
|
+
(`WARC-Profile: identical-payload-digest`) and `revisitOf` in `entries.jsonl` points at the
|
|
90
|
+
original.
|
|
91
|
+
- Requests carry no `Cookie` or `Authorization` header; every `Set-Cookie` value is `REDACTED`.
|
|
92
|
+
`pab audit` verifies both.
|
|
93
|
+
- Every header line is a header: a header sent several times is stored once per value. Recordings
|
|
94
|
+
made with record-replay before 0.5.5 stored such a header as one value joined by newlines, so
|
|
95
|
+
each further value is a line of its own; readers take those lines as values of the header on the
|
|
96
|
+
line before, and `pab audit` fails a bundle that has any.
|
|
97
|
+
- Custom WARC headers: `RR-Exchange-Id` (the id in `entries.jsonl`), `RR-Resource-Type`.
|
|
98
|
+
|
|
99
|
+
## entries.jsonl
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{"id":"e000123","ts":"2026-10-22T10:41:05.120Z","method":"GET","url":"https://www.amazon.com/Apple-.../dp/B0FQFB8FMG/ref=sr_1_1?...","status":200,"mime":"text/html","digest":"<sha256>","size":812345,"reqOffset":1234,"reqLength":456,"respOffset":789,"respLength":101112}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
POST bodies are kept base64-encoded in `reqBody`. Matching keys are not stored: the replay engine
|
|
106
|
+
computes them from the current rules when it starts, so changing a rule never requires a new
|
|
107
|
+
recording.
|
|
108
|
+
|
|
109
|
+
## pages.jsonl
|
|
110
|
+
|
|
111
|
+
Pages visited while recording, in order. `fetched` says how the address was reached: `GET` (a
|
|
112
|
+
document load), `POST` (a form result) or `none` (rewritten by page script). `note:
|
|
113
|
+
"not-found-probe"` marks a visit to a path that cannot exist, made to record the site's own 404
|
|
114
|
+
page; replay serves that page for documents it cannot find.
|
package/docs/deploy.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Running replays in containers
|
|
2
|
+
|
|
3
|
+
This repository ships the replay as images and says what a platform running agents against it
|
|
4
|
+
must ensure. How the platform schedules a replay (Pods, Services, network policies) belongs to
|
|
5
|
+
that platform's integration, not here.
|
|
6
|
+
|
|
7
|
+
## Images
|
|
8
|
+
|
|
9
|
+
Two images per task, built on the machine that runs the replay; nothing is pushed to a registry.
|
|
10
|
+
|
|
11
|
+
| Image | What is in it | Built again when |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `pab-replay:<digest>` | the replay service (`deploy/Dockerfile`): whistle, the replay plugin, `sites/` | the code or shared site rules change |
|
|
14
|
+
| `pab-task:<id>-<digest>` | one task directory (`deploy/task/Dockerfile`): recording, `rules/`, `task.yaml`, `grading.yaml` | that task changes |
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
sh deploy/build-images.sh tasks/<id> # builds what is missing, prints both image names
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Tags are digests of what goes into each image, so a run can record exactly which code and which
|
|
21
|
+
recording it used. Old tags are not removed. The task image keeps the recording packed; the
|
|
22
|
+
replay service unpacks it when it starts (about a second for a 75 MB recording, 214 MB
|
|
23
|
+
unpacked).
|
|
24
|
+
|
|
25
|
+
## Running one replay
|
|
26
|
+
|
|
27
|
+
- Run the task image once with a volume at `/out`: it copies the task there and exits.
|
|
28
|
+
- Run the replay image with that volume at `/task`. It serves the proxy on port 8899 and writes
|
|
29
|
+
every request, and how it was answered, to `/logs/replay.jsonl`. That log is what `pab grade`
|
|
30
|
+
scores the run from (docs/grading.md), so keep it after the run.
|
|
31
|
+
- One replay serves one task for one run: start a fresh one per run.
|
|
32
|
+
|
|
33
|
+
## What the platform must ensure
|
|
34
|
+
|
|
35
|
+
The replay itself:
|
|
36
|
+
|
|
37
|
+
- has no network access beyond answering the agent's browser (and whatever reads its log and
|
|
38
|
+
`http://rr.control/stats`). The proxy never opens a connection, but nothing should let it.
|
|
39
|
+
|
|
40
|
+
The agent's browser, so that the recording rather than the internet answers it:
|
|
41
|
+
|
|
42
|
+
1. sends everything through the proxy: `--proxy-server=http://<replay host>:8899`, with no
|
|
43
|
+
bypass list beyond loopback (some browser images add private ranges to `--proxy-bypass-list`
|
|
44
|
+
unless it is given);
|
|
45
|
+
2. turns QUIC off (`--disable-quic`): QUIC runs over UDP and does not go through an HTTP proxy;
|
|
46
|
+
3. accepts the proxy's certificates (`--ignore-certificate-errors`, or trust whistle's root CA):
|
|
47
|
+
the proxy answers HTTPS itself;
|
|
48
|
+
4. can reach nothing but the proxy (and DNS to find it);
|
|
49
|
+
5. starts from a fresh profile each run (no cache or service workers from an earlier run);
|
|
50
|
+
6. preferably uses the recording's time zone and language (`timezone` and `locale` in the
|
|
51
|
+
bundle's `manifest.json`).
|
|
52
|
+
|
|
53
|
+
Page clocks need nothing from the browser (see below). Agent tools that fetch pages without the
|
|
54
|
+
browser (HTTP fetch, search APIs) bypass the proxy: route them through it (`HTTPS_PROXY`) or turn
|
|
55
|
+
them off for replay runs.
|
|
56
|
+
|
|
57
|
+
`deploy/browser/` (`pab-browser`) is Chromium set up this way. It stands in for the agent's
|
|
58
|
+
browser in the local check.
|
|
59
|
+
|
|
60
|
+
## Page clocks
|
|
61
|
+
|
|
62
|
+
The replay service injects a small script at the top of every HTML page that shifts `Date` to the
|
|
63
|
+
recording time (`captured_from` in the manifest) and lets it run on from there; pages with a CSP
|
|
64
|
+
get the script's hash added to `script-src`. `RR_CLOCK=off` turns this off. libfaketime in the
|
|
65
|
+
browser container was tried and makes Chromium's CDP handshake hang; it would also miss agents
|
|
66
|
+
that bring their own browser.
|
|
67
|
+
|
|
68
|
+
## Local check
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm run build
|
|
72
|
+
sh deploy/local-pod-test.sh tasks/<id> <URL> # Docker internal network, no internet
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
It builds the images it lacks, runs the replay and the stand-in browser on a Docker network
|
|
76
|
+
without internet access, drives the browser over CDP from a third container
|
|
77
|
+
(`scripts/pod-smoke.ts`) to open `<URL>` (a page in the recording), and checks that the network
|
|
78
|
+
has no way out.
|
package/docs/grading.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Grading
|
|
2
|
+
|
|
3
|
+
A personal agent answers in text, so a task is scored from its answer with an LLM and from what
|
|
4
|
+
it did in the replay. Each task says how in `tasks/<id>/grading.yaml`, next to `task.yaml`. The
|
|
5
|
+
grading requirements are written by the person who set the task; the recording tool turns them
|
|
6
|
+
into this format when the recording is imported.
|
|
7
|
+
|
|
8
|
+
## Format
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
extract: # fields an LLM extracts from the agent's answer
|
|
12
|
+
price: { type: money, description: lowest price found, without shipping }
|
|
13
|
+
store: { type: text, description: the website selling at that price }
|
|
14
|
+
|
|
15
|
+
require: # compared by the program; any failure scores 0
|
|
16
|
+
- field: price
|
|
17
|
+
equals: 278.00 # numbers: equals, or min/max; tolerance defaults to 0.01
|
|
18
|
+
source: { url: "https://www.bestbuy.com/site/sony-wh1000xm5/6505727.p", text: "$278.00" }
|
|
19
|
+
- field: store
|
|
20
|
+
one_of: ["Best Buy", "bestbuy.com"] # text: equals, one_of or contains; case and spacing ignored
|
|
21
|
+
|
|
22
|
+
checks: # yes/no questions an LLM answers about the answer
|
|
23
|
+
- id: right_product
|
|
24
|
+
question: Is the product in the answer a new, black Sony WH-1000XM5 (not renewed, used or another model)?
|
|
25
|
+
required: true # "no" scores 0; the default when no weight is given
|
|
26
|
+
- id: says_shipping
|
|
27
|
+
question: Does the answer say whether shipping is free?
|
|
28
|
+
weight: 1 # partial credit
|
|
29
|
+
|
|
30
|
+
trace: # requests the agent made in the replay
|
|
31
|
+
- id: no_order
|
|
32
|
+
must_not: { method: POST, url: "*/checkout/place*" }
|
|
33
|
+
- id: added_to_cart
|
|
34
|
+
must: { method: POST, url: "*/cart/add*", hit: true } # hit: answered from the recording
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Field | Types | Tests |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `money`, `number` | numbers | `equals` (with `tolerance`), `min`, `max`, `one_of` |
|
|
40
|
+
| `text` | text | `equals`, `one_of`, `contains` |
|
|
41
|
+
| `boolean` | true / false | `equals` |
|
|
42
|
+
|
|
43
|
+
`source` says where the expected value appears in the recording: a recorded page (or API
|
|
44
|
+
response) and the text shown on it, or the text as it is in the response when page scripts fill
|
|
45
|
+
the value in (for example `"currentPrice":228`). Import checks that the page is in the recording
|
|
46
|
+
and holds that text, so the answer key is something the agent could have read during replay.
|
|
47
|
+
|
|
48
|
+
## Score
|
|
49
|
+
|
|
50
|
+
- Required items are every `require` entry, every check without `weight`, and every `trace`
|
|
51
|
+
entry. If any of them fails, the score is 0.
|
|
52
|
+
- Otherwise the score is the weight of the weighted checks that passed divided by their total
|
|
53
|
+
weight, or 1 when there are none.
|
|
54
|
+
|
|
55
|
+
A grading needs at least one of `require`, `checks` or `trace`.
|
|
56
|
+
|
|
57
|
+
## Run report
|
|
58
|
+
|
|
59
|
+
`pab grade <task> --log <replay-log.jsonl> --answer <file>` scores one run and writes two files
|
|
60
|
+
side by side, every time:
|
|
61
|
+
|
|
62
|
+
- `result.json`: the score, the extracted fields, each item with its result and reason (expected
|
|
63
|
+
and actual values, the LLM's reason, the matching request), the model that graded, the answer,
|
|
64
|
+
and the pages the agent opened.
|
|
65
|
+
- `report.html`: the same, as a page to read, rendered from `result.json`.
|
|
66
|
+
|
|
67
|
+
Pages are what the agent opened in a browser tab (`sec-fetch-dest: document` in the replay log),
|
|
68
|
+
not frames, scripts, styles or API calls. `pages.not_found` lists those that came back 404, one row
|
|
69
|
+
per normalized key with the site, the route, the first URL and how many times it was opened:
|
|
70
|
+
|
|
71
|
+
- `source: miss`: the recording has no copy. Recording the page lets a later run see it.
|
|
72
|
+
- any other source: the site itself answered 404 while recording.
|
|
73
|
+
|
|
74
|
+
Without `--answer`, or for a task without `grading.yaml`, nothing is scored but both files are
|
|
75
|
+
still written with the page list. When the grader fails, the files are written with a note and the
|
|
76
|
+
command exits with 1.
|
|
77
|
+
|
|
78
|
+
The grader is Claude (`claude-opus-5-5`, `--model` to change). With `ANTHROPIC_API_KEY` or
|
|
79
|
+
`ANTHROPIC_AUTH_TOKEN` set it calls the Claude API, where a declined request is re-run on
|
|
80
|
+
Anthropic's recommended fallback model; otherwise it runs a signed-in local `claude` CLI.
|
|
81
|
+
`--judge api|cli` picks one.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Site rules and request matching
|
|
2
|
+
|
|
3
|
+
## The matching key
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
key = method + host and normalized path + kept query parameters (sorted) + normalized body
|
|
7
|
+
e.g. GET www.amazon.com/dp/B0FQFB8FMG
|
|
8
|
+
GET www.amazon.com/s?k=airpods pro
|
|
9
|
+
POST shop.test/cart/add #body:{"ASIN":"B0TEST","quantity":"1"}
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The key ignores the scheme (http and https are the same request). Keys are computed when replay
|
|
13
|
+
starts, from the rules in effect, so a rule change takes effect on the next replay without a new
|
|
14
|
+
recording.
|
|
15
|
+
|
|
16
|
+
## Where rules come from
|
|
17
|
+
|
|
18
|
+
| File | Written by | Applies to |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `sites/_default.yaml` | people | every site: generic tracking parameters, ad and analytics hosts |
|
|
21
|
+
| `sites/<site>.yaml` | people | every task on that site |
|
|
22
|
+
| `tasks/<id>/rules/<site>.auto.yaml` | the recording tool | this task; regenerated with each recording |
|
|
23
|
+
| `tasks/<id>/rules/<site>.yaml` | people | this task |
|
|
24
|
+
|
|
25
|
+
Hand-written rules win over inferred ones; the task's hand-written rules win over `sites/`
|
|
26
|
+
(details in [tasks.md](tasks.md)).
|
|
27
|
+
|
|
28
|
+
### What is inferred
|
|
29
|
+
|
|
30
|
+
| Inferred | From |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Excluded bot-check pages (`exclude`) | visible text or path of a captcha page, e.g. "I am human", `/checkpoint/` |
|
|
33
|
+
| Volatile parameters (`drop_params`, `body_drop`) | values that differ between loads of the same path and look random or leave the response unchanged; timestamps near the recording time |
|
|
34
|
+
| Page identity (`key` + `keep_params: []`) | an ID in the page's `<link rel="canonical">` (Amazon ASIN, numeric IDs, Target `A-…`): title slugs and tracking segments map to `/…/{id}` |
|
|
35
|
+
| Searches (`kind: search`) | common query parameter names (`q`, `k`, `query`, `field-keywords`, ...); suggestions by `prefix` and similar |
|
|
36
|
+
| Trackers (`block`) | near-empty responses on beacon-like paths (`events`, `batch`, `/px/`, `uedata`, ...); hosts that serve nothing else are blocked whole |
|
|
37
|
+
|
|
38
|
+
What people usually write: the occasional fix.
|
|
39
|
+
|
|
40
|
+
Effect on the Amazon task (same recording; self-check plus an agent run):
|
|
41
|
+
|
|
42
|
+
| | generic rules only | + inferred | + hand-written |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| self-check: API requests matched | 52 / 159 | 42 / 42 | 42 / 42 |
|
|
45
|
+
| agent searches with unrecorded wording | 404, no product | finds it, reads $199.00 | same |
|
|
46
|
+
|
|
47
|
+
## Rule file
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
site: amazon # rule name
|
|
51
|
+
hosts: ["amazon.com", "*.amazon.com"]
|
|
52
|
+
drop_params: [ref, qid, sr, "pd_rd_*"] # query parameters ignored when matching (* wildcards)
|
|
53
|
+
body_drop: ["*token*"] # request body fields ignored when matching (recursive)
|
|
54
|
+
block: ["fls-na.amazon.com", "www.amazon.com/1/batch/**"] # answered with 204 (host, or host/path)
|
|
55
|
+
exclude: ["www.amazon.com/checkpoint/**"] # never stored, never replayed
|
|
56
|
+
latest: ["*/gp/cart/*"] # answered with the last recording (carts); _default.yaml covers */cart*, */checkout*
|
|
57
|
+
xhr_not_found: { content_type: application/json, body: "{}" } # optional body for missed API calls
|
|
58
|
+
endpoints:
|
|
59
|
+
- name: product
|
|
60
|
+
match: "GET /*/dp/{asin}/**" # method optional; * one segment, ** any segments, {x} captures one
|
|
61
|
+
key: "/dp/{asin}" # normalized path
|
|
62
|
+
- name: search
|
|
63
|
+
match: "GET /s"
|
|
64
|
+
host: "www.amazon.com" # optional: only this host
|
|
65
|
+
keep_params: [k, page] # only these parameters count (allow-list, beats drop_params)
|
|
66
|
+
kind: search # free text: matched by word-set similarity
|
|
67
|
+
text_param: k
|
|
68
|
+
similarity: 0.6
|
|
69
|
+
- name: add_to_cart
|
|
70
|
+
match: "POST /cart/add-to-cart/**"
|
|
71
|
+
kind: stateful # changes state; a request with a body only matches exactly
|
|
72
|
+
body_keep: [ASIN, quantity] # only these body fields count: another item or quantity misses
|
|
73
|
+
- name: promo
|
|
74
|
+
match: "POST /cart/promo"
|
|
75
|
+
kind: promo # known codes get their recorded answer; unknown codes get the
|
|
76
|
+
text_param: code # answer recorded for invalid_code
|
|
77
|
+
invalid_code: BADCODE
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Other endpoint fields: `fuzzy` (allow same-path matching with different parameters; on by default
|
|
81
|
+
for GET, off for requests with a body) and `fuzzy_min` (share of parameters that must agree,
|
|
82
|
+
default 0.5; 0 for static files).
|
|
83
|
+
|
|
84
|
+
## Lookup order
|
|
85
|
+
|
|
86
|
+
1. `block` rules: 204.
|
|
87
|
+
2. Exact key. When a key was recorded more than once, the earliest copy wins ("browse first,
|
|
88
|
+
act later"), except for pages matching `latest` (carts, checkouts), where the last copy wins:
|
|
89
|
+
that is the state the task ended in.
|
|
90
|
+
3. Free text: `search` finds the most similar recorded search; `promo` as above.
|
|
91
|
+
4. Fuzzy: same method and path, the most query parameters in common (for `latest` pages, the
|
|
92
|
+
last of equally good matches).
|
|
93
|
+
5. Miss:
|
|
94
|
+
- trackers: 204;
|
|
95
|
+
- images, fonts, CSS, JS: 404 with an empty body;
|
|
96
|
+
- documents: the site's own 404 page (recorded during capture), else a plain 404 page;
|
|
97
|
+
- API requests: 404 with `xhr_not_found`, or `{}` when JSON is accepted.
|
|
98
|
+
6. CORS preflights (OPTIONS) that were not recorded are allowed.
|
|
99
|
+
|
|
100
|
+
The request type comes from `Sec-Fetch-Dest`, falling back to `Accept` and the file extension.
|
|
101
|
+
With `--log`, every request is logged as one JSON line: time, method, URL, key, type, source
|
|
102
|
+
(exact/fuzzy/search/promo/blocked/preflight/miss), status, matched record, duration.
|
|
103
|
+
|
|
104
|
+
## Fixing misses
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
node bin/pab.js inspect <task> "<url>" # normalized key, and which record it hits
|
|
108
|
+
node bin/pab.js replay <task> --log replay.jsonl && node bin/pab.js stats replay.jsonl
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- The same path keeps missing with different parameters: add the parameter to `drop_params`.
|
|
112
|
+
- Product URLs carry changing slugs and `ref` segments: map them with `match` + a `key` template.
|
|
113
|
+
- A POST carries a fresh token each time: `body_drop` it, or list the fields that matter in
|
|
114
|
+
`body_keep`.
|
package/docs/tasks.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Tasks
|
|
2
|
+
|
|
3
|
+
One directory per task under `tasks/`:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
tasks/<id>/
|
|
7
|
+
task.yaml the task card
|
|
8
|
+
recording/ the recording (docs/bundle-format.md)
|
|
9
|
+
grading.yaml how answers are scored (docs/grading.md)
|
|
10
|
+
rules/ site rules for this task
|
|
11
|
+
<site>.auto.yaml inferred from the recording; regenerated on every import, do not edit
|
|
12
|
+
<site>.yaml written by people; corrections and additions
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## task.yaml
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
id: amazon-airpods
|
|
19
|
+
title: Buy AirPods Pro 3 on Amazon
|
|
20
|
+
prompt: >- # the instruction given to the agent
|
|
21
|
+
I want Apple AirPods Pro 3 from Amazon, shipped to New York (ZIP 10001). ...
|
|
22
|
+
capture: # the browser's language and time zone while recording;
|
|
23
|
+
locale: en-US # replay keeps them
|
|
24
|
+
timezone: America/New_York
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Which rules apply
|
|
28
|
+
|
|
29
|
+
A task is replayed with `sites/` and then `tasks/<id>/rules/` layered over each other:
|
|
30
|
+
|
|
31
|
+
1. Hand-written files (`<site>.yaml`) win over inferred ones (`<site>.auto.yaml`), wherever they are.
|
|
32
|
+
2. Among hand-written files, the task's own win over `sites/`.
|
|
33
|
+
3. Endpoints of the winning file are tried first and its single values are kept; lists such as
|
|
34
|
+
`drop_params` and `block` are combined.
|
|
35
|
+
|
|
36
|
+
So a correction that only matters for one task goes into `tasks/<id>/rules/<site>.yaml`, and one
|
|
37
|
+
that helps every task on the site goes into `sites/<site>.yaml`. See [site-rules.md](site-rules.md).
|
|
38
|
+
|
|
39
|
+
## How a task is added
|
|
40
|
+
|
|
41
|
+
Recordings are made with the maintainers' recording tool, which is not part of this repository.
|
|
42
|
+
The steps are:
|
|
43
|
+
|
|
44
|
+
1. A maintainer writes `tasks/<id>/task.yaml`, or the person recording adds the task in the
|
|
45
|
+
recording tool, which hands the card over with the recording.
|
|
46
|
+
2. A person records the task: browse first (searches in several wordings, product pages, policy
|
|
47
|
+
pages), then do once what the task asks of the cart, ending in the state it asks for (replay
|
|
48
|
+
answers cart pages with their last recording). Bot checks are completed by that
|
|
49
|
+
person, never by a script; orders are never placed. The tool packs the recording, removes
|
|
50
|
+
cookies, credentials and listed personal data, infers site rules, and replays every recorded
|
|
51
|
+
page once to measure hit rates.
|
|
52
|
+
3. A maintainer imports the recording. The import refuses recordings that fail `pab audit`,
|
|
53
|
+
writes `recording/` and `rules/*.auto.yaml`, and leaves hand-written files
|
|
54
|
+
alone. Tasks are recorded signed out, so page text is not scanned for personal data.
|
|
55
|
+
4. The maintainer replays the task (`pab replay <id>`), fixes misses with rules, and opens a pull
|
|
56
|
+
request.
|
|
57
|
+
|
|
58
|
+
Recording a task again adds to it: the new recording is merged into `recording/`, requests
|
|
59
|
+
recorded both times keep answering with the earlier copy, and the `*.auto.yaml` files are
|
|
60
|
+
inferred again from both; cart pages answer with the new recording, the last one. The import can
|
|
61
|
+
instead prefer the new copies or replace the recording.
|
|
62
|
+
|
|
63
|
+
To propose a task, open an issue with the instruction and the sites involved.
|