summaryrefslogtreecommitdiff
path: root/libpanto/src/provider_anthropic_messages.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-01 08:21:00 -0600
committert <t@tjp.lol>2026-06-01 11:20:56 -0600
commit1beefefc69beee214430eb5bd2528a4f5692d2a8 (patch)
treea459dbde5da17babe7d872999341d2006ebfe02e /libpanto/src/provider_anthropic_messages.zig
parent5c016cfdbcb122e2b4f0496ef946642d68953c4b (diff)
Rename system extension layer to base for clarity
Replace all references to the "system" layer with "base" to better reflect its role as the foundational extension/tool layer in panto's hierarchy. The layer hierarchy now consistently uses: project > user > base. Includes: - Update agent README and build.zig documentation - Refactor tool registry and config module to support layered lookups - Add TestHarness abstraction for cleaner test setup - Improve JSON serialization with wire-encoded tool names - Add glob pattern matching for tool/extension discovery
Diffstat (limited to 'libpanto/src/provider_anthropic_messages.zig')
-rw-r--r--libpanto/src/provider_anthropic_messages.zig113
1 files changed, 58 insertions, 55 deletions
diff --git a/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig
index 5d5520a..02277be 100644
--- a/libpanto/src/provider_anthropic_messages.zig
+++ b/libpanto/src/provider_anthropic_messages.zig
@@ -23,62 +23,19 @@ const provider_mod = @import("provider.zig");
const sse_mod = @import("sse.zig");
const json_mod = @import("anthropic_messages_json.zig");
const config_mod = @import("config.zig");
+const tool_registry_mod = @import("tool_registry.zig");
-pub const AnthropicMessagesProvider = struct {
+/// A single Anthropic Messages streaming request. Transient: constructed
+/// per `streamStep`, holds only borrowed state (allocator, io, the global
+/// HTTP client, and the active config). Carries nothing across requests.
+pub const AnthropicMessagesRequest = struct {
allocator: Allocator,
io: Io,
- config: config_mod.AnthropicMessagesConfig,
- http_client: http.Client,
-
- pub fn init(
- allocator: Allocator,
- io: Io,
- cfg: config_mod.AnthropicMessagesConfig,
- ) AnthropicMessagesProvider {
- return .{
- .allocator = allocator,
- .io = io,
- .config = cfg,
- .http_client = .{ .allocator = allocator, .io = io },
- };
- }
-
- pub fn deinit(self: *AnthropicMessagesProvider) void {
- self.http_client.deinit();
- }
-
- pub fn provider(self: *AnthropicMessagesProvider) provider_mod.Provider {
- return .{ .ptr = self, .vtable = &vtable };
- }
-
- const vtable: provider_mod.ProviderVTable = .{
- .streamStep = vtableStreamStep,
- .deinit = vtableDeinit,
- };
-
- fn vtableStreamStep(
- ptr: *anyopaque,
- conv: *conversation.Conversation,
- tools: *const provider_mod.ToolRegistry,
- receiver: *provider_mod.Receiver,
- ) anyerror!void {
- const self: *AnthropicMessagesProvider = @ptrCast(@alignCast(ptr));
- return self.streamStep(conv, tools, receiver);
- }
-
- /// Called via the `Provider` interface. Tears down the impl AND frees
- /// its heap allocation, since `Provider.init` is the one that allocated
- /// it. Direct stack-allocated users (tests, embedders) call `deinit`
- /// themselves and never hit this path.
- fn vtableDeinit(ptr: *anyopaque) void {
- const self: *AnthropicMessagesProvider = @ptrCast(@alignCast(ptr));
- const allocator = self.allocator;
- self.deinit();
- allocator.destroy(self);
- }
+ config: *const config_mod.AnthropicMessagesConfig,
+ http_client: *http.Client,
pub fn streamStep(
- self: *AnthropicMessagesProvider,
+ self: *AnthropicMessagesRequest,
conv: *conversation.Conversation,
tools: *const provider_mod.ToolRegistry,
receiver: *provider_mod.Receiver,
@@ -92,7 +49,7 @@ pub const AnthropicMessagesProvider = struct {
}
fn streamStepInner(
- self: *AnthropicMessagesProvider,
+ self: *AnthropicMessagesRequest,
conv: *conversation.Conversation,
tools: *const provider_mod.ToolRegistry,
receiver: *provider_mod.Receiver,
@@ -106,7 +63,7 @@ pub const AnthropicMessagesProvider = struct {
const uri = try Uri.parse(url);
- const body = try json_mod.serializeRequest(self.allocator, &self.config, conv, tools);
+ const body = try json_mod.serializeRequest(self.allocator, self.config, conv, tools);
defer self.allocator.free(body);
const extra_headers = [_]http.Header{
@@ -294,10 +251,25 @@ const StreamState = struct {
.kind = kind,
};
// For tool_use blocks, capture the identity fields. Anthropic
- // delivers both whole on content_block_start.
+ // delivers both whole on content_block_start. The wire name is
+ // encoded (`__` for `.`); decode it here so everything downstream
+ // — onToolDetails, the stored ContentBlock, session logs, and
+ // dispatch — sees the internal (dotted) name. The decoded form is
+ // never longer than the wire form.
if (kind == .tool_use) {
if (tool_id) |id| ab.tool_id = try self.allocator.dupe(u8, id);
- if (tool_name) |n| ab.tool_name = try self.allocator.dupe(u8, n);
+ if (tool_name) |n| {
+ // Decode `__` -> `.` into an exact-size owned buffer so the
+ // stored slice is freeable as a whole allocation.
+ const owned = try self.allocator.alloc(u8, n.len);
+ errdefer self.allocator.free(owned);
+ const decoded = tool_registry_mod.decodeName(owned, n);
+ if (decoded.len == n.len) {
+ ab.tool_name = owned;
+ } else {
+ ab.tool_name = try self.allocator.realloc(owned, decoded.len);
+ }
+ }
}
self.active = ab;
const block_type: ?provider_mod.ContentBlockType = switch (kind) {
@@ -933,6 +905,37 @@ test "tool_use blocks are captured with id, name, and assembled input" {
try testing.expectEqualStrings("done", asst.content.items[1].Text.items);
}
+test "inbound wire tool name is decoded to dotted form" {
+ // Anthropic delivers the (wire-encoded) name whole at
+ // content_block_start; it is decoded to the internal dotted form for
+ // the conversation, session logs, and dispatch.
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+ try conv.addUserMessage("use a tool");
+
+ var rec = RecordingReceiver.init(allocator);
+ defer rec.deinit();
+ var recv = rec.receiver();
+
+ const events = [_][]const u8{
+ \\{"type":"message_start","message":{"role":"assistant"}}
+ ,
+ \\{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"t1","name":"calc__sum","input":{}}}
+ ,
+ \\{"type":"content_block_stop","index":0}
+ ,
+ \\{"type":"message_stop"}
+ ,
+ };
+
+ try runStreamedTurn(allocator, &conv, &recv, &events);
+
+ const tu = conv.messages.items[1].content.items[0].ToolUse;
+ try testing.expectEqualStrings("calc.sum", tu.name);
+}
+
test "error event propagates as Zig error" {
const allocator = testing.allocator;