code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -1,22 +1,209 @@
1
1
  # agent_tools.py
2
-
3
- from typing import List
2
+ import asyncio
3
+ import hashlib
4
+ import itertools
5
+ import json
6
+ import pickle
7
+ import re
8
+ import traceback
9
+ from datetime import datetime
10
+ from functools import partial
11
+ from pathlib import Path
12
+ from typing import List, Set
13
+
14
+ from dbos import DBOS, SetWorkflowID
4
15
  from pydantic import BaseModel
5
- from pydantic_ai import RunContext
6
16
 
17
+ # Import Agent from pydantic_ai to create temporary agents for invocation
18
+ from pydantic_ai import Agent, RunContext, UsageLimits
19
+ from pydantic_ai.messages import ModelMessage
20
+
21
+ from code_puppy.config import (
22
+ DATA_DIR,
23
+ get_message_limit,
24
+ get_use_dbos,
25
+ get_value,
26
+ )
7
27
  from code_puppy.messaging import (
8
- emit_info,
9
- emit_divider,
10
- emit_system_message,
28
+ SubAgentInvocationMessage,
29
+ SubAgentResponseMessage,
11
30
  emit_error,
31
+ emit_info,
32
+ emit_success,
33
+ get_message_bus,
34
+ get_session_context,
35
+ set_session_context,
12
36
  )
13
37
  from code_puppy.tools.common import generate_group_id
14
- from code_puppy.agents.agent_manager import get_available_agents, load_agent_config
38
+ from code_puppy.tools.subagent_context import subagent_context
15
39
 
16
- # Import Agent from pydantic_ai to create temporary agents for invocation
17
- from pydantic_ai import Agent
18
- from code_puppy.model_factory import ModelFactory
19
- from code_puppy.config import get_model_name
40
+ # Set to track active subagent invocation tasks
41
+ _active_subagent_tasks: Set[asyncio.Task] = set()
42
+
43
+ # Atomic counter for DBOS workflow IDs - ensures uniqueness even in rapid back-to-back calls
44
+ # itertools.count() is thread-safe for next() calls
45
+ _dbos_workflow_counter = itertools.count()
46
+
47
+
48
+ def _generate_dbos_workflow_id(base_id: str) -> str:
49
+ """Generate a unique DBOS workflow ID by appending an atomic counter.
50
+
51
+ DBOS requires workflow IDs to be unique across all executions.
52
+ This function ensures uniqueness by combining the base_id with
53
+ an atomically incrementing counter.
54
+
55
+ Args:
56
+ base_id: The base identifier (e.g., group_id from generate_group_id)
57
+
58
+ Returns:
59
+ A unique workflow ID in format: {base_id}-wf-{counter}
60
+ """
61
+ counter = next(_dbos_workflow_counter)
62
+ return f"{base_id}-wf-{counter}"
63
+
64
+
65
+ def _generate_session_hash_suffix() -> str:
66
+ """Generate a short SHA1 hash suffix based on current timestamp for uniqueness.
67
+
68
+ Returns:
69
+ A 6-character hex string, e.g., "a3f2b1"
70
+ """
71
+ timestamp = str(datetime.now().timestamp())
72
+ return hashlib.sha1(timestamp.encode()).hexdigest()[:6]
73
+
74
+
75
+ # Regex pattern for kebab-case session IDs
76
+ SESSION_ID_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
77
+ SESSION_ID_MAX_LENGTH = 128
78
+
79
+
80
+ def _validate_session_id(session_id: str) -> None:
81
+ """Validate that a session ID follows kebab-case naming conventions.
82
+
83
+ Args:
84
+ session_id: The session identifier to validate
85
+
86
+ Raises:
87
+ ValueError: If the session_id is invalid
88
+
89
+ Valid format:
90
+ - Lowercase letters (a-z)
91
+ - Numbers (0-9)
92
+ - Hyphens (-) to separate words
93
+ - No uppercase, no underscores, no special characters
94
+ - Length between 1 and 128 characters
95
+
96
+ Examples:
97
+ Valid: "my-session", "agent-session-1", "discussion-about-code"
98
+ Invalid: "MySession", "my_session", "my session", "my--session"
99
+ """
100
+ if not session_id:
101
+ raise ValueError("session_id cannot be empty")
102
+
103
+ if len(session_id) > SESSION_ID_MAX_LENGTH:
104
+ raise ValueError(
105
+ f"Invalid session_id '{session_id}': must be {SESSION_ID_MAX_LENGTH} characters or less"
106
+ )
107
+
108
+ if not SESSION_ID_PATTERN.match(session_id):
109
+ raise ValueError(
110
+ f"Invalid session_id '{session_id}': must be kebab-case "
111
+ "(lowercase letters, numbers, and hyphens only). "
112
+ "Examples: 'my-session', 'agent-session-1', 'discussion-about-code'"
113
+ )
114
+
115
+
116
+ def _get_subagent_sessions_dir() -> Path:
117
+ """Get the directory for storing subagent session data.
118
+
119
+ Returns:
120
+ Path to XDG data directory/subagent_sessions/
121
+ """
122
+ sessions_dir = Path(DATA_DIR) / "subagent_sessions"
123
+ sessions_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
124
+ return sessions_dir
125
+
126
+
127
+ def _save_session_history(
128
+ session_id: str,
129
+ message_history: List[ModelMessage],
130
+ agent_name: str,
131
+ initial_prompt: str | None = None,
132
+ ) -> None:
133
+ """Save session history to filesystem.
134
+
135
+ Args:
136
+ session_id: The session identifier (must be kebab-case)
137
+ message_history: List of messages to save
138
+ agent_name: Name of the agent being invoked
139
+ initial_prompt: The first prompt that started this session (for .txt metadata)
140
+
141
+ Raises:
142
+ ValueError: If session_id is not valid kebab-case format
143
+ """
144
+ # Validate session_id format before saving
145
+ _validate_session_id(session_id)
146
+
147
+ sessions_dir = _get_subagent_sessions_dir()
148
+
149
+ # Save pickle file with message history
150
+ pkl_path = sessions_dir / f"{session_id}.pkl"
151
+ with open(pkl_path, "wb") as f:
152
+ pickle.dump(message_history, f)
153
+
154
+ # Save or update txt file with metadata
155
+ txt_path = sessions_dir / f"{session_id}.txt"
156
+ if not txt_path.exists() and initial_prompt:
157
+ # Only write initial metadata on first save
158
+ metadata = {
159
+ "session_id": session_id,
160
+ "agent_name": agent_name,
161
+ "initial_prompt": initial_prompt,
162
+ "created_at": datetime.now().isoformat(),
163
+ "message_count": len(message_history),
164
+ }
165
+ with open(txt_path, "w") as f:
166
+ json.dump(metadata, f, indent=2)
167
+ elif txt_path.exists():
168
+ # Update message count on subsequent saves
169
+ try:
170
+ with open(txt_path, "r") as f:
171
+ metadata = json.load(f)
172
+ metadata["message_count"] = len(message_history)
173
+ metadata["last_updated"] = datetime.now().isoformat()
174
+ with open(txt_path, "w") as f:
175
+ json.dump(metadata, f, indent=2)
176
+ except Exception:
177
+ pass # If we can't update metadata, no big deal
178
+
179
+
180
+ def _load_session_history(session_id: str) -> List[ModelMessage]:
181
+ """Load session history from filesystem.
182
+
183
+ Args:
184
+ session_id: The session identifier (must be kebab-case)
185
+
186
+ Returns:
187
+ List of ModelMessage objects, or empty list if session doesn't exist
188
+
189
+ Raises:
190
+ ValueError: If session_id is not valid kebab-case format
191
+ """
192
+ # Validate session_id format before loading
193
+ _validate_session_id(session_id)
194
+
195
+ sessions_dir = _get_subagent_sessions_dir()
196
+ pkl_path = sessions_dir / f"{session_id}.pkl"
197
+
198
+ if not pkl_path.exists():
199
+ return []
200
+
201
+ try:
202
+ with open(pkl_path, "rb") as f:
203
+ return pickle.load(f)
204
+ except Exception:
205
+ # If pickle is corrupted or incompatible, return empty history
206
+ return []
20
207
 
21
208
 
22
209
  class AgentInfo(BaseModel):
@@ -24,6 +211,7 @@ class AgentInfo(BaseModel):
24
211
 
25
212
  name: str
26
213
  display_name: str
214
+ description: str
27
215
 
28
216
 
29
217
  class ListAgentsOutput(BaseModel):
@@ -38,6 +226,7 @@ class AgentInvokeOutput(BaseModel):
38
226
 
39
227
  response: str | None
40
228
  agent_name: str
229
+ session_id: str | None = None
41
230
  error: str | None = None
42
231
 
43
232
 
@@ -58,36 +247,50 @@ def register_list_agents(agent):
58
247
  # Generate a group ID for this tool execution
59
248
  group_id = generate_group_id("list_agents")
60
249
 
250
+ from rich.text import Text
251
+
252
+ from code_puppy.config import get_banner_color
253
+
254
+ list_agents_color = get_banner_color("list_agents")
61
255
  emit_info(
62
- "\n[bold white on blue] LIST AGENTS [/bold white on blue]",
256
+ Text.from_markup(
257
+ f"\n[bold white on {list_agents_color}] LIST AGENTS [/bold white on {list_agents_color}]"
258
+ ),
63
259
  message_group=group_id,
64
260
  )
65
- emit_divider(message_group=group_id)
66
261
 
67
262
  try:
68
- # Get available agents from the agent manager
263
+ from code_puppy.agents import get_agent_descriptions, get_available_agents
264
+
265
+ # Get available agents and their descriptions from the agent manager
69
266
  agents_dict = get_available_agents()
267
+ descriptions_dict = get_agent_descriptions()
70
268
 
71
269
  # Convert to list of AgentInfo objects
72
270
  agents = [
73
- AgentInfo(name=name, display_name=display_name)
271
+ AgentInfo(
272
+ name=name,
273
+ display_name=display_name,
274
+ description=descriptions_dict.get(name, "No description available"),
275
+ )
74
276
  for name, display_name in agents_dict.items()
75
277
  ]
76
278
 
77
- # Display the agents in the console
279
+ # Accumulate output into a single string and emit once
280
+ # Use Text.from_markup() to pass a Rich object that won't be escaped
281
+ lines = []
78
282
  for agent_item in agents:
79
- emit_system_message(
80
- f"- [bold]{agent_item.name}[/bold]: {agent_item.display_name}",
81
- message_group=group_id,
283
+ lines.append(
284
+ f"- [bold]{agent_item.name}[/bold]: {agent_item.display_name}\n"
285
+ f" [dim]{agent_item.description}[/dim]"
82
286
  )
287
+ emit_info(Text.from_markup("\n".join(lines)), message_group=group_id)
83
288
 
84
- emit_divider(message_group=group_id)
85
289
  return ListAgentsOutput(agents=agents)
86
290
 
87
291
  except Exception as e:
88
292
  error_msg = f"Error listing agents: {str(e)}"
89
293
  emit_error(error_msg, message_group=group_id)
90
- emit_divider(message_group=group_id)
91
294
  return ListAgentsOutput(agents=[], error=error_msg)
92
295
 
93
296
  return list_agents
@@ -101,35 +304,162 @@ def register_invoke_agent(agent):
101
304
  """
102
305
 
103
306
  @agent.tool
104
- def invoke_agent(
105
- context: RunContext, agent_name: str, prompt: str
307
+ async def invoke_agent(
308
+ context: RunContext, agent_name: str, prompt: str, session_id: str | None = None
106
309
  ) -> AgentInvokeOutput:
107
310
  """Invoke a specific sub-agent with a given prompt.
108
311
 
109
312
  Args:
110
313
  agent_name: The name of the agent to invoke
111
314
  prompt: The prompt to send to the agent
315
+ session_id: Optional session ID for maintaining conversation memory across invocations.
316
+
317
+ **Session ID Format:**
318
+ - Must be kebab-case (lowercase letters, numbers, hyphens only)
319
+ - Should be human-readable: e.g., "implement-oauth", "review-auth"
320
+ - For NEW sessions, a SHA1 hash suffix is automatically appended for uniqueness
321
+ - To CONTINUE a session, use the full session_id (with hash) from the previous invocation
322
+ - If None (default), auto-generates like "agent-name-session-1"
323
+
324
+ **When to use session_id:**
325
+ - **NEW SESSION**: Provide a base name like "review-auth" - we'll append a unique hash
326
+ - **CONTINUE SESSION**: Use the full session_id from output (e.g., "review-auth-a3f2b1")
327
+ - **ONE-OFF TASKS**: Leave as None (auto-generate)
328
+
329
+ **Most common pattern:** Leave session_id as None (auto-generate) unless you
330
+ specifically need conversational memory.
112
331
 
113
332
  Returns:
114
- AgentInvokeOutput: The agent's response to the prompt
333
+ AgentInvokeOutput: Contains:
334
+ - response (str | None): The agent's response to the prompt
335
+ - agent_name (str): Name of the invoked agent
336
+ - session_id (str | None): The full session ID (with hash suffix) - USE THIS to continue the conversation!
337
+ - error (str | None): Error message if invocation failed
338
+
339
+ Examples:
340
+ # COMMON CASE: One-off invocation, no memory needed (auto-generate session)
341
+ result = invoke_agent(
342
+ "qa-expert",
343
+ "Review this function: def add(a, b): return a + b"
344
+ )
345
+ # result.session_id will be something like "qa-expert-session-a3f2b1"
346
+
347
+ # MULTI-TURN: Start a NEW conversation with a base session ID
348
+ # A hash suffix is auto-appended: "review-add-function" -> "review-add-function-a3f2b1"
349
+ result1 = invoke_agent(
350
+ "qa-expert",
351
+ "Review this function: def add(a, b): return a + b",
352
+ session_id="review-add-function"
353
+ )
354
+ # result1.session_id contains the full ID like "review-add-function-a3f2b1"
355
+
356
+ # Continue the SAME conversation using session_id from the previous result
357
+ result2 = invoke_agent(
358
+ "qa-expert",
359
+ "Can you suggest edge cases for that function?",
360
+ session_id=result1.session_id # Use the session_id from previous output!
361
+ )
362
+
363
+ # Multiple INDEPENDENT reviews (each gets unique hash suffix)
364
+ auth_review = invoke_agent(
365
+ "code-reviewer",
366
+ "Review my authentication code",
367
+ session_id="auth-review" # -> "auth-review-<hash1>"
368
+ )
369
+ # auth_review.session_id contains the full ID to continue this review
370
+
371
+ payment_review = invoke_agent(
372
+ "code-reviewer",
373
+ "Review my payment processing code",
374
+ session_id="payment-review" # -> "payment-review-<hash2>"
375
+ )
376
+ # payment_review.session_id contains a different full ID
115
377
  """
378
+ from code_puppy.agents.agent_manager import load_agent
379
+
380
+ # Validate user-provided session_id if given
381
+ if session_id is not None:
382
+ try:
383
+ _validate_session_id(session_id)
384
+ except ValueError as e:
385
+ # Return error immediately if session_id is invalid
386
+ group_id = generate_group_id("invoke_agent", agent_name)
387
+ emit_error(str(e), message_group=group_id)
388
+ return AgentInvokeOutput(
389
+ response=None, agent_name=agent_name, error=str(e)
390
+ )
391
+
116
392
  # Generate a group ID for this tool execution
117
393
  group_id = generate_group_id("invoke_agent", agent_name)
118
394
 
119
- emit_info(
120
- f"\n[bold white on blue] INVOKE AGENT [/bold white on blue] {agent_name}",
121
- message_group=group_id,
395
+ # Check if this is an existing session or a new one
396
+ # For user-provided session_id, check if it exists
397
+ # For None, we'll generate a new one below
398
+ if session_id is not None:
399
+ message_history = _load_session_history(session_id)
400
+ is_new_session = len(message_history) == 0
401
+ else:
402
+ message_history = []
403
+ is_new_session = True
404
+
405
+ # Generate or finalize session_id
406
+ if session_id is None:
407
+ # Auto-generate a session ID with hash suffix for uniqueness
408
+ # Example: "qa-expert-session-a3f2b1"
409
+ hash_suffix = _generate_session_hash_suffix()
410
+ session_id = f"{agent_name}-session-{hash_suffix}"
411
+ elif is_new_session:
412
+ # User provided a base name for a NEW session - append hash suffix
413
+ # Example: "review-auth" -> "review-auth-a3f2b1"
414
+ hash_suffix = _generate_session_hash_suffix()
415
+ session_id = f"{session_id}-{hash_suffix}"
416
+ # else: continuing existing session, use session_id as-is
417
+
418
+ # Lazy imports to avoid circular dependency
419
+ from code_puppy.agents.subagent_stream_handler import subagent_stream_handler
420
+
421
+ # Emit structured invocation message via MessageBus
422
+ bus = get_message_bus()
423
+ bus.emit(
424
+ SubAgentInvocationMessage(
425
+ agent_name=agent_name,
426
+ session_id=session_id,
427
+ prompt=prompt,
428
+ is_new_session=is_new_session,
429
+ message_count=len(message_history),
430
+ )
431
+ )
432
+
433
+ # Save current session context and set the new one for this sub-agent
434
+ previous_session_id = get_session_context()
435
+ set_session_context(session_id)
436
+
437
+ # Set terminal session for browser-based terminal tools
438
+ # This uses contextvars which properly propagate through async tasks
439
+ from code_puppy.tools.browser.terminal_tools import (
440
+ _terminal_session_var,
441
+ set_terminal_session,
442
+ )
443
+
444
+ terminal_session_token = set_terminal_session(f"terminal-{session_id}")
445
+
446
+ # Set browser session for browser tools (qa-kitten, etc.)
447
+ # This allows parallel agent invocations to each have their own browser
448
+ from code_puppy.tools.browser.browser_manager import (
449
+ set_browser_session,
122
450
  )
123
- emit_divider(message_group=group_id)
124
- emit_system_message(f"Prompt: {prompt}", message_group=group_id)
125
- emit_divider(message_group=group_id)
451
+
452
+ browser_session_token = set_browser_session(f"browser-{session_id}")
126
453
 
127
454
  try:
455
+ # Lazy import to break circular dependency with messaging module
456
+ from code_puppy.model_factory import ModelFactory, make_model_settings
457
+
128
458
  # Load the specified agent config
129
- agent_config = load_agent_config(agent_name)
459
+ agent_config = load_agent(agent_name)
130
460
 
131
461
  # Get the current model for creating a temporary agent
132
- model_name = get_model_name()
462
+ model_name = agent_config.get_model_name()
133
463
  models_config = ModelFactory.load_config()
134
464
 
135
465
  # Only proceed if we have a valid model configuration
@@ -140,36 +470,205 @@ def register_invoke_agent(agent):
140
470
 
141
471
  # Create a temporary agent instance to avoid interfering with current agent state
142
472
  instructions = agent_config.get_system_prompt()
143
- temp_agent = Agent(
144
- model=model,
145
- instructions=instructions,
146
- output_type=str,
147
- retries=3,
473
+
474
+ # Add AGENTS.md content to subagents
475
+ puppy_rules = agent_config.load_puppy_rules()
476
+ if puppy_rules:
477
+ instructions += f"\n\n{puppy_rules}"
478
+
479
+ # Apply prompt additions (like file permission handling) to temporary agents
480
+ from code_puppy import callbacks
481
+ from code_puppy.model_utils import prepare_prompt_for_model
482
+
483
+ prompt_additions = callbacks.on_load_prompt()
484
+ if len(prompt_additions):
485
+ instructions += "\n" + "\n".join(prompt_additions)
486
+
487
+ # Handle claude-code models: swap instructions, and prepend system prompt only on first message
488
+ prepared = prepare_prompt_for_model(
489
+ model_name,
490
+ instructions,
491
+ prompt,
492
+ prepend_system_to_user=is_new_session, # Only prepend on first message
148
493
  )
494
+ instructions = prepared.instructions
495
+ prompt = prepared.user_prompt
496
+
497
+ subagent_name = f"temp-invoke-agent-{session_id}"
498
+ model_settings = make_model_settings(model_name)
499
+
500
+ # Get MCP servers for sub-agents (same as main agent)
501
+ from code_puppy.mcp_ import get_mcp_manager
502
+
503
+ mcp_servers = []
504
+ mcp_disabled = get_value("disable_mcp_servers")
505
+ if not (
506
+ mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on")
507
+ ):
508
+ manager = get_mcp_manager()
509
+ mcp_servers = manager.get_servers_for_agent()
510
+
511
+ if get_use_dbos():
512
+ from pydantic_ai.durable_exec.dbos import DBOSAgent
513
+
514
+ # For DBOS, create agent without MCP servers (to avoid serialization issues)
515
+ # and add them at runtime
516
+ temp_agent = Agent(
517
+ model=model,
518
+ instructions=instructions,
519
+ output_type=str,
520
+ retries=3,
521
+ toolsets=[], # MCP servers added separately for DBOS
522
+ history_processors=[agent_config.message_history_accumulator],
523
+ model_settings=model_settings,
524
+ )
525
+
526
+ # Register the tools that the agent needs
527
+ from code_puppy.tools import register_tools_for_agent
149
528
 
150
- # Register the tools that the agent needs
151
- from code_puppy.tools import register_tools_for_agent
529
+ agent_tools = agent_config.get_available_tools()
530
+ register_tools_for_agent(temp_agent, agent_tools)
152
531
 
153
- agent_tools = agent_config.get_available_tools()
154
- register_tools_for_agent(temp_agent, agent_tools)
532
+ # Wrap with DBOS - no streaming for sub-agents
533
+ dbos_agent = DBOSAgent(
534
+ temp_agent,
535
+ name=subagent_name,
536
+ )
537
+ temp_agent = dbos_agent
538
+
539
+ # Store MCP servers to add at runtime
540
+ subagent_mcp_servers = mcp_servers
541
+ else:
542
+ # Non-DBOS path - include MCP servers directly in the agent
543
+ temp_agent = Agent(
544
+ model=model,
545
+ instructions=instructions,
546
+ output_type=str,
547
+ retries=3,
548
+ toolsets=mcp_servers,
549
+ history_processors=[agent_config.message_history_accumulator],
550
+ model_settings=model_settings,
551
+ )
155
552
 
156
- # Run the temporary agent with the provided prompt
157
- result = temp_agent.run_sync(prompt)
553
+ # Register the tools that the agent needs
554
+ from code_puppy.tools import register_tools_for_agent
555
+
556
+ agent_tools = agent_config.get_available_tools()
557
+ register_tools_for_agent(temp_agent, agent_tools)
558
+
559
+ subagent_mcp_servers = None
560
+
561
+ # Run the temporary agent with the provided prompt as an asyncio task
562
+ # Pass the message_history from the session to continue the conversation
563
+ workflow_id = None # Track for potential cancellation
564
+
565
+ # Always use subagent_stream_handler to silence output and update console manager
566
+ # This ensures all sub-agent output goes through the aggregated dashboard
567
+ stream_handler = partial(subagent_stream_handler, session_id=session_id)
568
+
569
+ # Wrap the agent run in subagent context for tracking
570
+ with subagent_context(agent_name):
571
+ if get_use_dbos():
572
+ # Generate a unique workflow ID for DBOS - ensures no collisions in back-to-back calls
573
+ workflow_id = _generate_dbos_workflow_id(group_id)
574
+
575
+ # Add MCP servers to the DBOS agent's toolsets
576
+ # (temp_agent is discarded after this invocation, so no need to restore)
577
+ if subagent_mcp_servers:
578
+ temp_agent._toolsets = (
579
+ temp_agent._toolsets + subagent_mcp_servers
580
+ )
581
+
582
+ with SetWorkflowID(workflow_id):
583
+ task = asyncio.create_task(
584
+ temp_agent.run(
585
+ prompt,
586
+ message_history=message_history,
587
+ usage_limits=UsageLimits(
588
+ request_limit=get_message_limit()
589
+ ),
590
+ event_stream_handler=stream_handler,
591
+ )
592
+ )
593
+ _active_subagent_tasks.add(task)
594
+ else:
595
+ task = asyncio.create_task(
596
+ temp_agent.run(
597
+ prompt,
598
+ message_history=message_history,
599
+ usage_limits=UsageLimits(request_limit=get_message_limit()),
600
+ event_stream_handler=stream_handler,
601
+ )
602
+ )
603
+ _active_subagent_tasks.add(task)
604
+
605
+ try:
606
+ result = await task
607
+ finally:
608
+ _active_subagent_tasks.discard(task)
609
+ if task.cancelled():
610
+ if get_use_dbos() and workflow_id:
611
+ DBOS.cancel_workflow(workflow_id)
158
612
 
159
613
  # Extract the response from the result
160
614
  response = result.output
161
615
 
162
- emit_system_message(f"Response: {response}", message_group=group_id)
163
- emit_divider(message_group=group_id)
616
+ # Update the session history with the new messages from this interaction
617
+ # The result contains all_messages which includes the full conversation
618
+ updated_history = result.all_messages()
164
619
 
165
- return AgentInvokeOutput(response=response, agent_name=agent_name)
620
+ # Save to filesystem (include initial prompt only for new sessions)
621
+ _save_session_history(
622
+ session_id=session_id,
623
+ message_history=updated_history,
624
+ agent_name=agent_name,
625
+ initial_prompt=prompt if is_new_session else None,
626
+ )
627
+
628
+ # Emit structured response message via MessageBus
629
+ bus.emit(
630
+ SubAgentResponseMessage(
631
+ agent_name=agent_name,
632
+ session_id=session_id,
633
+ response=response,
634
+ message_count=len(updated_history),
635
+ )
636
+ )
637
+
638
+ # Emit clean completion summary
639
+ emit_success(
640
+ f"✓ {agent_name} completed successfully", message_group=group_id
641
+ )
642
+
643
+ return AgentInvokeOutput(
644
+ response=response, agent_name=agent_name, session_id=session_id
645
+ )
166
646
 
167
647
  except Exception as e:
168
- error_msg = f"Error invoking agent '{agent_name}': {str(e)}"
648
+ # Emit clean failure summary
649
+ emit_error(f"✗ {agent_name} failed: {str(e)}", message_group=group_id)
650
+
651
+ # Full traceback for debugging
652
+ error_msg = f"Error invoking agent '{agent_name}': {traceback.format_exc()}"
169
653
  emit_error(error_msg, message_group=group_id)
170
- emit_divider(message_group=group_id)
654
+
171
655
  return AgentInvokeOutput(
172
- response=None, agent_name=agent_name, error=error_msg
656
+ response=None,
657
+ agent_name=agent_name,
658
+ session_id=session_id,
659
+ error=error_msg,
173
660
  )
174
661
 
662
+ finally:
663
+ # Restore the previous session context
664
+ set_session_context(previous_session_id)
665
+ # Reset terminal session context
666
+ _terminal_session_var.reset(terminal_session_token)
667
+ # Reset browser session context
668
+ from code_puppy.tools.browser.browser_manager import (
669
+ _browser_session_var,
670
+ )
671
+
672
+ _browser_session_var.reset(browser_session_token)
673
+
175
674
  return invoke_agent