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.
- code_puppy/agents/__init__.py +8 -0
- code_puppy/agents/agent_manager.py +272 -1
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +11 -8
- code_puppy/agents/event_stream_handler.py +101 -8
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/chatgpt_codex_client.py +53 -0
- code_puppy/claude_cache_client.py +294 -41
- code_puppy/command_line/add_model_menu.py +13 -4
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/core_commands.py +89 -112
- code_puppy/command_line/model_picker_completion.py +3 -20
- code_puppy/command_line/model_settings_menu.py +21 -3
- code_puppy/config.py +145 -70
- code_puppy/gemini_model.py +706 -0
- code_puppy/http_utils.py +6 -3
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +50 -16
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +27 -24
- code_puppy/models.json +12 -12
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
- code_puppy/plugins/antigravity_oauth/transport.py +236 -45
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
- code_puppy/plugins/claude_code_oauth/utils.py +4 -1
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +52 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +83 -33
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
- code_puppy/prompts/codex_system_prompt.md +0 -310
- code_puppy/tools/browser/camoufox_manager.py +0 -235
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
- {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>
|
code_puppy/pydantic_patches.py
CHANGED
|
@@ -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()
|
code_puppy/status_display.py
CHANGED
|
@@ -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:
|
code_puppy/tools/__init__.py
CHANGED
|
@@ -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
|
|
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
|
|
code_puppy/tools/agent_tools.py
CHANGED
|
@@ -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 -
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
]
|