diff options
| author | t <t@tjp.lol> | 2026-06-03 09:11:36 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-03 12:34:13 -0600 |
| commit | ac5c4898dfa0a9e57424336774893dfc72b132e9 (patch) | |
| tree | 03eb8e32a001845dbaec163dfd4905ffc23d9fa6 /libpanto/src/tool.zig | |
| parent | 2416a308c0e467f8dbdbe09942aa903bca751c0e (diff) | |
image uploads
Diffstat (limited to 'libpanto/src/tool.zig')
| -rw-r--r-- | libpanto/src/tool.zig | 82 |
1 files changed, 76 insertions, 6 deletions
diff --git a/libpanto/src/tool.zig b/libpanto/src/tool.zig index d9fb178..c912099 100644 --- a/libpanto/src/tool.zig +++ b/libpanto/src/tool.zig @@ -6,9 +6,56 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const tool_source = @import("tool_source.zig"); -pub const ToolDecl = tool_source.ToolDecl; +/// Tool metadata: everything the LLM-facing wire needs (name, +/// description, schema) without an invocation vtable. This is the more +/// atomic type, so it lives here; `tool_source.zig` imports it. +pub const ToolDecl = struct { + name: []const u8, + description: []const u8, + schema_json: []const u8, +}; + +/// A binary attachment a tool may return alongside (or instead of) text: +/// an image or a document (PDF). +/// +/// `data` is the **raw, un-encoded file bytes** — tools do no encoding. +/// libpanto owns the heavy lifting at tool-result assembly: it +/// magic-byte-detects the type when `media_type` is null, resizes large +/// rasters, and base64-encodes for storage/serialization. +pub const MediaPart = struct { + /// Optional MIME hint, e.g. "image/png". When null, libpanto detects + /// the type from `data`'s leading bytes (magic numbers). + media_type: ?[]const u8 = null, + /// Raw (un-encoded) file bytes. + data: []const u8, +}; + +/// One element of a tool's result. A tool returns a `[]ResultPart`; the +/// agent assembles these into a `ToolResultBlock`. Bytes referenced by a +/// part are owned by the allocator passed to `invoke` / `invoke_batch`; +/// ownership transfers to the agent, which frees them. +pub const ResultPart = union(enum) { + text: []const u8, + media: MediaPart, + + /// Free the bytes this part owns, using `allocator`. + pub fn deinit(self: ResultPart, allocator: Allocator) void { + switch (self) { + .text => |t| allocator.free(t), + .media => |m| { + if (m.media_type) |mt| allocator.free(mt); + allocator.free(m.data); + }, + } + } +}; + +/// Free a `[]ResultPart` and every part it owns. +pub fn freeResultParts(allocator: Allocator, parts: []ResultPart) void { + for (parts) |p| p.deinit(allocator); + allocator.free(parts); +} pub const Tool = struct { /// Metadata: `name`, `description`, `schema_json`. Borrowed — the @@ -31,9 +78,11 @@ pub const Tool = struct { /// `input` is the raw JSON bytes the provider sent. The tool is /// responsible for parsing them if it cares about their structure. /// - /// Returns owned bytes allocated with `allocator`. These bytes - /// become the `content` of the ToolResult block sent back to the - /// LLM. The agent takes ownership and frees them. + /// Returns an owned slice of `ResultPart`s allocated with + /// `allocator`; each part's bytes are likewise owned. These become + /// the parts of the ToolResult block sent back to the LLM. The + /// agent takes ownership and frees the slice and every part (see + /// `freeResultParts`). /// /// Returning an error aborts the current turn. The agent surfaces /// the error to the user. Native tool implementations are @@ -44,7 +93,7 @@ pub const Tool = struct { ctx: *anyopaque, input: []const u8, allocator: Allocator, - ) anyerror![]u8, + ) anyerror![]ResultPart, /// Called when the tool is unregistered or the registry is torn /// down. Frees any resources owned by `ctx`, including `ctx` @@ -56,3 +105,24 @@ pub const Tool = struct { deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void, }; }; + +/// Convenience: allocate a single-element `[]ResultPart` holding one text +/// part that owns `text` (duped from the input slice). +pub fn textResult(allocator: Allocator, text: []const u8) ![]ResultPart { + const owned = try allocator.dupe(u8, text); + errdefer allocator.free(owned); + const parts = try allocator.alloc(ResultPart, 1); + parts[0] = .{ .text = owned }; + return parts; +} + +/// Convenience: wrap an already-owned `text` slice as a single-element +/// `[]ResultPart`. Takes ownership of `text`. +pub fn ownedTextResult(allocator: Allocator, text: []u8) ![]ResultPart { + const parts = allocator.alloc(ResultPart, 1) catch |e| { + allocator.free(text); + return e; + }; + parts[0] = .{ .text = text }; + return parts; +} |
