









继人手一个个人博客引擎、Todo List、记帐软件后,现在新入行的开发者是不是应该打造一个自己的 Coding Agent 了?
发了这条动态后我就开始手搓自己的 Coding Agent 了,选择的编程语言是 Zig,目标是制作一个内存占用极小的 Coding Agent CLI 工具。
在实现工具调用的时候,我希望工具可以在一定程度上被并发调用,我选择的方案是任务队列。当 LLM 生成工具调用的时候,就入队一个工具调用请求,固定数量的 Consumers 会分别从各自的调用循环中取出队列中的一个工具调用任务,并在单独的线程中执行工具代码。
实现任务队列的难点在于我一开始带入了 JS/C# 的编程习惯,例如在 C# 中,我们可以很容易地想到利用 Queue 跟 TaskCompletionSource 实现让队列的 Consumer 来唤醒等待任务执行完成的 Producer。但是 Zig 中手动管理内存让这里的情况变得复杂了,因为 Producer 跟 Consumers 同时持有 TaskCompletionSource (假设存在这么一个 struct)的指针,并且都互相不知道其他引用方何时会退出,这使得 TaskCompletionSource 的内存释放时机的选择变得困难。
为了让内存上的管理变得直观,我选择将 TaskCompletionSource 放进一个 HashMap,然后 Producer 跟 Consumer 都通过一个 Id 查表来读写 TaskCompletionSource。
这就由引发了第二个问题,Zig 的 Io 跟 Allocator 一样是一个接口,目前 0.16 版本默认是基于线程的实现,这就意味着 Producer 跟 Consumer 有可能会在多个线程中竞争 HashMap 本身的数据。要解决这个问题就得进一步引入锁,还好 Zig 提供了 std.Io.RwLock。有了这些东西,就可以完成第一版任务队列了:
const Runner = @This();
const session = @import("../session.zig");
const std = @import("std");
const Io = std.Io;
const concurrent_task = 2;
queue: std.Io.Queue(TaskHandle),
queue_storage: [](TaskHandle),
io: std.Io,
task_group: std.Io.Group,
gpa: std.mem.Allocator,
task_id: std.atomic.Value(TaskHandle) = std.atomic.Value(TaskHandle).init(0),
tasks: TaskStore,
userdata: *anyopaque,
on_result: OnResultCallback,
const OnResultCallback = *const fn (*anyopaque, Task) void;
const TaskMap = std.AutoHashMapUnmanaged(TaskHandle, Task);
const TaskStore = struct {
map: std.AutoHashMapUnmanaged(TaskHandle, Task) = .empty,
lock: std.Io.RwLock = .init,
io: std.Io,
gpa: std.mem.Allocator,
pub fn init(io: std.Io, gpa: std.mem.Allocator) TaskStore {
return .{
.io = io,
.gpa = gpa,
};
}
pub fn deinit(self: *TaskStore) void {
self.map.deinit(self.gpa);
}
pub fn add(self: *TaskStore, task: Task) !void {
self.lock.lockUncancelable(self.io);
defer self.lock.unlock(self.io);
try self.map.put(self.gpa, task.id, task);
}
pub fn get(self: *TaskStore, id: TaskHandle) *Task {
return self.map.getPtr(id).?;
}
pub fn remove(self: *TaskStore, id: TaskHandle) void {
self.lock.lockUncancelable(self.io);
defer self.lock.unlock(self.io);
_ = self.map.remove(id);
}
};
const TaskHandle = u64;
fn nextTaskId(self: *Runner) u64 {
return self.task_id.fetchAdd(1, .monotonic);
}
fn initTask(self: *Runner, tool_use: session.ToolUseStart) !TaskHandle {
const task = Task.init(self.io, tool_use, self.nextTaskId());
try self.tasks.add(task);
return task.id;
}
const log = std.log.scoped(.tool_runner);
pub fn init(io: std.Io, gpa: std.mem.Allocator, userdata: *anyopaque, on_result: OnResultCallback) !Runner {
const storage = try gpa.alloc(TaskHandle, concurrent_task * 10);
return Runner{
.io = io,
.queue = .init(storage),
.queue_storage = storage,
.task_group = .init,
.tasks = .init(io, gpa),
.gpa = gpa,
.userdata = userdata,
.on_result = on_result,
};
}
pub fn start(self: *Runner) !void {
for (0..concurrent_task) |i| {
try self.task_group.concurrent(self.io, consumer, .{self, i});
}
}
pub fn stop(self: *Runner) void {
self.task_group.cancel(self.io);
}
pub fn deinit(self: *Runner) void {
self.queue.close(self.io);
self.stop();
self.gpa.free(self.queue_storage);
self.tasks.deinit();
}
pub fn execute(self: *Runner, tool_call: session.ToolUseStart) !TaskHandle {
const handle = try self.initTask(tool_call);
log.info("Put task {d}", .{handle});
try self.queue.putOne(self.io, handle);
return handle;
}
pub fn interrupt(self: *Runner) void {
self.task_group.cancel(self.io);
}
fn runTool(self: *Runner, handle: TaskHandle) void {
var task = self.tasks.get(handle);
const tc = task.tool_use;
log.info("execute tool call: {s} {s}", .{ @tagName(tc.tool), tc.id });
task.tool_use_result = .{
.id = tc.id,
.content = "",
.timestamp = .{ .nanoseconds = 3 },
.tool = tc.tool,
.is_error = null,
};
}
fn consumer(self: *Runner, num: usize) void {
log.info("consumer started: {d}", .{num});
while (self.queue.getOne(self.io)) |task_handle| {
log.info("consumer {d} got task: {d}", .{num ,task_handle});
var task = self.tasks.get(task_handle);
if (task.canceled.load(.monotonic)) {
self.tasks.remove(task.id);
log.info("Consumer {d} skips canceled task {d}", .{num ,task.id});
continue;
}
task.group.async(self.io, runTool, .{ self, task_handle });
task.group.await(self.io) catch |err| {
var t = self.tasks.get(task_handle);
t.err = err;
log.info("Consumer {d} got error {s} for task {d}", .{num ,@errorName(err), task.id});
self.on_result(self.userdata, t.*);
continue;
};
const t = self.tasks.get(task_handle);
log.info("Consumer {d} got result for task {d}", .{num ,t.id});
self.on_result(self.userdata, t.*);
} else |_| {
log.info("Consumer {d} stopped", .{num});
}
}
const Task = struct {
tool_use: session.ToolUseStart,
tool_use_result: ?session.ToolUseCompleted = null,
group: Io.Group,
io: Io,
canceled: std.atomic.Value(bool) = .init(false),
id: TaskHandle,
err: ?anyerror = null,
fn init(io: Io, tool_use: session.ToolUseStart, id: TaskHandle) Task {
return .{ .io = io, .tool_use = tool_use, .group = .init, .id = id };
}
fn deinit(self: *Task) void {
self.cancel();
}
fn await(self: *Task) !session.ToolUseCompleted {
try self.group.await(self.io);
return self.tool_use_result orelse error.Canceled;
}
fn cancel(self: *Task) void {
self.canceled.store(true, .monotonic);
self.group.cancel(self.io);
}
pub fn jsonStringify(self: *const Task, jws: anytype) std.json.Stringify.Error!void {
try jws.write(.{
.id = self.id,
.err = self.err,
.tool_use = self.tool_use,
.tool_use_result = self.tool_use_result,
});
}
};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。