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.
Files changed (313) hide show
  1. code_puppy_core_plugins/__init__.py +5 -0
  2. code_puppy_core_plugins/acp/README.md +266 -0
  3. code_puppy_core_plugins/acp/__init__.py +8 -0
  4. code_puppy_core_plugins/acp/agent.py +494 -0
  5. code_puppy_core_plugins/acp/bridge.py +309 -0
  6. code_puppy_core_plugins/acp/capabilities.py +73 -0
  7. code_puppy_core_plugins/acp/commands.py +116 -0
  8. code_puppy_core_plugins/acp/content.py +115 -0
  9. code_puppy_core_plugins/acp/io_delegation.py +231 -0
  10. code_puppy_core_plugins/acp/mcp_config.py +66 -0
  11. code_puppy_core_plugins/acp/permissions.py +159 -0
  12. code_puppy_core_plugins/acp/persistence.py +197 -0
  13. code_puppy_core_plugins/acp/register_callbacks.py +110 -0
  14. code_puppy_core_plugins/acp/replay.py +114 -0
  15. code_puppy_core_plugins/acp/session.py +329 -0
  16. code_puppy_core_plugins/acp/session_config.py +179 -0
  17. code_puppy_core_plugins/acp/state.py +135 -0
  18. code_puppy_core_plugins/agent_skills/__init__.py +22 -0
  19. code_puppy_core_plugins/agent_skills/config.py +208 -0
  20. code_puppy_core_plugins/agent_skills/discovery.py +337 -0
  21. code_puppy_core_plugins/agent_skills/downloader.py +392 -0
  22. code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
  23. code_puppy_core_plugins/agent_skills/installer.py +22 -0
  24. code_puppy_core_plugins/agent_skills/metadata.py +219 -0
  25. code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
  26. code_puppy_core_plugins/agent_skills/provider.py +52 -0
  27. code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
  28. code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
  29. code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
  30. code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
  31. code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
  32. code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
  33. code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
  34. code_puppy_core_plugins/aws_bedrock/config.py +101 -0
  35. code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
  36. code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
  37. code_puppy_core_plugins/azure_foundry/README.md +238 -0
  38. code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
  39. code_puppy_core_plugins/azure_foundry/config.py +126 -0
  40. code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
  41. code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
  42. code_puppy_core_plugins/azure_foundry/token.py +182 -0
  43. code_puppy_core_plugins/azure_foundry/utils.py +375 -0
  44. code_puppy_core_plugins/btw/__init__.py +1 -0
  45. code_puppy_core_plugins/btw/inline_view.py +135 -0
  46. code_puppy_core_plugins/btw/register_callbacks.py +106 -0
  47. code_puppy_core_plugins/btw/side_query.py +91 -0
  48. code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
  49. code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
  50. code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
  51. code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
  52. code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
  53. code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
  54. code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
  55. code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
  56. code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
  57. code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
  58. code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
  59. code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
  60. code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
  61. code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
  62. code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
  63. code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
  64. code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
  65. code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
  66. code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
  67. code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
  68. code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
  69. code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
  70. code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
  71. code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
  72. code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
  73. code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
  74. code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
  75. code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
  76. code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
  77. code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
  78. code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
  79. code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
  80. code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
  81. code_puppy_core_plugins/computer_use/README.md +109 -0
  82. code_puppy_core_plugins/computer_use/__init__.py +1 -0
  83. code_puppy_core_plugins/computer_use/accessibility.py +163 -0
  84. code_puppy_core_plugins/computer_use/activation.py +75 -0
  85. code_puppy_core_plugins/computer_use/backend.py +584 -0
  86. code_puppy_core_plugins/computer_use/backend_types.py +5 -0
  87. code_puppy_core_plugins/computer_use/capture.py +110 -0
  88. code_puppy_core_plugins/computer_use/commands.py +62 -0
  89. code_puppy_core_plugins/computer_use/geometry.py +81 -0
  90. code_puppy_core_plugins/computer_use/inline_image.py +56 -0
  91. code_puppy_core_plugins/computer_use/keycodes.py +47 -0
  92. code_puppy_core_plugins/computer_use/policy.py +144 -0
  93. code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
  94. code_puppy_core_plugins/computer_use/safety.py +18 -0
  95. code_puppy_core_plugins/computer_use/settle.py +64 -0
  96. code_puppy_core_plugins/computer_use/state.py +86 -0
  97. code_puppy_core_plugins/computer_use/tools.py +469 -0
  98. code_puppy_core_plugins/context_indicator/__init__.py +14 -0
  99. code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
  100. code_puppy_core_plugins/context_indicator/usage.py +40 -0
  101. code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
  102. code_puppy_core_plugins/copilot_auth/config.py +93 -0
  103. code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
  104. code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
  105. code_puppy_core_plugins/copilot_auth/utils.py +587 -0
  106. code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
  107. code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
  108. code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
  109. code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
  110. code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
  111. code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
  112. code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
  113. code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
  114. code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
  115. code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
  116. code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
  117. code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
  118. code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
  119. code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
  120. code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
  121. code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
  122. code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
  123. code_puppy_core_plugins/emoji_filter/config.py +26 -0
  124. code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
  125. code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
  126. code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
  127. code_puppy_core_plugins/example_custom_command/README.md +328 -0
  128. code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
  129. code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
  130. code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
  131. code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
  132. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
  133. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
  134. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
  135. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
  136. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
  137. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
  138. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
  139. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
  140. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
  141. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
  142. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
  143. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
  144. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
  145. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
  146. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
  147. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
  148. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
  149. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
  150. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
  151. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
  152. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
  153. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
  154. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
  155. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
  156. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
  157. code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
  158. code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
  159. code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
  160. code_puppy_core_plugins/force_push_guard/detector.py +96 -0
  161. code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
  162. code_puppy_core_plugins/fork/__init__.py +7 -0
  163. code_puppy_core_plugins/fork/register_callbacks.py +445 -0
  164. code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
  165. code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
  166. code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
  167. code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
  168. code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
  169. code_puppy_core_plugins/grok_oauth/config.py +49 -0
  170. code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
  171. code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
  172. code_puppy_core_plugins/grok_oauth/utils.py +195 -0
  173. code_puppy_core_plugins/herdr/README.md +168 -0
  174. code_puppy_core_plugins/herdr/__init__.py +9 -0
  175. code_puppy_core_plugins/herdr/client.py +339 -0
  176. code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
  177. code_puppy_core_plugins/herdr/reporter.py +212 -0
  178. code_puppy_core_plugins/herdr/smoke.py +143 -0
  179. code_puppy_core_plugins/herdr/sources.py +133 -0
  180. code_puppy_core_plugins/hook_creator/__init__.py +1 -0
  181. code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
  182. code_puppy_core_plugins/hook_manager/__init__.py +1 -0
  183. code_puppy_core_plugins/hook_manager/config.py +290 -0
  184. code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
  185. code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
  186. code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
  187. code_puppy_core_plugins/meta_oauth/config.py +55 -0
  188. code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
  189. code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
  190. code_puppy_core_plugins/meta_oauth/utils.py +252 -0
  191. code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
  192. code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
  193. code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
  194. code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
  195. code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
  196. code_puppy_core_plugins/no_tools/__init__.py +0 -0
  197. code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
  198. code_puppy_core_plugins/oauth_pasteback.py +110 -0
  199. code_puppy_core_plugins/oauth_puppy_html.py +194 -0
  200. code_puppy_core_plugins/obsidian_agent/README.md +52 -0
  201. code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
  202. code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
  203. code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
  204. code_puppy_core_plugins/ollama/__init__.py +1 -0
  205. code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
  206. code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
  207. code_puppy_core_plugins/ollama_setup/completer.py +38 -0
  208. code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
  209. code_puppy_core_plugins/plugin_list/__init__.py +0 -0
  210. code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
  211. code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
  212. code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
  213. code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
  214. code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
  215. code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
  216. code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
  217. code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
  218. code_puppy_core_plugins/pop_command/__init__.py +1 -0
  219. code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
  220. code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
  221. code_puppy_core_plugins/prompt_newline/config.py +21 -0
  222. code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
  223. code_puppy_core_plugins/prune/__init__.py +1 -0
  224. code_puppy_core_plugins/prune/prune_menu.py +308 -0
  225. code_puppy_core_plugins/prune/prune_model.py +517 -0
  226. code_puppy_core_plugins/prune/prune_render.py +354 -0
  227. code_puppy_core_plugins/prune/register_callbacks.py +388 -0
  228. code_puppy_core_plugins/puppy_kennel/README.md +203 -0
  229. code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
  230. code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
  231. code_puppy_core_plugins/puppy_kennel/config.py +36 -0
  232. code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
  233. code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
  234. code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
  235. code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
  236. code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
  237. code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
  238. code_puppy_core_plugins/puppy_kennel/state.py +68 -0
  239. code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
  240. code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
  241. code_puppy_core_plugins/puppy_spinner/README.md +93 -0
  242. code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
  243. code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
  244. code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
  245. code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
  246. code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
  247. code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
  248. code_puppy_core_plugins/quick_resume/__init__.py +0 -0
  249. code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
  250. code_puppy_core_plugins/review_pr/__init__.py +1 -0
  251. code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
  252. code_puppy_core_plugins/shell_safety/__init__.py +6 -0
  253. code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
  254. code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
  255. code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
  256. code_puppy_core_plugins/stack_dump/__init__.py +1 -0
  257. code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
  258. code_puppy_core_plugins/statusline/__init__.py +14 -0
  259. code_puppy_core_plugins/statusline/config.py +68 -0
  260. code_puppy_core_plugins/statusline/payload.py +138 -0
  261. code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
  262. code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
  263. code_puppy_core_plugins/statusline/runner.py +122 -0
  264. code_puppy_core_plugins/statusline/statusline_command.py +197 -0
  265. code_puppy_core_plugins/steer_queue/__init__.py +10 -0
  266. code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
  267. code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
  268. code_puppy_core_plugins/subagent_panel/README.md +102 -0
  269. code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
  270. code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
  271. code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
  272. code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
  273. code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
  274. code_puppy_core_plugins/subagent_panel/state.py +231 -0
  275. code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
  276. code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
  277. code_puppy_core_plugins/theme/README.md +86 -0
  278. code_puppy_core_plugins/theme/__init__.py +14 -0
  279. code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
  280. code_puppy_core_plugins/theme/content_styles.py +146 -0
  281. code_puppy_core_plugins/theme/osc_palette.py +178 -0
  282. code_puppy_core_plugins/theme/picker.py +358 -0
  283. code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
  284. code_puppy_core_plugins/theme/register_callbacks.py +348 -0
  285. code_puppy_core_plugins/theme/rich_themes.py +202 -0
  286. code_puppy_core_plugins/theme/themes.py +720 -0
  287. code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
  288. code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
  289. code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
  290. code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
  291. code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
  292. code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
  293. code_puppy_core_plugins/universal_constructor/models.py +138 -0
  294. code_puppy_core_plugins/universal_constructor/provider.py +62 -0
  295. code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
  296. code_puppy_core_plugins/universal_constructor/registry.py +302 -0
  297. code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
  298. code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
  299. code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
  300. code_puppy_core_plugins/wiggum/__init__.py +1 -0
  301. code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
  302. code_puppy_core_plugins/wiggum/judge.py +299 -0
  303. code_puppy_core_plugins/wiggum/judge_config.py +249 -0
  304. code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
  305. code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
  306. code_puppy_core_plugins/wiggum/state.py +89 -0
  307. code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
  308. code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
  309. code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
  310. code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
  311. code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
  312. code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
  313. code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,309 @@
1
+ """``EventBridge`` — translate Code Puppy runtime events into ACP updates.
2
+
3
+ While Code Puppy runs a turn, pydantic-ai's ``event_stream_handler`` fires the
4
+ ``stream_event`` callback for each streamed text/thinking delta, and the tool
5
+ layer fires ``pre_tool_call`` / ``post_tool_call``. This bridge registers those
6
+ hooks (the same seam the shipping ``frontend_emitter`` plugin uses) and maps
7
+ each event to a ``session/update`` notification sent through the SDK's
8
+ ``AgentSideConnection.session_update``.
9
+
10
+ We use hooks rather than consuming the ``MessageBus`` because the bus is
11
+ single-consumer and buffers until a renderer attaches, and agent text does not
12
+ arrive there as clean deltas. See ``README.md`` → *Event source*.
13
+
14
+ Tool calls get human-friendly titles ("Edit foo.py", "Run: pytest"), a ``kind``
15
+ so the client picks the right icon, file ``locations``, and — on completion —
16
+ the unified diff as inline content so edits are visible in the client's UI.
17
+
18
+ Routing/run-context (which session is active, whether text streamed, the
19
+ open-tool-call stack) lives in ``state`` so the permission and I/O backends can
20
+ share it. This bridge is pure translation.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ import os
27
+ from typing import Any, Dict, List, Optional
28
+
29
+ from acp.helpers import (
30
+ start_tool_call,
31
+ text_block,
32
+ tool_content,
33
+ update_agent_message_text,
34
+ update_agent_thought_text,
35
+ update_tool_call,
36
+ )
37
+ from acp.schema import ToolCallLocation
38
+
39
+ from code_puppy.callbacks import register_callback, unregister_callback
40
+ from code_puppy_core_plugins.acp import state
41
+ from code_puppy.tools.common import resolve_path
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Map Code Puppy tool names to ACP tool-call ``kind`` values so the client
46
+ # picks the right icon/affordance. Unknown tools fall back to "other".
47
+ _TOOL_KINDS = {
48
+ "read_file": "read",
49
+ "list_files": "read",
50
+ "grep": "search",
51
+ "edit_file": "edit",
52
+ "create_file": "edit",
53
+ "replace_in_file": "edit",
54
+ "delete_snippet": "edit",
55
+ "delete_file": "delete",
56
+ "agent_run_shell_command": "execute",
57
+ "agent_share_your_reasoning": "think",
58
+ }
59
+
60
+ # Human-readable verbs for building tool-call titles like "Edit foo.py".
61
+ _TOOL_VERBS = {
62
+ "read_file": "Read",
63
+ "list_files": "List",
64
+ "grep": "Search",
65
+ "edit_file": "Edit",
66
+ "create_file": "Create",
67
+ "replace_in_file": "Edit",
68
+ "delete_snippet": "Edit",
69
+ "delete_file": "Delete",
70
+ }
71
+
72
+ # Tool-arg keys that commonly carry the target file path, in priority order.
73
+ _PATH_ARG_KEYS = ("file_path", "path", "target_file", "filename")
74
+
75
+ # Cap on inline diff/output content so a huge edit never floods the client.
76
+ _MAX_DIFF_CHARS = 8000
77
+
78
+ # Interactive TUI tools that can't run headless over ACP (they'd render a terminal
79
+ # picker + read stdin, the JSON-RPC pipe). Until ACP ratifies structured elicitation
80
+ # (an RFD), block these and steer the model to plain-text questions instead.
81
+ _INTERACTIVE_TOOLS = {"ask_user_question"}
82
+ _INTERACTIVE_BLOCK = {
83
+ "blocked": True,
84
+ "error_message": (
85
+ "[BLOCKED] Interactive pickers aren't available over ACP. Ask the user "
86
+ "your question(s) directly in your normal text response and wait for "
87
+ "their reply — do not call this tool."
88
+ ),
89
+ }
90
+
91
+
92
+ class EventBridge:
93
+ """Forward Code Puppy runtime hook events to the client as updates."""
94
+
95
+ def register(self) -> None:
96
+ """Register the runtime hooks. Call once when the connection opens."""
97
+ register_callback("stream_event", self._on_stream_event)
98
+ register_callback("pre_tool_call", self._on_pre_tool_call)
99
+ register_callback("post_tool_call", self._on_post_tool_call)
100
+
101
+ def unregister(self) -> None:
102
+ """Remove the runtime hooks. Call on teardown so no callbacks leak.
103
+
104
+ Mirrors ``register`` so a closed connection leaves no dangling bridge
105
+ callbacks wired into the global registry (which would double-emit if a
106
+ second connection opened in the same process).
107
+ """
108
+ for phase, handler in (
109
+ ("stream_event", self._on_stream_event),
110
+ ("pre_tool_call", self._on_pre_tool_call),
111
+ ("post_tool_call", self._on_post_tool_call),
112
+ ):
113
+ try:
114
+ unregister_callback(phase, handler)
115
+ except Exception: # noqa: BLE001
116
+ logger.debug(
117
+ "ACP: bridge unregister failed for %s", phase, exc_info=True
118
+ )
119
+
120
+ # ---- Hook handlers ----------------------------------------------------
121
+ async def _on_stream_event(
122
+ self, event_type: str, event_data: Any, agent_session_id: Optional[str] = None
123
+ ) -> None:
124
+ """Map streamed text/thinking to message/thought chunks.
125
+
126
+ Two event shapes carry visible content and BOTH must be forwarded:
127
+
128
+ * ``part_start`` -- a new text/thinking part whose *initial* content
129
+ (``part.content``) is the opening of the message. pydantic-ai often
130
+ packs the first token(s) here rather than in the first delta.
131
+ * ``part_delta`` -- each subsequent chunk (``delta.content_delta``).
132
+
133
+ Start-content and delta-content are disjoint (deltas are only the
134
+ *additions* after the start), so emitting both reconstructs the full
135
+ message with no overlap. Forwarding only ``part_delta`` -- as this
136
+ bridge used to -- silently drops the opening chunk whenever the model
137
+ front-loads it into the start event.
138
+
139
+ The per-run ``state`` session id wins over ``agent_session_id``: the
140
+ former is a ``ContextVar`` isolated to this prompt's task, whereas
141
+ ``agent_session_id`` is derived from the process-wide message-bus
142
+ session context, which a concurrent prompt on the same connection can
143
+ overwrite. We only fall back to it when no run is active in this context.
144
+ """
145
+ session_id = state.get_active_session_id() or agent_session_id
146
+ if session_id is None:
147
+ return
148
+ data = event_data if isinstance(event_data, dict) else {}
149
+ if event_type == "part_start":
150
+ part = data.get("part")
151
+ content = getattr(part, "content", None)
152
+ is_thinking = "Thinking" in (data.get("part_type", "") or "")
153
+ elif event_type == "part_delta":
154
+ delta = data.get("delta")
155
+ content = (
156
+ getattr(delta, "content_delta", None) if delta is not None else None
157
+ )
158
+ is_thinking = "Thinking" in (data.get("delta_type", "") or "")
159
+ else:
160
+ return
161
+ if not content:
162
+ return
163
+ if is_thinking:
164
+ update = update_agent_thought_text(content)
165
+ else:
166
+ state.note_streamed_text()
167
+ update = update_agent_message_text(content)
168
+ await self._send(session_id, update)
169
+
170
+ async def _on_pre_tool_call(
171
+ self, tool_name: str, tool_args: Dict[str, Any], context: Any = None
172
+ ) -> Any:
173
+ """Announce a tool call starting (status ``in_progress``).
174
+
175
+ Interactive TUI tools that can't run over ACP are blocked here (via the
176
+ ``pre_tool_call`` blocking contract) *before* we open a tool-call entry,
177
+ so the client never sees a dangling in-progress call.
178
+ """
179
+ session_id = state.get_active_session_id()
180
+ if session_id is None:
181
+ return None
182
+ if tool_name in _INTERACTIVE_TOOLS:
183
+ return dict(_INTERACTIVE_BLOCK)
184
+ tool_call_id = state.push_tool_call(tool_name)
185
+ update = start_tool_call(
186
+ tool_call_id,
187
+ self._title_for(tool_name, tool_args),
188
+ kind=_TOOL_KINDS.get(tool_name, "other"),
189
+ status="in_progress",
190
+ locations=self._locations_for(tool_args) or None,
191
+ raw_input=tool_args if isinstance(tool_args, dict) else {},
192
+ )
193
+ await self._send(session_id, update)
194
+
195
+ async def _on_post_tool_call(
196
+ self,
197
+ tool_name: str,
198
+ tool_args: Dict[str, Any],
199
+ result: Any,
200
+ duration_ms: float,
201
+ context: Any = None,
202
+ ) -> None:
203
+ """Report a tool call finishing (status ``completed`` / ``failed``)."""
204
+ session_id = state.get_active_session_id()
205
+ if session_id is None:
206
+ return
207
+ tool_call_id = state.pop_tool_call(tool_name)
208
+ if tool_call_id is None:
209
+ return
210
+ status = "completed" if self._is_success(result) else "failed"
211
+ update = update_tool_call(
212
+ tool_call_id,
213
+ status=status,
214
+ content=self._content_for(result) or None,
215
+ raw_output=self._summarize(result),
216
+ )
217
+ await self._send(session_id, update)
218
+
219
+ # ---- Helpers ----------------------------------------------------------
220
+ @staticmethod
221
+ def _title_for(tool_name: str, tool_args: Any) -> str:
222
+ """Build a human-friendly tool-call title (e.g. ``Edit foo.py``)."""
223
+ if tool_name == "agent_run_shell_command" and isinstance(tool_args, dict):
224
+ command = str(tool_args.get("command", "")).strip()
225
+ if command:
226
+ short = command if len(command) <= 60 else command[:57] + "..."
227
+ return f"Run: {short}"
228
+ verb = _TOOL_VERBS.get(tool_name)
229
+ path = EventBridge._first_path(tool_args)
230
+ if verb and path:
231
+ return f"{verb} {os.path.basename(path)}"
232
+ return tool_name
233
+
234
+ @staticmethod
235
+ def _first_path(tool_args: Any) -> Optional[str]:
236
+ if not isinstance(tool_args, dict):
237
+ return None
238
+ for key in _PATH_ARG_KEYS:
239
+ value = tool_args.get(key)
240
+ if isinstance(value, str) and value:
241
+ return value
242
+ return None
243
+
244
+ @staticmethod
245
+ def _locations_for(tool_args: Any) -> List[ToolCallLocation]:
246
+ """Extract ACP ``locations`` (files this tool touches) from its args.
247
+
248
+ Lets the client highlight the affected file in the agent panel.
249
+ Best-effort: empty list when no recognizable path arg is present.
250
+
251
+ Uses ``resolve_path`` (not ``os.path.abspath``) so a relative tool-arg
252
+ path is absolutized against the *session's* cwd -- the same base the
253
+ tools themselves resolve against -- rather than the ACP process cwd,
254
+ which would point the client at the wrong file.
255
+ """
256
+ path = EventBridge._first_path(tool_args)
257
+ return [ToolCallLocation(path=resolve_path(path))] if path else []
258
+
259
+ @staticmethod
260
+ def _content_for(result: Any) -> list:
261
+ """Attach the unified diff (if any) as an inline content block.
262
+
263
+ File-edit tools return a ``diff`` (unified diff text). We surface it as
264
+ a fenced ``diff`` code block so the change is visible inline in the
265
+ client's tool-call entry, in every ACP client — independent of whether
266
+ filesystem delegation is rendering a native diff too.
267
+ """
268
+ if not isinstance(result, dict):
269
+ return []
270
+ diff = result.get("diff")
271
+ if not isinstance(diff, str) or not diff.strip():
272
+ return []
273
+ if len(diff) > _MAX_DIFF_CHARS:
274
+ diff = diff[: _MAX_DIFF_CHARS - 3] + "..."
275
+ return [tool_content(text_block(f"```diff\n{diff}\n```"))]
276
+
277
+ async def _send(self, session_id: str, update: Any) -> None:
278
+ """Emit one ``session/update`` notification, swallowing failures.
279
+
280
+ A translation/transport hiccup on one event must never abort the agent
281
+ run, so we log and move on.
282
+ """
283
+ connection = state.get_connection()
284
+ if connection is None:
285
+ return
286
+ try:
287
+ await connection.session_update(session_id, update)
288
+ except Exception: # noqa: BLE001
289
+ logger.exception("Failed to emit session/update")
290
+
291
+ @staticmethod
292
+ def _is_success(result: Any) -> bool:
293
+ """Best-effort success check mirroring frontend_emitter's heuristic."""
294
+ if isinstance(result, dict):
295
+ if result.get("error"):
296
+ return False
297
+ if result.get("success") is False:
298
+ return False
299
+ return True
300
+
301
+ @staticmethod
302
+ def _summarize(result: Any) -> Any:
303
+ """Return a JSON-safe, size-bounded view of a tool result."""
304
+ if result is None or isinstance(result, (str, int, float, bool, dict, list)):
305
+ if isinstance(result, str) and len(result) > 4000:
306
+ return result[:3997] + "..."
307
+ return result
308
+ rendered = str(result)
309
+ return rendered[:3997] + "..." if len(rendered) > 4000 else rendered
@@ -0,0 +1,73 @@
1
+ """ACP capability negotiation, expressed in the official SDK models.
2
+
3
+ Both sides of an ACP connection describe what they can do. The client sends its
4
+ ``ClientCapabilities`` (can it read/write files? run terminals?) and expects
5
+ our ``AgentCapabilities`` back. The SDK provides typed models for both, so
6
+ this module is a thin, declarative mapping — no hand-rolled JSON.
7
+
8
+ Keeping the shapes here means ``agent.py`` stays about lifecycle, and the one
9
+ place we decide "what does Code Puppy advertise" is obvious and reviewable.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Tuple
15
+
16
+ from acp.schema import (
17
+ AgentCapabilities,
18
+ ClientCapabilities,
19
+ PromptCapabilities,
20
+ SessionAdditionalDirectoriesCapabilities,
21
+ SessionCapabilities,
22
+ SessionCloseCapabilities,
23
+ SessionForkCapabilities,
24
+ SessionListCapabilities,
25
+ SessionResumeCapabilities,
26
+ )
27
+
28
+
29
+ def agent_capabilities() -> AgentCapabilities:
30
+ """Describe what Code Puppy can do, for the ``initialize`` response.
31
+
32
+ * ``prompt_capabilities.embedded_context`` — we splice embedded text
33
+ resources into the prompt (see ``content.py``), so this is honestly
34
+ ``True``. ``image`` is also ``True``: image blocks are decoded into
35
+ ``BinaryContent`` / ``ImageUrl`` attachments (see ``content.py``). Audio
36
+ input is not consumed yet, so ``audio`` stays ``False``.
37
+ * ``load_session`` — we accept ``session/load`` and rehydrate the
38
+ session's persisted history into a fresh agent, so re-opened threads
39
+ continue the real conversation.
40
+ * ``session_capabilities`` — we support ``list``, ``close``, ``fork``
41
+ (in-memory history duplication), ``resume`` (persisted rehydration), and
42
+ ``additional_directories`` (multi-root), all advertised via the SDK's
43
+ marker sub-models.
44
+ """
45
+ return AgentCapabilities(
46
+ prompt_capabilities=PromptCapabilities(
47
+ image=True, audio=False, embedded_context=True
48
+ ),
49
+ load_session=True,
50
+ session_capabilities=SessionCapabilities(
51
+ list=SessionListCapabilities(),
52
+ close=SessionCloseCapabilities(),
53
+ fork=SessionForkCapabilities(),
54
+ resume=SessionResumeCapabilities(),
55
+ additional_directories=SessionAdditionalDirectoriesCapabilities(),
56
+ ),
57
+ )
58
+
59
+
60
+ def client_io_caps(caps: ClientCapabilities | None) -> Tuple[bool, bool, bool]:
61
+ """Return ``(fs_read, fs_write, terminal)`` from the client's capabilities.
62
+
63
+ Drives which I/O edges we delegate to the client vs run locally. Defensive: a
64
+ ``None`` blob or a newer client that omits fields degrades to "run
65
+ locally", never to a crash.
66
+ """
67
+ if caps is None:
68
+ return False, False, False
69
+ fs = getattr(caps, "fs", None)
70
+ fs_read = bool(getattr(fs, "read_text_file", False)) if fs else False
71
+ fs_write = bool(getattr(fs, "write_text_file", False)) if fs else False
72
+ terminal = bool(getattr(caps, "terminal", False))
73
+ return fs_read, fs_write, terminal
@@ -0,0 +1,116 @@
1
+ """Execute client-typed slash commands headless and return their output.
2
+
3
+ The advertised ``available_commands_update`` tells the client which ``/commands``
4
+ Code Puppy exposes; when the user picks one, the client sends it as a prompt.
5
+ Rather than feed ``/help`` to the model, we route it through Code Puppy's
6
+ ``command_handler`` and forward whatever it emits on the message bus back as an
7
+ ``agent_message_chunk``.
8
+
9
+ Command side-effects go to the ``MessageBus`` (``emit_info`` etc.), which has no
10
+ renderer in ACP mode. We capture that output by attaching a listener + draining
11
+ the queue around the call. We deliberately do **not** touch ``sys.stdin`` — the
12
+ SDK reads the JSON-RPC pipe from it — so interactive prompt_toolkit menus (a TUI
13
+ affordance) simply aren't driven over ACP; non-interactive commands (``/help``,
14
+ status-style commands) work well. Everything is best-effort and never raises
15
+ into the turn.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import logging
22
+ from typing import Any, List, Optional
23
+
24
+ from code_puppy_core_plugins.acp import state
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def is_command(text: str) -> bool:
30
+ """True if ``text`` is a leading ``/slash`` command."""
31
+ return text.strip().startswith("/")
32
+
33
+
34
+ async def run(session_id: str, command: str) -> Optional[str]:
35
+ """Execute ``command`` and forward its bus output to the client.
36
+
37
+ Returns ``None`` when the command was fully handled (its output has been
38
+ forwarded to the client). Returns a **string** when the command handler
39
+ signalled "process this as user input" -- i.e. a custom/markdown command
40
+ that expands into a prompt for the model. The caller then runs the agent on
41
+ that string, exactly as ``cli_runner`` does with a string command result.
42
+ Internal ``__SENTINEL__`` strings (e.g. ``__AUTOSAVE_LOAD__``) are TUI-only
43
+ affordances with no ACP meaning, so they are treated as handled, not modelled.
44
+ """
45
+ from code_puppy.command_line.command_handler import handle_command
46
+ from code_puppy.messaging.message_queue import get_global_queue
47
+
48
+ queue = get_global_queue()
49
+ collected: List[Any] = []
50
+
51
+ def _listener(message: Any) -> None:
52
+ collected.append(message)
53
+
54
+ queue.add_listener(_listener)
55
+ result: Any = None
56
+ try:
57
+ result = await asyncio.to_thread(handle_command, command.strip())
58
+ except Exception: # noqa: BLE001
59
+ logger.debug("ACP: slash command failed", exc_info=True)
60
+ finally:
61
+ # Drain anything still queued (no renderer thread runs in ACP mode).
62
+ while True:
63
+ message = queue.get_nowait()
64
+ if message is None:
65
+ break
66
+ collected.append(message)
67
+ queue.remove_listener(_listener)
68
+
69
+ text = _render(collected)
70
+ if _is_prompt_expansion(result):
71
+ # Handler expanded the command into a prompt: surface any incidental bus
72
+ # output first, then hand the prompt back to the caller to run through the
73
+ # agent (not display it as a canned answer).
74
+ if text:
75
+ await _emit(session_id, text)
76
+ return result.strip()
77
+ await _emit(session_id, text or f"Ran `{command.strip()}`.")
78
+ return None
79
+
80
+
81
+ def _is_prompt_expansion(result: Any) -> bool:
82
+ """True if ``handle_command`` returned a string to be modelled as a prompt.
83
+
84
+ A non-empty string means "process as user input"; ``__SENTINEL__`` strings
85
+ are internal TUI markers (e.g. ``__AUTOSAVE_LOAD__``) that must not reach
86
+ the model.
87
+ """
88
+ if not isinstance(result, str):
89
+ return False
90
+ stripped = result.strip()
91
+ if not stripped:
92
+ return False
93
+ return not (stripped.startswith("__") and stripped.endswith("__"))
94
+
95
+
96
+ def _render(messages: List[Any]) -> str:
97
+ """Flatten captured bus messages into display text."""
98
+ out: List[str] = []
99
+ for message in messages:
100
+ content = getattr(message, "content", None)
101
+ if content is None:
102
+ continue
103
+ out.append(content if isinstance(content, str) else str(content))
104
+ return "\n".join(s for s in out if s).strip()
105
+
106
+
107
+ async def _emit(session_id: str, text: str) -> None:
108
+ from acp.helpers import update_agent_message_text
109
+
110
+ connection = state.get_connection()
111
+ if connection is None or not text:
112
+ return
113
+ try:
114
+ await connection.session_update(session_id, update_agent_message_text(text))
115
+ except Exception: # noqa: BLE001
116
+ logger.debug("ACP: command output emit failed", exc_info=True)
@@ -0,0 +1,115 @@
1
+ """Parse ACP prompt content blocks into a Code Puppy prompt.
2
+
3
+ The client delivers a prompt as a list of typed content blocks. The SDK parses
4
+ the wire into pydantic models before our ``prompt`` handler runs, so we work
5
+ with objects (``TextContentBlock``, ``ImageContentBlock``,
6
+ ``EmbeddedResourceContentBlock``, …) rather than raw dicts.
7
+
8
+ We split a prompt into three parts that map onto ``run_with_mcp``'s signature:
9
+
10
+ * **text** — text blocks, embedded text resources (spliced in under a header,
11
+ honouring the advertised ``embeddedContext`` capability), and resource-link
12
+ references.
13
+ * **attachments** — image blocks carrying base64 data → pydantic-ai
14
+ ``BinaryContent`` (honouring the advertised ``image`` capability).
15
+ * **link_attachments** — image blocks carrying only a URI → ``ImageUrl``.
16
+
17
+ Blocks are duck-typed (``getattr``) so a newer SDK adding fields never breaks
18
+ parsing. Audio is not advertised, so audio blocks degrade to a text note.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import base64
24
+ import logging
25
+ from dataclasses import dataclass, field
26
+ from typing import Any, List
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class ParsedPrompt:
33
+ """A prompt split into text + multimodal attachments for ``run_with_mcp``."""
34
+
35
+ text: str = ""
36
+ attachments: List[Any] = field(default_factory=list) # BinaryContent
37
+ link_attachments: List[Any] = field(default_factory=list) # ImageUrl
38
+
39
+
40
+ def parse_prompt(blocks: List[Any]) -> ParsedPrompt:
41
+ """Split a list of ACP content blocks into text + attachments."""
42
+ parts: List[str] = []
43
+ result = ParsedPrompt()
44
+ for block in blocks or []:
45
+ text = _render_block(block, result)
46
+ if text:
47
+ parts.append(text)
48
+ result.text = "\n\n".join(parts)
49
+ return result
50
+
51
+
52
+ def flatten_prompt(blocks: List[Any]) -> str:
53
+ """Back-compat text-only view (used where attachments aren't needed)."""
54
+ return parse_prompt(blocks).text
55
+
56
+
57
+ def _render_block(block: Any, result: ParsedPrompt) -> str:
58
+ block_type = getattr(block, "type", None)
59
+
60
+ if block_type == "text":
61
+ return getattr(block, "text", "") or ""
62
+
63
+ if block_type == "resource":
64
+ return _render_embedded_resource(getattr(block, "resource", None))
65
+
66
+ if block_type == "resource_link":
67
+ uri = getattr(block, "uri", "") or ""
68
+ name = getattr(block, "name", "") or uri
69
+ return f"[Referenced resource: {name} ({uri})]" if uri else ""
70
+
71
+ if block_type == "image":
72
+ _collect_image(block, result)
73
+ return ""
74
+
75
+ if block_type == "audio":
76
+ return "[Audio attached]"
77
+
78
+ # Unknown/newer block type: fall back to any ``text`` it carries.
79
+ return getattr(block, "text", "") or ""
80
+
81
+
82
+ def _collect_image(block: Any, result: ParsedPrompt) -> None:
83
+ """Turn an image block into a BinaryContent (data) or ImageUrl (uri)."""
84
+ data = getattr(block, "data", None)
85
+ mime = getattr(block, "mime_type", None) or "image/png"
86
+ if data:
87
+ try:
88
+ from pydantic_ai import BinaryContent
89
+
90
+ result.attachments.append(
91
+ BinaryContent(data=base64.b64decode(data), media_type=mime)
92
+ )
93
+ return
94
+ except Exception: # noqa: BLE001
95
+ logger.debug("ACP: could not decode image data", exc_info=True)
96
+ uri = getattr(block, "uri", None)
97
+ if uri:
98
+ try:
99
+ from pydantic_ai import ImageUrl
100
+
101
+ result.link_attachments.append(ImageUrl(url=uri))
102
+ except Exception: # noqa: BLE001
103
+ logger.debug("ACP: could not build ImageUrl", exc_info=True)
104
+
105
+
106
+ def _render_embedded_resource(resource: Any) -> str:
107
+ """Render an embedded resource; only the text variant carries usable text."""
108
+ if resource is None:
109
+ return ""
110
+ text = getattr(resource, "text", None)
111
+ if not text:
112
+ uri = getattr(resource, "uri", "") or ""
113
+ return f"[Embedded binary resource: {uri}]" if uri else ""
114
+ uri = getattr(resource, "uri", "") or "embedded"
115
+ return f"<context uri={uri!r}>\n{text}\n</context>"