code-puppy 0.0.172__py3-none-any.whl → 0.0.174__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.
Files changed (34) hide show
  1. code_puppy/agent.py +14 -14
  2. code_puppy/agents/__init__.py +4 -6
  3. code_puppy/agents/agent_manager.py +15 -187
  4. code_puppy/agents/base_agent.py +798 -4
  5. code_puppy/command_line/command_handler.py +40 -41
  6. code_puppy/command_line/mcp/add_command.py +1 -1
  7. code_puppy/command_line/mcp/install_command.py +1 -1
  8. code_puppy/command_line/mcp/start_all_command.py +3 -6
  9. code_puppy/command_line/mcp/start_command.py +0 -5
  10. code_puppy/command_line/mcp/stop_all_command.py +3 -6
  11. code_puppy/command_line/mcp/stop_command.py +2 -6
  12. code_puppy/command_line/model_picker_completion.py +2 -2
  13. code_puppy/command_line/prompt_toolkit_completion.py +2 -2
  14. code_puppy/config.py +2 -3
  15. code_puppy/main.py +13 -49
  16. code_puppy/messaging/message_queue.py +4 -4
  17. code_puppy/summarization_agent.py +2 -2
  18. code_puppy/tools/agent_tools.py +5 -4
  19. code_puppy/tools/browser/vqa_agent.py +1 -3
  20. code_puppy/tools/command_runner.py +1 -1
  21. code_puppy/tui/app.py +49 -78
  22. code_puppy/tui/screens/settings.py +2 -2
  23. code_puppy/tui_state.py +55 -0
  24. {code_puppy-0.0.172.dist-info → code_puppy-0.0.174.dist-info}/METADATA +2 -2
  25. {code_puppy-0.0.172.dist-info → code_puppy-0.0.174.dist-info}/RECORD +29 -33
  26. code_puppy/agents/agent_orchestrator.json +0 -26
  27. code_puppy/agents/runtime_manager.py +0 -272
  28. code_puppy/command_line/meta_command_handler.py +0 -153
  29. code_puppy/message_history_processor.py +0 -486
  30. code_puppy/state_management.py +0 -159
  31. {code_puppy-0.0.172.data → code_puppy-0.0.174.data}/data/code_puppy/models.json +0 -0
  32. {code_puppy-0.0.172.dist-info → code_puppy-0.0.174.dist-info}/WHEEL +0 -0
  33. {code_puppy-0.0.172.dist-info → code_puppy-0.0.174.dist-info}/entry_points.txt +0 -0
  34. {code_puppy-0.0.172.dist-info → code_puppy-0.0.174.dist-info}/licenses/LICENSE +0 -0
@@ -1,272 +0,0 @@
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 sys
11
- import uuid
12
- from typing import Any, Optional
13
-
14
- # ExceptionGroup is available in Python 3.11+
15
- if sys.version_info >= (3, 11):
16
- from builtins import ExceptionGroup
17
- else:
18
- # For Python 3.10 and below, we can define a simple fallback
19
- class ExceptionGroup(Exception):
20
- def __init__(self, message, exceptions):
21
- super().__init__(message)
22
- self.exceptions = exceptions
23
-
24
-
25
- import mcp
26
- from pydantic_ai import Agent
27
- from pydantic_ai.exceptions import UsageLimitExceeded
28
- from pydantic_ai.usage import UsageLimits
29
-
30
- from code_puppy.messaging.message_queue import emit_info
31
-
32
-
33
- class RuntimeAgentManager:
34
- """
35
- Manages the runtime agent instance and ensures proper updates.
36
-
37
- This class acts as a proxy that always returns the current agent instance,
38
- ensuring that when the agent is reloaded, all code using this manager
39
- automatically gets the updated instance.
40
- """
41
-
42
- def __init__(self):
43
- """Initialize the runtime agent manager."""
44
- self._agent: Optional[Agent] = None
45
- self._last_model_name: Optional[str] = None
46
-
47
- def get_agent(self, force_reload: bool = False, message_group: str = "") -> Agent:
48
- """
49
- Get the current agent instance.
50
-
51
- This method always returns the most recent agent instance,
52
- automatically handling reloads when the model changes.
53
-
54
- Args:
55
- force_reload: If True, force a reload of the agent
56
-
57
- Returns:
58
- The current agent instance
59
- """
60
- from code_puppy.agent import get_code_generation_agent
61
-
62
- # Always get the current singleton - this ensures we have the latest
63
- current_agent = get_code_generation_agent(
64
- force_reload=force_reload, message_group=message_group
65
- )
66
- self._agent = current_agent
67
-
68
- return self._agent
69
-
70
- def reload_agent(self) -> Agent:
71
- """
72
- Force reload the agent.
73
-
74
- This is typically called after MCP servers are started/stopped.
75
-
76
- Returns:
77
- The newly loaded agent instance
78
- """
79
- message_group = uuid.uuid4()
80
- emit_info(
81
- "[bold cyan]Reloading agent with updated configuration...[/bold cyan]",
82
- message_group=message_group,
83
- )
84
- return self.get_agent(force_reload=True, message_group=message_group)
85
-
86
- async def run_with_mcp(
87
- self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs
88
- ) -> Any:
89
- """
90
- Run the agent with MCP servers and full cancellation support.
91
-
92
- This method ensures we're always using the current agent instance
93
- and handles Ctrl+C interruption properly by creating a cancellable task.
94
-
95
- Args:
96
- prompt: The user prompt to process
97
- usage_limits: Optional usage limits for the agent
98
- **kwargs: Additional arguments to pass to agent.run (e.g., message_history)
99
-
100
- Returns:
101
- The agent's response
102
-
103
- Raises:
104
- asyncio.CancelledError: When execution is cancelled by user
105
- """
106
- agent = self.get_agent()
107
- group_id = str(uuid.uuid4())
108
-
109
- # Function to run agent with MCP
110
- async def run_agent_task():
111
- try:
112
- async with agent:
113
- return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
114
- except* UsageLimitExceeded as ule:
115
- emit_info(f"Usage limit exceeded: {str(ule)}", group_id=group_id)
116
- emit_info(
117
- "The agent has reached its usage limit. You can ask it to continue by saying 'please continue' or similar.",
118
- group_id=group_id,
119
- )
120
- except* mcp.shared.exceptions.McpError as mcp_error:
121
- emit_info(f"MCP server error: {str(mcp_error)}", group_id=group_id)
122
- emit_info(f"{str(mcp_error)}", group_id=group_id)
123
- emit_info(
124
- "Try disabling any malfunctioning MCP servers", group_id=group_id
125
- )
126
- except* asyncio.exceptions.CancelledError:
127
- emit_info("Cancelled")
128
- except* InterruptedError as ie:
129
- emit_info(f"Interrupted: {str(ie)}")
130
- except* Exception as other_error:
131
- # Filter out CancelledError and UsageLimitExceeded from the exception group - let it propagate
132
- remaining_exceptions = []
133
-
134
- def collect_non_cancelled_exceptions(exc):
135
- if isinstance(exc, ExceptionGroup):
136
- for sub_exc in exc.exceptions:
137
- collect_non_cancelled_exceptions(sub_exc)
138
- elif not isinstance(
139
- exc, (asyncio.CancelledError, UsageLimitExceeded)
140
- ):
141
- remaining_exceptions.append(exc)
142
- emit_info(f"Unexpected error: {str(exc)}", group_id=group_id)
143
- emit_info(f"{str(exc.args)}", group_id=group_id)
144
-
145
- collect_non_cancelled_exceptions(other_error)
146
-
147
- # If there are CancelledError exceptions in the group, re-raise them
148
- cancelled_exceptions = []
149
-
150
- def collect_cancelled_exceptions(exc):
151
- if isinstance(exc, ExceptionGroup):
152
- for sub_exc in exc.exceptions:
153
- collect_cancelled_exceptions(sub_exc)
154
- elif isinstance(exc, asyncio.CancelledError):
155
- cancelled_exceptions.append(exc)
156
-
157
- collect_cancelled_exceptions(other_error)
158
-
159
- if cancelled_exceptions:
160
- # Re-raise the first CancelledError to propagate cancellation
161
- raise cancelled_exceptions[0]
162
-
163
- # Create the task FIRST
164
- agent_task = asyncio.create_task(run_agent_task())
165
-
166
- # Import shell process killer
167
- from code_puppy.tools.command_runner import kill_all_running_shell_processes
168
-
169
- # Ensure the interrupt handler only acts once per task
170
- def keyboard_interrupt_handler(sig, frame):
171
- """Signal handler for Ctrl+C - replicating exact original logic"""
172
-
173
- # First, nuke any running shell processes triggered by tools
174
- try:
175
- killed = kill_all_running_shell_processes()
176
- if killed:
177
- emit_info(f"Cancelled {killed} running shell process(es).")
178
- else:
179
- # Only cancel the agent task if no shell processes were killed
180
- if not agent_task.done():
181
- agent_task.cancel()
182
- except Exception as e:
183
- emit_info(f"Shell kill error: {e}")
184
- # If shell kill failed, still try to cancel the agent task
185
- if not agent_task.done():
186
- agent_task.cancel()
187
- # Don't call the original handler
188
- # This prevents the application from exiting
189
-
190
- try:
191
- # Save original handler and set our custom one AFTER task is created
192
- original_handler = signal.signal(signal.SIGINT, keyboard_interrupt_handler)
193
-
194
- # Wait for the task to complete or be cancelled
195
- result = await agent_task
196
- return result
197
- except asyncio.CancelledError:
198
- # Task was cancelled by our handler
199
- raise
200
- except KeyboardInterrupt:
201
- # Handle direct keyboard interrupt during await
202
- if not agent_task.done():
203
- agent_task.cancel()
204
- try:
205
- await agent_task
206
- except asyncio.CancelledError:
207
- pass
208
- raise asyncio.CancelledError()
209
- finally:
210
- # Restore original signal handler
211
- if original_handler:
212
- signal.signal(signal.SIGINT, original_handler)
213
-
214
- async def run(
215
- self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs
216
- ) -> Any:
217
- """
218
- Run the agent without explicitly managing MCP servers.
219
-
220
- Args:
221
- prompt: The user prompt to process
222
- usage_limits: Optional usage limits for the agent
223
- **kwargs: Additional arguments to pass to agent.run (e.g., message_history)
224
-
225
- Returns:
226
- The agent's response
227
- """
228
- agent = self.get_agent()
229
- try:
230
- return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
231
- except UsageLimitExceeded as ule:
232
- group_id = str(uuid.uuid4())
233
- emit_info(f"Usage limit exceeded: {str(ule)}", group_id=group_id)
234
- emit_info(
235
- "The agent has reached its usage limit. You can ask it to continue by saying 'please continue' or similar.",
236
- group_id=group_id,
237
- )
238
- # Return None or some default value to indicate the limit was reached
239
- return None
240
-
241
- def __getattr__(self, name: str) -> Any:
242
- """
243
- Proxy all other attribute access to the current agent.
244
-
245
- This allows the manager to be used as a drop-in replacement
246
- for direct agent access.
247
-
248
- Args:
249
- name: The attribute name to access
250
-
251
- Returns:
252
- The attribute from the current agent
253
- """
254
- agent = self.get_agent()
255
- return getattr(agent, name)
256
-
257
-
258
- # Global singleton instance
259
- _runtime_manager: Optional[RuntimeAgentManager] = None
260
-
261
-
262
- def get_runtime_agent_manager() -> RuntimeAgentManager:
263
- """
264
- Get the global runtime agent manager instance.
265
-
266
- Returns:
267
- The singleton RuntimeAgentManager instance
268
- """
269
- global _runtime_manager
270
- if _runtime_manager is None:
271
- _runtime_manager = RuntimeAgentManager()
272
- return _runtime_manager
@@ -1,153 +0,0 @@
1
- import os
2
-
3
- from rich.console import Console
4
-
5
- from code_puppy.command_line.model_picker_completion import (
6
- load_model_names,
7
- update_model_in_input,
8
- )
9
- from code_puppy.config import get_config_keys
10
- from code_puppy.command_line.utils import make_directory_table
11
- from code_puppy.command_line.motd import print_motd
12
-
13
- META_COMMANDS_HELP = """
14
- [bold magenta]Meta Commands Help[/bold magenta]
15
- ~help, ~h Show this help message
16
- ~cd <dir> Change directory or show directories
17
- ~m <model> Set active model
18
- ~motd Show the latest message of the day (MOTD)
19
- ~show Show puppy config key-values
20
- ~set Set puppy config key-values (message_limit, protected_token_count, compaction_threshold, allow_recursion, etc.)
21
- ~<unknown> Show unknown meta command warning
22
- """
23
-
24
-
25
- def handle_meta_command(command: str, console: Console) -> bool:
26
- """
27
- Handle meta/config commands prefixed with '~'.
28
- Returns True if the command was handled (even if just an error/help), False if not.
29
- """
30
- command = command.strip()
31
-
32
- if command.strip().startswith("~motd"):
33
- print_motd(console, force=True)
34
- return True
35
-
36
- if command.startswith("~cd"):
37
- tokens = command.split()
38
- if len(tokens) == 1:
39
- try:
40
- table = make_directory_table()
41
- console.print(table)
42
- except Exception as e:
43
- console.print(f"[red]Error listing directory:[/red] {e}")
44
- return True
45
- elif len(tokens) == 2:
46
- dirname = tokens[1]
47
- target = os.path.expanduser(dirname)
48
- if not os.path.isabs(target):
49
- target = os.path.join(os.getcwd(), target)
50
- if os.path.isdir(target):
51
- os.chdir(target)
52
- console.print(
53
- f"[bold green]Changed directory to:[/bold green] [cyan]{target}[/cyan]"
54
- )
55
- else:
56
- console.print(f"[red]Not a directory:[/red] [bold]{dirname}[/bold]")
57
- return True
58
-
59
- if command.strip().startswith("~show"):
60
- from code_puppy.command_line.model_picker_completion import get_active_model
61
- from code_puppy.config import (
62
- get_owner_name,
63
- get_puppy_name,
64
- get_yolo_mode,
65
- get_message_limit,
66
- )
67
-
68
- puppy_name = get_puppy_name()
69
- owner_name = get_owner_name()
70
- model = get_active_model()
71
- yolo_mode = get_yolo_mode()
72
- msg_limit = get_message_limit()
73
- console.print(f"""[bold magenta]🐶 Puppy Status[/bold magenta]
74
-
75
- [bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
76
- [bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
77
- [bold]model:[/bold] [green]{model}[/green]
78
- [bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
79
- [bold]message_limit:[/bold] [cyan]{msg_limit}[/cyan] requests per minute
80
- """)
81
- return True
82
-
83
- if command.startswith("~set"):
84
- # Syntax: ~set KEY=VALUE or ~set KEY VALUE
85
- from code_puppy.config import set_config_value
86
-
87
- tokens = command.split(None, 2)
88
- argstr = command[len("~set") :].strip()
89
- key = None
90
- value = None
91
- if "=" in argstr:
92
- key, value = argstr.split("=", 1)
93
- key = key.strip()
94
- value = value.strip()
95
- elif len(tokens) >= 3:
96
- key = tokens[1]
97
- value = tokens[2]
98
- elif len(tokens) == 2:
99
- key = tokens[1]
100
- value = ""
101
- else:
102
- console.print(
103
- f"[yellow]Usage:[/yellow] ~set KEY=VALUE or ~set KEY VALUE\nConfig keys: {', '.join(get_config_keys())}"
104
- )
105
- return True
106
- if key:
107
- set_config_value(key, value)
108
- console.print(
109
- f'[green]🌶 Set[/green] [cyan]{key}[/cyan] = "{value}" in puppy.cfg!'
110
- )
111
- else:
112
- console.print("[red]You must supply a key.[/red]")
113
- return True
114
-
115
- if command.startswith("~m"):
116
- # Try setting model and show confirmation
117
- new_input = update_model_in_input(command)
118
- if new_input is not None:
119
- from code_puppy.command_line.model_picker_completion import get_active_model
120
- from code_puppy.agents.runtime_manager import get_runtime_agent_manager
121
-
122
- model = get_active_model()
123
- # Make sure this is called for the test
124
- manager = get_runtime_agent_manager()
125
- manager.reload_agent()
126
- console.print(
127
- f"[bold green]Active model set and loaded:[/bold green] [cyan]{model}[/cyan]"
128
- )
129
- return True
130
- # If no model matched, show available models
131
- model_names = load_model_names()
132
- console.print("[yellow]Usage:[/yellow] ~m <model-name>")
133
- console.print(f"[yellow]Available models:[/yellow] {', '.join(model_names)}")
134
- return True
135
- if command in ("~help", "~h"):
136
- console.print(META_COMMANDS_HELP)
137
- return True
138
- if command.startswith("~"):
139
- name = command[1:].split()[0] if len(command) > 1 else ""
140
- if name:
141
- console.print(
142
- f"[yellow]Unknown meta command:[/yellow] {command}\n[dim]Type ~help for options.[/dim]"
143
- )
144
- else:
145
- # Show current model ONLY here
146
- from code_puppy.command_line.model_picker_completion import get_active_model
147
-
148
- current_model = get_active_model()
149
- console.print(
150
- f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
151
- )
152
- return True
153
- return False