code-puppy 0.0.341__py3-none-any.whl → 0.0.361__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 (86) hide show
  1. code_puppy/agents/__init__.py +2 -0
  2. code_puppy/agents/agent_manager.py +49 -0
  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 +34 -252
  7. code_puppy/agents/event_stream_handler.py +350 -0
  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/claude_cache_client.py +249 -34
  29. code_puppy/cli_runner.py +4 -3
  30. code_puppy/command_line/add_model_menu.py +8 -9
  31. code_puppy/command_line/core_commands.py +85 -0
  32. code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
  33. code_puppy/command_line/mcp/custom_server_form.py +54 -19
  34. code_puppy/command_line/mcp/custom_server_installer.py +8 -9
  35. code_puppy/command_line/mcp/handler.py +0 -2
  36. code_puppy/command_line/mcp/help_command.py +1 -5
  37. code_puppy/command_line/mcp/start_command.py +36 -18
  38. code_puppy/command_line/onboarding_slides.py +0 -1
  39. code_puppy/command_line/prompt_toolkit_completion.py +16 -10
  40. code_puppy/command_line/utils.py +54 -0
  41. code_puppy/config.py +66 -62
  42. code_puppy/mcp_/async_lifecycle.py +35 -4
  43. code_puppy/mcp_/managed_server.py +49 -20
  44. code_puppy/mcp_/manager.py +81 -52
  45. code_puppy/messaging/__init__.py +15 -0
  46. code_puppy/messaging/message_queue.py +11 -23
  47. code_puppy/messaging/messages.py +27 -0
  48. code_puppy/messaging/queue_console.py +1 -1
  49. code_puppy/messaging/rich_renderer.py +36 -1
  50. code_puppy/messaging/spinner/__init__.py +20 -2
  51. code_puppy/messaging/subagent_console.py +461 -0
  52. code_puppy/model_utils.py +54 -0
  53. code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
  54. code_puppy/plugins/antigravity_oauth/transport.py +1 -0
  55. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  56. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  57. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  58. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  59. code_puppy/status_display.py +6 -2
  60. code_puppy/tools/__init__.py +37 -1
  61. code_puppy/tools/agent_tools.py +139 -36
  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_navigation.py +7 -7
  67. code_puppy/tools/browser/browser_screenshot.py +78 -140
  68. code_puppy/tools/browser/browser_scripts.py +15 -13
  69. code_puppy/tools/browser/camoufox_manager.py +226 -64
  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.341.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
  79. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/RECORD +84 -53
  80. code_puppy/command_line/mcp/add_command.py +0 -170
  81. code_puppy/tools/browser/vqa_agent.py +0 -90
  82. {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
  83. {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
  84. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
  85. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
  86. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,361 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🐶 Code Puppy Terminal</title>
7
+
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- xterm.js CSS -->
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
13
+
14
+ <!-- xterm.js -->
15
+ <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
17
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
18
+
19
+ <style>
20
+ /* Custom terminal styling */
21
+ .xterm {
22
+ padding: 8px;
23
+ }
24
+
25
+ .xterm-viewport::-webkit-scrollbar {
26
+ width: 8px;
27
+ }
28
+
29
+ .xterm-viewport::-webkit-scrollbar-track {
30
+ background: #1e1e1e;
31
+ }
32
+
33
+ .xterm-viewport::-webkit-scrollbar-thumb {
34
+ background: #4a4a4a;
35
+ border-radius: 4px;
36
+ }
37
+
38
+ .xterm-viewport::-webkit-scrollbar-thumb:hover {
39
+ background: #5a5a5a;
40
+ }
41
+
42
+ .status-indicator {
43
+ width: 10px;
44
+ height: 10px;
45
+ border-radius: 50%;
46
+ animation: pulse 2s infinite;
47
+ }
48
+
49
+ .status-connected {
50
+ background-color: #22c55e;
51
+ box-shadow: 0 0 8px #22c55e;
52
+ }
53
+
54
+ .status-disconnected {
55
+ background-color: #ef4444;
56
+ box-shadow: 0 0 8px #ef4444;
57
+ animation: none;
58
+ }
59
+
60
+ .status-connecting {
61
+ background-color: #f59e0b;
62
+ box-shadow: 0 0 8px #f59e0b;
63
+ }
64
+
65
+ @keyframes pulse {
66
+ 0%, 100% { opacity: 1; }
67
+ 50% { opacity: 0.5; }
68
+ }
69
+ </style>
70
+ </head>
71
+ <body class="h-full bg-gray-900 text-white overflow-hidden">
72
+ <div class="h-full flex flex-col">
73
+ <!-- Header -->
74
+ <header class="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between">
75
+ <div class="flex items-center space-x-3">
76
+ <span class="text-2xl">🐶</span>
77
+ <h1 class="text-lg font-semibold text-gray-100">Code Puppy Terminal</h1>
78
+ </div>
79
+
80
+ <div class="flex items-center space-x-4">
81
+ <!-- Connection Status -->
82
+ <div class="flex items-center space-x-2">
83
+ <div id="status-indicator" class="status-indicator status-disconnected"></div>
84
+ <span id="status-text" class="text-sm text-gray-400">Disconnected</span>
85
+ </div>
86
+
87
+ <!-- Session ID -->
88
+ <div class="text-sm text-gray-500">
89
+ Session: <span id="session-id" class="font-mono text-gray-400">-</span>
90
+ </div>
91
+
92
+ <!-- Controls -->
93
+ <div class="flex items-center space-x-2">
94
+ <button id="btn-reconnect"
95
+ class="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
96
+ disabled>
97
+ Reconnect
98
+ </button>
99
+ <button id="btn-clear"
100
+ class="px-3 py-1 text-sm bg-gray-600 hover:bg-gray-700 rounded transition-colors">
101
+ Clear
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </header>
106
+
107
+ <!-- Terminal Container -->
108
+ <main class="flex-1 bg-black p-2">
109
+ <div id="terminal" class="h-full w-full"></div>
110
+ </main>
111
+
112
+ <!-- Footer -->
113
+ <footer class="bg-gray-800 border-t border-gray-700 px-4 py-1 flex items-center justify-between text-xs text-gray-500">
114
+ <div>
115
+ <span id="terminal-size">80x24</span>
116
+ </div>
117
+ <div class="flex items-center space-x-4">
118
+ <span>Ctrl+C to interrupt</span>
119
+ <span>•</span>
120
+ <span>Ctrl+D to exit</span>
121
+ </div>
122
+ </footer>
123
+ </div>
124
+
125
+ <script>
126
+ // Terminal configuration
127
+ const CONFIG = {
128
+ wsUrl: `ws://${window.location.host}/ws/terminal`,
129
+ sessionId: new URLSearchParams(window.location.search).get('session') || 'default',
130
+ theme: {
131
+ background: '#1e1e1e',
132
+ foreground: '#d4d4d4',
133
+ cursor: '#aeafad',
134
+ cursorAccent: '#1e1e1e',
135
+ selection: 'rgba(255, 255, 255, 0.3)',
136
+ black: '#000000',
137
+ red: '#cd3131',
138
+ green: '#0dbc79',
139
+ yellow: '#e5e510',
140
+ blue: '#2472c8',
141
+ magenta: '#bc3fbc',
142
+ cyan: '#11a8cd',
143
+ white: '#e5e5e5',
144
+ brightBlack: '#666666',
145
+ brightRed: '#f14c4c',
146
+ brightGreen: '#23d18b',
147
+ brightYellow: '#f5f543',
148
+ brightBlue: '#3b8eea',
149
+ brightMagenta: '#d670d6',
150
+ brightCyan: '#29b8db',
151
+ brightWhite: '#ffffff'
152
+ }
153
+ };
154
+
155
+ // State
156
+ let terminal = null;
157
+ let fitAddon = null;
158
+ let socket = null;
159
+ let reconnectAttempts = 0;
160
+ const MAX_RECONNECT_ATTEMPTS = 5;
161
+
162
+ // DOM Elements
163
+ const statusIndicator = document.getElementById('status-indicator');
164
+ const statusText = document.getElementById('status-text');
165
+ const sessionIdEl = document.getElementById('session-id');
166
+ const terminalSizeEl = document.getElementById('terminal-size');
167
+ const btnReconnect = document.getElementById('btn-reconnect');
168
+ const btnClear = document.getElementById('btn-clear');
169
+
170
+ // Initialize terminal
171
+ function initTerminal() {
172
+ terminal = new Terminal({
173
+ theme: CONFIG.theme,
174
+ fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", Menlo, Monaco, monospace',
175
+ fontSize: 14,
176
+ lineHeight: 1.2,
177
+ cursorBlink: true,
178
+ cursorStyle: 'block',
179
+ scrollback: 10000,
180
+ convertEol: true,
181
+ allowTransparency: true
182
+ });
183
+
184
+ // Load addons
185
+ fitAddon = new FitAddon.FitAddon();
186
+ const webLinksAddon = new WebLinksAddon.WebLinksAddon();
187
+
188
+ terminal.loadAddon(fitAddon);
189
+ terminal.loadAddon(webLinksAddon);
190
+
191
+ // Open terminal in container
192
+ const container = document.getElementById('terminal');
193
+ terminal.open(container);
194
+
195
+ // Fit to container
196
+ fitAddon.fit();
197
+ updateTerminalSize();
198
+
199
+ // Handle user input
200
+ terminal.onData(data => {
201
+ if (socket && socket.readyState === WebSocket.OPEN) {
202
+ socket.send(JSON.stringify({
203
+ type: 'input',
204
+ data: data
205
+ }));
206
+ }
207
+ });
208
+
209
+ // Handle resize
210
+ terminal.onResize(({ cols, rows }) => {
211
+ updateTerminalSize();
212
+ if (socket && socket.readyState === WebSocket.OPEN) {
213
+ socket.send(JSON.stringify({
214
+ type: 'resize',
215
+ cols: cols,
216
+ rows: rows
217
+ }));
218
+ }
219
+ });
220
+
221
+ // Welcome message
222
+ terminal.writeln('\x1b[1;36m🐶 Welcome to Code Puppy Terminal!\x1b[0m');
223
+ terminal.writeln('\x1b[90mConnecting to server...\x1b[0m');
224
+ terminal.writeln('');
225
+
226
+ // Connect to WebSocket
227
+ connect();
228
+
229
+ // Update session ID display
230
+ sessionIdEl.textContent = CONFIG.sessionId;
231
+ }
232
+
233
+ // Update terminal size display
234
+ function updateTerminalSize() {
235
+ if (terminal) {
236
+ terminalSizeEl.textContent = `${terminal.cols}x${terminal.rows}`;
237
+ }
238
+ }
239
+
240
+ // Update connection status
241
+ function setStatus(status) {
242
+ statusIndicator.className = 'status-indicator';
243
+
244
+ switch (status) {
245
+ case 'connected':
246
+ statusIndicator.classList.add('status-connected');
247
+ statusText.textContent = 'Connected';
248
+ btnReconnect.disabled = true;
249
+ break;
250
+ case 'disconnected':
251
+ statusIndicator.classList.add('status-disconnected');
252
+ statusText.textContent = 'Disconnected';
253
+ btnReconnect.disabled = false;
254
+ break;
255
+ case 'connecting':
256
+ statusIndicator.classList.add('status-connecting');
257
+ statusText.textContent = 'Connecting...';
258
+ btnReconnect.disabled = true;
259
+ break;
260
+ }
261
+ }
262
+
263
+ // Connect to WebSocket
264
+ function connect() {
265
+ if (socket && socket.readyState === WebSocket.OPEN) {
266
+ return;
267
+ }
268
+
269
+ setStatus('connecting');
270
+
271
+ const url = CONFIG.wsUrl;
272
+ socket = new WebSocket(url);
273
+
274
+ socket.onopen = () => {
275
+ setStatus('connected');
276
+ reconnectAttempts = 0;
277
+ terminal.writeln('\x1b[1;32m✓ Connected to server\x1b[0m');
278
+ terminal.writeln('');
279
+
280
+ // Send initial resize
281
+ socket.send(JSON.stringify({
282
+ type: 'resize',
283
+ cols: terminal.cols,
284
+ rows: terminal.rows
285
+ }));
286
+ };
287
+
288
+ socket.onmessage = (event) => {
289
+ try {
290
+ const message = JSON.parse(event.data);
291
+
292
+ switch (message.type) {
293
+ case 'output':
294
+ terminal.write(new TextDecoder().decode(Uint8Array.from(atob(message.data), c => c.charCodeAt(0))));
295
+ break;
296
+ case 'error':
297
+ terminal.writeln(`\x1b[1;31mError: ${message.message}\x1b[0m`);
298
+ break;
299
+ case 'session_started':
300
+ terminal.writeln(`\x1b[90mSession started: ${message.session_id}\x1b[0m`);
301
+ break;
302
+ }
303
+ } catch (e) {
304
+ // Plain text output
305
+ terminal.write(event.data);
306
+ }
307
+ };
308
+
309
+ socket.onclose = (event) => {
310
+ setStatus('disconnected');
311
+
312
+ if (!event.wasClean) {
313
+ terminal.writeln('');
314
+ terminal.writeln('\x1b[1;31m✗ Connection lost\x1b[0m');
315
+
316
+ // Auto-reconnect with backoff
317
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
318
+ reconnectAttempts++;
319
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
320
+ terminal.writeln(`\x1b[90mReconnecting in ${delay/1000}s (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...\x1b[0m`);
321
+ setTimeout(connect, delay);
322
+ } else {
323
+ terminal.writeln('\x1b[90mMax reconnection attempts reached. Click "Reconnect" to try again.\x1b[0m');
324
+ }
325
+ }
326
+ };
327
+
328
+ socket.onerror = (error) => {
329
+ console.error('WebSocket error:', error);
330
+ };
331
+ }
332
+
333
+ // Event listeners
334
+ btnReconnect.addEventListener('click', () => {
335
+ reconnectAttempts = 0;
336
+ connect();
337
+ });
338
+
339
+ btnClear.addEventListener('click', () => {
340
+ terminal.clear();
341
+ });
342
+
343
+ // Handle window resize
344
+ window.addEventListener('resize', () => {
345
+ if (fitAddon) {
346
+ fitAddon.fit();
347
+ }
348
+ });
349
+
350
+ // Handle page unload
351
+ window.addEventListener('beforeunload', () => {
352
+ if (socket) {
353
+ socket.close();
354
+ }
355
+ });
356
+
357
+ // Initialize on DOM ready
358
+ document.addEventListener('DOMContentLoaded', initTerminal);
359
+ </script>
360
+ </body>
361
+ </html>
@@ -0,0 +1,154 @@
1
+ """WebSocket endpoints for Code Puppy API.
2
+
3
+ Provides real-time communication channels:
4
+ - /ws/events - Server-sent events stream
5
+ - /ws/terminal - Interactive PTY terminal sessions
6
+ - /ws/health - Simple health check endpoint
7
+ """
8
+
9
+ import asyncio
10
+ import base64
11
+ import logging
12
+ import uuid
13
+
14
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def setup_websocket(app: FastAPI) -> None:
20
+ """Setup WebSocket endpoints for the application."""
21
+
22
+ @app.websocket("/ws/events")
23
+ async def websocket_events(websocket: WebSocket) -> None:
24
+ """Stream real-time events to connected clients."""
25
+ await websocket.accept()
26
+ logger.info("Events WebSocket client connected")
27
+
28
+ from code_puppy.plugins.frontend_emitter.emitter import (
29
+ get_recent_events,
30
+ subscribe,
31
+ unsubscribe,
32
+ )
33
+
34
+ event_queue = subscribe()
35
+
36
+ try:
37
+ recent_events = get_recent_events()
38
+ for event in recent_events:
39
+ await websocket.send_json(event)
40
+
41
+ while True:
42
+ try:
43
+ event = await asyncio.wait_for(event_queue.get(), timeout=30.0)
44
+ await websocket.send_json(event)
45
+ except asyncio.TimeoutError:
46
+ try:
47
+ await websocket.send_json({"type": "ping"})
48
+ except Exception:
49
+ break
50
+ except WebSocketDisconnect:
51
+ logger.info("Events WebSocket client disconnected")
52
+ except Exception as e:
53
+ logger.error(f"Events WebSocket error: {e}")
54
+ finally:
55
+ unsubscribe(event_queue)
56
+
57
+ @app.websocket("/ws/terminal")
58
+ async def websocket_terminal(websocket: WebSocket) -> None:
59
+ """Interactive terminal WebSocket endpoint."""
60
+ await websocket.accept()
61
+ logger.info("Terminal WebSocket client connected")
62
+
63
+ from code_puppy.api.pty_manager import get_pty_manager
64
+
65
+ manager = get_pty_manager()
66
+ session_id = str(uuid.uuid4())[:8]
67
+ session = None
68
+
69
+ # Get the current event loop for thread-safe scheduling
70
+ loop = asyncio.get_running_loop()
71
+
72
+ # Queue to receive PTY output in a thread-safe way
73
+ output_queue: asyncio.Queue[bytes] = asyncio.Queue()
74
+
75
+ # Output callback - called from thread pool, puts data in queue
76
+ def on_output(data: bytes) -> None:
77
+ try:
78
+ loop.call_soon_threadsafe(output_queue.put_nowait, data)
79
+ except Exception as e:
80
+ logger.error(f"on_output error: {e}")
81
+
82
+ async def output_sender() -> None:
83
+ """Coroutine that sends queued output to WebSocket."""
84
+ try:
85
+ while True:
86
+ data = await output_queue.get()
87
+ await websocket.send_json(
88
+ {
89
+ "type": "output",
90
+ "data": base64.b64encode(data).decode("ascii"),
91
+ }
92
+ )
93
+ except asyncio.CancelledError:
94
+ pass
95
+ except Exception as e:
96
+ logger.error(f"output_sender error: {e}")
97
+
98
+ sender_task = None
99
+
100
+ try:
101
+ # Create PTY session
102
+ session = await manager.create_session(
103
+ session_id=session_id,
104
+ on_output=on_output,
105
+ )
106
+
107
+ # Send session info
108
+ await websocket.send_json({"type": "session", "id": session_id})
109
+
110
+ # Start output sender task
111
+ sender_task = asyncio.create_task(output_sender())
112
+
113
+ # Handle incoming messages
114
+ while True:
115
+ try:
116
+ msg = await websocket.receive_json()
117
+
118
+ if msg.get("type") == "input":
119
+ data = msg.get("data", "")
120
+ if isinstance(data, str):
121
+ data = data.encode("utf-8")
122
+ await manager.write(session_id, data)
123
+ elif msg.get("type") == "resize":
124
+ cols = msg.get("cols", 80)
125
+ rows = msg.get("rows", 24)
126
+ await manager.resize(session_id, cols, rows)
127
+ except WebSocketDisconnect:
128
+ break
129
+ except Exception as e:
130
+ logger.error(f"Terminal WebSocket error: {e}")
131
+ break
132
+ except Exception as e:
133
+ logger.error(f"Terminal session error: {e}")
134
+ finally:
135
+ if sender_task:
136
+ sender_task.cancel()
137
+ try:
138
+ await sender_task
139
+ except asyncio.CancelledError:
140
+ pass
141
+ if session:
142
+ await manager.close_session(session_id)
143
+ logger.info("Terminal WebSocket disconnected")
144
+
145
+ @app.websocket("/ws/health")
146
+ async def websocket_health(websocket: WebSocket) -> None:
147
+ """Simple WebSocket health check - echoes messages back."""
148
+ await websocket.accept()
149
+ try:
150
+ while True:
151
+ data = await websocket.receive_text()
152
+ await websocket.send_text(f"echo: {data}")
153
+ except WebSocketDisconnect:
154
+ pass
code_puppy/callbacks.py CHANGED
@@ -18,6 +18,9 @@ PhaseType = Literal[
18
18
  "custom_command",
19
19
  "custom_command_help",
20
20
  "file_permission",
21
+ "pre_tool_call",
22
+ "post_tool_call",
23
+ "stream_event",
21
24
  ]
22
25
  CallbackFunc = Callable[..., Any]
23
26
 
@@ -36,6 +39,9 @@ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
36
39
  "custom_command": [],
37
40
  "custom_command_help": [],
38
41
  "file_permission": [],
42
+ "pre_tool_call": [],
43
+ "post_tool_call": [],
44
+ "stream_event": [],
39
45
  }
40
46
 
41
47
  logger = logging.getLogger(__name__)
@@ -271,3 +277,70 @@ def on_file_permission(
271
277
  message_group,
272
278
  operation_data,
273
279
  )
280
+
281
+
282
+ async def on_pre_tool_call(
283
+ tool_name: str, tool_args: dict, context: Any = None
284
+ ) -> List[Any]:
285
+ """Trigger callbacks before a tool is called.
286
+
287
+ This allows plugins to inspect, modify, or log tool calls before
288
+ they are executed.
289
+
290
+ Args:
291
+ tool_name: Name of the tool being called
292
+ tool_args: Arguments being passed to the tool
293
+ context: Optional context data for the tool call
294
+
295
+ Returns:
296
+ List of results from registered callbacks.
297
+ """
298
+ return await _trigger_callbacks("pre_tool_call", tool_name, tool_args, context)
299
+
300
+
301
+ async def on_post_tool_call(
302
+ tool_name: str,
303
+ tool_args: dict,
304
+ result: Any,
305
+ duration_ms: float,
306
+ context: Any = None,
307
+ ) -> List[Any]:
308
+ """Trigger callbacks after a tool completes.
309
+
310
+ This allows plugins to inspect tool results, log execution times,
311
+ or perform post-processing.
312
+
313
+ Args:
314
+ tool_name: Name of the tool that was called
315
+ tool_args: Arguments that were passed to the tool
316
+ result: The result returned by the tool
317
+ duration_ms: Execution time in milliseconds
318
+ context: Optional context data for the tool call
319
+
320
+ Returns:
321
+ List of results from registered callbacks.
322
+ """
323
+ return await _trigger_callbacks(
324
+ "post_tool_call", tool_name, tool_args, result, duration_ms, context
325
+ )
326
+
327
+
328
+ async def on_stream_event(
329
+ event_type: str, event_data: Any, agent_session_id: str | None = None
330
+ ) -> List[Any]:
331
+ """Trigger callbacks for streaming events.
332
+
333
+ This allows plugins to react to streaming events in real-time,
334
+ such as tokens being generated, tool calls starting, etc.
335
+
336
+ Args:
337
+ event_type: Type of the streaming event
338
+ event_data: Data associated with the event
339
+ agent_session_id: Optional session ID of the agent emitting the event
340
+
341
+ Returns:
342
+ List of results from registered callbacks.
343
+ """
344
+ return await _trigger_callbacks(
345
+ "stream_event", event_type, event_data, agent_session_id
346
+ )