code-puppy 0.0.127__py3-none-any.whl → 0.0.129__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 +1 -0
- code_puppy/agent.py +65 -69
- code_puppy/agents/agent_code_puppy.py +0 -3
- code_puppy/agents/runtime_manager.py +231 -0
- code_puppy/command_line/command_handler.py +56 -25
- code_puppy/command_line/mcp_commands.py +1298 -0
- code_puppy/command_line/meta_command_handler.py +3 -2
- code_puppy/command_line/model_picker_completion.py +21 -8
- code_puppy/http_utils.py +1 -1
- code_puppy/main.py +99 -158
- code_puppy/mcp/__init__.py +23 -0
- code_puppy/mcp/async_lifecycle.py +237 -0
- code_puppy/mcp/circuit_breaker.py +218 -0
- code_puppy/mcp/config_wizard.py +437 -0
- code_puppy/mcp/dashboard.py +291 -0
- code_puppy/mcp/error_isolation.py +360 -0
- code_puppy/mcp/examples/retry_example.py +208 -0
- code_puppy/mcp/health_monitor.py +549 -0
- code_puppy/mcp/managed_server.py +346 -0
- code_puppy/mcp/manager.py +701 -0
- code_puppy/mcp/registry.py +412 -0
- code_puppy/mcp/retry_manager.py +321 -0
- code_puppy/mcp/server_registry_catalog.py +751 -0
- code_puppy/mcp/status_tracker.py +355 -0
- code_puppy/messaging/spinner/textual_spinner.py +6 -2
- code_puppy/model_factory.py +19 -4
- code_puppy/models.json +8 -6
- code_puppy/tui/app.py +19 -27
- code_puppy/tui/tests/test_agent_command.py +22 -15
- {code_puppy-0.0.127.data → code_puppy-0.0.129.data}/data/code_puppy/models.json +8 -6
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/METADATA +4 -3
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/RECORD +35 -19
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/licenses/LICENSE +0 -0
code_puppy/__init__.py
CHANGED
code_puppy/agent.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import uuid
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import Dict, Optional
|
|
3
4
|
|
|
@@ -42,7 +43,9 @@ _code_generation_agent = None
|
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
|
|
46
|
+
"""Load MCP servers using the new manager while maintaining backward compatibility."""
|
|
45
47
|
from code_puppy.config import get_value, load_mcp_server_configs
|
|
48
|
+
from code_puppy.mcp import get_mcp_manager, ServerConfig
|
|
46
49
|
|
|
47
50
|
# Check if MCP servers are disabled
|
|
48
51
|
mcp_disabled = get_value("disable_mcp_servers")
|
|
@@ -50,86 +53,76 @@ def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
|
|
|
50
53
|
emit_system_message("[dim]MCP servers disabled via config[/dim]")
|
|
51
54
|
return []
|
|
52
55
|
|
|
56
|
+
# Get the MCP manager singleton
|
|
57
|
+
manager = get_mcp_manager()
|
|
58
|
+
|
|
59
|
+
# Load configurations from legacy file for backward compatibility
|
|
53
60
|
configs = load_mcp_server_configs()
|
|
54
61
|
if not configs:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if extra_headers:
|
|
64
|
-
server_headers.update(extra_headers)
|
|
65
|
-
user_headers = conf.get("headers") or {}
|
|
66
|
-
if isinstance(user_headers, dict) and user_headers:
|
|
62
|
+
# Check if manager already has servers (could be from new system)
|
|
63
|
+
existing_servers = manager.list_servers()
|
|
64
|
+
if not existing_servers:
|
|
65
|
+
emit_system_message("[dim]No MCP servers configured[/dim]")
|
|
66
|
+
return []
|
|
67
|
+
else:
|
|
68
|
+
# Register servers from legacy config with manager
|
|
69
|
+
for name, conf in configs.items():
|
|
67
70
|
try:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if server_type == "http" and url:
|
|
76
|
-
emit_system_message(
|
|
77
|
-
f"Registering MCP Server (HTTP) - {url} (timeout: {timeout}s, headers: {bool(server_headers)})"
|
|
78
|
-
)
|
|
79
|
-
http_client = create_reopenable_async_client(
|
|
80
|
-
timeout=timeout, headers=server_headers or None, verify=False
|
|
71
|
+
# Convert legacy format to new ServerConfig
|
|
72
|
+
server_config = ServerConfig(
|
|
73
|
+
id=conf.get("id", f"{name}_{hash(name)}"),
|
|
74
|
+
name=name,
|
|
75
|
+
type=conf.get("type", "sse"),
|
|
76
|
+
enabled=conf.get("enabled", True),
|
|
77
|
+
config=conf
|
|
81
78
|
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
args = conf.get("args", [])
|
|
90
|
-
timeout = conf.get(
|
|
91
|
-
"timeout", 30
|
|
92
|
-
) # Default 30 seconds for stdio servers (npm downloads can be slow)
|
|
93
|
-
if command:
|
|
94
|
-
emit_system_message(
|
|
95
|
-
f"Registering MCP Server (Stdio) - {command} {args} (timeout: {timeout}s)"
|
|
96
|
-
)
|
|
97
|
-
servers.append(MCPServerStdio(command, args=args, timeout=timeout))
|
|
79
|
+
|
|
80
|
+
# Check if server already registered
|
|
81
|
+
existing = manager.get_server_by_name(name)
|
|
82
|
+
if not existing:
|
|
83
|
+
# Register new server
|
|
84
|
+
manager.register_server(server_config)
|
|
85
|
+
emit_system_message(f"[dim]Registered MCP server: {name}[/dim]")
|
|
98
86
|
else:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
f"Invalid type '{server_type}' or missing URL for MCP server '{name}'"
|
|
112
|
-
)
|
|
113
|
-
except Exception as e:
|
|
114
|
-
emit_error(f"Failed to register MCP server '{name}': {str(e)}")
|
|
115
|
-
emit_info(f"Skipping server '{name}' and continuing with other servers...")
|
|
116
|
-
# Continue with other servers instead of crashing
|
|
117
|
-
continue
|
|
118
|
-
|
|
87
|
+
# Update existing server config if needed
|
|
88
|
+
if existing.config != server_config.config:
|
|
89
|
+
manager.update_server(existing.id, server_config)
|
|
90
|
+
emit_system_message(f"[dim]Updated MCP server: {name}[/dim]")
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
emit_error(f"Failed to register MCP server '{name}': {str(e)}")
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Get pydantic-ai compatible servers from manager
|
|
97
|
+
servers = manager.get_servers_for_agent()
|
|
98
|
+
|
|
119
99
|
if servers:
|
|
120
100
|
emit_system_message(
|
|
121
|
-
f"[green]Successfully
|
|
101
|
+
f"[green]Successfully loaded {len(servers)} MCP server(s)[/green]"
|
|
122
102
|
)
|
|
123
103
|
else:
|
|
124
104
|
emit_system_message(
|
|
125
|
-
"[yellow]No MCP servers
|
|
105
|
+
"[yellow]No MCP servers available (check if servers are enabled)[/yellow]"
|
|
126
106
|
)
|
|
127
|
-
|
|
107
|
+
|
|
128
108
|
return servers
|
|
129
109
|
|
|
130
110
|
|
|
131
|
-
def
|
|
111
|
+
def reload_mcp_servers():
|
|
112
|
+
"""Reload MCP servers without restarting the agent."""
|
|
113
|
+
from code_puppy.mcp import get_mcp_manager
|
|
114
|
+
|
|
115
|
+
manager = get_mcp_manager()
|
|
116
|
+
# Reload configurations
|
|
117
|
+
_load_mcp_servers()
|
|
118
|
+
# Return updated servers
|
|
119
|
+
return manager.get_servers_for_agent()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def reload_code_generation_agent(message_group: str | None):
|
|
132
123
|
"""Force-reload the agent, usually after a model change."""
|
|
124
|
+
if message_group is None:
|
|
125
|
+
message_group = str(uuid.uuid4())
|
|
133
126
|
global _code_generation_agent, _LAST_MODEL_NAME
|
|
134
127
|
from code_puppy.config import clear_model_cache, get_model_name
|
|
135
128
|
from code_puppy.agents import clear_agent_cache
|
|
@@ -139,14 +132,15 @@ def reload_code_generation_agent():
|
|
|
139
132
|
clear_agent_cache()
|
|
140
133
|
|
|
141
134
|
model_name = get_model_name()
|
|
142
|
-
emit_info(f"[bold cyan]Loading Model: {model_name}[/bold cyan]")
|
|
135
|
+
emit_info(f"[bold cyan]Loading Model: {model_name}[/bold cyan]", message_group=message_group)
|
|
143
136
|
models_config = ModelFactory.load_config()
|
|
144
137
|
model = ModelFactory.get_model(model_name, models_config)
|
|
145
138
|
|
|
146
139
|
# Get agent-specific system prompt
|
|
147
140
|
agent_config = get_current_agent_config()
|
|
148
141
|
emit_info(
|
|
149
|
-
f"[bold magenta]Loading Agent: {agent_config.display_name}[/bold magenta]"
|
|
142
|
+
f"[bold magenta]Loading Agent: {agent_config.display_name}[/bold magenta]",
|
|
143
|
+
message_group=message_group
|
|
150
144
|
)
|
|
151
145
|
|
|
152
146
|
instructions = agent_config.get_system_prompt()
|
|
@@ -183,17 +177,19 @@ def reload_code_generation_agent():
|
|
|
183
177
|
return _code_generation_agent
|
|
184
178
|
|
|
185
179
|
|
|
186
|
-
def get_code_generation_agent(force_reload=False):
|
|
180
|
+
def get_code_generation_agent(force_reload=False, message_group: str | None = None):
|
|
187
181
|
"""
|
|
188
182
|
Retrieve the agent with the currently configured model.
|
|
189
183
|
Forces a reload if the model has changed, or if force_reload is passed.
|
|
190
184
|
"""
|
|
191
185
|
global _code_generation_agent, _LAST_MODEL_NAME
|
|
186
|
+
if message_group is None:
|
|
187
|
+
message_group = str(uuid.uuid4())
|
|
192
188
|
from code_puppy.config import get_model_name
|
|
193
189
|
|
|
194
190
|
model_name = get_model_name()
|
|
195
191
|
if _code_generation_agent is None or _LAST_MODEL_NAME != model_name or force_reload:
|
|
196
|
-
return reload_code_generation_agent()
|
|
192
|
+
return reload_code_generation_agent(message_group)
|
|
197
193
|
return _code_generation_agent
|
|
198
194
|
|
|
199
195
|
|
|
@@ -140,9 +140,6 @@ Important rules:
|
|
|
140
140
|
Your solutions should be production-ready, maintainable, and follow best practices for the chosen language.
|
|
141
141
|
|
|
142
142
|
Return your final response as a string output
|
|
143
|
-
Walmart specific rules:
|
|
144
|
-
- You are operating inside Walmart Global Tech! Yay!
|
|
145
|
-
- Always use uv when working with python, and always use --index-url https://pypi.ci.artifacts.walmart.com/artifactory/api/pypi/external-pypi/simple
|
|
146
143
|
"""
|
|
147
144
|
|
|
148
145
|
prompt_additions = callbacks.on_load_prompt()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime agent manager that ensures proper agent instance updates.
|
|
3
|
+
|
|
4
|
+
This module provides a wrapper around the agent singleton that ensures
|
|
5
|
+
all references to the agent are properly updated when it's reloaded.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import signal
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Optional, Any
|
|
12
|
+
|
|
13
|
+
import mcp
|
|
14
|
+
from pydantic_ai import Agent
|
|
15
|
+
from pydantic_ai.usage import UsageLimits
|
|
16
|
+
|
|
17
|
+
from code_puppy.messaging.message_queue import emit_info, emit_warning
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RuntimeAgentManager:
|
|
21
|
+
"""
|
|
22
|
+
Manages the runtime agent instance and ensures proper updates.
|
|
23
|
+
|
|
24
|
+
This class acts as a proxy that always returns the current agent instance,
|
|
25
|
+
ensuring that when the agent is reloaded, all code using this manager
|
|
26
|
+
automatically gets the updated instance.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initialize the runtime agent manager."""
|
|
31
|
+
self._agent: Optional[Agent] = None
|
|
32
|
+
self._last_model_name: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
def get_agent(self, force_reload: bool = False, message_group: str = "") -> Agent:
|
|
35
|
+
"""
|
|
36
|
+
Get the current agent instance.
|
|
37
|
+
|
|
38
|
+
This method always returns the most recent agent instance,
|
|
39
|
+
automatically handling reloads when the model changes.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
force_reload: If True, force a reload of the agent
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The current agent instance
|
|
46
|
+
"""
|
|
47
|
+
from code_puppy.agent import get_code_generation_agent
|
|
48
|
+
|
|
49
|
+
# Always get the current singleton - this ensures we have the latest
|
|
50
|
+
current_agent = get_code_generation_agent(force_reload=force_reload, message_group=message_group)
|
|
51
|
+
self._agent = current_agent
|
|
52
|
+
|
|
53
|
+
return self._agent
|
|
54
|
+
|
|
55
|
+
def reload_agent(self) -> Agent:
|
|
56
|
+
"""
|
|
57
|
+
Force reload the agent.
|
|
58
|
+
|
|
59
|
+
This is typically called after MCP servers are started/stopped.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The newly loaded agent instance
|
|
63
|
+
"""
|
|
64
|
+
message_group = uuid.uuid4()
|
|
65
|
+
emit_info("[bold cyan]Reloading agent with updated configuration...[/bold cyan]", message_group=message_group)
|
|
66
|
+
return self.get_agent(force_reload=True, message_group=message_group)
|
|
67
|
+
|
|
68
|
+
async def run_with_mcp(self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs) -> Any:
|
|
69
|
+
"""
|
|
70
|
+
Run the agent with MCP servers and full cancellation support.
|
|
71
|
+
|
|
72
|
+
This method ensures we're always using the current agent instance
|
|
73
|
+
and handles Ctrl+C interruption properly by creating a cancellable task.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
prompt: The user prompt to process
|
|
77
|
+
usage_limits: Optional usage limits for the agent
|
|
78
|
+
**kwargs: Additional arguments to pass to agent.run (e.g., message_history)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The agent's response
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
asyncio.CancelledError: When execution is cancelled by user
|
|
85
|
+
"""
|
|
86
|
+
agent = self.get_agent()
|
|
87
|
+
group_id = str(uuid.uuid4())
|
|
88
|
+
# Function to run agent with MCP
|
|
89
|
+
async def run_agent_task():
|
|
90
|
+
try:
|
|
91
|
+
async with agent:
|
|
92
|
+
return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
|
|
93
|
+
except* mcp.shared.exceptions.McpError as mcp_error:
|
|
94
|
+
emit_warning(f"MCP server error: {str(mcp_error)}", group_id=group_id)
|
|
95
|
+
emit_warning(f"{str(mcp_error)}", group_id=group_id)
|
|
96
|
+
emit_warning(f"Try disabling any malfunctioning MCP servers", group_id=group_id)
|
|
97
|
+
except* InterruptedError as ie:
|
|
98
|
+
emit_warning(f"Interrupted: {str(ie)}")
|
|
99
|
+
except* Exception as other_error:
|
|
100
|
+
# Filter out CancelledError from the exception group - let it propagate
|
|
101
|
+
remaining_exceptions = []
|
|
102
|
+
def collect_non_cancelled_exceptions(exc):
|
|
103
|
+
if isinstance(exc, ExceptionGroup):
|
|
104
|
+
for sub_exc in exc.exceptions:
|
|
105
|
+
collect_non_cancelled_exceptions(sub_exc)
|
|
106
|
+
elif not isinstance(exc, asyncio.CancelledError):
|
|
107
|
+
remaining_exceptions.append(exc)
|
|
108
|
+
emit_warning(f"Unexpected error: {str(exc)}", group_id=group_id)
|
|
109
|
+
emit_warning(f"{str(exc.args)}", group_id=group_id)
|
|
110
|
+
|
|
111
|
+
collect_non_cancelled_exceptions(other_error)
|
|
112
|
+
|
|
113
|
+
# If there are CancelledError exceptions in the group, re-raise them
|
|
114
|
+
cancelled_exceptions = []
|
|
115
|
+
def collect_cancelled_exceptions(exc):
|
|
116
|
+
if isinstance(exc, ExceptionGroup):
|
|
117
|
+
for sub_exc in exc.exceptions:
|
|
118
|
+
collect_cancelled_exceptions(sub_exc)
|
|
119
|
+
elif isinstance(exc, asyncio.CancelledError):
|
|
120
|
+
cancelled_exceptions.append(exc)
|
|
121
|
+
|
|
122
|
+
collect_cancelled_exceptions(other_error)
|
|
123
|
+
|
|
124
|
+
if cancelled_exceptions:
|
|
125
|
+
# Re-raise the first CancelledError to propagate cancellation
|
|
126
|
+
raise cancelled_exceptions[0]
|
|
127
|
+
|
|
128
|
+
# Create the task FIRST
|
|
129
|
+
agent_task = asyncio.create_task(run_agent_task())
|
|
130
|
+
|
|
131
|
+
# Import shell process killer
|
|
132
|
+
from code_puppy.tools.command_runner import kill_all_running_shell_processes
|
|
133
|
+
|
|
134
|
+
# Ensure the interrupt handler only acts once per task
|
|
135
|
+
handled = False
|
|
136
|
+
|
|
137
|
+
def keyboard_interrupt_handler(sig, frame):
|
|
138
|
+
"""Signal handler for Ctrl+C - replicating exact original logic"""
|
|
139
|
+
nonlocal handled
|
|
140
|
+
if handled:
|
|
141
|
+
return
|
|
142
|
+
handled = True
|
|
143
|
+
|
|
144
|
+
# First, nuke any running shell processes triggered by tools
|
|
145
|
+
try:
|
|
146
|
+
killed = kill_all_running_shell_processes()
|
|
147
|
+
if killed:
|
|
148
|
+
emit_warning(f"Cancelled {killed} running shell process(es).")
|
|
149
|
+
else:
|
|
150
|
+
# Only cancel the agent task if no shell processes were killed
|
|
151
|
+
if not agent_task.done():
|
|
152
|
+
agent_task.cancel()
|
|
153
|
+
except Exception as e:
|
|
154
|
+
emit_warning(f"Shell kill error: {e}")
|
|
155
|
+
# If shell kill failed, still try to cancel the agent task
|
|
156
|
+
if not agent_task.done():
|
|
157
|
+
agent_task.cancel()
|
|
158
|
+
# Don't call the original handler
|
|
159
|
+
# This prevents the application from exiting
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Save original handler and set our custom one AFTER task is created
|
|
163
|
+
original_handler = signal.signal(signal.SIGINT, keyboard_interrupt_handler)
|
|
164
|
+
|
|
165
|
+
# Wait for the task to complete or be cancelled
|
|
166
|
+
result = await agent_task
|
|
167
|
+
return result
|
|
168
|
+
except asyncio.CancelledError:
|
|
169
|
+
# Task was cancelled by our handler
|
|
170
|
+
raise
|
|
171
|
+
except KeyboardInterrupt:
|
|
172
|
+
# Handle direct keyboard interrupt during await
|
|
173
|
+
if not agent_task.done():
|
|
174
|
+
agent_task.cancel()
|
|
175
|
+
try:
|
|
176
|
+
await agent_task
|
|
177
|
+
except asyncio.CancelledError:
|
|
178
|
+
pass
|
|
179
|
+
raise asyncio.CancelledError()
|
|
180
|
+
finally:
|
|
181
|
+
# Restore original signal handler
|
|
182
|
+
if original_handler:
|
|
183
|
+
signal.signal(signal.SIGINT, original_handler)
|
|
184
|
+
|
|
185
|
+
async def run(self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs) -> Any:
|
|
186
|
+
"""
|
|
187
|
+
Run the agent without explicitly managing MCP servers.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
prompt: The user prompt to process
|
|
191
|
+
usage_limits: Optional usage limits for the agent
|
|
192
|
+
**kwargs: Additional arguments to pass to agent.run (e.g., message_history)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The agent's response
|
|
196
|
+
"""
|
|
197
|
+
agent = self.get_agent()
|
|
198
|
+
return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
|
|
199
|
+
|
|
200
|
+
def __getattr__(self, name: str) -> Any:
|
|
201
|
+
"""
|
|
202
|
+
Proxy all other attribute access to the current agent.
|
|
203
|
+
|
|
204
|
+
This allows the manager to be used as a drop-in replacement
|
|
205
|
+
for direct agent access.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
name: The attribute name to access
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The attribute from the current agent
|
|
212
|
+
"""
|
|
213
|
+
agent = self.get_agent()
|
|
214
|
+
return getattr(agent, name)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Global singleton instance
|
|
218
|
+
_runtime_manager: Optional[RuntimeAgentManager] = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_runtime_agent_manager() -> RuntimeAgentManager:
|
|
222
|
+
"""
|
|
223
|
+
Get the global runtime agent manager instance.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The singleton RuntimeAgentManager instance
|
|
227
|
+
"""
|
|
228
|
+
global _runtime_manager
|
|
229
|
+
if _runtime_manager is None:
|
|
230
|
+
_runtime_manager = RuntimeAgentManager()
|
|
231
|
+
return _runtime_manager
|
|
@@ -9,23 +9,42 @@ from code_puppy.command_line.utils import make_directory_table
|
|
|
9
9
|
from code_puppy.config import get_config_keys
|
|
10
10
|
from code_puppy.tools.tools_content import tools_content
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/
|
|
24
|
-
/
|
|
25
|
-
/
|
|
26
|
-
/
|
|
27
|
-
|
|
28
|
-
"""
|
|
12
|
+
def get_commands_help():
|
|
13
|
+
"""Generate commands help using Rich Text objects to avoid markup conflicts."""
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
# Build help text programmatically
|
|
17
|
+
help_lines = []
|
|
18
|
+
|
|
19
|
+
# Title
|
|
20
|
+
help_lines.append(Text("Commands Help", style="bold magenta"))
|
|
21
|
+
|
|
22
|
+
# Commands - build each line programmatically
|
|
23
|
+
help_lines.append(Text("/help, /h", style="cyan") + Text(" Show this help message"))
|
|
24
|
+
help_lines.append(Text("/cd", style="cyan") + Text(" <dir> Change directory or show directories"))
|
|
25
|
+
help_lines.append(Text("/agent", style="cyan") + Text(" <name> Switch to a different agent or show available agents"))
|
|
26
|
+
help_lines.append(Text("/exit, /quit", style="cyan") + Text(" Exit interactive mode"))
|
|
27
|
+
help_lines.append(Text("/generate-pr-description", style="cyan") + Text(" [@dir] Generate comprehensive PR description"))
|
|
28
|
+
help_lines.append(Text("/model", style="cyan") + Text(" <model> Set active model"))
|
|
29
|
+
help_lines.append(Text("/mcp", style="cyan") + Text(" Manage MCP servers (list, start, stop, status, etc.)"))
|
|
30
|
+
help_lines.append(Text("/motd", style="cyan") + Text(" Show the latest message of the day (MOTD)"))
|
|
31
|
+
help_lines.append(Text("/show", style="cyan") + Text(" Show puppy config key-values"))
|
|
32
|
+
help_lines.append(Text("/compact", style="cyan") + Text(" Summarize and compact current chat history"))
|
|
33
|
+
help_lines.append(Text("/dump_context", style="cyan") + Text(" <name> Save current message history to file"))
|
|
34
|
+
help_lines.append(Text("/load_context", style="cyan") + Text(" <name> Load message history from file"))
|
|
35
|
+
help_lines.append(Text("/set", style="cyan") + Text(" Set puppy config key-values (e.g., /set yolo_mode true, /set compaction_strategy truncation)"))
|
|
36
|
+
help_lines.append(Text("/tools", style="cyan") + Text(" Show available tools and capabilities"))
|
|
37
|
+
help_lines.append(Text("/<unknown>", style="cyan") + Text(" Show unknown command warning"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Combine all lines
|
|
41
|
+
final_text = Text()
|
|
42
|
+
for i, line in enumerate(help_lines):
|
|
43
|
+
if i > 0:
|
|
44
|
+
final_text.append("\n")
|
|
45
|
+
final_text.append_text(line)
|
|
46
|
+
|
|
47
|
+
return final_text
|
|
29
48
|
|
|
30
49
|
|
|
31
50
|
def handle_command(command: str):
|
|
@@ -220,7 +239,7 @@ def handle_command(command: str):
|
|
|
220
239
|
set_current_agent,
|
|
221
240
|
get_agent_descriptions,
|
|
222
241
|
)
|
|
223
|
-
from code_puppy.
|
|
242
|
+
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
224
243
|
|
|
225
244
|
tokens = command.split()
|
|
226
245
|
|
|
@@ -272,7 +291,8 @@ def handle_command(command: str):
|
|
|
272
291
|
|
|
273
292
|
if set_current_agent(agent_name):
|
|
274
293
|
# Reload the agent with new configuration
|
|
275
|
-
|
|
294
|
+
manager = get_runtime_agent_manager()
|
|
295
|
+
manager.reload_agent()
|
|
276
296
|
new_agent = get_current_agent_config()
|
|
277
297
|
emit_success(
|
|
278
298
|
f"Switched to agent: {new_agent.display_name}",
|
|
@@ -297,25 +317,36 @@ def handle_command(command: str):
|
|
|
297
317
|
emit_warning("Usage: /agent [agent-name]")
|
|
298
318
|
return True
|
|
299
319
|
|
|
300
|
-
if command.startswith("/
|
|
320
|
+
if command.startswith("/model"):
|
|
301
321
|
# Try setting model and show confirmation
|
|
302
|
-
|
|
322
|
+
# Handle both /model and /m for backward compatibility
|
|
323
|
+
model_command = command.replace("/model", "/m") if command.startswith("/model") else command
|
|
324
|
+
new_input = update_model_in_input(model_command)
|
|
303
325
|
if new_input is not None:
|
|
304
|
-
from code_puppy.
|
|
326
|
+
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
305
327
|
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
306
328
|
|
|
307
329
|
model = get_active_model()
|
|
308
330
|
# Make sure this is called for the test
|
|
309
|
-
|
|
331
|
+
manager = get_runtime_agent_manager()
|
|
332
|
+
manager.reload_agent()
|
|
310
333
|
emit_success(f"Active model set and loaded: {model}")
|
|
311
334
|
return True
|
|
312
335
|
# If no model matched, show available models
|
|
313
336
|
model_names = load_model_names()
|
|
314
|
-
emit_warning("Usage: /
|
|
337
|
+
emit_warning("Usage: /model <model-name>")
|
|
315
338
|
emit_warning(f"Available models: {', '.join(model_names)}")
|
|
316
339
|
return True
|
|
340
|
+
|
|
341
|
+
if command.startswith("/mcp"):
|
|
342
|
+
from code_puppy.command_line.mcp_commands import MCPCommandHandler
|
|
343
|
+
handler = MCPCommandHandler()
|
|
344
|
+
return handler.handle_mcp_command(command)
|
|
317
345
|
if command in ("/help", "/h"):
|
|
318
|
-
|
|
346
|
+
import uuid
|
|
347
|
+
group_id = str(uuid.uuid4())
|
|
348
|
+
help_text = get_commands_help()
|
|
349
|
+
emit_info(help_text, message_group_id=group_id)
|
|
319
350
|
return True
|
|
320
351
|
|
|
321
352
|
if command.startswith("/generate-pr-description"):
|