summaryrefslogtreecommitdiff
path: root/libpanto/src/tool.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-03 09:11:36 -0600
committert <t@tjp.lol>2026-06-03 12:34:13 -0600
commitac5c4898dfa0a9e57424336774893dfc72b132e9 (patch)
tree03eb8e32a001845dbaec163dfd4905ffc23d9fa6 /libpanto/src/tool.zig
parent2416a308c0e467f8dbdbe09942aa903bca751c0e (diff)
image uploads
Diffstat (limited to 'libpanto/src/tool.zig')
-rw-r--r--libpanto/src/tool.zig82
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;
+}