diff --git a/build.zig.zon b/build.zig.zon index 59cd4f30..ee0220a6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -39,8 +39,8 @@ .hash = "N-V-__8AABGOuAC_dhAN07kfoP4dycCFi8Bka4O-tuhriNH8", }, .zenai = .{ - .url = "git+https://github.com/lightpanda-io/zenai.git#c1defb5f0e87c369f7fe1ee8d8675ba602dd97c4", - .hash = "zenai-0.0.0-iOY_VJK0AwBgzxh3cwzvK3smdRsCXLdmXi7C6N_FrHyq", + .url = "git+https://github.com/lightpanda-io/zenai.git#457ce97336ee6950177ee54843a5cd50f5a19c62", + .hash = "zenai-0.0.0-iOY_VKq0AwBZzXJdVisZas5LYmNaKeiMiPD-FFWM-lDN", }, .isocline = .{ .url = "git+https://github.com/arrufat/isocline.git#48d94027aec0408dc58af9ca2dfedf4720870e8c", diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 57afb2d9..09f7daf1 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -483,7 +483,7 @@ fn runRepl(self: *Agent) void { _ = self.runTurn(.{ .prompt = prompt, .record_comment = line, .label = label }); }, .tool_call => |tc| { - self.terminal.beginTool(tc.name, slash_split.?.rest); + self.terminal.beginTool(tc.name(), slash_split.?.rest); const result = self.cmd_runner.executeWithResult(aa, cmd); self.terminal.endTool(); self.cmd_runner.printResult(cmd, result); @@ -747,8 +747,7 @@ fn isRetryable(cmd: Command) bool { .tool_call => |t| t, else => return false, }; - const action = std.meta.stringToEnum(browser_tools.Action, tc.name) orelse return false; - return switch (action) { + return switch (tc.action) { .fill, .setChecked, .selectOption => true, else => false, }; @@ -841,9 +840,10 @@ fn runHealTurn(self: *Agent, arena: std.mem.Allocator, prompt: []const u8) ![]Co var cmds: std.ArrayList(Command) = .empty; for (result.tool_calls_made) |tc| { if (tc.is_error) continue; + const action = std.meta.stringToEnum(browser_tools.Action, tc.name) orelse continue; // `result.deinit()` (deferred above) frees the args arena before the // caller formats `cmds`; deep-copy into `arena` to outlive it. - const cmd = try Command.fromToolCallOwned(arena, tc.name, tc.arguments); + const cmd = try Command.fromToolCallOwned(arena, action, tc.arguments); if (!cmd.canHeal()) { self.terminal.printInfoFmt( "self-heal: ignoring {s} (navigation and eval are not allowed during heal)", @@ -1016,7 +1016,8 @@ fn processUserMessage(self: *Agent, input: TurnInput) !?[]const u8 { for (result.tool_calls_made, 0..) |tc, i| { if (tc.is_error) continue; if (last_extract_idx) |idx| if (std.mem.eql(u8, tc.name, "extract") and idx != i) continue; - const cmd = Command.fromToolCall(tc.name, tc.arguments); + const action = std.meta.stringToEnum(browser_tools.Action, tc.name) orelse continue; + const cmd = Command.fromToolCall(action, tc.arguments); if (!cmd.isRecorded()) continue; if (!recorded_any) { if (input.record_comment) |c| r.recordComment(c); @@ -1350,18 +1351,18 @@ fn promptNumberedChoice(header: []const u8, items: []const []const u8, default: // --- Tests --- test "canHeal: only page-local DOM commands are allowed" { - // Table driven over the actual tool flags so adding a new tool can't - // silently drift from the heal allow-list. The heal LLM is restricted to - // these tools by Command.canHeal(); navigation and eval are excluded. - const tc_allow = [_][]const u8{ "click", "hover", "waitForSelector", "fill", "selectOption", "setChecked", "scroll", "extract", "press" }; - const tc_deny = [_][]const u8{ "goto", "eval", "tree", "markdown", "search", "links" }; + // Table-driven over the live tool flags so adding a new tool can't + // silently drift from the heal allow-list. + const Action = browser_tools.Action; + const allow = [_]Action{ .click, .hover, .waitForSelector, .fill, .selectOption, .setChecked, .scroll, .extract, .press }; + const deny = [_]Action{ .goto, .eval, .tree, .markdown, .search, .links }; - for (tc_allow) |name| { - const cmd = Command.fromToolCall(name, null); + for (allow) |action| { + const cmd = Command.fromToolCall(action, null); try std.testing.expect(cmd.canHeal()); } - for (tc_deny) |name| { - const cmd = Command.fromToolCall(name, null); + for (deny) |action| { + const cmd = Command.fromToolCall(action, null); try std.testing.expect(!cmd.canHeal()); } diff --git a/src/agent/CommandRunner.zig b/src/agent/CommandRunner.zig index 264398d4..af659eab 100644 --- a/src/agent/CommandRunner.zig +++ b/src/agent/CommandRunner.zig @@ -45,10 +45,10 @@ pub fn executeWithResult(self: *CommandRunner, arena: std.mem.Allocator, cmd: Co .tool_call => |t| t, else => return .{ .text = "internal: command has no tool mapping", .is_error = true }, }; - const substituted = substituteStringArgs(arena, tc.name, tc.args) catch + const substituted = substituteStringArgs(arena, tc.action, tc.args) catch return .{ .text = "out of memory", .is_error = true }; - return browser_tools.call(arena, self.session, self.node_registry, tc.name, substituted) catch |err| .{ - .text = std.fmt.allocPrint(arena, "{s} failed: {s}", .{ tc.name, @errorName(err) }) catch "tool failed", + return browser_tools.call(arena, self.session, self.node_registry, tc.name(), substituted) catch |err| .{ + .text = std.fmt.allocPrint(arena, "{s} failed: {s}", .{ tc.name(), @errorName(err) }) catch "tool failed", .is_error = true, }; } @@ -56,11 +56,11 @@ pub fn executeWithResult(self: *CommandRunner, arena: std.mem.Allocator, cmd: Co /// Resolve `$LP_*` placeholders in string args before the tool runs. `fill`'s /// `value` is excluded — the tool resolves it internally and rewrites the /// result text so the credential never appears in the echoed confirmation. -fn substituteStringArgs(arena: std.mem.Allocator, tool_name: []const u8, args: ?std.json.Value) error{OutOfMemory}!?std.json.Value { +fn substituteStringArgs(arena: std.mem.Allocator, action: browser_tools.Action, args: ?std.json.Value) error{OutOfMemory}!?std.json.Value { const v = args orelse return null; if (v != .object) return v; - const is_fill = std.mem.eql(u8, tool_name, @tagName(browser_tools.Action.fill)); + const is_fill = action == .fill; const needsSub = struct { fn f(is_fill_: bool, key: []const u8, val: std.json.Value) bool { diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 3309c388..32a81b1a 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -173,7 +173,7 @@ fn dispatchBrowserTool( return server.sendError(id, code, @errorName(err)); }; - if (!result.is_error) recordIfActive(server, name, arguments); + if (!result.is_error) recordIfActive(server, action, arguments); try sendToolResultText(server, id, result.text, result.is_error); } @@ -182,16 +182,10 @@ fn surfacesErrorInBand(action: browser_tools.Action) bool { return action == .eval or action == .extract; } -fn surfacesErrorInBandByName(name: []const u8) bool { - const action = std.meta.stringToEnum(browser_tools.Action, name) orelse return false; - return surfacesErrorInBand(action); -} - -fn recordIfActive(server: *Server, name: []const u8, arguments: ?std.json.Value) void { +fn recordIfActive(server: *Server, action: browser_tools.Action, arguments: ?std.json.Value) void { if (server.recorder == null) return; - const cmd = Command.fromToolCall(name, arguments); - // `record` no-ops on non-recorded tools (read-only queries, env probes), - // so the gate lives there — see `Command.isRecorded`. + const cmd = Command.fromToolCall(action, arguments); + // `record` no-ops on non-recorded tools — see `Command.isRecorded`. server.recorder.?.record(cmd); } @@ -271,12 +265,12 @@ fn handleScriptStep(server: *Server, arena: std.mem.Allocator, id: std.json.Valu } const tc = cmd.tool_call; - const result = browser_tools.call(arena, server.session, &server.node_registry, tc.name, tc.args) catch |err| { - if (surfacesErrorInBandByName(tc.name)) { + const result = browser_tools.call(arena, server.session, &server.node_registry, tc.name(), tc.args) catch |err| { + if (surfacesErrorInBand(tc.action)) { return sendErrorContent(server, id, @errorName(err)); } const url = browser_tools.currentUrlOrPlaceholder(server.session); - const msg = std.fmt.allocPrint(arena, "{s} failed at line `{s}` (url: {s}): {s}", .{ tc.name, args.line, url, @errorName(err) }) catch @errorName(err); + const msg = std.fmt.allocPrint(arena, "{s} failed at line `{s}` (url: {s}): {s}", .{ tc.name(), args.line, url, @errorName(err) }) catch @errorName(err); return sendErrorContent(server, id, msg); }; @@ -285,7 +279,7 @@ fn handleScriptStep(server: *Server, arena: std.mem.Allocator, id: std.json.Valu switch (server.verifier.verify(arena, cmd)) { .failed => |reason| { const url = browser_tools.currentUrlOrPlaceholder(server.session); - const msg = std.fmt.allocPrint(arena, "{s} executed at line `{s}` but verification failed (url: {s}): {s}", .{ tc.name, args.line, url, reason }) catch reason; + const msg = std.fmt.allocPrint(arena, "{s} executed at line `{s}` but verification failed (url: {s}): {s}", .{ tc.name(), args.line, url, reason }) catch reason; return sendErrorContent(server, id, msg); }, .passed, .inconclusive => {}, diff --git a/src/script.zig b/src/script.zig index e0dfd3c2..8b8e1957 100644 --- a/src/script.zig +++ b/src/script.zig @@ -343,12 +343,12 @@ test "applyReplacements: heals a multi-line /eval block using iterator span" { var iter: Command.ScriptIterator = .init(arena.allocator(), content); const e1 = (try iter.next()).?; try std.testing.expect(e1.command == .tool_call); - try std.testing.expectEqualStrings("goto", e1.command.tool_call.name); + try std.testing.expectEqualStrings("goto", e1.command.tool_call.name()); const e2 = (try iter.next()).?; try std.testing.expect(e2.command == .tool_call); - try std.testing.expectEqualStrings("eval", e2.command.tool_call.name); + try std.testing.expectEqualStrings("eval", e2.command.tool_call.name()); const e3 = (try iter.next()).?; - try std.testing.expectEqualStrings("click", e3.command.tool_call.name); + try std.testing.expectEqualStrings("click", e3.command.tool_call.name()); try std.testing.expect((try iter.next()) == null); const replacements = [_]Replacement{.{ diff --git a/src/script/Recorder.zig b/src/script/Recorder.zig index e422cc55..d81e0208 100644 --- a/src/script/Recorder.zig +++ b/src/script/Recorder.zig @@ -365,7 +365,7 @@ test "record and parse: triple-quote round-trip" { const entry = (try iter.next()).?; const parsed_cmd = entry.command; - try std.testing.expectEqualStrings("extract", parsed_cmd.tool_call.name); + try std.testing.expectEqualStrings("extract", parsed_cmd.tool_call.name()); const original_val = original_cmd.tool_call.args.?.object.get("schema").?.string; const parsed_val = parsed_cmd.tool_call.args.?.object.get("schema").?.string; diff --git a/src/script/Verifier.zig b/src/script/Verifier.zig index 56c9f505..7be1a773 100644 --- a/src/script/Verifier.zig +++ b/src/script/Verifier.zig @@ -59,13 +59,12 @@ pub fn verify(self: *Verifier, arena: std.mem.Allocator, cmd: Command) VerifyRes .tool_call => |t| t, else => return .inconclusive, }; - const action = std.meta.stringToEnum(browser_tools.Action, tc.name) orelse return .inconclusive; const args = tc.args orelse return .inconclusive; if (args != .object) return .inconclusive; const selector = (args.object.get("selector") orelse return .inconclusive); if (selector != .string) return .inconclusive; - switch (action) { + switch (tc.action) { .fill => { const value = args.object.get("value") orelse return .inconclusive; if (value != .string) return .inconclusive; diff --git a/src/script/command.zig b/src/script/command.zig index 553816dc..d9839e07 100644 --- a/src/script/command.zig +++ b/src/script/command.zig @@ -22,6 +22,8 @@ const std = @import("std"); const lp = @import("lightpanda"); +const zenai = @import("zenai"); +const browser_tools = lp.tools; const schema = @import("schema.zig"); pub const ParseError = schema.ParseError || error{ @@ -35,16 +37,24 @@ pub const Command = union(enum) { comment: void, pub const ToolCall = struct { - name: []const u8, + action: browser_tools.Action, args: ?std.json.Value, + + pub fn name(self: ToolCall) [:0]const u8 { + return @tagName(self.action); + } }; + fn schemaOf(tc: ToolCall) *const schema.SchemaInfo { + return &schema.globalSchemas()[@intFromEnum(tc.action)]; + } + pub fn isRecorded(self: Command) bool { return switch (self) { .comment => false, .login, .accept_cookies => true, .tool_call => |tc| blk: { - const s = schema.findSchemaCanonical(schema.globalSchemas(), tc.name) orelse break :blk false; + const s = schemaOf(tc); if (!s.recorded) break :blk false; // backendNodeId is invalidated by any DOM mutation, so calls // using it aren't replayable. @@ -57,7 +67,7 @@ pub const Command = union(enum) { pub fn producesData(self: Command) bool { return switch (self) { - .tool_call => |tc| if (schema.findSchemaCanonical(schema.globalSchemas(), tc.name)) |s| s.produces_data else false, + .tool_call => |tc| schemaOf(tc).produces_data, else => false, }; } @@ -71,7 +81,7 @@ pub const Command = union(enum) { pub fn canHeal(self: Command) bool { return switch (self) { - .tool_call => |tc| if (schema.findSchemaCanonical(schema.globalSchemas(), tc.name)) |s| s.can_heal else false, + .tool_call => |tc| schemaOf(tc).can_heal, else => false, }; } @@ -99,7 +109,7 @@ pub const Command = union(enum) { const s = schema.findSchema(schemas, split.name) orelse return error.UnknownTool; const args = try schema.parseValue(arena, s, split.rest); - return .{ .tool_call = .{ .name = s.tool_name, .args = args } }; + return .{ .tool_call = .{ .action = s.action, .args = args } }; } /// Canonical recorder format. Round-trips with `parse`. @@ -112,16 +122,15 @@ pub const Command = union(enum) { } } - /// `name` and `arguments` must outlive the returned Command — use - /// `fromToolCallOwned` to deep-copy when they don't. - pub fn fromToolCall(tool_name: []const u8, arguments: ?std.json.Value) Command { - return .{ .tool_call = .{ .name = tool_name, .args = arguments } }; + /// `arguments` must outlive the returned Command — use `fromToolCallOwned` + /// to deep-copy when it doesn't. + pub fn fromToolCall(action: browser_tools.Action, arguments: ?std.json.Value) Command { + return .{ .tool_call = .{ .action = action, .args = arguments } }; } - pub fn fromToolCallOwned(arena: std.mem.Allocator, tool_name: []const u8, arguments: ?std.json.Value) std.mem.Allocator.Error!Command { - const owned_name = if (schema.findSchemaCanonical(schema.globalSchemas(), tool_name)) |s| s.tool_name else try arena.dupe(u8, tool_name); - const owned_args = if (arguments) |v| try dupeJsonValue(arena, v) else null; - return .{ .tool_call = .{ .name = owned_name, .args = owned_args } }; + pub fn fromToolCallOwned(arena: std.mem.Allocator, action: browser_tools.Action, arguments: ?std.json.Value) std.mem.Allocator.Error!Command { + const owned_args = if (arguments) |v| try zenai.json.dupeValue(arena, v) else null; + return .{ .tool_call = .{ .action = action, .args = owned_args } }; } /// Iterates `.lp` content, gluing multi-line `'''…'''` blocks into a @@ -174,7 +183,7 @@ pub const Command = union(enum) { .opener_line = trimmed, .raw_span = self.lines.buffer[line_start..span_end], .command = .{ .tool_call = .{ - .name = opener.tool_name, + .action = opener.action, .args = .{ .object = obj }, } }, }; @@ -192,7 +201,7 @@ pub const Command = union(enum) { } const BlockOpener = struct { - tool_name: []const u8, + action: browser_tools.Action, field: []const u8, quote_type: QuoteType, }; @@ -203,7 +212,7 @@ pub const Command = union(enum) { const s = schema.findSchema(schemas, split.name) orelse return null; if (!s.isMultiLineCapable()) return null; const qt = QuoteType.fromLiteral(split.rest) orelse return null; - return .{ .tool_name = s.tool_name, .field = s.required[0], .quote_type = qt }; + return .{ .action = s.action, .field = s.required[0], .quote_type = qt }; } fn collectMultiLineBlock(self: *ScriptIterator, quote_type: QuoteType) std.mem.Allocator.Error!?[]const u8 { @@ -227,36 +236,12 @@ pub const Command = union(enum) { }; }; -fn dupeJsonValue(a: std.mem.Allocator, value: std.json.Value) std.mem.Allocator.Error!std.json.Value { - return switch (value) { - .null, .bool, .integer, .float => value, - .number_string => |s| .{ .number_string = try a.dupe(u8, s) }, - .string => |s| .{ .string = try a.dupe(u8, s) }, - .array => |arr| blk: { - var new_arr = try std.json.Array.initCapacity(a, arr.items.len); - for (arr.items) |item| { - new_arr.appendAssumeCapacity(try dupeJsonValue(a, item)); - } - break :blk .{ .array = new_arr }; - }, - .object => |obj| blk: { - var new_obj: std.json.ObjectMap = .init(a); - try new_obj.ensureTotalCapacity(@intCast(obj.count())); - var it = obj.iterator(); - while (it.next()) |entry| { - new_obj.putAssumeCapacity(try a.dupe(u8, entry.key_ptr.*), try dupeJsonValue(a, entry.value_ptr.*)); - } - break :blk .{ .object = new_obj }; - }, - }; -} - // --- Formatting --- fn formatToolCall(tc: Command.ToolCall, writer: *std.Io.Writer) std.Io.Writer.Error!void { - const s_opt = schema.findSchemaCanonical(schema.globalSchemas(), tc.name); + const s = &schema.globalSchemas()[@intFromEnum(tc.action)]; try writer.writeByte('/'); - try writer.writeAll(tc.name); + try writer.writeAll(s.tool_name); const args_val = tc.args orelse return; if (args_val != .object) return; @@ -266,7 +251,7 @@ fn formatToolCall(tc: Command.ToolCall, writer: *std.Io.Writer) std.Io.Writer.Er // Positional form `/goto ''` only when args reduce to the single // required field; extra fields force kv so recordings stay unambiguous. var positional_emitted: ?[]const u8 = null; - if (s_opt) |s| { + { const has_one_required = s.required.len == 1; var visible: usize = 0; var it_v = args.iterator(); @@ -288,7 +273,7 @@ fn formatToolCall(tc: Command.ToolCall, writer: *std.Io.Writer) std.Io.Writer.Er while (it.next()) |entry| { const key = entry.key_ptr.*; if (positional_emitted) |p| if (std.mem.eql(u8, key, p)) continue; - if (s_opt) |s| if (isDefaultTrueBool(s, key, entry.value_ptr.*)) continue; + if (isDefaultTrueBool(s, key, entry.value_ptr.*)) continue; try writer.writeByte(' '); try writer.writeAll(key); try writer.writeByte('='); @@ -402,7 +387,7 @@ test "parse: /goto positional" { defer arena.deinit(); const cmd = try Command.parse(arena.allocator(), "/goto https://example.com"); try testing.expect(cmd == .tool_call); - try testing.expectEqualStrings("goto", cmd.tool_call.name); + try testing.expectEqualStrings("goto", cmd.tool_call.name()); try testing.expectEqualStrings("https://example.com", cmd.tool_call.args.?.object.get("url").?.string); } @@ -463,7 +448,7 @@ test "format: /eval emits triple-quote block for multi-line script" { try obj.put("script", .{ .string = "const x = 1;\nreturn x;" }); break :blk std.json.Value{ .object = obj }; }; - const cmd: Command = .{ .tool_call = .{ .name = "eval", .args = args } }; + const cmd: Command = .{ .tool_call = .{ .action = .eval, .args = args } }; var aw: std.Io.Writer.Allocating = .init(testing.allocator); defer aw.deinit(); @@ -534,13 +519,13 @@ test "ScriptIterator: basic slash commands" { const e1 = (try iter.next()).?; try testing.expect(e1.command == .tool_call); - try testing.expectEqualStrings("goto", e1.command.tool_call.name); + try testing.expectEqualStrings("goto", e1.command.tool_call.name()); const e2 = (try iter.next()).?; - try testing.expectEqualStrings("tree", e2.command.tool_call.name); + try testing.expectEqualStrings("tree", e2.command.tool_call.name()); const e3 = (try iter.next()).?; - try testing.expectEqualStrings("click", e3.command.tool_call.name); + try testing.expectEqualStrings("click", e3.command.tool_call.name()); try testing.expect((try iter.next()) == null); } @@ -560,16 +545,16 @@ test "ScriptIterator: multi-line /eval block" { var iter: Command.ScriptIterator = .init(arena.allocator(), content); const e1 = (try iter.next()).?; - try testing.expectEqualStrings("goto", e1.command.tool_call.name); + try testing.expectEqualStrings("goto", e1.command.tool_call.name()); const e2 = (try iter.next()).?; - try testing.expectEqualStrings("eval", e2.command.tool_call.name); + try testing.expectEqualStrings("eval", e2.command.tool_call.name()); const script_value = e2.command.tool_call.args.?.object.get("script").?.string; try testing.expect(std.mem.indexOf(u8, script_value, "const x = 1;") != null); try testing.expect(std.mem.indexOf(u8, script_value, "return x;") != null); const e3 = (try iter.next()).?; - try testing.expectEqualStrings("tree", e3.command.tool_call.name); + try testing.expectEqualStrings("tree", e3.command.tool_call.name()); try testing.expect((try iter.next()) == null); } @@ -611,14 +596,14 @@ test "ScriptIterator: strips trailing CR from CRLF-authored bodies" { var iter: Command.ScriptIterator = .init(arena.allocator(), content); const e1 = (try iter.next()).?; - try testing.expectEqualStrings("goto", e1.command.tool_call.name); + try testing.expectEqualStrings("goto", e1.command.tool_call.name()); const e2 = (try iter.next()).?; - try testing.expectEqualStrings("extract", e2.command.tool_call.name); + try testing.expectEqualStrings("extract", e2.command.tool_call.name()); try testing.expectEqualStrings("{\"t\":\"h1\"}", e2.command.tool_call.args.?.object.get("schema").?.string); const e3 = (try iter.next()).?; - try testing.expectEqualStrings("click", e3.command.tool_call.name); + try testing.expectEqualStrings("click", e3.command.tool_call.name()); try testing.expect((try iter.next()) == null); } @@ -629,7 +614,7 @@ test "fromToolCall: builds a tool_call Command" { var obj: std.json.ObjectMap = .init(arena.allocator()); try obj.put("url", .{ .string = "https://x" }); - const cmd = Command.fromToolCall("goto", .{ .object = obj }); + const cmd = Command.fromToolCall(.goto, .{ .object = obj }); try testing.expect(cmd == .tool_call); - try testing.expectEqualStrings("goto", cmd.tool_call.name); + try testing.expectEqualStrings("goto", cmd.tool_call.name()); } diff --git a/src/script/schema.zig b/src/script/schema.zig index 5fb957ba..89794efa 100644 --- a/src/script/schema.zig +++ b/src/script/schema.zig @@ -45,6 +45,7 @@ pub const max_hint_slots: usize = 16; /// Cached, schema-extracted view of a single browser tool. pub const SchemaInfo = struct { + action: browser_tools.Action, tool_name: []const u8, description: []const u8, required: []const []const u8, @@ -87,8 +88,9 @@ pub const ParseError = error{ OutOfMemory, }; -fn buildOne(arena: std.mem.Allocator, td: browser_tools.ToolDef, parsed: std.json.Value) !SchemaInfo { +fn buildOne(arena: std.mem.Allocator, action: browser_tools.Action, td: browser_tools.ToolDef, parsed: std.json.Value) !SchemaInfo { var info: SchemaInfo = .{ + .action = action, .tool_name = td.name, .description = td.description, .required = &.{}, @@ -366,7 +368,7 @@ fn initGlobal() void { const parsed = std.json.parseFromSliceLeaky(std.json.Value, a, td.input_schema, .{}) catch |err| { std.debug.panic("failed to parse schema for tool '{s}': {s}", .{ td.name, @errorName(err) }); }; - global_schemas_storage[i] = buildOne(a, td, parsed) catch |err| { + global_schemas_storage[i] = buildOne(a, @enumFromInt(i), td, parsed) catch |err| { std.debug.panic("failed to build schema for tool '{s}': {s}", .{ td.name, @errorName(err) }); }; } @@ -374,7 +376,7 @@ fn initGlobal() void { // --- Tests --- -const testing = std.testing; +const testing = @import("../testing.zig"); test "globalSchemas: comptime tool defs reduce cleanly" { const schemas = globalSchemas();