summaryrefslogtreecommitdiff
path: root/src/tool_source.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-07 11:26:32 -0600
committert <t@tjp.lol>2026-07-07 11:26:45 -0600
commitf83578fdc9264019a1a1cef8c5484a161167d3dd (patch)
tree888f11767f944d61e5ca8eb92fa1b2dba295a4b8 /src/tool_source.zig
initial commit, moved libpanto over from the pantograph repo
Diffstat (limited to 'src/tool_source.zig')
-rw-r--r--src/tool_source.zig104
1 files changed, 104 insertions, 0 deletions
diff --git a/src/tool_source.zig b/src/tool_source.zig
new file mode 100644
index 0000000..d5bc716
--- /dev/null
+++ b/src/tool_source.zig
@@ -0,0 +1,104 @@
+//! Batch-dispatched tool extension API: `ToolSource`.
+//!
+//! Where `Tool` is a single, thread-safe handler (one tool, one vtable,
+//! reentrant), `ToolSource` is a single owner of many tools whose runtime
+//! prefers to receive calls in *batches* on a single thread.
+//!
+//! Motivation: Lua. A Lua extension runtime maintains one long-lived
+//! `lua_State` so that module-globals, lazy connection pools, rate
+//! limiters, etc. survive across calls. A single `lua_State` is not safe
+//! for concurrent host entry, so the runtime can't satisfy `Tool`'s
+//! thread-safety contract directly. The runtime *can* dispatch many calls
+//! cooperatively (coroutines + an event loop), but it needs to be told
+//! all of them at once.
+//!
+//! The contract libpanto provides:
+//!
+//! - For a given turn, every `ToolUse` block whose tool name belongs to
+//! a particular source is delivered in a single `invoke_batch` call,
+//! on one thread.
+//! - Distinct sources still execute concurrently (one OS thread per
+//! source per turn), so a Lua source and a native source can run in
+//! parallel.
+//! - Single `Tool` registrations are unchanged. They each get their own
+//! thread when they appear alongside other tool calls in a turn.
+//!
+//! The "thread-safe" promise that `Tool.invoke` carries relaxes to
+//! "coroutine-safe within the source's runtime" for source-backed tools —
+//! enforcement is the source's problem.
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const tool = @import("tool.zig");
+
+/// Tool metadata: re-exported from `tool.zig`, which owns the more atomic
+/// type. `ToolSource`s declare their tools this way because they share a
+/// single dispatch path.
+pub const ToolDecl = tool.ToolDecl;
+pub const ResultPart = tool.ResultPart;
+pub const ResultParts = tool.ResultParts;
+
+/// One pending invocation passed to `invoke_batch`. Slices borrowed from
+/// the caller for the duration of the call.
+pub const Call = struct {
+ /// Which of the source's declared tools this call targets.
+ tool_name: []const u8,
+ /// Raw JSON bytes the provider sent. Borrowed.
+ input: []const u8,
+};
+
+/// Result for a single call. Mirrors the success/error split of
+/// `Tool.invoke`'s return shape. Owned by the caller-supplied allocator.
+pub const CallResult = union(enum) {
+ /// Owned parts (the `ResultParts` slice + each part's bytes), freed by
+ /// libpanto after assembling the ToolResult block (see
+ /// `tool.ResultParts.deinit`).
+ ok: ResultParts,
+ err: anyerror,
+};
+
+/// A grouped tool runtime.
+pub const ToolSource = struct {
+ /// Diagnostic name; surfaced in error messages and logs. Example
+ /// values: `"panto-lua"`, `"panto-python"`. Borrowed; lifetime owned
+ /// by the source.
+ name: []const u8,
+ /// Tool metadata for every tool this source owns. Borrowed.
+ tools: []const ToolDecl,
+ ctx: *anyopaque,
+ vtable: *const VTable,
+
+ pub const VTable = struct {
+ /// libpanto guarantees: for a given turn, every ToolUse call
+ /// whose tool name belongs to this source is delivered in one
+ /// `invoke_batch`, on one thread. Different sources still
+ /// execute in parallel.
+ ///
+ /// `calls` and `results` are parallel arrays of length N.
+ /// `results` is pre-allocated by libpanto; the source fills each
+ /// slot. The source decides internal scheduling — sequential,
+ /// coroutine fan-out, worker pool, etc.
+ ///
+ /// Two failure modes, both normally model-visible:
+ /// - Per-call: record `.{ .err = e }` in a `results[i]` slot.
+ /// That call gets an error `ToolResult`; siblings are
+ /// unaffected.
+ /// - Whole-batch: return an error from this function. libpanto
+ /// frees any `ok` slots already filled and maps the error onto
+ /// *every* member call as an error `ToolResult`.
+ /// In both cases the agent loop continues so the model can react.
+ /// Only hard host failures (`error.Canceled`, `error.OutOfMemory`)
+ /// abort the whole turn and propagate to the embedder.
+ invoke_batch: *const fn (
+ ctx: *anyopaque,
+ calls: []const Call,
+ results: []CallResult,
+ allocator: Allocator,
+ ) anyerror!void,
+
+ /// Called when the source is removed from the registry or the
+ /// registry is torn down. Frees any resources owned by `ctx`,
+ /// including `ctx` itself if heap-allocated.
+ deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void,
+ };
+};