activeagent 1.0.0.rc1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/lib/active_agent/providers/_base_provider.rb +94 -82
  4. data/lib/active_agent/providers/anthropic/_types.rb +2 -2
  5. data/lib/active_agent/providers/anthropic/options.rb +4 -6
  6. data/lib/active_agent/providers/anthropic/request.rb +157 -78
  7. data/lib/active_agent/providers/anthropic/transforms.rb +482 -0
  8. data/lib/active_agent/providers/anthropic_provider.rb +159 -59
  9. data/lib/active_agent/providers/common/messages/_types.rb +46 -3
  10. data/lib/active_agent/providers/common/messages/assistant.rb +20 -4
  11. data/lib/active_agent/providers/common/responses/base.rb +118 -70
  12. data/lib/active_agent/providers/common/usage.rb +385 -0
  13. data/lib/active_agent/providers/concerns/instrumentation.rb +263 -0
  14. data/lib/active_agent/providers/concerns/previewable.rb +39 -5
  15. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +62 -0
  16. data/lib/active_agent/providers/log_subscriber.rb +64 -246
  17. data/lib/active_agent/providers/mock_provider.rb +23 -23
  18. data/lib/active_agent/providers/ollama/chat/request.rb +214 -35
  19. data/lib/active_agent/providers/ollama/chat/transforms.rb +135 -0
  20. data/lib/active_agent/providers/ollama/embedding/request.rb +160 -47
  21. data/lib/active_agent/providers/ollama/embedding/transforms.rb +160 -0
  22. data/lib/active_agent/providers/ollama_provider.rb +0 -1
  23. data/lib/active_agent/providers/open_ai/_base.rb +3 -2
  24. data/lib/active_agent/providers/open_ai/chat/_types.rb +13 -1
  25. data/lib/active_agent/providers/open_ai/chat/request.rb +132 -186
  26. data/lib/active_agent/providers/open_ai/chat/transforms.rb +444 -0
  27. data/lib/active_agent/providers/open_ai/chat_provider.rb +95 -36
  28. data/lib/active_agent/providers/open_ai/embedding/_types.rb +13 -2
  29. data/lib/active_agent/providers/open_ai/embedding/request.rb +38 -70
  30. data/lib/active_agent/providers/open_ai/embedding/transforms.rb +88 -0
  31. data/lib/active_agent/providers/open_ai/responses/_types.rb +1 -7
  32. data/lib/active_agent/providers/open_ai/responses/request.rb +116 -135
  33. data/lib/active_agent/providers/open_ai/responses/transforms.rb +363 -0
  34. data/lib/active_agent/providers/open_ai/responses_provider.rb +115 -30
  35. data/lib/active_agent/providers/open_ai_provider.rb +0 -3
  36. data/lib/active_agent/providers/open_router/_types.rb +27 -1
  37. data/lib/active_agent/providers/open_router/options.rb +49 -1
  38. data/lib/active_agent/providers/open_router/request.rb +252 -66
  39. data/lib/active_agent/providers/open_router/requests/_types.rb +0 -1
  40. data/lib/active_agent/providers/open_router/requests/messages/_types.rb +37 -40
  41. data/lib/active_agent/providers/open_router/requests/messages/content/file.rb +19 -3
  42. data/lib/active_agent/providers/open_router/requests/messages/content/files/details.rb +15 -4
  43. data/lib/active_agent/providers/open_router/requests/plugin.rb +19 -3
  44. data/lib/active_agent/providers/open_router/requests/plugins/pdf_config.rb +30 -8
  45. data/lib/active_agent/providers/open_router/requests/prediction.rb +17 -0
  46. data/lib/active_agent/providers/open_router/requests/provider_preferences/max_price.rb +41 -7
  47. data/lib/active_agent/providers/open_router/requests/provider_preferences.rb +60 -19
  48. data/lib/active_agent/providers/open_router/requests/response_format.rb +30 -2
  49. data/lib/active_agent/providers/open_router/transforms.rb +164 -0
  50. data/lib/active_agent/providers/open_router_provider.rb +23 -0
  51. data/lib/active_agent/version.rb +1 -1
  52. metadata +17 -160
  53. data/lib/active_agent/generation_provider/open_router/types.rb +0 -505
  54. data/lib/active_agent/generation_provider/xai_provider.rb +0 -144
  55. data/lib/active_agent/providers/anthropic/requests/_types.rb +0 -190
  56. data/lib/active_agent/providers/anthropic/requests/container_params.rb +0 -19
  57. data/lib/active_agent/providers/anthropic/requests/content/base.rb +0 -21
  58. data/lib/active_agent/providers/anthropic/requests/content/sources/base.rb +0 -22
  59. data/lib/active_agent/providers/anthropic/requests/context_management_config.rb +0 -18
  60. data/lib/active_agent/providers/anthropic/requests/messages/_types.rb +0 -189
  61. data/lib/active_agent/providers/anthropic/requests/messages/assistant.rb +0 -23
  62. data/lib/active_agent/providers/anthropic/requests/messages/base.rb +0 -63
  63. data/lib/active_agent/providers/anthropic/requests/messages/content/_types.rb +0 -143
  64. data/lib/active_agent/providers/anthropic/requests/messages/content/base.rb +0 -21
  65. data/lib/active_agent/providers/anthropic/requests/messages/content/document.rb +0 -26
  66. data/lib/active_agent/providers/anthropic/requests/messages/content/image.rb +0 -23
  67. data/lib/active_agent/providers/anthropic/requests/messages/content/redacted_thinking.rb +0 -21
  68. data/lib/active_agent/providers/anthropic/requests/messages/content/search_result.rb +0 -27
  69. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/_types.rb +0 -171
  70. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/base.rb +0 -22
  71. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_base64.rb +0 -25
  72. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_file.rb +0 -23
  73. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_text.rb +0 -25
  74. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/document_url.rb +0 -23
  75. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_base64.rb +0 -27
  76. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_file.rb +0 -23
  77. data/lib/active_agent/providers/anthropic/requests/messages/content/sources/image_url.rb +0 -23
  78. data/lib/active_agent/providers/anthropic/requests/messages/content/text.rb +0 -22
  79. data/lib/active_agent/providers/anthropic/requests/messages/content/thinking.rb +0 -23
  80. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_result.rb +0 -24
  81. data/lib/active_agent/providers/anthropic/requests/messages/content/tool_use.rb +0 -28
  82. data/lib/active_agent/providers/anthropic/requests/messages/user.rb +0 -21
  83. data/lib/active_agent/providers/anthropic/requests/metadata.rb +0 -18
  84. data/lib/active_agent/providers/anthropic/requests/response_format.rb +0 -22
  85. data/lib/active_agent/providers/anthropic/requests/thinking_config/_types.rb +0 -60
  86. data/lib/active_agent/providers/anthropic/requests/thinking_config/base.rb +0 -20
  87. data/lib/active_agent/providers/anthropic/requests/thinking_config/disabled.rb +0 -16
  88. data/lib/active_agent/providers/anthropic/requests/thinking_config/enabled.rb +0 -20
  89. data/lib/active_agent/providers/anthropic/requests/tool_choice/_types.rb +0 -78
  90. data/lib/active_agent/providers/anthropic/requests/tool_choice/any.rb +0 -17
  91. data/lib/active_agent/providers/anthropic/requests/tool_choice/auto.rb +0 -17
  92. data/lib/active_agent/providers/anthropic/requests/tool_choice/base.rb +0 -20
  93. data/lib/active_agent/providers/anthropic/requests/tool_choice/none.rb +0 -16
  94. data/lib/active_agent/providers/anthropic/requests/tool_choice/tool.rb +0 -20
  95. data/lib/active_agent/providers/ollama/chat/requests/_types.rb +0 -3
  96. data/lib/active_agent/providers/ollama/chat/requests/messages/_types.rb +0 -116
  97. data/lib/active_agent/providers/ollama/chat/requests/messages/assistant.rb +0 -19
  98. data/lib/active_agent/providers/ollama/chat/requests/messages/user.rb +0 -19
  99. data/lib/active_agent/providers/ollama/embedding/requests/_types.rb +0 -83
  100. data/lib/active_agent/providers/ollama/embedding/requests/options.rb +0 -104
  101. data/lib/active_agent/providers/open_ai/chat/requests/_types.rb +0 -229
  102. data/lib/active_agent/providers/open_ai/chat/requests/audio.rb +0 -24
  103. data/lib/active_agent/providers/open_ai/chat/requests/messages/_types.rb +0 -123
  104. data/lib/active_agent/providers/open_ai/chat/requests/messages/assistant.rb +0 -42
  105. data/lib/active_agent/providers/open_ai/chat/requests/messages/base.rb +0 -78
  106. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/_types.rb +0 -133
  107. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/audio.rb +0 -35
  108. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/base.rb +0 -24
  109. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/file.rb +0 -26
  110. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/_types.rb +0 -60
  111. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/files/details.rb +0 -41
  112. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/image.rb +0 -37
  113. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/refusal.rb +0 -25
  114. data/lib/active_agent/providers/open_ai/chat/requests/messages/content/text.rb +0 -25
  115. data/lib/active_agent/providers/open_ai/chat/requests/messages/developer.rb +0 -25
  116. data/lib/active_agent/providers/open_ai/chat/requests/messages/function.rb +0 -25
  117. data/lib/active_agent/providers/open_ai/chat/requests/messages/system.rb +0 -25
  118. data/lib/active_agent/providers/open_ai/chat/requests/messages/tool.rb +0 -26
  119. data/lib/active_agent/providers/open_ai/chat/requests/messages/user.rb +0 -32
  120. data/lib/active_agent/providers/open_ai/chat/requests/prediction.rb +0 -46
  121. data/lib/active_agent/providers/open_ai/chat/requests/response_format.rb +0 -53
  122. data/lib/active_agent/providers/open_ai/chat/requests/stream_options.rb +0 -24
  123. data/lib/active_agent/providers/open_ai/chat/requests/tool_choice.rb +0 -26
  124. data/lib/active_agent/providers/open_ai/chat/requests/tools/_types.rb +0 -5
  125. data/lib/active_agent/providers/open_ai/chat/requests/tools/base.rb +0 -22
  126. data/lib/active_agent/providers/open_ai/chat/requests/tools/custom_tool.rb +0 -41
  127. data/lib/active_agent/providers/open_ai/chat/requests/tools/function_tool.rb +0 -51
  128. data/lib/active_agent/providers/open_ai/chat/requests/web_search_options.rb +0 -45
  129. data/lib/active_agent/providers/open_ai/embedding/requests/_types.rb +0 -49
  130. data/lib/active_agent/providers/open_ai/responses/requests/_types.rb +0 -231
  131. data/lib/active_agent/providers/open_ai/responses/requests/conversation.rb +0 -23
  132. data/lib/active_agent/providers/open_ai/responses/requests/inputs/_types.rb +0 -264
  133. data/lib/active_agent/providers/open_ai/responses/requests/inputs/assistant_message.rb +0 -22
  134. data/lib/active_agent/providers/open_ai/responses/requests/inputs/base.rb +0 -89
  135. data/lib/active_agent/providers/open_ai/responses/requests/inputs/code_interpreter_tool_call.rb +0 -30
  136. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call.rb +0 -28
  137. data/lib/active_agent/providers/open_ai/responses/requests/inputs/computer_tool_call_output.rb +0 -33
  138. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/_types.rb +0 -207
  139. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/base.rb +0 -22
  140. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_audio.rb +0 -26
  141. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_file.rb +0 -28
  142. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_image.rb +0 -28
  143. data/lib/active_agent/providers/open_ai/responses/requests/inputs/content/input_text.rb +0 -25
  144. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call.rb +0 -28
  145. data/lib/active_agent/providers/open_ai/responses/requests/inputs/custom_tool_call_output.rb +0 -27
  146. data/lib/active_agent/providers/open_ai/responses/requests/inputs/developer_message.rb +0 -20
  147. data/lib/active_agent/providers/open_ai/responses/requests/inputs/file_search_tool_call.rb +0 -25
  148. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_call_output.rb +0 -32
  149. data/lib/active_agent/providers/open_ai/responses/requests/inputs/function_tool_call.rb +0 -28
  150. data/lib/active_agent/providers/open_ai/responses/requests/inputs/image_gen_tool_call.rb +0 -27
  151. data/lib/active_agent/providers/open_ai/responses/requests/inputs/input_message.rb +0 -31
  152. data/lib/active_agent/providers/open_ai/responses/requests/inputs/item_reference.rb +0 -23
  153. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call.rb +0 -26
  154. data/lib/active_agent/providers/open_ai/responses/requests/inputs/local_shell_tool_call_output.rb +0 -33
  155. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_request.rb +0 -30
  156. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_approval_response.rb +0 -28
  157. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_list_tools.rb +0 -29
  158. data/lib/active_agent/providers/open_ai/responses/requests/inputs/mcp_tool_call.rb +0 -35
  159. data/lib/active_agent/providers/open_ai/responses/requests/inputs/output_message.rb +0 -35
  160. data/lib/active_agent/providers/open_ai/responses/requests/inputs/reasoning.rb +0 -33
  161. data/lib/active_agent/providers/open_ai/responses/requests/inputs/system_message.rb +0 -20
  162. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_call_base.rb +0 -27
  163. data/lib/active_agent/providers/open_ai/responses/requests/inputs/tool_message.rb +0 -23
  164. data/lib/active_agent/providers/open_ai/responses/requests/inputs/user_message.rb +0 -20
  165. data/lib/active_agent/providers/open_ai/responses/requests/inputs/web_search_tool_call.rb +0 -24
  166. data/lib/active_agent/providers/open_ai/responses/requests/prompt_reference.rb +0 -23
  167. data/lib/active_agent/providers/open_ai/responses/requests/reasoning.rb +0 -23
  168. data/lib/active_agent/providers/open_ai/responses/requests/stream_options.rb +0 -20
  169. data/lib/active_agent/providers/open_ai/responses/requests/text/_types.rb +0 -89
  170. data/lib/active_agent/providers/open_ai/responses/requests/text/base.rb +0 -22
  171. data/lib/active_agent/providers/open_ai/responses/requests/text/json_object.rb +0 -20
  172. data/lib/active_agent/providers/open_ai/responses/requests/text/json_schema.rb +0 -48
  173. data/lib/active_agent/providers/open_ai/responses/requests/text/plain.rb +0 -20
  174. data/lib/active_agent/providers/open_ai/responses/requests/text.rb +0 -41
  175. data/lib/active_agent/providers/open_ai/responses/requests/tool_choice.rb +0 -26
  176. data/lib/active_agent/providers/open_ai/responses/requests/tools/_types.rb +0 -112
  177. data/lib/active_agent/providers/open_ai/responses/requests/tools/base.rb +0 -25
  178. data/lib/active_agent/providers/open_ai/responses/requests/tools/code_interpreter_tool.rb +0 -23
  179. data/lib/active_agent/providers/open_ai/responses/requests/tools/computer_tool.rb +0 -27
  180. data/lib/active_agent/providers/open_ai/responses/requests/tools/custom_tool.rb +0 -28
  181. data/lib/active_agent/providers/open_ai/responses/requests/tools/file_search_tool.rb +0 -27
  182. data/lib/active_agent/providers/open_ai/responses/requests/tools/function_tool.rb +0 -29
  183. data/lib/active_agent/providers/open_ai/responses/requests/tools/image_generation_tool.rb +0 -37
  184. data/lib/active_agent/providers/open_ai/responses/requests/tools/local_shell_tool.rb +0 -21
  185. data/lib/active_agent/providers/open_ai/responses/requests/tools/mcp_tool.rb +0 -41
  186. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_preview_tool.rb +0 -24
  187. data/lib/active_agent/providers/open_ai/responses/requests/tools/web_search_tool.rb +0 -25
  188. data/lib/active_agent/providers/open_ai/schema.yml +0 -65937
  189. data/lib/active_agent/providers/open_router/requests/message.rb +0 -1
  190. data/lib/active_agent/providers/open_router/requests/messages/assistant.rb +0 -20
  191. data/lib/active_agent/providers/open_router/requests/messages/user.rb +0 -30
@@ -5,6 +5,7 @@ require_relative "_base_provider"
5
5
  require_gem!(:anthropic, __FILE__)
6
6
 
7
7
  require_relative "anthropic/_types"
8
+ require_relative "anthropic/transforms"
8
9
 
9
10
  module ActiveAgent
10
11
  module Providers
@@ -16,6 +17,17 @@ module ActiveAgent
16
17
  #
17
18
  # @see BaseProvider
18
19
  class AnthropicProvider < BaseProvider
20
+ # Lead-in message for JSON response format emulation
21
+ JSON_RESPONSE_FORMAT_LEAD_IN = "Here is the JSON requested:\n{"
22
+
23
+ attr_internal :json_format_retry_count
24
+
25
+ def initialize(kwargs = {})
26
+ super
27
+
28
+ self.json_format_retry_count = kwargs[:max_retries] || ::Anthropic::Client::DEFAULT_MAX_RETRIES
29
+ end
30
+
19
31
  # @todo Add support for Anthropic::BedrockClient and Anthropic::VertexClient
20
32
  # @return [Anthropic::Client]
21
33
  def client
@@ -24,10 +36,10 @@ module ActiveAgent
24
36
 
25
37
  protected
26
38
 
27
- # Prepares the request and handles tool choice cleanup.
39
+ # Removes forced tool choice after first use to prevent endless looping.
28
40
  #
29
- # Removes forced tool choice from subsequent requests to prevent endless looping
30
- # when the tool has already been used in the conversation.
41
+ # Clears tool_choice when the specified tool has already been called in the
42
+ # conversation, preventing the model from being forced to call it repeatedly.
31
43
  #
32
44
  # @see BaseProvider#prepare_prompt_request
33
45
  # @return [Request]
@@ -38,74 +50,109 @@ module ActiveAgent
38
50
  super
39
51
  end
40
52
 
41
- # @api private
42
- def prepare_prompt_request_tools
43
- return unless request.tool_choice
53
+ # Extracts function names from Anthropic's tool_use content blocks.
54
+ #
55
+ # @return [Array<String>]
56
+ def extract_used_function_names
57
+ message_stack.pluck(:content).flatten.select { _1[:type] == "tool_use" }.pluck(:name)
58
+ end
44
59
 
45
- functions_used = message_stack.pluck(:content).flatten.select { _1[:type] == "tool_use" }.pluck(:name)
60
+ # Checks if tool_choice requires the model to call any tool.
61
+ #
62
+ # @return [Boolean] true if tool_choice type is :any
63
+ def tool_choice_forces_required?
64
+ return false unless request.tool_choice.respond_to?(:type)
46
65
 
47
- if (request.tool_choice.type == "any" && functions_used.any?) ||
48
- (request.tool_choice.type == "tool" && functions_used.include?(request.tool_choice.name))
66
+ request.tool_choice.type == :any
67
+ end
49
68
 
50
- instrument("tool_choice_removed.provider.active_agent")
51
- request.tool_choice = nil
52
- end
69
+ # Checks if tool_choice requires a specific tool to be called.
70
+ #
71
+ # @return [Array<Boolean, String|nil>] [true, tool_name] if forcing a specific tool, [false, nil] otherwise
72
+ def tool_choice_forces_specific?
73
+ return [ false, nil ] unless request.tool_choice.respond_to?(:type)
74
+ return [ false, nil ] unless request.tool_choice.type == :tool
75
+
76
+ tool_name = request.tool_choice.respond_to?(:name) ? request.tool_choice.name : nil
77
+ [ true, tool_name ]
53
78
  end
54
79
 
55
80
  # @api private
56
81
  def prepare_prompt_request_response_format
57
- return unless request.response_format&.type == "json_object"
82
+ return unless request.response_format&.dig(:type) == "json_object"
58
83
 
59
84
  self.message_stack.push({
60
85
  role: "assistant",
61
- content: "Here is the JSON requested:\n{"
86
+ content: JSON_RESPONSE_FORMAT_LEAD_IN
62
87
  })
63
88
  end
64
89
 
90
+ # Selects between Anthropic's stable and beta message APIs.
91
+ #
92
+ # Uses beta API when explicitly requested via anthropic_beta option or when
93
+ # using MCP servers, which require beta features. Falls back to stable API
94
+ # for standard message creation.
95
+ #
96
+ # @see BaseProvider#api_prompt_executer
97
+ # @return [Anthropic::Messages, Anthropic::Resources::Beta::Messages]
65
98
  def api_prompt_executer
66
- client.messages
99
+ # Use beta API when anthropic_beta option is set or when using MCP servers
100
+ if options.anthropic_beta.present? || request.mcp_servers&.any?
101
+ client.beta.messages
102
+ else
103
+ client.messages
104
+ end
67
105
  end
68
106
 
69
- # Processes streaming response chunks from Anthropic's API.
107
+ # @see BaseProvider#api_response_normalize
108
+ # @param api_response [Anthropic::Models::Message]
109
+ # @return [Hash]
110
+ def api_response_normalize(api_response)
111
+ return api_response unless api_response
112
+
113
+ Anthropic::Transforms.gem_to_hash(api_response)
114
+ end
115
+
116
+ # Processes streaming chunks and builds message incrementally in message_stack.
70
117
  #
71
118
  # Handles chunk types: message_start, content_block_start, content_block_delta,
72
119
  # content_block_stop, message_delta, message_stop. Manages text deltas,
73
120
  # tool use inputs, and Claude's thinking/signature blocks.
74
121
  #
75
- # @param api_response_chunk [Object]
122
+ # @see BaseProvider#process_stream_chunk
123
+ # @param api_response_chunk [Anthropic::StreamEvent]
76
124
  # @return [void]
77
125
  def process_stream_chunk(api_response_chunk)
78
- api_response_chunk = api_response_chunk.as_json.deep_symbolize_keys
79
- chunk_type = api_response_chunk[:type].to_sym
126
+ chunk_type = api_response_chunk[:type]&.to_sym
80
127
 
81
- instrument("stream_chunk_processing.provider.active_agent", chunk_type:)
128
+ instrument("stream_chunk.active_agent", chunk_type:)
82
129
 
83
130
  broadcast_stream_open
84
131
 
85
132
  case chunk_type
86
133
  # Message Created
87
134
  when :message_start
88
- api_message = api_response_chunk.fetch(:message)
135
+ api_message = Anthropic::Transforms.gem_to_hash(api_response_chunk.message)
89
136
  self.message_stack.push(api_message)
90
137
  broadcast_stream_update(message_stack.last)
91
138
 
92
139
  # -> Content Block Create
93
140
  when :content_block_start
94
- api_content = api_response_chunk.fetch(:content_block)
141
+ api_content = Anthropic::Transforms.gem_to_hash(api_response_chunk.content_block)
95
142
  self.message_stack.last[:content].push(api_content)
96
143
  broadcast_stream_update(message_stack.last, api_content[:text])
97
144
 
98
145
  # -> -> Content Block Append
99
146
  when :content_block_delta
100
- index = api_response_chunk.fetch(:index)
147
+ index = api_response_chunk.index
101
148
  content = self.message_stack.last[:content][index]
102
- api_delta = api_response_chunk.fetch(:delta)
149
+ api_delta = api_response_chunk.delta
103
150
 
104
- case (delta_type = api_delta[:type].to_sym)
151
+ case api_delta.type.to_sym
105
152
  # -> -> -> Content Text Append
106
153
  when :text_delta
107
- content[:text] += api_delta[:text]
108
- broadcast_stream_update(message_stack.last, api_delta[:text])
154
+ content[:text] += api_delta.text
155
+ broadcast_stream_update(message_stack.last, api_delta.text)
109
156
 
110
157
  # -> -> -> Content Function Call Append
111
158
  when :input_json_delta
@@ -113,21 +160,29 @@ module ActiveAgent
113
160
  when :thinking_delta, :signature_delta
114
161
  # TODO: Add with thinking rendering support
115
162
  else
116
- fail "Unexpected Delta Type #{delta_type}"
163
+ raise "Unexpected delta type: #{api_delta.type}"
117
164
  end
118
165
  # -> Content Block Completed [Full Block]
119
166
  when :content_block_stop
120
- index = api_response_chunk.fetch(:index)
121
- api_content = api_response_chunk.fetch(:content_block)
167
+ index = api_response_chunk.index
168
+ api_content = Anthropic::Transforms.gem_to_hash(api_response_chunk.content_block)
122
169
  self.message_stack.last[:content][index] = api_content
123
170
 
124
171
  # Message Delta
125
172
  when :message_delta
126
- self.message_stack.last.merge!(api_response_chunk.fetch(:delta))
173
+ delta = Anthropic::Transforms.gem_to_hash(api_response_chunk.delta)
174
+ self.message_stack.last.merge!(delta)
127
175
 
128
176
  # Message Completed [Full Message]
129
177
  when :message_stop
130
- self.message_stack[-1] = api_response_chunk.fetch(:message)
178
+ api_message = Anthropic::Transforms.gem_to_hash(api_response_chunk.message)
179
+
180
+ # Handle _json_buf (gem >= 1.14.0)
181
+ api_message[:content]&.each do |content_block|
182
+ content_block.delete(:_json_buf) if content_block[:type] == "tool_use"
183
+ end
184
+
185
+ self.message_stack[-1] = api_message
131
186
 
132
187
  # Once we are finished, close out and run tooling callbacks (Recursive)
133
188
  process_prompt_finished if message_stack.last[:stop_reason]
@@ -137,12 +192,12 @@ module ActiveAgent
137
192
  # TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
138
193
  else
139
194
  # No-Op: Looks like internal tracking from gem wrapper
140
- return if api_response_chunk.key?(:snapshot)
141
- fail "Unexpected Chunk Type: #{api_response_chunk[:type]}"
195
+ return if api_response_chunk.respond_to?(:snapshot)
196
+ raise "Unexpected chunk type: #{api_response_chunk.type}"
142
197
  end
143
198
  end
144
199
 
145
- # Executes tool calls and creates user message with results.
200
+ # Executes tool calls and appends user message with results to message_stack.
146
201
  #
147
202
  # @param api_function_calls [Array<Hash>] with :name, :input, and :id keys
148
203
  # @return [void]
@@ -151,58 +206,103 @@ module ActiveAgent
151
206
  process_tool_call_function(api_function_call)
152
207
  end
153
208
 
154
- message = Anthropic::Requests::Messages::User.new(content:)
209
+ api_message = ::Anthropic::Models::MessageParam.new(role: "user", content:)
210
+ message = Anthropic::Transforms.gem_to_hash(api_message)
155
211
 
156
- message_stack.push(message.serialize)
212
+ message_stack.push(message)
157
213
  end
158
214
 
159
- # Executes a single tool call and returns the result.
215
+ # Executes a single tool call via callback.
160
216
  #
161
217
  # @param api_function_call [Hash] with :name, :input, and :id keys
162
- # @return [Anthropic::Requests::Content::ToolResult]
218
+ # @return [Anthropic::Models::ToolResultBlockParam]
163
219
  def process_tool_call_function(api_function_call)
164
- instrument("tool_execution.provider.active_agent", tool_name: api_function_call[:name])
220
+ instrument("tool_call.active_agent", tool_name: api_function_call[:name]) do
221
+ results = tools_function.call(
222
+ api_function_call[:name], **api_function_call[:input]
223
+ )
224
+
225
+ ::Anthropic::Models::ToolResultBlockParam.new(
226
+ type: "tool_result",
227
+ tool_use_id: api_function_call[:id],
228
+ content: results.to_json,
229
+ is_error: false
230
+ )
231
+ end
232
+ end
233
+
234
+ # Processes completed API response and handles JSON format retries.
235
+ #
236
+ # When response_format is json_object and the response fails JSON validation,
237
+ # recursively retries the request to obtain well-formed JSON.
238
+ #
239
+ # @see BaseProvider#process_prompt_finished
240
+ # @param api_response [Anthropic::Models::Message]
241
+ # @return [Common::PromptResponse, nil]
242
+ def process_prompt_finished(api_response = nil)
243
+ # Convert gem object to hash so that raw_response[:usage] works
244
+ api_response_hash = api_response ? Anthropic::Transforms.gem_to_hash(api_response) : nil
245
+
246
+ common_response = super(api_response_hash)
165
247
 
166
- results = tools_function.call(
167
- api_function_call[:name], **api_function_call[:input]
168
- )
248
+ # If we failed to get the expected well formed JSON Object Response, recursively try again
249
+ if request.response_format&.dig(:type) == "json_object" && common_response.message.parsed_json.nil? && json_format_retry_count > 0
250
+ self.json_format_retry_count -= 1
169
251
 
170
- Anthropic::Requests::Content::ToolResult.new(
171
- tool_use_id: api_function_call[:id],
172
- content: results.to_json,
173
- )
252
+ resolve_prompt
253
+ else
254
+ common_response
255
+ end
174
256
  end
175
257
 
176
- # Extracts messages from completed API response.
258
+ # Reconstructs JSON responses that were split due to Anthropic format constraints.
177
259
  #
178
- # Handles JSON response format by merging the leading `{` prefix back into
179
- # the message content after removing the assistant lead-in message.
260
+ # Anthropic's API doesn't natively support json_object response format, so we
261
+ # simulate it by having the assistant echo a JSON lead-in ("Here is the JSON requested:\n{"),
262
+ # then send the response back for completion. This method detects and reverses
263
+ # that workaround by stripping the lead-in message and prepending "{" to the response.
180
264
  #
181
- # @param api_response [Object]
265
+ # @see BaseProvider#process_prompt_finished_extract_messages
266
+ # @param api_response [Hash] API response with content blocks
182
267
  # @return [Array<Hash>, nil]
183
268
  def process_prompt_finished_extract_messages(api_response)
184
269
  return unless api_response
185
270
 
186
- message = api_response.as_json.deep_symbolize_keys
271
+ # Get the last message (may be either Hash or gem object)
272
+ last_message = request.messages.last
273
+ last_role = last_message.is_a?(Hash) ? last_message[:role] : last_message&.role
274
+ last_content = last_message.is_a?(Hash) ? last_message[:content] : last_message&.content
275
+
276
+ # Check if the last message in request is the JSON lead-in prompt
277
+ if last_role.to_sym == :assistant && last_content == JSON_RESPONSE_FORMAT_LEAD_IN
278
+ # Remove the lead-in message from the request
279
+ request.messages.pop
187
280
 
188
- if request.response_format&.type == "json_object"
189
- request.messages.pop # Remove the `Here is the JSON requested:\n{` lead in message
190
- message[:content][0][:text] = "{#{message[:content][0][:text]}" # Merge in `{` prefix
281
+ # Prepend "{" to the response's first content text
282
+ if api_response[:content]&.first&.dig(:text)
283
+ api_response[:content][0][:text] = "{#{api_response[:content][0][:text]}"
284
+ end
191
285
  end
192
286
 
193
- [ message ]
287
+ [ api_response ]
194
288
  end
195
289
 
196
- # Extracts function calls from message stack.
290
+ # Extracts tool_use blocks from message_stack and parses JSON inputs.
197
291
  #
198
- # Processes tool_use content blocks and converts JSON buffers into proper
199
- # input parameters for function execution.
292
+ # Handles JSON buffer parsing for gem versions and string inputs for gem >= 1.14.0.
200
293
  #
294
+ # @see BaseProvider#process_prompt_finished_extract_function_calls
201
295
  # @return [Array<Hash>] with :name, :input, and :id keys
202
296
  def process_prompt_finished_extract_function_calls
203
297
  message_stack.pluck(:content).flatten.select { _1 in { type: "tool_use" } }.map do |api_function_call|
204
298
  json_buf = api_function_call.delete(:json_buf)
205
299
  api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf
300
+
301
+ # Handle case where :input is still a JSON string (gem >= 1.14.0)
302
+ if api_function_call[:input].is_a?(String)
303
+ api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
304
+ end
305
+
206
306
  api_function_call
207
307
  end
208
308
  end
@@ -37,7 +37,7 @@ module ActiveAgent
37
37
  role = hash[:role]&.to_s
38
38
 
39
39
  case role
40
- when "system"
40
+ when "system", "developer"
41
41
  nil # System messages are dropped in common format, replaced by Instructions
42
42
  when "user", nil
43
43
  # Handle both standard format and format with `text` key
@@ -63,6 +63,13 @@ module ActiveAgent
63
63
  # Check if the value responds to to_common (provider-specific message)
64
64
  if value.respond_to?(:to_common)
65
65
  cast_message(value.to_common)
66
+ # Check if it's a gem model object that can be converted to hash
67
+ # Use JSON round-trip to ensure proper nested serialization
68
+ elsif value.respond_to?(:to_json)
69
+ hash = JSON.parse(value.to_json, symbolize_names: true)
70
+ cast_message(hash)
71
+ elsif value.respond_to?(:to_h)
72
+ cast_message(value.to_h)
66
73
  else
67
74
  raise ArgumentError, "Cannot cast #{value.class} to Message"
68
75
  end
@@ -78,7 +85,7 @@ module ActiveAgent
78
85
  when Hash
79
86
  value
80
87
  else
81
- raise ArgumentError, "Cannot serialize #{value.class}"
88
+ raise ArgumentError, "Cannot serialize #{value.class}"
82
89
  end
83
90
  end
84
91
  end
@@ -88,7 +95,9 @@ module ActiveAgent
88
95
  def cast(value)
89
96
  case value
90
97
  when Array
91
- value.map { |v| message_type.cast(v) }.compact
98
+ messages = value.map { |v| message_type.cast(v) }.compact
99
+ # Split messages with array content into separate messages
100
+ messages.flat_map { |msg| split_content_blocks(msg) }
92
101
  when nil
93
102
  []
94
103
  else
@@ -116,6 +125,40 @@ module ActiveAgent
116
125
  def message_type
117
126
  @message_type ||= MessageType.new
118
127
  end
128
+
129
+ # Splits an assistant message with array content into separate messages
130
+ # for each content block.
131
+ #
132
+ # @param message [Common::Messages::Base]
133
+ # @return [Array<Common::Messages::Base>]
134
+ def split_content_blocks(message)
135
+ # Only split assistant messages with array content
136
+ return [ message ] unless message.is_a?(Common::Messages::Assistant) && message.content.is_a?(Array)
137
+
138
+ message.content.map do |block|
139
+ case block[:type]&.to_s
140
+ when "text"
141
+ # Create a message for text blocks
142
+ Common::Messages::Assistant.new(role: "assistant", content: block[:text], name: message.name)
143
+ when "tool_use"
144
+ # Create a message with tool use info as string representation
145
+ tool_info = "[Tool Use: #{block[:name]}]\nID: #{block[:id]}\nInput: #{JSON.pretty_generate(block[:input])}"
146
+ Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
147
+ when "mcp_tool_use"
148
+ # Create a message with MCP tool use info
149
+ tool_info = "[MCP Tool Use: #{block[:name]}]\nID: #{block[:id]}\nServer: #{block[:server_name]}\nInput: #{JSON.pretty_generate(block[:input] || {})}"
150
+ Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name)
151
+ when "mcp_tool_result"
152
+ # Create a message with MCP tool result
153
+ result_info = "[MCP Tool Result]\n#{block[:content]}"
154
+ Common::Messages::Assistant.new(role: "assistant", content: result_info, name: message.name)
155
+ else
156
+ # For unknown block types, try to extract text
157
+ content = block[:text] || block.to_s
158
+ Common::Messages::Assistant.new(role: "assistant", content:, name: message.name)
159
+ end
160
+ end.compact
161
+ end
119
162
  end
120
163
  end
121
164
  end
@@ -9,7 +9,7 @@ module ActiveAgent
9
9
  # Represents messages sent by the AI assistant in a conversation.
10
10
  class Assistant < Base
11
11
  attribute :role, :string, as: "assistant"
12
- attribute :content, :string
12
+ attribute :content # Accept both string and array (provider-native formats)
13
13
  attribute :name, :string
14
14
 
15
15
  validates :content, presence: true
@@ -24,9 +24,16 @@ module ActiveAgent
24
24
  # @param normalize_names [Symbol, nil] key normalization method (e.g., :underscore)
25
25
  # @return [Hash, Array, nil] parsed JSON structure or nil if parsing fails
26
26
  def parsed_json(symbolize_names: true, normalize_names: :underscore)
27
- start_char = [ content.index("{"), content.index("[") ].compact.min
28
- end_char = [ content.rindex("}"), content.rindex("]") ].compact.max
29
- content_stripped = content[start_char..end_char] if start_char && end_char
27
+ # Handle array content (from content blocks) by searching through each block
28
+ content_str = if content.is_a?(Array)
29
+ content.map { |block| block.is_a?(Hash) ? block[:text] : block.to_s }.join("\n")
30
+ else
31
+ content.to_s
32
+ end
33
+
34
+ start_char = [ content_str.index("{"), content_str.index("[") ].compact.min
35
+ end_char = [ content_str.rindex("}"), content_str.rindex("]") ].compact.max
36
+ content_stripped = content_str[start_char..end_char] if start_char && end_char
30
37
  return unless content_stripped
31
38
 
32
39
  content_parsed = JSON.parse(content_stripped)
@@ -48,6 +55,15 @@ module ActiveAgent
48
55
  nil
49
56
  end
50
57
 
58
+ # Returns content as a string, handling both string and array formats
59
+ def text
60
+ if content.is_a?(Array)
61
+ content.map { |block| block.is_a?(Hash) ? block[:text] : block.to_s }.join("\n")
62
+ else
63
+ content.to_s
64
+ end
65
+ end
66
+
51
67
  alias_method :json_object, :parsed_json
52
68
  alias_method :parse_json, :parsed_json
53
69
  end