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,257 @@
1
+ import asyncio
2
+ import threading
3
+ import time
4
+
5
+ from rich.console import Console
6
+ from rich.live import Live
7
+ from rich.panel import Panel
8
+ from rich.spinner import Spinner
9
+ from rich.text import Text
10
+
11
+ # Global variable to track current token per second rate
12
+ CURRENT_TOKEN_RATE = 0.0
13
+ _TOKEN_RATE_LOCK = threading.Lock()
14
+
15
+
16
+ class StatusDisplay:
17
+ """
18
+ Displays real-time status information during model execution,
19
+ including token per second rate and rotating loading messages.
20
+ """
21
+
22
+ def __init__(self, console: Console):
23
+ self.console = console
24
+ self.token_count = 0
25
+ self.start_time = None
26
+ self.last_update_time = None
27
+ self.last_token_count = 0
28
+ self.current_rate = 0
29
+ self.is_active = False
30
+ self.task = None
31
+ self.live = None
32
+ self.loading_messages = [
33
+ "Fetching...",
34
+ "Sniffing around...",
35
+ "Wagging tail...",
36
+ "Pawsing for a moment...",
37
+ "Chasing tail...",
38
+ "Digging up results...",
39
+ "Barking at the data...",
40
+ "Rolling over...",
41
+ "Panting with excitement...",
42
+ "Chewing on it...",
43
+ "Prancing along...",
44
+ "Howling at the code...",
45
+ "Snuggling up to the task...",
46
+ "Bounding through data...",
47
+ "Puppy pondering...",
48
+ ]
49
+ self.current_message_index = 0
50
+ self.spinner = Spinner("dots", text="")
51
+
52
+ def _calculate_rate(self) -> float:
53
+ """Calculate the current token rate"""
54
+ current_time = time.time()
55
+ if self.last_update_time:
56
+ time_diff = current_time - self.last_update_time
57
+ token_diff = self.token_count - self.last_token_count
58
+ if time_diff > 0:
59
+ rate = token_diff / time_diff
60
+ # Smooth the rate calculation with the current rate
61
+ if self.current_rate > 0:
62
+ self.current_rate = (self.current_rate * 0.7) + (rate * 0.3)
63
+ else:
64
+ self.current_rate = rate
65
+
66
+ # Only ensure rate is not negative
67
+ self.current_rate = max(0, self.current_rate)
68
+
69
+ # Update the global rate for other components to access
70
+ global CURRENT_TOKEN_RATE
71
+ with _TOKEN_RATE_LOCK:
72
+ CURRENT_TOKEN_RATE = self.current_rate
73
+
74
+ self.last_update_time = current_time
75
+ self.last_token_count = self.token_count
76
+ return self.current_rate
77
+
78
+ def update_rate_from_sse(
79
+ self, completion_tokens: int, completion_time: float
80
+ ) -> None:
81
+ """Update the token rate directly using SSE time_info data
82
+
83
+ Args:
84
+ completion_tokens: Number of tokens in the completion (from SSE stream)
85
+ completion_time: Time taken for completion in seconds (from SSE stream)
86
+ """
87
+ if completion_time > 0:
88
+ # Using the direct t/s formula: tokens / time
89
+ rate = completion_tokens / completion_time
90
+
91
+ # Use a lighter smoothing for this more accurate data
92
+ if self.current_rate > 0:
93
+ self.current_rate = (self.current_rate * 0.3) + (
94
+ rate * 0.7
95
+ ) # Weight SSE data more heavily
96
+ else:
97
+ self.current_rate = rate
98
+
99
+ # Update the global rate
100
+ global CURRENT_TOKEN_RATE
101
+ with _TOKEN_RATE_LOCK:
102
+ CURRENT_TOKEN_RATE = self.current_rate
103
+
104
+ @staticmethod
105
+ def get_current_rate() -> float:
106
+ """Get the current token rate for use in other components"""
107
+ global CURRENT_TOKEN_RATE
108
+ with _TOKEN_RATE_LOCK:
109
+ return CURRENT_TOKEN_RATE
110
+
111
+ def update_token_count(self, tokens: int) -> None:
112
+ """Update the token count and recalculate the rate"""
113
+ # Reset timing if this is the first update of a new task
114
+ if self.start_time is None:
115
+ self.start_time = time.time()
116
+ self.last_update_time = self.start_time
117
+ # Reset token counters for new task
118
+ self.last_token_count = 0
119
+ self.current_rate = 0.0
120
+ # Set initial token count
121
+ self.token_count = tokens if tokens >= 0 else 0
122
+ return # Don't calculate rate on first initialization
123
+
124
+ # Allow for incremental updates (common for streaming) or absolute updates
125
+ if tokens > self.token_count or tokens < 0:
126
+ # Incremental update or reset
127
+ self.token_count = tokens if tokens >= 0 else 0
128
+ else:
129
+ # If tokens <= current count but > 0, treat as incremental
130
+ # This handles simulated token streaming
131
+ self.token_count += tokens
132
+
133
+ self._calculate_rate()
134
+
135
+ def _get_status_panel(self) -> Panel:
136
+ """Generate a status panel with current rate and animated message"""
137
+ rate_text = (
138
+ f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
139
+ )
140
+
141
+ # Update spinner
142
+ self.spinner.update()
143
+
144
+ # Rotate through loading messages every few updates
145
+ if int(time.time() * 2) % 4 == 0:
146
+ self.current_message_index = (self.current_message_index + 1) % len(
147
+ self.loading_messages
148
+ )
149
+
150
+ # Create a highly visible status message
151
+ status_text = Text.assemble(
152
+ Text(f"⏳ {rate_text} ", style="bold cyan"),
153
+ str(self.spinner),
154
+ Text(
155
+ f" {self.loading_messages[self.current_message_index]} ⏳",
156
+ style="bold yellow",
157
+ ),
158
+ )
159
+
160
+ # Use expanded panel with more visible formatting
161
+ return Panel(
162
+ status_text,
163
+ title="[bold blue]Agent Status[/bold blue]",
164
+ border_style="bright_blue",
165
+ expand=False,
166
+ padding=(1, 2),
167
+ )
168
+
169
+ def _get_status_text(self) -> Text:
170
+ """Generate a status text with current rate and animated message"""
171
+ rate_text = (
172
+ f"{self.current_rate:.1f} t/s" if self.current_rate > 0 else "Warming up..."
173
+ )
174
+
175
+ # Update spinner
176
+ self.spinner.update()
177
+
178
+ # Rotate through loading messages
179
+ self.current_message_index = (self.current_message_index + 1) % len(
180
+ self.loading_messages
181
+ )
182
+ message = self.loading_messages[self.current_message_index]
183
+
184
+ # Create a highly visible status text
185
+ return Text.assemble(
186
+ Text(f"⏳ {rate_text}", style="bold cyan"),
187
+ Text(f" {message}", style="yellow"),
188
+ )
189
+
190
+ async def _update_display(self) -> None:
191
+ """Update the display continuously while active using Rich Live display"""
192
+ # Lazy import to avoid circular dependency during module initialization
193
+ from code_puppy.messaging import emit_info
194
+
195
+ # Add a newline to ensure we're below the blue bar
196
+ emit_info("")
197
+
198
+ # Create a Live display that will update in-place
199
+ with Live(
200
+ self._get_status_text(),
201
+ console=self.console,
202
+ refresh_per_second=2, # Update twice per second
203
+ transient=False, # Keep the final state visible
204
+ ) as live:
205
+ # Keep updating the live display while active
206
+ while self.is_active:
207
+ live.update(self._get_status_text())
208
+ await asyncio.sleep(0.5)
209
+
210
+ def start(self) -> None:
211
+ """Start the status display"""
212
+ if not self.is_active:
213
+ self.is_active = True
214
+ self.start_time = time.time()
215
+ self.last_update_time = self.start_time
216
+ self.token_count = 0
217
+ self.last_token_count = 0
218
+ self.current_rate = 0
219
+ self.task = asyncio.create_task(self._update_display())
220
+
221
+ def stop(self) -> None:
222
+ """Stop the status display"""
223
+ # Lazy import to avoid circular dependency during module initialization
224
+ from code_puppy.messaging import emit_info
225
+
226
+ if self.is_active:
227
+ self.is_active = False
228
+ if self.task:
229
+ self.task.cancel()
230
+ self.task = None
231
+
232
+ # Print final stats
233
+ elapsed = time.time() - self.start_time if self.start_time else 0
234
+ avg_rate = self.token_count / elapsed if elapsed > 0 else 0
235
+ emit_info(
236
+ f"Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)"
237
+ )
238
+
239
+ # Reset state
240
+ self.start_time = None
241
+ self.token_count = 0
242
+ self.last_update_time = None
243
+ self.last_token_count = 0
244
+ self.current_rate = 0
245
+
246
+ # Reset global rate to 0 to avoid affecting subsequent tasks
247
+ global CURRENT_TOKEN_RATE
248
+ with _TOKEN_RATE_LOCK:
249
+ CURRENT_TOKEN_RATE = 0.0
250
+ else:
251
+ # Even if not active, ensure we print stats when stop is called
252
+ # This is for testing purposes
253
+ elapsed = time.time() - self.start_time if self.start_time else 0
254
+ avg_rate = self.token_count / elapsed if elapsed > 0 else 0
255
+ emit_info(
256
+ f"Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)"
257
+ )
@@ -0,0 +1,176 @@
1
+ import asyncio
2
+ import atexit
3
+ import threading
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import List
6
+
7
+ from pydantic_ai import Agent
8
+
9
+ from code_puppy.config import (
10
+ get_global_model_name,
11
+ )
12
+ from code_puppy.model_factory import ModelFactory, make_model_settings
13
+
14
+ # Keep a module-level agent reference to avoid rebuilding per call
15
+ _summarization_agent = None
16
+ _agent_lock = threading.Lock()
17
+
18
+ # Safe sync runner for async agent.run calls
19
+ # Avoids "event loop is already running" by offloading to a separate thread loop when needed
20
+ _thread_pool: ThreadPoolExecutor | None = None
21
+
22
+ # Reload counter
23
+ _reload_count = 0
24
+
25
+
26
+ def _ensure_thread_pool():
27
+ global _thread_pool
28
+ # Check if pool is None OR if it's been shutdown
29
+ if _thread_pool is None or _thread_pool._shutdown:
30
+ _thread_pool = ThreadPoolExecutor(
31
+ max_workers=1, thread_name_prefix="summarizer-loop"
32
+ )
33
+ return _thread_pool
34
+
35
+
36
+ def _shutdown_thread_pool():
37
+ global _thread_pool
38
+ if _thread_pool is not None:
39
+ _thread_pool.shutdown(wait=False)
40
+ _thread_pool = None
41
+
42
+
43
+ atexit.register(_shutdown_thread_pool)
44
+
45
+
46
+ async def _run_agent_async(agent: Agent, prompt: str, message_history: List):
47
+ return await agent.run(prompt, message_history=message_history)
48
+
49
+
50
+ class SummarizationError(Exception):
51
+ """Raised when summarization fails with details about the failure."""
52
+
53
+ def __init__(self, message: str, original_error: Exception | None = None):
54
+ self.original_error = original_error
55
+ super().__init__(message)
56
+
57
+
58
+ def run_summarization_sync(prompt: str, message_history: List) -> List:
59
+ """Run the summarization agent synchronously.
60
+
61
+ Raises:
62
+ SummarizationError: If summarization fails for any reason.
63
+ """
64
+ try:
65
+ agent = get_summarization_agent()
66
+ except Exception as e:
67
+ raise SummarizationError(
68
+ f"Failed to initialize summarization agent: {type(e).__name__}: {e}",
69
+ original_error=e,
70
+ ) from e
71
+
72
+ # Handle claude-code models: prepend system prompt to user prompt
73
+ from code_puppy.model_utils import prepare_prompt_for_model
74
+
75
+ model_name = get_global_model_name()
76
+ prepared = prepare_prompt_for_model(
77
+ model_name, _get_summarization_instructions(), prompt
78
+ )
79
+ prompt = prepared.user_prompt
80
+
81
+ def _run_in_thread():
82
+ """
83
+ Run the async agent in a dedicated thread with its own event loop.
84
+ Uses run_until_complete instead of asyncio.run to avoid shutting down
85
+ the default executor (which breaks DBOS in the main thread).
86
+ Does NOT touch global event loop state.
87
+ """
88
+ loop = asyncio.new_event_loop()
89
+ try:
90
+ coro = agent.run(prompt, message_history=message_history)
91
+ return loop.run_until_complete(coro)
92
+ finally:
93
+ # Clean up without shutting down the default executor
94
+ try:
95
+ # Cancel pending tasks
96
+ pending = asyncio.all_tasks(loop)
97
+ for task in pending:
98
+ task.cancel()
99
+ if pending:
100
+ loop.run_until_complete(
101
+ asyncio.gather(*pending, return_exceptions=True)
102
+ )
103
+ loop.run_until_complete(loop.shutdown_asyncgens())
104
+ finally:
105
+ loop.close()
106
+
107
+ try:
108
+ # Always use thread pool since we're likely in an existing event loop
109
+ pool = _ensure_thread_pool()
110
+ result = pool.submit(_run_in_thread).result()
111
+ return result.new_messages()
112
+ except Exception as e:
113
+ error_type = type(e).__name__
114
+ error_msg = str(e) if str(e) else "(no details available)"
115
+ raise SummarizationError(
116
+ f"LLM call failed during summarization: [{error_type}] {error_msg}",
117
+ original_error=e,
118
+ ) from e
119
+
120
+
121
+ def _get_summarization_instructions() -> str:
122
+ """Get the system instructions for the summarization agent."""
123
+ return """You are a message summarization expert. Your task is to summarize conversation messages
124
+ while preserving important context and information. The summaries should be concise but capture the essential content
125
+ and intent of the original messages. This is to help manage token usage in a conversation history
126
+ while maintaining context for the AI to continue the conversation effectively.
127
+
128
+ When summarizing:
129
+ 1. Keep summary concise but informative
130
+ 2. Preserve important context and key information and decisions
131
+ 3. Keep any important technical details
132
+ 4. Don't summarize the system message
133
+ 5. Make sure all tool calls and responses are summarized, as they are vital
134
+ 6. Focus on token usage efficiency and system message preservation"""
135
+
136
+
137
+ def reload_summarization_agent():
138
+ """Create a specialized agent for summarizing messages when context limit is reached."""
139
+ from code_puppy.model_utils import prepare_prompt_for_model
140
+
141
+ models_config = ModelFactory.load_config()
142
+ model_name = get_global_model_name()
143
+ model = ModelFactory.get_model(model_name, models_config)
144
+
145
+ # Handle claude-code models: swap instructions (prompt prepending happens in run_summarization_sync)
146
+ instructions = _get_summarization_instructions()
147
+ prepared = prepare_prompt_for_model(
148
+ model_name, instructions, "", prepend_system_to_user=False
149
+ )
150
+ instructions = prepared.instructions
151
+
152
+ model_settings = make_model_settings(model_name)
153
+
154
+ agent = Agent(
155
+ model=model,
156
+ instructions=instructions,
157
+ output_type=str,
158
+ retries=1, # Fewer retries for summarization
159
+ model_settings=model_settings,
160
+ )
161
+ # NOTE: We intentionally DON'T wrap in DBOSAgent here.
162
+ # Summarization is a simple one-shot call that doesn't need durable execution,
163
+ # and DBOSAgent causes async event loop conflicts with run_sync().
164
+ return agent
165
+
166
+
167
+ def get_summarization_agent(force_reload=True):
168
+ """
169
+ Retrieve the summarization agent with the currently set MODEL_NAME.
170
+ Forces a reload if the model has changed, or if force_reload is passed.
171
+ """
172
+ global _summarization_agent
173
+ with _agent_lock:
174
+ if force_reload or _summarization_agent is None:
175
+ _summarization_agent = reload_summarization_agent()
176
+ return _summarization_agent