summaryrefslogtreecommitdiff
path: root/libpanto/src/agent.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-02 10:50:05 -0600
committert <t@tjp.lol>2026-06-02 11:11:29 -0600
commit16ea45b6854232541fb45f7d2e5b767118cccf43 (patch)
tree1bf37d766e4b9de2b646c1475497ae7e9d5699c7 /libpanto/src/agent.zig
parent9c64a7d4462a11674e2dea481b037b5f5d9c62fc (diff)
tool call resiliency
Diffstat (limited to 'libpanto/src/agent.zig')
-rw-r--r--libpanto/src/agent.zig33
1 files changed, 31 insertions, 2 deletions
diff --git a/libpanto/src/agent.zig b/libpanto/src/agent.zig
index 08cc91c..a42c16c 100644
--- a/libpanto/src/agent.zig
+++ b/libpanto/src/agent.zig
@@ -43,6 +43,23 @@ const Entry = tool_registry_mod.Entry;
pub const Config = config_mod.Config;
+fn isValidToolInput(input: []const u8) bool {
+ if (input.len == 0) return true;
+ if (input[0] != '{') return true; // legacy tests/tools may use opaque bytes
+ var parsed = std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, input, .{}) catch return false;
+ defer parsed.deinit();
+ return parsed.value == .object;
+}
+
+fn invalidInputResult(allocator: Allocator, input: []const u8) ![]u8 {
+ return std.fmt.allocPrint(
+ allocator,
+ "Tool call was not executed: tool input was incomplete or invalid JSON. Partial input: {s}",
+ .{input},
+ );
+}
+
+
pub const Agent = struct {
allocator: Allocator,
io: Io,
@@ -132,6 +149,17 @@ pub const Agent = struct {
for (assistant_msg.content.items) |block| {
if (block != .ToolUse) continue;
const tu = block.ToolUse;
+ if (!isValidToolInput(tu.input.items)) {
+ try calls.append(.{
+ .tool_use_id = tu.id,
+ .tool_name = tu.name,
+ .input = tu.input.items,
+ .entry = null,
+ .result = try invalidInputResult(self.allocator, tu.input.items),
+ .err = null,
+ });
+ continue;
+ }
const entry = self.config.registry.lookup(tu.name) orelse {
// Unknown tool: abort the turn with a clear error.
return error.UnknownTool;
@@ -236,7 +264,7 @@ const FlatCall = struct {
tool_use_id: []const u8, // borrowed from assistant_msg
tool_name: []const u8, // borrowed from assistant_msg
input: []const u8, // borrowed from assistant_msg
- entry: Entry,
+ entry: ?Entry,
/// Owned result bytes from `Tool.invoke` or `ToolSource.invoke_batch`.
/// Allocated with the agent's allocator. Transferred into a
@@ -292,7 +320,8 @@ fn buildGroups(
}
for (calls, 0..) |c, i| {
- switch (c.entry) {
+ const ent = c.entry orelse continue;
+ switch (ent) {
.single => |t| try out.append(.{ .single = .{ .tool = t, .call_index = i } }),
.source => |sr| {
const gop = try pending.getOrPut(sr.source);