agent: use dupeMessages from zenai library

This commit is contained in:
Adrià Arrufat
2026-05-05 08:33:43 +02:00
parent 80075efd6b
commit 976d188820
2 changed files with 4 additions and 95 deletions

View File

@@ -39,8 +39,8 @@
.hash = "N-V-__8AAJ4HAgCX79UDBfNwhqAqUVoGC44ib6UYa18q6oa_",
},
.zenai = .{
.url = "git+https://github.com/lightpanda-io/zenai.git#4bc132a1a8b32aa7db23d56e787720c530951030",
.hash = "zenai-0.0.0-iOY_VLtbAwBYwm-NqJjSqRjP_pC1j1WqsoTjNSfqiAhG",
.url = "git+https://github.com/lightpanda-io/zenai.git#317c8b7026319720509e6686eddc41ccbc3c12af",
.hash = "zenai-0.0.0-iOY_VBpsAwDcvCGSoNZAY0-Or7ZK6cMTcpaJVSLRndym",
},
.libidn2 = .{
.url = "https://ftp.gnu.org/gnu/libidn/libidn2-2.3.8.tar.gz",

View File

@@ -696,9 +696,9 @@ fn pruneMessages(self: *Self) void {
// Dupe the kept tail into a scratch slice in the new arena first. Only
// mutate self.messages once every dupe has succeeded — otherwise a
// partial failure would leave self.messages.items[1..] pointing into
// the freed `new_arena`, since dupeMessage already wrote into it.
// the freed `new_arena`.
var new_arena: std.heap.ArenaAllocator = .init(self.allocator);
const duped = dupeMessages(new_arena.allocator(), msgs[tail_start..]) orelse {
const duped = zenai.provider.dupeMessages(new_arena.allocator(), msgs[tail_start..]) catch {
new_arena.deinit();
return;
};
@@ -710,64 +710,6 @@ fn pruneMessages(self: *Self) void {
self.message_arena = new_arena;
}
fn dupeMessages(arena: std.mem.Allocator, msgs: []const zenai.provider.Message) ?[]zenai.provider.Message {
const out = arena.alloc(zenai.provider.Message, msgs.len) catch return null;
for (msgs, 0..) |msg, i| {
out[i] = dupeMessage(arena, msg) orelse return null;
}
return out;
}
fn dupeMessage(alloc: std.mem.Allocator, msg: zenai.provider.Message) ?zenai.provider.Message {
return .{
.role = msg.role,
.content = if (msg.content) |c| alloc.dupe(u8, c) catch return null else null,
.tool_calls = if (msg.tool_calls) |tcs| dupeToolCalls(alloc, tcs) catch return null else null,
.tool_results = if (msg.tool_results) |trs| dupeToolResults(alloc, trs) catch return null else null,
.parts = if (msg.parts) |ps| dupeParts(alloc, ps) catch return null else null,
};
}
fn dupeToolCalls(alloc: std.mem.Allocator, calls: []const zenai.provider.ToolCall) ![]const zenai.provider.ToolCall {
const out = try alloc.alloc(zenai.provider.ToolCall, calls.len);
for (calls, 0..) |tc, i| {
out[i] = .{
.id = try alloc.dupe(u8, tc.id),
.name = try alloc.dupe(u8, tc.name),
.arguments = try alloc.dupe(u8, tc.arguments),
.thought_signature = if (tc.thought_signature) |ts| try alloc.dupe(u8, ts) else null,
};
}
return out;
}
fn dupeToolResults(alloc: std.mem.Allocator, results: []const zenai.provider.ToolResult) ![]const zenai.provider.ToolResult {
const out = try alloc.alloc(zenai.provider.ToolResult, results.len);
for (results, 0..) |tr, i| {
out[i] = .{
.id = try alloc.dupe(u8, tr.id),
.name = try alloc.dupe(u8, tr.name),
.content = try alloc.dupe(u8, tr.content),
.thought_signature = if (tr.thought_signature) |ts| try alloc.dupe(u8, ts) else null,
};
}
return out;
}
fn dupeParts(alloc: std.mem.Allocator, parts: []const zenai.provider.ContentPart) ![]const zenai.provider.ContentPart {
const out = try alloc.alloc(zenai.provider.ContentPart, parts.len);
for (parts, 0..) |p, i| {
out[i] = switch (p) {
.text => |t| .{ .text = try alloc.dupe(u8, t) },
.image => |img| .{ .image = .{
.data = try alloc.dupe(u8, img.data),
.mime_type = try alloc.dupe(u8, img.mime_type),
} },
};
}
return out;
}
/// Self-heal must only patch the current page; navigation and arbitrary
/// scripting are blocked even if the model emits them via `goto` / `eval`.
/// docs/agent.md guarantees "no navigation away from the current page".
@@ -1303,22 +1245,6 @@ test "formatReplacement: multiple commands produce multi-line replacement" {
);
}
test "dupeMessages: happy path" {
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena.deinit();
const src = [_]zenai.provider.Message{
.{ .role = .user, .content = "hello" },
.{ .role = .assistant, .content = "world" },
};
const out = dupeMessages(arena.allocator(), &src) orelse return error.UnexpectedNull;
try std.testing.expectEqual(@as(usize, 2), out.len);
try std.testing.expectEqualStrings("hello", out[0].content.?);
try std.testing.expectEqualStrings("world", out[1].content.?);
try std.testing.expect(out[0].content.?.ptr != src[0].content.?.ptr);
}
test "writeHealedScript: applies replacements and saves backup" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
@@ -1378,20 +1304,3 @@ test "isHealAllowed: blocks goto and eval_js, allows page-local commands" {
try std.testing.expect(isHealAllowed(.{ .check = .{ .selector = "#c", .checked = true } }));
try std.testing.expect(isHealAllowed(.{ .scroll = .{ .x = 0, .y = 100 } }));
}
test "dupeMessages: returns null on mid-iteration alloc failure" {
// The contract pruneMessages depends on: on any partial failure,
// dupeMessages returns null without mutating its inputs. pruneMessages
// can then deinit the scratch arena and leave self.messages untouched.
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena.deinit();
var failing = std.testing.FailingAllocator.init(arena.allocator(), .{ .fail_index = 2 });
const src = [_]zenai.provider.Message{
.{ .role = .user, .content = "hello" },
.{ .role = .assistant, .content = "world" },
.{ .role = .user, .content = "third" },
};
try std.testing.expect(dupeMessages(failing.allocator(), &src) == null);
try std.testing.expect(failing.has_induced_failure);
}