code-puppy-core-plugins 0.0.2__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_core_plugins/__init__.py +5 -0
- code_puppy_core_plugins/acp/README.md +266 -0
- code_puppy_core_plugins/acp/__init__.py +8 -0
- code_puppy_core_plugins/acp/agent.py +494 -0
- code_puppy_core_plugins/acp/bridge.py +309 -0
- code_puppy_core_plugins/acp/capabilities.py +73 -0
- code_puppy_core_plugins/acp/commands.py +116 -0
- code_puppy_core_plugins/acp/content.py +115 -0
- code_puppy_core_plugins/acp/io_delegation.py +231 -0
- code_puppy_core_plugins/acp/mcp_config.py +66 -0
- code_puppy_core_plugins/acp/permissions.py +159 -0
- code_puppy_core_plugins/acp/persistence.py +197 -0
- code_puppy_core_plugins/acp/register_callbacks.py +110 -0
- code_puppy_core_plugins/acp/replay.py +114 -0
- code_puppy_core_plugins/acp/session.py +329 -0
- code_puppy_core_plugins/acp/session_config.py +179 -0
- code_puppy_core_plugins/acp/state.py +135 -0
- code_puppy_core_plugins/agent_skills/__init__.py +22 -0
- code_puppy_core_plugins/agent_skills/config.py +208 -0
- code_puppy_core_plugins/agent_skills/discovery.py +337 -0
- code_puppy_core_plugins/agent_skills/downloader.py +392 -0
- code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
- code_puppy_core_plugins/agent_skills/installer.py +22 -0
- code_puppy_core_plugins/agent_skills/metadata.py +219 -0
- code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
- code_puppy_core_plugins/agent_skills/provider.py +52 -0
- code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
- code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
- code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
- code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
- code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
- code_puppy_core_plugins/aws_bedrock/config.py +101 -0
- code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
- code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
- code_puppy_core_plugins/azure_foundry/README.md +238 -0
- code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
- code_puppy_core_plugins/azure_foundry/config.py +126 -0
- code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
- code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
- code_puppy_core_plugins/azure_foundry/token.py +182 -0
- code_puppy_core_plugins/azure_foundry/utils.py +375 -0
- code_puppy_core_plugins/btw/__init__.py +1 -0
- code_puppy_core_plugins/btw/inline_view.py +135 -0
- code_puppy_core_plugins/btw/register_callbacks.py +106 -0
- code_puppy_core_plugins/btw/side_query.py +91 -0
- code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
- code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
- code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
- code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
- code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
- code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
- code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
- code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
- code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
- code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
- code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
- code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
- code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
- code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
- code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
- code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
- code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
- code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
- code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
- code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
- code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
- code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
- code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
- code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
- code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
- code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
- code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
- code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
- code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
- code_puppy_core_plugins/computer_use/README.md +109 -0
- code_puppy_core_plugins/computer_use/__init__.py +1 -0
- code_puppy_core_plugins/computer_use/accessibility.py +163 -0
- code_puppy_core_plugins/computer_use/activation.py +75 -0
- code_puppy_core_plugins/computer_use/backend.py +584 -0
- code_puppy_core_plugins/computer_use/backend_types.py +5 -0
- code_puppy_core_plugins/computer_use/capture.py +110 -0
- code_puppy_core_plugins/computer_use/commands.py +62 -0
- code_puppy_core_plugins/computer_use/geometry.py +81 -0
- code_puppy_core_plugins/computer_use/inline_image.py +56 -0
- code_puppy_core_plugins/computer_use/keycodes.py +47 -0
- code_puppy_core_plugins/computer_use/policy.py +144 -0
- code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
- code_puppy_core_plugins/computer_use/safety.py +18 -0
- code_puppy_core_plugins/computer_use/settle.py +64 -0
- code_puppy_core_plugins/computer_use/state.py +86 -0
- code_puppy_core_plugins/computer_use/tools.py +469 -0
- code_puppy_core_plugins/context_indicator/__init__.py +14 -0
- code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
- code_puppy_core_plugins/context_indicator/usage.py +40 -0
- code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
- code_puppy_core_plugins/copilot_auth/config.py +93 -0
- code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
- code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
- code_puppy_core_plugins/copilot_auth/utils.py +587 -0
- code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
- code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
- code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
- code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
- code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
- code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
- code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
- code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
- code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
- code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
- code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
- code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
- code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
- code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
- code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
- code_puppy_core_plugins/emoji_filter/config.py +26 -0
- code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
- code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
- code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
- code_puppy_core_plugins/example_custom_command/README.md +328 -0
- code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
- code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
- code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
- code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
- code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
- code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
- code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
- code_puppy_core_plugins/force_push_guard/detector.py +96 -0
- code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
- code_puppy_core_plugins/fork/__init__.py +7 -0
- code_puppy_core_plugins/fork/register_callbacks.py +445 -0
- code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
- code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
- code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
- code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
- code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
- code_puppy_core_plugins/grok_oauth/config.py +49 -0
- code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
- code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
- code_puppy_core_plugins/grok_oauth/utils.py +195 -0
- code_puppy_core_plugins/herdr/README.md +168 -0
- code_puppy_core_plugins/herdr/__init__.py +9 -0
- code_puppy_core_plugins/herdr/client.py +339 -0
- code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
- code_puppy_core_plugins/herdr/reporter.py +212 -0
- code_puppy_core_plugins/herdr/smoke.py +143 -0
- code_puppy_core_plugins/herdr/sources.py +133 -0
- code_puppy_core_plugins/hook_creator/__init__.py +1 -0
- code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy_core_plugins/hook_manager/__init__.py +1 -0
- code_puppy_core_plugins/hook_manager/config.py +290 -0
- code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
- code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
- code_puppy_core_plugins/meta_oauth/config.py +55 -0
- code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
- code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
- code_puppy_core_plugins/meta_oauth/utils.py +252 -0
- code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
- code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
- code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
- code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
- code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
- code_puppy_core_plugins/no_tools/__init__.py +0 -0
- code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
- code_puppy_core_plugins/oauth_pasteback.py +110 -0
- code_puppy_core_plugins/oauth_puppy_html.py +194 -0
- code_puppy_core_plugins/obsidian_agent/README.md +52 -0
- code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
- code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
- code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
- code_puppy_core_plugins/ollama/__init__.py +1 -0
- code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
- code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
- code_puppy_core_plugins/ollama_setup/completer.py +38 -0
- code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
- code_puppy_core_plugins/plugin_list/__init__.py +0 -0
- code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
- code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
- code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
- code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
- code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
- code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
- code_puppy_core_plugins/pop_command/__init__.py +1 -0
- code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
- code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
- code_puppy_core_plugins/prompt_newline/config.py +21 -0
- code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
- code_puppy_core_plugins/prune/__init__.py +1 -0
- code_puppy_core_plugins/prune/prune_menu.py +308 -0
- code_puppy_core_plugins/prune/prune_model.py +517 -0
- code_puppy_core_plugins/prune/prune_render.py +354 -0
- code_puppy_core_plugins/prune/register_callbacks.py +388 -0
- code_puppy_core_plugins/puppy_kennel/README.md +203 -0
- code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
- code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
- code_puppy_core_plugins/puppy_kennel/config.py +36 -0
- code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
- code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
- code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
- code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
- code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
- code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
- code_puppy_core_plugins/puppy_kennel/state.py +68 -0
- code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
- code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
- code_puppy_core_plugins/puppy_spinner/README.md +93 -0
- code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
- code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
- code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
- code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
- code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
- code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
- code_puppy_core_plugins/quick_resume/__init__.py +0 -0
- code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
- code_puppy_core_plugins/review_pr/__init__.py +1 -0
- code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
- code_puppy_core_plugins/shell_safety/__init__.py +6 -0
- code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
- code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
- code_puppy_core_plugins/stack_dump/__init__.py +1 -0
- code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
- code_puppy_core_plugins/statusline/__init__.py +14 -0
- code_puppy_core_plugins/statusline/config.py +68 -0
- code_puppy_core_plugins/statusline/payload.py +138 -0
- code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
- code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
- code_puppy_core_plugins/statusline/runner.py +122 -0
- code_puppy_core_plugins/statusline/statusline_command.py +197 -0
- code_puppy_core_plugins/steer_queue/__init__.py +10 -0
- code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
- code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
- code_puppy_core_plugins/subagent_panel/README.md +102 -0
- code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
- code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
- code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
- code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
- code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
- code_puppy_core_plugins/subagent_panel/state.py +231 -0
- code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
- code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
- code_puppy_core_plugins/theme/README.md +86 -0
- code_puppy_core_plugins/theme/__init__.py +14 -0
- code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
- code_puppy_core_plugins/theme/content_styles.py +146 -0
- code_puppy_core_plugins/theme/osc_palette.py +178 -0
- code_puppy_core_plugins/theme/picker.py +358 -0
- code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
- code_puppy_core_plugins/theme/register_callbacks.py +348 -0
- code_puppy_core_plugins/theme/rich_themes.py +202 -0
- code_puppy_core_plugins/theme/themes.py +720 -0
- code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
- code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
- code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
- code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
- code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
- code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
- code_puppy_core_plugins/universal_constructor/models.py +138 -0
- code_puppy_core_plugins/universal_constructor/provider.py +62 -0
- code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
- code_puppy_core_plugins/universal_constructor/registry.py +302 -0
- code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
- code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
- code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
- code_puppy_core_plugins/wiggum/__init__.py +1 -0
- code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
- code_puppy_core_plugins/wiggum/judge.py +299 -0
- code_puppy_core_plugins/wiggum/judge_config.py +249 -0
- code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
- code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
- code_puppy_core_plugins/wiggum/state.py +89 -0
- code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
- code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
- code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
- code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
- code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
- code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
- code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Event emitter for frontend integration.
|
|
2
|
+
|
|
3
|
+
Provides a global event queue that WebSocket handlers can subscribe to.
|
|
4
|
+
Events are JSON-serializable dicts with type, timestamp, data, and an
|
|
5
|
+
optional ``session_id`` channel for multi-tenant routing.
|
|
6
|
+
|
|
7
|
+
Session-channel model
|
|
8
|
+
---------------------
|
|
9
|
+
- Producers may attach a ``session_id`` to an event in two ways:
|
|
10
|
+
1. Explicit kwarg: ``emit_event(type, data, session_id="abc")``
|
|
11
|
+
2. ContextVar: set ``code_puppy_core_plugins.frontend_emitter.session_context.current_emitter_session_id``
|
|
12
|
+
before calling ``emit_event``; the value is
|
|
13
|
+
picked up automatically.
|
|
14
|
+
The explicit kwarg always wins over the ContextVar.
|
|
15
|
+
|
|
16
|
+
- Consumers may subscribe in two ways:
|
|
17
|
+
1. ``subscribe()`` -> wildcard (all events)
|
|
18
|
+
2. ``subscribe(session_id="abc")`` -> only events whose
|
|
19
|
+
``session_id`` matches
|
|
20
|
+
``"abc"``.
|
|
21
|
+
Wildcard subscribers also receive session-tagged events. Session
|
|
22
|
+
subscribers only receive events whose ``session_id`` exactly matches
|
|
23
|
+
their filter (events with ``session_id is None`` are NOT delivered
|
|
24
|
+
to session subscribers).
|
|
25
|
+
|
|
26
|
+
This design is fully backward compatible: existing 2-arg callers of
|
|
27
|
+
``emit_event`` and bare ``subscribe()`` calls behave identically to
|
|
28
|
+
before -- they just see ``session_id: None`` on emitted events.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
34
|
+
import logging
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from datetime import datetime, timezone
|
|
37
|
+
from typing import Any, Dict, List, Optional, Set
|
|
38
|
+
from uuid import uuid4
|
|
39
|
+
|
|
40
|
+
from code_puppy.config import (
|
|
41
|
+
get_frontend_emitter_enabled,
|
|
42
|
+
get_frontend_emitter_max_recent_events,
|
|
43
|
+
get_frontend_emitter_queue_size,
|
|
44
|
+
)
|
|
45
|
+
from code_puppy_core_plugins.frontend_emitter.session_context import (
|
|
46
|
+
current_emitter_session_id,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Sentinel for "no explicit kwarg passed". Can't use ``None``: it's a meaningful
|
|
53
|
+
# explicit value (no session context even if a ContextVar is set), letting callers
|
|
54
|
+
# opt out of the ContextVar fallback by passing ``session_id=None``.
|
|
55
|
+
class _Unset:
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
return "<unset>"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_UNSET: Any = _Unset()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(eq=False)
|
|
64
|
+
class _Subscriber:
|
|
65
|
+
"""A subscription handle. Wraps an asyncio.Queue plus an optional filter."""
|
|
66
|
+
|
|
67
|
+
queue: "asyncio.Queue[Dict[str, Any]]"
|
|
68
|
+
session_id: Optional[str] = None # None == wildcard (all events)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Two parallel structures kept in sync by subscribe/unsubscribe:
|
|
72
|
+
# ``_subscribers`` (legacy queue set, back-compat for unsubscribe(queue)) and
|
|
73
|
+
# ``_subscriber_records`` (queue + filter metadata, iterated during fan-out).
|
|
74
|
+
_subscribers: Set["asyncio.Queue[Dict[str, Any]]"] = set()
|
|
75
|
+
_subscriber_records: Set[_Subscriber] = set()
|
|
76
|
+
_recent_events: List[Dict[str, Any]] = [] # Keep last N events for new subscribers
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_session_id(explicit: Any) -> Optional[str]:
|
|
80
|
+
"""Resolve the effective session_id for an emit call.
|
|
81
|
+
|
|
82
|
+
Precedence: explicit kwarg > ContextVar > None.
|
|
83
|
+
|
|
84
|
+
Passing ``session_id=None`` *explicitly* opts out of the ContextVar
|
|
85
|
+
fallback (the resolved value is ``None``). Only the ``_UNSET``
|
|
86
|
+
sentinel triggers the ContextVar lookup.
|
|
87
|
+
"""
|
|
88
|
+
if explicit is _UNSET:
|
|
89
|
+
try:
|
|
90
|
+
return current_emitter_session_id.get()
|
|
91
|
+
except LookupError:
|
|
92
|
+
return None
|
|
93
|
+
return explicit
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def emit_event(
|
|
97
|
+
event_type: str,
|
|
98
|
+
data: Any = None,
|
|
99
|
+
session_id: Any = _UNSET,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Emit an event to all matching subscribers.
|
|
102
|
+
|
|
103
|
+
Creates a structured event dict with unique ID, type, timestamp,
|
|
104
|
+
data, and ``session_id`` channel, then broadcasts it to all active
|
|
105
|
+
subscribers whose filter matches.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
event_type: Type of event (e.g., "tool_call_start", "stream_token").
|
|
109
|
+
data: Event data payload - should be JSON-serializable.
|
|
110
|
+
session_id: Optional session identifier. If omitted, the value of
|
|
111
|
+
the ``code_puppy_core_plugins.frontend_emitter.session_context.current_emitter_session_id`` ContextVar is
|
|
112
|
+
used (or ``None`` if no ContextVar is set). Pass
|
|
113
|
+
``session_id=None`` explicitly to bypass the ContextVar
|
|
114
|
+
fallback.
|
|
115
|
+
|
|
116
|
+
Routing rules:
|
|
117
|
+
- A wildcard subscriber (``subscribe()`` with no filter) receives
|
|
118
|
+
ALL events regardless of their ``session_id``.
|
|
119
|
+
- A session-filtered subscriber (``subscribe(session_id="x")``)
|
|
120
|
+
receives ONLY events whose resolved ``session_id == "x"``.
|
|
121
|
+
Events with ``session_id is None`` are NOT delivered to
|
|
122
|
+
session-filtered subscribers.
|
|
123
|
+
"""
|
|
124
|
+
# Early return if emitter is disabled
|
|
125
|
+
if not get_frontend_emitter_enabled():
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
resolved_sid = _resolve_session_id(session_id)
|
|
129
|
+
|
|
130
|
+
event: Dict[str, Any] = {
|
|
131
|
+
"id": str(uuid4()),
|
|
132
|
+
"type": event_type,
|
|
133
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
134
|
+
"session_id": resolved_sid,
|
|
135
|
+
"data": data or {},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Store in recent events for replay to new subscribers
|
|
139
|
+
max_recent = get_frontend_emitter_max_recent_events()
|
|
140
|
+
_recent_events.append(event)
|
|
141
|
+
if len(_recent_events) > max_recent:
|
|
142
|
+
_recent_events.pop(0)
|
|
143
|
+
|
|
144
|
+
# Queues managed via subscribe(); anything in ``_subscribers`` not here is an
|
|
145
|
+
# "orphan" (added directly) we still honour as a wildcard for back-compat.
|
|
146
|
+
managed_qids = {id(sub.queue) for sub in _subscriber_records}
|
|
147
|
+
|
|
148
|
+
# 1. Deliver to filter-matching managed subscribers.
|
|
149
|
+
for sub in list(_subscriber_records):
|
|
150
|
+
# Filter: None -> wildcard deliver-all; == sid -> exact match; else skip.
|
|
151
|
+
if sub.session_id is not None and sub.session_id != resolved_sid:
|
|
152
|
+
continue
|
|
153
|
+
try:
|
|
154
|
+
sub.queue.put_nowait(event)
|
|
155
|
+
except asyncio.QueueFull:
|
|
156
|
+
logger.warning(f"Subscriber queue full, dropping event: {event_type}")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Failed to emit event to subscriber: {e}")
|
|
159
|
+
|
|
160
|
+
# 2. Back-compat: deliver to orphan queues (added directly, no subscribe()); exclude
|
|
161
|
+
# managed ones so they don't double-fire and session filtering stays respected.
|
|
162
|
+
for orphan_q in list(_subscribers):
|
|
163
|
+
if id(orphan_q) in managed_qids:
|
|
164
|
+
continue
|
|
165
|
+
try:
|
|
166
|
+
orphan_q.put_nowait(event)
|
|
167
|
+
except asyncio.QueueFull:
|
|
168
|
+
logger.warning(f"Subscriber queue full, dropping event: {event_type}")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to emit event to subscriber: {e}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def subscribe(
|
|
174
|
+
session_id: Optional[str] = None,
|
|
175
|
+
) -> "asyncio.Queue[Dict[str, Any]]":
|
|
176
|
+
"""Subscribe to events.
|
|
177
|
+
|
|
178
|
+
Creates and returns a new async queue that will receive future events.
|
|
179
|
+
The queue has a configurable max size (via frontend_emitter_queue_size)
|
|
180
|
+
to prevent unbounded memory growth if the subscriber is slow.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
session_id: If provided, the queue will ONLY receive events whose
|
|
184
|
+
``session_id`` exactly matches this value. If omitted (or
|
|
185
|
+
``None``), the queue acts as a wildcard and receives every
|
|
186
|
+
emitted event.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
An ``asyncio.Queue`` that will receive matching event dictionaries.
|
|
190
|
+
"""
|
|
191
|
+
queue_size = get_frontend_emitter_queue_size()
|
|
192
|
+
queue: "asyncio.Queue[Dict[str, Any]]" = asyncio.Queue(maxsize=queue_size)
|
|
193
|
+
_subscribers.add(queue)
|
|
194
|
+
_subscriber_records.add(_Subscriber(queue=queue, session_id=session_id))
|
|
195
|
+
logger.debug(
|
|
196
|
+
f"New subscriber added (session_id={session_id!r}), "
|
|
197
|
+
f"total subscribers: {len(_subscriber_records)}"
|
|
198
|
+
)
|
|
199
|
+
return queue
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def unsubscribe(queue: "asyncio.Queue[Dict[str, Any]]") -> None:
|
|
203
|
+
"""Unsubscribe from events.
|
|
204
|
+
|
|
205
|
+
Removes the queue from the subscriber set. Safe to call even if the
|
|
206
|
+
queue was never subscribed or already unsubscribed.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
queue: The queue returned from ``subscribe()``.
|
|
210
|
+
"""
|
|
211
|
+
_subscribers.discard(queue)
|
|
212
|
+
# Drop matching rich-set records too; compare by queue identity (subscribe always
|
|
213
|
+
# allocates a fresh queue, so one record per queue).
|
|
214
|
+
to_drop = {sub for sub in _subscriber_records if sub.queue is queue}
|
|
215
|
+
_subscriber_records.difference_update(to_drop)
|
|
216
|
+
logger.debug(
|
|
217
|
+
f"Subscriber removed, remaining subscribers: {len(_subscriber_records)}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_recent_events() -> List[Dict[str, Any]]:
|
|
222
|
+
"""Get recent events for new subscribers.
|
|
223
|
+
|
|
224
|
+
Returns a copy of the most recent events (up to
|
|
225
|
+
``frontend_emitter_max_recent_events``). Useful for letting new
|
|
226
|
+
WebSocket connections "catch up" on recent activity.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
A list of recent event dictionaries.
|
|
230
|
+
"""
|
|
231
|
+
return _recent_events.copy()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_subscriber_count() -> int:
|
|
235
|
+
"""Get the current number of active subscribers.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Number of active subscriber queues.
|
|
239
|
+
"""
|
|
240
|
+
return len(_subscriber_records)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def clear_recent_events() -> None:
|
|
244
|
+
"""Clear the recent events buffer.
|
|
245
|
+
|
|
246
|
+
Useful for testing or resetting state.
|
|
247
|
+
"""
|
|
248
|
+
_recent_events.clear()
|
|
249
|
+
logger.debug("Recent events cleared")
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Callback registration for frontend event emission.
|
|
2
|
+
|
|
3
|
+
This module registers callbacks for various agent events and emits them
|
|
4
|
+
to subscribed WebSocket handlers via the emitter module.
|
|
5
|
+
|
|
6
|
+
Session channel
|
|
7
|
+
---------------
|
|
8
|
+
None of the emit-sites below pass an explicit ``session_id`` to
|
|
9
|
+
``emit_event``. Instead they rely on the implicit fallback to
|
|
10
|
+
``code_puppy_core_plugins.frontend_emitter.session_context.current_emitter_session_id`` -- so any embedder (e.g. a
|
|
11
|
+
WebSocket backend handling multiple sessions concurrently) just needs
|
|
12
|
+
to set the ContextVar at the start of an agent run and every event
|
|
13
|
+
emitted by code-puppy during that run will be tagged with the
|
|
14
|
+
correct session_id automatically. No imports from
|
|
15
|
+
``code_puppy.api.*`` are added here; the contract is purely through
|
|
16
|
+
the ContextVar primitive in ``code_puppy_core_plugins.frontend_emitter.session_context``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import time
|
|
24
|
+
from typing import Any, Dict, Optional
|
|
25
|
+
|
|
26
|
+
from code_puppy.callbacks import register_callback
|
|
27
|
+
from code_puppy_core_plugins.frontend_emitter.emitter import emit_event
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Max serialized size for a structured tool-args payload before falling back to a
|
|
33
|
+
# truncated string: fits a typical 16-64 KB WebSocket frame after JSON wrapping while
|
|
34
|
+
# still carrying real file paths / small snippets / config blobs verbatim.
|
|
35
|
+
_MAX_ARGS_SERIALIZED_BYTES = 4096
|
|
36
|
+
|
|
37
|
+
# Maximum length for the textual representation we attach when a value
|
|
38
|
+
# can't be JSON-serialised or exceeds the size cap above.
|
|
39
|
+
_MAX_ARGS_TRUNCATED_STR = 4000
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def on_pre_tool_call(
|
|
43
|
+
tool_name: str, tool_args: Dict[str, Any], context: Any = None
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Emit an event when a tool call starts.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
tool_name: Name of the tool being called.
|
|
49
|
+
tool_args: Arguments being passed to the tool.
|
|
50
|
+
context: Optional context data for the tool call.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
emit_event(
|
|
54
|
+
"tool_call_start",
|
|
55
|
+
{
|
|
56
|
+
"tool_name": tool_name,
|
|
57
|
+
"tool_args": _sanitize_args(tool_args),
|
|
58
|
+
"start_time": time.time(),
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
logger.debug(f"Emitted tool_call_start for {tool_name}")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Failed to emit pre_tool_call event: {e}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def on_post_tool_call(
|
|
67
|
+
tool_name: str,
|
|
68
|
+
tool_args: Dict[str, Any],
|
|
69
|
+
result: Any,
|
|
70
|
+
duration_ms: float,
|
|
71
|
+
context: Any = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Emit an event when a tool call completes.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
tool_name: Name of the tool that was called.
|
|
77
|
+
tool_args: Arguments that were passed to the tool.
|
|
78
|
+
result: The result returned by the tool.
|
|
79
|
+
duration_ms: Execution time in milliseconds.
|
|
80
|
+
context: Optional context data for the tool call.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
emit_event(
|
|
84
|
+
"tool_call_complete",
|
|
85
|
+
{
|
|
86
|
+
"tool_name": tool_name,
|
|
87
|
+
"tool_args": _sanitize_args(tool_args),
|
|
88
|
+
"duration_ms": duration_ms,
|
|
89
|
+
"success": _is_successful_result(result),
|
|
90
|
+
"result_summary": _summarize_result(result),
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
logger.debug(
|
|
94
|
+
f"Emitted tool_call_complete for {tool_name} ({duration_ms:.2f}ms)"
|
|
95
|
+
)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Failed to emit post_tool_call event: {e}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def on_stream_event(
|
|
101
|
+
event_type: str, event_data: Any, agent_session_id: Optional[str] = None
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Emit streaming events from the agent.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
event_type: Type of the streaming event.
|
|
107
|
+
event_data: Data associated with the event.
|
|
108
|
+
agent_session_id: Optional session ID of the agent emitting the
|
|
109
|
+
event. This is a *callback-supplied* identifier from the
|
|
110
|
+
agent layer; it is independent of the WebSocket-session
|
|
111
|
+
ContextVar used by the emitter and is preserved verbatim
|
|
112
|
+
inside the event payload.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
emit_event(
|
|
116
|
+
"stream_event",
|
|
117
|
+
{
|
|
118
|
+
"event_type": event_type,
|
|
119
|
+
"event_data": _sanitize_event_data(event_data),
|
|
120
|
+
"agent_session_id": agent_session_id,
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
logger.debug(f"Emitted stream_event: {event_type}")
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.error(f"Failed to emit stream_event: {e}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def on_invoke_agent(*args: Any, **kwargs: Any) -> None:
|
|
129
|
+
"""Emit an event when an agent is invoked.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
*args: Positional arguments from the invoke_agent callback.
|
|
133
|
+
**kwargs: Keyword arguments from the invoke_agent callback.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
# Extract relevant info from args/kwargs
|
|
137
|
+
agent_info = {
|
|
138
|
+
"agent_name": kwargs.get("agent_name") or (args[0] if args else None),
|
|
139
|
+
"session_id": kwargs.get("session_id"),
|
|
140
|
+
"prompt_preview": _truncate_string(
|
|
141
|
+
kwargs.get("prompt") or (args[1] if len(args) > 1 else None),
|
|
142
|
+
max_length=200,
|
|
143
|
+
),
|
|
144
|
+
}
|
|
145
|
+
emit_event("agent_invoked", agent_info)
|
|
146
|
+
logger.debug(f"Emitted agent_invoked: {agent_info.get('agent_name')}")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"Failed to emit invoke_agent event: {e}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ─── sanitizers ─────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _sanitize_args(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
155
|
+
"""Sanitize tool arguments for safe emission.
|
|
156
|
+
|
|
157
|
+
Strategy:
|
|
158
|
+
* Primitives (str / int / float / bool / None) round-trip directly,
|
|
159
|
+
with strings capped at 500 chars.
|
|
160
|
+
* Structured values (list / dict) are JSON-serialised and kept
|
|
161
|
+
verbatim if the serialised form fits within ``_MAX_ARGS_SERIALIZED_BYTES``;
|
|
162
|
+
otherwise replaced by a truncated string representation (NOT the
|
|
163
|
+
opaque ``<list[N]>`` stub the previous implementation produced).
|
|
164
|
+
* Anything that can't be JSON-serialised falls back to ``repr()``,
|
|
165
|
+
capped at ``_MAX_ARGS_TRUNCATED_STR`` chars.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
args: The raw tool arguments dict (or any value -- non-dicts are
|
|
169
|
+
normalised to an empty dict for back-compat).
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Sanitized arguments safe for emission over a WebSocket.
|
|
173
|
+
"""
|
|
174
|
+
if not isinstance(args, dict):
|
|
175
|
+
return {}
|
|
176
|
+
|
|
177
|
+
sanitized: Dict[str, Any] = {}
|
|
178
|
+
for key, value in args.items():
|
|
179
|
+
if isinstance(value, str):
|
|
180
|
+
sanitized[key] = _truncate_string(value, max_length=500)
|
|
181
|
+
elif isinstance(value, bool) or value is None:
|
|
182
|
+
# Note: must check ``bool`` before ``int`` because bool is a subclass of int.
|
|
183
|
+
sanitized[key] = value
|
|
184
|
+
elif isinstance(value, (int, float)):
|
|
185
|
+
sanitized[key] = value
|
|
186
|
+
elif isinstance(value, (list, dict, tuple)):
|
|
187
|
+
sanitized[key] = _preserve_structured(value)
|
|
188
|
+
else:
|
|
189
|
+
# Unknown type: best-effort textual representation.
|
|
190
|
+
try:
|
|
191
|
+
sanitized[key] = _truncate_string(
|
|
192
|
+
repr(value), max_length=_MAX_ARGS_TRUNCATED_STR
|
|
193
|
+
)
|
|
194
|
+
except Exception:
|
|
195
|
+
sanitized[key] = f"<{type(value).__name__}>"
|
|
196
|
+
|
|
197
|
+
return sanitized
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _preserve_structured(value: Any) -> Any:
|
|
201
|
+
"""Round-trip a structured value through JSON if it fits within budget.
|
|
202
|
+
|
|
203
|
+
Returns the original value (so it serialises naturally downstream)
|
|
204
|
+
when the serialised form is <= ``_MAX_ARGS_SERIALIZED_BYTES`` bytes,
|
|
205
|
+
otherwise a truncated string preview. This replaces the previous
|
|
206
|
+
behaviour of emitting an opaque ``<list[N]>`` stub.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
serialised = json.dumps(value, default=str, ensure_ascii=False)
|
|
210
|
+
except (TypeError, ValueError):
|
|
211
|
+
# Not JSON-serialisable at all -- fall back to a short repr.
|
|
212
|
+
try:
|
|
213
|
+
return _truncate_string(repr(value), max_length=_MAX_ARGS_TRUNCATED_STR)
|
|
214
|
+
except Exception:
|
|
215
|
+
return f"<{type(value).__name__}>"
|
|
216
|
+
|
|
217
|
+
if len(serialised.encode("utf-8")) <= _MAX_ARGS_SERIALIZED_BYTES:
|
|
218
|
+
# Small enough -- return the original Python value so it
|
|
219
|
+
# round-trips cleanly through whatever JSON encoder runs later.
|
|
220
|
+
return value
|
|
221
|
+
|
|
222
|
+
# Too big: return a truncated preview string so the consumer at
|
|
223
|
+
# least sees the shape and the first portion of the content.
|
|
224
|
+
return _truncate_string(serialised, max_length=_MAX_ARGS_TRUNCATED_STR)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _sanitize_event_data(data: Any) -> Any:
|
|
228
|
+
"""Sanitize event data for safe emission.
|
|
229
|
+
|
|
230
|
+
Special-cased for pydantic-ai delta-style objects:
|
|
231
|
+
* Anything carrying a ``content_delta`` attribute (text streaming)
|
|
232
|
+
is normalised to ``{"type": <class_name>, "content_delta": <str>}``.
|
|
233
|
+
* Anything carrying an ``args_delta`` attribute (tool-call streaming)
|
|
234
|
+
is normalised to ``{"type": <class_name>, "args_delta": <str>,
|
|
235
|
+
"tool_call_id": <id|None>, "tool_name": <name|None>}``.
|
|
236
|
+
* Anything carrying a ``part`` attribute (part_start / part_end
|
|
237
|
+
wrappers) is unwrapped recursively into its part.
|
|
238
|
+
* Plain dicts / lists / scalars retain the previous behaviour
|
|
239
|
+
(capped recursion, capped length).
|
|
240
|
+
|
|
241
|
+
Without this normalisation, ``repr()`` of a pydantic-ai delta produces
|
|
242
|
+
something like ``<TextPartDelta>`` and downstream consumers (browsers,
|
|
243
|
+
GUI clients) lose the actual streamed content.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
data: The raw event data.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Sanitized data safe for emission.
|
|
250
|
+
"""
|
|
251
|
+
if data is None:
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
if isinstance(data, str):
|
|
255
|
+
return _truncate_string(data, max_length=1000)
|
|
256
|
+
|
|
257
|
+
if isinstance(data, (int, float, bool)):
|
|
258
|
+
return data
|
|
259
|
+
|
|
260
|
+
if isinstance(data, dict):
|
|
261
|
+
return {k: _sanitize_event_data(v) for k, v in list(data.items())[:20]}
|
|
262
|
+
|
|
263
|
+
if isinstance(data, (list, tuple)):
|
|
264
|
+
return [_sanitize_event_data(item) for item in data[:20]]
|
|
265
|
+
|
|
266
|
+
# pydantic-ai delta extraction: duck-type (hasattr), not isinstance, so we don't
|
|
267
|
+
# import pydantic-ai here and stay compatible with future delta subclasses.
|
|
268
|
+
|
|
269
|
+
type_name = type(data).__name__
|
|
270
|
+
|
|
271
|
+
# 1. content_delta (TextPartDelta)
|
|
272
|
+
if hasattr(data, "content_delta"):
|
|
273
|
+
content = getattr(data, "content_delta", "")
|
|
274
|
+
return {
|
|
275
|
+
"type": type_name,
|
|
276
|
+
"content_delta": _truncate_string(content, max_length=1000),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# 2. args_delta (ToolCallPartDelta)
|
|
280
|
+
if hasattr(data, "args_delta"):
|
|
281
|
+
args_delta = getattr(data, "args_delta", "")
|
|
282
|
+
return {
|
|
283
|
+
"type": type_name,
|
|
284
|
+
"args_delta": _truncate_string(args_delta, max_length=1000),
|
|
285
|
+
"tool_call_id": _safe_getattr_str(data, "tool_call_id"),
|
|
286
|
+
"tool_name": _safe_getattr_str(data, "tool_name"),
|
|
287
|
+
"tool_name_delta": _safe_getattr_str(data, "tool_name_delta"),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# 3. part-wrapping events (PartStartEvent / PartEndEvent / etc.) --
|
|
291
|
+
# unwrap the inner ``part`` so its content is reachable too.
|
|
292
|
+
if hasattr(data, "part"):
|
|
293
|
+
try:
|
|
294
|
+
inner = _sanitize_event_data(data.part)
|
|
295
|
+
except Exception:
|
|
296
|
+
inner = f"<{type_name}.part>"
|
|
297
|
+
return {"type": type_name, "part": inner}
|
|
298
|
+
|
|
299
|
+
# 4. ToolCallPart payloads: preserve structured tool metadata for
|
|
300
|
+
# downstream stream bridges (/ws/chat typed event parser).
|
|
301
|
+
if hasattr(data, "tool_name") and hasattr(data, "args"):
|
|
302
|
+
return {
|
|
303
|
+
"type": type_name,
|
|
304
|
+
"tool_call_id": _safe_getattr_str(data, "tool_call_id"),
|
|
305
|
+
"tool_name": _safe_getattr_str(data, "tool_name"),
|
|
306
|
+
"args": _sanitize_args(getattr(data, "args", {})),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# 5. Anything with a usable content/text attribute on the top level
|
|
310
|
+
# (e.g. fully-formed TextPart, ThinkingPart).
|
|
311
|
+
if hasattr(data, "content"):
|
|
312
|
+
return {
|
|
313
|
+
"type": type_name,
|
|
314
|
+
"content": _truncate_string(getattr(data, "content", ""), max_length=1000),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return f"<{type_name}>"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _safe_getattr_str(obj: Any, name: str) -> Optional[str]:
|
|
321
|
+
"""Return ``str(getattr(obj, name))`` or ``None`` if missing / failing."""
|
|
322
|
+
try:
|
|
323
|
+
val = getattr(obj, name, None)
|
|
324
|
+
return None if val is None else str(val)
|
|
325
|
+
except Exception:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _is_successful_result(result: Any) -> bool:
|
|
330
|
+
"""Determine if a tool result indicates success.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
result: The tool result.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
True if the result appears successful.
|
|
337
|
+
"""
|
|
338
|
+
if result is None:
|
|
339
|
+
return True # No result often means success
|
|
340
|
+
|
|
341
|
+
if isinstance(result, dict):
|
|
342
|
+
# Check for error indicators
|
|
343
|
+
if result.get("error"):
|
|
344
|
+
return False
|
|
345
|
+
if result.get("success") is False:
|
|
346
|
+
return False
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
if isinstance(result, bool):
|
|
350
|
+
return result
|
|
351
|
+
|
|
352
|
+
return True # Default to success
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _summarize_result(result: Any) -> str:
|
|
356
|
+
"""Create a brief summary of a tool result.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
result: The tool result.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
A string summary of the result.
|
|
363
|
+
"""
|
|
364
|
+
if result is None:
|
|
365
|
+
return "<no result>"
|
|
366
|
+
|
|
367
|
+
if isinstance(result, str):
|
|
368
|
+
return _truncate_string(result, max_length=200)
|
|
369
|
+
|
|
370
|
+
if isinstance(result, dict):
|
|
371
|
+
if "error" in result:
|
|
372
|
+
return f"Error: {_truncate_string(str(result['error']), max_length=100)}"
|
|
373
|
+
if "message" in result:
|
|
374
|
+
return _truncate_string(str(result["message"]), max_length=100)
|
|
375
|
+
return f"<dict with {len(result)} keys>"
|
|
376
|
+
|
|
377
|
+
if isinstance(result, (list, tuple)):
|
|
378
|
+
return f"<{type(result).__name__}[{len(result)}]>"
|
|
379
|
+
|
|
380
|
+
return _truncate_string(str(result), max_length=200)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _truncate_string(value: Any, max_length: int = 100) -> Optional[str]:
|
|
384
|
+
"""Truncate a string value if it exceeds ``max_length``.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
value: The value to truncate (will be converted to str).
|
|
388
|
+
max_length: Maximum length before truncation.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Truncated string or ``None`` if value is ``None``.
|
|
392
|
+
"""
|
|
393
|
+
if value is None:
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
s = str(value)
|
|
397
|
+
if len(s) > max_length:
|
|
398
|
+
return s[: max_length - 3] + "..."
|
|
399
|
+
return s
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def register() -> None:
|
|
403
|
+
"""Register all frontend emitter callbacks."""
|
|
404
|
+
register_callback("pre_tool_call", on_pre_tool_call)
|
|
405
|
+
register_callback("post_tool_call", on_post_tool_call)
|
|
406
|
+
register_callback("stream_event", on_stream_event)
|
|
407
|
+
register_callback("invoke_agent", on_invoke_agent)
|
|
408
|
+
logger.debug("Frontend emitter callbacks registered")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# Auto-register callbacks when this module is imported
|
|
412
|
+
register()
|