code-puppy 0.0.135__tar.gz → 0.0.137__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.
Files changed (135) hide show
  1. {code_puppy-0.0.135 → code_puppy-0.0.137}/PKG-INFO +1 -1
  2. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/agent.py +15 -17
  3. code_puppy-0.0.137/code_puppy/agents/agent_manager.py +522 -0
  4. code_puppy-0.0.137/code_puppy/agents/base_agent.py +116 -0
  5. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/agents/runtime_manager.py +68 -42
  6. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/command_handler.py +82 -33
  7. code_puppy-0.0.137/code_puppy/command_line/mcp/__init__.py +10 -0
  8. code_puppy-0.0.137/code_puppy/command_line/mcp/add_command.py +183 -0
  9. code_puppy-0.0.137/code_puppy/command_line/mcp/base.py +35 -0
  10. code_puppy-0.0.137/code_puppy/command_line/mcp/handler.py +133 -0
  11. code_puppy-0.0.137/code_puppy/command_line/mcp/help_command.py +146 -0
  12. code_puppy-0.0.137/code_puppy/command_line/mcp/install_command.py +176 -0
  13. code_puppy-0.0.137/code_puppy/command_line/mcp/list_command.py +94 -0
  14. code_puppy-0.0.137/code_puppy/command_line/mcp/logs_command.py +126 -0
  15. code_puppy-0.0.137/code_puppy/command_line/mcp/remove_command.py +82 -0
  16. code_puppy-0.0.137/code_puppy/command_line/mcp/restart_command.py +92 -0
  17. code_puppy-0.0.137/code_puppy/command_line/mcp/search_command.py +117 -0
  18. code_puppy-0.0.137/code_puppy/command_line/mcp/start_all_command.py +126 -0
  19. code_puppy-0.0.137/code_puppy/command_line/mcp/start_command.py +98 -0
  20. code_puppy-0.0.137/code_puppy/command_line/mcp/status_command.py +185 -0
  21. code_puppy-0.0.137/code_puppy/command_line/mcp/stop_all_command.py +109 -0
  22. code_puppy-0.0.137/code_puppy/command_line/mcp/stop_command.py +79 -0
  23. code_puppy-0.0.137/code_puppy/command_line/mcp/test_command.py +107 -0
  24. code_puppy-0.0.137/code_puppy/command_line/mcp/utils.py +129 -0
  25. code_puppy-0.0.137/code_puppy/command_line/mcp/wizard_utils.py +259 -0
  26. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/model_picker_completion.py +21 -4
  27. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/prompt_toolkit_completion.py +9 -0
  28. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/config.py +5 -5
  29. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/main.py +23 -17
  30. code_puppy-0.0.137/code_puppy/mcp/__init__.py +49 -0
  31. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/async_lifecycle.py +51 -49
  32. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/blocking_startup.py +125 -113
  33. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/captured_stdio_server.py +63 -70
  34. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/circuit_breaker.py +63 -47
  35. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/config_wizard.py +169 -136
  36. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/dashboard.py +79 -71
  37. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/error_isolation.py +147 -100
  38. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/examples/retry_example.py +55 -42
  39. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/health_monitor.py +152 -141
  40. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/managed_server.py +100 -93
  41. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/manager.py +168 -156
  42. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/registry.py +148 -110
  43. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/retry_manager.py +63 -61
  44. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/server_registry_catalog.py +271 -225
  45. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/status_tracker.py +80 -80
  46. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/mcp/system_tools.py +47 -52
  47. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/messaging/message_queue.py +20 -13
  48. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/messaging/renderers.py +30 -15
  49. code_puppy-0.0.137/code_puppy/state_management.py +200 -0
  50. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/app.py +64 -7
  51. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/chat_view.py +3 -3
  52. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/human_input_modal.py +12 -8
  53. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/screens/__init__.py +2 -2
  54. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  55. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_agent_command.py +3 -3
  56. {code_puppy-0.0.135 → code_puppy-0.0.137}/pyproject.toml +1 -1
  57. code_puppy-0.0.135/code_puppy/agents/agent_manager.py +0 -211
  58. code_puppy-0.0.135/code_puppy/agents/base_agent.py +0 -60
  59. code_puppy-0.0.135/code_puppy/command_line/mcp_commands.py +0 -1789
  60. code_puppy-0.0.135/code_puppy/mcp/__init__.py +0 -23
  61. code_puppy-0.0.135/code_puppy/state_management.py +0 -97
  62. {code_puppy-0.0.135 → code_puppy-0.0.137}/.gitignore +0 -0
  63. {code_puppy-0.0.135 → code_puppy-0.0.137}/LICENSE +0 -0
  64. {code_puppy-0.0.135 → code_puppy-0.0.137}/README.md +0 -0
  65. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/__init__.py +0 -0
  66. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/__main__.py +0 -0
  67. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/agents/__init__.py +0 -0
  68. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/agents/agent_code_puppy.py +0 -0
  69. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/agents/agent_creator_agent.py +0 -0
  70. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/agents/json_agent.py +0 -0
  71. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/callbacks.py +0 -0
  72. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/__init__.py +0 -0
  73. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/file_path_completion.py +0 -0
  74. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/load_context_completion.py +0 -0
  75. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/meta_command_handler.py +0 -0
  76. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/motd.py +0 -0
  77. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/command_line/utils.py +0 -0
  78. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/http_utils.py +0 -0
  79. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/message_history_processor.py +0 -0
  80. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/messaging/__init__.py +0 -0
  81. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/messaging/queue_console.py +0 -0
  82. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/messaging/spinner/__init__.py +0 -0
  83. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  84. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  85. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  86. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/model_factory.py +0 -0
  87. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/models.json +0 -0
  88. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/plugins/__init__.py +0 -0
  89. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/reopenable_async_client.py +0 -0
  90. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/status_display.py +0 -0
  91. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/summarization_agent.py +0 -0
  92. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/token_utils.py +0 -0
  93. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tools/__init__.py +0 -0
  94. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tools/command_runner.py +0 -0
  95. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tools/common.py +0 -0
  96. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tools/file_modifications.py +0 -0
  97. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tools/file_operations.py +0 -0
  98. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tools/token_check.py +0 -0
  99. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tools/tools_content.py +0 -0
  100. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/__init__.py +0 -0
  101. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/__init__.py +0 -0
  102. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/command_history_modal.py +0 -0
  103. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/copy_button.py +0 -0
  104. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/custom_widgets.py +0 -0
  105. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/input_area.py +0 -0
  106. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/sidebar.py +0 -0
  107. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/components/status_bar.py +0 -0
  108. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/messages.py +0 -0
  109. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/models/__init__.py +0 -0
  110. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/models/chat_message.py +0 -0
  111. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/models/command_history.py +0 -0
  112. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/models/enums.py +0 -0
  113. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/screens/help.py +0 -0
  114. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/screens/settings.py +0 -0
  115. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/screens/tools.py +0 -0
  116. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/__init__.py +0 -0
  117. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_chat_message.py +0 -0
  118. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_chat_view.py +0 -0
  119. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_command_history.py +0 -0
  120. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_copy_button.py +0 -0
  121. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_custom_widgets.py +0 -0
  122. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_disclaimer.py +0 -0
  123. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_enums.py +0 -0
  124. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_file_browser.py +0 -0
  125. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_help.py +0 -0
  126. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_history_file_reader.py +0 -0
  127. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_input_area.py +0 -0
  128. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_settings.py +0 -0
  129. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_sidebar.py +0 -0
  130. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_sidebar_history.py +0 -0
  131. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_sidebar_history_navigation.py +0 -0
  132. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_status_bar.py +0 -0
  133. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_timestamped_history.py +0 -0
  134. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/tui/tests/test_tools.py +0 -0
  135. {code_puppy-0.0.135 → code_puppy-0.0.137}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.135
3
+ Version: 0.0.137
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -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 get_mcp_manager, ServerConfig
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(f"[bold cyan]Loading Model: {model_name}[/bold cyan]", message_group=message_group)
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)