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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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,
};
};
|