@tangle-network/browser-agent-driver 0.15.0 → 0.16.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/dist/brain/index.d.ts +22 -1
- package/dist/brain/index.d.ts.map +1 -1
- package/dist/brain/index.js +57 -1
- package/dist/brain/index.js.map +1 -1
- package/dist/cli-view-live.d.ts +60 -0
- package/dist/cli-view-live.d.ts.map +1 -0
- package/dist/cli-view-live.js +216 -0
- package/dist/cli-view-live.js.map +1 -0
- package/dist/cli-view.d.ts.map +1 -1
- package/dist/cli-view.js +4 -1
- package/dist/cli-view.js.map +1 -1
- package/dist/cli.js +57 -1
- package/dist/cli.js.map +1 -1
- package/dist/extensions/loader.d.ts +40 -0
- package/dist/extensions/loader.d.ts.map +1 -0
- package/dist/extensions/loader.js +85 -0
- package/dist/extensions/loader.js.map +1 -0
- package/dist/extensions/types.d.ts +132 -0
- package/dist/extensions/types.d.ts.map +1 -0
- package/dist/extensions/types.js +161 -0
- package/dist/extensions/types.js.map +1 -0
- package/dist/runner/decision-cache.d.ts +105 -0
- package/dist/runner/decision-cache.d.ts.map +1 -0
- package/dist/runner/decision-cache.js +150 -0
- package/dist/runner/decision-cache.js.map +1 -0
- package/dist/runner/deterministic-patterns.d.ts +41 -0
- package/dist/runner/deterministic-patterns.d.ts.map +1 -0
- package/dist/runner/deterministic-patterns.js +137 -0
- package/dist/runner/deterministic-patterns.js.map +1 -0
- package/dist/runner/events.d.ts +224 -0
- package/dist/runner/events.d.ts.map +1 -0
- package/dist/runner/events.js +147 -0
- package/dist/runner/events.js.map +1 -0
- package/dist/runner/runner.d.ts +22 -0
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/runner.js +259 -8
- package/dist/runner/runner.js.map +1 -1
- package/dist/test-runner.d.ts +19 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +18 -3
- package/dist/test-runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension API for user customization.
|
|
3
|
+
*
|
|
4
|
+
* Users supply a `bad.config.{js,mjs,ts}` file in the cwd (or pass
|
|
5
|
+
* `--extension <path>`) to augment the agent without forking. Extensions
|
|
6
|
+
* can:
|
|
7
|
+
*
|
|
8
|
+
* - Subscribe to TurnEvents (observe everything bad does in real time)
|
|
9
|
+
* - Mutate decisions before execute (override the agent's choice)
|
|
10
|
+
* - Add rules to specific system-prompt sections (search, dataExtraction,
|
|
11
|
+
* heavy, reasoning) without overwriting the whole prompt
|
|
12
|
+
* - Add per-domain rules that fire only on matching URLs
|
|
13
|
+
* - Register custom design-audit rubric fragments programmatically
|
|
14
|
+
*
|
|
15
|
+
* The API is intentionally narrow: it surfaces the levers we know users
|
|
16
|
+
* need without creating a sprawling plugin surface that's hard to evolve.
|
|
17
|
+
*
|
|
18
|
+
* Example bad.config.mjs:
|
|
19
|
+
*
|
|
20
|
+
* export default {
|
|
21
|
+
* addRules: {
|
|
22
|
+
* dataExtraction: 'When extracting prices, always include the currency symbol.',
|
|
23
|
+
* },
|
|
24
|
+
* addRulesForDomain: {
|
|
25
|
+
* 'stripe.com': {
|
|
26
|
+
* extraRules: 'On stripe.com, prefer the Dashboard nav over the marketing site search.',
|
|
27
|
+
* },
|
|
28
|
+
* },
|
|
29
|
+
* addAuditFragments: [
|
|
30
|
+
* {
|
|
31
|
+
* id: 'crypto-trust-signals',
|
|
32
|
+
* dimension: 'trust',
|
|
33
|
+
* weight: 'high',
|
|
34
|
+
* appliesWhen: { domain: ['crypto'] },
|
|
35
|
+
* body: 'Score crypto-app trust signals: cert badges, SOC2, audit reports, founders.',
|
|
36
|
+
* },
|
|
37
|
+
* ],
|
|
38
|
+
* onTurnEvent(event) {
|
|
39
|
+
* if (event.type === 'execute-completed' && !event.success) {
|
|
40
|
+
* console.log('Action failed:', event.error)
|
|
41
|
+
* }
|
|
42
|
+
* },
|
|
43
|
+
* }
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Combine N extensions into a single resolved object. The runner uses the
|
|
47
|
+
* resolved form so it doesn't iterate the extension list on every turn.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveExtensions(extensions) {
|
|
50
|
+
const combinedRules = {};
|
|
51
|
+
const combinedDomainRules = {};
|
|
52
|
+
const combinedAuditFragments = [];
|
|
53
|
+
for (const ext of extensions) {
|
|
54
|
+
if (ext.addRules) {
|
|
55
|
+
for (const key of ['global', 'search', 'dataExtraction', 'heavy']) {
|
|
56
|
+
const value = ext.addRules[key];
|
|
57
|
+
if (value) {
|
|
58
|
+
combinedRules[key] = combinedRules[key]
|
|
59
|
+
? `${combinedRules[key]}\n\n${value}`
|
|
60
|
+
: value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (ext.addRulesForDomain) {
|
|
65
|
+
for (const [domain, rules] of Object.entries(ext.addRulesForDomain)) {
|
|
66
|
+
const existing = combinedDomainRules[domain];
|
|
67
|
+
if (existing && rules.extraRules) {
|
|
68
|
+
existing.extraRules = `${existing.extraRules ?? ''}\n\n${rules.extraRules}`.trim();
|
|
69
|
+
}
|
|
70
|
+
else if (rules.extraRules) {
|
|
71
|
+
combinedDomainRules[domain] = { extraRules: rules.extraRules };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (ext.addAuditFragments) {
|
|
76
|
+
combinedAuditFragments.push(...ext.addAuditFragments);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
extensions,
|
|
81
|
+
fanOutTurnEvent: (event) => {
|
|
82
|
+
for (const ext of extensions) {
|
|
83
|
+
if (ext.onTurnEvent) {
|
|
84
|
+
try {
|
|
85
|
+
ext.onTurnEvent(event);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.error('[bad-extension] onTurnEvent threw:', err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
applyMutateDecision: (decision, ctx) => {
|
|
95
|
+
let current = decision;
|
|
96
|
+
let mutated = false;
|
|
97
|
+
const sources = [];
|
|
98
|
+
for (let idx = 0; idx < extensions.length; idx++) {
|
|
99
|
+
const ext = extensions[idx];
|
|
100
|
+
if (!ext.mutateDecision)
|
|
101
|
+
continue;
|
|
102
|
+
try {
|
|
103
|
+
const result = ext.mutateDecision(current, ctx);
|
|
104
|
+
if (result && result.action !== current.action) {
|
|
105
|
+
current = result;
|
|
106
|
+
mutated = true;
|
|
107
|
+
sources.push(`extension[${idx}]`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.error('[bad-extension] mutateDecision threw:', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { decision: current, mutated, sources };
|
|
116
|
+
},
|
|
117
|
+
combinedRules,
|
|
118
|
+
combinedDomainRules,
|
|
119
|
+
combinedAuditFragments: combinedAuditFragments.length > 0 ? combinedAuditFragments : undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Pick the per-domain rules that apply to a given URL. Returns the
|
|
124
|
+
* concatenation of every matching domain's extraRules, in registration order.
|
|
125
|
+
*/
|
|
126
|
+
export function rulesForUrl(url, domainRules) {
|
|
127
|
+
const matches = [];
|
|
128
|
+
let host;
|
|
129
|
+
try {
|
|
130
|
+
host = new URL(url).hostname;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
for (const [domain, rules] of Object.entries(domainRules)) {
|
|
136
|
+
if (host.includes(domain) && rules.extraRules) {
|
|
137
|
+
matches.push(rules.extraRules);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return matches.length > 0 ? matches.join('\n\n') : undefined;
|
|
141
|
+
}
|
|
142
|
+
/** Type-guard for extension shape (used by the loader) */
|
|
143
|
+
export function isBadExtension(value) {
|
|
144
|
+
if (!value || typeof value !== 'object')
|
|
145
|
+
return false;
|
|
146
|
+
const ext = value;
|
|
147
|
+
// Empty objects are valid extensions (a no-op extension is a valid one).
|
|
148
|
+
// Non-function values for hook fields are invalid.
|
|
149
|
+
if (ext.onTurnEvent !== undefined && typeof ext.onTurnEvent !== 'function')
|
|
150
|
+
return false;
|
|
151
|
+
if (ext.mutateDecision !== undefined && typeof ext.mutateDecision !== 'function')
|
|
152
|
+
return false;
|
|
153
|
+
if (ext.addRules !== undefined && (typeof ext.addRules !== 'object' || ext.addRules === null))
|
|
154
|
+
return false;
|
|
155
|
+
if (ext.addRulesForDomain !== undefined && (typeof ext.addRulesForDomain !== 'object' || ext.addRulesForDomain === null))
|
|
156
|
+
return false;
|
|
157
|
+
if (ext.addAuditFragments !== undefined && !Array.isArray(ext.addAuditFragments))
|
|
158
|
+
return false;
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/extensions/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-session decision cache — short-circuits brain.decide() when the runner
|
|
3
|
+
* encounters a state it has already seen in this run.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: brain.decide() fires every turn unconditionally, even when
|
|
6
|
+
* the (snapshot, url, goal, last-effect, turn-budget) is byte-identical to a
|
|
7
|
+
* previous turn. That happens more often than you'd think — agents back up to
|
|
8
|
+
* known pages, retry after recoverable failures, or revisit a confirmation
|
|
9
|
+
* step. Each of those is a 1-3 second LLM call that produces the same answer
|
|
10
|
+
* the agent already gave a few turns ago.
|
|
11
|
+
*
|
|
12
|
+
* Cache contract:
|
|
13
|
+
* - In-session ONLY. Never persists across runs. Page state changes silently
|
|
14
|
+
* between sessions and a stale cached decision is a correctness landmine.
|
|
15
|
+
* - Bounded LRU. Default 50 entries.
|
|
16
|
+
* - TTL per entry (default 10 minutes). Lets the cache evict slow stale
|
|
17
|
+
* entries even within a long session.
|
|
18
|
+
* - Hash includes turn-budget bucket — "what would I do here at turn 18 of
|
|
19
|
+
* 20" must NOT reuse "what would I do here at turn 5 of 20" because the
|
|
20
|
+
* LLM's risk tolerance changes near the budget cap.
|
|
21
|
+
* - Cache hits emit `decide-skipped-cached` events on the bus so the live
|
|
22
|
+
* viewer can flag them and the user can audit cache effectiveness.
|
|
23
|
+
*
|
|
24
|
+
* What is NOT cached:
|
|
25
|
+
* - The full BrainDecision is cached, but the cached value omits raw LLM
|
|
26
|
+
* output and token counts (those are run-specific telemetry, not part of
|
|
27
|
+
* the decision itself).
|
|
28
|
+
* - Recovery feedback turns are NEVER cached — they're inherently context-
|
|
29
|
+
* dependent on the failure trail.
|
|
30
|
+
*/
|
|
31
|
+
import type { BrainDecision } from '../brain/index.js';
|
|
32
|
+
export interface DecisionCacheOptions {
|
|
33
|
+
/** Max entries before LRU eviction. Default 50. */
|
|
34
|
+
maxEntries?: number;
|
|
35
|
+
/** Time-to-live per entry in milliseconds. Default 10 minutes. */
|
|
36
|
+
ttlMs?: number;
|
|
37
|
+
/** Now() function for tests. Defaults to Date.now. */
|
|
38
|
+
now?: () => number;
|
|
39
|
+
}
|
|
40
|
+
export interface DecisionCacheKey {
|
|
41
|
+
/** SHA1 of normalized snapshot text */
|
|
42
|
+
snapshotHash: string;
|
|
43
|
+
/** Page URL */
|
|
44
|
+
url: string;
|
|
45
|
+
/** Goal text */
|
|
46
|
+
goal: string;
|
|
47
|
+
/** Last action's expectedEffect (empty string for first turn) */
|
|
48
|
+
lastEffect: string;
|
|
49
|
+
/**
|
|
50
|
+
* Turn budget bucket: how much of the turn budget is left, bucketed by
|
|
51
|
+
* thirds (early/mid/late). The agent's strategy changes near the cap, so
|
|
52
|
+
* "what to do here at turn 18/20" must not reuse "what to do here at turn
|
|
53
|
+
* 5/20".
|
|
54
|
+
*/
|
|
55
|
+
budgetBucket: 'early' | 'mid' | 'late';
|
|
56
|
+
}
|
|
57
|
+
export declare class DecisionCache {
|
|
58
|
+
private entries;
|
|
59
|
+
private readonly maxEntries;
|
|
60
|
+
private readonly ttlMs;
|
|
61
|
+
private readonly now;
|
|
62
|
+
private hits;
|
|
63
|
+
private misses;
|
|
64
|
+
constructor(options?: DecisionCacheOptions);
|
|
65
|
+
/** Hash a key into a stable opaque string. Exposed for tests. */
|
|
66
|
+
static hashKey(key: DecisionCacheKey): string;
|
|
67
|
+
/**
|
|
68
|
+
* Hash a snapshot string. Done by the cache rather than the caller so
|
|
69
|
+
* future cache implementations can swap the hash algorithm without
|
|
70
|
+
* cascading changes.
|
|
71
|
+
*/
|
|
72
|
+
static hashSnapshot(snapshot: string): string;
|
|
73
|
+
/** Bucket the turn position so cache hits respect strategic horizons. */
|
|
74
|
+
static budgetBucket(currentTurn: number, maxTurns: number): 'early' | 'mid' | 'late';
|
|
75
|
+
/**
|
|
76
|
+
* Look up a cached decision. Returns undefined on miss or stale entry.
|
|
77
|
+
* Records hit/miss for `getStats()`.
|
|
78
|
+
*/
|
|
79
|
+
get(key: DecisionCacheKey): {
|
|
80
|
+
decision: BrainDecision;
|
|
81
|
+
hash: string;
|
|
82
|
+
} | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Store a decision under the given key. Evicts the oldest entry if the
|
|
85
|
+
* cache is full.
|
|
86
|
+
*/
|
|
87
|
+
set(key: DecisionCacheKey, decision: BrainDecision): string;
|
|
88
|
+
/** Drop all entries. Tests + explicit cache-bust use. */
|
|
89
|
+
clear(): void;
|
|
90
|
+
/** Hit / miss counters for telemetry + tests. */
|
|
91
|
+
getStats(): {
|
|
92
|
+
hits: number;
|
|
93
|
+
misses: number;
|
|
94
|
+
size: number;
|
|
95
|
+
hitRate: number;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Strip volatile fields from a BrainDecision before caching. raw LLM
|
|
99
|
+
* output and token counts are run-specific telemetry, not part of the
|
|
100
|
+
* decision; replaying them on a cache hit would lie about what just
|
|
101
|
+
* happened.
|
|
102
|
+
*/
|
|
103
|
+
private stripVolatile;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=decision-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision-cache.d.ts","sourceRoot":"","sources":["../../src/runner/decision-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEtD,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe;IACf,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,YAAY,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;CACvC;AASD,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,MAAM,CAAI;gBAEN,OAAO,GAAE,oBAAyB;IAM9C,iEAAiE;IACjE,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,GAAG,MAAM;IAc7C;;;;OAIG;IACH,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI7C,yEAAyE;IACzE,MAAM,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM;IAOpF;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,gBAAgB,GAAG;QAAE,QAAQ,EAAE,aAAa,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;IAmBjF;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,gBAAgB,EAAE,QAAQ,EAAE,aAAa,GAAG,MAAM;IAe3D,yDAAyD;IACzD,KAAK,IAAI,IAAI;IAMb,iDAAiD;IACjD,QAAQ,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAU3E;;;;;OAKG;IACH,OAAO,CAAC,aAAa;CAWtB"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-session decision cache — short-circuits brain.decide() when the runner
|
|
3
|
+
* encounters a state it has already seen in this run.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: brain.decide() fires every turn unconditionally, even when
|
|
6
|
+
* the (snapshot, url, goal, last-effect, turn-budget) is byte-identical to a
|
|
7
|
+
* previous turn. That happens more often than you'd think — agents back up to
|
|
8
|
+
* known pages, retry after recoverable failures, or revisit a confirmation
|
|
9
|
+
* step. Each of those is a 1-3 second LLM call that produces the same answer
|
|
10
|
+
* the agent already gave a few turns ago.
|
|
11
|
+
*
|
|
12
|
+
* Cache contract:
|
|
13
|
+
* - In-session ONLY. Never persists across runs. Page state changes silently
|
|
14
|
+
* between sessions and a stale cached decision is a correctness landmine.
|
|
15
|
+
* - Bounded LRU. Default 50 entries.
|
|
16
|
+
* - TTL per entry (default 10 minutes). Lets the cache evict slow stale
|
|
17
|
+
* entries even within a long session.
|
|
18
|
+
* - Hash includes turn-budget bucket — "what would I do here at turn 18 of
|
|
19
|
+
* 20" must NOT reuse "what would I do here at turn 5 of 20" because the
|
|
20
|
+
* LLM's risk tolerance changes near the budget cap.
|
|
21
|
+
* - Cache hits emit `decide-skipped-cached` events on the bus so the live
|
|
22
|
+
* viewer can flag them and the user can audit cache effectiveness.
|
|
23
|
+
*
|
|
24
|
+
* What is NOT cached:
|
|
25
|
+
* - The full BrainDecision is cached, but the cached value omits raw LLM
|
|
26
|
+
* output and token counts (those are run-specific telemetry, not part of
|
|
27
|
+
* the decision itself).
|
|
28
|
+
* - Recovery feedback turns are NEVER cached — they're inherently context-
|
|
29
|
+
* dependent on the failure trail.
|
|
30
|
+
*/
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
export class DecisionCache {
|
|
33
|
+
entries = new Map();
|
|
34
|
+
maxEntries;
|
|
35
|
+
ttlMs;
|
|
36
|
+
now;
|
|
37
|
+
hits = 0;
|
|
38
|
+
misses = 0;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.maxEntries = options.maxEntries ?? 50;
|
|
41
|
+
this.ttlMs = options.ttlMs ?? 10 * 60 * 1000; // 10 minutes
|
|
42
|
+
this.now = options.now ?? Date.now;
|
|
43
|
+
}
|
|
44
|
+
/** Hash a key into a stable opaque string. Exposed for tests. */
|
|
45
|
+
static hashKey(key) {
|
|
46
|
+
const h = createHash('sha1');
|
|
47
|
+
h.update(key.snapshotHash);
|
|
48
|
+
h.update('\u0000');
|
|
49
|
+
h.update(key.url);
|
|
50
|
+
h.update('\u0000');
|
|
51
|
+
h.update(key.goal);
|
|
52
|
+
h.update('\u0000');
|
|
53
|
+
h.update(key.lastEffect);
|
|
54
|
+
h.update('\u0000');
|
|
55
|
+
h.update(key.budgetBucket);
|
|
56
|
+
return h.digest('hex');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Hash a snapshot string. Done by the cache rather than the caller so
|
|
60
|
+
* future cache implementations can swap the hash algorithm without
|
|
61
|
+
* cascading changes.
|
|
62
|
+
*/
|
|
63
|
+
static hashSnapshot(snapshot) {
|
|
64
|
+
return createHash('sha1').update(snapshot).digest('hex');
|
|
65
|
+
}
|
|
66
|
+
/** Bucket the turn position so cache hits respect strategic horizons. */
|
|
67
|
+
static budgetBucket(currentTurn, maxTurns) {
|
|
68
|
+
const ratio = currentTurn / Math.max(maxTurns, 1);
|
|
69
|
+
if (ratio < 0.33)
|
|
70
|
+
return 'early';
|
|
71
|
+
if (ratio < 0.75)
|
|
72
|
+
return 'mid';
|
|
73
|
+
return 'late';
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Look up a cached decision. Returns undefined on miss or stale entry.
|
|
77
|
+
* Records hit/miss for `getStats()`.
|
|
78
|
+
*/
|
|
79
|
+
get(key) {
|
|
80
|
+
const hash = DecisionCache.hashKey(key);
|
|
81
|
+
const entry = this.entries.get(hash);
|
|
82
|
+
if (!entry) {
|
|
83
|
+
this.misses++;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (this.now() - entry.storedAt > this.ttlMs) {
|
|
87
|
+
this.entries.delete(hash);
|
|
88
|
+
this.misses++;
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
// LRU bump: re-insert to mark recently used
|
|
92
|
+
this.entries.delete(hash);
|
|
93
|
+
this.entries.set(hash, entry);
|
|
94
|
+
this.hits++;
|
|
95
|
+
return { decision: entry.decision, hash };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Store a decision under the given key. Evicts the oldest entry if the
|
|
99
|
+
* cache is full.
|
|
100
|
+
*/
|
|
101
|
+
set(key, decision) {
|
|
102
|
+
const hash = DecisionCache.hashKey(key);
|
|
103
|
+
if (this.entries.size >= this.maxEntries && !this.entries.has(hash)) {
|
|
104
|
+
// Evict oldest (Map iteration order = insertion order)
|
|
105
|
+
const oldest = this.entries.keys().next().value;
|
|
106
|
+
if (oldest !== undefined)
|
|
107
|
+
this.entries.delete(oldest);
|
|
108
|
+
}
|
|
109
|
+
this.entries.set(hash, {
|
|
110
|
+
decision: this.stripVolatile(decision),
|
|
111
|
+
storedAt: this.now(),
|
|
112
|
+
hash,
|
|
113
|
+
});
|
|
114
|
+
return hash;
|
|
115
|
+
}
|
|
116
|
+
/** Drop all entries. Tests + explicit cache-bust use. */
|
|
117
|
+
clear() {
|
|
118
|
+
this.entries.clear();
|
|
119
|
+
this.hits = 0;
|
|
120
|
+
this.misses = 0;
|
|
121
|
+
}
|
|
122
|
+
/** Hit / miss counters for telemetry + tests. */
|
|
123
|
+
getStats() {
|
|
124
|
+
const total = this.hits + this.misses;
|
|
125
|
+
return {
|
|
126
|
+
hits: this.hits,
|
|
127
|
+
misses: this.misses,
|
|
128
|
+
size: this.entries.size,
|
|
129
|
+
hitRate: total > 0 ? this.hits / total : 0,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Strip volatile fields from a BrainDecision before caching. raw LLM
|
|
134
|
+
* output and token counts are run-specific telemetry, not part of the
|
|
135
|
+
* decision; replaying them on a cache hit would lie about what just
|
|
136
|
+
* happened.
|
|
137
|
+
*/
|
|
138
|
+
stripVolatile(decision) {
|
|
139
|
+
return {
|
|
140
|
+
...decision,
|
|
141
|
+
raw: '[cached]',
|
|
142
|
+
tokensUsed: 0,
|
|
143
|
+
inputTokens: 0,
|
|
144
|
+
outputTokens: 0,
|
|
145
|
+
cacheReadInputTokens: undefined,
|
|
146
|
+
cacheCreationInputTokens: undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=decision-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision-cache.js","sourceRoot":"","sources":["../../src/runner/decision-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic UI pattern matchers — short-circuit brain.decide() when the
|
|
3
|
+
* page state has exactly one obvious action.
|
|
4
|
+
*
|
|
5
|
+
* Cookie banners with a single "Accept" button are everywhere. Modals with
|
|
6
|
+
* one "Close" button. Wizard pages with one "Next". The agent doesn't need
|
|
7
|
+
* an LLM call to decide what to do — the answer is deterministic. Skipping
|
|
8
|
+
* the LLM call saves 1-3s per match.
|
|
9
|
+
*
|
|
10
|
+
* Pattern matchers run on the snapshot text and return either an Action to
|
|
11
|
+
* execute or null. Matchers must be:
|
|
12
|
+
* - Specific: false positives waste a turn (worse than no skip)
|
|
13
|
+
* - Cheap: no I/O, no LLM, no DOM walks beyond regex on the snapshot
|
|
14
|
+
* - Reversible: if execute fails, the next turn falls through to brain.decide()
|
|
15
|
+
*
|
|
16
|
+
* Each matcher returns a `PatternMatch` with the action to take and a
|
|
17
|
+
* `patternId` so the bus can emit `decide-skipped-pattern` events for audit.
|
|
18
|
+
*/
|
|
19
|
+
import type { Action, PageState } from '../types.js';
|
|
20
|
+
export interface PatternMatch {
|
|
21
|
+
action: Action;
|
|
22
|
+
patternId: string;
|
|
23
|
+
reasoning: string;
|
|
24
|
+
expectedEffect: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Try every pattern in order. Returns the first match, or null if none fire.
|
|
28
|
+
*
|
|
29
|
+
* Patterns must be ordered from MOST SPECIFIC to LEAST SPECIFIC so a precise
|
|
30
|
+
* match (e.g., "single accept button on a cookie banner") wins over a
|
|
31
|
+
* generic one (e.g., "any single visible button").
|
|
32
|
+
*/
|
|
33
|
+
export declare function matchDeterministicPattern(state: PageState): PatternMatch | null;
|
|
34
|
+
type Matcher = (state: PageState) => PatternMatch | null;
|
|
35
|
+
/** Exported for tests so individual matchers can be exercised in isolation. */
|
|
36
|
+
export declare const __test: {
|
|
37
|
+
cookieBannerMatcher: Matcher;
|
|
38
|
+
singleButtonModalMatcher: Matcher;
|
|
39
|
+
};
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=deterministic-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deterministic-patterns.d.ts","sourceRoot":"","sources":["../../src/runner/deterministic-patterns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEpD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,GAAG,YAAY,GAAG,IAAI,CAM/E;AAED,KAAK,OAAO,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,YAAY,GAAG,IAAI,CAAA;AAwGxD,+EAA+E;AAC/E,eAAO,MAAM,MAAM;;;CAGlB,CAAA"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic UI pattern matchers — short-circuit brain.decide() when the
|
|
3
|
+
* page state has exactly one obvious action.
|
|
4
|
+
*
|
|
5
|
+
* Cookie banners with a single "Accept" button are everywhere. Modals with
|
|
6
|
+
* one "Close" button. Wizard pages with one "Next". The agent doesn't need
|
|
7
|
+
* an LLM call to decide what to do — the answer is deterministic. Skipping
|
|
8
|
+
* the LLM call saves 1-3s per match.
|
|
9
|
+
*
|
|
10
|
+
* Pattern matchers run on the snapshot text and return either an Action to
|
|
11
|
+
* execute or null. Matchers must be:
|
|
12
|
+
* - Specific: false positives waste a turn (worse than no skip)
|
|
13
|
+
* - Cheap: no I/O, no LLM, no DOM walks beyond regex on the snapshot
|
|
14
|
+
* - Reversible: if execute fails, the next turn falls through to brain.decide()
|
|
15
|
+
*
|
|
16
|
+
* Each matcher returns a `PatternMatch` with the action to take and a
|
|
17
|
+
* `patternId` so the bus can emit `decide-skipped-pattern` events for audit.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Try every pattern in order. Returns the first match, or null if none fire.
|
|
21
|
+
*
|
|
22
|
+
* Patterns must be ordered from MOST SPECIFIC to LEAST SPECIFIC so a precise
|
|
23
|
+
* match (e.g., "single accept button on a cookie banner") wins over a
|
|
24
|
+
* generic one (e.g., "any single visible button").
|
|
25
|
+
*/
|
|
26
|
+
export function matchDeterministicPattern(state) {
|
|
27
|
+
for (const matcher of MATCHERS) {
|
|
28
|
+
const match = matcher(state);
|
|
29
|
+
if (match)
|
|
30
|
+
return match;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// ── Pattern: cookie banner / consent modal accept ───────────────────────
|
|
35
|
+
//
|
|
36
|
+
// Match conditions:
|
|
37
|
+
// - The snapshot has at most ONE dialog/banner/alertdialog containing a
|
|
38
|
+
// button matching /accept|agree|got it|i understand|allow all|allow cookies|continue/i
|
|
39
|
+
// - Other dialogs (if any) are smaller content dialogs (no buttons or
|
|
40
|
+
// only "Close" — the cookie consent is the largest one)
|
|
41
|
+
//
|
|
42
|
+
// Why this is safe: cookie banners are functionally homogeneous across the
|
|
43
|
+
// web. The user wants to dismiss them and proceed. There's no scenario
|
|
44
|
+
// where "click Accept on the cookie banner" is the wrong choice.
|
|
45
|
+
const COOKIE_ACCEPT_RE = /\b(accept(?:\s+all)?|agree|got\s+it|i\s+understand|allow\s+all|allow\s+cookies|continue)\b/i;
|
|
46
|
+
const COOKIE_BANNER_REF_RE = /^\s*(button|link)\s+\[ref=([^\]]+)\][^"]*"([^"]*)"/im;
|
|
47
|
+
const cookieBannerMatcher = (state) => {
|
|
48
|
+
// Look for any element line containing both an accept-style verb AND a ref
|
|
49
|
+
const lines = state.snapshot.split('\n');
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const refMatch = line.match(/\[ref=([^\]]+)\][^"]*"([^"]*)"/i);
|
|
52
|
+
if (!refMatch)
|
|
53
|
+
continue;
|
|
54
|
+
const text = refMatch[2];
|
|
55
|
+
if (!COOKIE_ACCEPT_RE.test(text))
|
|
56
|
+
continue;
|
|
57
|
+
// Filter out generic continues that aren't on a banner. Require the line
|
|
58
|
+
// to be a button or link AND for the snapshot to mention "cookie" or
|
|
59
|
+
// "consent" or "privacy" or "gdpr" within 500 chars of this line.
|
|
60
|
+
if (!/\b(button|link)\b/i.test(line))
|
|
61
|
+
continue;
|
|
62
|
+
const lineIdx = state.snapshot.indexOf(line);
|
|
63
|
+
const windowStart = Math.max(0, lineIdx - 500);
|
|
64
|
+
const windowEnd = Math.min(state.snapshot.length, lineIdx + 500);
|
|
65
|
+
const window = state.snapshot.slice(windowStart, windowEnd).toLowerCase();
|
|
66
|
+
if (!/cookie|consent|privacy|gdpr|tracking/i.test(window))
|
|
67
|
+
continue;
|
|
68
|
+
return {
|
|
69
|
+
action: { action: 'click', selector: `@${refMatch[1]}` },
|
|
70
|
+
patternId: 'cookie-banner-accept',
|
|
71
|
+
reasoning: `Deterministic pattern: cookie/consent banner with "${text}" button. Skipping LLM call.`,
|
|
72
|
+
expectedEffect: 'cookie banner dismissed',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
// ── Pattern: single-action modal close ──────────────────────────────────
|
|
78
|
+
//
|
|
79
|
+
// Match conditions:
|
|
80
|
+
// - Snapshot has exactly one alertdialog/dialog
|
|
81
|
+
// - The dialog has exactly ONE button or close affordance
|
|
82
|
+
// - The button text is "Close", "Dismiss", "OK", or "X"
|
|
83
|
+
//
|
|
84
|
+
// Excludes confirmation dialogs (Yes/No, Save/Cancel) — those need real
|
|
85
|
+
// thought.
|
|
86
|
+
const CLOSE_VERBS_RE = /^\s*(close|dismiss|ok|×|x|got it)\s*$/i;
|
|
87
|
+
const singleButtonModalMatcher = (state) => {
|
|
88
|
+
// Find a line starting with dialog/alertdialog (no leading whitespace —
|
|
89
|
+
// a top-level snapshot element). Children of the dialog are indented
|
|
90
|
+
// beneath it.
|
|
91
|
+
const lines = state.snapshot.split('\n');
|
|
92
|
+
let dialogIdx = -1;
|
|
93
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
94
|
+
if (/^(dialog|alertdialog)\b/i.test(lines[idx])) {
|
|
95
|
+
dialogIdx = idx;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (dialogIdx === -1)
|
|
100
|
+
return null;
|
|
101
|
+
// The dialog region is its line plus the indented children that follow.
|
|
102
|
+
// Stop at the next non-indented line (sibling element).
|
|
103
|
+
const regionLines = [lines[dialogIdx]];
|
|
104
|
+
for (let idx = dialogIdx + 1; idx < lines.length; idx++) {
|
|
105
|
+
if (lines[idx].length > 0 && !/^\s/.test(lines[idx]))
|
|
106
|
+
break;
|
|
107
|
+
regionLines.push(lines[idx]);
|
|
108
|
+
}
|
|
109
|
+
// Count interactive elements (buttons / links with refs) in the region
|
|
110
|
+
const interactiveLines = regionLines.filter((l) => /\b(button|link)\b\s+\[ref=/.test(l));
|
|
111
|
+
if (interactiveLines.length !== 1)
|
|
112
|
+
return null;
|
|
113
|
+
const match = interactiveLines[0].match(/\[ref=([^\]]+)\][^"]*"([^"]*)"/);
|
|
114
|
+
if (!match)
|
|
115
|
+
return null;
|
|
116
|
+
const text = match[2];
|
|
117
|
+
if (!CLOSE_VERBS_RE.test(text))
|
|
118
|
+
return null;
|
|
119
|
+
return {
|
|
120
|
+
action: { action: 'click', selector: `@${match[1]}` },
|
|
121
|
+
patternId: 'single-button-modal-close',
|
|
122
|
+
reasoning: `Deterministic pattern: modal with single "${text}" button. Skipping LLM call.`,
|
|
123
|
+
expectedEffect: 'modal dismissed',
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
// ── Registered matcher list ─────────────────────────────────────────────
|
|
127
|
+
// Order: most specific first.
|
|
128
|
+
const MATCHERS = [
|
|
129
|
+
cookieBannerMatcher,
|
|
130
|
+
singleButtonModalMatcher,
|
|
131
|
+
];
|
|
132
|
+
/** Exported for tests so individual matchers can be exercised in isolation. */
|
|
133
|
+
export const __test = {
|
|
134
|
+
cookieBannerMatcher,
|
|
135
|
+
singleButtonModalMatcher,
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=deterministic-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deterministic-patterns.js","sourceRoot":"","sources":["../../src/runner/deterministic-patterns.ts"],"names":[],"mappings":"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"}
|