summaryrefslogtreecommitdiff
path: root/libpanto/src/provider.zig
diff options
context:
space:
mode:
authorT <t@tjp.lol>2026-05-27 07:30:51 -0600
committerT <t@tjp.lol>2026-05-27 07:34:46 -0600
commit75fd7d7833052383e1b61d4275d12c9a993c02d3 (patch)
treee63f25a697cac215281afe76e71892a69af23cb2 /libpanto/src/provider.zig
parenteddebe4e8131b7d2112b067fed07e7493d8b1984 (diff)
separate event for providing tool details
OpenAI and Anthropic have tool details (id, name) converge on different timelines. In all cases they are available in `onBlockComplete` which comes with the full block, and previously we had an optional `BlockMeta` argument to `onBlockStart` - the anthropic timeline. We removed the `BlockMeta` from `onBlockStart` since it was always going to be unreliable, and now have an explicit `onToolDetails` instead. This allows us to provide the tool id and name as early as possible from the openai provider (could be in the middle of content deltas), and provide a reliable signal across providers. Also, fix test output.
Diffstat (limited to 'libpanto/src/provider.zig')
-rw-r--r--libpanto/src/provider.zig21
1 files changed, 15 insertions, 6 deletions
diff --git a/libpanto/src/provider.zig b/libpanto/src/provider.zig
index fd170f0..bc39026 100644
--- a/libpanto/src/provider.zig
+++ b/libpanto/src/provider.zig
@@ -19,12 +19,16 @@ pub const ContentBlockType = enum {
/// stops streaming, calls `onError(err)`, and propagates `err` out of
/// `streamStep`. No partial assistant message is appended to the conversation.
///
-/// Identity fields (tool id/name for ToolUse blocks) are intentionally *not*
-/// surfaced at `onBlockStart`. They cannot be reliably known at start time
-/// across all wire protocols — OpenAI streaming may deliver `id` and `name`
-/// in fragments across multiple deltas — so any "meta" we passed at start
-/// would be a phantom guarantee. Receivers that need identity get it from
-/// the fully-assembled `ContentBlock` delivered to `onBlockComplete`.
+/// Tool-use identity (`id`, `name`) is delivered via `onToolDetails`, fired
+/// once per ToolUse block at the earliest moment both fields are known. For
+/// Anthropic this is immediately after `onBlockStart`, before any deltas. For
+/// OpenAI Chat Completions this may be partway through the arg-deltas, since
+/// the wire protocol can split `id` and `name` across multiple streaming
+/// chunks. The only guarantees are: it fires strictly after the block's
+/// `onBlockStart`, strictly before its `onBlockComplete`, and at most once
+/// per ToolUse block. It never fires for non-ToolUse blocks. If a tool_use
+/// block is dropped because identity never fully arrived, `onToolDetails`
+/// (and `onBlockComplete`) never fire for it.
///
/// `onError` is the receiver's cleanup hook. It fires exactly once per failed
/// turn, whether the error originated in the receiver itself (a write failure)
@@ -34,6 +38,7 @@ pub const ContentBlockType = enum {
pub const ReceiverVTable = struct {
onMessageStart: *const fn (*anyopaque, conversation.MessageRole) anyerror!void,
onBlockStart: *const fn (*anyopaque, ContentBlockType, usize) anyerror!void,
+ onToolDetails: *const fn (*anyopaque, usize, []const u8, []const u8) anyerror!void,
onContentDelta: *const fn (*anyopaque, usize, []const u8) anyerror!void,
onBlockComplete: *const fn (*anyopaque, usize, conversation.ContentBlock) anyerror!void,
onMessageComplete: *const fn (*anyopaque, conversation.Message) anyerror!void,
@@ -52,6 +57,10 @@ pub const Receiver = struct {
try self.vtable.onBlockStart(self.ptr, block_type, index);
}
+ pub fn onToolDetails(self: Receiver, block_index: usize, id: []const u8, name: []const u8) !void {
+ try self.vtable.onToolDetails(self.ptr, block_index, id, name);
+ }
+
pub fn onContentDelta(self: Receiver, block_index: usize, delta: []const u8) !void {
try self.vtable.onContentDelta(self.ptr, block_index, delta);
}