summaryrefslogtreecommitdiff
path: root/libpanto/src/provider_anthropic_messages.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/provider_anthropic_messages.zig')
-rw-r--r--libpanto/src/provider_anthropic_messages.zig69
1 files changed, 59 insertions, 10 deletions
diff --git a/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig
index 02277be..5c24b8f 100644
--- a/libpanto/src/provider_anthropic_messages.zig
+++ b/libpanto/src/provider_anthropic_messages.zig
@@ -181,6 +181,7 @@ const StreamState = struct {
/// `onMessageComplete`, the latter stamps `null`.
usage: provider_mod.Usage = .{},
usage_seen: bool = false,
+ stop_reason: ?[]u8 = null,
const ActiveBlock = struct {
/// Index reported on the wire (Anthropic's content-array index).
@@ -210,6 +211,7 @@ const StreamState = struct {
}
for (self.blocks.items) |*b| b.deinit(self.allocator);
self.blocks.deinit(self.allocator);
+ if (self.stop_reason) |s| self.allocator.free(s);
}
fn ensureStarted(self: *StreamState, receiver: *provider_mod.Receiver) !void {
@@ -322,6 +324,40 @@ const StreamState = struct {
a.signature = try self.allocator.dupe(u8, sig);
}
+ fn setStopReason(self: *StreamState, reason: ?[]const u8) !void {
+ if (self.stop_reason) |old| self.allocator.free(old);
+ self.stop_reason = if (reason) |r| try self.allocator.dupe(u8, r) else null;
+ }
+
+ fn isJSONObject(input: []const u8) bool {
+ if (input.len == 0) return true;
+ var parsed = std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, input, .{}) catch return false;
+ defer parsed.deinit();
+ return parsed.value == .object;
+ }
+
+ fn appendAnthropicFailureResult(
+ allocator: Allocator,
+ blocks: *std.ArrayList(conversation.ContentBlock),
+ tool_use_id: []const u8,
+ input: []const u8,
+ stop_reason: ?[]const u8,
+ ) !void {
+ const id_copy = try allocator.dupe(u8, tool_use_id);
+ errdefer allocator.free(id_copy);
+ const msg = try std.fmt.allocPrint(
+ allocator,
+ "Tool call was not executed: Anthropic stopped before completing valid tool input JSON (stop_reason={s}). Partial input: {s}",
+ .{ stop_reason orelse "unknown", input },
+ );
+ errdefer allocator.free(msg);
+ var content: conversation.TextualBlock = .empty;
+ errdefer content.deinit(allocator);
+ try content.appendSlice(allocator, msg);
+ allocator.free(msg);
+ try blocks.append(allocator, .{ .ToolResult = .{ .tool_use_id = id_copy, .content = content } });
+ }
+
/// Close the active block: append it to `blocks` and emit onBlockComplete.
fn closeBlock(
self: *StreamState,
@@ -355,11 +391,22 @@ const StreamState = struct {
.text = a.text_buf,
.signature = a.signature,
} },
- .tool_use => .{ .ToolUse = .{
- .id = a.tool_id.?,
- .name = a.tool_name.?,
- .input = a.text_buf,
- } },
+ .tool_use => blk: {
+ if (!isJSONObject(a.text_buf.items)) {
+ try appendAnthropicFailureResult(
+ self.allocator,
+ &self.blocks,
+ a.tool_id.?,
+ a.text_buf.items,
+ self.stop_reason,
+ );
+ }
+ break :blk .{ .ToolUse = .{
+ .id = a.tool_id.?,
+ .name = a.tool_name.?,
+ .input = a.text_buf,
+ } };
+ },
.unsupported => unreachable,
};
@@ -389,7 +436,8 @@ const StreamState = struct {
self.finalized = true;
if (self.active != null) {
- // Wire dropped us mid-block. Close it cleanly anyway.
+ // Preserve an interrupted tool call so the agent can answer it
+ // with a synthetic error ToolResult instead of invoking it.
try self.closeBlock(receiver);
}
@@ -434,12 +482,13 @@ fn handleEvent(
if (d.signature_delta) |sig| try state.setSignature(sig);
if (d.input_json_delta) |j| try state.appendInputJsonDelta(receiver, j);
},
- .content_block_stop => {
- try state.closeBlock(receiver);
+ .content_block_stop => |s| {
+ if (state.active) |a| {
+ if (a.wire_index == s.index) try state.closeBlock(receiver);
+ }
},
.message_delta => |d| {
- // We don't act on stop_reason directly; message_stop is the
- // authoritative end-of-stream signal.
+ try state.setStopReason(d.stop_reason);
state.mergeUsage(d.usage);
},
.message_stop => {