Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions lib/active_agent/providers/anthropic_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,14 @@ def process_stream_chunk(api_response_chunk)
when :ping
# No-Op Keep Awake
when :overloaded_error
# TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
# TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events

# Higher-level convenience events from anthropic gem's MessageStream
when :text, :input_json, :citation, :thinking, :signature
# No-Op; Already handled via :content_block_delta

else
# No-Op: Looks like internal tracking from gem wrapper
# No-Op; Internal tracking from gem wrapper
return if api_response_chunk.respond_to?(:snapshot)
raise "Unexpected chunk type: #{api_response_chunk.type}"
end
Expand Down Expand Up @@ -296,11 +301,16 @@ def process_prompt_finished_extract_messages(api_response)
def process_prompt_finished_extract_function_calls
message_stack.pluck(:content).flatten.select { _1 in { type: "tool_use" } }.map do |api_function_call|
json_buf = api_function_call.delete(:json_buf)
api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf
api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf.present?

# Handle case where :input is still a JSON string (gem >= 1.14.0)
# For tools with no parameters, input may be an empty string
if api_function_call[:input].is_a?(String)
api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
if api_function_call[:input].present?
api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
else
api_function_call[:input] = {}
end
end

api_function_call
Expand Down
47 changes: 47 additions & 0 deletions test/providers/anthropic/empty_tool_input_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require "test_helper"
require_relative "../../../lib/active_agent/providers/anthropic_provider"

module Providers
module Anthropic
class EmptyToolInputTest < ActiveSupport::TestCase
setup do
@provider = ActiveAgent::Providers::AnthropicProvider.new(
service: "Anthropic",
model: "claude-sonnet-4-5",
messages: [ { role: "user", content: "Hello" } ],
stream_broadcaster: ->(message, delta, event_type) { }
)
end

test "handles empty string input for tools with no parameters" do
@provider.send(:message_stack).push({
role: "assistant",
content: [
{ type: "tool_use", id: "tool_123", name: "no_param_tool", input: "" }
]
})

result = @provider.send(:process_prompt_finished_extract_function_calls)

assert_equal 1, result.size
assert_equal({}, result.first[:input])
end

test "handles empty json_buf gracefully" do
@provider.send(:message_stack).push({
role: "assistant",
content: [
{ type: "tool_use", id: "tool_123", name: "no_param_tool", json_buf: "", input: nil }
]
})

result = @provider.send(:process_prompt_finished_extract_function_calls)

assert_equal 1, result.size
assert_nil result.first[:input]
end
end
end
end
46 changes: 46 additions & 0 deletions test/providers/anthropic/streaming_events_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require "test_helper"
require_relative "../../../lib/active_agent/providers/anthropic_provider"

module Providers
module Anthropic
class StreamingEventsTest < ActiveSupport::TestCase
setup do
@provider = ActiveAgent::Providers::AnthropicProvider.new(
service: "Anthropic",
model: "claude-sonnet-4-5",
messages: [ { role: "user", content: "Hello" } ],
stream_broadcaster: ->(message, delta, event_type) { }
)

@provider.send(:message_stack).push({
role: "assistant",
content: [ { type: "text", text: "" } ]
})
end

MockEvent = Struct.new(:type, keyword_init: true) do
def [](key)
send(key) if respond_to?(key)
end
end

test "handles :signature event without raising" do
event = MockEvent.new(type: :signature)

assert_nothing_raised do
@provider.send(:process_stream_chunk, event)
end
end

test "handles :thinking event without raising" do
event = MockEvent.new(type: :thinking)

assert_nothing_raised do
@provider.send(:process_stream_chunk, event)
end
end
end
end
end