newcode 0.1.1__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 (289) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +147 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +630 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +122 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +380 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +167 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2145 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +296 -0
  28. code_puppy/agents/pack/husky.py +307 -0
  29. code_puppy/agents/pack/retriever.py +380 -0
  30. code_puppy/agents/pack/shepherd.py +327 -0
  31. code_puppy/agents/pack/terrier.py +281 -0
  32. code_puppy/agents/pack/watchdog.py +357 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +674 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +664 -0
  49. code_puppy/cli_runner.py +1038 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +526 -0
  57. code_puppy/command_line/command_handler.py +283 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +853 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +91 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/skills_completion.py +160 -0
  97. code_puppy/command_line/uc_menu.py +893 -0
  98. code_puppy/command_line/utils.py +93 -0
  99. code_puppy/command_line/wiggum_state.py +78 -0
  100. code_puppy/config.py +1787 -0
  101. code_puppy/error_logging.py +133 -0
  102. code_puppy/gemini_code_assist.py +385 -0
  103. code_puppy/gemini_model.py +754 -0
  104. code_puppy/hook_engine/README.md +105 -0
  105. code_puppy/hook_engine/__init__.py +15 -0
  106. code_puppy/hook_engine/aliases.py +155 -0
  107. code_puppy/hook_engine/engine.py +195 -0
  108. code_puppy/hook_engine/executor.py +293 -0
  109. code_puppy/hook_engine/matcher.py +145 -0
  110. code_puppy/hook_engine/models.py +222 -0
  111. code_puppy/hook_engine/registry.py +106 -0
  112. code_puppy/hook_engine/validator.py +141 -0
  113. code_puppy/http_utils.py +361 -0
  114. code_puppy/keymap.py +128 -0
  115. code_puppy/main.py +10 -0
  116. code_puppy/mcp_/__init__.py +66 -0
  117. code_puppy/mcp_/async_lifecycle.py +286 -0
  118. code_puppy/mcp_/blocking_startup.py +469 -0
  119. code_puppy/mcp_/captured_stdio_server.py +275 -0
  120. code_puppy/mcp_/circuit_breaker.py +290 -0
  121. code_puppy/mcp_/config_wizard.py +507 -0
  122. code_puppy/mcp_/dashboard.py +308 -0
  123. code_puppy/mcp_/error_isolation.py +407 -0
  124. code_puppy/mcp_/examples/retry_example.py +226 -0
  125. code_puppy/mcp_/health_monitor.py +589 -0
  126. code_puppy/mcp_/managed_server.py +428 -0
  127. code_puppy/mcp_/manager.py +807 -0
  128. code_puppy/mcp_/mcp_logs.py +224 -0
  129. code_puppy/mcp_/registry.py +451 -0
  130. code_puppy/mcp_/retry_manager.py +337 -0
  131. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  132. code_puppy/mcp_/status_tracker.py +355 -0
  133. code_puppy/mcp_/system_tools.py +209 -0
  134. code_puppy/mcp_prompts/__init__.py +1 -0
  135. code_puppy/mcp_prompts/hook_creator.py +103 -0
  136. code_puppy/messaging/__init__.py +255 -0
  137. code_puppy/messaging/bus.py +613 -0
  138. code_puppy/messaging/commands.py +167 -0
  139. code_puppy/messaging/markdown_patches.py +57 -0
  140. code_puppy/messaging/message_queue.py +361 -0
  141. code_puppy/messaging/messages.py +569 -0
  142. code_puppy/messaging/queue_console.py +271 -0
  143. code_puppy/messaging/renderers.py +311 -0
  144. code_puppy/messaging/rich_renderer.py +1153 -0
  145. code_puppy/messaging/spinner/__init__.py +83 -0
  146. code_puppy/messaging/spinner/console_spinner.py +240 -0
  147. code_puppy/messaging/spinner/spinner_base.py +96 -0
  148. code_puppy/messaging/subagent_console.py +460 -0
  149. code_puppy/model_factory.py +848 -0
  150. code_puppy/model_switching.py +63 -0
  151. code_puppy/model_utils.py +168 -0
  152. code_puppy/models.json +130 -0
  153. code_puppy/models_dev_api.json +1 -0
  154. code_puppy/models_dev_parser.py +592 -0
  155. code_puppy/plugins/__init__.py +186 -0
  156. code_puppy/plugins/agent_skills/__init__.py +22 -0
  157. code_puppy/plugins/agent_skills/config.py +175 -0
  158. code_puppy/plugins/agent_skills/discovery.py +136 -0
  159. code_puppy/plugins/agent_skills/downloader.py +392 -0
  160. code_puppy/plugins/agent_skills/installer.py +22 -0
  161. code_puppy/plugins/agent_skills/metadata.py +219 -0
  162. code_puppy/plugins/agent_skills/prompt_builder.py +100 -0
  163. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  164. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  165. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  166. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  167. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  168. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  169. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  170. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  171. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  172. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  173. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  174. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  175. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  176. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  177. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  178. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  179. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  180. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  181. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  182. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  183. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  184. code_puppy/plugins/chatgpt_oauth/test_plugin.py +295 -0
  185. code_puppy/plugins/chatgpt_oauth/utils.py +499 -0
  186. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  187. code_puppy/plugins/claude_code_hooks/config.py +131 -0
  188. code_puppy/plugins/claude_code_hooks/register_callbacks.py +163 -0
  189. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  190. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  191. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  192. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  193. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  194. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  195. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  196. code_puppy/plugins/claude_code_oauth/utils.py +601 -0
  197. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  198. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  199. code_puppy/plugins/example_custom_command/README.md +280 -0
  200. code_puppy/plugins/example_custom_command/register_callbacks.py +48 -0
  201. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  202. code_puppy/plugins/file_permission_handler/register_callbacks.py +528 -0
  203. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  204. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  205. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  206. code_puppy/plugins/hook_creator/__init__.py +1 -0
  207. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  208. code_puppy/plugins/hook_manager/__init__.py +1 -0
  209. code_puppy/plugins/hook_manager/config.py +277 -0
  210. code_puppy/plugins/hook_manager/hooks_menu.py +551 -0
  211. code_puppy/plugins/hook_manager/register_callbacks.py +205 -0
  212. code_puppy/plugins/oauth_puppy_html.py +224 -0
  213. code_puppy/plugins/scheduler/__init__.py +1 -0
  214. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  215. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  216. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  217. code_puppy/plugins/shell_safety/__init__.py +6 -0
  218. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  219. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  220. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  221. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  222. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  223. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  224. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  225. code_puppy/plugins/universal_constructor/models.py +138 -0
  226. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  227. code_puppy/plugins/universal_constructor/registry.py +302 -0
  228. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  229. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  230. code_puppy/pydantic_patches.py +317 -0
  231. code_puppy/reopenable_async_client.py +232 -0
  232. code_puppy/round_robin_model.py +150 -0
  233. code_puppy/scheduler/__init__.py +41 -0
  234. code_puppy/scheduler/__main__.py +9 -0
  235. code_puppy/scheduler/cli.py +118 -0
  236. code_puppy/scheduler/config.py +126 -0
  237. code_puppy/scheduler/daemon.py +280 -0
  238. code_puppy/scheduler/executor.py +155 -0
  239. code_puppy/scheduler/platform.py +19 -0
  240. code_puppy/scheduler/platform_unix.py +22 -0
  241. code_puppy/scheduler/platform_win.py +32 -0
  242. code_puppy/session_storage.py +338 -0
  243. code_puppy/status_display.py +257 -0
  244. code_puppy/summarization_agent.py +176 -0
  245. code_puppy/terminal_utils.py +418 -0
  246. code_puppy/tools/__init__.py +470 -0
  247. code_puppy/tools/agent_tools.py +616 -0
  248. code_puppy/tools/ask_user_question/__init__.py +26 -0
  249. code_puppy/tools/ask_user_question/constants.py +73 -0
  250. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  251. code_puppy/tools/ask_user_question/handler.py +232 -0
  252. code_puppy/tools/ask_user_question/models.py +304 -0
  253. code_puppy/tools/ask_user_question/registration.py +36 -0
  254. code_puppy/tools/ask_user_question/renderers.py +309 -0
  255. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  256. code_puppy/tools/ask_user_question/theme.py +155 -0
  257. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  258. code_puppy/tools/browser/__init__.py +37 -0
  259. code_puppy/tools/browser/browser_control.py +289 -0
  260. code_puppy/tools/browser/browser_interactions.py +545 -0
  261. code_puppy/tools/browser/browser_locators.py +640 -0
  262. code_puppy/tools/browser/browser_manager.py +378 -0
  263. code_puppy/tools/browser/browser_navigation.py +251 -0
  264. code_puppy/tools/browser/browser_screenshot.py +179 -0
  265. code_puppy/tools/browser/browser_scripts.py +462 -0
  266. code_puppy/tools/browser/browser_workflows.py +221 -0
  267. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  268. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  269. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  270. code_puppy/tools/browser/terminal_tools.py +525 -0
  271. code_puppy/tools/command_runner.py +1346 -0
  272. code_puppy/tools/common.py +1409 -0
  273. code_puppy/tools/display.py +84 -0
  274. code_puppy/tools/file_modifications.py +739 -0
  275. code_puppy/tools/file_operations.py +802 -0
  276. code_puppy/tools/scheduler_tools.py +412 -0
  277. code_puppy/tools/skills_tools.py +251 -0
  278. code_puppy/tools/subagent_context.py +158 -0
  279. code_puppy/tools/tools_content.py +51 -0
  280. code_puppy/tools/universal_constructor.py +889 -0
  281. code_puppy/uvx_detection.py +242 -0
  282. code_puppy/version_checker.py +82 -0
  283. newcode-0.1.1.data/data/code_puppy/models.json +130 -0
  284. newcode-0.1.1.data/data/code_puppy/models_dev_api.json +1 -0
  285. newcode-0.1.1.dist-info/METADATA +154 -0
  286. newcode-0.1.1.dist-info/RECORD +289 -0
  287. newcode-0.1.1.dist-info/WHEEL +4 -0
  288. newcode-0.1.1.dist-info/entry_points.txt +3 -0
  289. newcode-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,616 @@
1
+ # agent_tools.py
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
15
+ from pydantic import BaseModel
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
+ )
27
+ from code_puppy.messaging import (
28
+ SubAgentInvocationMessage,
29
+ SubAgentResponseMessage,
30
+ emit_error,
31
+ emit_info,
32
+ emit_success,
33
+ get_message_bus,
34
+ get_session_context,
35
+ set_session_context,
36
+ )
37
+ from code_puppy.tools.common import generate_group_id
38
+ from code_puppy.tools.subagent_context import subagent_context
39
+
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 []
207
+
208
+
209
+ class AgentInfo(BaseModel):
210
+ """Information about an available agent."""
211
+
212
+ name: str
213
+ display_name: str
214
+ description: str
215
+
216
+
217
+ class ListAgentsOutput(BaseModel):
218
+ """Output for the list_agents tool."""
219
+
220
+ agents: List[AgentInfo]
221
+ error: str | None = None
222
+
223
+
224
+ class AgentInvokeOutput(BaseModel):
225
+ """Output for the invoke_agent tool."""
226
+
227
+ response: str | None
228
+ agent_name: str
229
+ session_id: str | None = None
230
+ error: str | None = None
231
+
232
+
233
+ def register_list_agents(agent):
234
+ """Register the list_agents tool with the provided agent.
235
+
236
+ Args:
237
+ agent: The agent to register the tool with
238
+ """
239
+
240
+ @agent.tool
241
+ def list_agents(context: RunContext) -> ListAgentsOutput:
242
+ """List all available sub-agents that can be invoked."""
243
+ # Generate a group ID for this tool execution
244
+ group_id = generate_group_id("list_agents")
245
+
246
+ from rich.text import Text
247
+
248
+ from code_puppy.config import get_banner_color
249
+
250
+ list_agents_color = get_banner_color("list_agents")
251
+ emit_info(
252
+ Text.from_markup(
253
+ f"\n[bold white on {list_agents_color}] LIST AGENTS [/bold white on {list_agents_color}]"
254
+ ),
255
+ message_group=group_id,
256
+ )
257
+
258
+ try:
259
+ from code_puppy.agents import get_agent_descriptions, get_available_agents
260
+
261
+ # Get available agents and their descriptions from the agent manager
262
+ agents_dict = get_available_agents()
263
+ descriptions_dict = get_agent_descriptions()
264
+
265
+ # Convert to list of AgentInfo objects
266
+ agents = [
267
+ AgentInfo(
268
+ name=name,
269
+ display_name=display_name,
270
+ description=descriptions_dict.get(name, "No description available"),
271
+ )
272
+ for name, display_name in agents_dict.items()
273
+ ]
274
+
275
+ # Accumulate output into a single string and emit once
276
+ # Use Text.from_markup() to pass a Rich object that won't be escaped
277
+ lines = []
278
+ for agent_item in agents:
279
+ lines.append(
280
+ f"- [bold]{agent_item.name}[/bold]: {agent_item.display_name}\n"
281
+ f" [dim]{agent_item.description}[/dim]"
282
+ )
283
+ emit_info(Text.from_markup("\n".join(lines)), message_group=group_id)
284
+
285
+ return ListAgentsOutput(agents=agents)
286
+
287
+ except Exception as e:
288
+ error_msg = f"Error listing agents: {str(e)}"
289
+ emit_error(error_msg, message_group=group_id)
290
+ return ListAgentsOutput(agents=[], error=error_msg)
291
+
292
+ return list_agents
293
+
294
+
295
+ def register_invoke_agent(agent):
296
+ """Register the invoke_agent tool with the provided agent.
297
+
298
+ Args:
299
+ agent: The agent to register the tool with
300
+ """
301
+
302
+ @agent.tool
303
+ async def invoke_agent(
304
+ context: RunContext, agent_name: str, prompt: str, session_id: str | None = None
305
+ ) -> AgentInvokeOutput:
306
+ """Invoke a specific sub-agent with a given prompt.
307
+
308
+ Args:
309
+ agent_name: The name of the agent to invoke
310
+ prompt: The prompt to send to the agent
311
+ session_id: Optional session ID for maintaining conversation memory across invocations.
312
+ Must be kebab-case. Hash suffix auto-appended for new sessions.
313
+ To continue a session, use the full session_id from the previous response.
314
+
315
+ Returns:
316
+ AgentInvokeOutput: Contains response, agent_name, session_id, and error fields.
317
+ """
318
+ from code_puppy.agents.agent_manager import load_agent
319
+
320
+ # Validate user-provided session_id if given
321
+ if session_id is not None:
322
+ try:
323
+ _validate_session_id(session_id)
324
+ except ValueError as e:
325
+ # Return error immediately if session_id is invalid
326
+ group_id = generate_group_id("invoke_agent", agent_name)
327
+ emit_error(str(e), message_group=group_id)
328
+ return AgentInvokeOutput(
329
+ response=None, agent_name=agent_name, error=str(e)
330
+ )
331
+
332
+ # Generate a group ID for this tool execution
333
+ group_id = generate_group_id("invoke_agent", agent_name)
334
+
335
+ # Check if this is an existing session or a new one
336
+ # For user-provided session_id, check if it exists
337
+ # For None, we'll generate a new one below
338
+ if session_id is not None:
339
+ message_history = _load_session_history(session_id)
340
+ is_new_session = len(message_history) == 0
341
+ else:
342
+ message_history = []
343
+ is_new_session = True
344
+
345
+ # Generate or finalize session_id
346
+ if session_id is None:
347
+ # Auto-generate a session ID with hash suffix for uniqueness
348
+ # Example: "qa-expert-session-a3f2b1"
349
+ hash_suffix = _generate_session_hash_suffix()
350
+ session_id = f"{agent_name}-session-{hash_suffix}"
351
+ elif is_new_session:
352
+ # User provided a base name for a NEW session - append hash suffix
353
+ # Example: "review-auth" -> "review-auth-a3f2b1"
354
+ hash_suffix = _generate_session_hash_suffix()
355
+ session_id = f"{session_id}-{hash_suffix}"
356
+ # else: continuing existing session, use session_id as-is
357
+
358
+ # Lazy imports to avoid circular dependency
359
+ from code_puppy.agents.subagent_stream_handler import subagent_stream_handler
360
+
361
+ # Emit structured invocation message via MessageBus
362
+ bus = get_message_bus()
363
+ bus.emit(
364
+ SubAgentInvocationMessage(
365
+ agent_name=agent_name,
366
+ session_id=session_id,
367
+ prompt=prompt,
368
+ is_new_session=is_new_session,
369
+ message_count=len(message_history),
370
+ )
371
+ )
372
+
373
+ # Save current session context and set the new one for this sub-agent
374
+ previous_session_id = get_session_context()
375
+ set_session_context(session_id)
376
+
377
+ # Set terminal session for browser-based terminal tools
378
+ # This uses contextvars which properly propagate through async tasks
379
+ from code_puppy.tools.browser.terminal_tools import (
380
+ _terminal_session_var,
381
+ set_terminal_session,
382
+ )
383
+
384
+ terminal_session_token = set_terminal_session(f"terminal-{session_id}")
385
+
386
+ # Set browser session for browser tools (qa-kitten, etc.)
387
+ # This allows parallel agent invocations to each have their own browser
388
+ from code_puppy.tools.browser.browser_manager import (
389
+ set_browser_session,
390
+ )
391
+
392
+ browser_session_token = set_browser_session(f"browser-{session_id}")
393
+
394
+ try:
395
+ # Lazy import to break circular dependency with messaging module
396
+ from code_puppy.model_factory import ModelFactory, make_model_settings
397
+
398
+ # Load the specified agent config
399
+ agent_config = load_agent(agent_name)
400
+
401
+ # Get the current model for creating a temporary agent
402
+ model_name = agent_config.get_model_name()
403
+ models_config = ModelFactory.load_config()
404
+
405
+ # Only proceed if we have a valid model configuration
406
+ if model_name not in models_config:
407
+ raise ValueError(f"Model '{model_name}' not found in configuration")
408
+
409
+ model = ModelFactory.get_model(model_name, models_config)
410
+
411
+ # Create a temporary agent instance to avoid interfering with current agent state
412
+ instructions = agent_config.get_full_system_prompt()
413
+
414
+ # Add AGENTS.md content to subagents
415
+ puppy_rules = agent_config.load_puppy_rules()
416
+ if puppy_rules:
417
+ instructions += f"\n\n{puppy_rules}"
418
+
419
+ # Apply prompt additions (like file permission handling) to temporary agents
420
+ from code_puppy import callbacks
421
+ from code_puppy.model_utils import prepare_prompt_for_model
422
+
423
+ prompt_additions = callbacks.on_load_prompt()
424
+ if len(prompt_additions):
425
+ instructions += "\n" + "\n".join(prompt_additions)
426
+
427
+ # Handle claude-code models: swap instructions, and prepend system prompt only on first message
428
+ prepared = prepare_prompt_for_model(
429
+ model_name,
430
+ instructions,
431
+ prompt,
432
+ prepend_system_to_user=is_new_session, # Only prepend on first message
433
+ )
434
+ instructions = prepared.instructions
435
+ prompt = prepared.user_prompt
436
+
437
+ import uuid as _uuid
438
+
439
+ subagent_name = f"temp-invoke-agent-{session_id}-{_uuid.uuid4().hex[:8]}"
440
+ model_settings = make_model_settings(model_name)
441
+
442
+ # Get MCP servers for sub-agents (same as main agent)
443
+ from code_puppy.mcp_ import get_mcp_manager
444
+
445
+ mcp_servers = []
446
+ mcp_disabled = get_value("disable_mcp_servers")
447
+ if not (
448
+ mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on")
449
+ ):
450
+ manager = get_mcp_manager()
451
+ mcp_servers = manager.get_servers_for_agent()
452
+
453
+ if get_use_dbos():
454
+ from pydantic_ai.durable_exec.dbos import DBOSAgent
455
+
456
+ # For DBOS, create agent without MCP servers (to avoid serialization issues)
457
+ # and add them at runtime
458
+ temp_agent = Agent(
459
+ model=model,
460
+ instructions=instructions,
461
+ output_type=str,
462
+ retries=10,
463
+ toolsets=[], # MCP servers added separately for DBOS
464
+ history_processors=[agent_config.message_history_accumulator],
465
+ model_settings=model_settings,
466
+ )
467
+
468
+ # Register the tools that the agent needs
469
+ from code_puppy.tools import register_tools_for_agent
470
+
471
+ agent_tools = agent_config.get_available_tools()
472
+ register_tools_for_agent(temp_agent, agent_tools, model_name=model_name)
473
+
474
+ # Wrap with DBOS - no streaming for sub-agents
475
+ dbos_agent = DBOSAgent(
476
+ temp_agent,
477
+ name=subagent_name,
478
+ )
479
+ temp_agent = dbos_agent
480
+
481
+ # Store MCP servers to add at runtime
482
+ subagent_mcp_servers = mcp_servers
483
+ else:
484
+ # Non-DBOS path - include MCP servers directly in the agent
485
+ temp_agent = Agent(
486
+ model=model,
487
+ instructions=instructions,
488
+ output_type=str,
489
+ retries=10,
490
+ toolsets=mcp_servers,
491
+ history_processors=[agent_config.message_history_accumulator],
492
+ model_settings=model_settings,
493
+ )
494
+
495
+ # Register the tools that the agent needs
496
+ from code_puppy.tools import register_tools_for_agent
497
+
498
+ agent_tools = agent_config.get_available_tools()
499
+ register_tools_for_agent(temp_agent, agent_tools, model_name=model_name)
500
+
501
+ subagent_mcp_servers = None
502
+
503
+ # Run the temporary agent with the provided prompt as an asyncio task
504
+ # Pass the message_history from the session to continue the conversation
505
+ workflow_id = None # Track for potential cancellation
506
+
507
+ # Always use subagent_stream_handler to silence output and update console manager
508
+ # This ensures all sub-agent output goes through the aggregated dashboard
509
+ stream_handler = partial(subagent_stream_handler, session_id=session_id)
510
+
511
+ # Wrap the agent run in subagent context for tracking
512
+ with subagent_context(agent_name):
513
+ if get_use_dbos():
514
+ # Generate a unique workflow ID for DBOS - ensures no collisions in back-to-back calls
515
+ workflow_id = _generate_dbos_workflow_id(group_id)
516
+
517
+ # Add MCP servers to the DBOS agent's toolsets
518
+ # (temp_agent is discarded after this invocation, so no need to restore)
519
+ if subagent_mcp_servers:
520
+ temp_agent._toolsets = (
521
+ temp_agent._toolsets + subagent_mcp_servers
522
+ )
523
+
524
+ with SetWorkflowID(workflow_id):
525
+ task = asyncio.create_task(
526
+ temp_agent.run(
527
+ prompt,
528
+ message_history=message_history,
529
+ usage_limits=UsageLimits(
530
+ request_limit=get_message_limit()
531
+ ),
532
+ event_stream_handler=stream_handler,
533
+ )
534
+ )
535
+ _active_subagent_tasks.add(task)
536
+ else:
537
+ task = asyncio.create_task(
538
+ temp_agent.run(
539
+ prompt,
540
+ message_history=message_history,
541
+ usage_limits=UsageLimits(request_limit=get_message_limit()),
542
+ event_stream_handler=stream_handler,
543
+ )
544
+ )
545
+ _active_subagent_tasks.add(task)
546
+
547
+ try:
548
+ result = await task
549
+ finally:
550
+ _active_subagent_tasks.discard(task)
551
+ if task.cancelled():
552
+ if get_use_dbos() and workflow_id:
553
+ await DBOS.cancel_workflow_async(workflow_id)
554
+
555
+ # Extract the response from the result
556
+ response = result.output
557
+
558
+ # Update the session history with the new messages from this interaction
559
+ # The result contains all_messages which includes the full conversation
560
+ updated_history = result.all_messages()
561
+
562
+ # Save to filesystem (include initial prompt only for new sessions)
563
+ _save_session_history(
564
+ session_id=session_id,
565
+ message_history=updated_history,
566
+ agent_name=agent_name,
567
+ initial_prompt=prompt if is_new_session else None,
568
+ )
569
+
570
+ # Emit structured response message via MessageBus
571
+ bus.emit(
572
+ SubAgentResponseMessage(
573
+ agent_name=agent_name,
574
+ session_id=session_id,
575
+ response=response,
576
+ message_count=len(updated_history),
577
+ )
578
+ )
579
+
580
+ # Emit clean completion summary
581
+ emit_success(
582
+ f"✓ {agent_name} completed successfully", message_group=group_id
583
+ )
584
+
585
+ return AgentInvokeOutput(
586
+ response=response, agent_name=agent_name, session_id=session_id
587
+ )
588
+
589
+ except Exception as e:
590
+ # Emit clean failure summary
591
+ emit_error(f"✗ {agent_name} failed: {str(e)}", message_group=group_id)
592
+
593
+ # Full traceback for debugging
594
+ error_msg = f"Error invoking agent '{agent_name}': {traceback.format_exc()}"
595
+ emit_error(error_msg, message_group=group_id)
596
+
597
+ return AgentInvokeOutput(
598
+ response=None,
599
+ agent_name=agent_name,
600
+ session_id=session_id,
601
+ error=error_msg,
602
+ )
603
+
604
+ finally:
605
+ # Restore the previous session context
606
+ set_session_context(previous_session_id)
607
+ # Reset terminal session context
608
+ _terminal_session_var.reset(terminal_session_token)
609
+ # Reset browser session context
610
+ from code_puppy.tools.browser.browser_manager import (
611
+ _browser_session_var,
612
+ )
613
+
614
+ _browser_session_var.reset(browser_session_token)
615
+
616
+ return invoke_agent
@@ -0,0 +1,26 @@
1
+ """Ask User Question tool for code-puppy.
2
+
3
+ This tool allows agents to ask users interactive multiple-choice questions
4
+ through a terminal TUI interface. Uses prompt_toolkit for the split-panel
5
+ UI similar to the /colors command.
6
+ """
7
+
8
+ from .handler import ask_user_question
9
+ from .models import (
10
+ AskUserQuestionInput,
11
+ AskUserQuestionOutput,
12
+ Question,
13
+ QuestionAnswer,
14
+ QuestionOption,
15
+ )
16
+ from .registration import register_ask_user_question
17
+
18
+ __all__ = [
19
+ "ask_user_question",
20
+ "register_ask_user_question",
21
+ "AskUserQuestionInput",
22
+ "AskUserQuestionOutput",
23
+ "Question",
24
+ "QuestionAnswer",
25
+ "QuestionOption",
26
+ ]