From e1747d5f2907ec4fb29bf87209863ccbbd3a121f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Sun, 12 Apr 2026 10:20:22 +0200 Subject: [PATCH] agent: improve eval parsing and command recording - Extract `getJsonString` helper in Agent.zig. - Match specific triple quote types in EVAL blocks to allow nested quotes. - Use `cmd.format` and larger buffers in Recorder.zig. --- src/agent/Agent.zig | 36 +++++++++++++++++------------------- src/agent/Command.zig | 33 +++++++++++++++++++++++++-------- src/agent/Recorder.zig | 14 ++++++++++---- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 2afd0e6e..23abcd4d 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -415,31 +415,22 @@ fn toolCallToCommand(arena: std.mem.Allocator, tool_name: []const u8, arguments: else => return null, }; - const getString = struct { - fn f(o: std.json.ObjectMap, key: []const u8) ?[]const u8 { - return switch (o.get(key) orelse return null) { - .string => |s| s, - else => null, - }; - } - }.f; - return switch (action) { - .goto => .{ .goto = getString(obj, "url") orelse return null }, - .click => .{ .click = getString(obj, "selector") orelse return null }, - .hover => .{ .hover = getString(obj, "selector") orelse return null }, - .eval => .{ .eval_js = getString(obj, "script") orelse return null }, - .waitForSelector => .{ .wait = getString(obj, "selector") orelse return null }, + .goto => .{ .goto = getJsonString(obj, "url") orelse return null }, + .click => .{ .click = getJsonString(obj, "selector") orelse return null }, + .hover => .{ .hover = getJsonString(obj, "selector") orelse return null }, + .eval => .{ .eval_js = getJsonString(obj, "script") orelse return null }, + .waitForSelector => .{ .wait = getJsonString(obj, "selector") orelse return null }, .fill => .{ .type_cmd = .{ - .selector = getString(obj, "selector") orelse return null, - .value = getString(obj, "value") orelse return null, + .selector = getJsonString(obj, "selector") orelse return null, + .value = getJsonString(obj, "value") orelse return null, } }, .selectOption => .{ .select = .{ - .selector = getString(obj, "selector") orelse return null, - .value = getString(obj, "value") orelse return null, + .selector = getJsonString(obj, "selector") orelse return null, + .value = getJsonString(obj, "value") orelse return null, } }, .setChecked => .{ .check = .{ - .selector = getString(obj, "selector") orelse return null, + .selector = getJsonString(obj, "selector") orelse return null, .checked = switch (obj.get("checked") orelse return null) { .bool => |b| b, else => return null, @@ -461,6 +452,13 @@ fn toolCallToCommand(arena: std.mem.Allocator, tool_name: []const u8, arguments: }; } +fn getJsonString(o: std.json.ObjectMap, key: []const u8) ?[]const u8 { + return switch (o.get(key) orelse return null) { + .string => |s| s, + else => null, + }; +} + fn getEnvApiKey(provider_type: Config.AiProvider) ?[:0]const u8 { return switch (provider_type) { .anthropic => std.posix.getenv("ANTHROPIC_API_KEY"), diff --git a/src/agent/Command.zig b/src/agent/Command.zig index 1bb0b5a3..4efc4a39 100644 --- a/src/agent/Command.zig +++ b/src/agent/Command.zig @@ -248,9 +248,9 @@ pub const ScriptIterator = struct { const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); if (trimmed.len == 0) continue; - if (isEvalTripleQuote(trimmed)) { + if (isEvalTripleQuote(trimmed)) |quote_type| { const start_line = self.line_num; - if (self.collectEvalBlock()) |js| { + if (self.collectEvalBlock(quote_type)) |js| { return .{ .line_num = start_line, .raw_line = trimmed, @@ -274,21 +274,23 @@ pub const ScriptIterator = struct { return null; } - fn isEvalTripleQuote(line: []const u8) bool { + fn isEvalTripleQuote(line: []const u8) ?[]const u8 { const cmd_end = std.mem.indexOfAny(u8, line, &std.ascii.whitespace) orelse line.len; const cmd_word = line[0..cmd_end]; - if (!std.ascii.eqlIgnoreCase(cmd_word, "EVAL")) return false; + if (!std.ascii.eqlIgnoreCase(cmd_word, "EVAL")) return null; const rest = std.mem.trim(u8, line[cmd_end..], &std.ascii.whitespace); - return std.mem.startsWith(u8, rest, "\"\"\"") or std.mem.startsWith(u8, rest, "'''"); + if (std.mem.startsWith(u8, rest, "\"\"\"")) return "\"\"\""; + if (std.mem.startsWith(u8, rest, "'''")) return "'''"; + return null; } - /// Collect lines until closing triple quote (""" or '''), return the JS content. - fn collectEvalBlock(self: *ScriptIterator) ?[]const u8 { + /// Collect lines until matching closing triple quote, return the JS content. + fn collectEvalBlock(self: *ScriptIterator, quote_type: []const u8) ?[]const u8 { var parts: std.ArrayListUnmanaged(u8) = .empty; while (self.lines.next()) |line| { self.line_num += 1; const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); - if (std.mem.eql(u8, trimmed, "\"\"\"") or std.mem.eql(u8, trimmed, "'''")) { + if (std.mem.eql(u8, trimmed, quote_type)) { return parts.toOwnedSlice(self.allocator) catch null; } if (parts.items.len > 0) { @@ -671,6 +673,21 @@ test "ScriptIterator unterminated EVAL" { try std.testing.expectEqualStrings("unterminated EVAL block", e1.command.natural_language); } +test "ScriptIterator multi-line EVAL mismatched triple quote" { + const script = + \\EVAL """ + \\ const s = " ''' "; + \\ console.log(s); + \\""" + ; + var iter: ScriptIterator = .init(script, std.testing.allocator); + + const e1 = iter.next().?; + try std.testing.expect(e1.command == .eval_js); + try std.testing.expectEqualStrings(" const s = \" ''' \";\n console.log(s);", e1.command.eval_js); + std.testing.allocator.free(e1.command.eval_js); +} + test "trimMatchingQuotes" { const cases = [_]struct { in: []const u8, out: ?[]const u8 }{ .{ .in = "'hello'", .out = "hello" }, diff --git a/src/agent/Recorder.zig b/src/agent/Recorder.zig index a3346ab8..49513c7a 100644 --- a/src/agent/Recorder.zig +++ b/src/agent/Recorder.zig @@ -37,16 +37,22 @@ pub fn record(self: *Self, cmd: Command.Command) void { const f = self.file orelse return; if (!cmd.isRecorded()) return; - var buf: [1024]u8 = undefined; - const line = std.fmt.bufPrint(&buf, "{f}\n", .{cmd}) catch return; - _ = f.write(line) catch return; + var buf: [8192]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + var aw: std.Io.Writer.Allocating = .init(fba.allocator()); + + cmd.format(&aw.writer) catch return; + aw.writer.writeByte('\n') catch return; + + _ = f.write(aw.written()) catch return; self.needs_separator = true; } pub fn recordComment(self: *Self, comment: []const u8) void { const f = self.file orelse return; - var buf: [1024]u8 = undefined; const prefix: []const u8 = if (self.needs_separator) "\n# " else "# "; + + var buf: [8192]u8 = undefined; const line = std.fmt.bufPrint(&buf, "{s}{s}\n", .{ prefix, comment }) catch return; _ = f.write(line) catch return; self.needs_separator = true;