agent: add /clear and /reset REPL commands

- `/clear` forgets conversation history, usage, and node IDs while
  keeping the loaded page and cookies.
- `/reset` does a full reset, starting a fresh browser session.
This commit is contained in:
Adrià Arrufat committed 2026-06-05 14:13:10 +02:00
1 parent 2d2914e878
commit c355b0bda8
3 files changed
+53 -4

No files matched your search

+4 -1
View File
@@ -307,7 +307,10 @@ See [agent-script.md](agent-script.md) for the full script format reference.
the REPL, `/verbosity <low|medium|high>` tunes the log level,
`/effort <none|minimal|low|medium|high|xhigh>` sets the per-turn reasoning
budget (saved to `.lp-agent.zon`), and `/usage` prints cumulative token usage
and the cache hit rate for the session. These are REPL-only and never recorded.
and the cache hit rate for the session — `/clear` forgets the conversation
(history, usage, recorded actions, node IDs) while keeping the loaded page and
cookies, and `/reset` goes further by also starting a fresh browser session,
dropping the page, cookies, storage, and history. These are REPL-only and never recorded.
```
> /goto https://example.com
> /findElement role=button name=Submit
+46 -2
View File
@@ -321,8 +321,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
try self.browser.init(app, .{}, null);
errdefer self.browser.deinit();
self.session = try self.browser.newSession(notification);
self.session.cancel_hook = .{ .context = @ptrCast(self), .check = checkCancel };
try self.startSession();
self.ai_client = if (llm) |l| try zenai.provider.Client.init(allocator, l, .{ .base_url = opts.base_url, .retry_policy = .long_running }) else null;
errdefer if (self.ai_client) |c| c.deinit(allocator);
@@ -361,6 +360,13 @@ pub fn deinit(self: *Agent) void {
self.allocator.destroy(self);
}
/// Create a fresh browser session and wire its cancel hook back to this agent
/// so Ctrl-C aborts in-flight page work. Used at startup and by `/reset`.
fn startSession(self: *Agent) !void {
self.session = try self.browser.newSession(self.notification);
self.session.cancel_hook = .{ .context = @ptrCast(self), .check = checkCancel };
}
// Tool definitions are compile-time constant; project them once per process.
var global_tools_storage: [browser_tools.tool_defs.len]ProviderTool = undefined;
var global_tools_once = std.once(initGlobalTools);
@@ -634,6 +640,8 @@ fn handleMeta(self: *Agent, arena: std.mem.Allocator, meta: *const SlashCommand.
.verbosity => self.handleVerbosity(rest),
.effort => self.handleEffort(rest),
.usage => self.handleUsage(),
.clear => self.handleClear(),
.reset => self.handleReset(),
.save => self.handleSave(arena, rest),
.load => self.handleLoad(rest),
.model => self.handleModel(arena, rest),
@@ -690,6 +698,34 @@ fn handleUsage(self: *Agent) void {
}
}
/// Drop everything tied to the conversation: history (the system prompt
/// re-seeds lazily on the next turn), cumulative usage, the recorded action
/// buffer, and DOM node IDs. Shared by `/clear` and `/reset`.
fn clearConversation(self: *Agent) void {
self.rollbackMessages(0);
self.save_buffer.reset();
self.total_usage = .{};
self.node_registry.reset();
}
/// Forget the conversation while leaving the browser session live — the loaded
/// page stays put and cookies/logins are preserved.
fn handleClear(self: *Agent) void {
self.clearConversation();
self.terminal.printInfo("Cleared conversation, usage, and node IDs. Page and cookies kept.", .{});
}
/// 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.terminal.printError("reset failed: {s}", .{@errorName(err)});
return;
};
self.clearConversation();
self.terminal.printInfo("Reset conversation and browser session. Page, cookies, and storage cleared.", .{});
}
fn handleLoad(self: *Agent, rest: []const u8) void {
const path = std.mem.trim(u8, rest, &std.ascii.whitespace);
if (path.len == 0) {
@@ -1173,6 +1209,14 @@ fn printSlashHelp(self: *Agent, arena: std.mem.Allocator, target: []const u8) vo
"/usage — show cumulative token usage and cache hit rate for this session",
.{},
),
.clear => self.terminal.printInfo(
"/clear — forget the conversation (history, usage, recorded actions, node IDs); keeps the loaded page and cookies",
.{},
),
.reset => self.terminal.printInfo(
"/reset — full reset: everything /clear does plus a new browser session, dropping the page, cookies, storage, and history",
.{},
),
.save => self.terminal.printInfo(
"/save [filename.js] [prompt] — save the session to [filename.js] (a random session-*.js if omitted). With an LLM, synthesizes an idiomatic script from the session and the optional prompt; with --no-llm, dumps the recorded actions verbatim.",
.{},
+3 -1
View File
@@ -47,7 +47,7 @@ pub const MetaCommand = struct {
/// Dispatched by `Agent.handleMeta` via an exhaustive switch so adding
/// a new meta command is a compile error until it's wired up there too.
const Tag = enum { help, quit, verbosity, effort, usage, save, load, model, provider };
const Tag = enum { help, quit, verbosity, effort, usage, clear, reset, save, load, model, provider };
};
pub const meta_commands = [_]MetaCommand{
@@ -56,6 +56,8 @@ pub const meta_commands = [_]MetaCommand{
.{ .tag = .verbosity, .name = "verbosity", .hint = "<low|medium|high>", .values = &.{ "low", "medium", "high" }, .description = "Set agent verbosity" },
.{ .tag = .effort, .name = "effort", .hint = "<none|minimal|low|medium|high|xhigh>", .values = &.{ "none", "minimal", "low", "medium", "high", "xhigh" }, .description = "Set per-turn reasoning effort" },
.{ .tag = .usage, .name = "usage", .hint = "", .values = &.{}, .description = "Show token usage and cache stats for this session" },
.{ .tag = .clear, .name = "clear", .hint = "", .values = &.{}, .description = "Clear conversation history and usage (keeps page/cookies)" },
.{ .tag = .reset, .name = "reset", .hint = "", .values = &.{}, .description = "Reset conversation and browser session (drops page/cookies)" },
.{ .tag = .save, .name = "save", .hint = "[filename.js] [prompt]", .values = &.{}, .description = "Save this session to a file" },
.{ .tag = .load, .name = "load", .hint = "<path>", .values = &.{}, .description = "Load and run a script from disk" },
.{ .tag = .model, .name = "model", .hint = "[name]", .values = &.{}, .description = "Change the model" },