code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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/__init__.py +7 -1
- code_puppy/agents/__init__.py +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -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 +174 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +233 -627
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +1 -4
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -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/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- 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 +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -182
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -15
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -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"]
|
code_puppy/api/app.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""FastAPI application factory for Code Puppy API."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import AsyncGenerator
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, Request
|
|
10
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
11
|
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
|
12
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Default request timeout (seconds) - fail fast!
|
|
17
|
+
REQUEST_TIMEOUT = 30.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TimeoutMiddleware(BaseHTTPMiddleware):
|
|
21
|
+
"""Middleware to enforce request timeouts and prevent hanging requests."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, app, timeout: float = REQUEST_TIMEOUT):
|
|
24
|
+
super().__init__(app)
|
|
25
|
+
self.timeout = timeout
|
|
26
|
+
|
|
27
|
+
async def dispatch(self, request: Request, call_next):
|
|
28
|
+
# Skip timeout for WebSocket upgrades and streaming endpoints
|
|
29
|
+
if request.headers.get(
|
|
30
|
+
"upgrade", ""
|
|
31
|
+
).lower() == "websocket" or request.url.path.startswith("/ws/"):
|
|
32
|
+
return await call_next(request)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
return await asyncio.wait_for(
|
|
36
|
+
call_next(request),
|
|
37
|
+
timeout=self.timeout,
|
|
38
|
+
)
|
|
39
|
+
except asyncio.TimeoutError:
|
|
40
|
+
return JSONResponse(
|
|
41
|
+
status_code=504,
|
|
42
|
+
content={
|
|
43
|
+
"detail": f"Request timed out after {self.timeout}s",
|
|
44
|
+
"error": "timeout",
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@asynccontextmanager
|
|
50
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
51
|
+
"""Lifespan context manager for startup and shutdown events.
|
|
52
|
+
|
|
53
|
+
Handles graceful cleanup of resources when the server shuts down.
|
|
54
|
+
"""
|
|
55
|
+
# Startup: nothing special needed yet, but this is where you'd do it
|
|
56
|
+
logger.info("🐶 Code Puppy API starting up...")
|
|
57
|
+
yield
|
|
58
|
+
# Shutdown: clean up all the things!
|
|
59
|
+
logger.info("🐶 Code Puppy API shutting down, cleaning up...")
|
|
60
|
+
|
|
61
|
+
# 1. Close all PTY sessions
|
|
62
|
+
try:
|
|
63
|
+
from code_puppy.api.pty_manager import get_pty_manager
|
|
64
|
+
|
|
65
|
+
pty_manager = get_pty_manager()
|
|
66
|
+
await pty_manager.close_all()
|
|
67
|
+
logger.info("✓ All PTY sessions closed")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Error closing PTY sessions: {e}")
|
|
70
|
+
|
|
71
|
+
# 2. Remove PID file so /api status knows we're gone
|
|
72
|
+
try:
|
|
73
|
+
from code_puppy.config import STATE_DIR
|
|
74
|
+
|
|
75
|
+
pid_file = Path(STATE_DIR) / "api_server.pid"
|
|
76
|
+
if pid_file.exists():
|
|
77
|
+
pid_file.unlink()
|
|
78
|
+
logger.info("✓ PID file removed")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Error removing PID file: {e}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_app() -> FastAPI:
|
|
84
|
+
"""Create and configure the FastAPI application."""
|
|
85
|
+
app = FastAPI(
|
|
86
|
+
lifespan=lifespan,
|
|
87
|
+
title="Code Puppy API",
|
|
88
|
+
description="REST API and Interactive Terminal for Code Puppy",
|
|
89
|
+
version="1.0.0",
|
|
90
|
+
docs_url="/docs",
|
|
91
|
+
redoc_url="/redoc",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Timeout middleware - added first so it wraps everything
|
|
95
|
+
app.add_middleware(TimeoutMiddleware, timeout=REQUEST_TIMEOUT)
|
|
96
|
+
|
|
97
|
+
# CORS middleware for frontend access
|
|
98
|
+
app.add_middleware(
|
|
99
|
+
CORSMiddleware,
|
|
100
|
+
allow_origins=["*"], # Local/trusted
|
|
101
|
+
allow_credentials=True,
|
|
102
|
+
allow_methods=["*"],
|
|
103
|
+
allow_headers=["*"],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Include routers
|
|
107
|
+
from code_puppy.api.routers import agents, commands, config, sessions
|
|
108
|
+
|
|
109
|
+
app.include_router(config.router, prefix="/api/config", tags=["config"])
|
|
110
|
+
app.include_router(commands.router, prefix="/api/commands", tags=["commands"])
|
|
111
|
+
app.include_router(sessions.router, prefix="/api/sessions", tags=["sessions"])
|
|
112
|
+
app.include_router(agents.router, prefix="/api/agents", tags=["agents"])
|
|
113
|
+
|
|
114
|
+
# WebSocket endpoints (events + terminal)
|
|
115
|
+
from code_puppy.api.websocket import setup_websocket
|
|
116
|
+
|
|
117
|
+
setup_websocket(app)
|
|
118
|
+
|
|
119
|
+
# Templates directory
|
|
120
|
+
templates_dir = Path(__file__).parent / "templates"
|
|
121
|
+
|
|
122
|
+
@app.get("/")
|
|
123
|
+
async def root():
|
|
124
|
+
"""Landing page with links to terminal and docs."""
|
|
125
|
+
return HTMLResponse(
|
|
126
|
+
content="""
|
|
127
|
+
<!DOCTYPE html>
|
|
128
|
+
<html>
|
|
129
|
+
<head>
|
|
130
|
+
<title>Code Puppy 🐶</title>
|
|
131
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
132
|
+
</head>
|
|
133
|
+
<body class="bg-gray-900 text-white min-h-screen flex items-center justify-center">
|
|
134
|
+
<div class="text-center">
|
|
135
|
+
<h1 class="text-6xl mb-4">🐶</h1>
|
|
136
|
+
<h2 class="text-3xl font-bold mb-8">Code Puppy</h2>
|
|
137
|
+
<div class="space-x-4">
|
|
138
|
+
<a href="/terminal" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-lg font-semibold">
|
|
139
|
+
Open Terminal
|
|
140
|
+
</a>
|
|
141
|
+
<a href="/docs" class="px-6 py-3 bg-gray-700 hover:bg-gray-600 rounded-lg text-lg">
|
|
142
|
+
API Docs
|
|
143
|
+
</a>
|
|
144
|
+
</div>
|
|
145
|
+
<p class="mt-8 text-gray-400">
|
|
146
|
+
WebSocket: ws://localhost:8765/ws/terminal
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
</body>
|
|
150
|
+
</html>
|
|
151
|
+
"""
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@app.get("/terminal")
|
|
155
|
+
async def terminal_page():
|
|
156
|
+
"""Serve the interactive terminal page."""
|
|
157
|
+
html_file = templates_dir / "terminal.html"
|
|
158
|
+
if html_file.exists():
|
|
159
|
+
return FileResponse(html_file, media_type="text/html")
|
|
160
|
+
return HTMLResponse(
|
|
161
|
+
content="<h1>Terminal template not found</h1>",
|
|
162
|
+
status_code=404,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@app.get("/health")
|
|
166
|
+
async def health():
|
|
167
|
+
return {"status": "healthy"}
|
|
168
|
+
|
|
169
|
+
return app
|
code_puppy/api/main.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Entry point for running the FastAPI server."""
|
|
2
|
+
|
|
3
|
+
import uvicorn
|
|
4
|
+
|
|
5
|
+
from code_puppy.api.app import create_app
|
|
6
|
+
|
|
7
|
+
app = create_app()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
11
|
+
"""Run the FastAPI server.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
host: The host address to bind to. Defaults to localhost.
|
|
15
|
+
port: The port number to listen on. Defaults to 8765.
|
|
16
|
+
"""
|
|
17
|
+
uvicorn.run(app, host=host, port=port)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|