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,739 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from collections.abc import Callable, Mapping
5
+ from typing import TYPE_CHECKING, Any
6
+ from urllib.parse import quote
7
+
8
+ import httpx
9
+
10
+ from ..branding import env_get
11
+ from ..error_text import sanitize_error_text_for_output
12
+ from ..provider_telemetry import ProviderCallTelemetryRecorder
13
+ from .metadata import (
14
+ GEMINI_INTERACTIONS_PROVIDER_METADATA_KEY,
15
+ ProviderRouteIdentity,
16
+ build_provider_route_identity,
17
+ canonicalize_extra_headers,
18
+ credential_scope_fingerprint,
19
+ gate_messages_for_provider_route,
20
+ merge_canonical_headers,
21
+ stamp_response_for_route,
22
+ )
23
+ from .provider_limits import (
24
+ DEFAULT_PROVIDER_CONCURRENCY_CAPS,
25
+ ProviderRetrySettings,
26
+ best_effort_provider_key,
27
+ run_provider_limited_call,
28
+ )
29
+ from .request_plan import LLMRequestPlan
30
+ from .request_shape import build_request_shape_report
31
+ from .temperature_compat import documented_temperature_omit_reason
32
+ from .types import (
33
+ InputTokenCount,
34
+ LLMError,
35
+ LLMResponse,
36
+ LLMUsage,
37
+ ReasoningOutput,
38
+ ReasoningOutputKind,
39
+ UsageConfidence,
40
+ UsageContract,
41
+ )
42
+
43
+ if TYPE_CHECKING:
44
+ from ..config import AppConfig
45
+
46
+ GEMINI_INTERACTIONS_EXPERIMENT_ENV = "ALYSIS_EXPERIMENTAL_GEMINI_INTERACTIONS"
47
+ GEMINI_INTERACTIONS_CONFIG_FLAG = "experimental_gemini_interactions_enabled"
48
+ _INTERACTIONS_API_REVISION = "2026-05-20"
49
+ GEMINI_INTERACTIONS_ROUTE_REVISION = _INTERACTIONS_API_REVISION
50
+
51
+
52
+ def gemini_interactions_enabled(cfg: AppConfig | None = None) -> bool:
53
+ """Return whether the experimental Gemini Interactions protocol is explicitly enabled."""
54
+ env_value = str(env_get(GEMINI_INTERACTIONS_EXPERIMENT_ENV, "") or "").strip().lower()
55
+ if env_value in {"1", "true", "yes", "on"}:
56
+ return True
57
+ if env_value in {"0", "false", "no", "off"}:
58
+ return False
59
+ return bool(getattr(cfg, GEMINI_INTERACTIONS_CONFIG_FLAG, False))
60
+
61
+
62
+ def gemini_interactions_disabled_message() -> str:
63
+ return (
64
+ "protocol='gemini_interactions' is experimental and disabled by default. "
65
+ f"Set {GEMINI_INTERACTIONS_EXPERIMENT_ENV}=1 or run "
66
+ f"`alysis config set {GEMINI_INTERACTIONS_CONFIG_FLAG} true` to enable the "
67
+ "text-only prototype. Gemini GenerateContent remains the stable native Gemini protocol."
68
+ )
69
+
70
+
71
+ class GeminiInteractionsClient:
72
+ """Minimal text-only prototype for Google's beta Gemini Interactions API."""
73
+
74
+ usage_contract = UsageContract(
75
+ response_usage_confidence=UsageConfidence.AUTHORITATIVE,
76
+ input_token_count_strategy="gemini_count_tokens_projection",
77
+ )
78
+ usage_counts_authoritative = usage_contract.response_usage_authoritative
79
+ supports_tool_calling = False
80
+ supports_forced_tool_choice = False
81
+
82
+ def __init__(
83
+ self,
84
+ *,
85
+ base_url: str,
86
+ api_key: str,
87
+ model: str,
88
+ timeout_s: float = 60.0,
89
+ temperature: float = 0.2,
90
+ prompt_cache_key: str | None = None,
91
+ prompt_cache_retention: str | None = None,
92
+ prompt_cache_policy_metadata: Mapping[str, Any] | None = None,
93
+ enable_thinking: bool | None = None,
94
+ reasoning_effort: str | None = None,
95
+ transport: httpx.BaseTransport | None = None,
96
+ extra_headers: dict[str, str] | None = None,
97
+ provider_key: str | None = None,
98
+ provider_concurrency_caps: dict[str, int] | None = None,
99
+ provider_retry_settings: ProviderRetrySettings | None = None,
100
+ provider_sleep_fn: Callable[[float], None] | None = None,
101
+ provider_random_fn: Callable[[], float] | None = None,
102
+ usage_contract: UsageContract | None = None,
103
+ route_identity: ProviderRouteIdentity | None = None,
104
+ ) -> None:
105
+ self.base_url = _gemini_interactions_base_url(base_url)
106
+ self.api_key = api_key
107
+ self.model = model
108
+ self.timeout_s = timeout_s
109
+ self.temperature = temperature
110
+ self.prompt_cache_key = str(prompt_cache_key or "").strip() or None
111
+ self.prompt_cache_retention = str(prompt_cache_retention or "").strip() or None
112
+ self.prompt_cache_policy_metadata = (
113
+ copy.deepcopy(dict(prompt_cache_policy_metadata))
114
+ if isinstance(prompt_cache_policy_metadata, Mapping)
115
+ else None
116
+ )
117
+ self.enable_thinking = enable_thinking
118
+ self.reasoning_effort = str(reasoning_effort or "").strip().lower() or None
119
+ self._transport = transport
120
+ self.extra_headers = canonicalize_extra_headers(extra_headers)
121
+ self.provider_key = str(provider_key or "").strip() or None
122
+ self.route_identity = route_identity or build_provider_route_identity(
123
+ protocol="gemini_interactions",
124
+ base_url=self.base_url,
125
+ provider_key=self.provider_key,
126
+ model=self.model,
127
+ credential_scope=credential_scope_fingerprint(self.api_key),
128
+ routing_headers=self.extra_headers,
129
+ protocol_revision=GEMINI_INTERACTIONS_ROUTE_REVISION,
130
+ )
131
+ self.provider_concurrency_caps = dict(
132
+ DEFAULT_PROVIDER_CONCURRENCY_CAPS
133
+ if provider_concurrency_caps is None
134
+ else provider_concurrency_caps
135
+ )
136
+ self.provider_retry_settings = provider_retry_settings or ProviderRetrySettings()
137
+ self._provider_sleep_fn = provider_sleep_fn
138
+ self._provider_random_fn = provider_random_fn
139
+ self.usage_contract = usage_contract or type(self).usage_contract
140
+ self.usage_counts_authoritative = self.usage_contract.response_usage_authoritative
141
+ self._input_token_count_available: bool | None = None
142
+ self._thinking_summaries_supported: bool | None = None
143
+
144
+ def _headers(self) -> dict[str, str]:
145
+ return merge_canonical_headers(
146
+ {
147
+ "Api-Revision": _INTERACTIONS_API_REVISION,
148
+ "Content-Type": "application/json",
149
+ "User-Agent": "alysis-code/0.1.0",
150
+ "x-goog-api-key": self.api_key,
151
+ },
152
+ self.extra_headers,
153
+ )
154
+
155
+ @staticmethod
156
+ def _llm_error_from_response(response: httpx.Response) -> LLMError:
157
+ try:
158
+ data = response.json()
159
+ except Exception:
160
+ body = response.text
161
+ if len(body) > 1000:
162
+ body = body[:1000] + "...(truncated)"
163
+ return LLMError(
164
+ sanitize_error_text_for_output(
165
+ f"Gemini Interactions error {response.status_code}: {body}"
166
+ )
167
+ )
168
+ message = _extract_error_message(data)
169
+ if message:
170
+ return LLMError(
171
+ sanitize_error_text_for_output(
172
+ f"Gemini Interactions error {response.status_code}: {message}"
173
+ )
174
+ )
175
+ return LLMError(
176
+ sanitize_error_text_for_output(
177
+ f"Gemini Interactions error {response.status_code}: {data!r}"
178
+ )
179
+ )
180
+
181
+ def count_input_tokens(
182
+ self,
183
+ *,
184
+ messages: list[dict[str, Any]],
185
+ tools: list[dict[str, Any]] | None = None,
186
+ tool_choice: Any | None = None,
187
+ ) -> InputTokenCount | None:
188
+ if self._input_token_count_available is False or tools or tool_choice is not None:
189
+ return None
190
+ messages = gate_messages_for_provider_route(messages, self.route_identity)
191
+ prompt = _last_user_text(messages)
192
+ if not prompt:
193
+ return None
194
+ _validate_text_only_single_turn_history(messages)
195
+ encoded_model = quote(self.model, safe="")
196
+ url = f"{self.base_url}/models/{encoded_model}:countTokens"
197
+ payload = {
198
+ "contents": [{"role": "user", "parts": [{"text": prompt}]}],
199
+ }
200
+
201
+ def _send_request() -> InputTokenCount | None:
202
+ try:
203
+ with httpx.Client(timeout=self.timeout_s, transport=self._transport) as client:
204
+ response = client.post(url, headers=self._headers(), json=payload)
205
+ except httpx.HTTPError as exc:
206
+ raise LLMError(
207
+ "Gemini input token count request failed: "
208
+ f"{sanitize_error_text_for_output(exc)}"
209
+ ) from exc
210
+ if response.status_code in {404, 405, 501}:
211
+ self._input_token_count_available = False
212
+ return None
213
+ if response.status_code >= 400:
214
+ raise self._llm_error_from_response(response)
215
+ try:
216
+ data = response.json()
217
+ except Exception as exc: # noqa: BLE001
218
+ raise LLMError("Gemini input token count returned non-JSON response") from exc
219
+ count = _int_or_none(data.get("totalTokens") if isinstance(data, dict) else None)
220
+ if count is None or count < 0:
221
+ raise LLMError("Gemini input token count response omitted totalTokens")
222
+ self._input_token_count_available = True
223
+ return InputTokenCount(
224
+ input_tokens=count,
225
+ confidence=UsageConfidence.REPORTED,
226
+ raw_provider_usage=copy.deepcopy(data),
227
+ )
228
+
229
+ return run_provider_limited_call(
230
+ call=_send_request,
231
+ provider_key=self.provider_key,
232
+ provider_concurrency_caps=self.provider_concurrency_caps,
233
+ retry_settings=self.provider_retry_settings,
234
+ operation="gemini_interactions_count_input_tokens",
235
+ sleep_fn=self._provider_sleep_fn,
236
+ random_fn=self._provider_random_fn,
237
+ retry_deadline_allows=getattr(self, "_provider_retry_deadline_allows", None),
238
+ )
239
+
240
+ def chat(
241
+ self,
242
+ *,
243
+ messages: list[dict[str, Any]],
244
+ tools: list[dict[str, Any]] | None = None,
245
+ tool_choice: Any | None = None,
246
+ response_format: dict[str, Any] | None = None,
247
+ stream: bool = False,
248
+ on_text_delta: Callable[[str], None] | None = None,
249
+ on_reasoning_delta: Callable[[str], None] | None = None,
250
+ temperature: float | None = None,
251
+ max_tokens: int | None = None,
252
+ ) -> LLMResponse:
253
+ del on_text_delta
254
+ messages = gate_messages_for_provider_route(messages, self.route_identity)
255
+ if stream:
256
+ raise LLMError("Gemini Interactions prototype does not support streaming yet")
257
+ if tools:
258
+ raise LLMError("Gemini Interactions prototype does not support tools yet")
259
+ if tool_choice is not None:
260
+ raise LLMError("Gemini Interactions prototype does not support tool_choice yet")
261
+ if response_format is not None:
262
+ raise LLMError("Gemini Interactions prototype does not support response_format yet")
263
+ if self.prompt_cache_key or self.prompt_cache_retention:
264
+ raise LLMError("Gemini Interactions does not support prompt_cache_key settings")
265
+ if self.enable_thinking is not None or self.reasoning_effort:
266
+ raise LLMError("Gemini Interactions prototype does not support thinking settings yet")
267
+
268
+ prompt = _last_user_text(messages)
269
+ if not prompt:
270
+ raise LLMError("Gemini Interactions prototype requires a non-empty user message")
271
+ _validate_text_only_single_turn_history(messages)
272
+
273
+ payload: dict[str, Any] = {
274
+ "input": prompt,
275
+ "model": self.model,
276
+ }
277
+ temperature_omit_reason = documented_temperature_omit_reason(self.model)
278
+ generation_config: dict[str, Any] = {}
279
+ if temperature_omit_reason is None:
280
+ if temperature is not None:
281
+ generation_config["temperature"] = float(temperature)
282
+ elif self.temperature is not None:
283
+ generation_config["temperature"] = self.temperature
284
+ if max_tokens is not None:
285
+ generation_config["max_output_tokens"] = int(max_tokens)
286
+ if on_reasoning_delta is not None and self._thinking_summaries_supported is not False:
287
+ generation_config["thinking_summaries"] = "auto"
288
+ if generation_config:
289
+ payload["generation_config"] = generation_config
290
+ original_payload = copy.deepcopy(payload)
291
+ request_plan = LLMRequestPlan.from_chat_args(
292
+ messages=messages,
293
+ tools=tools,
294
+ tool_choice=tool_choice,
295
+ response_format=response_format,
296
+ stream=stream,
297
+ temperature=temperature,
298
+ max_tokens=max_tokens,
299
+ )
300
+
301
+ def _request_plan_metadata(
302
+ current_payload: dict[str, Any],
303
+ *,
304
+ input_mode: str,
305
+ fallback_used: bool = False,
306
+ ) -> dict[str, Any]:
307
+ extra: dict[str, Any] = {}
308
+ if temperature_omit_reason is not None:
309
+ extra.update(
310
+ {
311
+ "temperature_omitted": True,
312
+ "temperature_omit_reason": temperature_omit_reason,
313
+ }
314
+ )
315
+ if fallback_used:
316
+ extra["fallback_used"] = True
317
+ return request_plan.request_plan_metadata(
318
+ input_mode=input_mode,
319
+ continuation_strategy="full_replay",
320
+ provider_payload=original_payload,
321
+ sent_provider_payload=current_payload,
322
+ cache_policy_metadata=self.prompt_cache_policy_metadata,
323
+ extra=extra or None,
324
+ )
325
+
326
+ request_plan_metadata = _request_plan_metadata(payload, input_mode="full")
327
+
328
+ provider_key = self.provider_key or best_effort_provider_key(
329
+ base_url=self.base_url,
330
+ model=self.model,
331
+ )
332
+ telemetry = ProviderCallTelemetryRecorder(
333
+ provider_key=provider_key,
334
+ protocol="gemini_interactions",
335
+ model=self.model,
336
+ base_url=self.base_url,
337
+ stream=False,
338
+ tools=tools,
339
+ web_search_mode="off",
340
+ web_search_adapter="gemini_grounding",
341
+ native_web_search=False,
342
+ cache_policy=self.prompt_cache_policy_metadata,
343
+ request_plan=request_plan_metadata,
344
+ request_shape=build_request_shape_report(
345
+ messages=messages,
346
+ tools=tools,
347
+ cache_policy=self.prompt_cache_policy_metadata,
348
+ provider_payload=payload,
349
+ ),
350
+ operation="gemini_interactions_chat",
351
+ )
352
+ telemetry_on_reasoning_delta = telemetry.wrap_reasoning_delta(on_reasoning_delta)
353
+
354
+ def _send_request() -> LLMResponse:
355
+ nonlocal request_plan_metadata
356
+ thinking_summaries_fallback_used = False
357
+ try:
358
+ with httpx.Client(timeout=self.timeout_s, transport=self._transport) as client:
359
+ while True:
360
+ response = client.post(
361
+ f"{self.base_url}/interactions",
362
+ headers=self._headers(),
363
+ json=payload,
364
+ )
365
+ if response.status_code < 400:
366
+ break
367
+ if (
368
+ not thinking_summaries_fallback_used
369
+ and _thinking_summaries_request_rejected(response)
370
+ and _remove_thinking_summaries_request(payload)
371
+ ):
372
+ thinking_summaries_fallback_used = True
373
+ self._thinking_summaries_supported = False
374
+ request_plan_metadata = _request_plan_metadata(
375
+ payload,
376
+ input_mode="retry_without_thinking_summaries",
377
+ fallback_used=True,
378
+ )
379
+ telemetry.set_request_plan(request_plan_metadata)
380
+ telemetry.set_request_shape(
381
+ build_request_shape_report(
382
+ messages=messages,
383
+ tools=tools,
384
+ cache_policy=self.prompt_cache_policy_metadata,
385
+ provider_payload=payload,
386
+ )
387
+ )
388
+ continue
389
+ raise self._llm_error_from_response(response)
390
+ except Exception as exc: # noqa: BLE001
391
+ if isinstance(exc, LLMError):
392
+ raise
393
+ raise LLMError(
394
+ f"Gemini Interactions request failed: {sanitize_error_text_for_output(exc)}"
395
+ ) from exc
396
+ return self._parse_chat_response(
397
+ response,
398
+ request_plan_metadata=request_plan_metadata,
399
+ on_reasoning_delta=telemetry_on_reasoning_delta,
400
+ )
401
+
402
+ return stamp_response_for_route(
403
+ telemetry.run(
404
+ lambda: run_provider_limited_call(
405
+ call=_send_request,
406
+ provider_key=provider_key,
407
+ provider_concurrency_caps=self.provider_concurrency_caps,
408
+ retry_settings=self.provider_retry_settings,
409
+ operation="gemini_interactions_chat",
410
+ sleep_fn=self._provider_sleep_fn,
411
+ random_fn=self._provider_random_fn,
412
+ on_retry=telemetry.on_retry,
413
+ on_retry_event=getattr(
414
+ self,
415
+ "_provider_retry_event_observer",
416
+ None,
417
+ ),
418
+ retry_deadline_allows=getattr(self, "_provider_retry_deadline_allows", None),
419
+ )
420
+ ),
421
+ self.route_identity,
422
+ )
423
+
424
+ def _parse_chat_response(
425
+ self,
426
+ response: httpx.Response,
427
+ request_plan_metadata: dict[str, Any] | None = None,
428
+ on_reasoning_delta: Callable[[str], None] | None = None,
429
+ ) -> LLMResponse:
430
+ try:
431
+ data = response.json()
432
+ except Exception as exc:
433
+ raise LLMError(f"Gemini Interactions returned invalid JSON: {exc}") from exc
434
+ if not isinstance(data, dict):
435
+ raise LLMError("Gemini Interactions returned non-object JSON")
436
+ text = _interaction_output_text(data).strip()
437
+ reasoning = _interaction_thought_summaries(data)
438
+ actions = data.get("actions")
439
+ if isinstance(actions, list) and actions:
440
+ raise LLMError(
441
+ "Gemini Interactions returned actions, but this prototype only supports text output"
442
+ )
443
+ metadata: dict[str, Any] = {}
444
+ interaction_id = data.get("id") or data.get("interaction_id") or data.get("interactionId")
445
+ if isinstance(interaction_id, str) and interaction_id:
446
+ metadata["interaction_id"] = interaction_id
447
+ response_payload = data.get("response")
448
+ if isinstance(response_payload, dict):
449
+ metadata["response"] = copy.deepcopy(response_payload)
450
+ if request_plan_metadata:
451
+ metadata["request_plan"] = copy.deepcopy(request_plan_metadata)
452
+
453
+ parsed_response = LLMResponse(
454
+ content=text,
455
+ tool_calls=[],
456
+ raw=data,
457
+ response_model=_response_model(data) or self.model,
458
+ usage=_usage_from_response(data),
459
+ provider_metadata=(
460
+ {GEMINI_INTERACTIONS_PROVIDER_METADATA_KEY: metadata} if metadata else None
461
+ ),
462
+ reasoning=reasoning,
463
+ )
464
+ if on_reasoning_delta is not None:
465
+ for item in reasoning:
466
+ on_reasoning_delta(item.text)
467
+ return parsed_response
468
+
469
+
470
+ def _thinking_summaries_request_rejected(response: httpx.Response) -> bool:
471
+ if response.status_code not in {400, 422}:
472
+ return False
473
+ detail = response.text.casefold()
474
+ names_summary_field = any(
475
+ marker in detail for marker in ("thinking_summaries", "thinking summaries")
476
+ )
477
+ rejects_field = any(
478
+ marker in detail
479
+ for marker in (
480
+ "unsupported",
481
+ "not supported",
482
+ "unknown field",
483
+ "unknown name",
484
+ "unrecognized",
485
+ "invalid field",
486
+ "invalid argument",
487
+ )
488
+ )
489
+ return names_summary_field and rejects_field
490
+
491
+
492
+ def _remove_thinking_summaries_request(payload: dict[str, Any]) -> bool:
493
+ generation_config = payload.get("generation_config")
494
+ if not isinstance(generation_config, dict) or "thinking_summaries" not in generation_config:
495
+ return False
496
+ generation_config.pop("thinking_summaries", None)
497
+ if not generation_config:
498
+ payload.pop("generation_config", None)
499
+ return True
500
+
501
+
502
+ def _gemini_interactions_base_url(base_url: str) -> str:
503
+ normalized = str(base_url or "").strip().rstrip("/")
504
+ if normalized.endswith("/openai"):
505
+ normalized = normalized.removesuffix("/openai")
506
+ return normalized or "https://generativelanguage.googleapis.com/v1beta"
507
+
508
+
509
+ def _last_user_text(messages: list[dict[str, Any]]) -> str:
510
+ for message in reversed(messages):
511
+ if not isinstance(message, dict):
512
+ continue
513
+ if str(message.get("role") or "").strip() != "user":
514
+ continue
515
+ text = _content_text(message.get("content"))
516
+ if text.strip():
517
+ return text.strip()
518
+ return ""
519
+
520
+
521
+ def _content_text(value: Any) -> str:
522
+ if value is None:
523
+ return ""
524
+ if isinstance(value, str):
525
+ return value
526
+ if isinstance(value, list):
527
+ return "".join(_content_text(item) for item in value)
528
+ if isinstance(value, dict):
529
+ text = value.get("text") or value.get("content")
530
+ return text if isinstance(text, str) else ""
531
+ return str(value)
532
+
533
+
534
+ def _interaction_output_text(data: dict[str, Any]) -> str:
535
+ step_text = _interaction_model_output_text(data)
536
+ if step_text:
537
+ return step_text
538
+ output_text = data.get("output_text")
539
+ if isinstance(output_text, str):
540
+ return output_text
541
+ output = data.get("output")
542
+ if isinstance(output, str):
543
+ return output
544
+ text = _collect_text(output)
545
+ if text:
546
+ return text
547
+ text = _collect_text(data.get("outputs"))
548
+ if text:
549
+ return text
550
+ return _collect_text(data.get("response"))
551
+
552
+
553
+ def _interaction_model_output_text(data: dict[str, Any]) -> str:
554
+ steps = data.get("steps")
555
+ if not isinstance(steps, list):
556
+ return ""
557
+ parts: list[str] = []
558
+ for step in steps:
559
+ if not isinstance(step, dict) or step.get("type") != "model_output":
560
+ continue
561
+ content = step.get("content")
562
+ if not isinstance(content, list):
563
+ continue
564
+ for block in content:
565
+ if not isinstance(block, dict) or block.get("type") != "text":
566
+ continue
567
+ text = block.get("text")
568
+ if isinstance(text, str):
569
+ parts.append(text)
570
+ return "".join(parts)
571
+
572
+
573
+ def _interaction_thought_summaries(data: dict[str, Any]) -> tuple[ReasoningOutput, ...]:
574
+ steps = data.get("steps")
575
+ if not isinstance(steps, list):
576
+ return ()
577
+ summaries: list[ReasoningOutput] = []
578
+ for step in steps:
579
+ if not isinstance(step, dict) or step.get("type") != "thought":
580
+ continue
581
+ summary = step.get("summary")
582
+ if not isinstance(summary, list):
583
+ continue
584
+ for block in summary:
585
+ if not isinstance(block, dict) or block.get("type") != "text":
586
+ continue
587
+ text = block.get("text")
588
+ if isinstance(text, str) and text.strip():
589
+ summaries.append(
590
+ ReasoningOutput(
591
+ text=text,
592
+ kind=ReasoningOutputKind.SUMMARY,
593
+ provider="gemini",
594
+ )
595
+ )
596
+ return tuple(summaries)
597
+
598
+
599
+ def _collect_text(value: Any) -> str:
600
+ if value is None:
601
+ return ""
602
+ if isinstance(value, str):
603
+ return value
604
+ if isinstance(value, list):
605
+ return "".join(_collect_text(item) for item in value)
606
+ if isinstance(value, dict):
607
+ item_type = str(value.get("type") or "").strip().lower()
608
+ if item_type in {"thought", "thought_summary", "thought_signature"}:
609
+ return ""
610
+ parts: list[str] = []
611
+ text = value.get("text")
612
+ if isinstance(text, str):
613
+ parts.append(text)
614
+ for key in ("content", "parts", "candidates", "output", "response"):
615
+ child = value.get(key)
616
+ if child is not None:
617
+ parts.append(_collect_text(child))
618
+ return "".join(parts)
619
+ return ""
620
+
621
+
622
+ def _usage_from_response(data: dict[str, Any]) -> LLMUsage | None:
623
+ usage = data.get("usageMetadata") or data.get("usage_metadata") or data.get("usage")
624
+ if not isinstance(usage, dict):
625
+ return None
626
+ prompt_tokens = _int_or_none(
627
+ usage.get("promptTokenCount")
628
+ or usage.get("prompt_tokens")
629
+ or usage.get("input_tokens")
630
+ or usage.get("total_input_tokens")
631
+ )
632
+ completion_tokens = _int_or_none(
633
+ usage.get("candidatesTokenCount")
634
+ or usage.get("completion_tokens")
635
+ or usage.get("output_tokens")
636
+ or usage.get("total_output_tokens")
637
+ )
638
+ tool_use_prompt_tokens = _int_or_none(
639
+ usage.get("toolUsePromptTokenCount")
640
+ or usage.get("tool_use_prompt_token_count")
641
+ or usage.get("tool_use_prompt_tokens")
642
+ )
643
+ thoughts_tokens = _int_or_none(
644
+ usage.get("thoughtsTokenCount")
645
+ or usage.get("thoughts_token_count")
646
+ or usage.get("thinking_tokens")
647
+ or usage.get("total_thought_tokens")
648
+ )
649
+ total_tokens = _int_or_none(usage.get("totalTokenCount") or usage.get("total_tokens"))
650
+ cached_tokens = _int_or_none(
651
+ usage.get("cachedContentTokenCount")
652
+ or usage.get("cached_content_token_count")
653
+ or usage.get("cached_prompt_tokens")
654
+ )
655
+ # Gemini reports tool-use prompts separately from the ordinary prompt and
656
+ # candidates separately from thinking. Fold them into their billing sides;
657
+ # the untouched provider payload retains the original breakdown.
658
+ if tool_use_prompt_tokens:
659
+ prompt_tokens = (prompt_tokens or 0) + tool_use_prompt_tokens
660
+ if thoughts_tokens:
661
+ completion_tokens = (completion_tokens or 0) + thoughts_tokens
662
+ if (
663
+ prompt_tokens is None
664
+ and completion_tokens is None
665
+ and total_tokens is None
666
+ and cached_tokens is None
667
+ ):
668
+ return None
669
+ input_tokens_uncached = None
670
+ if prompt_tokens is not None and cached_tokens is not None:
671
+ input_tokens_uncached = max(0, prompt_tokens - cached_tokens)
672
+ return LLMUsage(
673
+ prompt_tokens=prompt_tokens,
674
+ completion_tokens=completion_tokens,
675
+ total_tokens=total_tokens,
676
+ cached_prompt_tokens=cached_tokens,
677
+ input_tokens_uncached=input_tokens_uncached,
678
+ cache_read_input_tokens=cached_tokens,
679
+ reasoning_tokens=thoughts_tokens,
680
+ raw_provider_usage=copy.deepcopy(usage),
681
+ )
682
+
683
+
684
+ def _int_or_none(value: Any) -> int | None:
685
+ try:
686
+ return int(value)
687
+ except (TypeError, ValueError):
688
+ return None
689
+
690
+
691
+ def _response_model(data: dict[str, Any]) -> str | None:
692
+ for key in ("modelVersion", "model_version", "model"):
693
+ value = data.get(key)
694
+ if isinstance(value, str) and value.strip():
695
+ return value.strip()
696
+ response = data.get("response")
697
+ if isinstance(response, dict):
698
+ for key in ("modelVersion", "model_version", "model"):
699
+ value = response.get(key)
700
+ if isinstance(value, str) and value.strip():
701
+ return value.strip()
702
+ return None
703
+
704
+
705
+ def _validate_text_only_single_turn_history(messages: list[dict[str, Any]]) -> None:
706
+ user_message_count = 0
707
+ for message in messages:
708
+ if not isinstance(message, dict):
709
+ continue
710
+ role = str(message.get("role") or "").strip()
711
+ if role in {"system", "developer"}:
712
+ raise LLMError(
713
+ "Gemini Interactions prototype does not support system/developer instructions yet"
714
+ )
715
+ if role == "user":
716
+ user_message_count += 1
717
+ continue
718
+ raise LLMError(
719
+ "Gemini Interactions prototype supports only a single text-only user turn; "
720
+ "server-side previous_interaction_id continuation is not implemented yet"
721
+ )
722
+ if user_message_count != 1:
723
+ raise LLMError(
724
+ "Gemini Interactions prototype supports exactly one user message until "
725
+ "previous_interaction_id continuation is implemented"
726
+ )
727
+
728
+
729
+ def _extract_error_message(data: Any) -> str:
730
+ if isinstance(data, dict):
731
+ error = data.get("error")
732
+ if isinstance(error, dict):
733
+ message = error.get("message")
734
+ if isinstance(message, str) and message.strip():
735
+ return message.strip()
736
+ message = data.get("message")
737
+ if isinstance(message, str) and message.strip():
738
+ return message.strip()
739
+ return ""