summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLucas Faria Mendes <dsn.lucas@proton.me>2026-02-19 18:01:26 +0000
committerLucas Faria Mendes <dsn.lucas@proton.me>2026-02-19 18:01:26 +0000
commite3024d5779e26252f86e4a00d4d5b07708891e3e (patch)
tree2a617a3d007b90fa6fe4f7c3208ba63ecfd62f07 /src
parent0bcf66bd052116c75fb96fec3d8498c1cd3db32d (diff)
downloadclaude-zig-e3024d5779e26252f86e4a00d4d5b07708891e3e.tar.gz
claude-zig-e3024d5779e26252f86e4a00d4d5b07708891e3e.zip
idk this code dont make sense
Diffstat (limited to 'src')
-rw-r--r--src/main.zig292
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;
+ }
+ }
}