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.
- 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_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +34 -252
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/claude_cache_client.py +249 -34
- code_puppy/cli_runner.py +4 -3
- code_puppy/command_line/add_model_menu.py +8 -9
- code_puppy/command_line/core_commands.py +85 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
- code_puppy/command_line/mcp/custom_server_form.py +54 -19
- code_puppy/command_line/mcp/custom_server_installer.py +8 -9
- code_puppy/command_line/mcp/handler.py +0 -2
- code_puppy/command_line/mcp/help_command.py +1 -5
- code_puppy/command_line/mcp/start_command.py +36 -18
- code_puppy/command_line/onboarding_slides.py +0 -1
- code_puppy/command_line/prompt_toolkit_completion.py +16 -10
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +66 -62
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/managed_server.py +49 -20
- code_puppy/mcp_/manager.py +81 -52
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/message_queue.py +11 -23
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_utils.py +54 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
- code_puppy/plugins/antigravity_oauth/transport.py +1 -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/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +139 -36
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/camoufox_manager.py +226 -64
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/RECORD +84 -53
- code_puppy/command_line/mcp/add_command.py +0 -170
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.341.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"]
|