Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1ff2f9d
feat(stagehand): expose typed network capture events
shrey150 Aug 27, 2026
c0de0a1
docs(v4): document network page events
shrey150 Aug 27, 2026
feb1bdb
chore(cli): import Browse V3 baseline
shrey150 Aug 27, 2026
fc52dfc
fix(cli): respect dependency release-age policy
shrey150 Aug 27, 2026
1be9c24
refactor(cli): preserve exact V3 import
shrey150 Aug 27, 2026
34c5c7d
refactor(cli): migrate Browse runtime to Stagehand V4
shrey150 Aug 28, 2026
b30e897
Merge branch 'agent/browse-v4-3-cli-baseline' into agent/browse-v4-4-…
shrey150 Aug 28, 2026
34f3163
refactor(cli): isolate network capture from V4 runtime
shrey150 Aug 28, 2026
cbe4928
test(cli): keep isolated network gap lint-clean
shrey150 Aug 28, 2026
8be49d1
fix(cli): capture network traffic through a CDP sidecar
shrey150 Aug 28, 2026
1233760
Merge branch 'agent/browse-v4-4-runtime' into agent/browse-v4-network…
shrey150 Aug 28, 2026
d26e50e
refactor(cli): preserve the V3 network writer
shrey150 Aug 28, 2026
82661fc
Merge branch 'agent/browse-v4-4-runtime' into agent/browse-v4-network…
shrey150 Aug 28, 2026
037ad0d
style(cli): format network capture adapter
shrey150 Aug 28, 2026
3b55df5
style(cli): format isolated runtime tests
shrey150 Aug 28, 2026
342dcd8
Merge branch 'agent/browse-v4-4-runtime' into agent/browse-v4-network…
shrey150 Aug 28, 2026
df91115
fix(cli): drop the V3 dependency override on V4
shrey150 Aug 28, 2026
86ea880
Merge branch 'agent/browse-v4-4-runtime' into agent/browse-v4-network…
shrey150 Aug 28, 2026
389ed7c
Merge branch 'agent/browse-v4-cli-cursor-overlay' into agent/browse-v…
shrey150 Aug 31, 2026
ce1976e
refactor(cli): inherit split cursor implementation
shrey150 Aug 31, 2026
c11b6cf
Merge branch 'agent/browse-v4-cli-cursor-overlay' into agent/browse-v…
shrey150 Aug 31, 2026
5d39c89
Merge branch 'agent/browse-v4-cli-cursor-overlay' into agent/browse-v…
shrey150 Aug 31, 2026
6507eb5
Merge branch 'agent/browse-v4-cli-cursor-overlay' of https://github.c…
shrey150 Sep 1, 2026
342306b
Merge branch 'agent/browse-v4-cli-cursor-overlay' of https://github.c…
shrey150 Sep 1, 2026
61cc82f
Merge branch 'agent/browse-v4-cli-cursor-overlay' into agent/browse-v…
shrey150 Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cli/src/lib/driver/commands/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import type { DriverCommandHandlers } from "./types.js";
export const networkHandlers: DriverCommandHandlers = {
async "network.on"(manager) {
const page = await manager.activePage();
return manager.network.enable(page);
const websocketUrl = await manager.networkWebSocketDebuggerUrl();
return manager.network.enable(page, websocketUrl);
},

async "network.off"(manager) {
Expand Down
105 changes: 60 additions & 45 deletions packages/cli/src/lib/driver/network-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
getNetworkDir,
writePrivateFile,
} from "./daemon/paths.js";
import { DriverError } from "./errors.js";
import {
NetworkCdpSidecar,
type NetworkCdpSession,
} from "./network-cdp-sidecar.js";

interface PendingRequest {
body: string | null;
Expand All @@ -26,37 +29,40 @@ interface ResponseMetadata {
statusText: string;
}

type CdpSession = {
off?: (event: string, listener: (...args: unknown[]) => void) => void;
on: (event: string, listener: (...args: unknown[]) => void) => void;
send: <T = unknown>(
method: string,
params?: Record<string, unknown>,
) => Promise<T>;
type StagehandV4Page = {
pageId: string;
};

/**
* The V3 Browse network writer, adapted only at the CDP-session boundary.
* Keeping request correlation and the on-disk request/response schema here
* unchanged gives the V4 CLI observable parity without adding a public
* Stagehand network-event API.
*/
export class NetworkCapture {
private cdpSession: CdpSession | null = null;
private cdpSession: NetworkCdpSession | null = null;
private counter = 0;
private enabled = false;
private readonly pendingRequests = new Map<string, PendingRequest>();
private readonly requestDirs = new Map<string, Promise<string | null>>();
private readonly requestStartTimes = new Map<string, number>();
private readonly responseMetadata = new Map<string, ResponseMetadata>();
private readonly listeners: Array<[string, (...args: unknown[]) => void]> =
[];
private readonly listeners: Array<[string, (params: unknown) => void]> = [];
private networkDir: string | null = null;

constructor(private readonly session: string) {}
constructor(
private readonly session: string,
private readonly sidecar = new NetworkCdpSidecar(),
) {}

async enable(
page: unknown,
page: StagehandV4Page,
browserWebSocketDebuggerUrl: string,
): Promise<{ alreadyEnabled?: boolean; enabled: true; path: string }> {
if (this.enabled && this.networkDir) {
if (this.enabled && this.networkDir && this.cdpSession?.connected) {
return { alreadyEnabled: true, enabled: true, path: this.networkDir };
}

const cdpSession = await this.networkCdpSession(page);
if (this.enabled) await this.disable();

await ensureRuntimeDir();
this.networkDir = getNetworkDir(this.session);
Expand All @@ -67,24 +73,34 @@ export class NetworkCapture {
this.requestStartTimes.clear();
this.responseMetadata.clear();

const cdpSession = await this.sidecar.attach(
browserWebSocketDebuggerUrl,
page.pageId,
);
this.cdpSession = cdpSession;
await cdpSession.send("Network.enable", {
maxResourceBufferSize: 5_000_000,
maxTotalBufferSize: 10_000_000,
});
try {
await cdpSession.send("Network.enable", {
maxResourceBufferSize: 5_000_000,
maxTotalBufferSize: 10_000_000,
});

this.addListener("Network.requestWillBeSent", (params) => {
void this.handleRequestWillBeSent(params);
});
this.addListener("Network.responseReceived", (params) => {
this.handleResponseReceived(params);
});
this.addListener("Network.loadingFinished", (params) => {
void this.handleLoadingFinished(params);
});
this.addListener("Network.loadingFailed", (params) => {
void this.handleLoadingFailed(params);
});
this.addListener("Network.requestWillBeSent", (params) => {
void this.handleRequestWillBeSent(params);
});
this.addListener("Network.responseReceived", (params) => {
this.handleResponseReceived(params);
});
this.addListener("Network.loadingFinished", (params) => {
void this.handleLoadingFinished(params);
});
this.addListener("Network.loadingFailed", (params) => {
void this.handleLoadingFailed(params);
});
} catch (error) {
this.cdpSession = null;
await cdpSession.detach().catch(() => undefined);
throw error;
}

this.enabled = true;
return { enabled: true, path: this.networkDir };
Expand All @@ -99,20 +115,27 @@ export class NetworkCapture {
return { alreadyDisabled: true, enabled: false, path: this.networkDir };
}

const cdpSession = this.cdpSession;
this.enabled = false;
for (const [event, listener] of this.listeners) {
this.cdpSession?.off?.(event, listener);
cdpSession?.off(event, listener);
}
this.listeners.length = 0;

await this.cdpSession?.send("Network.disable").catch(() => undefined);
await cdpSession?.send("Network.disable").catch(() => undefined);
await cdpSession?.detach().catch(() => undefined);
this.cdpSession = null;
this.enabled = false;
return { enabled: false, path: this.networkDir };
}

async close(): Promise<void> {
await this.disable().catch(() => undefined);
this.sidecar.close();
}

path(): { enabled: boolean; path: string } {
return {
enabled: this.enabled,
enabled: this.enabled && (this.cdpSession?.connected ?? false),
path: this.networkDir ?? getNetworkDir(this.session),
};
}
Expand Down Expand Up @@ -146,20 +169,12 @@ export class NetworkCapture {

private addListener(
event: string,
listener: (...args: unknown[]) => void,
listener: (params: unknown) => void,
): void {
this.cdpSession?.on(event, listener);
this.listeners.push([event, listener]);
}

private async networkCdpSession(page: unknown): Promise<CdpSession> {
void page;
throw new DriverError(
"Network capture is not available in this Stagehand V4 runtime. Apply the CLI CDP sidecar fast-follow to restore `browse network on`.",
{ code: "network_capture_unavailable" },
);
}

private handleRequestWillBeSent(params: unknown): void {
if (!this.enabled || !this.networkDir) return;
const event = params as {
Expand Down
Loading
Loading