diff options
Diffstat (limited to 'src/tool.zig')
| -rw-r--r-- | src/tool.zig | 149 |
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, + }; +}; + |
