The Agent Client Protocol (ACP) standardises communication between code editors — interactive programs for viewing and editing source code — and coding agents — programs that use generative AI to autonomously modify code.
This repository is a native Zig implementation. It gives you allocator-aware wire types, a transport-agnostic synchronous SDK, proxy orchestration, an interactive trace viewer, a reference agent binary, and a comptime-reflective schema generator.
Status: working core, public API stabilising toward
v0.1.0. Wire format matches the canonical protocol; the public surface is unstable until tagged.
Core SDK
acp-schema— Wire-format types: methods, requests, responses, notifications, content variants, tool calls, errors. Forward-compatibleunknownbuckets on every public union so newer peers don't crash older clients.acp— SynchronousConnection, vtable-basedTransport, comptime-typedDispatcher,Sessionstate machine, capability negotiation, fixed-capacity trace ring buffer.acp-async— Newline-delimited framer,BufferPairdeterministic test transport,FileTransportoverstd.Io.File, subprocessChildspawn.
Proxy orchestration
acp-conductor— Compose N typed interceptors between client and agent ends with pass / short-circuit / drop semantics.acp-trace-viewer— Interactive TUI for replaying captured traces.
Examples and testing
acp-cookbook— Minimal runnable client and agent.acp-test— In-memoryPipePairtransport and end-to-end contract tests.yopo— Reference agent binary that drives the full contract over an in-process pipe.
Tooling
tools/gen_schema— Comptime-reflective generator that emits a canonical JSON catalog of the public schema.
zig fetch --save git+https://github.com/MagnovaAI/acp-zigIn your build.zig:
const acp_dep = b.dependency("acp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("acp", acp_dep.module("acp"));
exe.root_module.addImport("acp-schema", acp_dep.module("acp-schema"));const std = @import("std");
const acp = @import("acp");
const schema = @import("acp-schema");
pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const transport: acp.Transport = my_transport; // FileTransport, BufferPair, etc.
var conn = acp.Connection.init(allocator, transport);
const resp = try conn.request(
schema.agent.InitializeResponse,
schema.agent.method_initialize,
.{ .protocolVersion = schema.ProtocolVersion.V1 },
);
defer resp.deinit();
std.debug.print("peer speaks protocol v{d}\n", .{resp.value.protocolVersion.value});
}const Agent = struct {
pub const Params = schema.agent.InitializeRequest;
pub const Result = schema.agent.InitializeResponse;
pub fn handle(_: *@This(), _: std.mem.Allocator, params: Params) acp.AcpError!Result {
return .{ .protocolVersion = params.protocolVersion };
}
};
var dispatcher = acp.Dispatcher.init(allocator);
defer dispatcher.deinit();
var agent: Agent = .{};
try dispatcher.registerRequest(schema.agent.method_initialize, Agent, &agent);
var conn = acp.Connection.init(allocator, transport);
conn.setRequestHandler(dispatcher.requestHandler());
try conn.serve();A full client+agent round-trip lives in src/acp-test/contract_handshake.zig. The reference contract is in src/yopo/main.zig.
zig version # must report 0.16.0
zig build # build all libraries + binaries
zig build test --summary all # 119 pass + 11 gated skips, zero leaks
zig fmt --check src/ tools/ # formatting check
zig build yopo # run the reference contract suite
zig build gen-schema -- /tmp/schema.json # emit canonical schema catalog
zig build trace-viewer # build the TUI viewerEach unstable method is gated behind its own build flag, off by default:
zig build test \
-Dunstable_logout=true \
-Dunstable_session_fork=true \
-Dunstable_session_resume=true \
-Dunstable_session_close=true \
-Dunstable_session_model=trueOther flags: unstable_elicitation, unstable_nes, unstable_cancel_request, unstable_session_usage, unstable_session_additional_directories, unstable_llm_providers, unstable_message_id, unstable_boolean_config, unstable_auth_methods.
- One error set across boundaries.
AcpErroris the only error type returned across package boundaries — neveranyerror!T. Failure modes are explicit. - Allocator-aware everywhere. Every owning type takes an allocator and exposes a matching
deinit. No global allocator, no hidden heap. - Forward-compatible unions. Public tagged unions expose an
unknown: std.json.Valuevariant so a peer running a newer revision never crashes an older client. - Comptime-typed dispatch.
Dispatcherregisters handlers by method name with concreteParams/Resulttypes. The thunk parses, invokes, and re-marshals through a per-call arena. - Per-frame tracing. Attach a
TraceBufferto aConnectionand every JSON-RPC frame is recorded with direction and a monotonic sequence number. Diagnostics never block the protocol path.
src/
├── acp-schema/ # wire types + JSON codec
├── acp/ # core SDK
├── acp-async/ # framing + real-IO transports
├── acp-conductor/ # proxy chain
├── acp-test/ # in-memory transport + contract tests
├── acp-trace-viewer/ # TUI viewer
├── acp-cookbook/ # examples
└── yopo/ # reference agent binary
tools/
└── gen_schema/ # canonical schema catalog generator
- Bug reports: open a bug report issue. Include
zig version, OS / arch, and a minimal failing snippet. - Pull requests: the PR template lists the merge checklist. Each PR should keep
zig build testgreen with zero leaks andzig fmt --check src/ tools/clean. Wire-format changes need an explicit note in the PR body. - Larger proposals: open a discussion first so we can align on the wire-format impact before any code lands.
See CHANGELOG.md for what's in each release.
MIT.