summaryrefslogtreecommitdiff
path: root/src/tool.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tool.zig')
-rw-r--r--src/tool.zig149
1 files changed, 149 insertions, 0 deletions
diff --git a/src/tool.zig b/src/tool.zig
new file mode 100644
index 0000000..c96dfee
--- /dev/null
+++ b/src/tool.zig
@@ -0,0 +1,149 @@
+//! Native tool extension API.
+//!
+//! A `Tool` is the boundary between the agent loop and any extension runtime
+//! — native Zig code, a Lua bridge, a future Python or Go bridge. libpanto
+//! itself does not parse tool inputs or outputs; it just dispatches.
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+/// 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 `ResultParts` (a thin
+/// wrapper around `[]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);
+ },
+ }
+ }
+};
+
+/// A tool's full result: an owned slice of `ResultPart`s. The value the
+/// `Tool`/`ToolSource` vtable returns and the agent loop assembles — a thin
+/// wrapper around `[]ResultPart` that carries the construction/teardown
+/// ergonomics a bare slice alias can't. Build one with
+/// `fromText`/`fromTextOwned` (or wrap a hand-built slice as
+/// `.{ .items = slice }`); release it (slice + every part's bytes) with
+/// `deinit`.
+pub const ResultParts = struct {
+ items: []ResultPart,
+
+ /// A single text part that owns `text` (duped from the input slice).
+ pub fn fromText(allocator: Allocator, text: []const u8) !ResultParts {
+ const owned = try allocator.dupe(u8, text);
+ errdefer allocator.free(owned);
+ const parts = try allocator.alloc(ResultPart, 1);
+ parts[0] = .{ .text = owned };
+ return .{ .items = parts };
+ }
+
+ /// A single text part wrapping an already-owned `text` slice. Takes
+ /// ownership of `text` (frees it if the allocation below fails).
+ pub fn fromTextOwned(allocator: Allocator, text: []u8) !ResultParts {
+ const parts = allocator.alloc(ResultPart, 1) catch |e| {
+ allocator.free(text);
+ return e;
+ };
+ parts[0] = .{ .text = text };
+ return .{ .items = parts };
+ }
+
+ /// Free the slice and every part it owns.
+ pub fn deinit(self: ResultParts, allocator: Allocator) void {
+ for (self.items) |p| p.deinit(allocator);
+ allocator.free(self.items);
+ }
+};
+
+pub const Tool = struct {
+ /// Metadata: `name`, `description`, `schema_json`. Borrowed — the
+ /// lifetime of every string in `decl` is owned by whoever
+ /// constructs the `Tool`. Typically the same owner that backs
+ /// `ctx` (e.g. an adapter for an out-of-process runtime, or a
+ /// `comptime` static in a native tool).
+ decl: ToolDecl,
+
+ /// Opaque context pointer passed back to every vtable call.
+ ctx: *anyopaque,
+
+ vtable: *const VTable,
+
+ pub const VTable = struct {
+ /// Invoke the tool. MUST be thread-safe — the agent may call
+ /// `invoke` concurrently from multiple threads when the LLM emits
+ /// multiple ToolUse blocks in a single response.
+ ///
+ /// `input` is the raw JSON bytes the provider sent. The tool is
+ /// responsible for parsing them if it cares about their structure.
+ ///
+ /// Returns a `ResultParts` 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 `ResultParts.deinit`).
+ /// Build the return value with `ResultParts.fromText` /
+ /// `.fromTextOwned` for the common single-text case, or wrap a
+ /// hand-built slice as `.{ .items = slice }`.
+ ///
+ /// Returning an error normally becomes a model-visible error
+ /// `ToolResult`: the agent synthesizes an error result for this
+ /// call (and keeps the matching `ToolResult` for every other call
+ /// in the batch), then lets the model continue so it can correct
+ /// arguments, try another tool, or explain the failure. Only hard
+ /// host failures (`error.Canceled`, `error.OutOfMemory`) abort the
+ /// whole turn and propagate to the embedder.
+ ///
+ /// Native tool implementations are responsible for catching their
+ /// own panics — a panic in `invoke` will crash the process.
+ /// Adapters that bridge to safer languages (Lua, Python, Go) should
+ /// convert panics/exceptions into errors.
+ invoke: *const fn (
+ ctx: *anyopaque,
+ input: []const u8,
+ allocator: Allocator,
+ ) anyerror!ResultParts,
+
+ /// Called when the tool is unregistered or the registry is torn
+ /// down. Frees any resources owned by `ctx`, including `ctx`
+ /// itself if it was heap-allocated.
+ ///
+ /// The strings inside `decl` are also typically owned by the
+ /// same allocation as `ctx` — the tool's deinit hook is
+ /// responsible for freeing them.
+ deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void,
+ };
+};
+