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.
Files changed (81) hide show
  1. package/README.md +128 -2
  2. package/bin/pab.js +5 -0
  3. package/dist/src/browser.d.ts +29 -0
  4. package/dist/src/browser.js +37 -0
  5. package/dist/src/browser.js.map +1 -0
  6. package/dist/src/cli.d.ts +20 -0
  7. package/dist/src/cli.js +331 -0
  8. package/dist/src/cli.js.map +1 -0
  9. package/dist/src/core/archive.d.ts +49 -0
  10. package/dist/src/core/archive.js +213 -0
  11. package/dist/src/core/archive.js.map +1 -0
  12. package/dist/src/core/audit.d.ts +15 -0
  13. package/dist/src/core/audit.js +85 -0
  14. package/dist/src/core/audit.js.map +1 -0
  15. package/dist/src/core/bundle.d.ts +42 -0
  16. package/dist/src/core/bundle.js +80 -0
  17. package/dist/src/core/bundle.js.map +1 -0
  18. package/dist/src/core/lookup.d.ts +15 -0
  19. package/dist/src/core/lookup.js +82 -0
  20. package/dist/src/core/lookup.js.map +1 -0
  21. package/dist/src/core/normalize.d.ts +45 -0
  22. package/dist/src/core/normalize.js +143 -0
  23. package/dist/src/core/normalize.js.map +1 -0
  24. package/dist/src/core/rules.d.ts +38 -0
  25. package/dist/src/core/rules.js +187 -0
  26. package/dist/src/core/rules.js.map +1 -0
  27. package/dist/src/core/store.d.ts +26 -0
  28. package/dist/src/core/store.js +78 -0
  29. package/dist/src/core/store.js.map +1 -0
  30. package/dist/src/core/types.d.ts +125 -0
  31. package/dist/src/core/types.js +3 -0
  32. package/dist/src/core/types.js.map +1 -0
  33. package/dist/src/core/util.d.ts +21 -0
  34. package/dist/src/core/util.js +150 -0
  35. package/dist/src/core/util.js.map +1 -0
  36. package/dist/src/core/warc.d.ts +55 -0
  37. package/dist/src/core/warc.js +258 -0
  38. package/dist/src/core/warc.js.map +1 -0
  39. package/dist/src/core/writer.d.ts +26 -0
  40. package/dist/src/core/writer.js +119 -0
  41. package/dist/src/core/writer.js.map +1 -0
  42. package/dist/src/grading.d.ts +63 -0
  43. package/dist/src/grading.js +173 -0
  44. package/dist/src/grading.js.map +1 -0
  45. package/dist/src/judge.d.ts +25 -0
  46. package/dist/src/judge.js +119 -0
  47. package/dist/src/judge.js.map +1 -0
  48. package/dist/src/paths.d.ts +2 -0
  49. package/dist/src/paths.js +40 -0
  50. package/dist/src/paths.js.map +1 -0
  51. package/dist/src/replay/clock.d.ts +19 -0
  52. package/dist/src/replay/clock.js +60 -0
  53. package/dist/src/replay/clock.js.map +1 -0
  54. package/dist/src/replay/engine.d.ts +53 -0
  55. package/dist/src/replay/engine.js +280 -0
  56. package/dist/src/replay/engine.js.map +1 -0
  57. package/dist/src/replay/launcher.d.ts +36 -0
  58. package/dist/src/replay/launcher.js +163 -0
  59. package/dist/src/replay/launcher.js.map +1 -0
  60. package/dist/src/replay/whistle-plugin.d.ts +24 -0
  61. package/dist/src/replay/whistle-plugin.js +82 -0
  62. package/dist/src/replay/whistle-plugin.js.map +1 -0
  63. package/dist/src/report.d.ts +3 -0
  64. package/dist/src/report.js +143 -0
  65. package/dist/src/report.js.map +1 -0
  66. package/dist/src/scoring.d.ts +87 -0
  67. package/dist/src/scoring.js +232 -0
  68. package/dist/src/scoring.js.map +1 -0
  69. package/dist/src/tasks.d.ts +61 -0
  70. package/dist/src/tasks.js +152 -0
  71. package/dist/src/tasks.js.map +1 -0
  72. package/docs/bundle-format.md +114 -0
  73. package/docs/deploy.md +78 -0
  74. package/docs/grading.md +81 -0
  75. package/docs/site-rules.md +114 -0
  76. package/docs/tasks.md +63 -0
  77. package/package.json +121 -4
  78. package/plugins/whistle.bench-replay/index.js +1 -0
  79. package/plugins/whistle.bench-replay/package.json +7 -0
  80. package/sites/_default.yaml +80 -0
  81. 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,MAAY,EAAE,oCAAgB;AAC9B,MAAY,IAAI,sCAAkB;AAClC,gDAAwB;AACxB,4CAA4E;AAC5E,mCAAuC;AA4BvC;IACuB,IAAI;IAAzB,YAAqB,IAAY;oBAAZ,IAAI;IAAW,CAAC;IAErC,sDAAsD;IACtD,MAAM,CAAC,OAAO;QACZ,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,oBAAY,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,IAAI,QAAQ,KAAa,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAChE,IAAI,QAAQ,KAAa,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAChE,wFAAwF;IACxF,IAAI,QAAQ,KAAa,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAExG,OAAO;QACL,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QAC7C,OAAO,EAAE;aACN,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;aAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC;aAC5E,IAAI,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC,EAAU;QACb,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,YAAY,EAAE,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACxG,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC7B,CAAC;CACF;;AAED;IACuB,IAAI;IAAiB,GAAG;IAA7C,YAAqB,IAAU,EAAW,GAAW;oBAAhC,IAAI;mBAAiB,GAAG;IAAW,CAAC;IAEzD,IAAI,EAAE,KAAa,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACpD,IAAI,QAAQ,KAAa,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IACnE,IAAI,YAAY,KAAa,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IACvE,IAAI,QAAQ,KAAa,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/D,6DAA6D;IAC7D,IAAI,WAAW,KAAa,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IAEzE,IAAI;QACF,MAAM,IAAI,GAAG,cAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAa,CAAC;QAC5E,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;IAC9C,CAAC;IAED,gEAAgE;IAChE,cAAc;QACZ,OAAO,IAAA,wBAAc,EAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3C,CAAC;IAED,mGAAmG;IACnG,KAAK,CAAC,SAAS;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,EAAE,sBAAsB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QAC9F,MAAM,MAAM,GAAG,IAAA,mBAAS,EAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,EAAE,IAAI,MAAM,EAAE,CAAC,CAAC;QAC3D,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACjE,MAAM,GAAG,GAAG,GAAG,IAAI,QAAQ,OAAO,CAAC,GAAG,EAAE,CAAC;QACzC,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC;YACH,MAAM,IAAA,yBAAe,EAAC,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;YAC9C,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAClD,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACjD,MAAM,GAAG,CAAC;QACZ,CAAC;QACD,uCAAuC;QACvC,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9C,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBACtG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,EAAE;YACb,SAAS,EAAE,MAAM,IAAI,CAAC,SAAS,EAAE;YACjC,SAAS,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC;YAC9C,IAAI,EAAE,IAAI;SACX,CAAC;IACJ,CAAC;CACF;;AAED;;;GAGG;AACI,KAAK,wBAAwB,GAAW,EAAE,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE;IACpE,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC;QACtD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IAC3E,CAAC;IACD,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC;QAClD,4FAA4F;QAC5F,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,OAAO,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC3I,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;AACjC,CAAC"}
@@ -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.
@@ -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.