codepp 0.0.437__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 +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- 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 +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -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_scheduler.py +121 -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 +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -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 +453 -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 +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- 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 +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- 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 +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -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 +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -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 +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -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 +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -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 +470 -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/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -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/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- 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 +378 -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 +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Event stream handler for processing streaming events from agent runs."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import AsyncIterable
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic_ai import PartDeltaEvent, PartEndEvent, PartStartEvent, RunContext
|
|
9
|
+
from pydantic_ai.messages import (
|
|
10
|
+
TextPart,
|
|
11
|
+
TextPartDelta,
|
|
12
|
+
ThinkingPart,
|
|
13
|
+
ThinkingPartDelta,
|
|
14
|
+
ToolCallPart,
|
|
15
|
+
ToolCallPartDelta,
|
|
16
|
+
)
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.markup import escape
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
from code_puppy.config import get_banner_color, get_subagent_verbose
|
|
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
|
+
|
|
50
|
+
|
|
51
|
+
# Module-level console for streaming output
|
|
52
|
+
# Set via set_streaming_console() to share console with spinner
|
|
53
|
+
_streaming_console: Optional[Console] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_streaming_console(console: Optional[Console]) -> None:
|
|
57
|
+
"""Set the console used for streaming output.
|
|
58
|
+
|
|
59
|
+
This should be called with the same console used by the spinner
|
|
60
|
+
to avoid Live display conflicts that cause line duplication.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
console: The Rich console to use, or None to use a fallback.
|
|
64
|
+
"""
|
|
65
|
+
global _streaming_console
|
|
66
|
+
_streaming_console = console
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_streaming_console() -> Console:
|
|
70
|
+
"""Get the console for streaming output.
|
|
71
|
+
|
|
72
|
+
Returns the configured console or creates a fallback Console.
|
|
73
|
+
"""
|
|
74
|
+
if _streaming_console is not None:
|
|
75
|
+
return _streaming_console
|
|
76
|
+
return Console()
|
|
77
|
+
|
|
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
|
+
|
|
88
|
+
async def event_stream_handler(
|
|
89
|
+
ctx: RunContext,
|
|
90
|
+
events: AsyncIterable[Any],
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Handle streaming events from the agent run.
|
|
93
|
+
|
|
94
|
+
This function processes streaming events and emits TextPart, ThinkingPart,
|
|
95
|
+
and ToolCallPart content with styled banners/tokens as they stream in.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
ctx: The run context.
|
|
99
|
+
events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
|
|
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
|
+
|
|
107
|
+
from termflow import Parser as TermflowParser
|
|
108
|
+
from termflow import Renderer as TermflowRenderer
|
|
109
|
+
|
|
110
|
+
# Use the module-level console (set via set_streaming_console)
|
|
111
|
+
console = get_streaming_console()
|
|
112
|
+
|
|
113
|
+
# Track which part indices we're currently streaming (for Text/Thinking/Tool parts)
|
|
114
|
+
streaming_parts: set[int] = set()
|
|
115
|
+
thinking_parts: set[int] = set() # Track which parts are thinking (for dim style)
|
|
116
|
+
text_parts: set[int] = set() # Track which parts are text
|
|
117
|
+
tool_parts: set[int] = set() # Track which parts are tool calls
|
|
118
|
+
banner_printed: set[int] = set() # Track if banner was already printed
|
|
119
|
+
token_count: dict[int, int] = {} # Track token count per text/tool part
|
|
120
|
+
tool_names: dict[int, str] = {} # Track tool name per tool part index
|
|
121
|
+
did_stream_anything = False # Track if we streamed any content
|
|
122
|
+
|
|
123
|
+
# Termflow streaming state for text parts
|
|
124
|
+
termflow_parsers: dict[int, TermflowParser] = {}
|
|
125
|
+
termflow_renderers: dict[int, TermflowRenderer] = {}
|
|
126
|
+
termflow_line_buffers: dict[int, str] = {} # Buffer incomplete lines
|
|
127
|
+
|
|
128
|
+
async def _print_thinking_banner() -> None:
|
|
129
|
+
"""Print the THINKING banner with spinner pause and line clear."""
|
|
130
|
+
nonlocal did_stream_anything
|
|
131
|
+
|
|
132
|
+
pause_all_spinners()
|
|
133
|
+
await asyncio.sleep(0.1) # Delay to let spinner fully clear
|
|
134
|
+
# Clear line and print newline before banner
|
|
135
|
+
console.print(" " * 50, end="\r")
|
|
136
|
+
console.print() # Newline before banner
|
|
137
|
+
# Bold banner with configurable color and lightning bolt
|
|
138
|
+
thinking_color = get_banner_color("thinking")
|
|
139
|
+
console.print(
|
|
140
|
+
Text.from_markup(
|
|
141
|
+
f"[bold white on {thinking_color}] THINKING [/bold white on {thinking_color}] [dim]\u26a1 "
|
|
142
|
+
),
|
|
143
|
+
end="",
|
|
144
|
+
)
|
|
145
|
+
did_stream_anything = True
|
|
146
|
+
|
|
147
|
+
async def _print_response_banner() -> None:
|
|
148
|
+
"""Print the AGENT RESPONSE banner with spinner pause and line clear."""
|
|
149
|
+
nonlocal did_stream_anything
|
|
150
|
+
|
|
151
|
+
pause_all_spinners()
|
|
152
|
+
await asyncio.sleep(0.1) # Delay to let spinner fully clear
|
|
153
|
+
# Clear line and print newline before banner
|
|
154
|
+
console.print(" " * 50, end="\r")
|
|
155
|
+
console.print() # Newline before banner
|
|
156
|
+
response_color = get_banner_color("agent_response")
|
|
157
|
+
console.print(
|
|
158
|
+
Text.from_markup(
|
|
159
|
+
f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
did_stream_anything = True
|
|
163
|
+
|
|
164
|
+
async for event in events:
|
|
165
|
+
# PartStartEvent - register the part but defer banner until content arrives
|
|
166
|
+
if isinstance(event, PartStartEvent):
|
|
167
|
+
# Fire stream event callback for part_start
|
|
168
|
+
_fire_stream_event(
|
|
169
|
+
"part_start",
|
|
170
|
+
{
|
|
171
|
+
"index": event.index,
|
|
172
|
+
"part_type": type(event.part).__name__,
|
|
173
|
+
"part": event.part,
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
part = event.part
|
|
178
|
+
if isinstance(part, ThinkingPart):
|
|
179
|
+
streaming_parts.add(event.index)
|
|
180
|
+
thinking_parts.add(event.index)
|
|
181
|
+
# If there's initial content, print banner + content now
|
|
182
|
+
if part.content and part.content.strip():
|
|
183
|
+
await _print_thinking_banner()
|
|
184
|
+
escaped = escape(part.content)
|
|
185
|
+
console.print(f"[dim]{escaped}[/dim]", end="")
|
|
186
|
+
banner_printed.add(event.index)
|
|
187
|
+
elif isinstance(part, TextPart):
|
|
188
|
+
streaming_parts.add(event.index)
|
|
189
|
+
text_parts.add(event.index)
|
|
190
|
+
# Initialize termflow streaming for this text part
|
|
191
|
+
termflow_parsers[event.index] = TermflowParser()
|
|
192
|
+
termflow_renderers[event.index] = TermflowRenderer(
|
|
193
|
+
output=console.file, width=console.width
|
|
194
|
+
)
|
|
195
|
+
termflow_line_buffers[event.index] = ""
|
|
196
|
+
# Handle initial content if present
|
|
197
|
+
if part.content and part.content.strip():
|
|
198
|
+
await _print_response_banner()
|
|
199
|
+
banner_printed.add(event.index)
|
|
200
|
+
termflow_line_buffers[event.index] = part.content
|
|
201
|
+
elif isinstance(part, ToolCallPart):
|
|
202
|
+
streaming_parts.add(event.index)
|
|
203
|
+
tool_parts.add(event.index)
|
|
204
|
+
token_count[event.index] = 0 # Initialize token counter
|
|
205
|
+
# Capture tool name from the start event
|
|
206
|
+
tool_names[event.index] = part.tool_name or ""
|
|
207
|
+
# Track tool name for display
|
|
208
|
+
banner_printed.add(
|
|
209
|
+
event.index
|
|
210
|
+
) # Use banner_printed to track if we've shown tool info
|
|
211
|
+
|
|
212
|
+
# PartDeltaEvent - stream the content as it arrives
|
|
213
|
+
elif isinstance(event, PartDeltaEvent):
|
|
214
|
+
# Fire stream event callback for part_delta
|
|
215
|
+
_fire_stream_event(
|
|
216
|
+
"part_delta",
|
|
217
|
+
{
|
|
218
|
+
"index": event.index,
|
|
219
|
+
"delta_type": type(event.delta).__name__,
|
|
220
|
+
"delta": event.delta,
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if event.index in streaming_parts:
|
|
225
|
+
delta = event.delta
|
|
226
|
+
if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
|
|
227
|
+
if delta.content_delta:
|
|
228
|
+
# For text parts, stream markdown with termflow
|
|
229
|
+
if event.index in text_parts:
|
|
230
|
+
# Print banner on first content
|
|
231
|
+
if event.index not in banner_printed:
|
|
232
|
+
await _print_response_banner()
|
|
233
|
+
banner_printed.add(event.index)
|
|
234
|
+
|
|
235
|
+
# Add content to line buffer
|
|
236
|
+
termflow_line_buffers[event.index] += delta.content_delta
|
|
237
|
+
|
|
238
|
+
# Process complete lines
|
|
239
|
+
parser = termflow_parsers[event.index]
|
|
240
|
+
renderer = termflow_renderers[event.index]
|
|
241
|
+
buffer = termflow_line_buffers[event.index]
|
|
242
|
+
|
|
243
|
+
while "\n" in buffer:
|
|
244
|
+
line, buffer = buffer.split("\n", 1)
|
|
245
|
+
events_to_render = parser.parse_line(line)
|
|
246
|
+
renderer.render_all(events_to_render)
|
|
247
|
+
|
|
248
|
+
termflow_line_buffers[event.index] = buffer
|
|
249
|
+
else:
|
|
250
|
+
# For thinking parts, stream immediately (dim)
|
|
251
|
+
if event.index not in banner_printed:
|
|
252
|
+
await _print_thinking_banner()
|
|
253
|
+
banner_printed.add(event.index)
|
|
254
|
+
escaped = escape(delta.content_delta)
|
|
255
|
+
console.print(f"[dim]{escaped}[/dim]", end="")
|
|
256
|
+
elif isinstance(delta, ToolCallPartDelta):
|
|
257
|
+
# For tool calls, estimate tokens from args_delta content
|
|
258
|
+
# args_delta contains the streaming JSON arguments
|
|
259
|
+
args_delta = getattr(delta, "args_delta", "") or ""
|
|
260
|
+
if args_delta:
|
|
261
|
+
# Rough estimate: 4 chars ≈ 1 token (same heuristic as subagent_stream_handler)
|
|
262
|
+
estimated_tokens = max(1, len(args_delta) // 4)
|
|
263
|
+
token_count[event.index] += estimated_tokens
|
|
264
|
+
else:
|
|
265
|
+
# Even empty deltas count as activity
|
|
266
|
+
token_count[event.index] += 1
|
|
267
|
+
|
|
268
|
+
# Update tool name if delta provides more of it
|
|
269
|
+
tool_name_delta = getattr(delta, "tool_name_delta", "") or ""
|
|
270
|
+
if tool_name_delta:
|
|
271
|
+
tool_names[event.index] = (
|
|
272
|
+
tool_names.get(event.index, "") + tool_name_delta
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Use stored tool name for display
|
|
276
|
+
tool_name = tool_names.get(event.index, "")
|
|
277
|
+
count = token_count[event.index]
|
|
278
|
+
# Display with tool wrench icon and tool name
|
|
279
|
+
if tool_name:
|
|
280
|
+
console.print(
|
|
281
|
+
f" \U0001f527 Calling {tool_name}... {count} token(s) ",
|
|
282
|
+
end="\r",
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
console.print(
|
|
286
|
+
f" \U0001f527 Calling tool... {count} token(s) ",
|
|
287
|
+
end="\r",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# PartEndEvent - finish the streaming with a newline
|
|
291
|
+
elif isinstance(event, PartEndEvent):
|
|
292
|
+
# Fire stream event callback for part_end
|
|
293
|
+
_fire_stream_event(
|
|
294
|
+
"part_end",
|
|
295
|
+
{
|
|
296
|
+
"index": event.index,
|
|
297
|
+
"next_part_kind": getattr(event, "next_part_kind", None),
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if event.index in streaming_parts:
|
|
302
|
+
# For text parts, finalize termflow rendering
|
|
303
|
+
if event.index in text_parts:
|
|
304
|
+
# Render any remaining buffered content
|
|
305
|
+
if event.index in termflow_parsers:
|
|
306
|
+
parser = termflow_parsers[event.index]
|
|
307
|
+
renderer = termflow_renderers[event.index]
|
|
308
|
+
remaining = termflow_line_buffers.get(event.index, "")
|
|
309
|
+
|
|
310
|
+
# Parse and render any remaining partial line
|
|
311
|
+
if remaining.strip():
|
|
312
|
+
events_to_render = parser.parse_line(remaining)
|
|
313
|
+
renderer.render_all(events_to_render)
|
|
314
|
+
|
|
315
|
+
# Finalize the parser to close any open blocks
|
|
316
|
+
final_events = parser.finalize()
|
|
317
|
+
renderer.render_all(final_events)
|
|
318
|
+
|
|
319
|
+
# Clean up termflow state
|
|
320
|
+
del termflow_parsers[event.index]
|
|
321
|
+
del termflow_renderers[event.index]
|
|
322
|
+
del termflow_line_buffers[event.index]
|
|
323
|
+
# For tool parts, clear the chunk counter line
|
|
324
|
+
elif event.index in tool_parts:
|
|
325
|
+
# Clear the chunk counter line by printing spaces and returning
|
|
326
|
+
console.print(" " * 50, end="\r")
|
|
327
|
+
# For thinking parts, just print newline
|
|
328
|
+
elif event.index in banner_printed:
|
|
329
|
+
console.print() # Final newline after streaming
|
|
330
|
+
|
|
331
|
+
# Clean up token count and tool names
|
|
332
|
+
token_count.pop(event.index, None)
|
|
333
|
+
tool_names.pop(event.index, None)
|
|
334
|
+
# Clean up all tracking sets
|
|
335
|
+
streaming_parts.discard(event.index)
|
|
336
|
+
thinking_parts.discard(event.index)
|
|
337
|
+
text_parts.discard(event.index)
|
|
338
|
+
tool_parts.discard(event.index)
|
|
339
|
+
banner_printed.discard(event.index)
|
|
340
|
+
|
|
341
|
+
# Resume spinner if next part is NOT text/thinking/tool (avoid race condition)
|
|
342
|
+
# If next part is None or handled differently, it's safe to resume
|
|
343
|
+
# Note: spinner itself handles blank line before appearing
|
|
344
|
+
next_kind = getattr(event, "next_part_kind", None)
|
|
345
|
+
if next_kind not in ("text", "thinking", "tool-call"):
|
|
346
|
+
resume_all_spinners()
|
|
347
|
+
|
|
348
|
+
# Spinner is resumed in PartEndEvent when appropriate (based on next_part_kind)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""JSON-based agent configuration system."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from .base_agent import BaseAgent
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JSONAgent(BaseAgent):
|
|
14
|
+
"""Agent configured from a JSON file."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, json_path: str):
|
|
17
|
+
"""Initialize agent from JSON file.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
json_path: Path to the JSON configuration file.
|
|
21
|
+
"""
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.json_path = json_path
|
|
24
|
+
self._config = self._load_config()
|
|
25
|
+
self._validate_config()
|
|
26
|
+
|
|
27
|
+
def _load_config(self) -> Dict:
|
|
28
|
+
"""Load configuration from JSON file."""
|
|
29
|
+
try:
|
|
30
|
+
with open(self.json_path, "r", encoding="utf-8") as f:
|
|
31
|
+
return json.load(f)
|
|
32
|
+
except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Failed to load JSON agent config from {self.json_path}: {e}"
|
|
35
|
+
) from e
|
|
36
|
+
|
|
37
|
+
def _validate_config(self) -> None:
|
|
38
|
+
"""Validate required fields in configuration."""
|
|
39
|
+
required_fields = ["name", "description", "system_prompt", "tools"]
|
|
40
|
+
for field in required_fields:
|
|
41
|
+
if field not in self._config:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Missing required field '{field}' in JSON agent config: {self.json_path}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Validate tools is a list
|
|
47
|
+
if not isinstance(self._config["tools"], list):
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"'tools' must be a list in JSON agent config: {self.json_path}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Validate system_prompt is string or list
|
|
53
|
+
system_prompt = self._config["system_prompt"]
|
|
54
|
+
if not isinstance(system_prompt, (str, list)):
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"'system_prompt' must be a string or list in JSON agent config: {self.json_path}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def name(self) -> str:
|
|
61
|
+
"""Get agent name from JSON config."""
|
|
62
|
+
return self._config["name"]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def display_name(self) -> str:
|
|
66
|
+
"""Get display name from JSON config, fallback to name with emoji."""
|
|
67
|
+
return self._config.get("display_name", f"{self.name.title()} 🤖")
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def description(self) -> str:
|
|
71
|
+
"""Get description from JSON config."""
|
|
72
|
+
return self._config["description"]
|
|
73
|
+
|
|
74
|
+
def get_system_prompt(self) -> str:
|
|
75
|
+
"""Get system prompt from JSON config."""
|
|
76
|
+
system_prompt = self._config["system_prompt"]
|
|
77
|
+
|
|
78
|
+
# If it's a list, join with newlines
|
|
79
|
+
if isinstance(system_prompt, list):
|
|
80
|
+
return "\n".join(system_prompt)
|
|
81
|
+
|
|
82
|
+
return system_prompt
|
|
83
|
+
|
|
84
|
+
def get_available_tools(self) -> List[str]:
|
|
85
|
+
"""Get available tools from JSON config.
|
|
86
|
+
|
|
87
|
+
Supports both built-in tools and Universal Constructor (UC) tools.
|
|
88
|
+
UC tools are identified by checking the UC registry.
|
|
89
|
+
"""
|
|
90
|
+
from code_puppy.tools import get_available_tool_names
|
|
91
|
+
|
|
92
|
+
available_tools = get_available_tool_names()
|
|
93
|
+
|
|
94
|
+
# Also get UC tool names
|
|
95
|
+
uc_tool_names = set()
|
|
96
|
+
try:
|
|
97
|
+
from code_puppy.plugins.universal_constructor.registry import get_registry
|
|
98
|
+
|
|
99
|
+
registry = get_registry()
|
|
100
|
+
for tool in registry.list_tools():
|
|
101
|
+
if tool.meta.enabled:
|
|
102
|
+
uc_tool_names.add(tool.full_name)
|
|
103
|
+
except ImportError:
|
|
104
|
+
pass # UC module not available
|
|
105
|
+
except Exception as e:
|
|
106
|
+
# Log unexpected errors but don't fail
|
|
107
|
+
import logging
|
|
108
|
+
|
|
109
|
+
logging.debug(f"UC registry access failed: {e}")
|
|
110
|
+
|
|
111
|
+
# Return tools that are either built-in OR UC tools
|
|
112
|
+
requested_tools = []
|
|
113
|
+
for tool in self._config["tools"]:
|
|
114
|
+
if tool in available_tools:
|
|
115
|
+
requested_tools.append(tool)
|
|
116
|
+
elif tool in uc_tool_names:
|
|
117
|
+
# UC tool - mark it specially so base_agent knows to handle it
|
|
118
|
+
requested_tools.append(f"uc:{tool}")
|
|
119
|
+
|
|
120
|
+
return requested_tools
|
|
121
|
+
|
|
122
|
+
def get_user_prompt(self) -> Optional[str]:
|
|
123
|
+
"""Get custom user prompt from JSON config."""
|
|
124
|
+
return self._config.get("user_prompt")
|
|
125
|
+
|
|
126
|
+
def get_tools_config(self) -> Optional[Dict]:
|
|
127
|
+
"""Get tool configuration from JSON config."""
|
|
128
|
+
return self._config.get("tools_config")
|
|
129
|
+
|
|
130
|
+
def refresh_config(self) -> None:
|
|
131
|
+
"""Reload the agent configuration from disk.
|
|
132
|
+
|
|
133
|
+
This keeps long-lived agent instances in sync after external edits.
|
|
134
|
+
"""
|
|
135
|
+
self._config = self._load_config()
|
|
136
|
+
self._validate_config()
|
|
137
|
+
|
|
138
|
+
def get_model_name(self) -> Optional[str]:
|
|
139
|
+
"""Get pinned model name from JSON config, if specified.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Model name to use for this agent, or None to use global default.
|
|
143
|
+
"""
|
|
144
|
+
result = self._config.get("model")
|
|
145
|
+
if result is None:
|
|
146
|
+
result = super().get_model_name()
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def discover_json_agents() -> Dict[str, str]:
|
|
151
|
+
"""Discover JSON agent files in the user's and project's agents directories.
|
|
152
|
+
|
|
153
|
+
Searches two locations:
|
|
154
|
+
1. User agents directory (~/.code_puppy/agents/)
|
|
155
|
+
2. Project agents directory (<CWD>/.code_puppy/agents/) - if it exists
|
|
156
|
+
|
|
157
|
+
Project agents take priority over user agents when names collide.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Dict mapping agent names to their JSON file paths.
|
|
161
|
+
"""
|
|
162
|
+
from code_puppy.config import (
|
|
163
|
+
get_project_agents_directory,
|
|
164
|
+
get_user_agents_directory,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
agents: Dict[str, str] = {}
|
|
168
|
+
|
|
169
|
+
# 1. Discover user-level agents first
|
|
170
|
+
user_agents_dir = Path(get_user_agents_directory())
|
|
171
|
+
if user_agents_dir.exists() and user_agents_dir.is_dir():
|
|
172
|
+
for json_file in user_agents_dir.glob("*.json"):
|
|
173
|
+
try:
|
|
174
|
+
agent = JSONAgent(str(json_file))
|
|
175
|
+
agents[agent.name] = str(json_file)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.debug(
|
|
178
|
+
"Skipping invalid user agent file: %s (reason: %s: %s)",
|
|
179
|
+
json_file,
|
|
180
|
+
type(e).__name__,
|
|
181
|
+
str(e),
|
|
182
|
+
)
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# 2. Discover project-level agents (overrides user agents on name collision)
|
|
186
|
+
project_agents_dir_str = get_project_agents_directory()
|
|
187
|
+
if project_agents_dir_str is not None:
|
|
188
|
+
project_agents_dir = Path(project_agents_dir_str)
|
|
189
|
+
for json_file in project_agents_dir.glob("*.json"):
|
|
190
|
+
try:
|
|
191
|
+
agent = JSONAgent(str(json_file))
|
|
192
|
+
agents[agent.name] = str(json_file)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.debug(
|
|
195
|
+
"Skipping invalid project agent file: %s (reason: %s: %s)",
|
|
196
|
+
json_file,
|
|
197
|
+
type(e).__name__,
|
|
198
|
+
str(e),
|
|
199
|
+
)
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
return agents
|
|
@@ -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
|
+
]
|