@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
package/dist/runner/runner.js
CHANGED
|
@@ -31,6 +31,9 @@ import { detectAiTanglePartnerTemplateVisibleState, detectAiTangleVerifiedOutput
|
|
|
31
31
|
import { shouldUseVisibleLinkScout, shouldUseVisibleLinkScoutPage, shouldUseBoundedBranchExplorer, inspectBranchPreview, scoreBranchPreview } from './scout.js';
|
|
32
32
|
import { buildOverrideProducers, buildScoutLinkRecommendationText, buildBranchLinkRecommendationText } from './overrides.js';
|
|
33
33
|
import { RunRegistry } from '../memory/run-registry.js';
|
|
34
|
+
import { ensureBus } from './events.js';
|
|
35
|
+
import { DecisionCache } from './decision-cache.js';
|
|
36
|
+
import { matchDeterministicPattern } from './deterministic-patterns.js';
|
|
34
37
|
/** Build a structured session record from a completed run */
|
|
35
38
|
function buildSession(scenario, result) {
|
|
36
39
|
const lastTurn = result.turns[result.turns.length - 1];
|
|
@@ -79,6 +82,14 @@ export class BrowserAgent {
|
|
|
79
82
|
knowledge;
|
|
80
83
|
selectorCache;
|
|
81
84
|
cachedPostState;
|
|
85
|
+
bus;
|
|
86
|
+
currentRunId = '';
|
|
87
|
+
// In-session decision cache. Lazy-skips brain.decide() when the (snapshot,
|
|
88
|
+
// url, goal, last-effect, budget-bucket) is byte-identical to a previous
|
|
89
|
+
// turn in this run. The cache is fresh per `run()` invocation — never
|
|
90
|
+
// persists, never crosses runs.
|
|
91
|
+
decisionCache;
|
|
92
|
+
extensions;
|
|
82
93
|
constructor(options) {
|
|
83
94
|
this.driver = options.driver;
|
|
84
95
|
this.config = options.config || {};
|
|
@@ -86,6 +97,17 @@ export class BrowserAgent {
|
|
|
86
97
|
this.onTurn = options.onTurn;
|
|
87
98
|
this.onPhaseTiming = options.onPhaseTiming;
|
|
88
99
|
this.referenceTrajectory = options.referenceTrajectory;
|
|
100
|
+
this.bus = ensureBus(options.eventBus);
|
|
101
|
+
this.extensions = options.extensions;
|
|
102
|
+
if (this.extensions) {
|
|
103
|
+
// Forward extension-supplied prompt rules into the brain so they get
|
|
104
|
+
// included in every system prompt build. Domain-keyed rules are
|
|
105
|
+
// matched per-turn against the current URL inside composeSystemPromptParts.
|
|
106
|
+
this.brain.setExtensionRules(this.extensions.combinedRules, this.extensions.combinedDomainRules);
|
|
107
|
+
// Subscribe extensions to the event bus so onTurnEvent fires for
|
|
108
|
+
// every emitted event without callers having to wire it up.
|
|
109
|
+
this.bus.subscribe(this.extensions.fanOutTurnEvent, false);
|
|
110
|
+
}
|
|
89
111
|
this.projectStore = options.projectStore;
|
|
90
112
|
this.runRegistry = options.runRegistry;
|
|
91
113
|
}
|
|
@@ -100,7 +122,19 @@ export class BrowserAgent {
|
|
|
100
122
|
const runId = scenario.sessionId
|
|
101
123
|
? `${scenario.sessionId}_${Date.now()}`
|
|
102
124
|
: RunRegistry.generateRunId();
|
|
125
|
+
this.currentRunId = runId;
|
|
103
126
|
const domain = safeHostname(scenario.startUrl || '') || 'unknown';
|
|
127
|
+
// Emit run-started so subscribers (live viewer, events.jsonl sink) can
|
|
128
|
+
// initialize their state. The bus is a no-op when no eventBus was passed
|
|
129
|
+
// to the constructor, so this is free for non-live runs.
|
|
130
|
+
this.bus.emitNow({
|
|
131
|
+
type: 'run-started',
|
|
132
|
+
runId,
|
|
133
|
+
turn: 0,
|
|
134
|
+
goal: scenario.goal,
|
|
135
|
+
startUrl: scenario.startUrl,
|
|
136
|
+
maxTurns,
|
|
137
|
+
});
|
|
104
138
|
const buildResult = (result) => {
|
|
105
139
|
const agentResult = {
|
|
106
140
|
...result,
|
|
@@ -118,6 +152,17 @@ export class BrowserAgent {
|
|
|
118
152
|
reason: agentResult.reason,
|
|
119
153
|
turnCount: agentResult.turns.length,
|
|
120
154
|
});
|
|
155
|
+
// Emit run-completed so the live viewer can swap to a "finished" UI
|
|
156
|
+
// and the events.jsonl sink can flush its tail.
|
|
157
|
+
this.bus.emitNow({
|
|
158
|
+
type: 'run-completed',
|
|
159
|
+
runId,
|
|
160
|
+
turn: 0,
|
|
161
|
+
success: agentResult.success,
|
|
162
|
+
totalTurns: agentResult.turns.length,
|
|
163
|
+
totalMs: agentResult.totalMs,
|
|
164
|
+
...(agentResult.reason ? { reason: agentResult.reason } : {}),
|
|
165
|
+
});
|
|
121
166
|
return agentResult;
|
|
122
167
|
};
|
|
123
168
|
// Wrap onTurn to include mid-run manifest updates (every 3 turns)
|
|
@@ -138,6 +183,15 @@ export class BrowserAgent {
|
|
|
138
183
|
this.brain.reset();
|
|
139
184
|
this.cachedPostState = undefined;
|
|
140
185
|
let executeTimeoutRecoveries = 0;
|
|
186
|
+
// Fresh decision cache per run. The cache is strictly in-session — page
|
|
187
|
+
// state changes silently between runs and a stale cached decision is a
|
|
188
|
+
// correctness landmine. Disable via BAD_DECISION_CACHE=0.
|
|
189
|
+
this.decisionCache = process.env.BAD_DECISION_CACHE === '0'
|
|
190
|
+
? undefined
|
|
191
|
+
: new DecisionCache();
|
|
192
|
+
// Track the previous turn's expectedEffect so we can include it in the
|
|
193
|
+
// cache key. Empty string for turn 1.
|
|
194
|
+
let lastEffectForCacheKey = '';
|
|
141
195
|
// Pre-warm the provider connection in parallel with everything else.
|
|
142
196
|
// Without this, turn 1's first LLM call eats 600ms (Anthropic) to
|
|
143
197
|
// 1200ms (OpenAI) of cold-start TLS+DNS+HTTP/2 setup. By the time
|
|
@@ -191,9 +245,17 @@ export class BrowserAgent {
|
|
|
191
245
|
});
|
|
192
246
|
}
|
|
193
247
|
const turnStart = Date.now();
|
|
248
|
+
this.bus.emitNow({ type: 'turn-started', runId, turn: i });
|
|
194
249
|
try {
|
|
195
250
|
// -- 1. Check for recovery before observing --
|
|
196
|
-
|
|
251
|
+
// Only run analyzeRecovery when there's a non-zero error trail. Used
|
|
252
|
+
// to run unconditionally; lazy-skipping it when there are no recent
|
|
253
|
+
// errors avoids the per-turn cost on the happy path. (Gen 5 lazy
|
|
254
|
+
// decision graph computation, change #20 in the pursuit spec.)
|
|
255
|
+
const hasErrorTrail = turns.length >= 2
|
|
256
|
+
&& (runState.consecutiveErrors > 0
|
|
257
|
+
|| turns.slice(-5).some((t) => t.error || t.verified === false));
|
|
258
|
+
if (hasErrorTrail) {
|
|
197
259
|
const lastState = turns[turns.length - 1]?.state || { url: '', title: '', snapshot: '' };
|
|
198
260
|
const recovery = analyzeRecovery({
|
|
199
261
|
recentTurns: turns.slice(-5),
|
|
@@ -201,6 +263,15 @@ export class BrowserAgent {
|
|
|
201
263
|
consecutiveErrors: runState.consecutiveErrors,
|
|
202
264
|
});
|
|
203
265
|
if (recovery) {
|
|
266
|
+
const forcedActionLabel = recovery.forceBrowserAction?.action ?? recovery.forceAction;
|
|
267
|
+
this.bus.emitNow({
|
|
268
|
+
type: 'recovery-fired',
|
|
269
|
+
runId,
|
|
270
|
+
turn: i,
|
|
271
|
+
strategy: recovery.strategy,
|
|
272
|
+
feedback: recovery.feedback,
|
|
273
|
+
...(forcedActionLabel ? { forcedAction: forcedActionLabel } : {}),
|
|
274
|
+
});
|
|
204
275
|
if (this.config.debug) {
|
|
205
276
|
const forced = recovery.forceBrowserAction
|
|
206
277
|
? ` (force: ${recovery.forceBrowserAction.action})`
|
|
@@ -251,6 +322,7 @@ export class BrowserAgent {
|
|
|
251
322
|
// -- 2. Observe (with retry) --
|
|
252
323
|
// Reuse the snapshot from verifyEffect if available (no page mutations between them)
|
|
253
324
|
const observeStartedAt = Date.now();
|
|
325
|
+
this.bus.emitNow({ type: 'observe-started', runId, turn: i });
|
|
254
326
|
const state = this.cachedPostState ?? await withRetry(() => this.driver.observe(), 1, // Observe failures are DOM access issues, not transient — retrying 3x wastes 3s
|
|
255
327
|
retryDelayMs, (attempt, err) => {
|
|
256
328
|
if (this.config.debug) {
|
|
@@ -258,10 +330,29 @@ export class BrowserAgent {
|
|
|
258
330
|
}
|
|
259
331
|
}, scenario.signal);
|
|
260
332
|
this.cachedPostState = undefined;
|
|
333
|
+
const observeDurationMs = Date.now() - observeStartedAt;
|
|
261
334
|
if (phaseTimings.firstObserveMs === undefined) {
|
|
262
|
-
phaseTimings.firstObserveMs =
|
|
335
|
+
phaseTimings.firstObserveMs = observeDurationMs;
|
|
263
336
|
this.onPhaseTiming?.('observe', phaseTimings.firstObserveMs);
|
|
264
337
|
}
|
|
338
|
+
// Snapshot bytes only — never wire the full snapshot, it's huge.
|
|
339
|
+
// Screenshot data URL travels through observe-completed when vision
|
|
340
|
+
// is on so the live viewer can render it without a separate fetch.
|
|
341
|
+
const screenshotDataUrl = state.screenshot
|
|
342
|
+
? (state.screenshot.startsWith('data:')
|
|
343
|
+
? state.screenshot
|
|
344
|
+
: `data:image/jpeg;base64,${state.screenshot}`)
|
|
345
|
+
: undefined;
|
|
346
|
+
this.bus.emitNow({
|
|
347
|
+
type: 'observe-completed',
|
|
348
|
+
runId,
|
|
349
|
+
turn: i,
|
|
350
|
+
url: state.url,
|
|
351
|
+
title: state.title,
|
|
352
|
+
snapshotBytes: state.snapshot.length,
|
|
353
|
+
...(screenshotDataUrl ? { screenshot: screenshotDataUrl } : {}),
|
|
354
|
+
durationMs: observeDurationMs,
|
|
355
|
+
});
|
|
265
356
|
// Auto-navigate: if we're on about:blank with a startUrl, navigate without
|
|
266
357
|
// consuming an LLM turn. The agent always does wait->navigate on blank pages.
|
|
267
358
|
if (state.url === 'about:blank' &&
|
|
@@ -580,18 +671,106 @@ export class BrowserAgent {
|
|
|
580
671
|
const decisionState = forceVision
|
|
581
672
|
? await this.attachDecisionScreenshot(state)
|
|
582
673
|
: state;
|
|
583
|
-
// -- 4. Decide (
|
|
674
|
+
// -- 4. Decide (deterministic patterns → cache → LLM) --
|
|
584
675
|
const decideStartedAt = Date.now();
|
|
585
|
-
|
|
676
|
+
this.bus.emitNow({ type: 'decide-started', runId, turn: i });
|
|
677
|
+
// Lazy decisions, level 1: deterministic UI pattern matching. If the
|
|
678
|
+
// page is a recognized pattern (cookie banner with single Accept,
|
|
679
|
+
// single-button modal close), the action is obvious and we skip the
|
|
680
|
+
// LLM entirely. Pattern matchers are bypassed under the same
|
|
681
|
+
// conditions as the cache below.
|
|
682
|
+
const previousTurn = turns[turns.length - 1];
|
|
683
|
+
const canSkipDecide = !forceVision
|
|
684
|
+
&& !finalExtraContext
|
|
685
|
+
&& !previousTurn?.error
|
|
686
|
+
&& !previousTurn?.verificationFailure
|
|
687
|
+
&& process.env.BAD_PATTERN_SKIP !== '0';
|
|
688
|
+
let patternMatch = null;
|
|
689
|
+
if (canSkipDecide) {
|
|
690
|
+
patternMatch = matchDeterministicPattern(decisionState);
|
|
691
|
+
}
|
|
692
|
+
// Lazy decisions, level 2: in-session decision cache.
|
|
693
|
+
const canUseCache = this.decisionCache !== undefined
|
|
694
|
+
&& canSkipDecide
|
|
695
|
+
&& !patternMatch;
|
|
696
|
+
const cacheKey = canUseCache
|
|
697
|
+
? {
|
|
698
|
+
snapshotHash: DecisionCache.hashSnapshot(decisionState.snapshot),
|
|
699
|
+
url: decisionState.url,
|
|
700
|
+
goal: scenario.goal,
|
|
701
|
+
lastEffect: lastEffectForCacheKey,
|
|
702
|
+
budgetBucket: DecisionCache.budgetBucket(i, maxTurns),
|
|
703
|
+
}
|
|
704
|
+
: undefined;
|
|
705
|
+
const cached = canUseCache && cacheKey
|
|
706
|
+
? this.decisionCache.get(cacheKey)
|
|
707
|
+
: undefined;
|
|
708
|
+
let decision;
|
|
709
|
+
if (patternMatch) {
|
|
710
|
+
// Pattern match — synthesize a decision, no LLM call.
|
|
711
|
+
decision = {
|
|
712
|
+
action: patternMatch.action,
|
|
713
|
+
raw: '[deterministic-pattern]',
|
|
714
|
+
reasoning: patternMatch.reasoning,
|
|
715
|
+
expectedEffect: patternMatch.expectedEffect,
|
|
716
|
+
tokensUsed: 0,
|
|
717
|
+
inputTokens: 0,
|
|
718
|
+
outputTokens: 0,
|
|
719
|
+
};
|
|
720
|
+
this.bus.emitNow({
|
|
721
|
+
type: 'decide-skipped-pattern',
|
|
722
|
+
runId,
|
|
723
|
+
turn: i,
|
|
724
|
+
action: decision.action,
|
|
725
|
+
patternId: patternMatch.patternId,
|
|
726
|
+
});
|
|
586
727
|
if (this.config.debug) {
|
|
587
|
-
console.log(`[Runner]
|
|
728
|
+
console.log(`[Runner] Pattern SKIP (${patternMatch.patternId}, turn ${i}) — skipping LLM`);
|
|
588
729
|
}
|
|
589
|
-
}
|
|
730
|
+
}
|
|
731
|
+
else if (cached) {
|
|
732
|
+
// Cache hit — replay the decision, no LLM call.
|
|
733
|
+
decision = cached.decision;
|
|
734
|
+
this.bus.emitNow({
|
|
735
|
+
type: 'decide-skipped-cached',
|
|
736
|
+
runId,
|
|
737
|
+
turn: i,
|
|
738
|
+
action: decision.action,
|
|
739
|
+
cacheKey: cached.hash,
|
|
740
|
+
});
|
|
741
|
+
if (this.config.debug) {
|
|
742
|
+
console.log(`[Runner] Decision cache HIT (turn ${i}, key ${cached.hash.slice(0, 8)}…) — skipping LLM`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
decision = await withRetry(() => this.brain.decide(scenario.goal, decisionState, finalExtraContext || undefined, { current: i, max: maxTurns }, { forceVision }), retries, retryDelayMs, (attempt, err) => {
|
|
747
|
+
if (this.config.debug) {
|
|
748
|
+
console.log(`[Runner] LLM retry ${attempt}: ${err.message}`);
|
|
749
|
+
}
|
|
750
|
+
}, scenario.signal);
|
|
751
|
+
if (cacheKey && this.decisionCache) {
|
|
752
|
+
// Store the fresh decision so a future identical turn replays it.
|
|
753
|
+
this.decisionCache.set(cacheKey, decision);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const decideDurationMs = Date.now() - decideStartedAt;
|
|
590
757
|
if (phaseTimings.firstDecideMs === undefined) {
|
|
591
|
-
phaseTimings.firstDecideMs =
|
|
758
|
+
phaseTimings.firstDecideMs = decideDurationMs;
|
|
592
759
|
this.onPhaseTiming?.('decide', phaseTimings.firstDecideMs);
|
|
593
760
|
}
|
|
594
761
|
let { action, nextActions, raw, reasoning, plan, currentStep, expectedEffect, tokensUsed, inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens, modelUsed } = decision;
|
|
762
|
+
this.bus.emitNow({
|
|
763
|
+
type: 'decide-completed',
|
|
764
|
+
runId,
|
|
765
|
+
turn: i,
|
|
766
|
+
action,
|
|
767
|
+
...(reasoning ? { reasoning } : {}),
|
|
768
|
+
...(expectedEffect ? { expectedEffect } : {}),
|
|
769
|
+
...(inputTokens !== undefined ? { inputTokens } : {}),
|
|
770
|
+
...(outputTokens !== undefined ? { outputTokens } : {}),
|
|
771
|
+
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
772
|
+
durationMs: decideDurationMs,
|
|
773
|
+
});
|
|
595
774
|
// -- 4b. Override pipeline — scored selection of post-decision overrides --
|
|
596
775
|
const overrideCtx = {
|
|
597
776
|
state,
|
|
@@ -611,6 +790,42 @@ export class BrowserAgent {
|
|
|
611
790
|
reasoning = `${reasoning}\n[${overrideWinner.reasoningTag}] ${overrideWinner.feedback}`;
|
|
612
791
|
expectedEffect = overrideWinner.expectedEffect;
|
|
613
792
|
nextActions = [];
|
|
793
|
+
this.bus.emitNow({
|
|
794
|
+
type: 'override-applied',
|
|
795
|
+
runId,
|
|
796
|
+
turn: i,
|
|
797
|
+
source: 'override-pipeline',
|
|
798
|
+
reasoningTag: overrideWinner.reasoningTag,
|
|
799
|
+
feedback: overrideWinner.feedback,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
// -- 4c. User extension mutateDecision (final say) --
|
|
803
|
+
// Runs AFTER the built-in override pipeline so user extensions can
|
|
804
|
+
// veto or replace any decision the built-ins might have produced.
|
|
805
|
+
// Mutations are emitted as override events on the bus for audit.
|
|
806
|
+
if (this.extensions?.applyMutateDecision) {
|
|
807
|
+
const mutated = this.extensions.applyMutateDecision({ ...decision, action, reasoning, expectedEffect }, {
|
|
808
|
+
goal: scenario.goal,
|
|
809
|
+
turn: i,
|
|
810
|
+
maxTurns,
|
|
811
|
+
state: decisionState,
|
|
812
|
+
...(turns[turns.length - 1]?.error
|
|
813
|
+
? { lastError: turns[turns.length - 1].error }
|
|
814
|
+
: {}),
|
|
815
|
+
});
|
|
816
|
+
if (mutated.mutated) {
|
|
817
|
+
action = mutated.decision.action;
|
|
818
|
+
reasoning = mutated.decision.reasoning ?? reasoning;
|
|
819
|
+
expectedEffect = mutated.decision.expectedEffect ?? expectedEffect;
|
|
820
|
+
this.bus.emitNow({
|
|
821
|
+
type: 'override-applied',
|
|
822
|
+
runId,
|
|
823
|
+
turn: i,
|
|
824
|
+
source: 'extension',
|
|
825
|
+
reasoningTag: mutated.sources.join(','),
|
|
826
|
+
feedback: 'extension mutateDecision applied',
|
|
827
|
+
});
|
|
828
|
+
}
|
|
614
829
|
}
|
|
615
830
|
const turn = {
|
|
616
831
|
turn: i,
|
|
@@ -903,8 +1118,9 @@ export class BrowserAgent {
|
|
|
903
1118
|
// where withOverlayRecovery × withRetry multiplied a 22s timeout to 135s).
|
|
904
1119
|
const executeWallClockMs = 45_000;
|
|
905
1120
|
let execResult;
|
|
1121
|
+
const executeStartedAt = Date.now();
|
|
1122
|
+
this.bus.emitNow({ type: 'execute-started', runId, turn: i, action });
|
|
906
1123
|
try {
|
|
907
|
-
const executeStartedAt = Date.now();
|
|
908
1124
|
const executePromise = withRetry(() => this.driver.execute(action), retries, retryDelayMs, (attempt, err) => {
|
|
909
1125
|
if (this.config.debug) {
|
|
910
1126
|
console.log(`[Runner] Execute retry ${attempt}: ${err.message}`);
|
|
@@ -916,8 +1132,27 @@ export class BrowserAgent {
|
|
|
916
1132
|
phaseTimings.firstExecuteMs = Date.now() - executeStartedAt;
|
|
917
1133
|
this.onPhaseTiming?.('execute', phaseTimings.firstExecuteMs);
|
|
918
1134
|
}
|
|
1135
|
+
this.bus.emitNow({
|
|
1136
|
+
type: 'execute-completed',
|
|
1137
|
+
runId,
|
|
1138
|
+
turn: i,
|
|
1139
|
+
action,
|
|
1140
|
+
success: execResult.success,
|
|
1141
|
+
...(execResult.error ? { error: execResult.error } : {}),
|
|
1142
|
+
...(execResult.bounds ? { bounds: execResult.bounds } : {}),
|
|
1143
|
+
durationMs: Date.now() - executeStartedAt,
|
|
1144
|
+
});
|
|
919
1145
|
}
|
|
920
1146
|
catch (err) {
|
|
1147
|
+
this.bus.emitNow({
|
|
1148
|
+
type: 'execute-completed',
|
|
1149
|
+
runId,
|
|
1150
|
+
turn: i,
|
|
1151
|
+
action,
|
|
1152
|
+
success: false,
|
|
1153
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1154
|
+
durationMs: Date.now() - executeStartedAt,
|
|
1155
|
+
});
|
|
921
1156
|
if (err instanceof StaleRefError) {
|
|
922
1157
|
// Stale ref — re-observe and cache for next turn (avoid double observe).
|
|
923
1158
|
// Only inject available refs, not the full snapshot — next turn's observe
|
|
@@ -1040,6 +1275,8 @@ export class BrowserAgent {
|
|
|
1040
1275
|
}
|
|
1041
1276
|
// -- 8. Post-action verification --
|
|
1042
1277
|
if (expectedEffect && !turn.error) {
|
|
1278
|
+
const verifyStartedAt = Date.now();
|
|
1279
|
+
this.bus.emitNow({ type: 'verify-started', runId, turn: i, expectedEffect });
|
|
1043
1280
|
const verifyResult = await this.verifyEffect(expectedEffect, state, action.action);
|
|
1044
1281
|
turn.verified = verifyResult.verified;
|
|
1045
1282
|
if (!verifyResult.verified) {
|
|
@@ -1051,6 +1288,14 @@ export class BrowserAgent {
|
|
|
1051
1288
|
else if (this.config.debug) {
|
|
1052
1289
|
console.log(`[Runner] Verification passed`);
|
|
1053
1290
|
}
|
|
1291
|
+
this.bus.emitNow({
|
|
1292
|
+
type: 'verify-completed',
|
|
1293
|
+
runId,
|
|
1294
|
+
turn: i,
|
|
1295
|
+
verified: verifyResult.verified,
|
|
1296
|
+
...(verifyResult.reason ? { reason: verifyResult.reason } : {}),
|
|
1297
|
+
durationMs: Date.now() - verifyStartedAt,
|
|
1298
|
+
});
|
|
1054
1299
|
}
|
|
1055
1300
|
else if (!turn.error
|
|
1056
1301
|
&& !this.cachedPostState
|
|
@@ -1068,6 +1313,12 @@ export class BrowserAgent {
|
|
|
1068
1313
|
turn.durationMs = Date.now() - turnStart;
|
|
1069
1314
|
turns.push(turn);
|
|
1070
1315
|
this.onTurn?.(turn);
|
|
1316
|
+
this.bus.emitNow({ type: 'turn-completed', runId, turn: i, turnArtifact: turn });
|
|
1317
|
+
// Stash the expectedEffect so the NEXT turn's cache key includes it.
|
|
1318
|
+
// The post-action page state depends on what the agent claimed would
|
|
1319
|
+
// happen, so two turns with the same snapshot but different last
|
|
1320
|
+
// effects might warrant different decisions.
|
|
1321
|
+
lastEffectForCacheKey = expectedEffect ?? '';
|
|
1071
1322
|
}
|
|
1072
1323
|
catch (err) {
|
|
1073
1324
|
runState.recordError();
|