agent: fold the browser quartet onto lp.ToolSession

Agent held the same Browser/Session/Notification/registry fields and
teardown order as mcp/Server.zig, plus its own "enableConsoleCapture
after every newSession" for /reset. ToolSession.restartSession() owns
that now; init is written in terms of it.

Claude-Session: https://claude.ai/code/session_01M6WGk8wZSE28efFQkYT9SK
This commit is contained in:
Adrià Arrufat committed 2026-08-27 16:40:53 +02:00
1 parent dadbae121c
commit 0865f4e337
2 files changed
+29 -38

No files matched your search

+6
View File
@@ -46,6 +46,12 @@ pub fn init(self: *ToolSession, app: *App) !void {
try self.browser.init(app, .{}, null);
errdefer self.browser.deinit();
try self.restartSession();
}
/// Replace the session with a fresh one on the same browser: page, cookies,
/// storage and history gone. The old `session` pointer is invalid afterwards.
pub fn restartSession(self: *ToolSession) !void {
self.session = try self.browser.newSession(self.notification);
try self.session.enableConsoleCapture();
}
+23 -38
View File
@@ -32,7 +32,6 @@ const ScriptRuntime = lp.Runtime;
const Candidate = zenai.provider.Candidate;
const App = @import("../App.zig");
const CDPNode = @import("../cdp/Node.zig");
const Conversation = @import("Conversation.zig");
const Terminal = @import("Terminal.zig");
const SlashCommand = @import("SlashCommand.zig");
@@ -149,10 +148,7 @@ model_base_url: ?[:0]const u8,
/// `model_completion_arena`; invalidated on `/provider` switch.
model_completions: ?ModelCompletions,
model_completion_arena: std.heap.ArenaAllocator,
notification: *lp.Notification,
browser: lp.Browser,
session: *lp.Session,
node_registry: CDPNode.Registry,
ts: lp.ToolSession,
terminal: Terminal,
save_buffer: Recorder,
save_path: ?[]u8,
@@ -308,9 +304,6 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
std.debug.print("\n", .{});
}
const notification: *lp.Notification = try .init(allocator);
errdefer notification.deinit();
const self = try allocator.create(Agent);
errdefer allocator.destroy(self);
@@ -328,10 +321,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
.model_base_url = opts.base_url,
.model_completions = null,
.model_completion_arena = .init(allocator),
.notification = notification,
.browser = undefined,
.session = undefined,
.node_registry = .init(allocator),
.ts = undefined,
.terminal = .init(allocator, history_paths, verbosity, will_repl),
.save_buffer = .init(allocator),
.save_path = null,
@@ -345,16 +335,14 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
.one_shot_attachments = if (opts.attach.items.len == 0) null else opts.attach.items,
.available_providers = available_providers,
};
errdefer self.node_registry.deinit();
errdefer self.terminal.deinit();
errdefer self.conversation.deinit();
self.terminal.installLogSink();
errdefer self.terminal.uninstallLogSink();
try self.browser.init(app, .{}, null);
errdefer self.browser.deinit();
try self.startSession();
try self.ts.init(app);
errdefer self.ts.deinit();
self.installCancelHook();
self.ai_client = if (self.credential) |*c| try zenai.provider.Client.init(lp.io, allocator, c.provider, c.keySlice(), .{ .base_url = opts.base_url, .retry_policy = .long_running, .bill_to = hfBillTo(c.provider), .environ = lp.environ(), .account_id = c.accountId() }) else null;
errdefer if (self.ai_client) |c| c.deinit(allocator);
@@ -381,9 +369,7 @@ pub fn deinit(self: *Agent) void {
self.terminal.deinit();
self.conversation.deinit();
self.model_completion_arena.deinit();
self.node_registry.deinit();
self.browser.deinit();
self.notification.deinit();
self.ts.deinit();
if (self.ai_client) |ai_client| ai_client.deinit(self.allocator);
if (self.credential) |*c| c.deinit(self.allocator);
self.allocator.free(self.model);
@@ -395,15 +381,13 @@ pub fn deinit(self: *Agent) void {
/// isocline idle hook; returns the delay in ms before the next invocation.
fn idlePump(arg: ?*anyopaque) callconv(.c) c_long {
const self: *Agent = @ptrCast(@alignCast(arg.?));
return self.session.idleSlice();
return self.ts.session.idleSlice();
}
/// Create a fresh browser session and wire its cancel hook back to this agent
/// so Ctrl-C aborts in-flight page work. Startup and `/reset`.
fn startSession(self: *Agent) !void {
self.session = try self.browser.newSession(self.notification);
self.session.cancel_hook = .{ .context = @ptrCast(self), .check = checkCancel };
try self.session.enableConsoleCapture();
/// Wire the session's cancel hook back to this agent so Ctrl-C aborts
/// in-flight page work. Startup and `/reset`.
fn installCancelHook(self: *Agent) void {
self.ts.session.cancel_hook = .{ .context = @ptrCast(self), .check = checkCancel };
}
// Compile-time constant; projected once per process to avoid rebuilding per call.
@@ -435,7 +419,7 @@ pub fn requestCancel(self: *Agent) void {
runtime.terminate();
}
}
self.browser.env.terminate();
self.ts.browser.env.terminate();
}
/// Lives in main's stack so it can be registered with the sighandler before the
@@ -477,7 +461,7 @@ fn drainCancellation(self: *Agent, baseline: usize) error{UserCancelled} {
fn resetAfterCancel(self: *Agent, baseline: usize) void {
self.endStreamedText();
self.conversation.rollback(baseline);
self.browser.env.cancelTerminate();
self.ts.browser.env.cancelTerminate();
self.cancel_requested.store(false, .release);
self.http_interrupt.reset();
}
@@ -612,7 +596,7 @@ fn runRepl(self: *Agent) void {
// Slash commands and idle Ctrl-C set the cancel flag without clearing
// V8's terminate state; drain both before the next turn.
if (self.cancel_requested.swap(false, .acq_rel)) {
self.browser.env.cancelTerminate();
self.ts.browser.env.cancelTerminate();
}
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
@@ -630,7 +614,7 @@ fn runRepl(self: *Agent) void {
// `line` keeps the `$LP_*` placeholder so the secret never reaches
// the recorder; only the evaluated copy is expanded.
const script = browser_tools.substituteEnvVars(aa, line) catch line;
const result = browser_tools.evalScript(aa, self.session, &self.node_registry, script) catch |err| {
const result = browser_tools.evalScript(aa, self.ts.session, &self.ts.registry, script) catch |err| {
self.terminal.printError("{s}", .{switch (err) {
error.OutOfMemory => "out of memory",
error.FrameNotLoaded => "no page loaded — run /goto <url> first (Esc exits JS mode)",
@@ -640,7 +624,7 @@ fn runRepl(self: *Agent) void {
};
// Surface console output: slash commands (and thus /consoleLogs)
// are unreachable in JS mode, so a console must echo logs itself.
const logs = std.mem.trimEnd(u8, self.session.drainConsoleMessages(), "\n");
const logs = std.mem.trimEnd(u8, self.ts.session.drainConsoleMessages(), "\n");
if (logs.len > 0) self.printData(logs);
if (result.is_error) {
self.terminal.printError("{s}", .{result.text});
@@ -807,7 +791,7 @@ fn clearConversation(self: *Agent) void {
if (self.save_path) |p| self.allocator.free(p);
self.save_path = null;
self.total_usage = .{};
self.node_registry.reset();
self.ts.registry.reset();
}
/// Forget the conversation while leaving the browser session live — loaded page
@@ -820,10 +804,11 @@ fn handleClear(self: *Agent) void {
/// Full clean slate: everything `/clear` drops, plus a fresh browser session,
/// so the loaded page, cookies, storage, and history are gone too.
fn handleReset(self: *Agent) void {
self.startSession() catch |err| {
self.ts.restartSession() catch |err| {
self.terminal.printError("reset failed: {s}", .{@errorName(err)});
return;
};
self.installCancelHook();
self.clearConversation();
self.terminal.printInfo("Reset conversation and browser session. Page, cookies, and storage cleared.", .{});
}
@@ -1488,7 +1473,7 @@ fn runCommand(self: *Agent, arena: std.mem.Allocator, cmd: Command) browser_tool
.tool_call => |t| t,
else => return .{ .text = "internal: command has no tool mapping", .is_error = true },
};
return browser_tools.call(arena, self.session, &self.node_registry, tc.name(), tc.args) catch |err| .{
return browser_tools.call(arena, self.ts.session, &self.ts.registry, tc.name(), tc.args) catch |err| .{
.text = switch (err) {
error.OutOfMemory => "out of memory",
error.FrameNotLoaded => "no page loaded — run /goto <url> first",
@@ -1548,7 +1533,7 @@ fn runScript(self: *Agent, path: []const u8) bool {
return false;
};
const runtime = ScriptRuntime.init(self.allocator, self.browser.app, self.session, &self.node_registry) catch |err| {
const runtime = ScriptRuntime.init(self.allocator, self.ts.browser.app, self.ts.session, &self.ts.registry) catch |err| {
self.terminal.printError("Failed to initialize script runtime: {s}", .{@errorName(err)});
return false;
};
@@ -1561,7 +1546,7 @@ fn runScript(self: *Agent, path: []const u8) bool {
self.active_script_runtime = null;
self.script_runtime_mutex.unlock(lp.io);
runtime.cancelTerminate();
self.browser.env.cancelTerminate();
self.ts.browser.env.cancelTerminate();
self.cancel_requested.store(false, .release);
}
@@ -1922,7 +1907,7 @@ fn handleToolCall(ctx: *anyopaque, allocator: std.mem.Allocator, tool_name: []co
self.terminal.spinner.setTool(tool_name, args_str);
defer self.terminal.spinner.setThinking();
const outcome: zenai.provider.Client.ToolHandler.Result = if (browser_tools.call(allocator, self.session, &self.node_registry, tool_name, arguments)) |result|
const outcome: zenai.provider.Client.ToolHandler.Result = if (browser_tools.call(allocator, self.ts.session, &self.ts.registry, tool_name, arguments)) |result|
.{ .content = capToolOutput(allocator, tool_name, result.text), .is_error = result.is_error }
else |err|
.{ .content = std.fmt.allocPrint(allocator, "Error: {s}", .{browser_tools.errorMessage(err)}) catch "Error: tool execution failed", .is_error = true };