alysis-code 0.13.0__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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,329 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from ..runtime_kind import RuntimeKind, normalize_runtime_kind
7
+ from .errors import McpProtocolError
8
+ from .models import PromptsMode
9
+ from .untrusted_content import build_untrusted_mcp_text_block
10
+
11
+ _PROMPT_ENABLED_RUNTIME_KINDS = frozenset(
12
+ {
13
+ RuntimeKind.INTERACTIVE_CHAT,
14
+ RuntimeKind.ONE_SHOT,
15
+ RuntimeKind.FORGE_EXEC,
16
+ }
17
+ )
18
+ _INLINE_PROMPT_TEXT_CHAR_LIMIT = 12_000
19
+
20
+
21
+ class McpPromptNormalizationError(ValueError, McpProtocolError):
22
+ def __init__(
23
+ self,
24
+ message: str = "",
25
+ *,
26
+ server_id: str | None = None,
27
+ method: str | None = None,
28
+ cause: BaseException | None = None,
29
+ ) -> None:
30
+ super().__init__(message)
31
+ self.server_id = server_id
32
+ self.method = method
33
+ self.cause = cause
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class McpPromptArgument:
38
+ name: str
39
+ description: str | None
40
+ required: bool
41
+ raw_payload: dict[str, Any] = field(repr=False)
42
+
43
+ def as_payload(self) -> dict[str, Any]:
44
+ payload: dict[str, Any] = {
45
+ "name": self.name,
46
+ "required": self.required,
47
+ }
48
+ if self.description:
49
+ payload["description"] = self.description
50
+ return payload
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class McpListedPrompt:
55
+ name: str
56
+ title: str | None
57
+ description: str | None
58
+ arguments: tuple[McpPromptArgument, ...]
59
+ raw_payload: dict[str, Any] = field(repr=False)
60
+
61
+ def as_payload(self, *, server_id: str) -> dict[str, Any]:
62
+ payload: dict[str, Any] = {
63
+ "server_id": server_id,
64
+ "name": self.name,
65
+ "arguments": [argument.as_payload() for argument in self.arguments],
66
+ }
67
+ if self.title:
68
+ payload["title"] = self.title
69
+ if self.description:
70
+ payload["description"] = self.description
71
+ return payload
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class McpPromptMessage:
76
+ role: str
77
+ content: tuple[dict[str, Any], ...]
78
+ content_summary: str
79
+ text: str | None = None
80
+ raw_payload: dict[str, Any] = field(repr=False, default_factory=dict)
81
+
82
+ def as_payload(self, *, server_id: str, prompt_name: str) -> dict[str, Any]:
83
+ content_items: list[dict[str, Any]] = []
84
+ for item in self.content:
85
+ payload_item = dict(item)
86
+ item_text = payload_item.get("text")
87
+ mime_type = payload_item.get("mime_type")
88
+ if isinstance(item_text, str):
89
+ payload_item["text"] = build_untrusted_mcp_text_block(
90
+ source_type="prompt_get",
91
+ server_id=server_id,
92
+ source_name=prompt_name,
93
+ text=item_text,
94
+ mime_type=mime_type if isinstance(mime_type, str) else None,
95
+ )
96
+ content_items.append(payload_item)
97
+ payload: dict[str, Any] = {
98
+ "role": self.role,
99
+ "content_summary": self.content_summary,
100
+ "content": content_items,
101
+ }
102
+ if self.text is not None:
103
+ payload["text"] = build_untrusted_mcp_text_block(
104
+ source_type="prompt_get",
105
+ server_id=server_id,
106
+ source_name=prompt_name,
107
+ text=self.text,
108
+ )
109
+ return payload
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class McpGetPromptResult:
114
+ description: str | None
115
+ messages: tuple[McpPromptMessage, ...]
116
+ content_summary: str
117
+ text: str
118
+ raw_payload: dict[str, Any] = field(repr=False)
119
+
120
+
121
+ def prompts_runtime_supported(runtime_kind: RuntimeKind | str) -> bool:
122
+ return normalize_runtime_kind(runtime_kind) in _PROMPT_ENABLED_RUNTIME_KINDS
123
+
124
+
125
+ def prompts_mode_enabled(
126
+ *,
127
+ prompts_mode: PromptsMode,
128
+ runtime_kind: RuntimeKind | str,
129
+ ) -> bool:
130
+ return prompts_mode == "listed_get_only" and prompts_runtime_supported(runtime_kind)
131
+
132
+
133
+ def _require_object(value: Any, *, context: str) -> dict[str, Any]:
134
+ if not isinstance(value, dict):
135
+ raise McpPromptNormalizationError(f"{context} must be an object.")
136
+ return value
137
+
138
+
139
+ def _require_string(value: Any, *, context: str) -> str:
140
+ if not isinstance(value, str):
141
+ raise McpPromptNormalizationError(f"{context} must be a string.")
142
+ cleaned = value.strip()
143
+ if not cleaned:
144
+ raise McpPromptNormalizationError(f"{context} cannot be empty.")
145
+ return cleaned
146
+
147
+
148
+ def _optional_string(value: Any, *, context: str) -> str | None:
149
+ if value is None:
150
+ return None
151
+ if not isinstance(value, str):
152
+ raise McpPromptNormalizationError(f"{context} must be a string when present.")
153
+ cleaned = value.strip()
154
+ return cleaned or None
155
+
156
+
157
+ def _normalize_prompt_arguments(value: Any, *, context: str) -> tuple[McpPromptArgument, ...]:
158
+ if value is None:
159
+ return ()
160
+ if not isinstance(value, list):
161
+ raise McpPromptNormalizationError(f"{context} must be an array when present.")
162
+ normalized: list[McpPromptArgument] = []
163
+ seen_names: set[str] = set()
164
+ for index, raw_item in enumerate(value):
165
+ payload = _require_object(raw_item, context=f"{context}[{index}]")
166
+ name = _require_string(payload.get("name"), context=f"{context}[{index}].name")
167
+ name_key = name.casefold()
168
+ if name_key in seen_names:
169
+ raise McpPromptNormalizationError(
170
+ f"{context} contains repeated argument name {name!r}."
171
+ )
172
+ seen_names.add(name_key)
173
+ required = payload.get("required", False)
174
+ if not isinstance(required, bool):
175
+ raise McpPromptNormalizationError(
176
+ f"{context}[{index}].required must be a boolean when present."
177
+ )
178
+ normalized.append(
179
+ McpPromptArgument(
180
+ name=name,
181
+ description=_optional_string(
182
+ payload.get("description"),
183
+ context=f"{context}[{index}].description",
184
+ ),
185
+ required=required,
186
+ raw_payload=dict(payload),
187
+ )
188
+ )
189
+ return tuple(normalized)
190
+
191
+
192
+ def _normalize_prompt_content_items(
193
+ value: Any, *, context: str
194
+ ) -> tuple[tuple[dict[str, Any], ...], str]:
195
+ raw_items = value if isinstance(value, list) else [value]
196
+ normalized: list[dict[str, Any]] = []
197
+ summaries: list[str] = []
198
+ for index, raw_item in enumerate(raw_items):
199
+ payload = _require_object(raw_item, context=f"{context}[{index}]")
200
+ item_type = _require_string(payload.get("type"), context=f"{context}[{index}].type")
201
+ normalized_item: dict[str, Any] = {"type": item_type}
202
+ summary = f"{item_type} item omitted"
203
+ mime_type = _optional_string(
204
+ payload.get("mimeType"),
205
+ context=f"{context}[{index}].mimeType",
206
+ )
207
+ if mime_type:
208
+ normalized_item["mime_type"] = mime_type
209
+ summary = f"{mime_type} {summary}"
210
+ if item_type == "text":
211
+ text = _require_string(payload.get("text"), context=f"{context}[{index}].text")
212
+ inline_text = text
213
+ truncated = False
214
+ if len(inline_text) > _INLINE_PROMPT_TEXT_CHAR_LIMIT:
215
+ inline_text = inline_text[:_INLINE_PROMPT_TEXT_CHAR_LIMIT]
216
+ truncated = True
217
+ summary = f"text({len(text)} chars)"
218
+ if mime_type:
219
+ summary = f"{mime_type} {summary}"
220
+ if truncated:
221
+ summary += f", truncated to {_INLINE_PROMPT_TEXT_CHAR_LIMIT} chars"
222
+ normalized_item["text"] = inline_text
223
+ else:
224
+ uri = _optional_string(payload.get("uri"), context=f"{context}[{index}].uri")
225
+ if uri:
226
+ normalized_item["uri"] = uri
227
+ resource = payload.get("resource")
228
+ if isinstance(resource, dict):
229
+ resource_uri = _optional_string(
230
+ resource.get("uri"),
231
+ context=f"{context}[{index}].resource.uri",
232
+ )
233
+ if resource_uri:
234
+ normalized_item["resource_uri"] = resource_uri
235
+ normalized_item["summary"] = summary
236
+ normalized.append(normalized_item)
237
+ summaries.append(summary)
238
+ return tuple(normalized), ", ".join(summaries) or "no content"
239
+
240
+
241
+ def normalize_list_prompts_result(
242
+ result: dict[str, Any],
243
+ ) -> tuple[tuple[McpListedPrompt, ...], str | None]:
244
+ prompts = result.get("prompts")
245
+ if not isinstance(prompts, list):
246
+ raise McpPromptNormalizationError("prompts/list result.prompts must be an array.")
247
+ normalized: list[McpListedPrompt] = []
248
+ for index, raw_item in enumerate(prompts):
249
+ payload = _require_object(raw_item, context=f"prompts/list prompt[{index}]")
250
+ normalized.append(
251
+ McpListedPrompt(
252
+ name=_require_string(
253
+ payload.get("name"), context=f"prompts/list prompt[{index}].name"
254
+ ),
255
+ title=_optional_string(
256
+ payload.get("title"),
257
+ context=f"prompts/list prompt[{index}].title",
258
+ ),
259
+ description=_optional_string(
260
+ payload.get("description"),
261
+ context=f"prompts/list prompt[{index}].description",
262
+ ),
263
+ arguments=_normalize_prompt_arguments(
264
+ payload.get("arguments"),
265
+ context=f"prompts/list prompt[{index}].arguments",
266
+ ),
267
+ raw_payload=dict(payload),
268
+ )
269
+ )
270
+ next_cursor = result.get("nextCursor")
271
+ if next_cursor is None:
272
+ return tuple(normalized), None
273
+ return tuple(normalized), _require_string(next_cursor, context="prompts/list nextCursor")
274
+
275
+
276
+ def normalize_get_prompt_result(
277
+ result: dict[str, Any],
278
+ *,
279
+ expected_name: str,
280
+ ) -> McpGetPromptResult:
281
+ if "name" in result:
282
+ result_name = _require_string(result.get("name"), context="prompts/get result.name")
283
+ if result_name != expected_name:
284
+ raise McpPromptNormalizationError(
285
+ "prompts/get result.name must match the requested prompt name."
286
+ )
287
+ messages = result.get("messages")
288
+ if not isinstance(messages, list):
289
+ raise McpPromptNormalizationError("prompts/get result.messages must be an array.")
290
+ normalized_messages: list[McpPromptMessage] = []
291
+ text_parts: list[str] = []
292
+ summaries: list[str] = []
293
+ for index, raw_item in enumerate(messages):
294
+ payload = _require_object(raw_item, context=f"prompts/get messages[{index}]")
295
+ role = _require_string(payload.get("role"), context=f"prompts/get messages[{index}].role")
296
+ content_items, content_summary = _normalize_prompt_content_items(
297
+ payload.get("content"),
298
+ context=f"prompts/get messages[{index}].content",
299
+ )
300
+ text_parts.extend(
301
+ item["text"]
302
+ for item in content_items
303
+ if isinstance(item.get("text"), str) and str(item.get("text")).strip()
304
+ )
305
+ summaries.append(f"{role}: {content_summary}")
306
+ normalized_messages.append(
307
+ McpPromptMessage(
308
+ role=role,
309
+ content=content_items,
310
+ content_summary=content_summary,
311
+ text="\n\n".join(
312
+ item["text"]
313
+ for item in content_items
314
+ if isinstance(item.get("text"), str) and str(item.get("text")).strip()
315
+ )
316
+ or None,
317
+ raw_payload=dict(payload),
318
+ )
319
+ )
320
+ return McpGetPromptResult(
321
+ description=_optional_string(
322
+ result.get("description"),
323
+ context="prompts/get result.description",
324
+ ),
325
+ messages=tuple(normalized_messages),
326
+ content_summary="; ".join(summaries) or "no messages",
327
+ text="\n\n".join(text_parts),
328
+ raw_payload=dict(result),
329
+ )
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from ..runtime_kind import RuntimeKind, normalize_runtime_kind
7
+ from .errors import McpProtocolError
8
+ from .models import ResourcesMode
9
+ from .untrusted_content import _looks_like_binary_blob, build_untrusted_mcp_text_block
10
+
11
+ _RESOURCE_ENABLED_RUNTIME_KINDS = frozenset(
12
+ {
13
+ RuntimeKind.INTERACTIVE_CHAT,
14
+ RuntimeKind.ONE_SHOT,
15
+ RuntimeKind.FORGE_EXEC,
16
+ }
17
+ )
18
+ _INLINE_RESOURCE_TEXT_CHAR_LIMIT = 12_000
19
+
20
+
21
+ class McpResourceNormalizationError(ValueError, McpProtocolError):
22
+ def __init__(
23
+ self,
24
+ message: str = "",
25
+ *,
26
+ server_id: str | None = None,
27
+ method: str | None = None,
28
+ cause: BaseException | None = None,
29
+ ) -> None:
30
+ super().__init__(message)
31
+ self.server_id = server_id
32
+ self.method = method
33
+ self.cause = cause
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class McpListedResource:
38
+ uri: str
39
+ name: str
40
+ description: str | None
41
+ mime_type: str | None
42
+ size: int | None
43
+ raw_payload: dict[str, Any] = field(repr=False)
44
+
45
+ def as_tool_payload(self, *, server_id: str) -> dict[str, Any]:
46
+ payload: dict[str, Any] = {
47
+ "server_id": server_id,
48
+ "uri": self.uri,
49
+ "name": self.name,
50
+ }
51
+ if self.description:
52
+ payload["description"] = self.description
53
+ if self.mime_type:
54
+ payload["mime_type"] = self.mime_type
55
+ if self.size is not None:
56
+ payload["size"] = self.size
57
+ return payload
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class McpReadResourceContent:
62
+ uri: str
63
+ mime_type: str | None
64
+ content_summary: str
65
+ text: str | None = None
66
+ raw_payload: dict[str, Any] = field(repr=False, default_factory=dict)
67
+
68
+ def as_tool_payload(self, *, server_id: str, resource_uri: str) -> dict[str, Any]:
69
+ payload: dict[str, Any] = {
70
+ "uri": self.uri,
71
+ "content_summary": self.content_summary,
72
+ }
73
+ if self.mime_type:
74
+ payload["mime_type"] = self.mime_type
75
+ if self.text is not None:
76
+ payload["text"] = build_untrusted_mcp_text_block(
77
+ source_type="resource_read",
78
+ server_id=server_id,
79
+ source_name=resource_uri,
80
+ text=self.text,
81
+ mime_type=self.mime_type,
82
+ )
83
+ return payload
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class McpReadResourceResult:
88
+ contents: tuple[McpReadResourceContent, ...]
89
+ content_summary: str
90
+ text: str
91
+ mime_type: str | None
92
+ raw_payload: dict[str, Any] = field(repr=False)
93
+
94
+
95
+ def resources_runtime_supported(runtime_kind: RuntimeKind | str) -> bool:
96
+ return normalize_runtime_kind(runtime_kind) in _RESOURCE_ENABLED_RUNTIME_KINDS
97
+
98
+
99
+ def resources_mode_enabled(
100
+ *,
101
+ resources_mode: ResourcesMode,
102
+ runtime_kind: RuntimeKind | str,
103
+ ) -> bool:
104
+ return resources_mode == "listed_read_only" and resources_runtime_supported(runtime_kind)
105
+
106
+
107
+ def _require_object(value: Any, *, context: str) -> dict[str, Any]:
108
+ if not isinstance(value, dict):
109
+ raise McpResourceNormalizationError(f"{context} must be an object.")
110
+ return value
111
+
112
+
113
+ def _require_string(value: Any, *, context: str) -> str:
114
+ if not isinstance(value, str):
115
+ raise McpResourceNormalizationError(f"{context} must be a string.")
116
+ cleaned = value.strip()
117
+ if not cleaned:
118
+ raise McpResourceNormalizationError(f"{context} cannot be empty.")
119
+ return cleaned
120
+
121
+
122
+ def _optional_string(value: Any, *, context: str) -> str | None:
123
+ if value is None:
124
+ return None
125
+ if not isinstance(value, str):
126
+ raise McpResourceNormalizationError(f"{context} must be a string when present.")
127
+ cleaned = value.strip()
128
+ return cleaned or None
129
+
130
+
131
+ def _optional_size(value: Any, *, context: str) -> int | None:
132
+ if value is None:
133
+ return None
134
+ if isinstance(value, bool) or not isinstance(value, int):
135
+ raise McpResourceNormalizationError(
136
+ f"{context} must be a non-negative integer when present."
137
+ )
138
+ if value < 0:
139
+ raise McpResourceNormalizationError(f"{context} must be >= 0 when present.")
140
+ return int(value)
141
+
142
+
143
+ def _mime_is_textual(mime_type: str | None) -> bool:
144
+ if mime_type is None:
145
+ return True
146
+ lowered = mime_type.strip().lower()
147
+ if not lowered:
148
+ return True
149
+ if lowered.startswith("text/"):
150
+ return True
151
+ return (
152
+ lowered == "application/json"
153
+ or lowered.endswith("+json")
154
+ or lowered == "application/xml"
155
+ or lowered.endswith("+xml")
156
+ or lowered in {"application/yaml", "application/x-yaml", "text/yaml"}
157
+ or lowered.endswith("+yaml")
158
+ or lowered in {"application/javascript", "application/x-javascript"}
159
+ )
160
+
161
+
162
+ def _inline_text_payload(
163
+ *,
164
+ text: str,
165
+ mime_type: str | None,
166
+ ) -> tuple[str | None, str]:
167
+ if not _mime_is_textual(mime_type):
168
+ mime_label = mime_type or "non-text"
169
+ return None, f"{mime_label} text omitted ({len(text)} chars)"
170
+ if _looks_like_binary_blob(text):
171
+ return None, f"binary-like text omitted ({len(text)} chars)"
172
+ inline_text = text
173
+ truncated = False
174
+ if len(inline_text) > _INLINE_RESOURCE_TEXT_CHAR_LIMIT:
175
+ inline_text = inline_text[:_INLINE_RESOURCE_TEXT_CHAR_LIMIT]
176
+ truncated = True
177
+ summary_kind = "json text" if mime_type and "json" in mime_type.lower() else "text"
178
+ summary = f"{summary_kind}({len(text)} chars)"
179
+ if mime_type:
180
+ summary = f"{mime_type} {summary}"
181
+ if truncated:
182
+ summary += f", truncated to {_INLINE_RESOURCE_TEXT_CHAR_LIMIT} chars"
183
+ return inline_text, summary
184
+
185
+
186
+ def normalize_list_resources_result(
187
+ result: dict[str, Any],
188
+ ) -> tuple[tuple[McpListedResource, ...], str | None]:
189
+ resources = result.get("resources")
190
+ if not isinstance(resources, list):
191
+ raise McpResourceNormalizationError("resources/list result.resources must be an array.")
192
+ normalized: list[McpListedResource] = []
193
+ for index, raw_item in enumerate(resources):
194
+ payload = _require_object(raw_item, context=f"resources/list resource[{index}]")
195
+ normalized.append(
196
+ McpListedResource(
197
+ uri=_require_string(
198
+ payload.get("uri"), context=f"resources/list resource[{index}].uri"
199
+ ),
200
+ name=_require_string(
201
+ payload.get("name"), context=f"resources/list resource[{index}].name"
202
+ ),
203
+ description=_optional_string(
204
+ payload.get("description"),
205
+ context=f"resources/list resource[{index}].description",
206
+ ),
207
+ mime_type=_optional_string(
208
+ payload.get("mimeType"),
209
+ context=f"resources/list resource[{index}].mimeType",
210
+ ),
211
+ size=_optional_size(
212
+ payload.get("size"),
213
+ context=f"resources/list resource[{index}].size",
214
+ ),
215
+ raw_payload=dict(payload),
216
+ )
217
+ )
218
+ next_cursor = result.get("nextCursor")
219
+ if next_cursor is None:
220
+ return tuple(normalized), None
221
+ return tuple(normalized), _require_string(next_cursor, context="resources/list nextCursor")
222
+
223
+
224
+ def normalize_read_resource_result(
225
+ result: dict[str, Any],
226
+ *,
227
+ expected_uri: str,
228
+ ) -> McpReadResourceResult:
229
+ contents = result.get("contents")
230
+ if not isinstance(contents, list):
231
+ raise McpResourceNormalizationError("resources/read result.contents must be an array.")
232
+ normalized: list[McpReadResourceContent] = []
233
+ text_parts: list[str] = []
234
+ for index, raw_item in enumerate(contents):
235
+ payload = _require_object(raw_item, context=f"resources/read contents[{index}]")
236
+ item_uri = _optional_string(
237
+ payload.get("uri"), context=f"resources/read contents[{index}].uri"
238
+ )
239
+ resolved_uri = item_uri or expected_uri
240
+ if resolved_uri != expected_uri:
241
+ raise McpResourceNormalizationError(
242
+ f"resources/read contents[{index}].uri must match the requested resource URI."
243
+ )
244
+ mime_type = _optional_string(
245
+ payload.get("mimeType"),
246
+ context=f"resources/read contents[{index}].mimeType",
247
+ )
248
+ has_text = "text" in payload
249
+ has_blob = "blob" in payload
250
+ if has_text and has_blob:
251
+ raise McpResourceNormalizationError(
252
+ f"resources/read contents[{index}] cannot include both text and blob."
253
+ )
254
+ if not has_text and not has_blob:
255
+ raise McpResourceNormalizationError(
256
+ f"resources/read contents[{index}] must include text or blob."
257
+ )
258
+ inline_text: str | None = None
259
+ content_summary: str
260
+ if has_text:
261
+ raw_text = payload.get("text")
262
+ if not isinstance(raw_text, str):
263
+ raise McpResourceNormalizationError(
264
+ f"resources/read contents[{index}].text must be a string."
265
+ )
266
+ inline_text, content_summary = _inline_text_payload(text=raw_text, mime_type=mime_type)
267
+ if inline_text is not None:
268
+ text_parts.append(inline_text)
269
+ else:
270
+ raw_blob = payload.get("blob")
271
+ if not isinstance(raw_blob, str):
272
+ raise McpResourceNormalizationError(
273
+ f"resources/read contents[{index}].blob must be a string."
274
+ )
275
+ blob_summary = f"blob omitted ({len(raw_blob)} chars)"
276
+ if mime_type:
277
+ blob_summary = f"{mime_type} {blob_summary}"
278
+ content_summary = blob_summary
279
+ normalized.append(
280
+ McpReadResourceContent(
281
+ uri=resolved_uri,
282
+ mime_type=mime_type,
283
+ content_summary=content_summary,
284
+ text=inline_text,
285
+ raw_payload=dict(payload),
286
+ )
287
+ )
288
+ mime_type = next((item.mime_type for item in normalized if item.mime_type), None)
289
+ return McpReadResourceResult(
290
+ contents=tuple(normalized),
291
+ content_summary=", ".join(item.content_summary for item in normalized) or "no content",
292
+ text="\n\n".join(text_parts),
293
+ mime_type=mime_type,
294
+ raw_payload=dict(result),
295
+ )