code-puppy 0.0.134__tar.gz → 0.0.136__tar.gz
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-0.0.134 → code_puppy-0.0.136}/PKG-INFO +1 -1
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/agent.py +15 -17
- code_puppy-0.0.136/code_puppy/agents/agent_manager.py +522 -0
- code_puppy-0.0.136/code_puppy/agents/base_agent.py +116 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/agents/runtime_manager.py +68 -42
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/command_handler.py +82 -33
- code_puppy-0.0.136/code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/add_command.py +183 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/base.py +35 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/handler.py +133 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/help_command.py +146 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/install_command.py +176 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/logs_command.py +126 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/restart_command.py +92 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/search_command.py +117 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/start_all_command.py +126 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/start_command.py +98 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/status_command.py +185 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/stop_all_command.py +109 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/stop_command.py +79 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy-0.0.136/code_puppy/command_line/mcp/wizard_utils.py +259 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/model_picker_completion.py +21 -4
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/prompt_toolkit_completion.py +9 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/main.py +23 -17
- code_puppy-0.0.136/code_puppy/mcp/__init__.py +49 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/async_lifecycle.py +51 -49
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/blocking_startup.py +125 -113
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/captured_stdio_server.py +63 -70
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/circuit_breaker.py +63 -47
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/config_wizard.py +169 -136
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/dashboard.py +79 -71
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/error_isolation.py +147 -100
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/examples/retry_example.py +55 -42
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/health_monitor.py +152 -141
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/managed_server.py +100 -97
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/manager.py +168 -156
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/registry.py +148 -110
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/retry_manager.py +63 -61
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/server_registry_catalog.py +271 -225
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/status_tracker.py +80 -80
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/mcp/system_tools.py +47 -52
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/messaging/message_queue.py +20 -13
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/messaging/renderers.py +30 -15
- code_puppy-0.0.136/code_puppy/state_management.py +200 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/app.py +64 -7
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/chat_view.py +3 -3
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/human_input_modal.py +12 -8
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/screens/__init__.py +2 -2
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/screens/mcp_install_wizard.py +208 -179
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_agent_command.py +3 -3
- {code_puppy-0.0.134 → code_puppy-0.0.136}/pyproject.toml +1 -1
- code_puppy-0.0.134/code_puppy/agents/agent_manager.py +0 -211
- code_puppy-0.0.134/code_puppy/agents/base_agent.py +0 -60
- code_puppy-0.0.134/code_puppy/command_line/mcp_commands.py +0 -1789
- code_puppy-0.0.134/code_puppy/mcp/__init__.py +0 -23
- code_puppy-0.0.134/code_puppy/state_management.py +0 -97
- {code_puppy-0.0.134 → code_puppy-0.0.136}/.gitignore +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/LICENSE +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/README.md +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/meta_command_handler.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/motd.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/config.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/message_history_processor.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/messaging/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/messaging/spinner/console_spinner.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/messaging/spinner/spinner_base.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/models.json +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/plugins/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/summarization_agent.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/token_utils.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tools/command_runner.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tools/file_operations.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tools/token_check.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/command_history_modal.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/copy_button.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/custom_widgets.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/input_area.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/sidebar.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/components/status_bar.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/messages.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/models/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/models/chat_message.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/models/command_history.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/models/enums.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/screens/help.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/screens/settings.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/screens/tools.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/__init__.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_chat_message.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_chat_view.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_command_history.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_copy_button.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_custom_widgets.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_disclaimer.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_enums.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_file_browser.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_help.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_history_file_reader.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_input_area.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_settings.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_sidebar.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_sidebar_history.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_sidebar_history_navigation.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_status_bar.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_timestamped_history.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/tui/tests/test_tools.py +0 -0
- {code_puppy-0.0.134 → code_puppy-0.0.136}/code_puppy/version_checker.py +0 -0
|
@@ -3,15 +3,10 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Dict, Optional
|
|
4
4
|
|
|
5
5
|
from pydantic_ai import Agent
|
|
6
|
-
from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP
|
|
7
6
|
from pydantic_ai.settings import ModelSettings
|
|
8
7
|
from pydantic_ai.usage import UsageLimits
|
|
9
8
|
|
|
10
9
|
from code_puppy.agents import get_current_agent_config
|
|
11
|
-
from code_puppy.http_utils import (
|
|
12
|
-
create_reopenable_async_client,
|
|
13
|
-
resolve_env_var_in_header,
|
|
14
|
-
)
|
|
15
10
|
from code_puppy.message_history_processor import (
|
|
16
11
|
get_model_context_length,
|
|
17
12
|
message_history_accumulator,
|
|
@@ -45,7 +40,7 @@ _code_generation_agent = None
|
|
|
45
40
|
def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
|
|
46
41
|
"""Load MCP servers using the new manager while maintaining backward compatibility."""
|
|
47
42
|
from code_puppy.config import get_value, load_mcp_server_configs
|
|
48
|
-
from code_puppy.mcp import
|
|
43
|
+
from code_puppy.mcp import ServerConfig, get_mcp_manager
|
|
49
44
|
|
|
50
45
|
# Check if MCP servers are disabled
|
|
51
46
|
mcp_disabled = get_value("disable_mcp_servers")
|
|
@@ -55,7 +50,7 @@ def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
|
|
|
55
50
|
|
|
56
51
|
# Get the MCP manager singleton
|
|
57
52
|
manager = get_mcp_manager()
|
|
58
|
-
|
|
53
|
+
|
|
59
54
|
# Load configurations from legacy file for backward compatibility
|
|
60
55
|
configs = load_mcp_server_configs()
|
|
61
56
|
if not configs:
|
|
@@ -74,9 +69,9 @@ def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
|
|
|
74
69
|
name=name,
|
|
75
70
|
type=conf.get("type", "sse"),
|
|
76
71
|
enabled=conf.get("enabled", True),
|
|
77
|
-
config=conf
|
|
72
|
+
config=conf,
|
|
78
73
|
)
|
|
79
|
-
|
|
74
|
+
|
|
80
75
|
# Check if server already registered
|
|
81
76
|
existing = manager.get_server_by_name(name)
|
|
82
77
|
if not existing:
|
|
@@ -88,14 +83,14 @@ def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
|
|
|
88
83
|
if existing.config != server_config.config:
|
|
89
84
|
manager.update_server(existing.id, server_config)
|
|
90
85
|
emit_system_message(f"[dim]Updated MCP server: {name}[/dim]")
|
|
91
|
-
|
|
86
|
+
|
|
92
87
|
except Exception as e:
|
|
93
88
|
emit_error(f"Failed to register MCP server '{name}': {str(e)}")
|
|
94
89
|
continue
|
|
95
|
-
|
|
90
|
+
|
|
96
91
|
# Get pydantic-ai compatible servers from manager
|
|
97
92
|
servers = manager.get_servers_for_agent()
|
|
98
|
-
|
|
93
|
+
|
|
99
94
|
if servers:
|
|
100
95
|
emit_system_message(
|
|
101
96
|
f"[green]Successfully loaded {len(servers)} MCP server(s)[/green]"
|
|
@@ -104,14 +99,14 @@ def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
|
|
|
104
99
|
emit_system_message(
|
|
105
100
|
"[yellow]No MCP servers available (check if servers are enabled)[/yellow]"
|
|
106
101
|
)
|
|
107
|
-
|
|
102
|
+
|
|
108
103
|
return servers
|
|
109
104
|
|
|
110
105
|
|
|
111
106
|
def reload_mcp_servers():
|
|
112
107
|
"""Reload MCP servers without restarting the agent."""
|
|
113
108
|
from code_puppy.mcp import get_mcp_manager
|
|
114
|
-
|
|
109
|
+
|
|
115
110
|
manager = get_mcp_manager()
|
|
116
111
|
# Reload configurations
|
|
117
112
|
_load_mcp_servers()
|
|
@@ -124,15 +119,18 @@ def reload_code_generation_agent(message_group: str | None):
|
|
|
124
119
|
if message_group is None:
|
|
125
120
|
message_group = str(uuid.uuid4())
|
|
126
121
|
global _code_generation_agent, _LAST_MODEL_NAME
|
|
127
|
-
from code_puppy.config import clear_model_cache, get_model_name
|
|
128
122
|
from code_puppy.agents import clear_agent_cache
|
|
123
|
+
from code_puppy.config import clear_model_cache, get_model_name
|
|
129
124
|
|
|
130
125
|
# Clear both ModelFactory cache and config cache when force reloading
|
|
131
126
|
clear_model_cache()
|
|
132
127
|
clear_agent_cache()
|
|
133
128
|
|
|
134
129
|
model_name = get_model_name()
|
|
135
|
-
emit_info(
|
|
130
|
+
emit_info(
|
|
131
|
+
f"[bold cyan]Loading Model: {model_name}[/bold cyan]",
|
|
132
|
+
message_group=message_group,
|
|
133
|
+
)
|
|
136
134
|
models_config = ModelFactory.load_config()
|
|
137
135
|
model = ModelFactory.get_model(model_name, models_config)
|
|
138
136
|
|
|
@@ -140,7 +138,7 @@ def reload_code_generation_agent(message_group: str | None):
|
|
|
140
138
|
agent_config = get_current_agent_config()
|
|
141
139
|
emit_info(
|
|
142
140
|
f"[bold magenta]Loading Agent: {agent_config.display_name}[/bold magenta]",
|
|
143
|
-
message_group=message_group
|
|
141
|
+
message_group=message_group,
|
|
144
142
|
)
|
|
145
143
|
|
|
146
144
|
instructions = agent_config.get_system_prompt()
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""Agent manager for handling different agent configurations."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import pkgutil
|
|
7
|
+
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional, Type, Union
|
|
10
|
+
|
|
11
|
+
from ..callbacks import on_agent_reload
|
|
12
|
+
from ..messaging import emit_warning
|
|
13
|
+
from .base_agent import BaseAgent
|
|
14
|
+
from .json_agent import JSONAgent, discover_json_agents
|
|
15
|
+
|
|
16
|
+
# Registry of available agents (Python classes and JSON file paths)
|
|
17
|
+
_AGENT_REGISTRY: Dict[str, Union[Type[BaseAgent], str]] = {}
|
|
18
|
+
_CURRENT_AGENT_CONFIG: Optional[BaseAgent] = None
|
|
19
|
+
|
|
20
|
+
# Terminal session-based agent selection
|
|
21
|
+
_SESSION_AGENTS_CACHE: dict[str, str] = {}
|
|
22
|
+
_SESSION_FILE_LOADED: bool = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Session persistence file path
|
|
26
|
+
def _get_session_file_path() -> Path:
|
|
27
|
+
"""Get the path to the terminal sessions file."""
|
|
28
|
+
from ..config import CONFIG_DIR
|
|
29
|
+
|
|
30
|
+
return Path(CONFIG_DIR) / "terminal_sessions.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_terminal_session_id() -> str:
|
|
34
|
+
"""Get a unique identifier for the current terminal session.
|
|
35
|
+
|
|
36
|
+
Uses parent process ID (PPID) as the session identifier.
|
|
37
|
+
This works across all platforms and provides session isolation.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str: Unique session identifier (e.g., "session_12345")
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
ppid = os.getppid()
|
|
44
|
+
return f"session_{ppid}"
|
|
45
|
+
except (OSError, AttributeError):
|
|
46
|
+
# Fallback to current process ID if PPID unavailable
|
|
47
|
+
return f"fallback_{os.getpid()}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _is_process_alive(pid: int) -> bool:
|
|
51
|
+
"""Check if a process with the given PID is still alive.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
pid: Process ID to check
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
bool: True if process exists, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
# On Unix: os.kill(pid, 0) raises OSError if process doesn't exist
|
|
61
|
+
# On Windows: This also works with signal 0
|
|
62
|
+
os.kill(pid, 0)
|
|
63
|
+
return True
|
|
64
|
+
except (OSError, ProcessLookupError):
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _cleanup_dead_sessions(sessions: dict[str, str]) -> dict[str, str]:
|
|
69
|
+
"""Remove sessions for processes that no longer exist.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
sessions: Dictionary of session_id -> agent_name
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
dict: Cleaned sessions dictionary
|
|
76
|
+
"""
|
|
77
|
+
cleaned = {}
|
|
78
|
+
for session_id, agent_name in sessions.items():
|
|
79
|
+
if session_id.startswith("session_"):
|
|
80
|
+
try:
|
|
81
|
+
pid_str = session_id.replace("session_", "")
|
|
82
|
+
pid = int(pid_str)
|
|
83
|
+
if _is_process_alive(pid):
|
|
84
|
+
cleaned[session_id] = agent_name
|
|
85
|
+
# else: skip dead session
|
|
86
|
+
except (ValueError, TypeError):
|
|
87
|
+
# Invalid session ID format, keep it anyway
|
|
88
|
+
cleaned[session_id] = agent_name
|
|
89
|
+
else:
|
|
90
|
+
# Non-standard session ID (like "fallback_"), keep it
|
|
91
|
+
cleaned[session_id] = agent_name
|
|
92
|
+
return cleaned
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _load_session_data() -> dict[str, str]:
|
|
96
|
+
"""Load terminal session data from the JSON file.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
dict: Session ID to agent name mapping
|
|
100
|
+
"""
|
|
101
|
+
session_file = _get_session_file_path()
|
|
102
|
+
try:
|
|
103
|
+
if session_file.exists():
|
|
104
|
+
with open(session_file, "r", encoding="utf-8") as f:
|
|
105
|
+
data = json.load(f)
|
|
106
|
+
# Clean up dead sessions while loading
|
|
107
|
+
return _cleanup_dead_sessions(data)
|
|
108
|
+
return {}
|
|
109
|
+
except (json.JSONDecodeError, IOError, OSError):
|
|
110
|
+
# File corrupted or permission issues, start fresh
|
|
111
|
+
return {}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _save_session_data(sessions: dict[str, str]) -> None:
|
|
115
|
+
"""Save terminal session data to the JSON file.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
sessions: Session ID to agent name mapping
|
|
119
|
+
"""
|
|
120
|
+
session_file = _get_session_file_path()
|
|
121
|
+
try:
|
|
122
|
+
# Ensure the config directory exists
|
|
123
|
+
session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
# Clean up dead sessions before saving
|
|
126
|
+
cleaned_sessions = _cleanup_dead_sessions(sessions)
|
|
127
|
+
|
|
128
|
+
# Write to file atomically (write to temp file, then rename)
|
|
129
|
+
temp_file = session_file.with_suffix(".tmp")
|
|
130
|
+
with open(temp_file, "w", encoding="utf-8") as f:
|
|
131
|
+
json.dump(cleaned_sessions, f, indent=2)
|
|
132
|
+
|
|
133
|
+
# Atomic rename (works on all platforms)
|
|
134
|
+
temp_file.replace(session_file)
|
|
135
|
+
|
|
136
|
+
except (IOError, OSError):
|
|
137
|
+
# File permission issues, etc. - just continue without persistence
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _ensure_session_cache_loaded() -> None:
|
|
142
|
+
"""Ensure the session cache is loaded from disk."""
|
|
143
|
+
global _SESSION_AGENTS_CACHE, _SESSION_FILE_LOADED
|
|
144
|
+
if not _SESSION_FILE_LOADED:
|
|
145
|
+
_SESSION_AGENTS_CACHE.update(_load_session_data())
|
|
146
|
+
_SESSION_FILE_LOADED = True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Persistent storage for agent message histories
|
|
150
|
+
_AGENT_HISTORIES: Dict[str, Dict[str, any]] = {}
|
|
151
|
+
# Structure: {agent_name: {"message_history": [...], "compacted_hashes": set(...)}}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _save_agent_history(agent_name: str, agent: BaseAgent) -> None:
|
|
155
|
+
"""Save an agent's message history to persistent storage.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
agent_name: The name of the agent
|
|
159
|
+
agent: The agent instance to save history from
|
|
160
|
+
"""
|
|
161
|
+
global _AGENT_HISTORIES
|
|
162
|
+
_AGENT_HISTORIES[agent_name] = {
|
|
163
|
+
"message_history": agent.get_message_history().copy(),
|
|
164
|
+
"compacted_hashes": agent.get_compacted_message_hashes().copy(),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _restore_agent_history(agent_name: str, agent: BaseAgent) -> None:
|
|
169
|
+
"""Restore an agent's message history from persistent storage.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
agent_name: The name of the agent
|
|
173
|
+
agent: The agent instance to restore history to
|
|
174
|
+
"""
|
|
175
|
+
global _AGENT_HISTORIES
|
|
176
|
+
if agent_name in _AGENT_HISTORIES:
|
|
177
|
+
stored_data = _AGENT_HISTORIES[agent_name]
|
|
178
|
+
agent.set_message_history(stored_data["message_history"])
|
|
179
|
+
# Restore compacted hashes
|
|
180
|
+
for hash_val in stored_data["compacted_hashes"]:
|
|
181
|
+
agent.add_compacted_message_hash(hash_val)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _discover_agents(message_group_id: Optional[str] = None):
|
|
185
|
+
"""Dynamically discover all agent classes and JSON agents."""
|
|
186
|
+
# Always clear the registry to force refresh
|
|
187
|
+
_AGENT_REGISTRY.clear()
|
|
188
|
+
|
|
189
|
+
# 1. Discover Python agent classes in the agents package
|
|
190
|
+
import code_puppy.agents as agents_package
|
|
191
|
+
|
|
192
|
+
# Iterate through all modules in the agents package
|
|
193
|
+
for _, modname, _ in pkgutil.iter_modules(agents_package.__path__):
|
|
194
|
+
if modname.startswith("_") or modname in [
|
|
195
|
+
"base_agent",
|
|
196
|
+
"json_agent",
|
|
197
|
+
"agent_manager",
|
|
198
|
+
]:
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
# Import the module
|
|
203
|
+
module = importlib.import_module(f"code_puppy.agents.{modname}")
|
|
204
|
+
|
|
205
|
+
# Look for BaseAgent subclasses
|
|
206
|
+
for attr_name in dir(module):
|
|
207
|
+
attr = getattr(module, attr_name)
|
|
208
|
+
if (
|
|
209
|
+
isinstance(attr, type)
|
|
210
|
+
and issubclass(attr, BaseAgent)
|
|
211
|
+
and attr not in [BaseAgent, JSONAgent]
|
|
212
|
+
):
|
|
213
|
+
# Create an instance to get the name
|
|
214
|
+
agent_instance = attr()
|
|
215
|
+
_AGENT_REGISTRY[agent_instance.name] = attr
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
# Skip problematic modules
|
|
219
|
+
emit_warning(
|
|
220
|
+
f"Warning: Could not load agent module {modname}: {e}",
|
|
221
|
+
message_group=message_group_id,
|
|
222
|
+
)
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
# 2. Discover JSON agents in user directory
|
|
226
|
+
try:
|
|
227
|
+
json_agents = discover_json_agents()
|
|
228
|
+
|
|
229
|
+
# Add JSON agents to registry (store file path instead of class)
|
|
230
|
+
for agent_name, json_path in json_agents.items():
|
|
231
|
+
_AGENT_REGISTRY[agent_name] = json_path
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
emit_warning(
|
|
235
|
+
f"Warning: Could not discover JSON agents: {e}",
|
|
236
|
+
message_group=message_group_id,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_available_agents() -> Dict[str, str]:
|
|
241
|
+
"""Get a dictionary of available agents with their display names.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Dict mapping agent names to display names.
|
|
245
|
+
"""
|
|
246
|
+
# Generate a message group ID for this operation
|
|
247
|
+
message_group_id = str(uuid.uuid4())
|
|
248
|
+
_discover_agents(message_group_id=message_group_id)
|
|
249
|
+
|
|
250
|
+
agents = {}
|
|
251
|
+
for name, agent_ref in _AGENT_REGISTRY.items():
|
|
252
|
+
try:
|
|
253
|
+
if isinstance(agent_ref, str): # JSON agent (file path)
|
|
254
|
+
agent_instance = JSONAgent(agent_ref)
|
|
255
|
+
else: # Python agent (class)
|
|
256
|
+
agent_instance = agent_ref()
|
|
257
|
+
agents[name] = agent_instance.display_name
|
|
258
|
+
except Exception:
|
|
259
|
+
agents[name] = name.title() # Fallback
|
|
260
|
+
|
|
261
|
+
return agents
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_current_agent_name() -> str:
|
|
265
|
+
"""Get the name of the currently active agent for this terminal session.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
The name of the current agent for this session, defaults to 'code-puppy'.
|
|
269
|
+
"""
|
|
270
|
+
_ensure_session_cache_loaded()
|
|
271
|
+
session_id = get_terminal_session_id()
|
|
272
|
+
return _SESSION_AGENTS_CACHE.get(session_id, "code-puppy")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def set_current_agent(agent_name: str) -> bool:
|
|
276
|
+
"""Set the current agent by name.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
agent_name: The name of the agent to set as current.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if the agent was set successfully, False if agent not found.
|
|
283
|
+
"""
|
|
284
|
+
# Generate a message group ID for agent switching
|
|
285
|
+
message_group_id = str(uuid.uuid4())
|
|
286
|
+
_discover_agents(message_group_id=message_group_id)
|
|
287
|
+
|
|
288
|
+
# Save current agent's history before switching
|
|
289
|
+
global _CURRENT_AGENT_CONFIG, _CURRENT_AGENT_NAME
|
|
290
|
+
if _CURRENT_AGENT_CONFIG is not None:
|
|
291
|
+
_save_agent_history(_CURRENT_AGENT_CONFIG.name, _CURRENT_AGENT_CONFIG)
|
|
292
|
+
|
|
293
|
+
# Clear the cached config when switching agents
|
|
294
|
+
_CURRENT_AGENT_CONFIG = None
|
|
295
|
+
agent_obj = load_agent_config(agent_name)
|
|
296
|
+
|
|
297
|
+
# Restore the agent's history if it exists
|
|
298
|
+
_restore_agent_history(agent_name, agent_obj)
|
|
299
|
+
|
|
300
|
+
# Update session-based agent selection and persist to disk
|
|
301
|
+
_ensure_session_cache_loaded()
|
|
302
|
+
session_id = get_terminal_session_id()
|
|
303
|
+
_SESSION_AGENTS_CACHE[session_id] = agent_name
|
|
304
|
+
_save_session_data(_SESSION_AGENTS_CACHE)
|
|
305
|
+
|
|
306
|
+
on_agent_reload(agent_obj.id, agent_name)
|
|
307
|
+
return True
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def get_current_agent_config() -> BaseAgent:
|
|
311
|
+
"""Get the current agent configuration.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
The current agent configuration instance.
|
|
315
|
+
"""
|
|
316
|
+
global _CURRENT_AGENT_CONFIG
|
|
317
|
+
|
|
318
|
+
if _CURRENT_AGENT_CONFIG is None:
|
|
319
|
+
agent_name = get_current_agent_name()
|
|
320
|
+
_CURRENT_AGENT_CONFIG = load_agent_config(agent_name)
|
|
321
|
+
# Restore the agent's history if it exists
|
|
322
|
+
_restore_agent_history(agent_name, _CURRENT_AGENT_CONFIG)
|
|
323
|
+
|
|
324
|
+
return _CURRENT_AGENT_CONFIG
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def load_agent_config(agent_name: str) -> BaseAgent:
|
|
328
|
+
"""Load an agent configuration by name.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
agent_name: The name of the agent to load.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
The agent configuration instance.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
ValueError: If the agent is not found.
|
|
338
|
+
"""
|
|
339
|
+
# Generate a message group ID for agent loading
|
|
340
|
+
message_group_id = str(uuid.uuid4())
|
|
341
|
+
_discover_agents(message_group_id=message_group_id)
|
|
342
|
+
|
|
343
|
+
if agent_name not in _AGENT_REGISTRY:
|
|
344
|
+
# Fallback to code-puppy if agent not found
|
|
345
|
+
if "code-puppy" in _AGENT_REGISTRY:
|
|
346
|
+
agent_name = "code-puppy"
|
|
347
|
+
else:
|
|
348
|
+
raise ValueError(
|
|
349
|
+
f"Agent '{agent_name}' not found and no fallback available"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
agent_ref = _AGENT_REGISTRY[agent_name]
|
|
353
|
+
if isinstance(agent_ref, str): # JSON agent (file path)
|
|
354
|
+
return JSONAgent(agent_ref)
|
|
355
|
+
else: # Python agent (class)
|
|
356
|
+
return agent_ref()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_agent_descriptions() -> Dict[str, str]:
|
|
360
|
+
"""Get descriptions for all available agents.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Dict mapping agent names to their descriptions.
|
|
364
|
+
"""
|
|
365
|
+
# Generate a message group ID for this operation
|
|
366
|
+
message_group_id = str(uuid.uuid4())
|
|
367
|
+
_discover_agents(message_group_id=message_group_id)
|
|
368
|
+
|
|
369
|
+
descriptions = {}
|
|
370
|
+
for name, agent_ref in _AGENT_REGISTRY.items():
|
|
371
|
+
try:
|
|
372
|
+
if isinstance(agent_ref, str): # JSON agent (file path)
|
|
373
|
+
agent_instance = JSONAgent(agent_ref)
|
|
374
|
+
else: # Python agent (class)
|
|
375
|
+
agent_instance = agent_ref()
|
|
376
|
+
descriptions[name] = agent_instance.description
|
|
377
|
+
except Exception:
|
|
378
|
+
descriptions[name] = "No description available"
|
|
379
|
+
|
|
380
|
+
return descriptions
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def clear_agent_cache():
|
|
384
|
+
"""Clear the cached agent configuration to force reload."""
|
|
385
|
+
global _CURRENT_AGENT_CONFIG
|
|
386
|
+
_CURRENT_AGENT_CONFIG = None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def reset_to_default_agent():
|
|
390
|
+
"""Reset the current agent to the default (code-puppy) for this terminal session.
|
|
391
|
+
|
|
392
|
+
This is useful for testing or when you want to start fresh.
|
|
393
|
+
"""
|
|
394
|
+
global _CURRENT_AGENT_CONFIG
|
|
395
|
+
_ensure_session_cache_loaded()
|
|
396
|
+
session_id = get_terminal_session_id()
|
|
397
|
+
if session_id in _SESSION_AGENTS_CACHE:
|
|
398
|
+
del _SESSION_AGENTS_CACHE[session_id]
|
|
399
|
+
_save_session_data(_SESSION_AGENTS_CACHE)
|
|
400
|
+
_CURRENT_AGENT_CONFIG = None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def refresh_agents():
|
|
404
|
+
"""Refresh the agent discovery to pick up newly created agents.
|
|
405
|
+
|
|
406
|
+
This clears the agent registry cache and forces a rediscovery of all agents.
|
|
407
|
+
"""
|
|
408
|
+
# Generate a message group ID for agent refreshing
|
|
409
|
+
message_group_id = str(uuid.uuid4())
|
|
410
|
+
_discover_agents(message_group_id=message_group_id)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def clear_all_agent_histories():
|
|
414
|
+
"""Clear all agent message histories from persistent storage.
|
|
415
|
+
|
|
416
|
+
This is useful for debugging or when you want a fresh start.
|
|
417
|
+
"""
|
|
418
|
+
global _AGENT_HISTORIES
|
|
419
|
+
_AGENT_HISTORIES.clear()
|
|
420
|
+
# Also clear the current agent's history
|
|
421
|
+
if _CURRENT_AGENT_CONFIG is not None:
|
|
422
|
+
_CURRENT_AGENT_CONFIG.messages = []
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def cleanup_dead_terminal_sessions() -> int:
|
|
426
|
+
"""Clean up terminal sessions for processes that no longer exist.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
int: Number of dead sessions removed
|
|
430
|
+
"""
|
|
431
|
+
_ensure_session_cache_loaded()
|
|
432
|
+
original_count = len(_SESSION_AGENTS_CACHE)
|
|
433
|
+
cleaned_cache = _cleanup_dead_sessions(_SESSION_AGENTS_CACHE)
|
|
434
|
+
|
|
435
|
+
if len(cleaned_cache) != original_count:
|
|
436
|
+
_SESSION_AGENTS_CACHE.clear()
|
|
437
|
+
_SESSION_AGENTS_CACHE.update(cleaned_cache)
|
|
438
|
+
_save_session_data(_SESSION_AGENTS_CACHE)
|
|
439
|
+
|
|
440
|
+
return original_count - len(cleaned_cache)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# Agent-aware message history functions
|
|
444
|
+
def get_current_agent_message_history():
|
|
445
|
+
"""Get the message history for the currently active agent.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
List of messages from the current agent's conversation history.
|
|
449
|
+
"""
|
|
450
|
+
current_agent = get_current_agent_config()
|
|
451
|
+
return current_agent.get_message_history()
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def set_current_agent_message_history(history):
|
|
455
|
+
"""Set the message history for the currently active agent.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
history: List of messages to set as the current agent's conversation history.
|
|
459
|
+
"""
|
|
460
|
+
current_agent = get_current_agent_config()
|
|
461
|
+
current_agent.set_message_history(history)
|
|
462
|
+
# Also update persistent storage
|
|
463
|
+
_save_agent_history(current_agent.name, current_agent)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def clear_current_agent_message_history():
|
|
467
|
+
"""Clear the message history for the currently active agent."""
|
|
468
|
+
current_agent = get_current_agent_config()
|
|
469
|
+
current_agent.clear_message_history()
|
|
470
|
+
# Also clear from persistent storage
|
|
471
|
+
global _AGENT_HISTORIES
|
|
472
|
+
if current_agent.name in _AGENT_HISTORIES:
|
|
473
|
+
_AGENT_HISTORIES[current_agent.name] = {
|
|
474
|
+
"message_history": [],
|
|
475
|
+
"compacted_hashes": set(),
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def append_to_current_agent_message_history(message):
|
|
480
|
+
"""Append a message to the currently active agent's history.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
message: Message to append to the current agent's conversation history.
|
|
484
|
+
"""
|
|
485
|
+
current_agent = get_current_agent_config()
|
|
486
|
+
current_agent.append_to_message_history(message)
|
|
487
|
+
# Also update persistent storage
|
|
488
|
+
_save_agent_history(current_agent.name, current_agent)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def extend_current_agent_message_history(history):
|
|
492
|
+
"""Extend the currently active agent's message history with multiple messages.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
history: List of messages to append to the current agent's conversation history.
|
|
496
|
+
"""
|
|
497
|
+
current_agent = get_current_agent_config()
|
|
498
|
+
current_agent.extend_message_history(history)
|
|
499
|
+
# Also update persistent storage
|
|
500
|
+
_save_agent_history(current_agent.name, current_agent)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def get_current_agent_compacted_message_hashes():
|
|
504
|
+
"""Get the set of compacted message hashes for the currently active agent.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Set of hashes for messages that have been compacted/summarized.
|
|
508
|
+
"""
|
|
509
|
+
current_agent = get_current_agent_config()
|
|
510
|
+
return current_agent.get_compacted_message_hashes()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def add_current_agent_compacted_message_hash(message_hash: str):
|
|
514
|
+
"""Add a message hash to the current agent's set of compacted message hashes.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
message_hash: Hash of a message that has been compacted/summarized.
|
|
518
|
+
"""
|
|
519
|
+
current_agent = get_current_agent_config()
|
|
520
|
+
current_agent.add_compacted_message_hash(message_hash)
|
|
521
|
+
# Also update persistent storage
|
|
522
|
+
_save_agent_history(current_agent.name, current_agent)
|