diff options
Diffstat (limited to 'libpanto/src/provider_anthropic_messages.zig')
| -rw-r--r-- | libpanto/src/provider_anthropic_messages.zig | 67 |
1 files changed, 65 insertions, 2 deletions
diff --git a/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig index 9740c0e..be4268e 100644 --- a/libpanto/src/provider_anthropic_messages.zig +++ b/libpanto/src/provider_anthropic_messages.zig @@ -187,6 +187,7 @@ pub const ResumableResponse = struct { const vtable: provider_mod.ProviderStream.VTable = .{ .produce = produceVT, .deinit = deinitVT, + .last_error = lastErrorVT, }; fn produceVT(ptr: *anyopaque, out: *EventQueue) anyerror!ProduceStatus { @@ -194,6 +195,11 @@ pub const ResumableResponse = struct { return self.produce(out); } + fn lastErrorVT(ptr: *anyopaque) ?[]const u8 { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + return self.state.stream_error_message; + } + fn deinitVT(ptr: *anyopaque) void { const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); self.deinit(); @@ -274,6 +280,10 @@ const StreamState = struct { usage: provider_mod.Usage = .{}, usage_seen: bool = false, stop_reason: ?[]u8 = null, + /// Owned, human-readable description of a mid-stream `error` event + /// (e.g. `"overloaded_error: Overloaded"`), surfaced to the agent via + /// `ProviderStream.lastError` so the retry notice can show *why*. + stream_error_message: ?[]u8 = null, const ActiveBlock = struct { /// Index reported on the wire (Anthropic's content-array index). @@ -304,6 +314,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); + if (self.stream_error_message) |s| self.allocator.free(s); } fn ensureStarted(self: *StreamState, out: *EventQueue) !void { @@ -435,6 +446,22 @@ const StreamState = struct { self.stop_reason = if (reason) |r| try self.allocator.dupe(u8, r) else null; } + /// Record a readable description of a mid-stream `error` event, combining + /// the error `kind` and `message` into one owned string (either may be + /// absent). Replaces any previous value. + fn setStreamErrorMessage(self: *StreamState, kind: ?[]const u8, message: ?[]const u8) !void { + if (self.stream_error_message) |old| self.allocator.free(old); + self.stream_error_message = null; + self.stream_error_message = if (kind != null and message != null) + try std.fmt.allocPrint(self.allocator, "{s}: {s}", .{ kind.?, message.? }) + else if (kind) |k| + try self.allocator.dupe(u8, k) + else if (message) |m| + try self.allocator.dupe(u8, m) + else + null; + } + /// Close the active block: append it to `blocks` and emit block_complete. fn closeBlock( self: *StreamState, @@ -576,8 +603,17 @@ fn handleEvent( if (!@import("builtin").is_test) { std.log.err("anthropic stream error: {?s}: {?s}", .{ e.kind, e.message }); } - // Mid-stream error event (e.g. `overloaded_error`) before the - // message was committed: retryable malformed-stream failure. + // Stash a readable description so the agent's retry notice can + // explain *why* the stream failed instead of only showing the + // bare error name. Owned by `state`; freed in `deinit`. + state.setStreamErrorMessage(e.kind, e.message) catch {}; + // Mid-stream error event before the message was committed. Map + // the common overload case to a dedicated retryable error so the + // UI can say "overloaded" rather than "malformed stream"; other + // kinds stay as the generic retryable malformed-stream error. + if (e.kind) |k| { + if (std.mem.eql(u8, k, "overloaded_error")) return error.ProviderOverloaded; + } return error.ProviderStreamMalformed; }, .unknown => { @@ -1073,7 +1109,34 @@ test "error event propagates as Zig error" { &state, &queue, ); + // `overloaded_error` maps to the dedicated retryable error, and the + // provider's diagnostic is stashed for the agent's retry notice. + try testing.expectError(error.ProviderOverloaded, result); + try testing.expectEqualStrings("overloaded_error: too busy", state.stream_error_message.?); +} + +test "non-overloaded error event stays malformed and stashes message" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var queue = EventQueue.init(allocator); + defer queue.deinit(); + + var state: StreamState = .init(allocator); + defer state.deinit(); + + const result = handleEvent( + allocator, + \\{"type":"error","error":{"type":"api_error","message":"boom"}} + , + &state, + &queue, + ); try testing.expectError(error.ProviderStreamMalformed, result); + try testing.expectEqualStrings("api_error: boom", state.stream_error_message.?); } test "two streamed turns persist assistant replies in the conversation" { |
