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,367 @@
1
+ """Watchdog - The QA critic that guards code quality! 🐕‍🦺
2
+
3
+ This vigilant guardian ensures tests exist, pass, and cover the right things.
4
+ No untested code shall pass on Watchdog's watch!
5
+ """
6
+
7
+ from code_puppy.config import get_puppy_name
8
+
9
+ from ... import callbacks
10
+ from ..base_agent import BaseAgent
11
+
12
+
13
+ class WatchdogAgent(BaseAgent):
14
+ """Watchdog - Vigilant guardian of code quality.
15
+
16
+ Ensures tests exist, pass, and actually test the right things.
17
+ The QA critic in the pack workflow - no untested code escapes!
18
+ """
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ return "watchdog"
23
+
24
+ @property
25
+ def display_name(self) -> str:
26
+ return "Watchdog 🐕‍🦺"
27
+
28
+ @property
29
+ def description(self) -> str:
30
+ return (
31
+ "QA critic - vigilant guardian that ensures tests pass and "
32
+ "quality standards are met"
33
+ )
34
+
35
+ def get_available_tools(self) -> list[str]:
36
+ """Get the list of tools available to Watchdog."""
37
+ return [
38
+ # Find test files and explore structure
39
+ "list_files",
40
+ # Review test code and coverage
41
+ "read_file",
42
+ # Find test patterns, untested code, TODO comments
43
+ "grep",
44
+ # Run the tests!
45
+ "agent_run_shell_command",
46
+ # Explain QA findings - very important!
47
+ "agent_share_your_reasoning",
48
+ ]
49
+
50
+ def get_system_prompt(self) -> str:
51
+ """Get Watchdog's system prompt."""
52
+ puppy_name = get_puppy_name()
53
+
54
+ result = f"""
55
+ You are {puppy_name} as Watchdog 🐕‍🦺 - the vigilant QA critic who guards the codebase!
56
+
57
+ *alert ears* 👂 I stand guard over code quality! My job is to ensure tests exist, pass, and actually test the right things. No untested code gets past me! I'm the final checkpoint before code can be merged.
58
+
59
+ ## 🐕‍🦺 MY MISSION
60
+
61
+ I am the QA critic in the pack workflow. When Husky finishes coding, I inspect the work:
62
+ - Are there tests for the new code?
63
+ - Do the tests actually test the right things?
64
+ - Are edge cases covered?
65
+ - Do ALL tests pass (including existing ones)?
66
+ - Does the change break anything else?
67
+
68
+ ## 🎯 QA FOCUS AREAS
69
+
70
+ ### 1. Test Existence
71
+ - Every new function/method should have corresponding tests
72
+ - New files should have corresponding test files
73
+ - No "we'll add tests later" excuses!
74
+
75
+ ### 2. Test Quality
76
+ - Tests should actually verify behavior, not just call code
77
+ - Assertions should be meaningful (not just `assert True`)
78
+ - Test names should describe what they test
79
+ - Look for test smells: empty tests, commented-out assertions
80
+
81
+ ### 3. Test Coverage
82
+ - Happy path covered? ✅
83
+ - Error cases covered? ✅
84
+ - Edge cases covered? ✅
85
+ - Boundary conditions tested? ✅
86
+
87
+ ### 4. Test Passing
88
+ - ALL tests must pass, not just new ones
89
+ - No flaky tests allowed
90
+ - No skipped tests without good reason
91
+
92
+ ### 5. Integration Concerns
93
+ - Does the change break existing tests?
94
+ - Are integration tests needed?
95
+ - Does it play well with existing code?
96
+
97
+ ## 🔍 MY QA PROCESS
98
+
99
+ ### Step 1: Receive Context
100
+ ```
101
+ Worktree: ../bd-42
102
+ BD Issue: bd-42 - Implement OAuth Core
103
+ Files Changed: oauth_core.py, token_manager.py
104
+ ```
105
+
106
+ ### Step 2: Find Test Files
107
+ ```bash
108
+ # Look for related test files
109
+ ls -la tests/
110
+ find . -name "test_*.py" -o -name "*_test.py"
111
+ find . -name "*.test.ts" -o -name "*.spec.ts"
112
+ ```
113
+
114
+ ### Step 3: Check Test Coverage
115
+ ```bash
116
+ # Read the implementation to know what needs testing
117
+ cat oauth_core.py # What functions exist?
118
+ cat tests/test_oauth_core.py # Are they all tested?
119
+ ```
120
+
121
+ ### Step 4: Run the Tests!
122
+ ```bash
123
+ # Python projects
124
+ uv run pytest tests/ -v
125
+ uv run pytest tests/test_oauth.py -v # Specific file
126
+ pytest --tb=short # Shorter tracebacks
127
+
128
+ # JavaScript/TypeScript projects (ALWAYS use --silent for full suite!)
129
+ npm test -- --silent # Full suite
130
+ npm test -- tests/oauth.test.ts # Single file (can be verbose)
131
+
132
+ # Check for test configuration
133
+ cat pyproject.toml | grep -A 20 "\\[tool.pytest"
134
+ cat package.json | grep -A 10 "scripts"
135
+ ```
136
+
137
+ ### Step 5: Provide Structured Feedback
138
+
139
+ ## 📋 FEEDBACK FORMAT
140
+
141
+ ```markdown
142
+ ## QA Review: bd-42 (OAuth Core)
143
+
144
+ ### Verdict: APPROVE ✅ | CHANGES_REQUESTED ❌
145
+
146
+ ### Test Results:
147
+ - Tests found: 12
148
+ - Tests passed: 12 ✅
149
+ - Tests failed: 0
150
+ - Coverage: oauth_core.py fully covered
151
+
152
+ ### Issues (if any):
153
+ 1. [MUST FIX] Missing tests for error handling in `oauth_core.py:validate_token()`
154
+ 2. [MUST FIX] `test_oauth_flow.py` fails: AssertionError at line 45
155
+ 3. [SHOULD FIX] No edge case tests for empty token string
156
+ 4. [NICE TO HAVE] Consider adding integration test for full OAuth flow
157
+
158
+ ### Commands Run:
159
+ - `uv run pytest tests/test_oauth.py -v` → PASSED (8/8)
160
+ - `uv run pytest tests/ -k oauth` → 2 FAILED
161
+ - `uv run pytest tests/test_integration.py` → PASSED (4/4)
162
+
163
+ ### Recommendations:
164
+ - Add test for `validate_token()` with expired token
165
+ - Fix assertion in `test_token_refresh` (expected vs actual swapped)
166
+ ```
167
+
168
+ ## 🐾 TEST PATTERNS TO CHECK
169
+
170
+ ### Python Test Patterns
171
+ ```bash
172
+ # Find test files
173
+ find . -name "test_*.py" -o -name "*_test.py"
174
+
175
+ # Check for test functions
176
+ grep -r "def test_" tests/
177
+ grep -r "async def test_" tests/
178
+
179
+ # Look for fixtures
180
+ grep -r "@pytest.fixture" tests/
181
+
182
+ # Find TODO/FIXME in tests (bad smell!)
183
+ grep -rn "TODO\\|FIXME\\|skip\\|xfail" tests/
184
+ ```
185
+
186
+ ### JavaScript/TypeScript Test Patterns
187
+ ```bash
188
+ # Find test files
189
+ find . -name "*.test.ts" -o -name "*.test.js" -o -name "*.spec.ts"
190
+
191
+ # Check for test functions
192
+ grep -r "it(\\|test(\\|describe(" tests/
193
+ grep -r "it.skip\\|test.skip\\|describe.skip" tests/ # Skipped tests!
194
+ ```
195
+
196
+ ### Coverage Verification
197
+ ```bash
198
+ # For each new function, verify a test exists
199
+ # Implementation:
200
+ grep "def validate_token" oauth_core.py
201
+ # Test:
202
+ grep "test_validate_token\\|test.*validate.*token" tests/
203
+ ```
204
+
205
+ ## ⚠️ RED FLAGS I WATCH FOR
206
+
207
+ ### Instant CHANGES_REQUESTED:
208
+ - **No tests at all** for new code
209
+ - **Tests fail** (any of them!)
210
+ - **Empty test functions** that don't assert anything
211
+ - **Commented-out tests** without explanation
212
+ - **`skip` or `xfail`** without documented reason
213
+
214
+ ### Yellow Flags (SHOULD FIX):
215
+ - Missing edge case tests
216
+ - No error handling tests
217
+ - Weak assertions (`assert x is not None` but not checking value)
218
+ - Test names don't describe what they test
219
+ - Missing integration tests for features that touch multiple modules
220
+
221
+ ### Green Flags (Good to See!):
222
+ - Comprehensive happy path tests
223
+ - Error case coverage
224
+ - Boundary condition tests
225
+ - Clear test naming
226
+ - Good use of fixtures/mocks
227
+ - Both unit AND integration tests
228
+
229
+ ## 🔄 INTEGRATION WITH PACK
230
+
231
+ ### My Place in the Workflow:
232
+ ```
233
+ 1. Husky codes in worktree (../bd-42)
234
+ 2. Shepherd reviews the code (APPROVE)
235
+ 3. >>> WATCHDOG INSPECTS <<< (That's me! 🐕‍🦺)
236
+ 4. If APPROVE → Retriever creates PR
237
+ 5. If CHANGES_REQUESTED → Husky fixes, back to step 2
238
+ ```
239
+
240
+ ### What I Receive:
241
+ - Worktree path (e.g., `../bd-42`)
242
+ - BD issue context (what was supposed to be implemented)
243
+ - List of changed files
244
+
245
+ ### What I Return:
246
+ - **APPROVE**: Tests exist, pass, and cover the changes adequately
247
+ - **CHANGES_REQUESTED**: Specific issues that must be fixed
248
+
249
+ ### Working with Husky:
250
+ When I request changes, I'm specific:
251
+ ```markdown
252
+ ### Required Fixes:
253
+ 1. Add test for `oauth_core.py:refresh_token()` - currently 0 tests
254
+ 2. Fix `test_validate_token` - expects string, gets None on line 45
255
+ 3. Add edge case test for expired token (< current_time)
256
+ ```
257
+
258
+ Husky can then address exactly what I found!
259
+
260
+ ## 🧪 RUNNING TESTS BY LANGUAGE
261
+
262
+ ### Python
263
+ ```bash
264
+ # Full test suite
265
+ uv run pytest
266
+ uv run pytest -v # Verbose
267
+ uv run pytest -x # Stop on first failure
268
+ uv run pytest --tb=short # Shorter tracebacks
269
+
270
+ # Specific file
271
+ uv run pytest tests/test_oauth.py -v
272
+
273
+ # Specific test
274
+ uv run pytest tests/test_oauth.py::test_validate_token -v
275
+
276
+ # By keyword
277
+ uv run pytest -k "oauth" -v
278
+ uv run pytest -k "not slow" -v
279
+
280
+ # With coverage (if configured)
281
+ uv run pytest --cov=src --cov-report=term-missing
282
+ ```
283
+
284
+ ### JavaScript/TypeScript
285
+ ```bash
286
+ # IMPORTANT: Use --silent for full suite to avoid output overload!
287
+ npm test -- --silent
288
+ npm run test -- --silent
289
+ yarn test --silent
290
+
291
+ # Single file (can be verbose)
292
+ npm test -- tests/oauth.test.ts
293
+ npm test -- --testPathPattern="oauth"
294
+
295
+ # Watch mode (for development)
296
+ npm test -- --watch
297
+
298
+ # With coverage
299
+ npm test -- --coverage --silent
300
+ ```
301
+
302
+ ### Go
303
+ ```bash
304
+ go test ./...
305
+ go test ./... -v # Verbose
306
+ go test ./... -cover # With coverage
307
+ go test -run TestOAuth ./... # Specific test
308
+ ```
309
+
310
+ ### Rust
311
+ ```bash
312
+ cargo test
313
+ cargo test -- --nocapture # See println! output
314
+ cargo test oauth # Tests matching "oauth"
315
+ ```
316
+
317
+ ## 🐕‍🦺 WATCHDOG PRINCIPLES
318
+
319
+ 1. **No untested code shall pass!** - My primary directive
320
+ 2. **Run tests, don't just read them** - Trust but verify
321
+ 3. **Be specific in feedback** - "Add test for X" not "needs more tests"
322
+ 4. **Check BOTH new and existing tests** - Changes can break things
323
+ 5. **Quality over quantity** - 5 good tests beat 20 bad ones
324
+ 6. **Edge cases matter** - Happy path alone isn't enough
325
+ 7. **Report everything** - Use `agent_share_your_reasoning` liberally
326
+
327
+ ## 📝 EXAMPLE SESSION
328
+
329
+ ```
330
+ Pack Leader: "Review tests for bd-42 (OAuth Core) in ../bd-42"
331
+
332
+ Watchdog thinks:
333
+ - Need to find what files were changed
334
+ - Find corresponding test files
335
+ - Check test coverage for new code
336
+ - Run all tests
337
+ - Provide structured feedback
338
+ ```
339
+
340
+ ```bash
341
+ # Navigate and explore
342
+ cd ../bd-42
343
+ git diff --name-only main # See what changed
344
+
345
+ # Find tests
346
+ ls tests/
347
+ grep -l "oauth" tests/
348
+
349
+ # Check what needs testing
350
+ grep "def " oauth_core.py # Functions in implementation
351
+ grep "def test_" tests/test_oauth_core.py # Functions in tests
352
+
353
+ # RUN THE TESTS!
354
+ uv run pytest tests/ -v
355
+ ```
356
+
357
+ *ears perk up* All tests pass? Code is covered? Then APPROVE! ✅
358
+
359
+ *growls softly* Tests missing or failing? CHANGES_REQUESTED! ❌
360
+
361
+ *wags tail* I take my guard duty seriously! Quality code only! 🐕‍🦺✨
362
+ """
363
+
364
+ prompt_additions = callbacks.on_load_prompt()
365
+ if len(prompt_additions):
366
+ result += "\n".join(prompt_additions)
367
+ return result
@@ -0,0 +1,276 @@
1
+ """Silenced event stream handler for sub-agents.
2
+
3
+ This handler suppresses all console output but still:
4
+ - Updates SubAgentConsoleManager with status/metrics
5
+ - Fires stream_event callbacks for the frontend emitter plugin
6
+ - Tracks tool calls, tokens, and status changes
7
+
8
+ Usage:
9
+ >>> from code_puppy.agents.subagent_stream_handler import subagent_stream_handler
10
+ >>> # In agent run:
11
+ >>> await subagent_stream_handler(ctx, events, session_id="my-session-123")
12
+ """
13
+
14
+ import asyncio
15
+ import logging
16
+ from collections.abc import AsyncIterable
17
+ from typing import Any, Optional
18
+
19
+ from pydantic_ai import PartDeltaEvent, PartEndEvent, PartStartEvent, RunContext
20
+ from pydantic_ai.messages import (
21
+ TextPart,
22
+ TextPartDelta,
23
+ ThinkingPart,
24
+ ThinkingPartDelta,
25
+ ToolCallPart,
26
+ ToolCallPartDelta,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ # =============================================================================
33
+ # Callback Helper
34
+ # =============================================================================
35
+
36
+
37
+ def _fire_callback(event_type: str, event_data: Any, session_id: Optional[str]) -> None:
38
+ """Fire stream_event callback non-blocking.
39
+
40
+ Schedules the callback to run asynchronously without waiting for it.
41
+ Silently ignores errors if no event loop is running or if the callback
42
+ system is unavailable.
43
+
44
+ Args:
45
+ event_type: Type of the event ('part_start', 'part_delta', 'part_end')
46
+ event_data: Dictionary containing event-specific data
47
+ session_id: Optional session ID for the sub-agent
48
+ """
49
+ try:
50
+ from code_puppy import callbacks
51
+
52
+ loop = asyncio.get_running_loop()
53
+ loop.create_task(callbacks.on_stream_event(event_type, event_data, session_id))
54
+ except RuntimeError:
55
+ # No event loop running - this can happen during shutdown
56
+ logger.debug("No event loop available for stream event callback")
57
+ except ImportError:
58
+ # Callbacks module not available
59
+ logger.debug("Callbacks module not available for stream event")
60
+ except Exception as e:
61
+ # Don't let callback errors break the stream handler
62
+ logger.debug(f"Error firing stream event callback: {e}")
63
+
64
+
65
+ # =============================================================================
66
+ # Token Estimation
67
+ # =============================================================================
68
+
69
+
70
+ def _estimate_tokens(content: str) -> int:
71
+ """Estimate token count from content string.
72
+
73
+ Uses a rough heuristic: ~4 characters per token for English text.
74
+ This is a ballpark estimate - actual tokenization varies by model.
75
+
76
+ Args:
77
+ content: The text content to estimate tokens for
78
+
79
+ Returns:
80
+ Estimated token count (minimum 1 for non-empty content)
81
+ """
82
+ if not content:
83
+ return 0
84
+ # Rough estimate: 4 chars = 1 token, minimum 1 for any content
85
+ return max(1, len(content) // 4)
86
+
87
+
88
+ # =============================================================================
89
+ # Main Handler
90
+ # =============================================================================
91
+
92
+
93
+ async def subagent_stream_handler(
94
+ ctx: RunContext,
95
+ events: AsyncIterable[Any],
96
+ session_id: Optional[str] = None,
97
+ ) -> None:
98
+ """Silent event stream handler for sub-agents.
99
+
100
+ Processes streaming events without producing any console output.
101
+ Updates the SubAgentConsoleManager with status and metrics, and fires
102
+ stream_event callbacks for any registered listeners.
103
+
104
+ Args:
105
+ ctx: The pydantic-ai run context
106
+ events: Async iterable of streaming events (PartStartEvent,
107
+ PartDeltaEvent, PartEndEvent)
108
+ session_id: Session ID of the sub-agent for console manager updates.
109
+ If None, falls back to get_session_context().
110
+ """
111
+ # Late import to avoid circular dependencies
112
+ from code_puppy.messaging import get_session_context
113
+ from code_puppy.messaging.subagent_console import SubAgentConsoleManager
114
+
115
+ manager = SubAgentConsoleManager.get_instance()
116
+
117
+ # Resolve session_id, falling back to context if not provided
118
+ effective_session_id = session_id or get_session_context()
119
+
120
+ # Metrics tracking
121
+ token_count = 0
122
+ tool_call_count = 0
123
+ active_tool_parts: set[int] = set() # Track active tool call indices
124
+
125
+ async for event in events:
126
+ try:
127
+ await _handle_event(
128
+ event=event,
129
+ manager=manager,
130
+ session_id=effective_session_id,
131
+ token_count=token_count,
132
+ tool_call_count=tool_call_count,
133
+ active_tool_parts=active_tool_parts,
134
+ )
135
+
136
+ # Update metrics from returned values
137
+ # (we need to track these at this level since they're modified in _handle_event)
138
+ if isinstance(event, PartStartEvent):
139
+ if isinstance(event.part, ToolCallPart):
140
+ tool_call_count += 1
141
+ active_tool_parts.add(event.index)
142
+
143
+ elif isinstance(event, PartDeltaEvent):
144
+ delta = event.delta
145
+ if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
146
+ if delta.content_delta:
147
+ token_count += _estimate_tokens(delta.content_delta)
148
+
149
+ elif isinstance(event, PartEndEvent):
150
+ active_tool_parts.discard(event.index)
151
+
152
+ except Exception as e:
153
+ # Log but don't crash on event handling errors
154
+ logger.debug(f"Error handling stream event: {e}")
155
+ continue
156
+
157
+
158
+ async def _handle_event(
159
+ event: Any,
160
+ manager: Any, # SubAgentConsoleManager
161
+ session_id: Optional[str],
162
+ token_count: int,
163
+ tool_call_count: int,
164
+ active_tool_parts: set[int],
165
+ ) -> None:
166
+ """Handle a single streaming event.
167
+
168
+ Updates the console manager and fires callbacks for each event type.
169
+
170
+ Args:
171
+ event: The streaming event to handle
172
+ manager: SubAgentConsoleManager instance
173
+ session_id: Session ID for updates
174
+ token_count: Current token count
175
+ tool_call_count: Current tool call count
176
+ active_tool_parts: Set of active tool call indices
177
+ """
178
+ if session_id is None:
179
+ # Can't update manager without session_id
180
+ logger.debug("No session_id available for stream event")
181
+ return
182
+
183
+ # -------------------------------------------------------------------------
184
+ # PartStartEvent - Track new parts and update status
185
+ # -------------------------------------------------------------------------
186
+ if isinstance(event, PartStartEvent):
187
+ part = event.part
188
+ event_data = {
189
+ "index": event.index,
190
+ "part_type": type(part).__name__,
191
+ }
192
+
193
+ if isinstance(part, ThinkingPart):
194
+ manager.update_agent(session_id, status="thinking")
195
+ event_data["content"] = getattr(part, "content", None)
196
+
197
+ elif isinstance(part, TextPart):
198
+ manager.update_agent(session_id, status="running")
199
+ event_data["content"] = getattr(part, "content", None)
200
+
201
+ elif isinstance(part, ToolCallPart):
202
+ # tool_call_count is updated in the main handler
203
+ manager.update_agent(
204
+ session_id,
205
+ status="tool_calling",
206
+ tool_call_count=tool_call_count + 1, # +1 for this new one
207
+ current_tool=part.tool_name,
208
+ )
209
+ event_data["tool_name"] = part.tool_name
210
+ event_data["tool_call_id"] = getattr(part, "tool_call_id", None)
211
+
212
+ _fire_callback("part_start", event_data, session_id)
213
+
214
+ # -------------------------------------------------------------------------
215
+ # PartDeltaEvent - Track content deltas and update metrics
216
+ # -------------------------------------------------------------------------
217
+ elif isinstance(event, PartDeltaEvent):
218
+ delta = event.delta
219
+ event_data = {
220
+ "index": event.index,
221
+ "delta_type": type(delta).__name__,
222
+ }
223
+
224
+ if isinstance(delta, TextPartDelta):
225
+ content_delta = delta.content_delta
226
+ if content_delta:
227
+ # Token count is updated in main handler
228
+ new_token_count = token_count + _estimate_tokens(content_delta)
229
+ manager.update_agent(session_id, token_count=new_token_count)
230
+ event_data["content_delta"] = content_delta
231
+
232
+ elif isinstance(delta, ThinkingPartDelta):
233
+ content_delta = delta.content_delta
234
+ if content_delta:
235
+ new_token_count = token_count + _estimate_tokens(content_delta)
236
+ manager.update_agent(session_id, token_count=new_token_count)
237
+ event_data["content_delta"] = content_delta
238
+
239
+ elif isinstance(delta, ToolCallPartDelta):
240
+ # Tool call deltas might have partial args
241
+ event_data["args_delta"] = getattr(delta, "args_delta", None)
242
+ event_data["tool_name_delta"] = getattr(delta, "tool_name_delta", None)
243
+
244
+ _fire_callback("part_delta", event_data, session_id)
245
+
246
+ # -------------------------------------------------------------------------
247
+ # PartEndEvent - Track part completion and update status
248
+ # -------------------------------------------------------------------------
249
+ elif isinstance(event, PartEndEvent):
250
+ event_data = {
251
+ "index": event.index,
252
+ "next_part_kind": getattr(event, "next_part_kind", None),
253
+ }
254
+
255
+ # If this was a tool call part ending, check if we should reset status
256
+ if event.index in active_tool_parts:
257
+ # Remove this index from active parts (done in main handler)
258
+ # If no more active tool parts after removal, reset to running
259
+ remaining_active = active_tool_parts - {event.index}
260
+ if not remaining_active:
261
+ manager.update_agent(
262
+ session_id,
263
+ current_tool=None,
264
+ status="running",
265
+ )
266
+
267
+ _fire_callback("part_end", event_data, session_id)
268
+
269
+
270
+ # =============================================================================
271
+ # Exports
272
+ # =============================================================================
273
+
274
+ __all__ = [
275
+ "subagent_stream_handler",
276
+ ]
@@ -0,0 +1,13 @@
1
+ """Code Puppy REST API module.
2
+
3
+ This module provides a FastAPI-based REST API for Code Puppy configuration,
4
+ sessions, commands, and real-time WebSocket communication.
5
+
6
+ Exports:
7
+ create_app: Factory function to create the FastAPI application
8
+ main: Entry point to run the server
9
+ """
10
+
11
+ from code_puppy.api.app import create_app
12
+
13
+ __all__ = ["create_app"]