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.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- 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}]>"
|