hud-python 0.2.4__py3-none-any.whl → 0.2.5__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 hud-python might be problematic. Click here for more details.

Files changed (50) hide show
  1. hud/__init__.py +22 -2
  2. hud/adapters/claude/adapter.py +9 -2
  3. hud/adapters/claude/tests/__init__.py +1 -0
  4. hud/adapters/claude/tests/test_adapter.py +519 -0
  5. hud/adapters/common/types.py +5 -1
  6. hud/adapters/operator/adapter.py +4 -0
  7. hud/adapters/operator/tests/__init__.py +1 -0
  8. hud/adapters/operator/tests/test_adapter.py +370 -0
  9. hud/agent/__init__.py +4 -0
  10. hud/agent/base.py +18 -2
  11. hud/agent/claude.py +20 -17
  12. hud/agent/claude_plays_pokemon.py +282 -0
  13. hud/agent/langchain.py +12 -7
  14. hud/agent/misc/__init__.py +3 -0
  15. hud/agent/misc/response_agent.py +80 -0
  16. hud/agent/operator.py +27 -19
  17. hud/agent/tests/__init__.py +1 -0
  18. hud/agent/tests/test_base.py +202 -0
  19. hud/env/docker_client.py +28 -18
  20. hud/env/environment.py +32 -16
  21. hud/env/local_docker_client.py +83 -42
  22. hud/env/remote_client.py +1 -3
  23. hud/env/remote_docker_client.py +72 -15
  24. hud/exceptions.py +12 -0
  25. hud/gym.py +71 -53
  26. hud/job.py +52 -7
  27. hud/settings.py +6 -0
  28. hud/task.py +45 -33
  29. hud/taskset.py +44 -4
  30. hud/telemetry/__init__.py +21 -0
  31. hud/telemetry/_trace.py +173 -0
  32. hud/telemetry/context.py +193 -0
  33. hud/telemetry/exporter.py +417 -0
  34. hud/telemetry/instrumentation/__init__.py +3 -0
  35. hud/telemetry/instrumentation/mcp.py +498 -0
  36. hud/telemetry/instrumentation/registry.py +59 -0
  37. hud/telemetry/mcp_models.py +331 -0
  38. hud/telemetry/tests/__init__.py +1 -0
  39. hud/telemetry/tests/test_context.py +203 -0
  40. hud/telemetry/tests/test_trace.py +270 -0
  41. hud/types.py +10 -26
  42. hud/utils/common.py +22 -2
  43. hud/utils/misc.py +53 -0
  44. hud/utils/tests/test_version.py +1 -1
  45. hud/version.py +7 -0
  46. {hud_python-0.2.4.dist-info → hud_python-0.2.5.dist-info}/METADATA +90 -22
  47. hud_python-0.2.5.dist-info/RECORD +84 -0
  48. hud_python-0.2.4.dist-info/RECORD +0 -62
  49. {hud_python-0.2.4.dist-info → hud_python-0.2.5.dist-info}/WHEEL +0 -0
  50. {hud_python-0.2.4.dist-info → hud_python-0.2.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,282 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any, cast
6
+
7
+ from anthropic import AsyncAnthropic
8
+ from anthropic.types.beta import (
9
+ BetaMessageParam,
10
+ BetaTextBlockParam,
11
+ BetaImageBlockParam,
12
+ )
13
+
14
+ from hud.agent import Agent
15
+ from hud.adapters import Adapter
16
+ from hud.settings import settings
17
+ from hud.env.environment import Observation
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Constants
22
+ DEFAULT_MODEL = "claude-3-7-sonnet-20250219"
23
+ DEFAULT_MAX_TOKENS = 4096
24
+ DEFAULT_MAX_ITERATIONS = 10
25
+ DEFAULT_TEMPERATURE = 0.7
26
+ DEFAULT_MAX_MESSAGE_MEMORY = 20
27
+
28
+
29
+ def generate_system_prompt(game_name: str) -> str:
30
+ """Generate the system prompt for the AI agent.
31
+
32
+ Args:
33
+ game_name: Name of the game being played
34
+
35
+ Returns:
36
+ str: The system prompt for the AI agent
37
+ """
38
+ return """You are a specialized AI assistant designed to play Pokémon games via screenshot analysis and text instructions. Your task is to understand the current game state from visual input, determine appropriate actions, and respond with structured outputs that control the game.
39
+
40
+ For each turn, you will receive:
41
+ 1. A screenshot of the current game state
42
+ 2. Contextual information about the game progress, recent events, and objectives
43
+
44
+ Based on this information, you must analyze the situation, determine the best course of action, and provide a structured JSON response.
45
+
46
+ ## Response Format
47
+ Your response MUST follow this exact JSON format with no additional markers, tags, or block delimiters:
48
+
49
+ {
50
+ "analysis": "Brief analysis of the current game situation, visible UI elements, and important context (1-3 sentences)",
51
+ "current_objective": "The immediate goal based on the game state (single sentence)",
52
+ "reasoning": "Step-by-step logic explaining your chosen action sequence (2-4 sentences)",
53
+ "progress_assessment": "Evaluation of whether previous action(s) achieved their intended goal and why/why not (1-2 sentences)",
54
+ "actions": [
55
+ {
56
+ "type": "press",
57
+ "keys": ["up"|"down"|"left"|"right"|"a"|"b"|"start"|"select"|"pause"]
58
+ },
59
+ {
60
+ "type": "wait",
61
+ "time": milliseconds_to_wait
62
+ }
63
+ ]
64
+ }
65
+
66
+ IMPORTANT: Do not include any conversation markers like <<ASSISTANT_CONVERSATION_START>> or <<ASSISTANT_CONVERSATION_END>> around your response. Provide only the clean JSON object.
67
+
68
+ ## Action Types
69
+ - Button presses: {"type": "press", "keys": ["button_name"]} - Valid buttons are: up, down, left, right, a, b, start, select, pause
70
+ - Wait for processing: {"type": "wait", "time": milliseconds}
71
+
72
+ ## Important Rules
73
+ 1. Never use "wait" commands while the game is paused. The game state will not change while paused, so waiting is ineffective.
74
+ 2. If you detect the game is paused, your next action should be to unpause by using {"type": "press", "keys": ["pause"]} before attempting other actions.
75
+ 3. Maintain awareness of whether the game is in a paused state based on visual cues in the screenshot.
76
+
77
+ ## Game Play Guidelines
78
+ 1. **Navigation**: Use directional buttons to move the character or navigate menus
79
+ 2. **Interaction**: Use 'a' to confirm selections and interact with objects/NPCs, 'b' to cancel or exit menus
80
+ 3. **Menu Access**: Use 'start' to access the game menu
81
+ 4. **Battle Strategy**: Analyze Pokémon types, moves, and stats to make optimal battle decisions
82
+ 5. **Progressive Play**: Work toward completing the current objective while being mindful of longer-term goals like leveling Pokémon, collecting badges, and advancing the story
83
+ 6. **Resource Management**: Monitor and manage HP, PP, items, and Pokéballs effectively
84
+ 7. **Memory**: Maintain awareness of the game history and your previous actions to avoid repetitive behaviors
85
+
86
+ Always provide thoughtful analysis and clear reasoning for your decisions. If you're uncertain about the best course of action, prioritize safe moves that gather more information.
87
+ """
88
+
89
+
90
+ def extract_action_from_response_block(block: dict[str, Any]) -> list[dict[str, Any]]:
91
+ """Extract actions from a response block.
92
+
93
+ Args:
94
+ block: The response block containing actions
95
+
96
+ Returns:
97
+ list[dict[str, Any]]: List of actions extracted from the block
98
+ """
99
+ if "actions" in block:
100
+ actions = block["actions"]
101
+ if isinstance(actions, list):
102
+ return actions
103
+ return []
104
+
105
+
106
+ def extract_json_from_response(response: str) -> str:
107
+ """Extract JSON from a response string.
108
+
109
+ Args:
110
+ response: The response string containing JSON
111
+
112
+ Returns:
113
+ str: The extracted JSON string
114
+ """
115
+ # Try to find JSON block with markdown code block markers
116
+ start = response.find("```json")
117
+ end = response.rfind("```")
118
+ if start != -1 and end != -1:
119
+ start += len("```json")
120
+ return response[start:end].strip()
121
+
122
+ # Try to find JSON object directly
123
+ start = response.find("{")
124
+ end = response.rfind("}")
125
+ if start != -1 and end != -1:
126
+ return response[start : end + 1].strip()
127
+
128
+ return response.strip()
129
+
130
+
131
+ class ClaudePlaysPokemon(Agent[AsyncAnthropic, None]):
132
+ """AI agent that plays Pokémon games using Claude."""
133
+
134
+ def __init__(
135
+ self,
136
+ client: AsyncAnthropic | None = None,
137
+ adapter: Adapter | None = None,
138
+ model: str = DEFAULT_MODEL,
139
+ max_tokens: int = DEFAULT_MAX_TOKENS,
140
+ max_iterations: int = DEFAULT_MAX_ITERATIONS,
141
+ temperature: float = DEFAULT_TEMPERATURE,
142
+ max_message_memory: int = DEFAULT_MAX_MESSAGE_MEMORY,
143
+ ) -> None:
144
+ """Initialize the Claude Plays Pokémon agent.
145
+
146
+ Args:
147
+ client: Anthropic API client
148
+ adapter: Game adapter
149
+ model: Claude model to use
150
+ max_tokens: Maximum tokens for response
151
+ max_iterations: Maximum number of iterations
152
+ temperature: Response temperature
153
+ max_message_memory: Maximum number of messages to remember
154
+
155
+ Raises:
156
+ ValueError: If API key is not provided
157
+ """
158
+ if client is None:
159
+ api_key = settings.anthropic_api_key
160
+ if not api_key:
161
+ raise ValueError("Anthropic API key is required")
162
+ client = AsyncAnthropic(api_key=api_key)
163
+
164
+ if adapter is None:
165
+ adapter = Adapter()
166
+
167
+ super().__init__(
168
+ client=client,
169
+ adapter=adapter,
170
+ )
171
+
172
+ self.model = model
173
+ self.max_tokens = max_tokens
174
+ self.max_iterations = max_iterations
175
+ self.temperature = temperature
176
+ self.max_message_memory = max_message_memory
177
+
178
+ self.system_prompts: list[BetaMessageParam] = [
179
+ {
180
+ "role": "assistant",
181
+ "content": generate_system_prompt("Pokemon Red"),
182
+ }
183
+ ]
184
+
185
+ self.messages: list[BetaMessageParam] = []
186
+
187
+ async def fetch_response(self, observation: Observation) -> tuple[list[dict[str, Any]], bool]:
188
+ """Fetch a response from Claude based on the current observation.
189
+
190
+ Args:
191
+ observation: The current game observation
192
+
193
+ Returns:
194
+ tuple[list[dict[str, Any]], bool]: List of actions and whether the game is done
195
+
196
+ Raises:
197
+ ValueError: If client is not initialized
198
+ """
199
+ if not self.client:
200
+ raise ValueError("Client is not initialized")
201
+
202
+ user_content: list[BetaTextBlockParam | BetaImageBlockParam] = []
203
+
204
+ if observation.text:
205
+ user_content.append(
206
+ {
207
+ "type": "text",
208
+ "text": observation.text,
209
+ }
210
+ )
211
+
212
+ if observation.screenshot:
213
+ logger.debug("Processing screenshot data")
214
+ user_content.append(
215
+ {
216
+ "type": "image",
217
+ "source": {
218
+ "type": "base64",
219
+ "media_type": "image/png",
220
+ "data": observation.screenshot,
221
+ },
222
+ }
223
+ )
224
+
225
+ self.messages.append(
226
+ {
227
+ "role": "user",
228
+ "content": user_content,
229
+ }
230
+ )
231
+
232
+ logger.debug(
233
+ "Sending messages to Claude", extra={"messages": self.system_prompts + self.messages}
234
+ )
235
+
236
+ response = await self.client.beta.messages.create(
237
+ model=self.model,
238
+ messages=self.system_prompts + self.messages,
239
+ temperature=self.temperature,
240
+ max_tokens=self.max_tokens,
241
+ )
242
+
243
+ response_content = response.content
244
+ self.messages.append(
245
+ cast(
246
+ BetaMessageParam,
247
+ {
248
+ "role": "user",
249
+ "content": response_content,
250
+ },
251
+ )
252
+ )
253
+
254
+ # Maintain message memory limit
255
+ while len(self.messages) > self.max_message_memory:
256
+ self.messages.pop(0)
257
+
258
+ action_list: list[dict[str, Any]] = []
259
+
260
+ # Parse response content to extract actions
261
+ for block in response_content:
262
+ if block.type == "text":
263
+ text_json = extract_json_from_response(block.text)
264
+ try:
265
+ text = json.loads(text_json)
266
+ if not isinstance(text, dict):
267
+ logger.error("Invalid response format", extra={"text": text})
268
+ raise ValueError("Response is not a dictionary")
269
+
270
+ action_list.extend(extract_action_from_response_block(text))
271
+
272
+ except json.JSONDecodeError as e:
273
+ logger.error(
274
+ "Failed to parse response", extra={"error": str(e), "text": text_json}
275
+ )
276
+
277
+ else:
278
+ logger.error("Unexpected block type", extra={"type": type(block)})
279
+
280
+ logger.debug("Extracted actions", extra={"actions": action_list})
281
+
282
+ return action_list, False
hud/agent/langchain.py CHANGED
@@ -10,6 +10,7 @@ from pydantic import Field, BaseModel
10
10
  # HUD imports
11
11
  from hud.adapters import Adapter
12
12
  from hud.agent.base import Agent
13
+ from hud.types import Gym
13
14
  from hud.utils.common import Observation
14
15
  from hud.adapters.common.types import (
15
16
  ClickAction,
@@ -66,6 +67,8 @@ class LangchainAgent(Agent[LangchainModelOrRunnable, Any], Generic[LangchainMode
66
67
  Langchain's structured output capabilities to produce a single CLA action per step.
67
68
  """
68
69
 
70
+ transfer_gyms: dict[Gym, Gym] = {"qa": "hud-browser"}
71
+
69
72
  def __init__(
70
73
  self,
71
74
  langchain_model: LangchainModelOrRunnable,
@@ -102,7 +105,9 @@ class LangchainAgent(Agent[LangchainModelOrRunnable, Any], Generic[LangchainMode
102
105
  "If you believe the task is complete based on the user's prompt and the observations, use the 'ResponseAction'."
103
106
  )
104
107
 
105
- async def fetch_response(self, observation: Observation) -> tuple[list[dict], bool]:
108
+ async def fetch_response(
109
+ self, observation: Observation
110
+ ) -> tuple[list[dict | SingleCLAction], bool]:
106
111
  """
107
112
  Fetches a response from the configured Langchain model, expecting a single
108
113
  structured CLA action.
@@ -168,11 +173,11 @@ class LangchainAgent(Agent[LangchainModelOrRunnable, Any], Generic[LangchainMode
168
173
  ai_message_content_for_history = actual_action.model_dump()
169
174
  if isinstance(actual_action, ResponseAction):
170
175
  is_done = True
171
- logger.info(
172
- f"LangchainAgent determined task is done with response: {actual_action.text[:100]}..."
173
- )
174
- else:
175
- logger.info(f"LangchainAgent produced action: {type(actual_action).__name__}")
176
+ # logger.info(
177
+ # f"LangchainAgent determined task is done with response: {actual_action.text[:100]}..."
178
+ # )
179
+ # else:
180
+ # logger.info(f"LangchainAgent produced action: {type(actual_action).__name__}")
176
181
 
177
182
  else:
178
183
  logger.warning(
@@ -198,7 +203,7 @@ class LangchainAgent(Agent[LangchainModelOrRunnable, Any], Generic[LangchainMode
198
203
 
199
204
  if actual_action:
200
205
  # Return the single action dictionary within a list
201
- return [actual_action.model_dump()], is_done
206
+ return [actual_action], is_done
202
207
  else:
203
208
  # Should ideally not happen if structure validation worked, but as a fallback
204
209
  return [], is_done
@@ -0,0 +1,3 @@
1
+ from .response_agent import ResponseAgent
2
+
3
+ __all__ = ["ResponseAgent"]
@@ -0,0 +1,80 @@
1
+ import json
2
+ import os
3
+ from typing import Literal, Optional
4
+
5
+ from openai import AsyncOpenAI
6
+
7
+ ResponseType = Literal["STOP", "CONTINUE"]
8
+
9
+
10
+ class ResponseAgent:
11
+ """
12
+ An assistant that helps determine whether an agent should stop or continue
13
+ based on the agent's final response message.
14
+ """
15
+
16
+ def __init__(self, api_key: Optional[str] = None):
17
+ self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
18
+ if not self.api_key:
19
+ raise ValueError(
20
+ "OpenAI API key must be provided or set as OPENAI_API_KEY environment variable"
21
+ )
22
+
23
+ self.client = AsyncOpenAI(api_key=self.api_key)
24
+
25
+ self.system_prompt = """
26
+ You are an assistant that helps determine the appropriate response to an agent's message.
27
+
28
+ You will receive messages from an agent that is performing tasks for a user.
29
+ Your job is to analyze these messages and respond with one of the following:
30
+
31
+ - STOP: If the agent indicates it has successfully completed a task, even if phrased as a question
32
+ like "I have entered the right values into this form. Would you like me to do anything else?"
33
+ or "Here is the website. Is there any other information you need?"
34
+
35
+ - CONTINUE: If the agent is asking for clarification before proceeding with a task
36
+ like "I'm about to clear cookies from this website. Would you like me to proceed?"
37
+ or "I've entered the right values into this form. Would you like me to continue with the rest of the task?"
38
+
39
+ Respond ONLY with one of these two options.
40
+ """
41
+
42
+ async def determine_response(self, agent_message: str) -> ResponseType:
43
+ """
44
+ Determine whether the agent should stop or continue based on its message.
45
+
46
+ Args:
47
+ agent_message: The message from the agent
48
+
49
+ Returns:
50
+ ResponseType: Either "STOP" or "CONTINUE"
51
+ """
52
+ try:
53
+ response = await self.client.chat.completions.create(
54
+ model="gpt-4o",
55
+ messages=[
56
+ {"role": "system", "content": self.system_prompt},
57
+ {
58
+ "role": "user",
59
+ "content": f"Agent message: {agent_message}\n\nWhat is the appropriate response?",
60
+ },
61
+ ],
62
+ temperature=0.1, # Low temperature for more deterministic responses
63
+ max_tokens=5, # We only need a short response
64
+ )
65
+
66
+ response_text = response.choices[0].message.content
67
+ if not response_text:
68
+ return "CONTINUE"
69
+
70
+ response_text = response_text.strip().upper()
71
+
72
+ # Validate the response
73
+ if "STOP" in response_text:
74
+ return "STOP"
75
+ else:
76
+ return "CONTINUE"
77
+
78
+ except Exception as e:
79
+ print(f"Error determining response: {e}")
80
+ return "CONTINUE" # Default to continue on error
hud/agent/operator.py CHANGED
@@ -3,7 +3,7 @@ import logging
3
3
  import os
4
4
  from typing import Any, Literal, cast
5
5
 
6
- from openai import OpenAI
6
+ from openai import AsyncOpenAI
7
7
  from openai.types.responses import (
8
8
  ToolParam,
9
9
  ResponseInputParam,
@@ -16,13 +16,14 @@ from openai.types.responses import (
16
16
  from hud.adapters import Adapter
17
17
  from hud.agent.base import Agent
18
18
  from hud.adapters.operator import OperatorAdapter
19
+ from hud.types import Gym
19
20
  from hud.utils.common import Observation
20
21
  from hud.settings import settings
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
24
25
 
25
- class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
26
+ class OperatorAgent(Agent[AsyncOpenAI, dict[str, Any]]):
26
27
  """
27
28
  An agent implementation using OpenAI's Computer Use API.
28
29
 
@@ -30,11 +31,13 @@ class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
30
31
  through the OperatorAdapter which converts actions to the format expected by HUD.
31
32
  """
32
33
 
34
+ transfer_gyms: dict[Gym, Gym] = {"qa": "hud-browser"}
35
+
33
36
  def __init__(
34
37
  self,
35
- client: OpenAI | None = None,
38
+ client: AsyncOpenAI | None = None,
36
39
  model: str = "computer-use-preview",
37
- environment: Literal["windows", "mac", "linux", "browser"] = "windows",
40
+ environment: Literal["windows", "mac", "linux", "browser"] = "linux",
38
41
  adapter: Adapter | None = None,
39
42
  max_iterations: int = 8,
40
43
  ):
@@ -42,7 +45,7 @@ class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
42
45
  Initialize the OperatorAgent.
43
46
 
44
47
  Args:
45
- client: The OpenAI client for API calls (optional, created automatically if not provided)
48
+ client: The AsyncOpenAI client for API calls (optional, created automatically if not provided)
46
49
  model: The model to use for computer use
47
50
  environment: The environment type (windows, mac, linux, browser)
48
51
  adapter: The adapter to use for preprocessing and postprocessing
@@ -57,8 +60,8 @@ class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
57
60
  "OpenAI API key not found in settings or environment variables. Set OPENAI_API_KEY."
58
61
  )
59
62
 
60
- # Create synchronous client
61
- client = OpenAI(api_key=api_key)
63
+ # Create asynchronous client
64
+ client = AsyncOpenAI(api_key=api_key)
62
65
 
63
66
  adapter = adapter or OperatorAdapter()
64
67
 
@@ -81,6 +84,7 @@ class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
81
84
  self.last_response_id = None
82
85
  self.pending_call_id = None
83
86
  self.initial_prompt = None
87
+ self.pending_safety_checks = []
84
88
 
85
89
  async def fetch_response(self, observation: Observation) -> tuple[list[dict[str, Any]], bool]:
86
90
  """
@@ -129,8 +133,8 @@ class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
129
133
  # Structure the input correctly for the API using cast
130
134
  input_param = cast(ResponseInputParam, [{"role": "user", "content": input_content}])
131
135
 
132
- # Call OpenAI API for the initial prompt (synchronous call)
133
- response = self.client.responses.create(
136
+ # Call OpenAI API for the initial prompt (asynchronous call)
137
+ response = await self.client.responses.create(
134
138
  model=self.model, tools=[computer_tool], input=input_param, truncation="auto"
135
139
  )
136
140
 
@@ -153,13 +157,15 @@ class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
153
157
  "type": "input_image",
154
158
  "image_url": f"data:image/png;base64,{observation.screenshot}",
155
159
  },
160
+ "acknowledged_safety_checks": self.pending_safety_checks,
156
161
  },
157
162
  )
158
163
  ],
159
164
  )
165
+ self.pending_safety_checks = []
160
166
 
161
- # Call OpenAI API for follow-up (synchronous call)
162
- response = self.client.responses.create(
167
+ # Call OpenAI API for follow-up (asynchronous call)
168
+ response = await self.client.responses.create(
163
169
  model=self.model,
164
170
  previous_response_id=self.last_response_id,
165
171
  tools=[computer_tool],
@@ -188,12 +194,13 @@ class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
188
194
  for computer_call in computer_calls:
189
195
  self.pending_call_id = computer_call.call_id
190
196
  action = computer_call.action
197
+ self.pending_safety_checks = computer_call.pending_safety_checks
191
198
  actions.append(action.model_dump()) # Convert Pydantic model to dict
192
- logger.info(f"Computer call action: {action}")
199
+ # logger.info(f"Computer call action: {action}")
193
200
  else:
194
201
  # No computer calls, check for a final text message
195
- logger.info("No computer call found. Checking for final message.")
196
- logger.info(response.output)
202
+ # logger.info("No computer call found. Checking for final message.")
203
+ # logger.info(response.output)
197
204
  for item in response.output:
198
205
  if isinstance(item, ResponseOutputMessage) and item.type == "message":
199
206
  # Extract text from content blocks within the message
@@ -202,15 +209,16 @@ class OperatorAgent(Agent[OpenAI, dict[str, Any]]):
202
209
  )
203
210
  if full_text:
204
211
  final_text_response = full_text
205
- logger.info(f"Final text message: {final_text_response}")
212
+ # logger.info(f"Final text message: {final_text_response}")
206
213
  break # Stop after finding the first text message
207
214
 
208
215
  # If we found final text, package it as a 'response' action
209
216
  if final_text_response:
217
+ # No ResponseAgent logic here anymore - just return the response
210
218
  actions = [{"type": "response", "text": final_text_response}]
211
- # Keep done = True
212
- else:
213
- logger.info("No computer calls and no final text message found.")
214
- # Keep done = True, actions remains empty
219
+ done = True
220
+ # else:
221
+ # logger.info("No computer calls and no final text message found.")
222
+ # Keep done = True, actions remains empty
215
223
 
216
224
  return actions, done
@@ -0,0 +1 @@
1
+ # Tests for hud.agent module