diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 5b31f546..2d12f5c0 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -923,15 +923,13 @@ fn processUserMessage(self: *Self, user_input: []const u8, record_comment: ?[]co if (self.recorder.file != null) { var recorded_any = false; for (result.tool_calls_made) |tc| { - if (!tc.is_error) { - if (Command.fromToolCall(ma, tc.name, tc.arguments)) |cmd| { - if (!recorded_any) { - if (record_comment) |c| self.recorder.recordComment(c); - recorded_any = true; - } - self.recorder.record(cmd); - } + if (tc.is_error) continue; + const cmd = Command.fromToolCall(ma, tc.name, tc.arguments) orelse continue; + if (!recorded_any) { + if (record_comment) |c| self.recorder.recordComment(c); + recorded_any = true; } + self.recorder.record(cmd); } } diff --git a/src/agent/Command.zig b/src/agent/Command.zig index 97aea05e..129c475e 100644 --- a/src/agent/Command.zig +++ b/src/agent/Command.zig @@ -421,26 +421,26 @@ pub fn noSubstitute(_: std.mem.Allocator, input: []const u8) []const u8 { pub fn toToolCall(arena: std.mem.Allocator, cmd: Command, substitute: SubstituteFn) ?ToolCall { const Action = lp.tools.Action; return switch (cmd) { - .goto => |url| .{ .name = @tagName(Action.goto), .args_json = buildJson(arena, .{ .url = substitute(arena, url) }) }, - .click => |sel| .{ .name = @tagName(Action.click), .args_json = buildJson(arena, .{ .selector = substitute(arena, sel) }) }, - .type_cmd => |args| .{ .name = @tagName(Action.fill), .args_json = buildJson(arena, .{ + .goto => |url| .{ .name = @tagName(Action.goto), .args_json = stringifyJson(arena, .{ .url = substitute(arena, url) }) }, + .click => |sel| .{ .name = @tagName(Action.click), .args_json = stringifyJson(arena, .{ .selector = substitute(arena, sel) }) }, + .type_cmd => |args| .{ .name = @tagName(Action.fill), .args_json = stringifyJson(arena, .{ .selector = substitute(arena, args.selector), .value = args.value, }) }, - .wait => |sel| .{ .name = @tagName(Action.waitForSelector), .args_json = buildJson(arena, .{ .selector = sel }) }, - .scroll => |args| .{ .name = @tagName(Action.scroll), .args_json = buildJson(arena, .{ .x = args.x, .y = args.y }) }, - .hover => |sel| .{ .name = @tagName(Action.hover), .args_json = buildJson(arena, .{ .selector = substitute(arena, sel) }) }, - .select => |args| .{ .name = @tagName(Action.selectOption), .args_json = buildJson(arena, .{ + .wait => |sel| .{ .name = @tagName(Action.waitForSelector), .args_json = stringifyJson(arena, .{ .selector = sel }) }, + .scroll => |args| .{ .name = @tagName(Action.scroll), .args_json = stringifyJson(arena, .{ .x = args.x, .y = args.y }) }, + .hover => |sel| .{ .name = @tagName(Action.hover), .args_json = stringifyJson(arena, .{ .selector = substitute(arena, sel) }) }, + .select => |args| .{ .name = @tagName(Action.selectOption), .args_json = stringifyJson(arena, .{ .selector = substitute(arena, args.selector), .value = substitute(arena, args.value), }) }, - .check => |args| .{ .name = @tagName(Action.setChecked), .args_json = buildJson(arena, .{ + .check => |args| .{ .name = @tagName(Action.setChecked), .args_json = stringifyJson(arena, .{ .selector = substitute(arena, args.selector), .checked = args.checked, }) }, .tree => .{ .name = @tagName(Action.tree), .args_json = "" }, .markdown => .{ .name = @tagName(Action.markdown), .args_json = "" }, - .eval_js => |script| .{ .name = @tagName(Action.eval), .args_json = buildJson(arena, .{ .script = script }) }, + .eval_js => |script| .{ .name = @tagName(Action.eval), .args_json = stringifyJson(arena, .{ .script = script }) }, .extract, .natural_language, .comment, .login, .accept_cookies => null, }; } @@ -501,7 +501,7 @@ fn getJsonString(o: std.json.ObjectMap, key: []const u8) ?[]const u8 { }; } -pub fn buildJson(arena: std.mem.Allocator, value: anytype) []const u8 { +pub fn stringifyJson(arena: std.mem.Allocator, value: anytype) []const u8 { var aw: std.Io.Writer.Allocating = .init(arena); std.json.Stringify.value(value, .{}, &aw.writer) catch return "{}"; return aw.written(); diff --git a/src/agent/CommandExecutor.zig b/src/agent/CommandExecutor.zig index 6d64990c..22248edf 100644 --- a/src/agent/CommandExecutor.zig +++ b/src/agent/CommandExecutor.zig @@ -23,6 +23,10 @@ pub const ExecResult = struct { failed: bool, }; +/// Caller contract: `cmd` must not be `.natural_language`, `.comment`, +/// `.login`, or `.accept_cookies` — those are filtered upstream (see +/// `Agent.runRepl`) because they have no tool mapping and would hit the +/// `unreachable` arm below. pub fn executeWithResult(self: *Self, arena: std.mem.Allocator, cmd: Command.Command) ExecResult { if (cmd == .extract) return self.execExtract(arena, cmd.extract); @@ -64,7 +68,7 @@ fn execExtract(self: *Self, arena: std.mem.Allocator, raw_selector: []const u8) const script = std.fmt.allocPrint( arena, "JSON.stringify(Array.from(document.querySelectorAll({s})).map(el => el.textContent.trim()))", - .{Command.buildJson(arena, selector)}, + .{Command.stringifyJson(arena, selector)}, ) catch return .{ .output = "failed to build extract script", .failed = true }; const result = self.tool_executor.callEval(arena, script); diff --git a/src/agent/Verifier.zig b/src/agent/Verifier.zig index 2d936549..c6d62e65 100644 --- a/src/agent/Verifier.zig +++ b/src/agent/Verifier.zig @@ -67,7 +67,7 @@ fn queryElementProperty(self: *Self, arena: std.mem.Allocator, selector: []const const script = std.fmt.allocPrint( arena, "(function(){{ var el = document.querySelector({s}); return el ? {s} : null; }})()", - .{ Command.buildJson(arena, selector), js_property }, + .{ Command.stringifyJson(arena, selector), js_property }, ) catch return null; const result = self.tool_executor.callEval(arena, script); if (result.is_error) return null; diff --git a/src/browser/tools.zig b/src/browser/tools.zig index e3bc6002..a1b8470f 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -967,23 +967,22 @@ fn resolveBySelector(session: *lp.Session, selector: []const u8) ToolError!NodeA const ParseArgsError = error{ OutOfMemory, InvalidParams }; -/// For tools where every field is optional. Missing args → default `T`; -/// wrong-typed args still error (don't silently default). -fn parseArgsOrDefault(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value) ParseArgsError!T { - const args_raw = arguments orelse return .{}; - return std.json.parseFromValueLeaky(T, arena, args_raw, .{ .ignore_unknown_fields = true }) catch |err| switch (err) { +fn parseValue(comptime T: type, arena: std.mem.Allocator, value: std.json.Value) ParseArgsError!T { + return std.json.parseFromValueLeaky(T, arena, value, .{ .ignore_unknown_fields = true }) catch |err| switch (err) { error.OutOfMemory => error.OutOfMemory, else => error.InvalidParams, }; } +/// For tools where every field is optional. Missing args → default `T`; +/// wrong-typed args still error (don't silently default). +fn parseArgsOrDefault(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value) ParseArgsError!T { + return parseValue(T, arena, arguments orelse return .{}); +} + /// Required-args parse: missing or malformed both surface as `InvalidParams`. fn parseArgs(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value) ParseArgsError!T { - const args_raw = arguments orelse return error.InvalidParams; - return std.json.parseFromValueLeaky(T, arena, args_raw, .{ .ignore_unknown_fields = true }) catch |err| switch (err) { - error.OutOfMemory => error.OutOfMemory, - else => error.InvalidParams, - }; + return parseValue(T, arena, arguments orelse return error.InvalidParams); } pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8 { @@ -991,10 +990,10 @@ pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8 // Pages routinely contain `$5.99`-style content where `$` is incidental. // Lowercase `$lp_…` falls through here too — `std.posix.getenv` is // case-sensitive on Linux, so it would never resolve anyway. - if (std.mem.indexOf(u8, input, "$LP_") == null) return input; + const first_lp = std.mem.indexOf(u8, input, "$LP_") orelse return input; var result: std.ArrayList(u8) = .empty; - var i: usize = 0; + var i: usize = first_lp; var last_copy: usize = 0; while (std.mem.indexOfScalarPos(u8, input, i, '$')) |dollar| { const var_start = dollar + 1;