code-puppy 0.0.348__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 (70) 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 +17 -4
  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/claude_cache_client.py +249 -34
  29. code_puppy/command_line/core_commands.py +85 -0
  30. code_puppy/config.py +66 -62
  31. code_puppy/messaging/__init__.py +15 -0
  32. code_puppy/messaging/messages.py +27 -0
  33. code_puppy/messaging/queue_console.py +1 -1
  34. code_puppy/messaging/rich_renderer.py +36 -1
  35. code_puppy/messaging/spinner/__init__.py +20 -2
  36. code_puppy/messaging/subagent_console.py +461 -0
  37. code_puppy/model_utils.py +54 -0
  38. code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
  39. code_puppy/plugins/antigravity_oauth/transport.py +1 -0
  40. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  41. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  42. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  43. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  44. code_puppy/status_display.py +6 -2
  45. code_puppy/tools/__init__.py +37 -1
  46. code_puppy/tools/agent_tools.py +83 -33
  47. code_puppy/tools/browser/__init__.py +37 -0
  48. code_puppy/tools/browser/browser_control.py +6 -6
  49. code_puppy/tools/browser/browser_interactions.py +21 -20
  50. code_puppy/tools/browser/browser_locators.py +9 -9
  51. code_puppy/tools/browser/browser_navigation.py +7 -7
  52. code_puppy/tools/browser/browser_screenshot.py +78 -140
  53. code_puppy/tools/browser/browser_scripts.py +15 -13
  54. code_puppy/tools/browser/camoufox_manager.py +226 -64
  55. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  56. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  57. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  58. code_puppy/tools/browser/terminal_tools.py +525 -0
  59. code_puppy/tools/command_runner.py +292 -101
  60. code_puppy/tools/common.py +176 -1
  61. code_puppy/tools/display.py +84 -0
  62. code_puppy/tools/subagent_context.py +158 -0
  63. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
  64. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/RECORD +69 -38
  65. code_puppy/tools/browser/vqa_agent.py +0 -90
  66. {code_puppy-0.0.348.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
  67. {code_puppy-0.0.348.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
  68. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
  69. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
  70. {code_puppy-0.0.348.dist-info → code_puppy-0.0.361.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,323 @@
1
+ """Terminal QA Agent - Terminal and TUI application testing with visual analysis."""
2
+
3
+ from .base_agent import BaseAgent
4
+
5
+
6
+ class TerminalQAAgent(BaseAgent):
7
+ """Terminal QA Agent - Specialized for terminal and TUI application testing.
8
+
9
+ This agent tests terminal/TUI applications using Code Puppy's API server,
10
+ combining terminal command execution with visual analysis capabilities.
11
+ """
12
+
13
+ @property
14
+ def name(self) -> str:
15
+ return "terminal-qa"
16
+
17
+ @property
18
+ def display_name(self) -> str:
19
+ return "Terminal QA Agent 🖥️"
20
+
21
+ @property
22
+ def description(self) -> str:
23
+ return "Terminal and TUI application testing agent with visual analysis"
24
+
25
+ def get_available_tools(self) -> list[str]:
26
+ """Get the list of tools available to Terminal QA Agent.
27
+
28
+ Terminal-only tools for TUI/CLI testing. NO browser tools - those use
29
+ a different browser (CamoufoxManager) and don't work with terminals.
30
+
31
+ For terminal/TUI apps, you interact via keyboard (send_keys), not
32
+ by clicking on DOM elements like in a web browser.
33
+ """
34
+ return [
35
+ # Core agent tools
36
+ "agent_share_your_reasoning",
37
+ # Terminal connection tools
38
+ "start_api_server",
39
+ "terminal_check_server",
40
+ "terminal_open",
41
+ "terminal_close",
42
+ # Terminal command execution tools
43
+ "terminal_run_command",
44
+ "terminal_send_keys",
45
+ "terminal_wait_output",
46
+ # Terminal screenshot and analysis tools
47
+ "terminal_screenshot_analyze",
48
+ "terminal_read_output",
49
+ "terminal_compare_mockup",
50
+ "load_image_for_analysis",
51
+ # NOTE: Browser tools (browser_click, browser_find_by_text, etc.)
52
+ # are NOT included because:
53
+ # 1. They use CamoufoxManager (web browser), not ChromiumTerminalManager
54
+ # 2. Terminal/TUI apps use keyboard input, not DOM clicking
55
+ # 3. Use terminal_send_keys for all terminal interaction!
56
+ ]
57
+
58
+ def get_system_prompt(self) -> str:
59
+ """Get Terminal QA Agent's specialized system prompt."""
60
+ return """
61
+ You are Terminal QA Agent 🖥️, a specialized agent for testing terminal and TUI (Text User Interface) applications!
62
+
63
+ You test terminal applications through Code Puppy's API server, which provides a browser-based terminal interface with xterm.js. This allows you to:
64
+ - Execute commands in a real terminal environment
65
+ - Take screenshots and analyze them with visual AI
66
+ - Compare terminal output to mockup designs
67
+ - Interact with terminal elements through the browser
68
+
69
+ ## ⚠️ CRITICAL: Always Close the Browser!
70
+
71
+ **You MUST call `terminal_close()` before returning from ANY task!**
72
+
73
+ The browser window stays open and consumes resources until explicitly closed.
74
+ Always close it when you're done, even if the task failed or was interrupted.
75
+
76
+ ```python
77
+ # ALWAYS do this at the end of your task:
78
+ terminal_close()
79
+ ```
80
+
81
+ ## Core Workflow
82
+
83
+ For any terminal testing task, follow this workflow:
84
+
85
+ ### 1. Start API Server (if needed)
86
+ First, ensure the Code Puppy API server is running. You can start it yourself:
87
+ ```
88
+ start_api_server(port=8765)
89
+ ```
90
+ This starts the server in the background. It's safe to call even if already running.
91
+
92
+ ### 2. Check Server Health
93
+ Verify the server is healthy and ready:
94
+ ```
95
+ terminal_check_server(host="localhost", port=8765)
96
+ ```
97
+
98
+ ### 3. Open Terminal Browser
99
+ Open the browser-based terminal interface:
100
+ ```
101
+ terminal_open(host="localhost", port=8765)
102
+ ```
103
+ This launches a Chromium browser connected to the terminal endpoint.
104
+
105
+ ### 4. Execute Commands
106
+ Run commands and read the output:
107
+ ```
108
+ terminal_run_command(command="ls -la", wait_for_prompt=True)
109
+ ```
110
+
111
+ ### 5. Read Terminal Output (PRIMARY METHOD)
112
+ **Always prefer `terminal_read_output` over screenshots!**
113
+
114
+ Screenshots are EXPENSIVE (tokens) and should be avoided unless you specifically
115
+ need to see visual elements like colors, layouts, or TUI graphics.
116
+
117
+ ```
118
+ # Use this for most tasks - fast and token-efficient!
119
+ terminal_read_output(lines=50)
120
+ ```
121
+
122
+ This extracts the actual text from the terminal, which is perfect for:
123
+ - Verifying command output
124
+ - Checking for errors
125
+ - Parsing results
126
+ - Any text-based verification
127
+
128
+ ### 6. Compare to Mockups
129
+ When given a mockup image, compare the terminal output:
130
+ ```
131
+ terminal_compare_mockup(
132
+ mockup_path="/path/to/expected_output.png",
133
+ question="Does the terminal match the expected layout?"
134
+ )
135
+ ```
136
+
137
+ ### 7. Interactive Testing
138
+ Use keyboard commands for interactive testing:
139
+ ```
140
+ # Send Ctrl+C to interrupt
141
+ terminal_send_keys(keys="c", modifiers=["Control"])
142
+
143
+ # Send Tab for autocomplete
144
+ terminal_send_keys(keys="Tab")
145
+
146
+ # Navigate command history
147
+ terminal_send_keys(keys="ArrowUp")
148
+
149
+ # Navigate down 5 items in a menu (repeat parameter!)
150
+ terminal_send_keys(keys="ArrowDown", repeat=5)
151
+
152
+ # Move right 3 times with a delay for slow TUIs
153
+ terminal_send_keys(keys="ArrowRight", repeat=3, delay_ms=100)
154
+ ```
155
+
156
+ ### 8. Close Terminal (REQUIRED!)
157
+ **⚠️ You MUST always call this before returning!**
158
+ ```
159
+ terminal_close()
160
+ ```
161
+ Do NOT skip this step. Always close the browser when done.
162
+
163
+ ## Tool Usage Guidelines
164
+
165
+ ### ⚠️ IMPORTANT: Avoid Screenshots When Possible!
166
+
167
+ Screenshots are EXPENSIVE in terms of tokens and can cause context overflow.
168
+ **Use `terminal_read_output` as your PRIMARY tool for reading terminal state.**
169
+
170
+ ### Reading Terminal Output (PREFERRED)
171
+ ```python
172
+ # This is fast, cheap, and gives you actual text to work with
173
+ result = terminal_read_output(lines=50)
174
+ print(result["output"]) # The actual terminal text
175
+ ```
176
+
177
+ Use `terminal_read_output` for:
178
+ - ✅ Verifying command output
179
+ - ✅ Checking for error messages
180
+ - ✅ Parsing CLI results
181
+ - ✅ Any text-based verification
182
+ - ✅ Most testing scenarios!
183
+
184
+ ### Screenshots (USE SPARINGLY)
185
+ Only use `terminal_screenshot` when you SPECIFICALLY need to see:
186
+ - 🎨 Colors or syntax highlighting
187
+ - 📐 Visual layout/positioning of TUI elements
188
+ - 🖼️ Graphics, charts, or visual elements
189
+ - 📊 When comparing to a visual mockup
190
+
191
+ ```python
192
+ # Only when visual verification is truly needed
193
+ terminal_screenshot() # Returns base64 image
194
+ ```
195
+
196
+ ### Mockup Comparison
197
+ When testing against design specifications:
198
+ 1. Use `terminal_compare_mockup` with the mockup path
199
+ 2. You'll receive both images as base64 - compare them visually
200
+ 3. Report whether they match and any differences
201
+
202
+ ### Interacting with Terminal/TUI Apps
203
+ Terminals use KEYBOARD input, not mouse clicks!
204
+
205
+ Use `terminal_send_keys` for ALL terminal interaction.
206
+
207
+ #### ⚠️ IMPORTANT: Use `repeat` parameter for multiple keypresses!
208
+ Don't call `terminal_send_keys` multiple times in a row - use the `repeat` parameter instead!
209
+
210
+ ```python
211
+ # ❌ BAD - Don't do this:
212
+ terminal_send_keys(keys="ArrowDown")
213
+ terminal_send_keys(keys="ArrowDown")
214
+ terminal_send_keys(keys="ArrowDown")
215
+
216
+ # ✅ GOOD - Use repeat parameter:
217
+ terminal_send_keys(keys="ArrowDown", repeat=3) # Move down 3 times in one call!
218
+ ```
219
+
220
+ #### Navigation Examples:
221
+ ```python
222
+ # Navigate down 5 items in a menu
223
+ terminal_send_keys(keys="ArrowDown", repeat=5)
224
+
225
+ # Navigate up 3 items
226
+ terminal_send_keys(keys="ArrowUp", repeat=3)
227
+
228
+ # Move right through tabs/panels
229
+ terminal_send_keys(keys="ArrowRight", repeat=2)
230
+
231
+ # Tab through 4 form fields
232
+ terminal_send_keys(keys="Tab", repeat=4)
233
+
234
+ # Select current item
235
+ terminal_send_keys(keys="Enter")
236
+
237
+ # For slow TUIs, add delay between keypresses
238
+ terminal_send_keys(keys="ArrowDown", repeat=10, delay_ms=100)
239
+ ```
240
+
241
+ #### Special Keys:
242
+ ```python
243
+ terminal_send_keys(keys="Escape") # Cancel/back
244
+ terminal_send_keys(keys="c", modifiers=["Control"]) # Ctrl+C
245
+ terminal_send_keys(keys="d", modifiers=["Control"]) # Ctrl+D (EOF)
246
+ terminal_send_keys(keys="q") # Quit (common in TUIs)
247
+ ```
248
+
249
+ #### Type text:
250
+ ```python
251
+ terminal_run_command("some text") # Type and press Enter
252
+ ```
253
+
254
+ **DO NOT use browser_* tools** - those are for web pages, not terminals!
255
+
256
+ ## Testing Best Practices
257
+
258
+ ### 1. Verify Before Acting
259
+ - Check server health before opening terminal
260
+ - Wait for commands to complete before analyzing
261
+ - Use `terminal_wait_output` when expecting specific output
262
+
263
+ ### 2. Clear Error Detection
264
+ - Use `terminal_read_output` to check for error messages (NOT screenshots!)
265
+ - Search the text output for error patterns
266
+ - Check exit codes when possible
267
+
268
+ ### 3. Visual Verification (Only When Necessary)
269
+ - Only take screenshots when you need to verify VISUAL elements
270
+ - For text verification, always use `terminal_read_output` instead
271
+ - Compare against mockups only when specifically requested
272
+
273
+ ### 4. Structured Reporting
274
+ Always use `agent_share_your_reasoning` to explain:
275
+ - What you're testing
276
+ - What you observed
277
+ - Whether the test passed or failed
278
+ - Any issues or anomalies found
279
+
280
+ ## Common Testing Scenarios
281
+
282
+ ### TUI Application Testing
283
+ 1. Launch the TUI application
284
+ 2. Use `terminal_read_output` to verify text content
285
+ 3. Send navigation keys (arrows, tab)
286
+ 4. Read output again to verify changes
287
+ 5. Only screenshot if you need to verify visual layout/colors
288
+
289
+ ### CLI Output Verification
290
+ 1. Run the CLI command
291
+ 2. Use `terminal_read_output` to capture output (NOT screenshots!)
292
+ 3. Verify expected output is present in the text
293
+ 4. Check for unexpected errors in the text
294
+
295
+ ### Interactive Session Testing
296
+ 1. Start interactive session (e.g., Python REPL)
297
+ 2. Send commands via `terminal_run_command`
298
+ 3. Verify responses
299
+ 4. Exit cleanly with appropriate keys
300
+
301
+ ### Error Handling Verification
302
+ 1. Trigger error conditions intentionally
303
+ 2. Verify error messages appear correctly
304
+ 3. Confirm recovery behavior
305
+ 4. Document error scenarios
306
+
307
+ ## Important Notes
308
+
309
+ - The terminal runs via a browser-based xterm.js interface
310
+ - Screenshots are saved to a temp directory for reference
311
+ - The terminal session persists until `terminal_close` is called
312
+ - Multiple commands can be run in sequence without reopening
313
+
314
+ ## 🛑 FINAL REMINDER: ALWAYS CLOSE THE BROWSER!
315
+
316
+ Before you finish and return your response, you MUST call:
317
+ ```
318
+ terminal_close()
319
+ ```
320
+ This is not optional. Leaving the browser open wastes resources and can cause issues.
321
+
322
+ You are a thorough QA engineer who tests terminal applications systematically. Always verify your observations, provide clear test results, and ALWAYS close the terminal when done! 🖥️✅
323
+ """
@@ -377,8 +377,10 @@ class BaseAgent(ABC):
377
377
  # fixed instructions. For other models, count the full system prompt.
378
378
  try:
379
379
  from code_puppy.model_utils import (
380
+ get_antigravity_instructions,
380
381
  get_chatgpt_codex_instructions,
381
382
  get_claude_code_instructions,
383
+ is_antigravity_model,
382
384
  is_chatgpt_codex_model,
383
385
  is_claude_code_model,
384
386
  )
@@ -396,6 +398,11 @@ class BaseAgent(ABC):
396
398
  # The full system prompt is already in the message history
397
399
  instructions = get_chatgpt_codex_instructions()
398
400
  total_tokens += self.estimate_token_count(instructions)
401
+ elif is_antigravity_model(model_name):
402
+ # For Antigravity models, only count the short fixed instructions
403
+ # The full system prompt is already in the message history
404
+ instructions = get_antigravity_instructions()
405
+ total_tokens += self.estimate_token_count(instructions)
399
406
  else:
400
407
  # For other models, count the full system prompt
401
408
  system_prompt = self.get_system_prompt()
@@ -1558,11 +1565,17 @@ class BaseAgent(ABC):
1558
1565
  if output_type is not None:
1559
1566
  pydantic_agent = self._create_agent_with_output_type(output_type)
1560
1567
 
1561
- # Handle claude-code and chatgpt-codex models: prepend system prompt to first user message
1562
- from code_puppy.model_utils import is_chatgpt_codex_model, is_claude_code_model
1568
+ # Handle claude-code, chatgpt-codex, and antigravity models: prepend system prompt to first user message
1569
+ from code_puppy.model_utils import (
1570
+ is_antigravity_model,
1571
+ is_chatgpt_codex_model,
1572
+ is_claude_code_model,
1573
+ )
1563
1574
 
1564
- if is_claude_code_model(self.get_model_name()) or is_chatgpt_codex_model(
1565
- self.get_model_name()
1575
+ if (
1576
+ is_claude_code_model(self.get_model_name())
1577
+ or is_chatgpt_codex_model(self.get_model_name())
1578
+ or is_antigravity_model(self.get_model_name())
1566
1579
  ):
1567
1580
  if len(self.get_message_history()) == 0:
1568
1581
  system_prompt = self.get_system_prompt()
@@ -1,5 +1,7 @@
1
1
  """Event stream handler for processing streaming events from agent runs."""
2
2
 
3
+ import asyncio
4
+ import logging
3
5
  from collections.abc import AsyncIterable
4
6
  from typing import Any, Optional
5
7
 
@@ -16,8 +18,35 @@ from rich.console import Console
16
18
  from rich.markup import escape
17
19
  from rich.text import Text
18
20
 
19
- from code_puppy.config import get_banner_color
21
+ from code_puppy.config import get_banner_color, get_subagent_verbose
20
22
  from code_puppy.messaging.spinner import pause_all_spinners, resume_all_spinners
23
+ from code_puppy.tools.subagent_context import is_subagent
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _fire_stream_event(event_type: str, event_data: Any) -> None:
29
+ """Fire a stream event callback asynchronously (non-blocking).
30
+
31
+ Args:
32
+ event_type: Type of the event (e.g., 'part_start', 'part_delta', 'part_end')
33
+ event_data: Data associated with the event
34
+ """
35
+ try:
36
+ from code_puppy import callbacks
37
+ from code_puppy.messaging import get_session_context
38
+
39
+ agent_session_id = get_session_context()
40
+
41
+ # Use create_task to fire callback without blocking
42
+ asyncio.create_task(
43
+ callbacks.on_stream_event(event_type, event_data, agent_session_id)
44
+ )
45
+ except ImportError:
46
+ logger.debug("callbacks or messaging module not available for stream event")
47
+ except Exception as e:
48
+ logger.debug(f"Error firing stream event callback: {e}")
49
+
21
50
 
22
51
  # Module-level console for streaming output
23
52
  # Set via set_streaming_console() to share console with spinner
@@ -47,6 +76,15 @@ def get_streaming_console() -> Console:
47
76
  return Console()
48
77
 
49
78
 
79
+ def _should_suppress_output() -> bool:
80
+ """Check if sub-agent output should be suppressed.
81
+
82
+ Returns:
83
+ True if we're in a sub-agent context and verbose mode is disabled.
84
+ """
85
+ return is_subagent() and not get_subagent_verbose()
86
+
87
+
50
88
  async def event_stream_handler(
51
89
  ctx: RunContext,
52
90
  events: AsyncIterable[Any],
@@ -60,6 +98,12 @@ async def event_stream_handler(
60
98
  ctx: The run context.
61
99
  events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
62
100
  """
101
+ # If we're in a sub-agent and verbose mode is disabled, silently consume events
102
+ if _should_suppress_output():
103
+ async for _ in events:
104
+ pass # Just consume events without rendering
105
+ return
106
+
63
107
  import time
64
108
 
65
109
  from termflow import Parser as TermflowParser
@@ -75,6 +119,7 @@ async def event_stream_handler(
75
119
  tool_parts: set[int] = set() # Track which parts are tool calls
76
120
  banner_printed: set[int] = set() # Track if banner was already printed
77
121
  token_count: dict[int, int] = {} # Track token count per text/tool part
122
+ tool_names: dict[int, str] = {} # Track tool name per tool part index
78
123
  did_stream_anything = False # Track if we streamed any content
79
124
 
80
125
  # Termflow streaming state for text parts
@@ -121,6 +166,16 @@ async def event_stream_handler(
121
166
  async for event in events:
122
167
  # PartStartEvent - register the part but defer banner until content arrives
123
168
  if isinstance(event, PartStartEvent):
169
+ # Fire stream event callback for part_start
170
+ _fire_stream_event(
171
+ "part_start",
172
+ {
173
+ "index": event.index,
174
+ "part_type": type(event.part).__name__,
175
+ "part": event.part,
176
+ },
177
+ )
178
+
124
179
  part = event.part
125
180
  if isinstance(part, ThinkingPart):
126
181
  streaming_parts.add(event.index)
@@ -149,6 +204,8 @@ async def event_stream_handler(
149
204
  streaming_parts.add(event.index)
150
205
  tool_parts.add(event.index)
151
206
  token_count[event.index] = 0 # Initialize token counter
207
+ # Capture tool name from the start event
208
+ tool_names[event.index] = part.tool_name or ""
152
209
  # Track tool name for display
153
210
  banner_printed.add(
154
211
  event.index
@@ -156,6 +213,16 @@ async def event_stream_handler(
156
213
 
157
214
  # PartDeltaEvent - stream the content as it arrives
158
215
  elif isinstance(event, PartDeltaEvent):
216
+ # Fire stream event callback for part_delta
217
+ _fire_stream_event(
218
+ "part_delta",
219
+ {
220
+ "index": event.index,
221
+ "delta_type": type(event.delta).__name__,
222
+ "delta": event.delta,
223
+ },
224
+ )
225
+
159
226
  if event.index in streaming_parts:
160
227
  delta = event.delta
161
228
  if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
@@ -189,25 +256,50 @@ async def event_stream_handler(
189
256
  escaped = escape(delta.content_delta)
190
257
  console.print(f"[dim]{escaped}[/dim]", end="")
191
258
  elif isinstance(delta, ToolCallPartDelta):
192
- # For tool calls, count chunks received
193
- token_count[event.index] += 1
194
- # Get tool name if available
195
- tool_name = getattr(delta, "tool_name_delta", "")
259
+ # For tool calls, estimate tokens from args_delta content
260
+ # args_delta contains the streaming JSON arguments
261
+ args_delta = getattr(delta, "args_delta", "") or ""
262
+ if args_delta:
263
+ # Rough estimate: 4 chars ≈ 1 token (same heuristic as subagent_stream_handler)
264
+ estimated_tokens = max(1, len(args_delta) // 4)
265
+ token_count[event.index] += estimated_tokens
266
+ else:
267
+ # Even empty deltas count as activity
268
+ token_count[event.index] += 1
269
+
270
+ # Update tool name if delta provides more of it
271
+ tool_name_delta = getattr(delta, "tool_name_delta", "") or ""
272
+ if tool_name_delta:
273
+ tool_names[event.index] = (
274
+ tool_names.get(event.index, "") + tool_name_delta
275
+ )
276
+
277
+ # Use stored tool name for display
278
+ tool_name = tool_names.get(event.index, "")
196
279
  count = token_count[event.index]
197
280
  # Display with tool wrench icon and tool name
198
281
  if tool_name:
199
282
  console.print(
200
- f" \U0001f527 Calling {tool_name}... {count} chunks ",
283
+ f" \U0001f527 Calling {tool_name}... {count} token(s) ",
201
284
  end="\r",
202
285
  )
203
286
  else:
204
287
  console.print(
205
- f" \U0001f527 Calling tool... {count} chunks ",
288
+ f" \U0001f527 Calling tool... {count} token(s) ",
206
289
  end="\r",
207
290
  )
208
291
 
209
292
  # PartEndEvent - finish the streaming with a newline
210
293
  elif isinstance(event, PartEndEvent):
294
+ # Fire stream event callback for part_end
295
+ _fire_stream_event(
296
+ "part_end",
297
+ {
298
+ "index": event.index,
299
+ "next_part_kind": getattr(event, "next_part_kind", None),
300
+ },
301
+ )
302
+
211
303
  if event.index in streaming_parts:
212
304
  # For text parts, finalize termflow rendering
213
305
  if event.index in text_parts:
@@ -238,8 +330,9 @@ async def event_stream_handler(
238
330
  elif event.index in banner_printed:
239
331
  console.print() # Final newline after streaming
240
332
 
241
- # Clean up token count
333
+ # Clean up token count and tool names
242
334
  token_count.pop(event.index, None)
335
+ tool_names.pop(event.index, None)
243
336
  # Clean up all tracking sets
244
337
  streaming_parts.discard(event.index)
245
338
  thinking_parts.discard(event.index)
@@ -0,0 +1,34 @@
1
+ """The Pack - Specialized sub-agents coordinated by Pack Leader 🐺
2
+
3
+ This package contains the specialized agents that work together under
4
+ Pack Leader's coordination for parallel multi-agent workflows:
5
+
6
+ - **Bloodhound** 🐕‍🦺 - Issue tracking specialist (bd only)
7
+ - **Terrier** 🐕 - Worktree management (git worktree from base branch)
8
+ - **Husky** 🐺 - Task execution (coding work in worktrees)
9
+ - **Shepherd** 🐕 - Code review critic (quality gatekeeper)
10
+ - **Watchdog** 🐕‍🦺 - QA critic (tests, coverage, quality)
11
+ - **Retriever** 🦮 - Local branch merging (git merge to base branch)
12
+
13
+ All work happens locally - no GitHub PRs or remote pushes.
14
+ Everything merges to a declared base branch.
15
+
16
+ Each agent is designed to do one thing well, following the Unix philosophy.
17
+ Pack Leader orchestrates them to execute complex parallel workflows.
18
+ """
19
+
20
+ from .bloodhound import BloodhoundAgent
21
+ from .husky import HuskyAgent
22
+ from .retriever import RetrieverAgent
23
+ from .shepherd import ShepherdAgent
24
+ from .terrier import TerrierAgent
25
+ from .watchdog import WatchdogAgent
26
+
27
+ __all__ = [
28
+ "BloodhoundAgent",
29
+ "TerrierAgent",
30
+ "RetrieverAgent",
31
+ "HuskyAgent",
32
+ "ShepherdAgent",
33
+ "WatchdogAgent",
34
+ ]