diff options
| author | Lucas Faria Mendes <dsn.lucas@proton.me> | 2026-02-19 18:01:26 +0000 |
|---|---|---|
| committer | Lucas Faria Mendes <dsn.lucas@proton.me> | 2026-02-19 18:01:26 +0000 |
| commit | e3024d5779e26252f86e4a00d4d5b07708891e3e (patch) | |
| tree | 2a617a3d007b90fa6fe4f7c3208ba63ecfd62f07 /src/main.zig | |
| parent | 0bcf66bd052116c75fb96fec3d8498c1cd3db32d (diff) | |
| download | claude-zig-e3024d5779e26252f86e4a00d4d5b07708891e3e.tar.gz claude-zig-e3024d5779e26252f86e4a00d4d5b07708891e3e.zip | |
idk this code dont make sense
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 292 |
1 files changed, 201 insertions, 91 deletions
diff --git a/src/main.zig b/src/main.zig index 48a1191..169f5b2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,58 @@ const std = @import("std"); +// A single message in the conversation history. We own all string memory. +const Message = struct { + role: []const u8, + content: ?[]const u8, // null for assistant messages that only have tool_calls + // For tool result messages + tool_call_id: ?[]const u8 = null, + // For assistant messages that contain tool calls (raw JSON value kept so + // we can re-serialize it verbatim) + tool_calls_raw: ?std.json.Value = null, + + allocator: std.mem.Allocator, + + fn deinit(self: *Message) void { + self.allocator.free(self.role); + if (self.content) |c| self.allocator.free(c); + if (self.tool_call_id) |id| self.allocator.free(id); + } +}; + +// Serialize the conversation history into the JSON messages array. +fn writeMessages(jw: *std.json.Stringify, messages: []const Message) !void { + try jw.beginArray(); + for (messages) |msg| { + try jw.beginObject(); + try jw.objectField("role"); + try jw.write(msg.role); + + if (msg.tool_call_id) |id| { + // tool result message + try jw.objectField("tool_call_id"); + try jw.write(id); + } + + if (msg.tool_calls_raw) |tc| { + // assistant message with tool_calls: write content as null + try jw.objectField("content"); + try jw.write(null); + try jw.objectField("tool_calls"); + try jw.write(tc); + } else { + try jw.objectField("content"); + if (msg.content) |c| { + try jw.write(c); + } else { + try jw.write(null); + } + } + + try jw.endObject(); + } + try jw.endArray(); +} + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -23,16 +76,52 @@ pub fn main() !void { const api_key = std.posix.getenv("OPENROUTER_API_KEY") orelse @panic("OPENROUTER_API_KEY is not set"); const base_url = std.posix.getenv("OPENROUTER_BASE_URL") orelse "https://openrouter.ai/api/v1"; - // Build request body - var body_out: std.io.Writer.Allocating = .init(allocator); - defer body_out.deinit(); - var jw: std.json.Stringify = .{ .writer = &body_out.writer }; - try jw.write(.{ - .model = "anthropic/claude-haiku-4.5", - .messages = &[_]struct { role: []const u8, content: []const u8 }{ - .{ .role = "user", .content = prompt_str }, - }, - .tools = &[_]struct { + const url_str = try std.fmt.allocPrint(allocator, "{s}/chat/completions", .{base_url}); + defer allocator.free(url_str); + + const auth_value = try std.fmt.allocPrint(allocator, "Bearer {s}", .{api_key}); + defer allocator.free(auth_value); + + // Conversation history — seed with the user prompt. + // In Zig 0.15 ArrayList is unmanaged: init via .{}, pass allocator to each op. + var messages: std.ArrayList(Message) = .{}; + defer { + for (messages.items) |*m| m.deinit(); + messages.deinit(allocator); + } + try messages.append(allocator, .{ + .role = try allocator.dupe(u8, "user"), + .content = try allocator.dupe(u8, prompt_str), + .allocator = allocator, + }); + + var client: std.http.Client = .{ .allocator = allocator }; + defer client.deinit(); + + // Keep parsed responses alive so tool_calls_raw values remain valid. + var parsed_responses: std.ArrayList(std.json.Parsed(std.json.Value)) = .{}; + defer { + for (parsed_responses.items) |*p| p.deinit(); + parsed_responses.deinit(allocator); + } + + // ── Agent loop ──────────────────────────────────────────────────────────── + while (true) { + // Build request body + var body_out: std.io.Writer.Allocating = .init(allocator); + defer body_out.deinit(); + var jw: std.json.Stringify = .{ .writer = &body_out.writer }; + + try jw.beginObject(); + + try jw.objectField("model"); + try jw.write("anthropic/claude-haiku-4.5"); + + try jw.objectField("messages"); + try writeMessages(&jw, messages.items); + + try jw.objectField("tools"); + try jw.write(&[_]struct { type: []const u8, function: struct { name: []const u8, @@ -48,92 +137,113 @@ pub fn main() !void { required: []const []const u8, }, }, - }{ - .{ - .type = "function", - .function = .{ - .name = "Read", - .description = "Read and return the contents of a file", - .parameters = .{ - .type = "object", - .properties = .{ - .file_path = .{ - .type = "string", - .description = "The path to the file to read", - }, + }{.{ + .type = "function", + .function = .{ + .name = "Read", + .description = "Read and return the contents of a file", + .parameters = .{ + .type = "object", + .properties = .{ + .file_path = .{ + .type = "string", + .description = "The path to the file to read", }, - .required = &[_][]const u8{"file_path"}, }, + .required = &[_][]const u8{"file_path"}, }, }, - }, - }); - const body = body_out.written(); - - // Build URL and auth header - const url_str = try std.fmt.allocPrint(allocator, "{s}/chat/completions", .{base_url}); - defer allocator.free(url_str); - - const auth_value = try std.fmt.allocPrint(allocator, "Bearer {s}", .{api_key}); - defer allocator.free(auth_value); - - // Make HTTP request - var client: std.http.Client = .{ .allocator = allocator }; - defer client.deinit(); - - var response_out: std.io.Writer.Allocating = .init(allocator); - defer response_out.deinit(); - - _ = try client.fetch(.{ - .location = .{ .url = url_str }, - .method = .POST, - .payload = body, - .extra_headers = &.{ - .{ .name = "content-type", .value = "application/json" }, - .{ .name = "authorization", .value = auth_value }, - }, - .response_writer = &response_out.writer, - }); - const response_body = response_out.written(); - - // Parse response - const parsed = try std.json.parseFromSlice(std.json.Value, allocator, response_body, .{}); - defer parsed.deinit(); - - const choices = parsed.value.object.get("choices") orelse @panic("No choices in response"); - if (choices.array.items.len == 0) { - @panic("No choices in response"); - } - - std.debug.print("Logs from your program will appear here!\n", .{}); - - const message = choices.array.items[0].object.get("message").?; - - // Check for tool_calls in the response - if (message.object.get("tool_calls")) |tool_calls_val| { - if (tool_calls_val != .null and tool_calls_val.array.items.len > 0) { - const tool_call = tool_calls_val.array.items[0]; - const func = tool_call.object.get("function").?; - const func_name = func.object.get("name").?.string; - const arguments_str = func.object.get("arguments").?.string; - - if (std.mem.eql(u8, func_name, "Read")) { - const args_parsed = try std.json.parseFromSlice(std.json.Value, allocator, arguments_str, .{}); - defer args_parsed.deinit(); - - const file_path = args_parsed.value.object.get("file_path").?.string; - std.debug.print("Reading file: {s}\n", .{file_path}); - - const file_contents = try std.fs.cwd().readFileAlloc(allocator, file_path, std.math.maxInt(usize)); - defer allocator.free(file_contents); - - try std.fs.File.stdout().writeAll(file_contents); - return; + }}); + + try jw.endObject(); + const body = body_out.written(); + + // Send request + var response_out: std.io.Writer.Allocating = .init(allocator); + defer response_out.deinit(); + + _ = try client.fetch(.{ + .location = .{ .url = url_str }, + .method = .POST, + .payload = body, + .extra_headers = &.{ + .{ .name = "content-type", .value = "application/json" }, + .{ .name = "authorization", .value = auth_value }, + }, + .response_writer = &response_out.writer, + }); + const response_body = response_out.written(); + std.debug.print("Response: {s}\n", .{response_body}); + + // Parse and keep alive for tool_calls_raw references + const parsed = try std.json.parseFromSlice(std.json.Value, allocator, response_body, .{}); + try parsed_responses.append(allocator, parsed); + const parsed_ptr = &parsed_responses.items[parsed_responses.items.len - 1]; + + const choices = parsed_ptr.value.object.get("choices") orelse @panic("No choices in response"); + if (choices.array.items.len == 0) @panic("Empty choices array"); + + const choice = choices.array.items[0]; + const finish_reason = choice.object.get("finish_reason").?.string; + const message = choice.object.get("message").?; + + // Check whether this turn has tool calls + const has_tool_calls = blk: { + const tc = message.object.get("tool_calls") orelse break :blk false; + break :blk tc != .null and tc.array.items.len > 0; + }; + + if (has_tool_calls) { + const tc_val = message.object.get("tool_calls").?; + try messages.append(allocator, .{ + .role = try allocator.dupe(u8, "assistant"), + .content = null, + .tool_calls_raw = tc_val, + .allocator = allocator, + }); + + // Execute every tool call and append results + for (tc_val.array.items) |tool_call| { + const call_id = tool_call.object.get("id").?.string; + const func = tool_call.object.get("function").?; + const func_name = func.object.get("name").?.string; + const arguments_str = func.object.get("arguments").?.string; + + std.debug.print("Tool call: {s}\n", .{func_name}); + + var result_owned: []u8 = &.{}; + + if (std.mem.eql(u8, func_name, "Read")) { + const args_parsed = try std.json.parseFromSlice(std.json.Value, allocator, arguments_str, .{}); + defer args_parsed.deinit(); + const file_path = args_parsed.value.object.get("file_path").?.string; + std.debug.print("Reading file: {s}\n", .{file_path}); + result_owned = try std.fs.cwd().readFileAlloc(allocator, file_path, std.math.maxInt(usize)); + } + + try messages.append(allocator, .{ + .role = try allocator.dupe(u8, "tool"), + .content = result_owned, + .tool_call_id = try allocator.dupe(u8, call_id), + .allocator = allocator, + }); } + } else { + // No tool calls — record the assistant reply and stop + const content_val = message.object.get("content") orelse std.json.Value{ .null = {} }; + const content_str = if (content_val == .string) content_val.string else ""; + try messages.append(allocator, .{ + .role = try allocator.dupe(u8, "assistant"), + .content = try allocator.dupe(u8, content_str), + .allocator = allocator, + }); } - } - // No tool call — print the text content directly - const content = message.object.get("content").?.string; - try std.fs.File.stdout().writeAll(content); + // Stop when the model signals it's done + if (std.mem.eql(u8, finish_reason, "stop") or !has_tool_calls) { + const last = messages.items[messages.items.len - 1]; + try std.fs.File.stdout().writeAll(last.content orelse ""); + break; + } + } } |