cua-agent 0.1.6__py3-none-any.whl → 0.1.18__py3-none-any.whl

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.

Potentially problematic release.


This version of cua-agent might be problematic. Click here for more details.

Files changed (57) hide show
  1. agent/__init__.py +3 -2
  2. agent/core/__init__.py +1 -6
  3. agent/core/{computer_agent.py → agent.py} +31 -76
  4. agent/core/{loop.py → base.py} +68 -127
  5. agent/core/factory.py +104 -0
  6. agent/core/messages.py +279 -125
  7. agent/core/provider_config.py +15 -0
  8. agent/core/types.py +45 -0
  9. agent/core/visualization.py +197 -0
  10. agent/providers/anthropic/api/client.py +142 -1
  11. agent/providers/anthropic/api_handler.py +140 -0
  12. agent/providers/anthropic/callbacks/__init__.py +5 -0
  13. agent/providers/anthropic/loop.py +207 -221
  14. agent/providers/anthropic/response_handler.py +226 -0
  15. agent/providers/anthropic/tools/bash.py +0 -97
  16. agent/providers/anthropic/utils.py +368 -0
  17. agent/providers/omni/__init__.py +1 -20
  18. agent/providers/omni/api_handler.py +42 -0
  19. agent/providers/omni/clients/anthropic.py +4 -0
  20. agent/providers/omni/image_utils.py +0 -72
  21. agent/providers/omni/loop.py +491 -607
  22. agent/providers/omni/parser.py +58 -4
  23. agent/providers/omni/tools/__init__.py +25 -7
  24. agent/providers/omni/tools/base.py +29 -0
  25. agent/providers/omni/tools/bash.py +43 -38
  26. agent/providers/omni/tools/computer.py +144 -182
  27. agent/providers/omni/tools/manager.py +25 -45
  28. agent/providers/omni/types.py +1 -3
  29. agent/providers/omni/utils.py +224 -145
  30. agent/providers/openai/__init__.py +6 -0
  31. agent/providers/openai/api_handler.py +453 -0
  32. agent/providers/openai/loop.py +440 -0
  33. agent/providers/openai/response_handler.py +205 -0
  34. agent/providers/openai/tools/__init__.py +15 -0
  35. agent/providers/openai/tools/base.py +79 -0
  36. agent/providers/openai/tools/computer.py +319 -0
  37. agent/providers/openai/tools/manager.py +106 -0
  38. agent/providers/openai/types.py +36 -0
  39. agent/providers/openai/utils.py +98 -0
  40. cua_agent-0.1.18.dist-info/METADATA +165 -0
  41. cua_agent-0.1.18.dist-info/RECORD +73 -0
  42. agent/README.md +0 -63
  43. agent/providers/anthropic/messages/manager.py +0 -112
  44. agent/providers/omni/callbacks.py +0 -78
  45. agent/providers/omni/clients/groq.py +0 -101
  46. agent/providers/omni/experiment.py +0 -276
  47. agent/providers/omni/messages.py +0 -171
  48. agent/providers/omni/tool_manager.py +0 -91
  49. agent/providers/omni/visualization.py +0 -130
  50. agent/types/__init__.py +0 -23
  51. agent/types/base.py +0 -41
  52. agent/types/messages.py +0 -36
  53. cua_agent-0.1.6.dist-info/METADATA +0 -120
  54. cua_agent-0.1.6.dist-info/RECORD +0 -64
  55. /agent/{types → core}/tools.py +0 -0
  56. {cua_agent-0.1.6.dist-info → cua_agent-0.1.18.dist-info}/WHEEL +0 -0
  57. {cua_agent-0.1.6.dist-info → cua_agent-0.1.18.dist-info}/entry_points.txt +0 -0
@@ -2,39 +2,36 @@
2
2
 
3
3
  import logging
4
4
  import asyncio
5
- import json
6
- import os
7
5
  from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast
8
- import base64
9
- from datetime import datetime
10
- from httpx import ConnectError, ReadTimeout
11
-
12
- # Anthropic-specific imports
13
- from anthropic import AsyncAnthropic
14
6
  from anthropic.types.beta import (
15
7
  BetaMessage,
16
8
  BetaMessageParam,
17
9
  BetaTextBlock,
18
- BetaTextBlockParam,
19
- BetaToolUseBlockParam,
20
10
  BetaContentBlockParam,
21
11
  )
12
+ import base64
13
+ from datetime import datetime
22
14
 
23
15
  # Computer
24
16
  from computer import Computer
25
17
 
26
18
  # Base imports
27
- from ...core.loop import BaseLoop
28
- from ...core.messages import ImageRetentionConfig as CoreImageRetentionConfig
19
+ from ...core.base import BaseLoop
20
+ from ...core.messages import StandardMessageManager, ImageRetentionConfig
21
+ from ...core.types import AgentResponse
29
22
 
30
23
  # Anthropic provider-specific imports
31
24
  from .api.client import AnthropicClientFactory, BaseAnthropicClient
32
25
  from .tools.manager import ToolManager
33
- from .messages.manager import MessageManager, ImageRetentionConfig
34
- from .callbacks.manager import CallbackManager
35
26
  from .prompts import SYSTEM_PROMPT
36
27
  from .types import LLMProvider
37
28
  from .tools import ToolResult
29
+ from .utils import to_anthropic_format, to_agent_response_format
30
+
31
+ # Import the new modules we created
32
+ from .api_handler import AnthropicAPIHandler
33
+ from .response_handler import AnthropicResponseHandler
34
+ from .callbacks.manager import CallbackManager
38
35
 
39
36
  # Constants
40
37
  COMPUTER_USE_BETA_FLAG = "computer-use-2025-01-24"
@@ -44,13 +41,22 @@ logger = logging.getLogger(__name__)
44
41
 
45
42
 
46
43
  class AnthropicLoop(BaseLoop):
47
- """Anthropic-specific implementation of the agent loop."""
44
+ """Anthropic-specific implementation of the agent loop.
45
+
46
+ This class extends BaseLoop to provide specialized support for Anthropic's Claude models
47
+ with their unique tool-use capabilities, custom message formatting, and
48
+ callback-driven approach to handling responses.
49
+ """
50
+
51
+ ###########################################
52
+ # INITIALIZATION AND CONFIGURATION
53
+ ###########################################
48
54
 
49
55
  def __init__(
50
56
  self,
51
57
  api_key: str,
52
58
  computer: Computer,
53
- model: str = "claude-3-7-sonnet-20250219", # Fixed model
59
+ model: str = "claude-3-7-sonnet-20250219",
54
60
  only_n_most_recent_images: Optional[int] = 2,
55
61
  base_dir: Optional[str] = "trajectories",
56
62
  max_retries: int = 3,
@@ -83,27 +89,33 @@ class AnthropicLoop(BaseLoop):
83
89
  **kwargs,
84
90
  )
85
91
 
86
- # Ensure model is always the fixed one
87
- self.model = "claude-3-7-sonnet-20250219"
92
+ # Initialize message manager
93
+ self.message_manager = StandardMessageManager(
94
+ config=ImageRetentionConfig(num_images_to_keep=only_n_most_recent_images)
95
+ )
88
96
 
89
97
  # Anthropic-specific attributes
90
98
  self.provider = LLMProvider.ANTHROPIC
91
99
  self.client = None
92
100
  self.retry_count = 0
93
101
  self.tool_manager = None
94
- self.message_manager = None
95
102
  self.callback_manager = None
103
+ self.queue = asyncio.Queue() # Initialize queue
96
104
 
97
- # Configure image retention with core config
98
- self.image_retention_config = CoreImageRetentionConfig(
99
- num_images_to_keep=only_n_most_recent_images
100
- )
105
+ # Initialize handlers
106
+ self.api_handler = AnthropicAPIHandler(self)
107
+ self.response_handler = AnthropicResponseHandler(self)
101
108
 
102
- # Message history
103
- self.message_history = []
109
+ ###########################################
110
+ # CLIENT INITIALIZATION - IMPLEMENTING ABSTRACT METHOD
111
+ ###########################################
104
112
 
105
113
  async def initialize_client(self) -> None:
106
- """Initialize the Anthropic API client and tools."""
114
+ """Initialize the Anthropic API client and tools.
115
+
116
+ Implements abstract method from BaseLoop to set up the Anthropic-specific
117
+ client, tool manager, message manager, and callback handlers.
118
+ """
107
119
  try:
108
120
  logger.info(f"Initializing Anthropic client with model {self.model}...")
109
121
 
@@ -112,14 +124,7 @@ class AnthropicLoop(BaseLoop):
112
124
  provider=self.provider, api_key=self.api_key, model=self.model
113
125
  )
114
126
 
115
- # Initialize message manager
116
- self.message_manager = MessageManager(
117
- image_retention_config=ImageRetentionConfig(
118
- num_images_to_keep=self.only_n_most_recent_images, enable_caching=True
119
- )
120
- )
121
-
122
- # Initialize callback manager
127
+ # Initialize callback manager with our callback handlers
123
128
  self.callback_manager = CallbackManager(
124
129
  content_callback=self._handle_content,
125
130
  tool_callback=self._handle_tool_result,
@@ -136,62 +141,22 @@ class AnthropicLoop(BaseLoop):
136
141
  self.client = None
137
142
  raise RuntimeError(f"Failed to initialize Anthropic client: {str(e)}")
138
143
 
139
- async def _process_screen(
140
- self, parsed_screen: Dict[str, Any], messages: List[Dict[str, Any]]
141
- ) -> None:
142
- """Process screen information and add to messages.
144
+ ###########################################
145
+ # MAIN LOOP - IMPLEMENTING ABSTRACT METHOD
146
+ ###########################################
143
147
 
144
- Args:
145
- parsed_screen: Dictionary containing parsed screen info
146
- messages: List of messages to update
147
- """
148
- try:
149
- # Extract screenshot from parsed screen
150
- screenshot_base64 = parsed_screen.get("screenshot_base64")
151
-
152
- if screenshot_base64:
153
- # Remove data URL prefix if present
154
- if "," in screenshot_base64:
155
- screenshot_base64 = screenshot_base64.split(",")[1]
156
-
157
- # Create Anthropic-compatible message with image
158
- screen_info_msg = {
159
- "role": "user",
160
- "content": [
161
- {
162
- "type": "image",
163
- "source": {
164
- "type": "base64",
165
- "media_type": "image/png",
166
- "data": screenshot_base64,
167
- },
168
- }
169
- ],
170
- }
171
-
172
- # Add screen info message to messages
173
- messages.append(screen_info_msg)
174
-
175
- except Exception as e:
176
- logger.error(f"Error processing screen info: {str(e)}")
177
- raise
178
-
179
- async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[Dict[str, Any], None]:
148
+ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentResponse, None]:
180
149
  """Run the agent loop with provided messages.
181
150
 
182
151
  Args:
183
- messages: List of message objects
152
+ messages: List of message objects in standard OpenAI format
184
153
 
185
154
  Yields:
186
- Dict containing response data
155
+ Agent response format
187
156
  """
188
157
  try:
189
158
  logger.info("Starting Anthropic loop run")
190
159
 
191
- # Reset message history and add new messages
192
- self.message_history = []
193
- self.message_history.extend(messages)
194
-
195
160
  # Create queue for response streaming
196
161
  queue = asyncio.Queue()
197
162
 
@@ -204,7 +169,7 @@ class AnthropicLoop(BaseLoop):
204
169
  logger.info("Client initialized successfully")
205
170
 
206
171
  # Start loop in background task
207
- loop_task = asyncio.create_task(self._run_loop(queue))
172
+ loop_task = asyncio.create_task(self._run_loop(queue, messages))
208
173
 
209
174
  # Process and yield messages as they arrive
210
175
  while True:
@@ -236,37 +201,87 @@ class AnthropicLoop(BaseLoop):
236
201
  "metadata": {"title": "❌ Error"},
237
202
  }
238
203
 
239
- async def _run_loop(self, queue: asyncio.Queue) -> None:
240
- """Run the agent loop with current message history.
204
+ ###########################################
205
+ # AGENT LOOP IMPLEMENTATION
206
+ ###########################################
207
+
208
+ async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) -> None:
209
+ """Run the agent loop with provided messages.
241
210
 
242
211
  Args:
243
212
  queue: Queue for response streaming
213
+ messages: List of messages in standard OpenAI format
244
214
  """
245
215
  try:
246
216
  while True:
247
- # Get up-to-date screen information
248
- parsed_screen = await self._get_parsed_screen_som()
249
-
250
- # Process screen info and update messages
251
- await self._process_screen(parsed_screen, self.message_history)
217
+ # Capture screenshot
218
+ try:
219
+ # Take screenshot - always returns raw PNG bytes
220
+ screenshot = await self.computer.interface.screenshot()
221
+ logger.info("Screenshot captured successfully")
222
+
223
+ # Convert PNG bytes to base64
224
+ base64_image = base64.b64encode(screenshot).decode("utf-8")
225
+ logger.info(f"Screenshot converted to base64 (size: {len(base64_image)} bytes)")
226
+
227
+ # Save screenshot if requested
228
+ if self.save_trajectory and self.experiment_manager:
229
+ try:
230
+ self._save_screenshot(base64_image, action_type="state")
231
+ logger.info("Screenshot saved to trajectory")
232
+ except Exception as e:
233
+ logger.error(f"Error saving screenshot: {str(e)}")
234
+
235
+ # Create screenshot message
236
+ screen_info_msg = {
237
+ "role": "user",
238
+ "content": [
239
+ {
240
+ "type": "image",
241
+ "source": {
242
+ "type": "base64",
243
+ "media_type": "image/png",
244
+ "data": base64_image,
245
+ },
246
+ }
247
+ ],
248
+ }
249
+ # Add screenshot to messages
250
+ messages.append(screen_info_msg)
251
+ logger.info("Screenshot message added to conversation")
252
252
 
253
- # Prepare messages and make API call
254
- if self.message_manager is None:
255
- raise RuntimeError(
256
- "Message manager not initialized. Call initialize_client() first."
257
- )
258
- prepared_messages = self.message_manager.prepare_messages(
259
- cast(List[BetaMessageParam], self.message_history.copy())
260
- )
253
+ except Exception as e:
254
+ logger.error(f"Error capturing or processing screenshot: {str(e)}")
255
+ raise
261
256
 
262
257
  # Create new turn directory for this API call
263
258
  self._create_turn_dir()
264
259
 
265
- # Use _make_api_call instead of direct client call to ensure logging
266
- response = await self._make_api_call(prepared_messages)
260
+ # Convert standard messages to Anthropic format using utility function
261
+ anthropic_messages, system_content = to_anthropic_format(messages.copy())
262
+
263
+ # Use API handler to make API call with Anthropic format
264
+ response = await self.api_handler.make_api_call(
265
+ messages=cast(List[BetaMessageParam], anthropic_messages),
266
+ system_prompt=system_content or SYSTEM_PROMPT,
267
+ )
268
+
269
+ # Use response handler to handle the response and get new messages
270
+ new_messages, should_continue = await self.response_handler.handle_response(
271
+ response, messages
272
+ )
267
273
 
268
- # Handle the response
269
- if not await self._handle_response(response, self.message_history):
274
+ # Add new messages to the parent's message history
275
+ messages.extend(new_messages)
276
+
277
+ openai_compatible_response = await to_agent_response_format(
278
+ response,
279
+ messages,
280
+ model=self.model,
281
+ )
282
+ await queue.put(openai_compatible_response)
283
+
284
+ if not should_continue:
270
285
  break
271
286
 
272
287
  # Signal completion
@@ -283,142 +298,101 @@ class AnthropicLoop(BaseLoop):
283
298
  )
284
299
  await queue.put(None)
285
300
 
286
- async def _make_api_call(self, messages: List[BetaMessageParam]) -> BetaMessage:
287
- """Make API call to Anthropic with retry logic.
288
-
289
- Args:
290
- messages: List of messages to send to the API
291
-
292
- Returns:
293
- API response
294
- """
295
- if self.client is None:
296
- raise RuntimeError("Client not initialized. Call initialize_client() first.")
297
- if self.tool_manager is None:
298
- raise RuntimeError("Tool manager not initialized. Call initialize_client() first.")
299
-
300
- last_error = None
301
-
302
- for attempt in range(self.max_retries):
303
- try:
304
- # Log request
305
- request_data = {
306
- "messages": messages,
307
- "max_tokens": self.max_tokens,
308
- "system": SYSTEM_PROMPT,
309
- }
310
- # Let ExperimentManager handle sanitization
311
- self._log_api_call("request", request_data)
312
-
313
- # Setup betas and system
314
- system = BetaTextBlockParam(
315
- type="text",
316
- text=SYSTEM_PROMPT,
317
- )
318
-
319
- betas = [COMPUTER_USE_BETA_FLAG]
320
- # Temporarily disable prompt caching due to "A maximum of 4 blocks with cache_control may be provided" error
321
- # if self.message_manager.image_retention_config.enable_caching:
322
- # betas.append(PROMPT_CACHING_BETA_FLAG)
323
- # system["cache_control"] = {"type": "ephemeral"}
324
-
325
- # Make API call
326
- response = await self.client.create_message(
327
- messages=messages,
328
- system=[system],
329
- tools=self.tool_manager.get_tool_params(),
330
- max_tokens=self.max_tokens,
331
- betas=betas,
332
- )
333
-
334
- # Let ExperimentManager handle sanitization
335
- self._log_api_call("response", request_data, response)
336
-
337
- return response
338
- except Exception as e:
339
- last_error = e
340
- logger.error(
341
- f"Error in API call (attempt {attempt + 1}/{self.max_retries}): {str(e)}"
342
- )
343
- self._log_api_call("error", {"messages": messages}, error=e)
344
-
345
- if attempt < self.max_retries - 1:
346
- await asyncio.sleep(self.retry_delay * (attempt + 1)) # Exponential backoff
347
- continue
348
-
349
- # If we get here, all retries failed
350
- error_message = f"API call failed after {self.max_retries} attempts"
351
- if last_error:
352
- error_message += f": {str(last_error)}"
353
-
354
- logger.error(error_message)
355
- raise RuntimeError(error_message)
301
+ ###########################################
302
+ # RESPONSE AND CALLBACK HANDLING
303
+ ###########################################
356
304
 
357
305
  async def _handle_response(self, response: BetaMessage, messages: List[Dict[str, Any]]) -> bool:
358
- """Handle the Anthropic API response.
306
+ """Handle a response from the Anthropic API.
359
307
 
360
308
  Args:
361
- response: API response
362
- messages: List of messages to update
309
+ response: The response from the Anthropic API
310
+ messages: The message history
363
311
 
364
312
  Returns:
365
- True if the loop should continue, False otherwise
313
+ bool: Whether to continue the conversation
366
314
  """
367
315
  try:
368
- # Convert response to parameter format
369
- response_params = self._response_to_params(response)
370
-
371
- # Add response to messages
372
- messages.append(
373
- {
374
- "role": "assistant",
375
- "content": response_params,
376
- }
316
+ # Convert response to standard format
317
+ openai_compatible_response = await to_agent_response_format(
318
+ response,
319
+ messages,
320
+ model=self.model,
377
321
  )
378
322
 
323
+ # Put the response on the queue
324
+ await self.queue.put(openai_compatible_response)
325
+
379
326
  if self.callback_manager is None:
380
327
  raise RuntimeError(
381
328
  "Callback manager not initialized. Call initialize_client() first."
382
329
  )
383
330
 
384
- # Handle tool use blocks and collect results
331
+ # Handle tool use blocks and collect ALL results before adding to messages
385
332
  tool_result_content = []
386
- for content_block in response_params:
333
+ has_tool_use = False
334
+
335
+ for content_block in response.content:
387
336
  # Notify callback of content
388
337
  self.callback_manager.on_content(cast(BetaContentBlockParam, content_block))
389
338
 
390
- # Handle tool use
391
- if content_block.get("type") == "tool_use":
339
+ # Handle tool use - carefully check and access attributes
340
+ if hasattr(content_block, "type") and content_block.type == "tool_use":
341
+ has_tool_use = True
392
342
  if self.tool_manager is None:
393
343
  raise RuntimeError(
394
344
  "Tool manager not initialized. Call initialize_client() first."
395
345
  )
346
+
347
+ # Safely get attributes
348
+ tool_name = getattr(content_block, "name", "")
349
+ tool_input = getattr(content_block, "input", {})
350
+ tool_id = getattr(content_block, "id", "")
351
+
396
352
  result = await self.tool_manager.execute_tool(
397
- name=content_block["name"],
398
- tool_input=cast(Dict[str, Any], content_block["input"]),
353
+ name=tool_name,
354
+ tool_input=cast(Dict[str, Any], tool_input),
399
355
  )
400
356
 
401
- # Create tool result and add to content
402
- tool_result = self._make_tool_result(
403
- cast(ToolResult, result), content_block["id"]
404
- )
357
+ # Create tool result
358
+ tool_result = self._make_tool_result(cast(ToolResult, result), tool_id)
405
359
  tool_result_content.append(tool_result)
406
360
 
407
361
  # Notify callback of tool result
408
- self.callback_manager.on_tool_result(
409
- cast(ToolResult, result), content_block["id"]
362
+ self.callback_manager.on_tool_result(cast(ToolResult, result), tool_id)
363
+
364
+ # If we had any tool_use blocks, we MUST add the tool_result message
365
+ # even if there were errors or no actual results
366
+ if has_tool_use:
367
+ # If somehow we have no tool results but had tool uses, add synthetic error results
368
+ if not tool_result_content:
369
+ logger.warning(
370
+ "Had tool uses but no tool results, adding synthetic error results"
410
371
  )
411
-
412
- # If no tool results, we're done
413
- if not tool_result_content:
414
- # Signal completion
372
+ for content_block in response.content:
373
+ if hasattr(content_block, "type") and content_block.type == "tool_use":
374
+ tool_id = getattr(content_block, "id", "")
375
+ if tool_id:
376
+ tool_result_content.append(
377
+ {
378
+ "type": "tool_result",
379
+ "tool_use_id": tool_id,
380
+ "content": {
381
+ "type": "error",
382
+ "text": "Tool execution was skipped or failed",
383
+ },
384
+ "is_error": True,
385
+ }
386
+ )
387
+
388
+ # Add ALL tool results as a SINGLE user message
389
+ messages.append({"role": "user", "content": tool_result_content})
390
+ return True
391
+ else:
392
+ # No tool uses, we're done
415
393
  self.callback_manager.on_content({"type": "text", "text": "<DONE>"})
416
394
  return False
417
395
 
418
- # Add tool results to message history
419
- messages.append({"content": tool_result_content, "role": "user"})
420
- return True
421
-
422
396
  except Exception as e:
423
397
  logger.error(f"Error handling response: {str(e)}")
424
398
  messages.append(
@@ -429,28 +403,41 @@ class AnthropicLoop(BaseLoop):
429
403
  )
430
404
  return False
431
405
 
432
- def _response_to_params(
433
- self,
434
- response: BetaMessage,
435
- ) -> List[Dict[str, Any]]:
436
- """Convert API response to message parameters.
406
+ def _response_to_blocks(self, response: BetaMessage) -> List[Dict[str, Any]]:
407
+ """Convert Anthropic API response to standard blocks format.
437
408
 
438
409
  Args:
439
410
  response: API response message
440
411
 
441
412
  Returns:
442
- List of content blocks
413
+ List of content blocks in standard format
443
414
  """
444
415
  result = []
445
416
  for block in response.content:
446
417
  if isinstance(block, BetaTextBlock):
447
418
  result.append({"type": "text", "text": block.text})
419
+ elif hasattr(block, "type") and block.type == "tool_use":
420
+ # Safely access attributes after confirming it's a tool_use
421
+ result.append(
422
+ {
423
+ "type": "tool_use",
424
+ "id": getattr(block, "id", ""),
425
+ "name": getattr(block, "name", ""),
426
+ "input": getattr(block, "input", {}),
427
+ }
428
+ )
448
429
  else:
449
- result.append(cast(Dict[str, Any], block.model_dump()))
430
+ # For other block types, convert to dict
431
+ block_dict = {}
432
+ for key, value in vars(block).items():
433
+ if not key.startswith("_"):
434
+ block_dict[key] = value
435
+ result.append(block_dict)
436
+
450
437
  return result
451
438
 
452
439
  def _make_tool_result(self, result: ToolResult, tool_use_id: str) -> Dict[str, Any]:
453
- """Convert a tool result to API format.
440
+ """Convert a tool result to standard format.
454
441
 
455
442
  Args:
456
443
  result: Tool execution result
@@ -489,12 +476,8 @@ class AnthropicLoop(BaseLoop):
489
476
  if result.base64_image:
490
477
  tool_result_content.append(
491
478
  {
492
- "type": "image",
493
- "source": {
494
- "type": "base64",
495
- "media_type": "image/png",
496
- "data": result.base64_image,
497
- },
479
+ "type": "image_url",
480
+ "image_url": {"url": f"data:image/png;base64,{result.base64_image}"},
498
481
  }
499
482
  )
500
483
 
@@ -519,16 +502,19 @@ class AnthropicLoop(BaseLoop):
519
502
  result_text = f"<s>{result.system}</s>\n{result_text}"
520
503
  return result_text
521
504
 
522
- def _handle_content(self, content: BetaContentBlockParam) -> None:
505
+ ###########################################
506
+ # CALLBACK HANDLERS
507
+ ###########################################
508
+
509
+ def _handle_content(self, content):
523
510
  """Handle content updates from the assistant."""
524
511
  if content.get("type") == "text":
525
- text_content = cast(BetaTextBlockParam, content)
526
- text = text_content["text"]
512
+ text = content.get("text", "")
527
513
  if text == "<DONE>":
528
514
  return
529
515
  logger.info(f"Assistant: {text}")
530
516
 
531
- def _handle_tool_result(self, result: ToolResult, tool_id: str) -> None:
517
+ def _handle_tool_result(self, result, tool_id):
532
518
  """Handle tool execution results."""
533
519
  if result.error:
534
520
  logger.error(f"Tool {tool_id} error: {result.error}")