refactor: unify URL handling and clean up agent logic

This commit is contained in:
Adrià Arrufat
2026-05-10 00:04:51 +02:00
parent a5e7ec16be
commit e7d6597e08
6 changed files with 24 additions and 25 deletions

View File

@@ -911,11 +911,8 @@ const tool_output_max_bytes: usize = 1 * 1024 * 1024;
fn capToolOutput(allocator: std.mem.Allocator, output: []const u8) []const u8 {
if (output.len <= tool_output_max_bytes) return output;
const prefix = output[0..tool_output_max_bytes];
return std.fmt.allocPrint(
allocator,
"{s}\n...[truncated, original {d} bytes]",
.{ prefix, output.len },
) catch prefix;
const suffix = std.fmt.allocPrint(allocator, "\n...[truncated, original {d} bytes]", .{output.len}) catch return prefix;
return std.mem.concat(allocator, u8, &.{ prefix, suffix }) catch prefix;
}
fn handleToolCall(ctx: *anyopaque, allocator: std.mem.Allocator, tool_name: []const u8, arguments: []const u8) zenai.provider.Client.ToolHandler.Result {

View File

@@ -530,15 +530,7 @@ pub fn fromToolCallValue(tool_name: []const u8, arguments: std.json.Value) ?Comm
} },
.scroll => blk: {
if (obj.get("backendNodeId") != null) break :blk null;
const x: i32 = switch (obj.get("x") orelse std.json.Value{ .integer = 0 }) {
.integer => |i| @intCast(i),
else => 0,
};
const y: i32 = switch (obj.get("y") orelse std.json.Value{ .integer = 0 }) {
.integer => |i| @intCast(i),
else => 0,
};
break :blk .{ .scroll = .{ .x = x, .y = y } };
break :blk .{ .scroll = .{ .x = getJsonI32(obj, "x", 0), .y = getJsonI32(obj, "y", 0) } };
},
else => null,
};
@@ -551,6 +543,13 @@ fn getJsonString(o: std.json.ObjectMap, key: []const u8) ?[]const u8 {
};
}
fn getJsonI32(o: std.json.ObjectMap, key: []const u8, default: i32) i32 {
return switch (o.get(key) orelse return default) {
.integer => |i| std.math.cast(i32, i) orelse default,
else => default,
};
}
// --- Tests ---
test "parse GOTO" {

View File

@@ -516,8 +516,8 @@ fn formatReplResult(arena: std.mem.Allocator, name: []const u8, result: []const
return aw.written();
}
pub fn printError(_: *Self, msg: []const u8) void {
std.debug.print("{s}{s}Error: {s}{s}\n", .{ ansi.bold, ansi.red, msg, ansi.reset });
pub fn printError(self: *Self, msg: []const u8) void {
self.printErrorFmt("{s}", .{msg});
}
pub fn printErrorFmt(_: *Self, comptime fmt: []const u8, args: anytype) void {

View File

@@ -73,8 +73,7 @@ pub fn schemaAllocator(self: *Self) std.mem.Allocator {
}
pub fn getCurrentUrl(self: *Self) []const u8 {
const page = self.session.currentFrame() orelse return "(no page loaded)";
return page.url;
return browser_tools.currentUrlOrPlaceholder(self.session);
}
/// Run a JavaScript expression and return the full result (text + error flag).

View File

@@ -948,6 +948,14 @@ fn execGetUrl(session: *lp.Session) ToolError![]const u8 {
return page.url;
}
/// URL of the active frame, or a stable placeholder when no page is loaded.
/// Use from contexts that just want a string for display/logging; callers
/// that need to react to "no page" should check `currentFrame()` directly.
pub fn currentUrlOrPlaceholder(session: *lp.Session) []const u8 {
const frame = session.currentFrame() orelse return "(no page loaded)";
return frame.url;
}
fn execGetCookies(arena: std.mem.Allocator, session: *lp.Session) ToolError![]const u8 {
const cookies = session.cookie_jar.cookies.items;
if (cookies.len == 0) return "No cookies.";
@@ -1030,6 +1038,7 @@ pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8
const first_lp = std.mem.indexOf(u8, input, "$LP_") orelse return input;
var result: std.ArrayList(u8) = .empty;
result.ensureTotalCapacity(arena, input.len) catch return input;
var i: usize = first_lp;
var last_copy: usize = 0;
while (std.mem.indexOfScalarPos(u8, input, i, '$')) |dollar| {

View File

@@ -288,7 +288,7 @@ fn handleScriptStep(server: *Server, arena: std.mem.Allocator, id: std.json.Valu
}
const result = browser_tools.call(arena, server.session, &server.node_registry, tcv.name, tcv.args) catch |err| {
const url = currentUrl(server) catch "";
const url = browser_tools.currentUrlOrPlaceholder(server.session);
const msg = std.fmt.allocPrint(arena, "{s} failed at line `{s}` (url: {s}): {s}", .{ tcv.name, args.line, url, @errorName(err) }) catch @errorName(err);
return sendErrorContent(server, id, msg);
};
@@ -298,7 +298,7 @@ fn handleScriptStep(server: *Server, arena: std.mem.Allocator, id: std.json.Valu
// roundtrip the same way an exec failure does.
const verification = server.verifier.verify(arena, cmd);
if (verification.result == .failed) {
const url = currentUrl(server) catch "";
const url = browser_tools.currentUrlOrPlaceholder(server.session);
const reason = verification.reason orelse "verification failed";
const msg = std.fmt.allocPrint(arena, "{s} executed at line `{s}` but verification failed (url: {s}): {s}", .{ tcv.name, args.line, url, reason }) catch reason;
return sendErrorContent(server, id, msg);
@@ -381,11 +381,6 @@ fn findLineSpan(content: []const u8, line: []const u8) error{ NotFound, Ambiguou
return found orelse error.NotFound;
}
fn currentUrl(server: *Server) ![]const u8 {
const frame = server.session.currentFrame() orelse return "(no page loaded)";
return frame.url;
}
fn sendErrorContent(server: *Server, id: std.json.Value, msg: []const u8) !void {
const content = [_]protocol.TextContent([]const u8){.{ .text = msg }};
try server.transport.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content, .isError = true });