summaryrefslogtreecommitdiff
path: root/libpanto/src/tool.zig
blob: c912099268d63ee0e918b0f656ae8aff36623faf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! 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 aborts the current turn. The agent surfaces
        /// the error to the user. 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;
}