//! 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 `[]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 /// 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 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 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![]ResultPart, /// 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, }; }; /// 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; }