@tangle-network/browser-agent-driver 0.24.2 → 0.26.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 (66) hide show
  1. package/dist/brain/index.d.ts.map +1 -1
  2. package/dist/brain/index.js +1 -0
  3. package/dist/brain/index.js.map +1 -1
  4. package/dist/cli-preview.d.ts +59 -0
  5. package/dist/cli-preview.d.ts.map +1 -0
  6. package/dist/cli-preview.js +144 -0
  7. package/dist/cli-preview.js.map +1 -0
  8. package/dist/cli-share.d.ts +77 -0
  9. package/dist/cli-share.d.ts.map +1 -0
  10. package/dist/cli-share.js +141 -0
  11. package/dist/cli-share.js.map +1 -0
  12. package/dist/cli-ui.d.ts.map +1 -1
  13. package/dist/cli-ui.js +8 -4
  14. package/dist/cli-ui.js.map +1 -1
  15. package/dist/cli.js +153 -5
  16. package/dist/cli.js.map +1 -1
  17. package/dist/drivers/cursor-overlay.d.ts +18 -10
  18. package/dist/drivers/cursor-overlay.d.ts.map +1 -1
  19. package/dist/drivers/cursor-overlay.js +221 -30
  20. package/dist/drivers/cursor-overlay.js.map +1 -1
  21. package/dist/drivers/overlay-label.d.ts +33 -0
  22. package/dist/drivers/overlay-label.d.ts.map +1 -0
  23. package/dist/drivers/overlay-label.js +92 -0
  24. package/dist/drivers/overlay-label.js.map +1 -0
  25. package/dist/drivers/playwright.d.ts +22 -0
  26. package/dist/drivers/playwright.d.ts.map +1 -1
  27. package/dist/drivers/playwright.js +87 -9
  28. package/dist/drivers/playwright.js.map +1 -1
  29. package/dist/drivers/snapshot.d.ts +12 -0
  30. package/dist/drivers/snapshot.d.ts.map +1 -1
  31. package/dist/drivers/snapshot.js +17 -0
  32. package/dist/drivers/snapshot.js.map +1 -1
  33. package/dist/drivers/types.d.ts +8 -0
  34. package/dist/drivers/types.d.ts.map +1 -1
  35. package/dist/runner/fan-out.d.ts +91 -0
  36. package/dist/runner/fan-out.d.ts.map +1 -0
  37. package/dist/runner/fan-out.js +183 -0
  38. package/dist/runner/fan-out.js.map +1 -0
  39. package/dist/runner/interrupt-controller.d.ts +67 -0
  40. package/dist/runner/interrupt-controller.d.ts.map +1 -0
  41. package/dist/runner/interrupt-controller.js +142 -0
  42. package/dist/runner/interrupt-controller.js.map +1 -0
  43. package/dist/runner/overlay-narration.d.ts +83 -0
  44. package/dist/runner/overlay-narration.d.ts.map +1 -0
  45. package/dist/runner/overlay-narration.js +172 -0
  46. package/dist/runner/overlay-narration.js.map +1 -0
  47. package/dist/runner/runner.d.ts +9 -0
  48. package/dist/runner/runner.d.ts.map +1 -1
  49. package/dist/runner/runner.js +93 -0
  50. package/dist/runner/runner.js.map +1 -1
  51. package/dist/runner/stream-webhook.d.ts +70 -0
  52. package/dist/runner/stream-webhook.d.ts.map +1 -0
  53. package/dist/runner/stream-webhook.js +132 -0
  54. package/dist/runner/stream-webhook.js.map +1 -0
  55. package/dist/skills/macro-loader.d.ts +1 -1
  56. package/dist/skills/macro-loader.d.ts.map +1 -1
  57. package/dist/supervisor/policy.js +11 -0
  58. package/dist/supervisor/policy.js.map +1 -1
  59. package/dist/test-runner.d.ts +7 -0
  60. package/dist/test-runner.d.ts.map +1 -1
  61. package/dist/test-runner.js +3 -0
  62. package/dist/test-runner.js.map +1 -1
  63. package/dist/types.d.ts +36 -1
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/viewer/viewer.html +264 -16
  66. package/package.json +2 -2
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Gen 33 — mid-run parallel fan-out executor.
3
+ *
4
+ * When the agent emits a `fanOut` action in the middle of a run, this
5
+ * module spawns N sub-agents in fresh tabs of the same BrowserContext,
6
+ * runs them in parallel with bounded concurrency, collects their
7
+ * verdicts, and returns a formatted feedback string the parent runner
8
+ * injects back into the agent's next turn.
9
+ *
10
+ * Architecture:
11
+ * - Same BrowserContext so cookies / localStorage / authenticated
12
+ * session carry over to every branch
13
+ * - Fresh Page per sub-agent so their DOM state doesn't collide with
14
+ * the parent page
15
+ * - PlaywrightDriver per sub-agent with showCursor OFF (the parent's
16
+ * cursor overlay is the one the viewer sees; sub-tabs are invisible
17
+ * to the recording)
18
+ * - Hard cap of 8 concurrent sub-agents to prevent runaway spawning
19
+ * - Each sub-agent times out at 60s × maxTurns(8) = 480s max; the
20
+ * outer fanOut times out at 900s regardless
21
+ * - Sub-agent failures are captured, never thrown to the parent —
22
+ * the parent sees "BRANCH 3: error: …" as part of the structured
23
+ * feedback and can choose to retry / ignore / incorporate
24
+ *
25
+ * This is different from Gen 21's parallel-runner.ts, which runs the
26
+ * ENTIRE run as parallel sub-goals decided up front. FanOut is called
27
+ * from inside a turn, with whatever sub-goals the agent synthesized on
28
+ * the fly from the current page's content.
29
+ */
30
+ import { PlaywrightDriver } from '../drivers/playwright.js';
31
+ import { BrowserAgent } from './runner.js';
32
+ /** Hard cap on concurrent sub-agents. Beyond 8 is a footgun. */
33
+ const MAX_CONCURRENT_SUBAGENTS = 8;
34
+ /** Default max turns per sub-agent when the action doesn't specify. */
35
+ const DEFAULT_SUB_MAX_TURNS = 8;
36
+ /** Timeout budget for the whole fan-out, even if individual sub-agents are still running. */
37
+ const FAN_OUT_TIMEOUT_MS = 900_000;
38
+ /**
39
+ * Run a fanOut action. Returns structured per-branch results + a
40
+ * human-readable feedback string.
41
+ */
42
+ export async function executeFanOut(action, opts) {
43
+ const startedAt = Date.now();
44
+ const subGoals = (action.subGoals ?? []).slice(0, MAX_CONCURRENT_SUBAGENTS);
45
+ if (subGoals.length === 0) {
46
+ return {
47
+ branches: [],
48
+ feedback: 'FAN-OUT ERROR: no subGoals specified.',
49
+ totalMs: 0,
50
+ tokensUsed: 0,
51
+ };
52
+ }
53
+ // Run all sub-agents concurrently. Every branch captures its own
54
+ // errors into the result record so one bad branch can't poison the
55
+ // whole fan-out.
56
+ const branches = await Promise.all(subGoals.map((sg, index) => runBranch(sg, index, opts))).catch((err) => {
57
+ // Shouldn't ever reach here — runBranch catches internally — but if
58
+ // Playwright throws a synchronous error from context.newPage we
59
+ // still want a structured shape back.
60
+ return subGoals.map((sg, index) => ({
61
+ index,
62
+ label: sg.label ?? `branch-${index + 1}`,
63
+ url: sg.url,
64
+ goal: sg.goal,
65
+ success: false,
66
+ verdict: `internal fan-out error: ${err.message}`,
67
+ turnsUsed: 0,
68
+ durationMs: 0,
69
+ }));
70
+ });
71
+ const totalMs = Date.now() - startedAt;
72
+ const tokensUsed = branches.reduce((s, b) => s + (b.tokensUsed ?? 0), 0);
73
+ return {
74
+ branches,
75
+ feedback: formatFeedback(branches, action.summarize),
76
+ totalMs,
77
+ tokensUsed,
78
+ };
79
+ }
80
+ async function runBranch(sg, index, opts) {
81
+ const label = sg.label ?? `branch-${index + 1}`;
82
+ const url = sg.url || opts.currentUrl || '';
83
+ const maxTurns = Math.max(1, Math.min(30, sg.maxTurns ?? DEFAULT_SUB_MAX_TURNS));
84
+ const startedAt = Date.now();
85
+ opts.onBranchStart?.(index, label);
86
+ let page;
87
+ try {
88
+ page = await opts.context.newPage();
89
+ const driver = new PlaywrightDriver(page, {
90
+ ...(opts.driverOptions ?? {}),
91
+ // Sub-tabs never show the cursor — the parent tab's overlay is
92
+ // the one the viewer sees. We don't want a phantom cursor fighting
93
+ // over which mouse position is "canonical" in the recording.
94
+ showCursor: false,
95
+ });
96
+ const subAgent = new BrowserAgent({
97
+ driver,
98
+ config: opts.config,
99
+ ...(opts.projectStore ? { projectStore: opts.projectStore } : {}),
100
+ ...(opts.macroPromptBlock ? { macroPromptBlock: opts.macroPromptBlock } : {}),
101
+ });
102
+ const scenario = {
103
+ goal: sg.goal,
104
+ startUrl: url,
105
+ maxTurns,
106
+ };
107
+ const perBranchTimeoutMs = Math.min(FAN_OUT_TIMEOUT_MS, 60_000 * maxTurns);
108
+ const result = await Promise.race([
109
+ subAgent.run(scenario),
110
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`branch ${label} timed out after ${perBranchTimeoutMs}ms`)), perBranchTimeoutMs)),
111
+ ]);
112
+ const maybeTokens = result.totalTokensUsed;
113
+ const branchResult = {
114
+ index,
115
+ label,
116
+ url,
117
+ goal: sg.goal,
118
+ success: Boolean(result.success),
119
+ verdict: (result.reason ?? '').slice(0, 2000),
120
+ turnsUsed: result.turns?.length ?? 0,
121
+ durationMs: Date.now() - startedAt,
122
+ ...(typeof maybeTokens === 'number' ? { tokensUsed: maybeTokens } : {}),
123
+ };
124
+ opts.onBranchComplete?.(index, label, result);
125
+ return branchResult;
126
+ }
127
+ catch (err) {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ return {
130
+ index,
131
+ label,
132
+ url,
133
+ goal: sg.goal,
134
+ success: false,
135
+ verdict: `error: ${msg}`.slice(0, 2000),
136
+ turnsUsed: 0,
137
+ durationMs: Date.now() - startedAt,
138
+ };
139
+ }
140
+ finally {
141
+ if (page && !page.isClosed()) {
142
+ await page.close().catch(() => { });
143
+ }
144
+ }
145
+ }
146
+ /**
147
+ * Format branch results as a single human-readable feedback block the
148
+ * parent agent's Brain can consume on the next turn. JSON-serializable
149
+ * structure first (so the agent can parse verdicts mechanically), then
150
+ * a plaintext summary. The parent model is free to prefer either.
151
+ */
152
+ export function formatFeedback(branches, summarize) {
153
+ const jsonPayload = branches.map((b) => ({
154
+ label: b.label,
155
+ success: b.success,
156
+ verdict: b.verdict,
157
+ turnsUsed: b.turnsUsed,
158
+ durationMs: b.durationMs,
159
+ }));
160
+ const lines = [];
161
+ lines.push(`FAN-OUT RESULTS (${branches.length} branches):`);
162
+ lines.push('```json');
163
+ lines.push(JSON.stringify(jsonPayload, null, 2));
164
+ lines.push('```');
165
+ if (summarize) {
166
+ lines.push('');
167
+ lines.push(`SUMMARIZATION HINT: ${summarize}`);
168
+ }
169
+ lines.push('');
170
+ lines.push('Branch details:');
171
+ for (const b of branches) {
172
+ const status = b.success ? '✓' : '✗';
173
+ lines.push(` [${status}] ${b.label} (${b.turnsUsed} turns, ${Math.round(b.durationMs / 100) / 10}s): ${truncateOneLine(b.verdict, 200)}`);
174
+ }
175
+ return lines.join('\n');
176
+ }
177
+ function truncateOneLine(s, max) {
178
+ const oneLine = s.replace(/\s+/g, ' ').trim();
179
+ return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
180
+ }
181
+ /** Exposed for tests to pin the concurrency cap. */
182
+ export const FAN_OUT_MAX_CONCURRENT = MAX_CONCURRENT_SUBAGENTS;
183
+ //# sourceMappingURL=fan-out.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.js","sourceRoot":"","sources":["../../src/runner/fan-out.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Gen 32 — pause / resume / abort via keyboard in interactive runs.
3
+ *
4
+ * Registers a raw-mode stdin handler that maps three keys:
5
+ *
6
+ * p → pause the runner before the next turn
7
+ * r → resume a paused run
8
+ * q → abort (the same as Ctrl-C, but doesn't kill the TTY)
9
+ *
10
+ * The controller exposes a single `waitIfPaused()` the runner awaits at
11
+ * the top of every turn. When the user hits `p`, the next call blocks
12
+ * on a resume promise. On `r`, the promise resolves and the loop
13
+ * continues. On `q`, it throws an abort signal the runner treats as a
14
+ * graceful cancellation.
15
+ *
16
+ * Design notes:
17
+ * - Raw mode only applies to a TTY. Non-interactive runs (CI,
18
+ * `bad --cases ... --json`) never engage this controller.
19
+ * - Keyboard capture is OPT-IN via `--interrupt`. Without it, the
20
+ * stdin is untouched and this module is a no-op.
21
+ * - Keyboard capture deliberately does NOT interfere with copy/paste
22
+ * — we only listen for the specific bytes `p`, `r`, `q`, and let
23
+ * everything else fall through unmodified.
24
+ */
25
+ import { EventEmitter } from 'node:events';
26
+ export declare class InterruptAborted extends Error {
27
+ constructor();
28
+ }
29
+ export interface InterruptControllerOptions {
30
+ /** stdin — overridable for tests. Default: process.stdin */
31
+ input?: NodeJS.ReadStream;
32
+ /** Called with user-facing status strings so the CLI can render them. */
33
+ onStatus?: (msg: string) => void;
34
+ }
35
+ export declare class InterruptController extends EventEmitter {
36
+ private paused;
37
+ private aborted;
38
+ private resumeResolvers;
39
+ private input;
40
+ private onStatus;
41
+ private attached;
42
+ private handler;
43
+ private prevRaw;
44
+ constructor(opts?: InterruptControllerOptions);
45
+ /**
46
+ * Start listening for keystrokes. Returns a detach function the
47
+ * caller MUST invoke when the run ends (even on error) — otherwise
48
+ * stdin stays in raw mode and the TTY is hosed.
49
+ */
50
+ attach(): () => void;
51
+ detach(): void;
52
+ /**
53
+ * Called from the runner at the top of each turn. If the run is paused,
54
+ * awaits resume. If aborted, throws `InterruptAborted`.
55
+ */
56
+ waitIfPaused(): Promise<void>;
57
+ /** Programmatically pause — useful from tests and SIGUSR1 handlers. */
58
+ pause(): void;
59
+ /** Programmatically resume. */
60
+ resume(): void;
61
+ /** Request a graceful abort. */
62
+ abort(): void;
63
+ get isPaused(): boolean;
64
+ get isAborted(): boolean;
65
+ private onKey;
66
+ }
67
+ //# sourceMappingURL=interrupt-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interrupt-controller.d.ts","sourceRoot":"","sources":["../../src/runner/interrupt-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C,qBAAa,gBAAiB,SAAQ,KAAK;;CAK1C;AAED,MAAM,WAAW,0BAA0B;IACzC,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACzB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAED,qBAAa,mBAAoB,SAAQ,YAAY;IACnD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,QAAQ,CAAqD;IACrE,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,OAAO,CAAQ;gBAEX,IAAI,GAAE,0BAA+B;IAMjD;;;;OAIG;IACH,MAAM,IAAI,MAAM,IAAI;IAYpB,MAAM,IAAI,IAAI;IAed;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAOnC,uEAAuE;IACvE,KAAK,IAAI,IAAI;IAOb,+BAA+B;IAC/B,MAAM,IAAI,IAAI;IASd,gCAAgC;IAChC,KAAK,IAAI,IAAI;IASb,IAAI,QAAQ,IAAI,OAAO,CAAuB;IAC9C,IAAI,SAAS,IAAI,OAAO,CAAwB;IAEhD,OAAO,CAAC,KAAK;CAYd"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Gen 32 — pause / resume / abort via keyboard in interactive runs.
3
+ *
4
+ * Registers a raw-mode stdin handler that maps three keys:
5
+ *
6
+ * p → pause the runner before the next turn
7
+ * r → resume a paused run
8
+ * q → abort (the same as Ctrl-C, but doesn't kill the TTY)
9
+ *
10
+ * The controller exposes a single `waitIfPaused()` the runner awaits at
11
+ * the top of every turn. When the user hits `p`, the next call blocks
12
+ * on a resume promise. On `r`, the promise resolves and the loop
13
+ * continues. On `q`, it throws an abort signal the runner treats as a
14
+ * graceful cancellation.
15
+ *
16
+ * Design notes:
17
+ * - Raw mode only applies to a TTY. Non-interactive runs (CI,
18
+ * `bad --cases ... --json`) never engage this controller.
19
+ * - Keyboard capture is OPT-IN via `--interrupt`. Without it, the
20
+ * stdin is untouched and this module is a no-op.
21
+ * - Keyboard capture deliberately does NOT interfere with copy/paste
22
+ * — we only listen for the specific bytes `p`, `r`, `q`, and let
23
+ * everything else fall through unmodified.
24
+ */
25
+ import { EventEmitter } from 'node:events';
26
+ export class InterruptAborted extends Error {
27
+ constructor() {
28
+ super('run aborted by user (pressed q)');
29
+ this.name = 'InterruptAborted';
30
+ }
31
+ }
32
+ export class InterruptController extends EventEmitter {
33
+ paused = false;
34
+ aborted = false;
35
+ resumeResolvers = [];
36
+ input;
37
+ onStatus;
38
+ attached = false;
39
+ handler = (buf) => this.onKey(buf);
40
+ prevRaw = false;
41
+ constructor(opts = {}) {
42
+ super();
43
+ this.input = opts.input ?? process.stdin;
44
+ this.onStatus = opts.onStatus ?? (() => { });
45
+ }
46
+ /**
47
+ * Start listening for keystrokes. Returns a detach function the
48
+ * caller MUST invoke when the run ends (even on error) — otherwise
49
+ * stdin stays in raw mode and the TTY is hosed.
50
+ */
51
+ attach() {
52
+ if (this.attached)
53
+ return () => this.detach();
54
+ if (!this.input.isTTY)
55
+ return () => { };
56
+ this.prevRaw = this.input.isRaw;
57
+ this.input.setRawMode(true);
58
+ this.input.resume();
59
+ this.input.on('data', this.handler);
60
+ this.attached = true;
61
+ this.onStatus('interrupt keys: [p] pause · [r] resume · [q] abort');
62
+ return () => this.detach();
63
+ }
64
+ detach() {
65
+ if (!this.attached)
66
+ return;
67
+ try {
68
+ this.input.off('data', this.handler);
69
+ this.input.setRawMode(this.prevRaw);
70
+ if (!process.stdin.isTTY || !this.prevRaw) {
71
+ this.input.pause();
72
+ }
73
+ }
74
+ catch { /* best-effort on shutdown */ }
75
+ this.attached = false;
76
+ // Release any awaiters so the runner can unwind.
77
+ for (const r of this.resumeResolvers)
78
+ r();
79
+ this.resumeResolvers = [];
80
+ }
81
+ /**
82
+ * Called from the runner at the top of each turn. If the run is paused,
83
+ * awaits resume. If aborted, throws `InterruptAborted`.
84
+ */
85
+ async waitIfPaused() {
86
+ if (this.aborted)
87
+ throw new InterruptAborted();
88
+ if (!this.paused)
89
+ return;
90
+ await new Promise((resolve) => this.resumeResolvers.push(resolve));
91
+ if (this.aborted)
92
+ throw new InterruptAborted();
93
+ }
94
+ /** Programmatically pause — useful from tests and SIGUSR1 handlers. */
95
+ pause() {
96
+ if (this.paused || this.aborted)
97
+ return;
98
+ this.paused = true;
99
+ this.emit('pause');
100
+ this.onStatus('⏸ paused — press [r] to resume or [q] to abort');
101
+ }
102
+ /** Programmatically resume. */
103
+ resume() {
104
+ if (!this.paused || this.aborted)
105
+ return;
106
+ this.paused = false;
107
+ this.emit('resume');
108
+ this.onStatus('▶ resumed');
109
+ for (const r of this.resumeResolvers)
110
+ r();
111
+ this.resumeResolvers = [];
112
+ }
113
+ /** Request a graceful abort. */
114
+ abort() {
115
+ if (this.aborted)
116
+ return;
117
+ this.aborted = true;
118
+ this.emit('abort');
119
+ this.onStatus('■ aborting — run will stop at the end of this turn');
120
+ for (const r of this.resumeResolvers)
121
+ r();
122
+ this.resumeResolvers = [];
123
+ }
124
+ get isPaused() { return this.paused; }
125
+ get isAborted() { return this.aborted; }
126
+ onKey(buf) {
127
+ // Match a single byte for p/r/q; fall through for everything else.
128
+ // Ctrl-C (0x03) from raw mode — forward as abort, else the process stays alive.
129
+ if (buf.length === 1 && buf[0] === 0x03) {
130
+ this.abort();
131
+ return;
132
+ }
133
+ const ch = buf.toString('utf-8');
134
+ if (ch === 'p' || ch === 'P')
135
+ this.pause();
136
+ else if (ch === 'r' || ch === 'R')
137
+ this.resume();
138
+ else if (ch === 'q' || ch === 'Q')
139
+ this.abort();
140
+ }
141
+ }
142
+ //# sourceMappingURL=interrupt-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interrupt-controller.js","sourceRoot":"","sources":["../../src/runner/interrupt-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Gen 32 — agent narration hooks for the cursor overlay.
3
+ *
4
+ * The runner emits `decide-completed` each turn with the LLM's raw
5
+ * reasoning text. That text carries three distinct signals the overlay
6
+ * wants to surface to a viewer:
7
+ *
8
+ * 1. **Current step** — a short phrase summarizing what the agent is
9
+ * about to do. Extracted from the first sentence of `reasoning`.
10
+ * 2. **Progress** — turn N of maxTurns, plus any inline progress
11
+ * ledger (`Done=[C-001, C-002, ...] Current=C-003`) the agent is
12
+ * already using in OFAC/batch-style prompts.
13
+ * 3. **Verdict moments** — conclusions like "POSITIVE MATCH",
14
+ * "CLEARED", "NEEDS REVIEW" that should fire a celebratory badge.
15
+ *
16
+ * These are pure functions — no I/O, no driver calls. The driver-facing
17
+ * hooks in runner.ts call them, then push results via
18
+ * `driver.setOverlayReasoning` / `setOverlayProgress` / `pushOverlayBadge`.
19
+ *
20
+ * Why a separate module: the runner is already 1500+ lines and dense;
21
+ * parsing logic belongs somewhere testable in isolation. Keeping this
22
+ * pure makes the overlay story auditable without standing up a browser.
23
+ */
24
+ /**
25
+ * Pull the first sentence (or first ~140 chars) of reasoning as a
26
+ * display summary. The reasoning panel renders the full text; this is a
27
+ * hook for callers that want a preview (e.g., badge text).
28
+ */
29
+ export declare function summarizeReasoning(reasoning: string | undefined): string;
30
+ /**
31
+ * Parse a "Current=C-XXX" marker from the reasoning, if present. Used to
32
+ * enrich the progress label ("Turn 27 · C-003"). Returns undefined when
33
+ * the reasoning doesn't carry a ledger marker — NOT every run uses the
34
+ * ledger shape.
35
+ */
36
+ export declare function extractCurrentMarker(reasoning: string | undefined): string | undefined;
37
+ /**
38
+ * Parse a "Done=[...]" ledger from reasoning. Returns the count of
39
+ * completed items, or undefined when no ledger exists. Used to compute
40
+ * a second progress indicator (items done vs items total) orthogonal to
41
+ * turn-of-maxTurns.
42
+ */
43
+ export declare function extractDoneCount(reasoning: string | undefined): number | undefined;
44
+ export interface VerdictEvent {
45
+ kind: 'positive' | 'cleared' | 'review';
46
+ text: string;
47
+ /** Raw verdict substring so we can dedupe later in the session */
48
+ marker: string;
49
+ }
50
+ /**
51
+ * Scan reasoning + action text for new verdict-worthy moments. Returns
52
+ * an array (possibly empty) of events the driver should surface as
53
+ * badges. The caller is responsible for deduplication across turns —
54
+ * this function returns ALL verdict markers visible in the text.
55
+ *
56
+ * For a reasoning string like:
57
+ * "C-003 PUTIN VLADIMIR: POSITIVE MATCH — Russia-EO14024 / SDN"
58
+ * emits: { kind: 'positive', text: 'C-003 PUTIN VLADIMIR · POSITIVE MATCH', marker: 'C-003:POSITIVE MATCH' }
59
+ */
60
+ export declare function detectVerdicts(reasoning: string | undefined): VerdictEvent[];
61
+ /**
62
+ * Stateful tracker that holds the set of verdict markers already
63
+ * surfaced this session, so we don't re-emit the same badge every turn
64
+ * for a verdict the agent keeps mentioning in its progress ledger.
65
+ */
66
+ export declare class VerdictTracker {
67
+ private seen;
68
+ /**
69
+ * Given new reasoning text, return only the verdicts that are NEW
70
+ * (haven't been emitted before in this session).
71
+ */
72
+ accept(reasoning: string | undefined): VerdictEvent[];
73
+ reset(): void;
74
+ }
75
+ /**
76
+ * Build the progress label shown in the top-left chip of the overlay.
77
+ * Combines turn counter with an optional ledger marker.
78
+ *
79
+ * buildProgressLabel(5, 65) → "Turn 5 · 65 max"
80
+ * buildProgressLabel(5, 65, 'C-003') → "Turn 5 · C-003"
81
+ */
82
+ export declare function buildProgressLabel(turn: number, maxTurns: number, marker?: string): string;
83
+ //# sourceMappingURL=overlay-narration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-narration.d.ts","sourceRoot":"","sources":["../../src/runner/overlay-narration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAQxE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKtF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQlF;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,EAAE,CA2C5E;AAED;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAoB;IAEhC;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,EAAE;IAWrD,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAGR"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Gen 32 — agent narration hooks for the cursor overlay.
3
+ *
4
+ * The runner emits `decide-completed` each turn with the LLM's raw
5
+ * reasoning text. That text carries three distinct signals the overlay
6
+ * wants to surface to a viewer:
7
+ *
8
+ * 1. **Current step** — a short phrase summarizing what the agent is
9
+ * about to do. Extracted from the first sentence of `reasoning`.
10
+ * 2. **Progress** — turn N of maxTurns, plus any inline progress
11
+ * ledger (`Done=[C-001, C-002, ...] Current=C-003`) the agent is
12
+ * already using in OFAC/batch-style prompts.
13
+ * 3. **Verdict moments** — conclusions like "POSITIVE MATCH",
14
+ * "CLEARED", "NEEDS REVIEW" that should fire a celebratory badge.
15
+ *
16
+ * These are pure functions — no I/O, no driver calls. The driver-facing
17
+ * hooks in runner.ts call them, then push results via
18
+ * `driver.setOverlayReasoning` / `setOverlayProgress` / `pushOverlayBadge`.
19
+ *
20
+ * Why a separate module: the runner is already 1500+ lines and dense;
21
+ * parsing logic belongs somewhere testable in isolation. Keeping this
22
+ * pure makes the overlay story auditable without standing up a browser.
23
+ */
24
+ const VERDICT_PATTERNS = [
25
+ { re: /\bPOSITIVE\s+MATCH\b/i, kind: 'positive' },
26
+ { re: /\bCLEARED\b/i, kind: 'cleared' },
27
+ { re: /\bNEEDS\s+REVIEW\b/i, kind: 'review' },
28
+ ];
29
+ /**
30
+ * Pull the first sentence (or first ~140 chars) of reasoning as a
31
+ * display summary. The reasoning panel renders the full text; this is a
32
+ * hook for callers that want a preview (e.g., badge text).
33
+ */
34
+ export function summarizeReasoning(reasoning) {
35
+ if (!reasoning)
36
+ return '';
37
+ const collapsed = reasoning.replace(/\s+/g, ' ').trim();
38
+ if (!collapsed)
39
+ return '';
40
+ // Cut at sentence boundary if short; otherwise hard-truncate.
41
+ const firstSentence = collapsed.match(/^(.+?[.!?])\s/);
42
+ if (firstSentence && firstSentence[1].length <= 180)
43
+ return firstSentence[1];
44
+ return collapsed.length > 180 ? collapsed.slice(0, 177).trimEnd() + '…' : collapsed;
45
+ }
46
+ /**
47
+ * Parse a "Current=C-XXX" marker from the reasoning, if present. Used to
48
+ * enrich the progress label ("Turn 27 · C-003"). Returns undefined when
49
+ * the reasoning doesn't carry a ledger marker — NOT every run uses the
50
+ * ledger shape.
51
+ */
52
+ export function extractCurrentMarker(reasoning) {
53
+ if (!reasoning)
54
+ return undefined;
55
+ const m = reasoning.match(/Current\s*=\s*([A-Za-z0-9][\w-]*)/);
56
+ if (m)
57
+ return m[1];
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Parse a "Done=[...]" ledger from reasoning. Returns the count of
62
+ * completed items, or undefined when no ledger exists. Used to compute
63
+ * a second progress indicator (items done vs items total) orthogonal to
64
+ * turn-of-maxTurns.
65
+ */
66
+ export function extractDoneCount(reasoning) {
67
+ if (!reasoning)
68
+ return undefined;
69
+ const m = reasoning.match(/Done\s*=\s*\[([^\]]*)\]/);
70
+ if (!m)
71
+ return undefined;
72
+ const body = m[1].trim();
73
+ if (!body)
74
+ return 0;
75
+ // Count comma-separated entries, tolerate trailing commas
76
+ return body.split(',').filter((s) => s.trim().length > 0).length;
77
+ }
78
+ /**
79
+ * Scan reasoning + action text for new verdict-worthy moments. Returns
80
+ * an array (possibly empty) of events the driver should surface as
81
+ * badges. The caller is responsible for deduplication across turns —
82
+ * this function returns ALL verdict markers visible in the text.
83
+ *
84
+ * For a reasoning string like:
85
+ * "C-003 PUTIN VLADIMIR: POSITIVE MATCH — Russia-EO14024 / SDN"
86
+ * emits: { kind: 'positive', text: 'C-003 PUTIN VLADIMIR · POSITIVE MATCH', marker: 'C-003:POSITIVE MATCH' }
87
+ */
88
+ export function detectVerdicts(reasoning) {
89
+ if (!reasoning)
90
+ return [];
91
+ const out = [];
92
+ // Case: inline customer-ID prefix (OFAC-style)
93
+ // "C-003 confirmed POSITIVE MATCH" / "C-003: CLEARED"
94
+ // Trailer is comma-bounded so enumerations like
95
+ // "C-001 POSITIVE MATCH, C-002 CLEARED, C-003 NEEDS REVIEW"
96
+ // yield one event per customer, not one event for the whole line.
97
+ const inlineRe = /\b([A-Z]-\d{3,4})[^\w]{1,6}([^.,\n]*?(?:POSITIVE\s+MATCH|CLEARED|NEEDS\s+REVIEW)[^.,\n]*)/gi;
98
+ let m;
99
+ const seenInText = new Set();
100
+ while ((m = inlineRe.exec(reasoning)) !== null) {
101
+ const cid = m[1];
102
+ const snippet = m[2].replace(/\s+/g, ' ').trim();
103
+ const kind = snippet.match(/POSITIVE/i) ? 'positive' : snippet.match(/CLEARED/i) ? 'cleared' : 'review';
104
+ const marker = `${cid}:${kind.toUpperCase()}`;
105
+ // Dedupe within a single reasoning string — agent may restate
106
+ // "C-001 POSITIVE" later in the same line without intending a new event.
107
+ if (seenInText.has(marker))
108
+ continue;
109
+ seenInText.add(marker);
110
+ const shortSnippet = snippet.length > 60 ? snippet.slice(0, 57) + '…' : snippet;
111
+ out.push({
112
+ kind,
113
+ text: `${cid} · ${shortSnippet}`,
114
+ marker,
115
+ });
116
+ }
117
+ // Fallback: bare verdict without customer ID. Only emit ONE per reasoning
118
+ // string in this mode (multiple would be noise without context).
119
+ if (out.length === 0) {
120
+ for (const pat of VERDICT_PATTERNS) {
121
+ const vm = reasoning.match(pat.re);
122
+ if (vm) {
123
+ out.push({
124
+ kind: pat.kind,
125
+ text: vm[0].replace(/\s+/g, ' ').trim().toUpperCase(),
126
+ marker: pat.kind.toUpperCase(),
127
+ });
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ return out;
133
+ }
134
+ /**
135
+ * Stateful tracker that holds the set of verdict markers already
136
+ * surfaced this session, so we don't re-emit the same badge every turn
137
+ * for a verdict the agent keeps mentioning in its progress ledger.
138
+ */
139
+ export class VerdictTracker {
140
+ seen = new Set();
141
+ /**
142
+ * Given new reasoning text, return only the verdicts that are NEW
143
+ * (haven't been emitted before in this session).
144
+ */
145
+ accept(reasoning) {
146
+ const found = detectVerdicts(reasoning);
147
+ const fresh = [];
148
+ for (const v of found) {
149
+ if (this.seen.has(v.marker))
150
+ continue;
151
+ this.seen.add(v.marker);
152
+ fresh.push(v);
153
+ }
154
+ return fresh;
155
+ }
156
+ reset() {
157
+ this.seen.clear();
158
+ }
159
+ }
160
+ /**
161
+ * Build the progress label shown in the top-left chip of the overlay.
162
+ * Combines turn counter with an optional ledger marker.
163
+ *
164
+ * buildProgressLabel(5, 65) → "Turn 5 · 65 max"
165
+ * buildProgressLabel(5, 65, 'C-003') → "Turn 5 · C-003"
166
+ */
167
+ export function buildProgressLabel(turn, maxTurns, marker) {
168
+ if (marker)
169
+ return `Turn ${turn} · ${marker}`;
170
+ return `Turn ${turn} / ${maxTurns}`;
171
+ }
172
+ //# sourceMappingURL=overlay-narration.js.map