code-puppy 0.0.353__py3-none-any.whl → 0.0.355__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 +2 -0
- code_puppy/agents/agent_manager.py +49 -0
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +1 -0
- code_puppy/agents/event_stream_handler.py +74 -1
- 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 +92 -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 +198 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +191 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/command_line/core_commands.py +85 -0
- code_puppy/config.py +63 -0
- 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 +34 -0
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- 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/status_display.py +6 -2
- code_puppy/tools/agent_tools.py +56 -50
- code_puppy/tools/browser/vqa_agent.py +1 -1
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +6 -1
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/METADATA +4 -3
- {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/RECORD +49 -24
- {code_puppy-0.0.353.data → code_puppy-0.0.355.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.353.data → code_puppy-0.0.355.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.353.dist-info → code_puppy-0.0.355.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()
|
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/agent_tools.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# agent_tools.py
|
|
2
2
|
import asyncio
|
|
3
|
+
from functools import partial
|
|
3
4
|
import hashlib
|
|
4
5
|
import itertools
|
|
5
6
|
import json
|
|
@@ -28,13 +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
|
-
from code_puppy.model_utils import is_claude_code_model
|
|
37
37
|
from code_puppy.tools.common import generate_group_id
|
|
38
|
+
from code_puppy.tools.subagent_context import subagent_context
|
|
38
39
|
|
|
39
40
|
# Set to track active subagent invocation tasks
|
|
40
41
|
_active_subagent_tasks: Set[asyncio.Task] = set()
|
|
@@ -414,6 +415,9 @@ def register_invoke_agent(agent):
|
|
|
414
415
|
session_id = f"{session_id}-{hash_suffix}"
|
|
415
416
|
# else: continuing existing session, use session_id as-is
|
|
416
417
|
|
|
418
|
+
# Lazy imports to avoid circular dependency
|
|
419
|
+
from code_puppy.agents.subagent_stream_handler import subagent_stream_handler
|
|
420
|
+
|
|
417
421
|
# Emit structured invocation message via MessageBus
|
|
418
422
|
bus = get_message_bus()
|
|
419
423
|
bus.emit(
|
|
@@ -431,6 +435,9 @@ def register_invoke_agent(agent):
|
|
|
431
435
|
set_session_context(session_id)
|
|
432
436
|
|
|
433
437
|
try:
|
|
438
|
+
# Lazy import to break circular dependency with messaging module
|
|
439
|
+
from code_puppy.model_factory import ModelFactory, make_model_settings
|
|
440
|
+
|
|
434
441
|
# Load the specified agent config
|
|
435
442
|
agent_config = load_agent(agent_name)
|
|
436
443
|
|
|
@@ -484,9 +491,6 @@ def register_invoke_agent(agent):
|
|
|
484
491
|
manager = get_mcp_manager()
|
|
485
492
|
mcp_servers = manager.get_servers_for_agent()
|
|
486
493
|
|
|
487
|
-
# Import display function for non-streaming output
|
|
488
|
-
from code_puppy.tools.display import display_non_streamed_result
|
|
489
|
-
|
|
490
494
|
if get_use_dbos():
|
|
491
495
|
from pydantic_ai.durable_exec.dbos import DBOSAgent
|
|
492
496
|
|
|
@@ -541,24 +545,36 @@ def register_invoke_agent(agent):
|
|
|
541
545
|
# Pass the message_history from the session to continue the conversation
|
|
542
546
|
workflow_id = None # Track for potential cancellation
|
|
543
547
|
|
|
544
|
-
#
|
|
545
|
-
#
|
|
546
|
-
stream_handler =
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
548
|
+
# Always use subagent_stream_handler to silence output and update console manager
|
|
549
|
+
# This ensures all sub-agent output goes through the aggregated dashboard
|
|
550
|
+
stream_handler = partial(subagent_stream_handler, session_id=session_id)
|
|
551
|
+
|
|
552
|
+
# Wrap the agent run in subagent context for tracking
|
|
553
|
+
with subagent_context(agent_name):
|
|
554
|
+
if get_use_dbos():
|
|
555
|
+
# Generate a unique workflow ID for DBOS - ensures no collisions in back-to-back calls
|
|
556
|
+
workflow_id = _generate_dbos_workflow_id(group_id)
|
|
557
|
+
|
|
558
|
+
# Add MCP servers to the DBOS agent's toolsets
|
|
559
|
+
# (temp_agent is discarded after this invocation, so no need to restore)
|
|
560
|
+
if subagent_mcp_servers:
|
|
561
|
+
temp_agent._toolsets = (
|
|
562
|
+
temp_agent._toolsets + subagent_mcp_servers
|
|
563
|
+
)
|
|
560
564
|
|
|
561
|
-
|
|
565
|
+
with SetWorkflowID(workflow_id):
|
|
566
|
+
task = asyncio.create_task(
|
|
567
|
+
temp_agent.run(
|
|
568
|
+
prompt,
|
|
569
|
+
message_history=message_history,
|
|
570
|
+
usage_limits=UsageLimits(
|
|
571
|
+
request_limit=get_message_limit()
|
|
572
|
+
),
|
|
573
|
+
event_stream_handler=stream_handler,
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
_active_subagent_tasks.add(task)
|
|
577
|
+
else:
|
|
562
578
|
task = asyncio.create_task(
|
|
563
579
|
temp_agent.run(
|
|
564
580
|
prompt,
|
|
@@ -568,38 +584,18 @@ def register_invoke_agent(agent):
|
|
|
568
584
|
)
|
|
569
585
|
)
|
|
570
586
|
_active_subagent_tasks.add(task)
|
|
571
|
-
else:
|
|
572
|
-
task = asyncio.create_task(
|
|
573
|
-
temp_agent.run(
|
|
574
|
-
prompt,
|
|
575
|
-
message_history=message_history,
|
|
576
|
-
usage_limits=UsageLimits(request_limit=get_message_limit()),
|
|
577
|
-
event_stream_handler=stream_handler,
|
|
578
|
-
)
|
|
579
|
-
)
|
|
580
|
-
_active_subagent_tasks.add(task)
|
|
581
587
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
588
|
+
try:
|
|
589
|
+
result = await task
|
|
590
|
+
finally:
|
|
591
|
+
_active_subagent_tasks.discard(task)
|
|
592
|
+
if task.cancelled():
|
|
593
|
+
if get_use_dbos() and workflow_id:
|
|
594
|
+
DBOS.cancel_workflow(workflow_id)
|
|
589
595
|
|
|
590
596
|
# Extract the response from the result
|
|
591
597
|
response = result.output
|
|
592
598
|
|
|
593
|
-
# Display the response using non-streaming output
|
|
594
|
-
from code_puppy.agents.event_stream_handler import get_streaming_console
|
|
595
|
-
|
|
596
|
-
display_non_streamed_result(
|
|
597
|
-
content=response,
|
|
598
|
-
console=get_streaming_console(),
|
|
599
|
-
banner_text=f"\u2713 {agent_name.upper()} RESPONSE",
|
|
600
|
-
banner_name="subagent_response",
|
|
601
|
-
)
|
|
602
|
-
|
|
603
599
|
# Update the session history with the new messages from this interaction
|
|
604
600
|
# The result contains all_messages which includes the full conversation
|
|
605
601
|
updated_history = result.all_messages()
|
|
@@ -622,13 +618,23 @@ def register_invoke_agent(agent):
|
|
|
622
618
|
)
|
|
623
619
|
)
|
|
624
620
|
|
|
621
|
+
# Emit clean completion summary
|
|
622
|
+
emit_success(
|
|
623
|
+
f"✓ {agent_name} completed successfully", message_group=group_id
|
|
624
|
+
)
|
|
625
|
+
|
|
625
626
|
return AgentInvokeOutput(
|
|
626
627
|
response=response, agent_name=agent_name, session_id=session_id
|
|
627
628
|
)
|
|
628
629
|
|
|
629
|
-
except Exception:
|
|
630
|
+
except Exception as e:
|
|
631
|
+
# Emit clean failure summary
|
|
632
|
+
emit_error(f"✗ {agent_name} failed: {str(e)}", message_group=group_id)
|
|
633
|
+
|
|
634
|
+
# Full traceback for debugging
|
|
630
635
|
error_msg = f"Error invoking agent '{agent_name}': {traceback.format_exc()}"
|
|
631
636
|
emit_error(error_msg, message_group=group_id)
|
|
637
|
+
|
|
632
638
|
return AgentInvokeOutput(
|
|
633
639
|
response=None,
|
|
634
640
|
agent_name=agent_name,
|
|
@@ -8,7 +8,6 @@ from pydantic import BaseModel, Field
|
|
|
8
8
|
from pydantic_ai import Agent, BinaryContent
|
|
9
9
|
|
|
10
10
|
from code_puppy.config import get_use_dbos, get_vqa_model_name
|
|
11
|
-
from code_puppy.model_factory import ModelFactory
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class VisualAnalysisResult(BaseModel):
|
|
@@ -31,6 +30,7 @@ def _get_vqa_instructions() -> str:
|
|
|
31
30
|
@lru_cache(maxsize=1)
|
|
32
31
|
def _load_vqa_agent(model_name: str) -> Agent[None, VisualAnalysisResult]:
|
|
33
32
|
"""Create a cached agent instance for visual analysis."""
|
|
33
|
+
from code_puppy.model_factory import ModelFactory
|
|
34
34
|
from code_puppy.model_utils import prepare_prompt_for_model
|
|
35
35
|
|
|
36
36
|
models_config = ModelFactory.load_config()
|
code_puppy/tools/common.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import fnmatch
|
|
3
|
+
import functools
|
|
2
4
|
import hashlib
|
|
5
|
+
import logging
|
|
3
6
|
import os
|
|
4
7
|
import sys
|
|
5
8
|
import time
|
|
6
9
|
from pathlib import Path
|
|
7
|
-
from typing import Callable, Optional, Tuple
|
|
10
|
+
from typing import Any, Callable, Optional, Tuple
|
|
8
11
|
|
|
9
12
|
from prompt_toolkit import Application
|
|
10
13
|
from prompt_toolkit.formatted_text import HTML
|
|
@@ -1406,3 +1409,175 @@ def generate_group_id(tool_name: str, extra_context: str = "") -> str:
|
|
|
1406
1409
|
short_hash = hash_obj.hexdigest()[:8]
|
|
1407
1410
|
|
|
1408
1411
|
return f"{tool_name}_{short_hash}"
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
# =============================================================================
|
|
1415
|
+
# TOOL CALLBACK WRAPPER
|
|
1416
|
+
# =============================================================================
|
|
1417
|
+
|
|
1418
|
+
logger = logging.getLogger(__name__)
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def with_tool_callbacks(tool_name: str) -> Callable:
|
|
1422
|
+
"""Decorator that wraps tool functions with pre/post callback hooks.
|
|
1423
|
+
|
|
1424
|
+
This decorator enables plugins to hook into tool execution for:
|
|
1425
|
+
- Logging and analytics
|
|
1426
|
+
- Pre-execution validation or modification
|
|
1427
|
+
- Post-execution result processing
|
|
1428
|
+
- Performance monitoring
|
|
1429
|
+
|
|
1430
|
+
Args:
|
|
1431
|
+
tool_name: The name of the tool being wrapped (e.g., 'edit_file', 'list_files')
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
A decorator function that wraps the tool with callbacks.
|
|
1435
|
+
|
|
1436
|
+
Example:
|
|
1437
|
+
@with_tool_callbacks('my_tool')
|
|
1438
|
+
async def my_tool_impl(ctx, **kwargs):
|
|
1439
|
+
return result
|
|
1440
|
+
"""
|
|
1441
|
+
|
|
1442
|
+
def decorator(func: Callable) -> Callable:
|
|
1443
|
+
@functools.wraps(func)
|
|
1444
|
+
async def async_wrapper(*args, **kwargs) -> Any:
|
|
1445
|
+
# Extract context from args if available (usually first arg is RunContext)
|
|
1446
|
+
context = None
|
|
1447
|
+
tool_args = kwargs.copy()
|
|
1448
|
+
|
|
1449
|
+
# Try to get session context
|
|
1450
|
+
try:
|
|
1451
|
+
from code_puppy.messaging import get_session_context
|
|
1452
|
+
|
|
1453
|
+
context = get_session_context()
|
|
1454
|
+
except ImportError:
|
|
1455
|
+
pass
|
|
1456
|
+
|
|
1457
|
+
# Fire pre-tool callback (non-blocking)
|
|
1458
|
+
try:
|
|
1459
|
+
from code_puppy import callbacks
|
|
1460
|
+
|
|
1461
|
+
asyncio.create_task(
|
|
1462
|
+
callbacks.on_pre_tool_call(tool_name, tool_args, context)
|
|
1463
|
+
)
|
|
1464
|
+
except ImportError:
|
|
1465
|
+
logger.debug("callbacks module not available for pre_tool_call")
|
|
1466
|
+
except Exception as e:
|
|
1467
|
+
logger.debug(f"Error in pre_tool_call callback: {e}")
|
|
1468
|
+
|
|
1469
|
+
# Execute the tool and measure duration
|
|
1470
|
+
start_time = time.perf_counter()
|
|
1471
|
+
result = None
|
|
1472
|
+
error = None
|
|
1473
|
+
|
|
1474
|
+
try:
|
|
1475
|
+
result = await func(*args, **kwargs)
|
|
1476
|
+
return result
|
|
1477
|
+
except Exception as e:
|
|
1478
|
+
error = e
|
|
1479
|
+
raise
|
|
1480
|
+
finally:
|
|
1481
|
+
end_time = time.perf_counter()
|
|
1482
|
+
duration_ms = (end_time - start_time) * 1000
|
|
1483
|
+
|
|
1484
|
+
# Fire post-tool callback (non-blocking)
|
|
1485
|
+
final_result = result if error is None else {"error": str(error)}
|
|
1486
|
+
try:
|
|
1487
|
+
from code_puppy import callbacks
|
|
1488
|
+
|
|
1489
|
+
asyncio.create_task(
|
|
1490
|
+
callbacks.on_post_tool_call(
|
|
1491
|
+
tool_name, tool_args, final_result, duration_ms, context
|
|
1492
|
+
)
|
|
1493
|
+
)
|
|
1494
|
+
except ImportError:
|
|
1495
|
+
logger.debug("callbacks module not available for post_tool_call")
|
|
1496
|
+
except Exception as e:
|
|
1497
|
+
logger.debug(f"Error in post_tool_call callback: {e}")
|
|
1498
|
+
|
|
1499
|
+
@functools.wraps(func)
|
|
1500
|
+
def sync_wrapper(*args, **kwargs) -> Any:
|
|
1501
|
+
"""Sync wrapper for non-async tool functions."""
|
|
1502
|
+
# Extract context
|
|
1503
|
+
context = None
|
|
1504
|
+
tool_args = kwargs.copy()
|
|
1505
|
+
|
|
1506
|
+
try:
|
|
1507
|
+
from code_puppy.messaging import get_session_context
|
|
1508
|
+
|
|
1509
|
+
context = get_session_context()
|
|
1510
|
+
except ImportError:
|
|
1511
|
+
pass
|
|
1512
|
+
|
|
1513
|
+
# For sync functions, we can't use asyncio.create_task directly
|
|
1514
|
+
# Instead, we'll try to schedule it if there's a running loop
|
|
1515
|
+
def fire_pre_callback():
|
|
1516
|
+
try:
|
|
1517
|
+
from code_puppy import callbacks
|
|
1518
|
+
|
|
1519
|
+
loop = asyncio.get_running_loop()
|
|
1520
|
+
asyncio.run_coroutine_threadsafe(
|
|
1521
|
+
callbacks.on_pre_tool_call(tool_name, tool_args, context),
|
|
1522
|
+
loop,
|
|
1523
|
+
)
|
|
1524
|
+
except RuntimeError:
|
|
1525
|
+
# No running loop - skip async callback
|
|
1526
|
+
pass
|
|
1527
|
+
except ImportError:
|
|
1528
|
+
pass
|
|
1529
|
+
except Exception as e:
|
|
1530
|
+
logger.debug(f"Error in sync pre_tool_call: {e}")
|
|
1531
|
+
|
|
1532
|
+
fire_pre_callback()
|
|
1533
|
+
|
|
1534
|
+
# Execute the tool
|
|
1535
|
+
start_time = time.perf_counter()
|
|
1536
|
+
result = None
|
|
1537
|
+
error = None
|
|
1538
|
+
|
|
1539
|
+
try:
|
|
1540
|
+
result = func(*args, **kwargs)
|
|
1541
|
+
return result
|
|
1542
|
+
except Exception as e:
|
|
1543
|
+
error = e
|
|
1544
|
+
raise
|
|
1545
|
+
finally:
|
|
1546
|
+
end_time = time.perf_counter()
|
|
1547
|
+
duration_ms = (end_time - start_time) * 1000
|
|
1548
|
+
|
|
1549
|
+
# Fire post-tool callback
|
|
1550
|
+
final_result = result if error is None else {"error": str(error)}
|
|
1551
|
+
|
|
1552
|
+
def fire_post_callback():
|
|
1553
|
+
try:
|
|
1554
|
+
from code_puppy import callbacks
|
|
1555
|
+
|
|
1556
|
+
loop = asyncio.get_running_loop()
|
|
1557
|
+
asyncio.run_coroutine_threadsafe(
|
|
1558
|
+
callbacks.on_post_tool_call(
|
|
1559
|
+
tool_name,
|
|
1560
|
+
tool_args,
|
|
1561
|
+
final_result,
|
|
1562
|
+
duration_ms,
|
|
1563
|
+
context,
|
|
1564
|
+
),
|
|
1565
|
+
loop,
|
|
1566
|
+
)
|
|
1567
|
+
except RuntimeError:
|
|
1568
|
+
# No running loop - skip async callback
|
|
1569
|
+
pass
|
|
1570
|
+
except ImportError:
|
|
1571
|
+
pass
|
|
1572
|
+
except Exception as e:
|
|
1573
|
+
logger.debug(f"Error in sync post_tool_call: {e}")
|
|
1574
|
+
|
|
1575
|
+
fire_post_callback()
|
|
1576
|
+
|
|
1577
|
+
# Return appropriate wrapper based on function type
|
|
1578
|
+
if asyncio.iscoroutinefunction(func):
|
|
1579
|
+
return async_wrapper
|
|
1580
|
+
else:
|
|
1581
|
+
return sync_wrapper
|
|
1582
|
+
|
|
1583
|
+
return decorator
|
code_puppy/tools/display.py
CHANGED
|
@@ -8,7 +8,8 @@ from typing import Optional
|
|
|
8
8
|
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
|
|
11
|
-
from code_puppy.config import get_banner_color
|
|
11
|
+
from code_puppy.config import get_banner_color, get_subagent_verbose
|
|
12
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def display_non_streamed_result(
|
|
@@ -33,6 +34,10 @@ def display_non_streamed_result(
|
|
|
33
34
|
>>> display_non_streamed_result("# Hello\n\nThis is **bold** text.")
|
|
34
35
|
# Renders with AGENT RESPONSE banner and formatted markdown
|
|
35
36
|
"""
|
|
37
|
+
# Skip display for sub-agents unless verbose mode
|
|
38
|
+
if is_subagent() and not get_subagent_verbose():
|
|
39
|
+
return
|
|
40
|
+
|
|
36
41
|
import time
|
|
37
42
|
|
|
38
43
|
from rich.text import Text
|