code-puppy 0.0.348__py3-none-any.whl → 0.0.372__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.
Files changed (87) hide show
  1. code_puppy/agents/__init__.py +8 -0
  2. code_puppy/agents/agent_manager.py +272 -1
  3. code_puppy/agents/agent_pack_leader.py +383 -0
  4. code_puppy/agents/agent_qa_kitten.py +12 -7
  5. code_puppy/agents/agent_terminal_qa.py +323 -0
  6. code_puppy/agents/base_agent.py +11 -8
  7. code_puppy/agents/event_stream_handler.py +101 -8
  8. code_puppy/agents/pack/__init__.py +34 -0
  9. code_puppy/agents/pack/bloodhound.py +304 -0
  10. code_puppy/agents/pack/husky.py +321 -0
  11. code_puppy/agents/pack/retriever.py +393 -0
  12. code_puppy/agents/pack/shepherd.py +348 -0
  13. code_puppy/agents/pack/terrier.py +287 -0
  14. code_puppy/agents/pack/watchdog.py +367 -0
  15. code_puppy/agents/subagent_stream_handler.py +276 -0
  16. code_puppy/api/__init__.py +13 -0
  17. code_puppy/api/app.py +169 -0
  18. code_puppy/api/main.py +21 -0
  19. code_puppy/api/pty_manager.py +446 -0
  20. code_puppy/api/routers/__init__.py +12 -0
  21. code_puppy/api/routers/agents.py +36 -0
  22. code_puppy/api/routers/commands.py +217 -0
  23. code_puppy/api/routers/config.py +74 -0
  24. code_puppy/api/routers/sessions.py +232 -0
  25. code_puppy/api/templates/terminal.html +361 -0
  26. code_puppy/api/websocket.py +154 -0
  27. code_puppy/callbacks.py +73 -0
  28. code_puppy/chatgpt_codex_client.py +53 -0
  29. code_puppy/claude_cache_client.py +294 -41
  30. code_puppy/command_line/add_model_menu.py +13 -4
  31. code_puppy/command_line/agent_menu.py +662 -0
  32. code_puppy/command_line/core_commands.py +89 -112
  33. code_puppy/command_line/model_picker_completion.py +3 -20
  34. code_puppy/command_line/model_settings_menu.py +21 -3
  35. code_puppy/config.py +145 -70
  36. code_puppy/gemini_model.py +706 -0
  37. code_puppy/http_utils.py +6 -3
  38. code_puppy/messaging/__init__.py +15 -0
  39. code_puppy/messaging/messages.py +27 -0
  40. code_puppy/messaging/queue_console.py +1 -1
  41. code_puppy/messaging/rich_renderer.py +36 -1
  42. code_puppy/messaging/spinner/__init__.py +20 -2
  43. code_puppy/messaging/subagent_console.py +461 -0
  44. code_puppy/model_factory.py +50 -16
  45. code_puppy/model_switching.py +63 -0
  46. code_puppy/model_utils.py +27 -24
  47. code_puppy/models.json +12 -12
  48. code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
  49. code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
  50. code_puppy/plugins/antigravity_oauth/transport.py +236 -45
  51. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
  52. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
  53. code_puppy/plugins/claude_code_oauth/utils.py +4 -1
  54. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  55. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  56. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  57. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  58. code_puppy/pydantic_patches.py +52 -0
  59. code_puppy/status_display.py +6 -2
  60. code_puppy/tools/__init__.py +37 -1
  61. code_puppy/tools/agent_tools.py +83 -33
  62. code_puppy/tools/browser/__init__.py +37 -0
  63. code_puppy/tools/browser/browser_control.py +6 -6
  64. code_puppy/tools/browser/browser_interactions.py +21 -20
  65. code_puppy/tools/browser/browser_locators.py +9 -9
  66. code_puppy/tools/browser/browser_manager.py +316 -0
  67. code_puppy/tools/browser/browser_navigation.py +7 -7
  68. code_puppy/tools/browser/browser_screenshot.py +78 -140
  69. code_puppy/tools/browser/browser_scripts.py +15 -13
  70. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  71. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  72. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  73. code_puppy/tools/browser/terminal_tools.py +525 -0
  74. code_puppy/tools/command_runner.py +292 -101
  75. code_puppy/tools/common.py +176 -1
  76. code_puppy/tools/display.py +84 -0
  77. code_puppy/tools/subagent_context.py +158 -0
  78. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
  79. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
  80. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
  81. code_puppy/prompts/codex_system_prompt.md +0 -310
  82. code_puppy/tools/browser/camoufox_manager.py +0 -235
  83. code_puppy/tools/browser/vqa_agent.py +0 -90
  84. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
  85. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
  86. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
  87. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,261 @@
1
+ """Callback registration for frontend event emission.
2
+
3
+ This module registers callbacks for various agent events and emits them
4
+ to subscribed WebSocket handlers via the emitter module.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from typing import Any, Dict, Optional
10
+
11
+ from code_puppy.callbacks import register_callback
12
+ from code_puppy.plugins.frontend_emitter.emitter import emit_event
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ async def on_pre_tool_call(
18
+ tool_name: str, tool_args: Dict[str, Any], context: Any = None
19
+ ) -> None:
20
+ """Emit an event when a tool call starts.
21
+
22
+ Args:
23
+ tool_name: Name of the tool being called
24
+ tool_args: Arguments being passed to the tool
25
+ context: Optional context data for the tool call
26
+ """
27
+ try:
28
+ emit_event(
29
+ "tool_call_start",
30
+ {
31
+ "tool_name": tool_name,
32
+ "tool_args": _sanitize_args(tool_args),
33
+ "start_time": time.time(),
34
+ },
35
+ )
36
+ logger.debug(f"Emitted tool_call_start for {tool_name}")
37
+ except Exception as e:
38
+ logger.error(f"Failed to emit pre_tool_call event: {e}")
39
+
40
+
41
+ async def on_post_tool_call(
42
+ tool_name: str,
43
+ tool_args: Dict[str, Any],
44
+ result: Any,
45
+ duration_ms: float,
46
+ context: Any = None,
47
+ ) -> None:
48
+ """Emit an event when a tool call completes.
49
+
50
+ Args:
51
+ tool_name: Name of the tool that was called
52
+ tool_args: Arguments that were passed to the tool
53
+ result: The result returned by the tool
54
+ duration_ms: Execution time in milliseconds
55
+ context: Optional context data for the tool call
56
+ """
57
+ try:
58
+ emit_event(
59
+ "tool_call_complete",
60
+ {
61
+ "tool_name": tool_name,
62
+ "tool_args": _sanitize_args(tool_args),
63
+ "duration_ms": duration_ms,
64
+ "success": _is_successful_result(result),
65
+ "result_summary": _summarize_result(result),
66
+ },
67
+ )
68
+ logger.debug(
69
+ f"Emitted tool_call_complete for {tool_name} ({duration_ms:.2f}ms)"
70
+ )
71
+ except Exception as e:
72
+ logger.error(f"Failed to emit post_tool_call event: {e}")
73
+
74
+
75
+ async def on_stream_event(
76
+ event_type: str, event_data: Any, agent_session_id: Optional[str] = None
77
+ ) -> None:
78
+ """Emit streaming events from the agent.
79
+
80
+ Args:
81
+ event_type: Type of the streaming event
82
+ event_data: Data associated with the event
83
+ agent_session_id: Optional session ID of the agent emitting the event
84
+ """
85
+ try:
86
+ emit_event(
87
+ "stream_event",
88
+ {
89
+ "event_type": event_type,
90
+ "event_data": _sanitize_event_data(event_data),
91
+ "agent_session_id": agent_session_id,
92
+ },
93
+ )
94
+ logger.debug(f"Emitted stream_event: {event_type}")
95
+ except Exception as e:
96
+ logger.error(f"Failed to emit stream_event: {e}")
97
+
98
+
99
+ async def on_invoke_agent(*args: Any, **kwargs: Any) -> None:
100
+ """Emit an event when an agent is invoked.
101
+
102
+ Args:
103
+ *args: Positional arguments from the invoke_agent callback
104
+ **kwargs: Keyword arguments from the invoke_agent callback
105
+ """
106
+ try:
107
+ # Extract relevant info from args/kwargs
108
+ agent_info = {
109
+ "agent_name": kwargs.get("agent_name") or (args[0] if args else None),
110
+ "session_id": kwargs.get("session_id"),
111
+ "prompt_preview": _truncate_string(
112
+ kwargs.get("prompt") or (args[1] if len(args) > 1 else None),
113
+ max_length=200,
114
+ ),
115
+ }
116
+ emit_event("agent_invoked", agent_info)
117
+ logger.debug(f"Emitted agent_invoked: {agent_info.get('agent_name')}")
118
+ except Exception as e:
119
+ logger.error(f"Failed to emit invoke_agent event: {e}")
120
+
121
+
122
+ def _sanitize_args(args: Dict[str, Any]) -> Dict[str, Any]:
123
+ """Sanitize tool arguments for safe emission.
124
+
125
+ Truncates large values and removes potentially sensitive data.
126
+
127
+ Args:
128
+ args: The raw tool arguments
129
+
130
+ Returns:
131
+ Sanitized arguments safe for emission
132
+ """
133
+ if not isinstance(args, dict):
134
+ return {}
135
+
136
+ sanitized: Dict[str, Any] = {}
137
+ for key, value in args.items():
138
+ if isinstance(value, str):
139
+ sanitized[key] = _truncate_string(value, max_length=500)
140
+ elif isinstance(value, (int, float, bool, type(None))):
141
+ sanitized[key] = value
142
+ elif isinstance(value, (list, dict)):
143
+ # Just indicate the type and length for complex types
144
+ sanitized[key] = f"<{type(value).__name__}[{len(value)}]>"
145
+ else:
146
+ sanitized[key] = f"<{type(value).__name__}>"
147
+
148
+ return sanitized
149
+
150
+
151
+ def _sanitize_event_data(data: Any) -> Any:
152
+ """Sanitize event data for safe emission.
153
+
154
+ Args:
155
+ data: The raw event data
156
+
157
+ Returns:
158
+ Sanitized data safe for emission
159
+ """
160
+ if data is None:
161
+ return None
162
+
163
+ if isinstance(data, str):
164
+ return _truncate_string(data, max_length=1000)
165
+
166
+ if isinstance(data, (int, float, bool)):
167
+ return data
168
+
169
+ if isinstance(data, dict):
170
+ return {k: _sanitize_event_data(v) for k, v in list(data.items())[:20]}
171
+
172
+ if isinstance(data, (list, tuple)):
173
+ return [_sanitize_event_data(item) for item in data[:20]]
174
+
175
+ return f"<{type(data).__name__}>"
176
+
177
+
178
+ def _is_successful_result(result: Any) -> bool:
179
+ """Determine if a tool result indicates success.
180
+
181
+ Args:
182
+ result: The tool result
183
+
184
+ Returns:
185
+ True if the result appears successful
186
+ """
187
+ if result is None:
188
+ return True # No result often means success
189
+
190
+ if isinstance(result, dict):
191
+ # Check for error indicators
192
+ if result.get("error"):
193
+ return False
194
+ if result.get("success") is False:
195
+ return False
196
+ return True
197
+
198
+ if isinstance(result, bool):
199
+ return result
200
+
201
+ return True # Default to success
202
+
203
+
204
+ def _summarize_result(result: Any) -> str:
205
+ """Create a brief summary of a tool result.
206
+
207
+ Args:
208
+ result: The tool result
209
+
210
+ Returns:
211
+ A string summary of the result
212
+ """
213
+ if result is None:
214
+ return "<no result>"
215
+
216
+ if isinstance(result, str):
217
+ return _truncate_string(result, max_length=200)
218
+
219
+ if isinstance(result, dict):
220
+ if "error" in result:
221
+ return f"Error: {_truncate_string(str(result['error']), max_length=100)}"
222
+ if "message" in result:
223
+ return _truncate_string(str(result["message"]), max_length=100)
224
+ return f"<dict with {len(result)} keys>"
225
+
226
+ if isinstance(result, (list, tuple)):
227
+ return f"<{type(result).__name__}[{len(result)}]>"
228
+
229
+ return _truncate_string(str(result), max_length=200)
230
+
231
+
232
+ def _truncate_string(value: Any, max_length: int = 100) -> Optional[str]:
233
+ """Truncate a string value if it exceeds max_length.
234
+
235
+ Args:
236
+ value: The value to truncate (will be converted to str)
237
+ max_length: Maximum length before truncation
238
+
239
+ Returns:
240
+ Truncated string or None if value is None
241
+ """
242
+ if value is None:
243
+ return None
244
+
245
+ s = str(value)
246
+ if len(s) > max_length:
247
+ return s[: max_length - 3] + "..."
248
+ return s
249
+
250
+
251
+ def register() -> None:
252
+ """Register all frontend emitter callbacks."""
253
+ register_callback("pre_tool_call", on_pre_tool_call)
254
+ register_callback("post_tool_call", on_post_tool_call)
255
+ register_callback("stream_event", on_stream_event)
256
+ register_callback("invoke_agent", on_invoke_agent)
257
+ logger.debug("Frontend emitter callbacks registered")
258
+
259
+
260
+ # Auto-register callbacks when this module is imported
261
+ register()
@@ -0,0 +1 @@
1
+ <identity>\nYou are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.\nYou are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.\nThe USER will send you requests, which you must always prioritize addressing. Along with each USER request, we will attach additional metadata about their current state, such as what files they have open and where their cursor is.\nThis information may or may not be relevant to the coding task, it is up for you to decide.\n</identity>\n\n<tool_calling>\nCall tools as you normally would. The following list provides additional guidance to help you avoid errors:\n - **Absolute paths only**. When using tools that accept file path arguments, ALWAYS use the absolute file path.\n</tool_calling>\n\n<web_application_development>\n## Technology Stack,\nYour web applications should be built using the following technologies:,\n1. **Core**: Use HTML for structure and Javascript for logic.\n2. **Styling (CSS)**: Use Vanilla CSS for maximum flexibility and control. Avoid using TailwindCSS unless the USER explicitly requests it; in this case, first confirm which TailwindCSS version to use.\n3. **Web App**: If the USER specifies that they want a more complex web app, use a framework like Next.js or Vite. Only do this if the USER explicitly requests a web app.\n4. **New Project Creation**: If you need to use a framework for a new app, use `npx` with the appropriate script, but there are some rules to follow:,\n - Use `npx -y` to automatically install the script and its dependencies\n - You MUST run the command with `--help` flag to see all available options first, \n - Initialize the app in the current directory with `./` (example: `npx -y create-vite-app@latest ./`),\n - You should run in non-interactive mode so that the user doesn't need to input anything,\n5. **Running Locally**: When running locally, use `npm run dev` or equivalent dev server. Only build the production bundle if the USER explicitly requests it or you are validating the code for correctness.\n\n# Design Aesthetics,\n1. **Use Rich Aesthetics**: The USER should be wowed at first glance by the design. Use best practices in modern web design (e.g. vibrant colors, dark modes, glassmorphism, and dynamic animations) to create a stunning first impression. Failure to do this is UNACCEPTABLE.\n2. **Prioritize Visual Excellence**: Implement designs that will WOW the user and feel extremely premium:\n\t\t- Avoid generic colors (plain red, blue, green). Use curated, harmonious color palettes (e.g., HSL tailored colors, sleek dark modes).\n - Using modern typography (e.g., from Google Fonts like Inter, Roboto, or Outfit) instead of browser defaults.\n\t\t- Use smooth gradients,\n\t\t- Add subtle micro-animations for enhanced user experience,\n3. **Use a Dynamic Design**: An interface that feels responsive and alive encourages interaction. Achieve this with hover effects and interactive elements. Micro-animations, in particular, are highly effective for improving user engagement.\n4. **Premium Designs**. Make a design that feels premium and state of the art. Avoid creating simple minimum viable products.\n4. **Don't use placeholders**. If you need an image, use your generate_image tool to create a working demonstration.,\n\n## Implementation Workflow,\nFollow this systematic approach when building web applications:,\n1. **Plan and Understand**:,\n\t\t- Fully understand the user's requirements,\n\t\t- Draw inspiration from modern, beautiful, and dynamic web designs,\n\t\t- Outline the features needed for the initial version,\n2. **Build the Foundation**:,\n\t\t- Start by creating/modifying `index.css`,\n\t\t- Implement the core design system with all tokens and utilities,\n3. **Create Components**:,\n\t\t- Build necessary components using your design system,\n\t\t- Ensure all components use predefined styles, not ad-hoc utilities,\n\t\t- Keep components focused and reusable,\n4. **Assemble Pages**:,\n\t\t- Update the main application to incorporate your design and components,\n\t\t- Ensure proper routing and navigation,\n\t\t- Implement responsive layouts,\n5. **Polish and Optimize**:,\n\t\t- Review the overall user experience,\n\t\t- Ensure smooth interactions and transitions,\n\t\t- Optimize performance where needed,\n\n## SEO Best Practices,\nAutomatically implement SEO best practices on every page:,\n- **Title Tags**: Include proper, descriptive title tags for each page,\n- **Meta Descriptions**: Add compelling meta descriptions that accurately summarize page content,\n- **Heading Structure**: Use a single `<h1>` per page with proper heading hierarchy,\n- **Semantic HTML**: Use appropriate HTML5 semantic elements,\n- **Unique IDs**: Ensure all interactive elements have unique, descriptive IDs for browser testing,\n- **Performance**: Ensure fast page load times through optimization,\nCRITICAL REMINDER: AESTHETICS ARE VERY IMPORTANT. If your web app looks simple and basic then you have FAILED!\n</web_application_development>\n<ephemeral_message>\nThere will be an <EPHEMERAL_MESSAGE> appearing in the conversation at times. This is not coming from the user, but instead injected by the system as important information to pay attention to. \nDo not respond to nor acknowledge those messages, but do follow them strictly.\n</ephemeral_message>\n\n\n<communication_style>\n- **Formatting**. Format your responses in github-style markdown to make your responses easier for the USER to parse. For example, use headers to organize your responses and bolded or italicized text to highlight important keywords. Use backticks to format file, directory, function, and class names. If providing a URL to the user, format this in markdown as well, for example `[label](example.com)`.\n- **Proactiveness**. As an agent, you are allowed to be proactive, but only in the course of completing the user's task. For example, if the user asks you to add a new component, you can edit the code, verify build and test statuses, and take any other obvious follow-up actions, such as performing additional research. However, avoid surprising the user. For example, if the user asks HOW to approach something, you should answer their question and instead of jumping into editing a file.\n- **Helpfulness**. Respond like a helpful software engineer who is explaining your work to a friendly collaborator on the project. Acknowledge mistakes or any backtracking you do as a result of new information.\n- **Ask for clarification**. If you are unsure about the USER's intent, always ask for clarification rather than making assumptions.\n</communication_style>
@@ -121,6 +121,57 @@ def patch_process_message_history() -> None:
121
121
  pass
122
122
 
123
123
 
124
+ def patch_tool_call_json_repair() -> None:
125
+ """Patch pydantic-ai's _call_tool to auto-repair malformed JSON arguments.
126
+
127
+ LLMs sometimes produce slightly broken JSON in tool calls (trailing commas,
128
+ missing quotes, etc.). This patch intercepts tool calls and runs json_repair
129
+ on the arguments before validation, preventing unnecessary retries.
130
+ """
131
+ try:
132
+ import json_repair
133
+ from pydantic_ai._tool_manager import ToolManager
134
+
135
+ # Store the original method
136
+ _original_call_tool = ToolManager._call_tool
137
+
138
+ async def _patched_call_tool(
139
+ self,
140
+ call,
141
+ *,
142
+ allow_partial: bool,
143
+ wrap_validation_errors: bool,
144
+ approved: bool,
145
+ ):
146
+ """Patched _call_tool that repairs malformed JSON before validation."""
147
+ # Only attempt repair if args is a string (JSON)
148
+ if isinstance(call.args, str) and call.args:
149
+ try:
150
+ repaired = json_repair.repair_json(call.args)
151
+ if repaired != call.args:
152
+ # Update the call args with repaired JSON
153
+ call.args = repaired
154
+ except Exception:
155
+ pass # If repair fails, let original validation handle it
156
+
157
+ # Call the original method
158
+ return await _original_call_tool(
159
+ self,
160
+ call,
161
+ allow_partial=allow_partial,
162
+ wrap_validation_errors=wrap_validation_errors,
163
+ approved=approved,
164
+ )
165
+
166
+ # Apply the patch
167
+ ToolManager._call_tool = _patched_call_tool
168
+
169
+ except ImportError:
170
+ pass # json_repair or pydantic_ai not available
171
+ except Exception:
172
+ pass # Don't crash on patch failure
173
+
174
+
124
175
  def apply_all_patches() -> None:
125
176
  """Apply all pydantic-ai monkey patches.
126
177
 
@@ -129,3 +180,4 @@ def apply_all_patches() -> None:
129
180
  patch_user_agent()
130
181
  patch_message_history_cleaning()
131
182
  patch_process_message_history()
183
+ patch_tool_call_json_repair()
@@ -7,8 +7,6 @@ from rich.panel import Panel
7
7
  from rich.spinner import Spinner
8
8
  from rich.text import Text
9
9
 
10
- from code_puppy.messaging import emit_info
11
-
12
10
  # Global variable to track current token per second rate
13
11
  CURRENT_TOKEN_RATE = 0.0
14
12
 
@@ -186,6 +184,9 @@ class StatusDisplay:
186
184
 
187
185
  async def _update_display(self) -> None:
188
186
  """Update the display continuously while active using Rich Live display"""
187
+ # Lazy import to avoid circular dependency during module initialization
188
+ from code_puppy.messaging import emit_info
189
+
189
190
  # Add a newline to ensure we're below the blue bar
190
191
  emit_info("")
191
192
 
@@ -214,6 +215,9 @@ class StatusDisplay:
214
215
 
215
216
  def stop(self) -> None:
216
217
  """Stop the status display"""
218
+ # Lazy import to avoid circular dependency during module initialization
219
+ from code_puppy.messaging import emit_info
220
+
217
221
  if self.is_active:
218
222
  self.is_active = False
219
223
  if self.task:
@@ -55,10 +55,32 @@ from code_puppy.tools.browser.browser_workflows import (
55
55
  register_read_workflow,
56
56
  register_save_workflow,
57
57
  )
58
+ from code_puppy.tools.browser.terminal_command_tools import (
59
+ register_run_terminal_command,
60
+ register_send_terminal_keys,
61
+ register_wait_terminal_output,
62
+ )
63
+ from code_puppy.tools.browser.terminal_screenshot_tools import (
64
+ register_load_image,
65
+ register_terminal_compare_mockup,
66
+ register_terminal_read_output,
67
+ register_terminal_screenshot,
68
+ )
69
+
70
+ # Terminal automation tools
71
+ from code_puppy.tools.browser.terminal_tools import (
72
+ register_check_terminal_server,
73
+ register_close_terminal,
74
+ register_open_terminal,
75
+ register_start_api_server,
76
+ )
58
77
  from code_puppy.tools.command_runner import (
59
78
  register_agent_run_shell_command,
60
79
  register_agent_share_your_reasoning,
61
80
  )
81
+ from code_puppy.tools.display import (
82
+ display_non_streamed_result as display_non_streamed_result,
83
+ )
62
84
  from code_puppy.tools.file_modifications import register_delete_file, register_edit_file
63
85
  from code_puppy.tools.file_operations import (
64
86
  register_grep,
@@ -121,12 +143,26 @@ TOOL_REGISTRY = {
121
143
  "browser_wait_for_element": register_wait_for_element,
122
144
  "browser_highlight_element": register_browser_highlight_element,
123
145
  "browser_clear_highlights": register_browser_clear_highlights,
124
- # Browser Screenshots and VQA
146
+ # Browser Screenshots
125
147
  "browser_screenshot_analyze": register_take_screenshot_and_analyze,
126
148
  # Browser Workflows
127
149
  "browser_save_workflow": register_save_workflow,
128
150
  "browser_list_workflows": register_list_workflows,
129
151
  "browser_read_workflow": register_read_workflow,
152
+ # Terminal Connection Tools
153
+ "terminal_check_server": register_check_terminal_server,
154
+ "terminal_open": register_open_terminal,
155
+ "terminal_close": register_close_terminal,
156
+ "start_api_server": register_start_api_server,
157
+ # Terminal Command Execution Tools
158
+ "terminal_run_command": register_run_terminal_command,
159
+ "terminal_send_keys": register_send_terminal_keys,
160
+ "terminal_wait_output": register_wait_terminal_output,
161
+ # Terminal Screenshot Tools
162
+ "terminal_screenshot_analyze": register_terminal_screenshot,
163
+ "terminal_read_output": register_terminal_read_output,
164
+ "terminal_compare_mockup": register_terminal_compare_mockup,
165
+ "load_image_for_analysis": register_load_image,
130
166
  }
131
167
 
132
168
 
@@ -7,6 +7,7 @@ import pickle
7
7
  import re
8
8
  import traceback
9
9
  from datetime import datetime
10
+ from functools import partial
10
11
  from pathlib import Path
11
12
  from typing import List, Set
12
13
 
@@ -28,12 +29,13 @@ from code_puppy.messaging import (
28
29
  SubAgentResponseMessage,
29
30
  emit_error,
30
31
  emit_info,
32
+ emit_success,
31
33
  get_message_bus,
32
34
  get_session_context,
33
35
  set_session_context,
34
36
  )
35
- from code_puppy.model_factory import ModelFactory, make_model_settings
36
37
  from code_puppy.tools.common import generate_group_id
38
+ from code_puppy.tools.subagent_context import subagent_context
37
39
 
38
40
  # Set to track active subagent invocation tasks
39
41
  _active_subagent_tasks: Set[asyncio.Task] = set()
@@ -413,6 +415,9 @@ def register_invoke_agent(agent):
413
415
  session_id = f"{session_id}-{hash_suffix}"
414
416
  # else: continuing existing session, use session_id as-is
415
417
 
418
+ # Lazy imports to avoid circular dependency
419
+ from code_puppy.agents.subagent_stream_handler import subagent_stream_handler
420
+
416
421
  # Emit structured invocation message via MessageBus
417
422
  bus = get_message_bus()
418
423
  bus.emit(
@@ -429,7 +434,27 @@ def register_invoke_agent(agent):
429
434
  previous_session_id = get_session_context()
430
435
  set_session_context(session_id)
431
436
 
437
+ # Set terminal session for browser-based terminal tools
438
+ # This uses contextvars which properly propagate through async tasks
439
+ from code_puppy.tools.browser.terminal_tools import (
440
+ _terminal_session_var,
441
+ set_terminal_session,
442
+ )
443
+
444
+ terminal_session_token = set_terminal_session(f"terminal-{session_id}")
445
+
446
+ # Set browser session for browser tools (qa-kitten, etc.)
447
+ # This allows parallel agent invocations to each have their own browser
448
+ from code_puppy.tools.browser.browser_manager import (
449
+ set_browser_session,
450
+ )
451
+
452
+ browser_session_token = set_browser_session(f"browser-{session_id}")
453
+
432
454
  try:
455
+ # Lazy import to break circular dependency with messaging module
456
+ from code_puppy.model_factory import ModelFactory, make_model_settings
457
+
433
458
  # Load the specified agent config
434
459
  agent_config = load_agent(agent_name)
435
460
 
@@ -483,9 +508,6 @@ def register_invoke_agent(agent):
483
508
  manager = get_mcp_manager()
484
509
  mcp_servers = manager.get_servers_for_agent()
485
510
 
486
- # Get the event_stream_handler for streaming output
487
- from code_puppy.agents.event_stream_handler import event_stream_handler
488
-
489
511
  if get_use_dbos():
490
512
  from pydantic_ai.durable_exec.dbos import DBOSAgent
491
513
 
@@ -507,11 +529,10 @@ def register_invoke_agent(agent):
507
529
  agent_tools = agent_config.get_available_tools()
508
530
  register_tools_for_agent(temp_agent, agent_tools)
509
531
 
510
- # Wrap with DBOS - pass event_stream_handler for streaming output
532
+ # Wrap with DBOS - no streaming for sub-agents
511
533
  dbos_agent = DBOSAgent(
512
534
  temp_agent,
513
535
  name=subagent_name,
514
- event_stream_handler=event_stream_handler,
515
536
  )
516
537
  temp_agent = dbos_agent
517
538
 
@@ -540,43 +561,54 @@ def register_invoke_agent(agent):
540
561
  # Run the temporary agent with the provided prompt as an asyncio task
541
562
  # Pass the message_history from the session to continue the conversation
542
563
  workflow_id = None # Track for potential cancellation
543
- if get_use_dbos():
544
- # Generate a unique workflow ID for DBOS - ensures no collisions in back-to-back calls
545
- workflow_id = _generate_dbos_workflow_id(group_id)
546
564
 
547
- # Add MCP servers to the DBOS agent's toolsets
548
- # (temp_agent is discarded after this invocation, so no need to restore)
549
- if subagent_mcp_servers:
550
- temp_agent._toolsets = temp_agent._toolsets + subagent_mcp_servers
565
+ # Always use subagent_stream_handler to silence output and update console manager
566
+ # This ensures all sub-agent output goes through the aggregated dashboard
567
+ stream_handler = partial(subagent_stream_handler, session_id=session_id)
568
+
569
+ # Wrap the agent run in subagent context for tracking
570
+ with subagent_context(agent_name):
571
+ if get_use_dbos():
572
+ # Generate a unique workflow ID for DBOS - ensures no collisions in back-to-back calls
573
+ workflow_id = _generate_dbos_workflow_id(group_id)
574
+
575
+ # Add MCP servers to the DBOS agent's toolsets
576
+ # (temp_agent is discarded after this invocation, so no need to restore)
577
+ if subagent_mcp_servers:
578
+ temp_agent._toolsets = (
579
+ temp_agent._toolsets + subagent_mcp_servers
580
+ )
551
581
 
552
- with SetWorkflowID(workflow_id):
582
+ with SetWorkflowID(workflow_id):
583
+ task = asyncio.create_task(
584
+ temp_agent.run(
585
+ prompt,
586
+ message_history=message_history,
587
+ usage_limits=UsageLimits(
588
+ request_limit=get_message_limit()
589
+ ),
590
+ event_stream_handler=stream_handler,
591
+ )
592
+ )
593
+ _active_subagent_tasks.add(task)
594
+ else:
553
595
  task = asyncio.create_task(
554
596
  temp_agent.run(
555
597
  prompt,
556
598
  message_history=message_history,
557
599
  usage_limits=UsageLimits(request_limit=get_message_limit()),
558
- event_stream_handler=event_stream_handler,
600
+ event_stream_handler=stream_handler,
559
601
  )
560
602
  )
561
603
  _active_subagent_tasks.add(task)
562
- else:
563
- task = asyncio.create_task(
564
- temp_agent.run(
565
- prompt,
566
- message_history=message_history,
567
- usage_limits=UsageLimits(request_limit=get_message_limit()),
568
- event_stream_handler=event_stream_handler,
569
- )
570
- )
571
- _active_subagent_tasks.add(task)
572
604
 
573
- try:
574
- result = await task
575
- finally:
576
- _active_subagent_tasks.discard(task)
577
- if task.cancelled():
578
- if get_use_dbos() and workflow_id:
579
- DBOS.cancel_workflow(workflow_id)
605
+ try:
606
+ result = await task
607
+ finally:
608
+ _active_subagent_tasks.discard(task)
609
+ if task.cancelled():
610
+ if get_use_dbos() and workflow_id:
611
+ DBOS.cancel_workflow(workflow_id)
580
612
 
581
613
  # Extract the response from the result
582
614
  response = result.output
@@ -603,13 +635,23 @@ def register_invoke_agent(agent):
603
635
  )
604
636
  )
605
637
 
638
+ # Emit clean completion summary
639
+ emit_success(
640
+ f"✓ {agent_name} completed successfully", message_group=group_id
641
+ )
642
+
606
643
  return AgentInvokeOutput(
607
644
  response=response, agent_name=agent_name, session_id=session_id
608
645
  )
609
646
 
610
- except Exception:
647
+ except Exception as e:
648
+ # Emit clean failure summary
649
+ emit_error(f"✗ {agent_name} failed: {str(e)}", message_group=group_id)
650
+
651
+ # Full traceback for debugging
611
652
  error_msg = f"Error invoking agent '{agent_name}': {traceback.format_exc()}"
612
653
  emit_error(error_msg, message_group=group_id)
654
+
613
655
  return AgentInvokeOutput(
614
656
  response=None,
615
657
  agent_name=agent_name,
@@ -620,5 +662,13 @@ def register_invoke_agent(agent):
620
662
  finally:
621
663
  # Restore the previous session context
622
664
  set_session_context(previous_session_id)
665
+ # Reset terminal session context
666
+ _terminal_session_var.reset(terminal_session_token)
667
+ # Reset browser session context
668
+ from code_puppy.tools.browser.browser_manager import (
669
+ _browser_session_var,
670
+ )
671
+
672
+ _browser_session_var.reset(browser_session_token)
623
673
 
624
674
  return invoke_agent
@@ -0,0 +1,37 @@
1
+ """Browser tools for terminal automation.
2
+
3
+ This module provides browser-based terminal automation tools.
4
+ """
5
+
6
+ from code_puppy.config import get_banner_color
7
+
8
+ from .browser_manager import (
9
+ cleanup_all_browsers,
10
+ get_browser_session,
11
+ get_session_browser_manager,
12
+ set_browser_session,
13
+ )
14
+
15
+
16
+ def format_terminal_banner(text: str) -> str:
17
+ """Format a terminal tool banner with the configured terminal_tool color.
18
+
19
+ Returns Rich markup string that can be used with Text.from_markup().
20
+
21
+ Args:
22
+ text: The banner text (e.g., "TERMINAL OPEN 🖥️ localhost:8765")
23
+
24
+ Returns:
25
+ Rich markup formatted string
26
+ """
27
+ color = get_banner_color("terminal_tool")
28
+ return f"[bold white on {color}] {text} [/bold white on {color}]"
29
+
30
+
31
+ __all__ = [
32
+ "format_terminal_banner",
33
+ "cleanup_all_browsers",
34
+ "get_browser_session",
35
+ "get_session_browser_manager",
36
+ "set_browser_session",
37
+ ]