cua-agent 0.1.5__py3-none-any.whl → 0.1.17__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 (52) hide show
  1. agent/__init__.py +3 -4
  2. agent/core/__init__.py +3 -10
  3. agent/core/computer_agent.py +207 -32
  4. agent/core/experiment.py +20 -3
  5. agent/core/loop.py +78 -120
  6. agent/core/messages.py +279 -125
  7. agent/core/telemetry.py +44 -32
  8. agent/core/types.py +35 -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 +224 -209
  14. agent/providers/anthropic/messages/manager.py +3 -1
  15. agent/providers/anthropic/response_handler.py +229 -0
  16. agent/providers/anthropic/tools/base.py +1 -1
  17. agent/providers/anthropic/tools/bash.py +0 -97
  18. agent/providers/anthropic/tools/collection.py +2 -2
  19. agent/providers/anthropic/tools/computer.py +34 -24
  20. agent/providers/anthropic/tools/manager.py +2 -2
  21. agent/providers/anthropic/utils.py +370 -0
  22. agent/providers/omni/__init__.py +1 -20
  23. agent/providers/omni/api_handler.py +42 -0
  24. agent/providers/omni/clients/anthropic.py +4 -0
  25. agent/providers/omni/image_utils.py +0 -72
  26. agent/providers/omni/loop.py +497 -607
  27. agent/providers/omni/parser.py +60 -5
  28. agent/providers/omni/tools/__init__.py +25 -8
  29. agent/providers/omni/tools/base.py +29 -0
  30. agent/providers/omni/tools/bash.py +43 -38
  31. agent/providers/omni/tools/computer.py +144 -181
  32. agent/providers/omni/tools/manager.py +26 -48
  33. agent/providers/omni/types.py +0 -4
  34. agent/providers/omni/utils.py +225 -144
  35. {cua_agent-0.1.5.dist-info → cua_agent-0.1.17.dist-info}/METADATA +6 -36
  36. cua_agent-0.1.17.dist-info/RECORD +63 -0
  37. agent/core/agent.py +0 -252
  38. agent/core/base_agent.py +0 -164
  39. agent/core/factory.py +0 -102
  40. agent/providers/omni/callbacks.py +0 -78
  41. agent/providers/omni/clients/groq.py +0 -101
  42. agent/providers/omni/experiment.py +0 -273
  43. agent/providers/omni/messages.py +0 -171
  44. agent/providers/omni/tool_manager.py +0 -91
  45. agent/providers/omni/visualization.py +0 -130
  46. agent/types/__init__.py +0 -26
  47. agent/types/base.py +0 -53
  48. agent/types/messages.py +0 -36
  49. cua_agent-0.1.5.dist-info/RECORD +0 -67
  50. /agent/{types → core}/tools.py +0 -0
  51. {cua_agent-0.1.5.dist-info → cua_agent-0.1.17.dist-info}/WHEEL +0 -0
  52. {cua_agent-0.1.5.dist-info → cua_agent-0.1.17.dist-info}/entry_points.txt +0 -0
@@ -2,38 +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,
10
+ BetaContentBlockParam,
20
11
  )
12
+ import base64
13
+ from datetime import datetime
21
14
 
22
15
  # Computer
23
16
  from computer import Computer
24
17
 
25
18
  # Base imports
26
19
  from ...core.loop import BaseLoop
27
- from ...core.messages import ImageRetentionConfig
20
+ from ...core.messages import StandardMessageManager, ImageRetentionConfig
21
+ from ...core.types import AgentResponse
28
22
 
29
23
  # Anthropic provider-specific imports
30
24
  from .api.client import AnthropicClientFactory, BaseAnthropicClient
31
25
  from .tools.manager import ToolManager
32
- from .messages.manager import MessageManager
33
- from .callbacks.manager import CallbackManager
34
26
  from .prompts import SYSTEM_PROMPT
35
27
  from .types import LLMProvider
36
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
37
35
 
38
36
  # Constants
39
37
  COMPUTER_USE_BETA_FLAG = "computer-use-2025-01-24"
@@ -43,13 +41,22 @@ logger = logging.getLogger(__name__)
43
41
 
44
42
 
45
43
  class AnthropicLoop(BaseLoop):
46
- """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
+ ###########################################
47
54
 
48
55
  def __init__(
49
56
  self,
50
57
  api_key: str,
51
- model: str = "claude-3-7-sonnet-20250219", # Fixed model
52
- computer: Optional[Computer] = None,
58
+ computer: Computer,
59
+ model: str = "claude-3-7-sonnet-20250219",
53
60
  only_n_most_recent_images: Optional[int] = 2,
54
61
  base_dir: Optional[str] = "trajectories",
55
62
  max_retries: int = 3,
@@ -69,7 +76,7 @@ class AnthropicLoop(BaseLoop):
69
76
  retry_delay: Delay between retries in seconds
70
77
  save_trajectory: Whether to save trajectory data
71
78
  """
72
- # Initialize base class
79
+ # Initialize base class with core config
73
80
  super().__init__(
74
81
  computer=computer,
75
82
  model=model,
@@ -82,27 +89,33 @@ class AnthropicLoop(BaseLoop):
82
89
  **kwargs,
83
90
  )
84
91
 
85
- # Ensure model is always the fixed one
86
- 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
+ )
87
96
 
88
97
  # Anthropic-specific attributes
89
98
  self.provider = LLMProvider.ANTHROPIC
90
99
  self.client = None
91
100
  self.retry_count = 0
92
101
  self.tool_manager = None
93
- self.message_manager = None
94
102
  self.callback_manager = None
103
+ self.queue = asyncio.Queue() # Initialize queue
95
104
 
96
- # Configure image retention
97
- self.image_retention_config = ImageRetentionConfig(
98
- num_images_to_keep=only_n_most_recent_images
99
- )
105
+ # Initialize handlers
106
+ self.api_handler = AnthropicAPIHandler(self)
107
+ self.response_handler = AnthropicResponseHandler(self)
100
108
 
101
- # Message history
102
- self.message_history = []
109
+ ###########################################
110
+ # CLIENT INITIALIZATION - IMPLEMENTING ABSTRACT METHOD
111
+ ###########################################
103
112
 
104
113
  async def initialize_client(self) -> None:
105
- """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
+ """
106
119
  try:
107
120
  logger.info(f"Initializing Anthropic client with model {self.model}...")
108
121
 
@@ -111,14 +124,7 @@ class AnthropicLoop(BaseLoop):
111
124
  provider=self.provider, api_key=self.api_key, model=self.model
112
125
  )
113
126
 
114
- # Initialize message manager
115
- self.message_manager = MessageManager(
116
- ImageRetentionConfig(
117
- num_images_to_keep=self.only_n_most_recent_images, enable_caching=True
118
- )
119
- )
120
-
121
- # Initialize callback manager
127
+ # Initialize callback manager with our callback handlers
122
128
  self.callback_manager = CallbackManager(
123
129
  content_callback=self._handle_content,
124
130
  tool_callback=self._handle_tool_result,
@@ -135,62 +141,22 @@ class AnthropicLoop(BaseLoop):
135
141
  self.client = None
136
142
  raise RuntimeError(f"Failed to initialize Anthropic client: {str(e)}")
137
143
 
138
- async def _process_screen(
139
- self, parsed_screen: Dict[str, Any], messages: List[Dict[str, Any]]
140
- ) -> None:
141
- """Process screen information and add to messages.
142
-
143
- Args:
144
- parsed_screen: Dictionary containing parsed screen info
145
- messages: List of messages to update
146
- """
147
- try:
148
- # Extract screenshot from parsed screen
149
- screenshot_base64 = parsed_screen.get("screenshot_base64")
150
-
151
- if screenshot_base64:
152
- # Remove data URL prefix if present
153
- if "," in screenshot_base64:
154
- screenshot_base64 = screenshot_base64.split(",")[1]
155
-
156
- # Create Anthropic-compatible message with image
157
- screen_info_msg = {
158
- "role": "user",
159
- "content": [
160
- {
161
- "type": "image",
162
- "source": {
163
- "type": "base64",
164
- "media_type": "image/png",
165
- "data": screenshot_base64,
166
- },
167
- }
168
- ],
169
- }
170
-
171
- # Add screen info message to messages
172
- messages.append(screen_info_msg)
173
-
174
- except Exception as e:
175
- logger.error(f"Error processing screen info: {str(e)}")
176
- raise
144
+ ###########################################
145
+ # MAIN LOOP - IMPLEMENTING ABSTRACT METHOD
146
+ ###########################################
177
147
 
178
- 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]:
179
149
  """Run the agent loop with provided messages.
180
150
 
181
151
  Args:
182
- messages: List of message objects
152
+ messages: List of message objects in standard OpenAI format
183
153
 
184
154
  Yields:
185
- Dict containing response data
155
+ Agent response format
186
156
  """
187
157
  try:
188
158
  logger.info("Starting Anthropic loop run")
189
159
 
190
- # Reset message history and add new messages
191
- self.message_history = []
192
- self.message_history.extend(messages)
193
-
194
160
  # Create queue for response streaming
195
161
  queue = asyncio.Queue()
196
162
 
@@ -203,7 +169,7 @@ class AnthropicLoop(BaseLoop):
203
169
  logger.info("Client initialized successfully")
204
170
 
205
171
  # Start loop in background task
206
- loop_task = asyncio.create_task(self._run_loop(queue))
172
+ loop_task = asyncio.create_task(self._run_loop(queue, messages))
207
173
 
208
174
  # Process and yield messages as they arrive
209
175
  while True:
@@ -235,33 +201,87 @@ class AnthropicLoop(BaseLoop):
235
201
  "metadata": {"title": "❌ Error"},
236
202
  }
237
203
 
238
- async def _run_loop(self, queue: asyncio.Queue) -> None:
239
- """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.
240
210
 
241
211
  Args:
242
212
  queue: Queue for response streaming
213
+ messages: List of messages in standard OpenAI format
243
214
  """
244
215
  try:
245
216
  while True:
246
- # Get up-to-date screen information
247
- parsed_screen = await self._get_parsed_screen_som()
248
-
249
- # Process screen info and update messages
250
- 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")
251
252
 
252
- # Prepare messages and make API call
253
- prepared_messages = self.message_manager.prepare_messages(
254
- cast(List[BetaMessageParam], self.message_history.copy())
255
- )
253
+ except Exception as e:
254
+ logger.error(f"Error capturing or processing screenshot: {str(e)}")
255
+ raise
256
256
 
257
257
  # Create new turn directory for this API call
258
258
  self._create_turn_dir()
259
259
 
260
- # Make API call
261
- 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
+ )
273
+
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)
262
283
 
263
- # Handle the response
264
- if not await self._handle_response(response, self.message_history):
284
+ if not should_continue:
265
285
  break
266
286
 
267
287
  # Signal completion
@@ -278,123 +298,101 @@ class AnthropicLoop(BaseLoop):
278
298
  )
279
299
  await queue.put(None)
280
300
 
281
- async def _make_api_call(self, messages: List[BetaMessageParam]) -> BetaMessage:
282
- """Make API call to Anthropic with retry logic.
283
-
284
- Args:
285
- messages: List of messages to send to the API
286
-
287
- Returns:
288
- API response
289
- """
290
- last_error = None
291
-
292
- for attempt in range(self.max_retries):
293
- try:
294
- # Log request
295
- request_data = {
296
- "messages": messages,
297
- "max_tokens": self.max_tokens,
298
- "system": SYSTEM_PROMPT,
299
- }
300
- self._log_api_call("request", request_data)
301
-
302
- # Setup betas and system
303
- system = BetaTextBlockParam(
304
- type="text",
305
- text=SYSTEM_PROMPT,
306
- )
307
-
308
- betas = [COMPUTER_USE_BETA_FLAG]
309
- # Temporarily disable prompt caching due to "A maximum of 4 blocks with cache_control may be provided" error
310
- # if self.message_manager.image_retention_config.enable_caching:
311
- # betas.append(PROMPT_CACHING_BETA_FLAG)
312
- # system["cache_control"] = {"type": "ephemeral"}
313
-
314
- # Make API call
315
- response = await self.client.create_message(
316
- messages=messages,
317
- system=[system],
318
- tools=self.tool_manager.get_tool_params(),
319
- max_tokens=self.max_tokens,
320
- betas=betas,
321
- )
322
-
323
- # Log success response
324
- self._log_api_call("response", request_data, response)
325
-
326
- return response
327
- except Exception as e:
328
- last_error = e
329
- logger.error(
330
- f"Error in API call (attempt {attempt + 1}/{self.max_retries}): {str(e)}"
331
- )
332
- self._log_api_call("error", {"messages": messages}, error=e)
333
-
334
- if attempt < self.max_retries - 1:
335
- await asyncio.sleep(self.retry_delay * (attempt + 1)) # Exponential backoff
336
- continue
337
-
338
- # If we get here, all retries failed
339
- error_message = f"API call failed after {self.max_retries} attempts"
340
- if last_error:
341
- error_message += f": {str(last_error)}"
342
-
343
- logger.error(error_message)
344
- raise RuntimeError(error_message)
301
+ ###########################################
302
+ # RESPONSE AND CALLBACK HANDLING
303
+ ###########################################
345
304
 
346
305
  async def _handle_response(self, response: BetaMessage, messages: List[Dict[str, Any]]) -> bool:
347
- """Handle the Anthropic API response.
306
+ """Handle a response from the Anthropic API.
348
307
 
349
308
  Args:
350
- response: API response
351
- messages: List of messages to update
309
+ response: The response from the Anthropic API
310
+ messages: The message history
352
311
 
353
312
  Returns:
354
- True if the loop should continue, False otherwise
313
+ bool: Whether to continue the conversation
355
314
  """
356
315
  try:
357
- # Convert response to parameter format
358
- response_params = self._response_to_params(response)
359
-
360
- # Add response to messages
361
- messages.append(
362
- {
363
- "role": "assistant",
364
- "content": response_params,
365
- }
316
+ # Convert response to standard format
317
+ openai_compatible_response = await to_agent_response_format(
318
+ response,
319
+ messages,
320
+ model=self.model,
366
321
  )
367
322
 
368
- # Handle tool use blocks and collect results
323
+ # Put the response on the queue
324
+ await self.queue.put(openai_compatible_response)
325
+
326
+ if self.callback_manager is None:
327
+ raise RuntimeError(
328
+ "Callback manager not initialized. Call initialize_client() first."
329
+ )
330
+
331
+ # Handle tool use blocks and collect ALL results before adding to messages
369
332
  tool_result_content = []
370
- for content_block in response_params:
333
+ has_tool_use = False
334
+
335
+ for content_block in response.content:
371
336
  # Notify callback of content
372
- self.callback_manager.on_content(content_block)
337
+ self.callback_manager.on_content(cast(BetaContentBlockParam, content_block))
338
+
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
342
+ if self.tool_manager is None:
343
+ raise RuntimeError(
344
+ "Tool manager not initialized. Call initialize_client() first."
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", "")
373
351
 
374
- # Handle tool use
375
- if content_block.get("type") == "tool_use":
376
352
  result = await self.tool_manager.execute_tool(
377
- name=content_block["name"],
378
- tool_input=cast(Dict[str, Any], content_block["input"]),
353
+ name=tool_name,
354
+ tool_input=cast(Dict[str, Any], tool_input),
379
355
  )
380
356
 
381
- # Create tool result and add to content
382
- tool_result = self._make_tool_result(result, content_block["id"])
357
+ # Create tool result
358
+ tool_result = self._make_tool_result(cast(ToolResult, result), tool_id)
383
359
  tool_result_content.append(tool_result)
384
360
 
385
361
  # Notify callback of tool result
386
- self.callback_manager.on_tool_result(result, content_block["id"])
387
-
388
- # If no tool results, we're done
389
- if not tool_result_content:
390
- # Signal completion
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"
371
+ )
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
391
393
  self.callback_manager.on_content({"type": "text", "text": "<DONE>"})
392
394
  return False
393
395
 
394
- # Add tool results to message history
395
- messages.append({"content": tool_result_content, "role": "user"})
396
- return True
397
-
398
396
  except Exception as e:
399
397
  logger.error(f"Error handling response: {str(e)}")
400
398
  messages.append(
@@ -405,28 +403,41 @@ class AnthropicLoop(BaseLoop):
405
403
  )
406
404
  return False
407
405
 
408
- def _response_to_params(
409
- self,
410
- response: BetaMessage,
411
- ) -> List[Dict[str, Any]]:
412
- """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.
413
408
 
414
409
  Args:
415
410
  response: API response message
416
411
 
417
412
  Returns:
418
- List of content blocks
413
+ List of content blocks in standard format
419
414
  """
420
415
  result = []
421
416
  for block in response.content:
422
417
  if isinstance(block, BetaTextBlock):
423
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
+ )
424
429
  else:
425
- 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
+
426
437
  return result
427
438
 
428
439
  def _make_tool_result(self, result: ToolResult, tool_use_id: str) -> Dict[str, Any]:
429
- """Convert a tool result to API format.
440
+ """Convert a tool result to standard format.
430
441
 
431
442
  Args:
432
443
  result: Tool execution result
@@ -465,12 +476,8 @@ class AnthropicLoop(BaseLoop):
465
476
  if result.base64_image:
466
477
  tool_result_content.append(
467
478
  {
468
- "type": "image",
469
- "source": {
470
- "type": "base64",
471
- "media_type": "image/png",
472
- "data": result.base64_image,
473
- },
479
+ "type": "image_url",
480
+ "image_url": {"url": f"data:image/png;base64,{result.base64_image}"},
474
481
  }
475
482
  )
476
483
 
@@ -495,16 +502,19 @@ class AnthropicLoop(BaseLoop):
495
502
  result_text = f"<s>{result.system}</s>\n{result_text}"
496
503
  return result_text
497
504
 
498
- def _handle_content(self, content: Dict[str, Any]) -> None:
505
+ ###########################################
506
+ # CALLBACK HANDLERS
507
+ ###########################################
508
+
509
+ def _handle_content(self, content):
499
510
  """Handle content updates from the assistant."""
500
511
  if content.get("type") == "text":
501
512
  text = content.get("text", "")
502
513
  if text == "<DONE>":
503
514
  return
504
-
505
515
  logger.info(f"Assistant: {text}")
506
516
 
507
- def _handle_tool_result(self, result: ToolResult, tool_id: str) -> None:
517
+ def _handle_tool_result(self, result, tool_id):
508
518
  """Handle tool execution results."""
509
519
  if result.error:
510
520
  logger.error(f"Tool {tool_id} error: {result.error}")
@@ -517,5 +527,10 @@ class AnthropicLoop(BaseLoop):
517
527
  """Handle API interactions."""
518
528
  if error:
519
529
  logger.error(f"API error: {error}")
530
+ self._log_api_call("error", request, error=error)
520
531
  else:
521
532
  logger.debug(f"API request: {request}")
533
+ if response:
534
+ self._log_api_call("response", request, response)
535
+ else:
536
+ self._log_api_call("request", request)
@@ -90,7 +90,9 @@ class MessageManager:
90
90
  blocks_with_cache_control += 1
91
91
  # Add cache control to the last content block only
92
92
  if content and len(content) > 0:
93
- content[-1]["cache_control"] = {"type": "ephemeral"}
93
+ content[-1]["cache_control"] = BetaCacheControlEphemeralParam(
94
+ type="ephemeral"
95
+ )
94
96
  else:
95
97
  # Remove any existing cache control
96
98
  if content and len(content) > 0: