From 1beefefc69beee214430eb5bd2528a4f5692d2a8 Mon Sep 17 00:00:00 2001 From: t Date: Mon, 1 Jun 2026 08:21:00 -0600 Subject: 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 --- libpanto/src/provider_anthropic_messages.zig | 113 ++++++++++++++------------- 1 file changed, 58 insertions(+), 55 deletions(-) (limited to 'libpanto/src/provider_anthropic_messages.zig') 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; -- cgit v1.3