codepp 0.0.437__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 (288) 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 +117 -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 +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -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 +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -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 +2156 -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 +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -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 +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -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 +532 -0
  57. code_puppy/command_line/command_handler.py +293 -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 +867 -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 +96 -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/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,356 @@
1
+ """Monkey patches for pydantic-ai.
2
+
3
+ This module contains all monkey patches needed to customize pydantic-ai behavior.
4
+ These patches MUST be applied before any other pydantic-ai imports to work correctly.
5
+
6
+ Usage:
7
+ from code_puppy.pydantic_patches import apply_all_patches
8
+ apply_all_patches()
9
+ """
10
+
11
+ import importlib.metadata
12
+ from typing import Any
13
+
14
+
15
+ def _get_code_puppy_version() -> str:
16
+ """Get the current code-puppy version."""
17
+ try:
18
+ return importlib.metadata.version("code-puppy")
19
+ except Exception:
20
+ return "0.0.0-dev"
21
+
22
+
23
+ def patch_user_agent() -> None:
24
+ """Patch pydantic-ai's User-Agent to use Code-Puppy's version.
25
+
26
+ pydantic-ai sets its own User-Agent ('pydantic-ai/x.x.x') via a @cache-decorated
27
+ function. We replace it with a dynamic function that returns:
28
+ - 'KimiCLI/0.63' for Kimi models
29
+ - 'Code-Puppy/{version}' for all other models
30
+
31
+ This MUST be called before any pydantic-ai models are created.
32
+ """
33
+ try:
34
+ import pydantic_ai.models as pydantic_models
35
+
36
+ version = _get_code_puppy_version()
37
+
38
+ # Clear cache if already called
39
+ if hasattr(pydantic_models.get_user_agent, "cache_clear"):
40
+ pydantic_models.get_user_agent.cache_clear()
41
+
42
+ def _get_dynamic_user_agent() -> str:
43
+ """Return User-Agent based on current model selection."""
44
+ try:
45
+ from code_puppy.config import get_global_model_name
46
+
47
+ model_name = get_global_model_name()
48
+ if model_name and "kimi" in model_name.lower():
49
+ return "KimiCLI/0.63"
50
+ except Exception:
51
+ pass
52
+ return f"Code-Puppy/{version}"
53
+
54
+ pydantic_models.get_user_agent = _get_dynamic_user_agent
55
+ except Exception:
56
+ pass # Don't crash on patch failure
57
+
58
+
59
+ def patch_message_history_cleaning() -> None:
60
+ """Disable overly strict message history cleaning in pydantic-ai."""
61
+ try:
62
+ from pydantic_ai import _agent_graph
63
+
64
+ _agent_graph._clean_message_history = lambda messages: messages
65
+ except Exception:
66
+ pass
67
+
68
+
69
+ def patch_process_message_history() -> None:
70
+ """Patch _process_message_history to skip strict ModelRequest validation.
71
+
72
+ Pydantic AI added a validation that history must end with ModelRequest,
73
+ but this breaks valid conversation flows. We patch it to skip that validation.
74
+ """
75
+ try:
76
+ from pydantic_ai import _agent_graph
77
+
78
+ async def _patched_process_message_history(messages, processors, run_context):
79
+ """Patched version that doesn't enforce ModelRequest at end."""
80
+ from pydantic_ai._agent_graph import (
81
+ _HistoryProcessorAsync,
82
+ _HistoryProcessorSync,
83
+ _HistoryProcessorSyncWithCtx,
84
+ cast,
85
+ exceptions,
86
+ is_async_callable,
87
+ is_takes_ctx,
88
+ run_in_executor,
89
+ )
90
+
91
+ for processor in processors:
92
+ takes_ctx = is_takes_ctx(processor)
93
+
94
+ if is_async_callable(processor):
95
+ if takes_ctx:
96
+ messages = await processor(run_context, messages)
97
+ else:
98
+ async_processor = cast(_HistoryProcessorAsync, processor)
99
+ messages = await async_processor(messages)
100
+ else:
101
+ if takes_ctx:
102
+ sync_processor_with_ctx = cast(
103
+ _HistoryProcessorSyncWithCtx, processor
104
+ )
105
+ messages = await run_in_executor(
106
+ sync_processor_with_ctx, run_context, messages
107
+ )
108
+ else:
109
+ sync_processor = cast(_HistoryProcessorSync, processor)
110
+ messages = await run_in_executor(sync_processor, messages)
111
+
112
+ if len(messages) == 0:
113
+ raise exceptions.UserError("Processed history cannot be empty.")
114
+
115
+ # NOTE: We intentionally skip the "must end with ModelRequest" validation
116
+ # that was added in newer Pydantic AI versions.
117
+
118
+ return messages
119
+
120
+ _agent_graph._process_message_history = _patched_process_message_history
121
+ except Exception:
122
+ pass
123
+
124
+
125
+ def patch_tool_call_json_repair() -> None:
126
+ """Patch pydantic-ai's _call_tool to auto-repair malformed JSON arguments.
127
+
128
+ LLMs sometimes produce slightly broken JSON in tool calls (trailing commas,
129
+ missing quotes, etc.). This patch intercepts tool calls and runs json_repair
130
+ on the arguments before validation, preventing unnecessary retries.
131
+ """
132
+ try:
133
+ import json_repair
134
+ from pydantic_ai._tool_manager import ToolManager
135
+
136
+ # Store the original method
137
+ _original_call_tool = ToolManager._call_tool
138
+
139
+ async def _patched_call_tool(
140
+ self,
141
+ call,
142
+ *,
143
+ allow_partial: bool,
144
+ wrap_validation_errors: bool,
145
+ approved: bool,
146
+ metadata: Any = None,
147
+ ):
148
+ """Patched _call_tool that repairs malformed JSON before validation."""
149
+ # Only attempt repair if args is a string (JSON)
150
+ if isinstance(call.args, str) and call.args:
151
+ try:
152
+ repaired = json_repair.repair_json(call.args)
153
+ if repaired != call.args:
154
+ # Update the call args with repaired JSON
155
+ call.args = repaired
156
+ except Exception:
157
+ pass # If repair fails, let original validation handle it
158
+
159
+ # Call the original method
160
+ return await _original_call_tool(
161
+ self,
162
+ call,
163
+ allow_partial=allow_partial,
164
+ wrap_validation_errors=wrap_validation_errors,
165
+ approved=approved,
166
+ metadata=metadata,
167
+ )
168
+
169
+ # Apply the patch
170
+ ToolManager._call_tool = _patched_call_tool
171
+
172
+ except ImportError:
173
+ pass # json_repair or pydantic_ai not available
174
+ except Exception:
175
+ pass # Don't crash on patch failure
176
+
177
+
178
+ def patch_tool_call_callbacks() -> None:
179
+ """Patch pydantic-ai tool handling to support callbacks and Claude Code tool names.
180
+
181
+ Claude Code OAuth prefixes tool names with ``cp_`` on the wire. pydantic-ai
182
+ classifies tool calls *before* ``_call_tool`` runs, so unprefixing only in
183
+ ``_call_tool`` is too late: prefixed tools get marked as ``unknown`` and can
184
+ burn through result retries, eventually raising ``UnexpectedModelBehavior``.
185
+
186
+ This patch normalizes Claude Code tool names early (during lookup/dispatch)
187
+ and wraps ``_call_tool`` so every tool invocation also triggers the
188
+ ``pre_tool_call`` and ``post_tool_call`` callbacks defined in
189
+ ``code_puppy.callbacks``.
190
+ """
191
+ import time
192
+
193
+ try:
194
+ from pydantic_ai._tool_manager import ToolManager
195
+
196
+ _original_call_tool = ToolManager._call_tool
197
+ _original_get_tool_def = ToolManager.get_tool_def
198
+ _original_handle_call = ToolManager.handle_call
199
+
200
+ # Tool name prefix used by Claude Code OAuth - tools are prefixed on
201
+ # outgoing requests, so we need to unprefix them when they come back.
202
+ TOOL_PREFIX = "cp_"
203
+
204
+ def _normalize_tool_name(name: Any) -> Any:
205
+ """Strip the ``cp_`` prefix if present."""
206
+ if isinstance(name, str) and name.startswith(TOOL_PREFIX):
207
+ return name[len(TOOL_PREFIX) :]
208
+ return name
209
+
210
+ def _normalize_call_tool_name(call: Any) -> tuple[Any, Any]:
211
+ """Normalize the tool_name on a call object in-place."""
212
+ tool_name = getattr(call, "tool_name", None)
213
+ normalized_name = _normalize_tool_name(tool_name)
214
+ if normalized_name != tool_name:
215
+ try:
216
+ call.tool_name = normalized_name
217
+ except (AttributeError, TypeError):
218
+ pass
219
+ return normalized_name, call
220
+
221
+ # -- Early normalization patches -----------------------------------------
222
+ # These run *before* pydantic-ai classifies the tool as function/output/
223
+ # unknown, so prefixed names resolve correctly.
224
+
225
+ def _patched_get_tool_def(self, name: str):
226
+ return _original_get_tool_def(self, _normalize_tool_name(name))
227
+
228
+ async def _patched_handle_call(
229
+ self,
230
+ call,
231
+ allow_partial: bool = False,
232
+ wrap_validation_errors: bool = True,
233
+ *,
234
+ approved: bool = False,
235
+ metadata: Any = None,
236
+ ):
237
+ _normalize_call_tool_name(call)
238
+ return await _original_handle_call(
239
+ self,
240
+ call,
241
+ allow_partial=allow_partial,
242
+ wrap_validation_errors=wrap_validation_errors,
243
+ approved=approved,
244
+ metadata=metadata,
245
+ )
246
+
247
+ # -- _call_tool wrapper with callbacks -----------------------------------
248
+
249
+ async def _patched_call_tool(
250
+ self,
251
+ call,
252
+ *,
253
+ allow_partial: bool,
254
+ wrap_validation_errors: bool,
255
+ approved: bool,
256
+ metadata: Any = None,
257
+ ):
258
+ tool_name, call = _normalize_call_tool_name(call)
259
+
260
+ # Normalise args to a dict for the callback contract
261
+ tool_args: dict = {}
262
+ if isinstance(call.args, dict):
263
+ tool_args = call.args
264
+ elif isinstance(call.args, str):
265
+ try:
266
+ import json
267
+
268
+ tool_args = json.loads(call.args)
269
+ except Exception:
270
+ tool_args = {"raw": call.args}
271
+
272
+ # --- pre_tool_call (with blocking support) ---
273
+ # Returns a string tool-result on block so pydantic-ai sees a clean
274
+ # "BLOCKED: ..." message and the agent can react gracefully, without
275
+ # triggering UnexpectedModelBehavior crashes.
276
+ try:
277
+ from code_puppy import callbacks
278
+ from code_puppy.messaging import emit_warning
279
+
280
+ callback_results = await callbacks.on_pre_tool_call(
281
+ tool_name, tool_args
282
+ )
283
+
284
+ for callback_result in callback_results:
285
+ if (
286
+ callback_result
287
+ and isinstance(callback_result, dict)
288
+ and callback_result.get("blocked")
289
+ ):
290
+ raw_reason = (
291
+ callback_result.get("error_message")
292
+ or callback_result.get("reason")
293
+ or ""
294
+ )
295
+ if "[BLOCKED]" in raw_reason:
296
+ clean_reason = raw_reason[
297
+ raw_reason.index("[BLOCKED]") :
298
+ ].strip()
299
+ else:
300
+ clean_reason = (
301
+ raw_reason.strip() or "Tool execution blocked by hook"
302
+ )
303
+ block_msg = f"🚫 Hook blocked this tool call: {clean_reason}"
304
+ emit_warning(block_msg)
305
+ return f"ERROR: {block_msg}\n\nThe hook policy prevented this tool from running. Please inform the user and do not retry this specific command."
306
+ except Exception:
307
+ pass # other errors don't block tool execution
308
+
309
+ start = time.perf_counter()
310
+ error: Exception | None = None
311
+ result = None
312
+ try:
313
+ result = await _original_call_tool(
314
+ self,
315
+ call,
316
+ allow_partial=allow_partial,
317
+ wrap_validation_errors=wrap_validation_errors,
318
+ approved=approved,
319
+ metadata=metadata,
320
+ )
321
+ return result
322
+ except Exception as exc:
323
+ error = exc
324
+ raise
325
+ finally:
326
+ duration_ms = (time.perf_counter() - start) * 1000
327
+ final_result = result if error is None else {"error": str(error)}
328
+ try:
329
+ from code_puppy import callbacks
330
+
331
+ await callbacks.on_post_tool_call(
332
+ tool_name, tool_args, final_result, duration_ms
333
+ )
334
+ except Exception:
335
+ pass # never block tool execution
336
+
337
+ ToolManager.get_tool_def = _patched_get_tool_def
338
+ ToolManager.handle_call = _patched_handle_call
339
+ ToolManager._call_tool = _patched_call_tool
340
+
341
+ except ImportError:
342
+ pass
343
+ except Exception:
344
+ pass
345
+
346
+
347
+ def apply_all_patches() -> None:
348
+ """Apply all pydantic-ai monkey patches.
349
+
350
+ Call this at the very top of main.py, before any other imports.
351
+ """
352
+ patch_user_agent()
353
+ patch_message_history_cleaning()
354
+ patch_process_message_history()
355
+ patch_tool_call_json_repair()
356
+ patch_tool_call_callbacks()
@@ -0,0 +1,232 @@
1
+ """
2
+ ReopenableAsyncClient - A reopenable httpx.AsyncClient wrapper.
3
+
4
+ This module provides a ReopenableAsyncClient class that extends httpx.AsyncClient
5
+ to support reopening after being closed, which the standard httpx.AsyncClient
6
+ doesn't support.
7
+ """
8
+
9
+ import asyncio
10
+ import threading
11
+ from typing import Optional, Union
12
+
13
+ import httpx
14
+
15
+
16
+ class ReopenableAsyncClient:
17
+ """
18
+ A wrapper around httpx.AsyncClient that can be reopened after being closed.
19
+
20
+ Standard httpx.AsyncClient becomes unusable after calling aclose().
21
+ This class allows you to reopen the client and continue using it.
22
+
23
+ Example:
24
+ >>> client = ReopenableAsyncClient(timeout=30.0)
25
+ >>> await client.get("https://httpbin.org/get")
26
+ >>> await client.aclose()
27
+ >>> # Client is now closed, but can be reopened
28
+ >>> await client.reopen()
29
+ >>> await client.get("https://httpbin.org/get") # Works!
30
+
31
+ The client preserves all original configuration when reopening.
32
+ """
33
+
34
+ class _StreamWrapper:
35
+ """Async context manager wrapper for streaming responses."""
36
+
37
+ def __init__(
38
+ self,
39
+ parent_client: "ReopenableAsyncClient",
40
+ method: str,
41
+ url: Union[str, httpx.URL],
42
+ **kwargs,
43
+ ):
44
+ self.parent_client = parent_client
45
+ self.method = method
46
+ self.url = url
47
+ self.kwargs = kwargs
48
+ self._stream_context = None
49
+
50
+ async def __aenter__(self):
51
+ client = await self.parent_client._ensure_client_open()
52
+ self._stream_context = client.stream(self.method, self.url, **self.kwargs)
53
+ return await self._stream_context.__aenter__()
54
+
55
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
56
+ if self._stream_context:
57
+ return await self._stream_context.__aexit__(exc_type, exc_val, exc_tb)
58
+
59
+ def __init__(self, client_class=None, **kwargs):
60
+ """
61
+ Initialize the ReopenableAsyncClient.
62
+
63
+ Args:
64
+ client_class: Class to use for creating the internal client (defaults to httpx.AsyncClient)
65
+ **kwargs: All arguments that would be passed to the client constructor
66
+ """
67
+ self._client_class = client_class or httpx.AsyncClient
68
+ self._client_kwargs = kwargs.copy()
69
+ self._client: Optional[httpx.AsyncClient] = None
70
+ self._is_closed = True
71
+ self._lock = asyncio.Lock()
72
+ self._sync_lock = threading.Lock()
73
+
74
+ async def _ensure_client_open(self) -> httpx.AsyncClient:
75
+ """
76
+ Ensure the underlying client is open and ready to use.
77
+
78
+ Returns:
79
+ The active client instance
80
+
81
+ Raises:
82
+ RuntimeError: If client cannot be opened
83
+ """
84
+ async with self._lock:
85
+ if self._is_closed or self._client is None:
86
+ await self._create_client()
87
+ return self._client
88
+
89
+ async def _create_client(self) -> None:
90
+ """Create a new client with the stored configuration."""
91
+ if self._client is not None and not self._is_closed:
92
+ # Close existing client first
93
+ await self._client.aclose()
94
+
95
+ self._client = self._client_class(**self._client_kwargs)
96
+ self._is_closed = False
97
+
98
+ async def reopen(self) -> None:
99
+ """
100
+ Explicitly reopen the client after it has been closed.
101
+
102
+ This is useful when you want to reuse a client that was previously closed.
103
+ """
104
+ async with self._lock:
105
+ await self._create_client()
106
+
107
+ async def aclose(self) -> None:
108
+ """
109
+ Close the underlying httpx.AsyncClient.
110
+
111
+ After calling this, the client can be reopened using reopen() or
112
+ automatically when making the next request.
113
+ """
114
+ async with self._lock:
115
+ if self._client is not None and not self._is_closed:
116
+ await self._client.aclose()
117
+ self._is_closed = True
118
+
119
+ @property
120
+ def is_closed(self) -> bool:
121
+ """Check if the client is currently closed."""
122
+ return self._is_closed or self._client is None
123
+
124
+ # Delegate all httpx.AsyncClient methods to the underlying client
125
+
126
+ async def get(self, url: Union[str, httpx.URL], **kwargs) -> httpx.Response:
127
+ """Make a GET request."""
128
+ client = await self._ensure_client_open()
129
+ return await client.get(url, **kwargs)
130
+
131
+ async def post(self, url: Union[str, httpx.URL], **kwargs) -> httpx.Response:
132
+ """Make a POST request."""
133
+ client = await self._ensure_client_open()
134
+ return await client.post(url, **kwargs)
135
+
136
+ async def put(self, url: Union[str, httpx.URL], **kwargs) -> httpx.Response:
137
+ """Make a PUT request."""
138
+ client = await self._ensure_client_open()
139
+ return await client.put(url, **kwargs)
140
+
141
+ async def patch(self, url: Union[str, httpx.URL], **kwargs) -> httpx.Response:
142
+ """Make a PATCH request."""
143
+ client = await self._ensure_client_open()
144
+ return await client.patch(url, **kwargs)
145
+
146
+ async def delete(self, url: Union[str, httpx.URL], **kwargs) -> httpx.Response:
147
+ """Make a DELETE request."""
148
+ client = await self._ensure_client_open()
149
+ return await client.delete(url, **kwargs)
150
+
151
+ async def head(self, url: Union[str, httpx.URL], **kwargs) -> httpx.Response:
152
+ """Make a HEAD request."""
153
+ client = await self._ensure_client_open()
154
+ return await client.head(url, **kwargs)
155
+
156
+ async def options(self, url: Union[str, httpx.URL], **kwargs) -> httpx.Response:
157
+ """Make an OPTIONS request."""
158
+ client = await self._ensure_client_open()
159
+ return await client.options(url, **kwargs)
160
+
161
+ async def request(
162
+ self, method: str, url: Union[str, httpx.URL], **kwargs
163
+ ) -> httpx.Response:
164
+ """Make a request with the specified HTTP method."""
165
+ client = await self._ensure_client_open()
166
+ return await client.request(method, url, **kwargs)
167
+
168
+ async def send(self, request: httpx.Request, **kwargs) -> httpx.Response:
169
+ """Send a pre-built request."""
170
+ client = await self._ensure_client_open()
171
+ return await client.send(request, **kwargs)
172
+
173
+ def build_request(
174
+ self, method: str, url: Union[str, httpx.URL], **kwargs
175
+ ) -> httpx.Request:
176
+ """
177
+ Build a request without sending it.
178
+
179
+ Note: This creates a temporary client if none exists, but doesn't keep it open.
180
+ """
181
+ with self._sync_lock:
182
+ if self._client is None or self._is_closed:
183
+ # Create temporary sync client for building request only
184
+ # Use httpx.Client (sync) so we can properly close it
185
+ temp_client = httpx.Client(**self._client_kwargs)
186
+ try:
187
+ return temp_client.build_request(method, url, **kwargs)
188
+ finally:
189
+ temp_client.close()
190
+ return self._client.build_request(method, url, **kwargs)
191
+
192
+ def stream(self, method: str, url: Union[str, httpx.URL], **kwargs):
193
+ """Stream a request. Returns an async context manager."""
194
+ return self._StreamWrapper(self, method, url, **kwargs)
195
+
196
+ # Context manager support
197
+ async def __aenter__(self):
198
+ """Async context manager entry."""
199
+ await self._ensure_client_open()
200
+ return self
201
+
202
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
203
+ """Async context manager exit."""
204
+ await self.aclose()
205
+
206
+ # Properties that don't require an active client
207
+ @property
208
+ def timeout(self) -> Optional[httpx.Timeout]:
209
+ """Get the configured timeout."""
210
+ return self._client_kwargs.get("timeout")
211
+
212
+ @property
213
+ def headers(self) -> httpx.Headers:
214
+ """Get the configured headers."""
215
+ if self._client is not None:
216
+ return self._client.headers
217
+ # Return headers from kwargs if client doesn't exist
218
+ headers = self._client_kwargs.get("headers", {})
219
+ return httpx.Headers(headers)
220
+
221
+ @property
222
+ def cookies(self) -> httpx.Cookies:
223
+ """Get the current cookies."""
224
+ if self._client is not None and not self._is_closed:
225
+ return self._client.cookies
226
+ # Return empty cookies if client doesn't exist or is closed
227
+ return httpx.Cookies()
228
+
229
+ def __repr__(self) -> str:
230
+ """String representation of the client."""
231
+ status = "closed" if self.is_closed else "open"
232
+ return f"<ReopenableAsyncClient [{status}]>"