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,742 @@
|
|
|
1
|
+
"""Agent manager for handling different agent configurations."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import pkgutil
|
|
7
|
+
import re
|
|
8
|
+
import threading
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Type, Union
|
|
12
|
+
|
|
13
|
+
from pydantic_ai.messages import ModelMessage
|
|
14
|
+
|
|
15
|
+
from code_puppy.agents.base_agent import BaseAgent
|
|
16
|
+
from code_puppy.agents.json_agent import JSONAgent, discover_json_agents
|
|
17
|
+
from code_puppy.callbacks import on_agent_reload, on_register_agents
|
|
18
|
+
from code_puppy.messaging import emit_success, emit_warning
|
|
19
|
+
|
|
20
|
+
# Registry of available agents (Python classes and JSON file paths)
|
|
21
|
+
_AGENT_REGISTRY: Dict[str, Union[Type[BaseAgent], str]] = {}
|
|
22
|
+
_AGENT_HISTORIES: Dict[str, List[ModelMessage]] = {}
|
|
23
|
+
_CURRENT_AGENT: Optional[BaseAgent] = None
|
|
24
|
+
|
|
25
|
+
# Terminal session-based agent selection
|
|
26
|
+
_SESSION_AGENTS_CACHE: dict[str, str] = {}
|
|
27
|
+
_SESSION_FILE_LOADED: bool = False
|
|
28
|
+
_SESSION_LOCK = threading.Lock()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Session persistence file path
|
|
32
|
+
def _get_session_file_path() -> Path:
|
|
33
|
+
"""Get the path to the terminal sessions file."""
|
|
34
|
+
from ..config import STATE_DIR
|
|
35
|
+
|
|
36
|
+
return Path(STATE_DIR) / "terminal_sessions.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_terminal_session_id() -> str:
|
|
40
|
+
"""Get a unique identifier for the current terminal session.
|
|
41
|
+
|
|
42
|
+
Uses parent process ID (PPID) as the session identifier.
|
|
43
|
+
This works across all platforms and provides session isolation.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: Unique session identifier (e.g., "session_12345")
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
ppid = os.getppid()
|
|
50
|
+
return f"session_{ppid}"
|
|
51
|
+
except (OSError, AttributeError):
|
|
52
|
+
# Fallback to current process ID if PPID unavailable
|
|
53
|
+
return f"fallback_{os.getpid()}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_process_alive(pid: int) -> bool:
|
|
57
|
+
"""Check if a process with the given PID is still alive, cross-platform.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
pid: Process ID to check
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
bool: True if process likely exists, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
if os.name == "nt":
|
|
67
|
+
# Windows: use OpenProcess to probe liveness safely
|
|
68
|
+
import ctypes
|
|
69
|
+
from ctypes import wintypes
|
|
70
|
+
|
|
71
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
72
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
73
|
+
kernel32.OpenProcess.argtypes = [
|
|
74
|
+
wintypes.DWORD,
|
|
75
|
+
wintypes.BOOL,
|
|
76
|
+
wintypes.DWORD,
|
|
77
|
+
]
|
|
78
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
79
|
+
handle = kernel32.OpenProcess(
|
|
80
|
+
PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid)
|
|
81
|
+
)
|
|
82
|
+
if handle:
|
|
83
|
+
kernel32.CloseHandle(handle)
|
|
84
|
+
return True
|
|
85
|
+
# If access denied, process likely exists but we can't query it
|
|
86
|
+
last_error = kernel32.GetLastError()
|
|
87
|
+
# ERROR_ACCESS_DENIED = 5
|
|
88
|
+
if last_error == 5:
|
|
89
|
+
return True
|
|
90
|
+
return False
|
|
91
|
+
else:
|
|
92
|
+
# Unix-like: signal 0 does not deliver a signal but checks existence
|
|
93
|
+
os.kill(int(pid), 0)
|
|
94
|
+
return True
|
|
95
|
+
except PermissionError:
|
|
96
|
+
# No permission to signal -> process exists
|
|
97
|
+
return True
|
|
98
|
+
except (OSError, ProcessLookupError):
|
|
99
|
+
# Process does not exist
|
|
100
|
+
return False
|
|
101
|
+
except ValueError:
|
|
102
|
+
# Invalid signal or pid format
|
|
103
|
+
return False
|
|
104
|
+
except Exception:
|
|
105
|
+
# Be conservative – don't crash session cleanup due to platform quirks
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cleanup_dead_sessions(sessions: dict[str, str]) -> dict[str, str]:
|
|
110
|
+
"""Remove sessions for processes that no longer exist.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
sessions: Dictionary of session_id -> agent_name
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
dict: Cleaned sessions dictionary
|
|
117
|
+
"""
|
|
118
|
+
cleaned = {}
|
|
119
|
+
for session_id, agent_name in sessions.items():
|
|
120
|
+
if session_id.startswith("session_"):
|
|
121
|
+
try:
|
|
122
|
+
pid_str = session_id.replace("session_", "")
|
|
123
|
+
pid = int(pid_str)
|
|
124
|
+
if _is_process_alive(pid):
|
|
125
|
+
cleaned[session_id] = agent_name
|
|
126
|
+
# else: skip dead session
|
|
127
|
+
except (ValueError, TypeError):
|
|
128
|
+
# Invalid session ID format, keep it anyway
|
|
129
|
+
cleaned[session_id] = agent_name
|
|
130
|
+
else:
|
|
131
|
+
# Non-standard session ID (like "fallback_"), keep it
|
|
132
|
+
cleaned[session_id] = agent_name
|
|
133
|
+
return cleaned
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load_session_data() -> dict[str, str]:
|
|
137
|
+
"""Load terminal session data from the JSON file.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
dict: Session ID to agent name mapping
|
|
141
|
+
"""
|
|
142
|
+
session_file = _get_session_file_path()
|
|
143
|
+
try:
|
|
144
|
+
if session_file.exists():
|
|
145
|
+
with open(session_file, "r", encoding="utf-8") as f:
|
|
146
|
+
data = json.load(f)
|
|
147
|
+
# Clean up dead sessions while loading
|
|
148
|
+
return _cleanup_dead_sessions(data)
|
|
149
|
+
return {}
|
|
150
|
+
except (json.JSONDecodeError, IOError, OSError):
|
|
151
|
+
# File corrupted or permission issues, start fresh
|
|
152
|
+
return {}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _save_session_data(sessions: dict[str, str]) -> None:
|
|
156
|
+
"""Save terminal session data to the JSON file.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
sessions: Session ID to agent name mapping
|
|
160
|
+
"""
|
|
161
|
+
session_file = _get_session_file_path()
|
|
162
|
+
try:
|
|
163
|
+
# Ensure the config directory exists
|
|
164
|
+
session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
|
|
166
|
+
# Clean up dead sessions before saving
|
|
167
|
+
cleaned_sessions = _cleanup_dead_sessions(sessions)
|
|
168
|
+
|
|
169
|
+
# Write to file atomically (write to temp file, then rename)
|
|
170
|
+
temp_file = session_file.with_suffix(".tmp")
|
|
171
|
+
with open(temp_file, "w", encoding="utf-8") as f:
|
|
172
|
+
json.dump(cleaned_sessions, f, indent=2)
|
|
173
|
+
|
|
174
|
+
# Atomic rename (works on all platforms)
|
|
175
|
+
temp_file.replace(session_file)
|
|
176
|
+
|
|
177
|
+
except (IOError, OSError):
|
|
178
|
+
# File permission issues, etc. - just continue without persistence
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _ensure_session_cache_loaded() -> None:
|
|
183
|
+
"""Ensure the session cache is loaded from disk."""
|
|
184
|
+
global _SESSION_AGENTS_CACHE, _SESSION_FILE_LOADED
|
|
185
|
+
with _SESSION_LOCK:
|
|
186
|
+
if not _SESSION_FILE_LOADED:
|
|
187
|
+
_SESSION_AGENTS_CACHE.update(_load_session_data())
|
|
188
|
+
_SESSION_FILE_LOADED = True
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _discover_agents(message_group_id: Optional[str] = None):
|
|
192
|
+
"""Dynamically discover all agent classes and JSON agents."""
|
|
193
|
+
# Always clear the registry to force refresh
|
|
194
|
+
_AGENT_REGISTRY.clear()
|
|
195
|
+
|
|
196
|
+
# 1. Discover Python agent classes in the agents package
|
|
197
|
+
import code_puppy.agents as agents_package
|
|
198
|
+
|
|
199
|
+
# Iterate through all modules in the agents package
|
|
200
|
+
for _, modname, _ in pkgutil.iter_modules(agents_package.__path__):
|
|
201
|
+
if modname.startswith("_") or modname in [
|
|
202
|
+
"base_agent",
|
|
203
|
+
"json_agent",
|
|
204
|
+
"agent_manager",
|
|
205
|
+
]:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
# Import the module
|
|
210
|
+
module = importlib.import_module(f"code_puppy.agents.{modname}")
|
|
211
|
+
|
|
212
|
+
# Look for BaseAgent subclasses
|
|
213
|
+
for attr_name in dir(module):
|
|
214
|
+
attr = getattr(module, attr_name)
|
|
215
|
+
if (
|
|
216
|
+
isinstance(attr, type)
|
|
217
|
+
and issubclass(attr, BaseAgent)
|
|
218
|
+
and attr not in [BaseAgent, JSONAgent]
|
|
219
|
+
):
|
|
220
|
+
# Create an instance to get the name
|
|
221
|
+
agent_instance = attr()
|
|
222
|
+
_AGENT_REGISTRY[agent_instance.name] = attr
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
# Skip problematic modules
|
|
226
|
+
emit_warning(
|
|
227
|
+
f"Warning: Could not load agent module {modname}: {e}",
|
|
228
|
+
message_group=message_group_id,
|
|
229
|
+
)
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
# 1b. Discover agents in sub-packages (like 'pack')
|
|
233
|
+
for _, subpkg_name, ispkg in pkgutil.iter_modules(agents_package.__path__):
|
|
234
|
+
if not ispkg or subpkg_name.startswith("_"):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Import the sub-package
|
|
239
|
+
subpkg = importlib.import_module(f"code_puppy.agents.{subpkg_name}")
|
|
240
|
+
|
|
241
|
+
# Iterate through modules in the sub-package
|
|
242
|
+
if not hasattr(subpkg, "__path__"):
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
for _, modname, _ in pkgutil.iter_modules(subpkg.__path__):
|
|
246
|
+
if modname.startswith("_"):
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
# Import the submodule
|
|
251
|
+
module = importlib.import_module(
|
|
252
|
+
f"code_puppy.agents.{subpkg_name}.{modname}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Look for BaseAgent subclasses
|
|
256
|
+
for attr_name in dir(module):
|
|
257
|
+
attr = getattr(module, attr_name)
|
|
258
|
+
if (
|
|
259
|
+
isinstance(attr, type)
|
|
260
|
+
and issubclass(attr, BaseAgent)
|
|
261
|
+
and attr not in [BaseAgent, JSONAgent]
|
|
262
|
+
):
|
|
263
|
+
# Create an instance to get the name
|
|
264
|
+
agent_instance = attr()
|
|
265
|
+
_AGENT_REGISTRY[agent_instance.name] = attr
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
emit_warning(
|
|
269
|
+
f"Warning: Could not load agent {subpkg_name}.{modname}: {e}",
|
|
270
|
+
message_group=message_group_id,
|
|
271
|
+
)
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
emit_warning(
|
|
276
|
+
f"Warning: Could not load agent sub-package {subpkg_name}: {e}",
|
|
277
|
+
message_group=message_group_id,
|
|
278
|
+
)
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# 2. Discover JSON agents in user directory
|
|
282
|
+
try:
|
|
283
|
+
json_agents = discover_json_agents()
|
|
284
|
+
|
|
285
|
+
# Add JSON agents to registry (store file path instead of class)
|
|
286
|
+
# Python (builtin) agents take precedence over JSON agents.
|
|
287
|
+
for agent_name, json_path in json_agents.items():
|
|
288
|
+
if agent_name in _AGENT_REGISTRY:
|
|
289
|
+
emit_warning(
|
|
290
|
+
f"JSON agent '{agent_name}' skipped: builtin Python agent with the same name takes precedence.",
|
|
291
|
+
message_group=message_group_id,
|
|
292
|
+
)
|
|
293
|
+
continue
|
|
294
|
+
_AGENT_REGISTRY[agent_name] = json_path
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
emit_warning(
|
|
298
|
+
f"Warning: Could not discover JSON agents: {e}",
|
|
299
|
+
message_group=message_group_id,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# 3. Discover agents registered by plugins
|
|
303
|
+
try:
|
|
304
|
+
results = on_register_agents()
|
|
305
|
+
for result in results:
|
|
306
|
+
if result is None:
|
|
307
|
+
continue
|
|
308
|
+
# Each result should be a list of agent definitions
|
|
309
|
+
agents_list = result if isinstance(result, list) else [result]
|
|
310
|
+
for agent_def in agents_list:
|
|
311
|
+
if not isinstance(agent_def, dict) or "name" not in agent_def:
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
agent_name = agent_def["name"]
|
|
315
|
+
|
|
316
|
+
# Support both class-based and JSON path-based registration
|
|
317
|
+
if "class" in agent_def:
|
|
318
|
+
agent_class = agent_def["class"]
|
|
319
|
+
if isinstance(agent_class, type) and issubclass(
|
|
320
|
+
agent_class, BaseAgent
|
|
321
|
+
):
|
|
322
|
+
_AGENT_REGISTRY[agent_name] = agent_class
|
|
323
|
+
elif "json_path" in agent_def:
|
|
324
|
+
json_path = agent_def["json_path"]
|
|
325
|
+
if isinstance(json_path, str):
|
|
326
|
+
_AGENT_REGISTRY[agent_name] = json_path
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
emit_warning(
|
|
330
|
+
f"Warning: Could not load plugin agents: {e}",
|
|
331
|
+
message_group=message_group_id,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_available_agents() -> Dict[str, str]:
|
|
336
|
+
"""Get a dictionary of available agents with their display names.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Dict mapping agent names to display names.
|
|
340
|
+
"""
|
|
341
|
+
from ..config import (
|
|
342
|
+
PACK_AGENT_NAMES,
|
|
343
|
+
UC_AGENT_NAMES,
|
|
344
|
+
get_pack_agents_enabled,
|
|
345
|
+
get_universal_constructor_enabled,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Generate a message group ID for this operation
|
|
349
|
+
message_group_id = str(uuid.uuid4())
|
|
350
|
+
_discover_agents(message_group_id=message_group_id)
|
|
351
|
+
|
|
352
|
+
# Check if pack agents are enabled
|
|
353
|
+
pack_agents_enabled = get_pack_agents_enabled()
|
|
354
|
+
|
|
355
|
+
# Check if UC is enabled
|
|
356
|
+
uc_enabled = get_universal_constructor_enabled()
|
|
357
|
+
|
|
358
|
+
agents = {}
|
|
359
|
+
for name, agent_ref in _AGENT_REGISTRY.items():
|
|
360
|
+
# Filter out pack agents if disabled
|
|
361
|
+
if not pack_agents_enabled and name in PACK_AGENT_NAMES:
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Filter out UC-dependent agents if UC is disabled
|
|
365
|
+
if not uc_enabled and name in UC_AGENT_NAMES:
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
if isinstance(agent_ref, str): # JSON agent (file path)
|
|
370
|
+
agent_instance = JSONAgent(agent_ref)
|
|
371
|
+
else: # Python agent (class)
|
|
372
|
+
agent_instance = agent_ref()
|
|
373
|
+
agents[name] = agent_instance.display_name
|
|
374
|
+
except Exception:
|
|
375
|
+
agents[name] = name.title() # Fallback
|
|
376
|
+
|
|
377
|
+
return agents
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def get_current_agent_name() -> str:
|
|
381
|
+
"""Get the name of the currently active agent for this terminal session.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
The name of the current agent for this session.
|
|
385
|
+
Priority: session agent > config default > 'code-puppy'.
|
|
386
|
+
"""
|
|
387
|
+
_ensure_session_cache_loaded()
|
|
388
|
+
session_id = get_terminal_session_id()
|
|
389
|
+
|
|
390
|
+
# First check for session-specific agent
|
|
391
|
+
with _SESSION_LOCK:
|
|
392
|
+
session_agent = _SESSION_AGENTS_CACHE.get(session_id)
|
|
393
|
+
if session_agent:
|
|
394
|
+
return session_agent
|
|
395
|
+
|
|
396
|
+
# Fall back to config default
|
|
397
|
+
from ..config import get_default_agent
|
|
398
|
+
|
|
399
|
+
return get_default_agent()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def set_current_agent(agent_name: str) -> bool:
|
|
403
|
+
"""Set the current agent by name.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
agent_name: The name of the agent to set as current.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
True if the agent was set successfully, False if agent not found.
|
|
410
|
+
"""
|
|
411
|
+
global _CURRENT_AGENT
|
|
412
|
+
curr_agent = get_current_agent()
|
|
413
|
+
if curr_agent is not None:
|
|
414
|
+
# Store a shallow copy so future mutations don't affect saved history
|
|
415
|
+
_AGENT_HISTORIES[curr_agent.name] = list(curr_agent.get_message_history())
|
|
416
|
+
# Generate a message group ID for agent switching
|
|
417
|
+
message_group_id = str(uuid.uuid4())
|
|
418
|
+
_discover_agents(message_group_id=message_group_id)
|
|
419
|
+
|
|
420
|
+
# Save current agent's history before switching
|
|
421
|
+
|
|
422
|
+
# Clear the cached config when switching agents
|
|
423
|
+
agent_obj = load_agent(agent_name)
|
|
424
|
+
_CURRENT_AGENT = agent_obj
|
|
425
|
+
|
|
426
|
+
# Update session-based agent selection and persist to disk
|
|
427
|
+
_ensure_session_cache_loaded()
|
|
428
|
+
session_id = get_terminal_session_id()
|
|
429
|
+
with _SESSION_LOCK:
|
|
430
|
+
_SESSION_AGENTS_CACHE[session_id] = agent_name
|
|
431
|
+
cache_snapshot = dict(_SESSION_AGENTS_CACHE)
|
|
432
|
+
_save_session_data(cache_snapshot)
|
|
433
|
+
if agent_obj.name in _AGENT_HISTORIES:
|
|
434
|
+
# Restore a copy to avoid sharing the same list instance
|
|
435
|
+
agent_obj.set_message_history(list(_AGENT_HISTORIES[agent_obj.name]))
|
|
436
|
+
on_agent_reload(agent_obj.id, agent_name)
|
|
437
|
+
return True
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def get_current_agent() -> BaseAgent:
|
|
441
|
+
"""Get the current agent configuration.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
The current agent configuration instance.
|
|
445
|
+
"""
|
|
446
|
+
global _CURRENT_AGENT
|
|
447
|
+
|
|
448
|
+
if _CURRENT_AGENT is None:
|
|
449
|
+
agent_name = get_current_agent_name()
|
|
450
|
+
_CURRENT_AGENT = load_agent(agent_name)
|
|
451
|
+
|
|
452
|
+
return _CURRENT_AGENT
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def load_agent(agent_name: str) -> BaseAgent:
|
|
456
|
+
"""Load an agent configuration by name.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
agent_name: The name of the agent to load.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
The agent configuration instance.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
ValueError: If the agent is not found.
|
|
466
|
+
"""
|
|
467
|
+
# Generate a message group ID for agent loading
|
|
468
|
+
message_group_id = str(uuid.uuid4())
|
|
469
|
+
_discover_agents(message_group_id=message_group_id)
|
|
470
|
+
|
|
471
|
+
if agent_name not in _AGENT_REGISTRY:
|
|
472
|
+
# Fallback to code-puppy if agent not found
|
|
473
|
+
if "code-puppy" in _AGENT_REGISTRY:
|
|
474
|
+
agent_name = "code-puppy"
|
|
475
|
+
else:
|
|
476
|
+
raise ValueError(
|
|
477
|
+
f"Agent '{agent_name}' not found and no fallback available"
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
agent_ref = _AGENT_REGISTRY[agent_name]
|
|
481
|
+
if isinstance(agent_ref, str): # JSON agent (file path)
|
|
482
|
+
return JSONAgent(agent_ref)
|
|
483
|
+
else: # Python agent (class)
|
|
484
|
+
return agent_ref()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def get_agent_descriptions() -> Dict[str, str]:
|
|
488
|
+
"""Get descriptions for all available agents.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
Dict mapping agent names to their descriptions.
|
|
492
|
+
"""
|
|
493
|
+
from ..config import (
|
|
494
|
+
PACK_AGENT_NAMES,
|
|
495
|
+
UC_AGENT_NAMES,
|
|
496
|
+
get_pack_agents_enabled,
|
|
497
|
+
get_universal_constructor_enabled,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Generate a message group ID for this operation
|
|
501
|
+
message_group_id = str(uuid.uuid4())
|
|
502
|
+
_discover_agents(message_group_id=message_group_id)
|
|
503
|
+
|
|
504
|
+
# Check if pack agents are enabled
|
|
505
|
+
pack_agents_enabled = get_pack_agents_enabled()
|
|
506
|
+
|
|
507
|
+
# Check if UC is enabled
|
|
508
|
+
uc_enabled = get_universal_constructor_enabled()
|
|
509
|
+
|
|
510
|
+
descriptions = {}
|
|
511
|
+
for name, agent_ref in _AGENT_REGISTRY.items():
|
|
512
|
+
# Filter out pack agents if disabled
|
|
513
|
+
if not pack_agents_enabled and name in PACK_AGENT_NAMES:
|
|
514
|
+
continue
|
|
515
|
+
|
|
516
|
+
# Filter out UC-dependent agents if UC is disabled
|
|
517
|
+
if not uc_enabled and name in UC_AGENT_NAMES:
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
if isinstance(agent_ref, str): # JSON agent (file path)
|
|
522
|
+
agent_instance = JSONAgent(agent_ref)
|
|
523
|
+
else: # Python agent (class)
|
|
524
|
+
agent_instance = agent_ref()
|
|
525
|
+
descriptions[name] = agent_instance.description
|
|
526
|
+
except Exception:
|
|
527
|
+
descriptions[name] = "No description available"
|
|
528
|
+
|
|
529
|
+
return descriptions
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def refresh_agents():
|
|
533
|
+
"""Refresh the agent discovery to pick up newly created agents.
|
|
534
|
+
|
|
535
|
+
This clears the agent registry cache and forces a rediscovery of all agents.
|
|
536
|
+
"""
|
|
537
|
+
# Generate a message group ID for agent refreshing
|
|
538
|
+
message_group_id = str(uuid.uuid4())
|
|
539
|
+
_discover_agents(message_group_id=message_group_id)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
_CLONE_NAME_PATTERN = re.compile(r"^(?P<base>.+)-clone-(?P<index>\d+)$")
|
|
543
|
+
_CLONE_DISPLAY_PATTERN = re.compile(r"\s*\(Clone\s+\d+\)$", re.IGNORECASE)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _strip_clone_suffix(agent_name: str) -> str:
|
|
547
|
+
"""Strip a trailing -clone-N suffix from a name if present."""
|
|
548
|
+
match = _CLONE_NAME_PATTERN.match(agent_name)
|
|
549
|
+
return match.group("base") if match else agent_name
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _strip_clone_display_suffix(display_name: str) -> str:
|
|
553
|
+
"""Remove a trailing "(Clone N)" suffix from display names."""
|
|
554
|
+
cleaned = _CLONE_DISPLAY_PATTERN.sub("", display_name).strip()
|
|
555
|
+
return cleaned or display_name
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def is_clone_agent_name(agent_name: str) -> bool:
|
|
559
|
+
"""Return True if the agent name looks like a clone."""
|
|
560
|
+
return bool(_CLONE_NAME_PATTERN.match(agent_name))
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _default_display_name(agent_name: str) -> str:
|
|
564
|
+
"""Build a default display name from an agent name."""
|
|
565
|
+
title = agent_name.title()
|
|
566
|
+
return f"{title} 🤖"
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _build_clone_display_name(display_name: str, clone_index: int) -> str:
|
|
570
|
+
"""Build a clone display name based on the source display name."""
|
|
571
|
+
base_name = _strip_clone_display_suffix(display_name)
|
|
572
|
+
return f"{base_name} (Clone {clone_index})"
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _filter_available_tools(tool_names: List[str]) -> List[str]:
|
|
576
|
+
"""Filter a tool list to only available tool names."""
|
|
577
|
+
from code_puppy.tools import get_available_tool_names
|
|
578
|
+
|
|
579
|
+
available_tools = set(get_available_tool_names())
|
|
580
|
+
return [tool for tool in tool_names if tool in available_tools]
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _next_clone_index(
|
|
584
|
+
base_name: str, existing_names: set[str], agents_dir: Path
|
|
585
|
+
) -> int:
|
|
586
|
+
"""Compute the next clone index for a base name."""
|
|
587
|
+
clone_pattern = re.compile(rf"^{re.escape(base_name)}-clone-(\\d+)$")
|
|
588
|
+
indices = []
|
|
589
|
+
for name in existing_names:
|
|
590
|
+
match = clone_pattern.match(name)
|
|
591
|
+
if match:
|
|
592
|
+
indices.append(int(match.group(1)))
|
|
593
|
+
|
|
594
|
+
next_index = max(indices, default=0) + 1
|
|
595
|
+
while True:
|
|
596
|
+
clone_name = f"{base_name}-clone-{next_index}"
|
|
597
|
+
clone_path = agents_dir / f"{clone_name}.json"
|
|
598
|
+
if clone_name not in existing_names and not clone_path.exists():
|
|
599
|
+
return next_index
|
|
600
|
+
next_index += 1
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def clone_agent(agent_name: str) -> Optional[str]:
|
|
604
|
+
"""Clone an agent definition into the user agents directory.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
agent_name: Source agent name to clone.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
The cloned agent name, or None if cloning failed.
|
|
611
|
+
"""
|
|
612
|
+
# Generate a message group ID for agent cloning
|
|
613
|
+
message_group_id = str(uuid.uuid4())
|
|
614
|
+
_discover_agents(message_group_id=message_group_id)
|
|
615
|
+
|
|
616
|
+
agent_ref = _AGENT_REGISTRY.get(agent_name)
|
|
617
|
+
if agent_ref is None:
|
|
618
|
+
emit_warning(f"Agent '{agent_name}' not found for cloning.")
|
|
619
|
+
return None
|
|
620
|
+
|
|
621
|
+
from ..config import get_agent_pinned_model, get_user_agents_directory
|
|
622
|
+
|
|
623
|
+
agents_dir = Path(get_user_agents_directory())
|
|
624
|
+
base_name = _strip_clone_suffix(agent_name)
|
|
625
|
+
existing_names = set(_AGENT_REGISTRY.keys())
|
|
626
|
+
clone_index = _next_clone_index(base_name, existing_names, agents_dir)
|
|
627
|
+
clone_name = f"{base_name}-clone-{clone_index}"
|
|
628
|
+
clone_path = agents_dir / f"{clone_name}.json"
|
|
629
|
+
|
|
630
|
+
try:
|
|
631
|
+
if isinstance(agent_ref, str):
|
|
632
|
+
with open(agent_ref, "r", encoding="utf-8") as f:
|
|
633
|
+
source_config = json.load(f)
|
|
634
|
+
|
|
635
|
+
source_display_name = source_config.get("display_name")
|
|
636
|
+
if not source_display_name:
|
|
637
|
+
source_display_name = _default_display_name(base_name)
|
|
638
|
+
|
|
639
|
+
clone_config = dict(source_config)
|
|
640
|
+
clone_config["name"] = clone_name
|
|
641
|
+
clone_config["display_name"] = _build_clone_display_name(
|
|
642
|
+
source_display_name, clone_index
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
tools = source_config.get("tools", [])
|
|
646
|
+
clone_config["tools"] = (
|
|
647
|
+
_filter_available_tools(tools) if isinstance(tools, list) else []
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
if not clone_config.get("model"):
|
|
651
|
+
clone_config.pop("model", None)
|
|
652
|
+
else:
|
|
653
|
+
agent_instance = agent_ref()
|
|
654
|
+
clone_config = {
|
|
655
|
+
"name": clone_name,
|
|
656
|
+
"display_name": _build_clone_display_name(
|
|
657
|
+
agent_instance.display_name, clone_index
|
|
658
|
+
),
|
|
659
|
+
"description": agent_instance.description,
|
|
660
|
+
"system_prompt": agent_instance.get_full_system_prompt(),
|
|
661
|
+
"tools": _filter_available_tools(agent_instance.get_available_tools()),
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
user_prompt = agent_instance.get_user_prompt()
|
|
665
|
+
if user_prompt is not None:
|
|
666
|
+
clone_config["user_prompt"] = user_prompt
|
|
667
|
+
|
|
668
|
+
tools_config = agent_instance.get_tools_config()
|
|
669
|
+
if tools_config is not None:
|
|
670
|
+
clone_config["tools_config"] = tools_config
|
|
671
|
+
|
|
672
|
+
pinned_model = get_agent_pinned_model(agent_instance.name)
|
|
673
|
+
if pinned_model:
|
|
674
|
+
clone_config["model"] = pinned_model
|
|
675
|
+
except Exception as exc:
|
|
676
|
+
emit_warning(f"Failed to build clone for '{agent_name}': {exc}")
|
|
677
|
+
return None
|
|
678
|
+
|
|
679
|
+
if clone_path.exists():
|
|
680
|
+
emit_warning(f"Clone target '{clone_name}' already exists.")
|
|
681
|
+
return None
|
|
682
|
+
|
|
683
|
+
try:
|
|
684
|
+
with open(clone_path, "w", encoding="utf-8") as f:
|
|
685
|
+
json.dump(clone_config, f, indent=2, ensure_ascii=False)
|
|
686
|
+
emit_success(f"Cloned '{agent_name}' to '{clone_name}'.")
|
|
687
|
+
return clone_name
|
|
688
|
+
except Exception as exc:
|
|
689
|
+
emit_warning(f"Failed to write clone file '{clone_path}': {exc}")
|
|
690
|
+
return None
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def delete_clone_agent(agent_name: str) -> bool:
|
|
694
|
+
"""Delete a cloned JSON agent definition.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
agent_name: Clone agent name to delete.
|
|
698
|
+
|
|
699
|
+
Returns:
|
|
700
|
+
True if the clone was deleted, False otherwise.
|
|
701
|
+
"""
|
|
702
|
+
message_group_id = str(uuid.uuid4())
|
|
703
|
+
_discover_agents(message_group_id=message_group_id)
|
|
704
|
+
|
|
705
|
+
if not is_clone_agent_name(agent_name):
|
|
706
|
+
emit_warning(f"Agent '{agent_name}' is not a clone.")
|
|
707
|
+
return False
|
|
708
|
+
|
|
709
|
+
if get_current_agent_name() == agent_name:
|
|
710
|
+
emit_warning("Cannot delete the active agent. Switch agents first.")
|
|
711
|
+
return False
|
|
712
|
+
|
|
713
|
+
agent_ref = _AGENT_REGISTRY.get(agent_name)
|
|
714
|
+
if agent_ref is None:
|
|
715
|
+
emit_warning(f"Clone '{agent_name}' not found.")
|
|
716
|
+
return False
|
|
717
|
+
|
|
718
|
+
if not isinstance(agent_ref, str):
|
|
719
|
+
emit_warning(f"Clone '{agent_name}' is not a JSON agent.")
|
|
720
|
+
return False
|
|
721
|
+
|
|
722
|
+
clone_path = Path(agent_ref)
|
|
723
|
+
if not clone_path.exists():
|
|
724
|
+
emit_warning(f"Clone file for '{agent_name}' does not exist.")
|
|
725
|
+
return False
|
|
726
|
+
|
|
727
|
+
from ..config import get_user_agents_directory
|
|
728
|
+
|
|
729
|
+
agents_dir = Path(get_user_agents_directory()).resolve()
|
|
730
|
+
if clone_path.resolve().parent != agents_dir:
|
|
731
|
+
emit_warning(f"Refusing to delete non-user clone '{agent_name}'.")
|
|
732
|
+
return False
|
|
733
|
+
|
|
734
|
+
try:
|
|
735
|
+
clone_path.unlink()
|
|
736
|
+
emit_success(f"Deleted clone '{agent_name}'.")
|
|
737
|
+
_AGENT_REGISTRY.pop(agent_name, None)
|
|
738
|
+
_AGENT_HISTORIES.pop(agent_name, None)
|
|
739
|
+
return True
|
|
740
|
+
except Exception as exc:
|
|
741
|
+
emit_warning(f"Failed to delete clone '{agent_name}': {exc}")
|
|
742
|
+
return False
|