agent: update CLI args and refactor code

This commit is contained in:
Adrià Arrufat
2026-04-09 17:51:56 +02:00
parent 9a8ad8951f
commit 7aa11c7006
5 changed files with 48 additions and 110 deletions

View File

@@ -19,7 +19,7 @@ const default_system_prompt =
\\click links, and extract information.
\\
\\When helping the user, navigate to relevant pages and extract information.
\\Use the semantic_tree or interactiveElements tools to understand page structure
\\Use the semanticTree or interactiveElements tools to understand page structure
\\before clicking or filling forms. Be concise in your responses.
\\
\\IMPORTANT RULES:
@@ -75,11 +75,10 @@ tools: []const zenai.provider.Tool,
model: []const u8,
system_prompt: []const u8,
script_file: ?[]const u8,
record_file: ?[]const u8,
self_heal: bool,
pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Self {
const is_script_mode = opts.script_file != null;
const is_script_mode = opts.script_file != null and !opts.save;
// API key is only required for REPL mode and self-healing
const api_key: ?[:0]const u8 = getEnvApiKey(opts.provider) orelse if (!is_script_mode) {
@@ -118,14 +117,13 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Self
.tool_executor = tool_executor,
.terminal = Terminal.init(null),
.cmd_executor = undefined,
.recorder = Recorder.init(opts.record_file),
.recorder = Recorder.init(if (opts.save) opts.script_file else null),
.messages = .empty,
.message_arena = std.heap.ArenaAllocator.init(allocator),
.tools = tools,
.model = opts.model orelse defaultModel(opts.provider),
.system_prompt = opts.system_prompt orelse default_system_prompt,
.script_file = opts.script_file,
.record_file = opts.record_file,
.script_file = if (!opts.save) opts.script_file else null,
.self_heal = opts.self_heal,
};
@@ -151,8 +149,8 @@ pub fn deinit(self: *Self) void {
}
pub fn run(self: *Self) void {
if (self.script_file) |script_file| {
self.runScript(script_file);
if (self.script_file) |path| {
self.runScript(path);
} else {
self.runRepl();
}
@@ -183,14 +181,12 @@ fn runRepl(self: *Self) void {
.comment => continue,
.login => {
self.processUserMessage(login_prompt, line) catch |err| {
const msg = std.fmt.allocPrint(self.allocator, "LOGIN failed: {s}", .{@errorName(err)}) catch "LOGIN failed";
self.terminal.printError(msg);
self.printAllocError("LOGIN failed: {s}", .{@errorName(err)});
};
},
.accept_cookies => {
self.processUserMessage(accept_cookies_prompt, line) catch |err| {
const msg = std.fmt.allocPrint(self.allocator, "ACCEPT_COOKIES failed: {s}", .{@errorName(err)}) catch "ACCEPT_COOKIES failed";
self.terminal.printError(msg);
self.printAllocError("ACCEPT_COOKIES failed: {s}", .{@errorName(err)});
};
},
.natural_language => {
@@ -198,8 +194,7 @@ fn runRepl(self: *Self) void {
if (std.mem.eql(u8, line, "quit")) break;
self.processUserMessage(line, line) catch |err| {
const msg = std.fmt.allocPrint(self.allocator, "Request failed: {s}", .{@errorName(err)}) catch "Request failed";
self.terminal.printError(msg);
self.printAllocError("Request failed: {s}", .{@errorName(err)});
};
},
else => {
@@ -212,17 +207,24 @@ fn runRepl(self: *Self) void {
self.terminal.printInfo("Goodbye!");
}
fn printAllocError(self: *Self, comptime fmt: []const u8, args: anytype) void {
const msg = std.fmt.allocPrint(self.allocator, fmt, args) catch {
self.terminal.printError(fmt);
return;
};
defer self.allocator.free(msg);
self.terminal.printError(msg);
}
fn runScript(self: *Self, path: []const u8) void {
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
const msg = std.fmt.allocPrint(self.allocator, "Failed to open script '{s}': {s}", .{ path, @errorName(err) }) catch "Failed to open script";
self.terminal.printError(msg);
self.printAllocError("Failed to open script '{s}': {s}", .{ path, @errorName(err) });
return;
};
defer file.close();
const content = file.readToEndAlloc(self.allocator, 10 * 1024 * 1024) catch |err| {
const msg = std.fmt.allocPrint(self.allocator, "Failed to read script: {s}", .{@errorName(err)}) catch "Failed to read script";
self.terminal.printError(msg);
self.printAllocError("Failed to read script: {s}", .{@errorName(err)});
return;
};
defer self.allocator.free(content);
@@ -251,28 +253,25 @@ fn runScript(self: *Self, path: []const u8) void {
continue;
},
.natural_language => {
const msg = std.fmt.allocPrint(self.allocator, "line {d}: unrecognized command: {s}", .{ entry.line_num, entry.raw_line }) catch "unrecognized command";
self.terminal.printError(msg);
self.printAllocError("line {d}: unrecognized command: {s}", .{ entry.line_num, entry.raw_line });
return;
},
.login, .accept_cookies => {
// High-level commands require LLM
if (self.ai_client == null) {
const msg = std.fmt.allocPrint(self.allocator, "line {d}: {s} requires an API key for LLM resolution", .{
self.printAllocError("line {d}: {s} requires an API key for LLM resolution", .{
entry.line_num,
entry.raw_line,
}) catch "LLM required";
self.terminal.printError(msg);
});
return;
}
const prompt = if (entry.command == .login) login_prompt else accept_cookies_prompt;
self.processUserMessage(prompt, "") catch |err| {
const msg = std.fmt.allocPrint(self.allocator, "line {d}: {s} failed: {s}", .{
self.printAllocError("line {d}: {s} failed: {s}", .{
entry.line_num,
entry.raw_line,
@errorName(err),
}) catch "command failed";
self.terminal.printError(msg);
});
return;
};
},
@@ -297,11 +296,10 @@ fn runScript(self: *Self, path: []const u8) void {
continue;
}
}
const msg = std.fmt.allocPrint(self.allocator, "line {d}: command failed: {s}", .{
self.printAllocError("line {d}: command failed: {s}", .{
entry.line_num,
entry.raw_line,
}) catch "command failed";
self.terminal.printError(msg);
});
return;
}
},

View File

@@ -76,39 +76,39 @@ pub fn parse(line: []const u8) Command {
const cmd_word = trimmed[0..cmd_end];
const rest = std.mem.trim(u8, trimmed[cmd_end..], &std.ascii.whitespace);
if (eqlIgnoreCase(cmd_word, "GOTO")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "GOTO")) {
if (rest.len == 0) return .{ .natural_language = trimmed };
return .{ .goto = rest };
}
if (eqlIgnoreCase(cmd_word, "CLICK")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "CLICK")) {
const arg = extractQuoted(rest) orelse rest;
if (arg.len == 0) return .{ .natural_language = trimmed };
return .{ .click = arg };
}
if (eqlIgnoreCase(cmd_word, "TYPE")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "TYPE")) {
const first = extractQuotedWithRemainder(rest) orelse return .{ .natural_language = trimmed };
const second_arg = std.mem.trim(u8, first.remainder, &std.ascii.whitespace);
const second = extractQuoted(second_arg) orelse return .{ .natural_language = trimmed };
return .{ .type_cmd = .{ .selector = first.value, .value = second } };
}
if (eqlIgnoreCase(cmd_word, "WAIT")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "WAIT")) {
const arg = extractQuoted(rest) orelse rest;
if (arg.len == 0) return .{ .natural_language = trimmed };
return .{ .wait = arg };
}
if (eqlIgnoreCase(cmd_word, "TREE")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "TREE")) {
return .{ .tree = {} };
}
if (eqlIgnoreCase(cmd_word, "MARKDOWN") or eqlIgnoreCase(cmd_word, "MD")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "MARKDOWN") or std.ascii.eqlIgnoreCase(cmd_word, "MD")) {
return .{ .markdown = {} };
}
if (eqlIgnoreCase(cmd_word, "EXTRACT")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "EXTRACT")) {
const selector = extractQuoted(rest) orelse {
if (rest.len == 0) return .{ .natural_language = trimmed };
return .{ .extract = .{ .selector = rest, .file = null } };
@@ -123,21 +123,21 @@ pub fn parse(line: []const u8) Command {
return .{ .extract = .{ .selector = selector, .file = null } };
}
if (eqlIgnoreCase(cmd_word, "EVAL")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "EVAL")) {
if (rest.len == 0) return .{ .natural_language = trimmed };
const arg = extractQuoted(rest) orelse rest;
return .{ .eval_js = arg };
}
if (eqlIgnoreCase(cmd_word, "LOGIN")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "LOGIN")) {
return .{ .login = {} };
}
if (eqlIgnoreCase(cmd_word, "ACCEPT_COOKIES") or eqlIgnoreCase(cmd_word, "ACCEPT-COOKIES")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "ACCEPT_COOKIES") or std.ascii.eqlIgnoreCase(cmd_word, "ACCEPT-COOKIES")) {
return .{ .accept_cookies = {} };
}
if (eqlIgnoreCase(cmd_word, "EXIT")) {
if (std.ascii.eqlIgnoreCase(cmd_word, "EXIT")) {
return .{ .exit = {} };
}
@@ -202,7 +202,7 @@ pub const ScriptIterator = struct {
fn isEvalTripleQuote(line: []const u8) bool {
const cmd_end = std.mem.indexOfAny(u8, line, &std.ascii.whitespace) orelse line.len;
const cmd_word = line[0..cmd_end];
if (!eqlIgnoreCase(cmd_word, "EVAL")) return false;
if (!std.ascii.eqlIgnoreCase(cmd_word, "EVAL")) return false;
const rest = std.mem.trim(u8, line[cmd_end..], &std.ascii.whitespace);
return std.mem.startsWith(u8, rest, "\"\"\"") or std.mem.startsWith(u8, rest, "'''");
}
@@ -248,14 +248,6 @@ fn extractQuoted(s: []const u8) ?[]const u8 {
return result.value;
}
pub fn eqlIgnoreCase(a: []const u8, comptime upper: []const u8) bool {
if (a.len != upper.len) return false;
for (a, upper) |ac, uc| {
if (std.ascii.toUpper(ac) != uc) return false;
}
return true;
}
// --- Tests ---
test "parse GOTO" {

View File

@@ -1,4 +1,5 @@
const std = @import("std");
const browser_tools = @import("lightpanda").tools;
const Command = @import("Command.zig");
const ToolExecutor = @import("ToolExecutor.zig");
const Terminal = @import("Terminal.zig");
@@ -126,43 +127,7 @@ fn execExtract(self: *Self, arena: std.mem.Allocator, args: Command.ExtractArgs)
return .{ .output = result, .failed = false };
}
/// Substitute $VAR_NAME references with values from the environment.
fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8 {
// Quick scan: if no $ present, return as-is
if (std.mem.indexOfScalar(u8, input, '$') == null) return input;
var result: std.ArrayListUnmanaged(u8) = .empty;
var i: usize = 0;
while (i < input.len) {
if (input[i] == '$') {
// Find the end of the variable name (alphanumeric + underscore)
const var_start = i + 1;
var var_end = var_start;
while (var_end < input.len and (std.ascii.isAlphanumeric(input[var_end]) or input[var_end] == '_')) {
var_end += 1;
}
if (var_end > var_start) {
const var_name = input[var_start..var_end];
// We need a null-terminated string for getenv
const var_name_z = arena.dupeZ(u8, var_name) catch return input;
if (std.posix.getenv(var_name_z)) |env_val| {
result.appendSlice(arena, env_val) catch return input;
} else {
// Keep the original $VAR if not found
result.appendSlice(arena, input[i..var_end]) catch return input;
}
i = var_end;
} else {
result.append(arena, '$') catch return input;
i += 1;
}
} else {
result.append(arena, input[i]) catch return input;
i += 1;
}
}
return result.toOwnedSlice(arena) catch input;
}
const substituteEnvVars = browser_tools.substituteEnvVars;
/// Escape a string for safe interpolation inside a JS double-quoted string literal.
fn escapeJs(arena: std.mem.Allocator, input: []const u8) []const u8 {