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,466 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass, replace
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..config import AppConfig
10
+ from ..model_registry import ModelRegistry
11
+ from .models import AssetError, AssetRecord, ComprehensionRecord
12
+ from .plan_binding import task_asset_briefing
13
+ from .surface import AssetSurface, SurfaceComprehensionStatus
14
+
15
+ LOGGER = logging.getLogger(__name__)
16
+ _TRUNCATION_SUFFIX = "... (truncated for context budget)"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class PlannerAssetsContext:
21
+ text_block: str
22
+ inline_image_paths: list[str]
23
+ pending_asset_ids: list[str]
24
+ failed_asset_ids: list[str]
25
+ minimal_asset_ids: list[str]
26
+ referenced_asset_ids: list[str]
27
+
28
+
29
+ class AssetReadinessPolicy(Enum):
30
+ BLOCK = "block"
31
+ SOFT = "soft"
32
+ PARTIAL = "partial"
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class AssetReadinessReport:
37
+ ready: list[str]
38
+ pending: list[str]
39
+ failed: list[str]
40
+ minimal: list[str]
41
+ timed_out: list[str]
42
+ policy: AssetReadinessPolicy
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class AssetReferenceReport:
47
+ deleted_referenced: list[tuple[str, str]]
48
+ missing_referenced: list[tuple[str, str]]
49
+ pinned_added: list[str]
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class PlannerAssetsBundle:
54
+ context: PlannerAssetsContext
55
+ inline_image_paths: list[str]
56
+ readiness_report: AssetReadinessReport
57
+ reference_report: AssetReferenceReport
58
+
59
+
60
+ def build_planner_assets_block(
61
+ surface: AssetSurface,
62
+ *,
63
+ include_pending: bool = False,
64
+ ) -> PlannerAssetsContext:
65
+ entries = sorted(
66
+ surface.list_assets(include_deleted=False),
67
+ key=lambda entry: (not entry.record.pinned, entry.record.id),
68
+ )
69
+ max_chars = max(int(surface.cfg.assets.planner.max_chars_per_asset), len(_TRUNCATION_SUFFIX))
70
+ lines = [
71
+ "## Available Assets",
72
+ "",
73
+ "The user has provided the following resources for this run. Use them when they help. "
74
+ "You may inspect text assets via `asset_read` and request re-comprehension via "
75
+ "`asset_inspect`. Do not invent asset ids.",
76
+ ]
77
+ inline_image_paths: list[str] = []
78
+ pending: list[str] = []
79
+ failed: list[str] = []
80
+ minimal: list[str] = []
81
+
82
+ if not entries:
83
+ lines.extend(["", "- (no assets attached)"])
84
+
85
+ for entry in entries:
86
+ record = entry.record
87
+ status = entry.comprehension_status
88
+ if status in {"pending", "running"}:
89
+ pending.append(record.id)
90
+ if not include_pending:
91
+ continue
92
+ elif status == "failed":
93
+ failed.append(record.id)
94
+ elif status == "minimal":
95
+ minimal.append(record.id)
96
+ comprehension = surface.comprehension_for(record.id)
97
+ block = _asset_block(
98
+ record=record,
99
+ status=status,
100
+ comprehension=comprehension,
101
+ max_chars=max_chars,
102
+ )
103
+ lines.extend(["", block])
104
+ if (
105
+ record.kind == "image"
106
+ and status in {"ready", "minimal", "failed"}
107
+ and record.stored_path
108
+ ):
109
+ inline_image_paths.append(str((surface.run_paths.root / record.stored_path).resolve()))
110
+
111
+ context = PlannerAssetsContext(
112
+ text_block="\n".join(lines).rstrip(),
113
+ inline_image_paths=inline_image_paths,
114
+ pending_asset_ids=pending,
115
+ failed_asset_ids=failed,
116
+ minimal_asset_ids=minimal,
117
+ referenced_asset_ids=[],
118
+ )
119
+ LOGGER.info(
120
+ "planner_assets_block assets_total=%s ready=%s minimal=%s failed=%s pending=%s image_inline=%s",
121
+ len(entries),
122
+ len([entry for entry in entries if entry.comprehension_status == "ready"]),
123
+ len(minimal),
124
+ len(failed),
125
+ len(pending),
126
+ len(inline_image_paths),
127
+ )
128
+ return context
129
+
130
+
131
+ def ensure_planner_asset_readiness(
132
+ surface: AssetSurface,
133
+ *,
134
+ policy: AssetReadinessPolicy,
135
+ timeout_seconds: float | None = None,
136
+ ) -> AssetReadinessReport:
137
+ timed_out: list[str] = []
138
+ _start_pending_comprehensions(surface)
139
+ if policy is AssetReadinessPolicy.BLOCK:
140
+ join_report = surface.join_pending(timeout_seconds=timeout_seconds)
141
+ timed_out = join_report.timed_out
142
+
143
+ report = _readiness_snapshot(surface=surface, policy=policy, timed_out=timed_out)
144
+ LOGGER.info(
145
+ "planner_assets_readiness policy=%s ready=%s pending=%s timed_out=%s",
146
+ policy.value,
147
+ len(report.ready),
148
+ len(report.pending),
149
+ len(report.timed_out),
150
+ )
151
+ if policy is AssetReadinessPolicy.BLOCK and (report.pending or report.timed_out):
152
+ offenders = list(dict.fromkeys([*report.pending, *report.timed_out]))
153
+ raise AssetError(
154
+ "Asset comprehension did not finish before planner readiness timeout: "
155
+ + ", ".join(offenders)
156
+ )
157
+ return report
158
+
159
+
160
+ def asset_reference_check(
161
+ plan: dict[str, Any] | None, surface: AssetSurface
162
+ ) -> AssetReferenceReport:
163
+ if not isinstance(plan, dict):
164
+ return AssetReferenceReport(deleted_referenced=[], missing_referenced=[], pinned_added=[])
165
+ records = {record.id: record for record in surface.index.records(include_deleted=True)}
166
+ referenced_ids: set[str] = set()
167
+ deleted: list[tuple[str, str]] = []
168
+ missing: list[tuple[str, str]] = []
169
+ for task in plan.get("tasks") or []:
170
+ if not isinstance(task, dict):
171
+ continue
172
+ task_id = str(task.get("id") or "").strip() or "(unknown)"
173
+ try:
174
+ briefing = task_asset_briefing(task)
175
+ except AssetError:
176
+ continue
177
+ if briefing is None:
178
+ continue
179
+ for entry in [*briefing.primary, *briefing.may_need]:
180
+ referenced_ids.add(entry.asset_id)
181
+ record = records.get(entry.asset_id)
182
+ if record is None:
183
+ missing.append((task_id, entry.asset_id))
184
+ elif record.deleted_at is not None:
185
+ deleted.append((task_id, entry.asset_id))
186
+ pinned_added = sorted(
187
+ record.id
188
+ for record in records.values()
189
+ if record.pinned and record.deleted_at is None and record.id not in referenced_ids
190
+ )
191
+ report = AssetReferenceReport(
192
+ deleted_referenced=list(dict.fromkeys(deleted)),
193
+ missing_referenced=list(dict.fromkeys(missing)),
194
+ pinned_added=pinned_added,
195
+ )
196
+ LOGGER.info(
197
+ "planner_assets_reference_check deleted_referenced=%s missing_referenced=%s pinned_added=%s",
198
+ len(report.deleted_referenced),
199
+ len(report.missing_referenced),
200
+ len(report.pinned_added),
201
+ )
202
+ return report
203
+
204
+
205
+ def build_planner_assets_bundle(
206
+ *,
207
+ cfg: AppConfig,
208
+ surface: AssetSurface,
209
+ plan: dict[str, Any] | None,
210
+ role_model: str,
211
+ model_registry: ModelRegistry,
212
+ ) -> PlannerAssetsBundle:
213
+ policy = AssetReadinessPolicy(str(cfg.assets.planner.readiness_policy))
214
+ readiness = ensure_planner_asset_readiness(
215
+ surface,
216
+ policy=policy,
217
+ timeout_seconds=float(cfg.assets.planner.readiness_timeout_seconds),
218
+ )
219
+ context = build_planner_assets_block(
220
+ surface,
221
+ include_pending=policy is AssetReadinessPolicy.PARTIAL,
222
+ )
223
+ reference_report = asset_reference_check(plan, surface)
224
+ referenced_ids = sorted(_collect_referenced_asset_ids_best_effort(plan or {}))
225
+ if reference_report.deleted_referenced or reference_report.missing_referenced:
226
+ context = replace(
227
+ context,
228
+ text_block=context.text_block + _drift_block(reference_report),
229
+ referenced_asset_ids=referenced_ids,
230
+ )
231
+ else:
232
+ context = replace(context, referenced_asset_ids=referenced_ids)
233
+ inline_paths: list[str] = []
234
+ if cfg.assets.planner.inline_images and _model_supports_vision(model_registry, role_model):
235
+ inline_paths = context.inline_image_paths[
236
+ : max(int(cfg.assets.planner.max_inline_images), 0)
237
+ ]
238
+ return PlannerAssetsBundle(
239
+ context=context,
240
+ inline_image_paths=inline_paths,
241
+ readiness_report=readiness,
242
+ reference_report=reference_report,
243
+ )
244
+
245
+
246
+ def _asset_block(
247
+ *,
248
+ record: AssetRecord,
249
+ status: SurfaceComprehensionStatus,
250
+ comprehension: ComprehensionRecord | None,
251
+ max_chars: int,
252
+ ) -> str:
253
+ header = f'### {record.id} - "{record.title}"'
254
+ if status in {"pending", "running"}:
255
+ return _truncate_block(
256
+ "\n".join(
257
+ [
258
+ header,
259
+ "- Status: comprehension pending - content not yet structured.",
260
+ f"- Kind: {_kind_label(record)}",
261
+ "- The planner should not rely on this asset until comprehension is ready.",
262
+ ]
263
+ ),
264
+ max_chars=max_chars,
265
+ )
266
+ if status == "minimal":
267
+ lines = [
268
+ header,
269
+ "- Status: minimal comprehension - only user-provided metadata is available.",
270
+ f"- Description: {record.description or '(none)'}",
271
+ f"- Kind: {_kind_label(record)}",
272
+ "- Treat as low-confidence context.",
273
+ ]
274
+ if record.kind == "image":
275
+ lines.append(
276
+ "- Visual/OCR note: no structured visual or OCR understanding is available; "
277
+ "ask the user or refresh after configuring a vision/OCR provider if the image matters."
278
+ )
279
+ return _truncate_block("\n".join(lines), max_chars=max_chars)
280
+ if status == "failed":
281
+ return _truncate_block(
282
+ "\n".join(
283
+ [
284
+ header,
285
+ "- Status: comprehension failed - ask the user about this asset if it matters.",
286
+ f"- Kind: {_kind_label(record)}",
287
+ f"- Error: {(comprehension.error if comprehension else '') or 'unknown'}",
288
+ ]
289
+ ),
290
+ max_chars=max_chars,
291
+ )
292
+ if comprehension is None:
293
+ return _truncate_block(
294
+ "\n".join(
295
+ [
296
+ header,
297
+ "- Status: comprehension unavailable.",
298
+ f"- Kind: {_kind_label(record)}",
299
+ ]
300
+ ),
301
+ max_chars=max_chars,
302
+ )
303
+ lines = [
304
+ header,
305
+ f"- Kind: {_kind_label(record)}",
306
+ "- Source: "
307
+ + _source_line(comprehension)
308
+ + f" - confidence: {_confidence_label(comprehension)}",
309
+ f"- Pinned: {'yes' if record.pinned else 'no'}",
310
+ f"- Summary: {comprehension.data.semantic_summary or '(none)'}",
311
+ ]
312
+ _append_entities(lines, comprehension)
313
+ _append_list(lines, "Stated facts", comprehension.data.stated_facts)
314
+ _append_list(lines, "Stated decisions", comprehension.data.stated_decisions)
315
+ _append_list(lines, "Stated constraints", comprehension.data.stated_constraints)
316
+ _append_list(lines, "Actionable signals", comprehension.data.actionable_signals)
317
+ _append_list(lines, "Open questions", comprehension.data.open_questions)
318
+ return _truncate_block("\n".join(lines), max_chars=max_chars)
319
+
320
+
321
+ def _source_line(comprehension: ComprehensionRecord) -> str:
322
+ lang = comprehension.detected_language or "unknown"
323
+ if comprehension.language_confidence is None:
324
+ return f"{comprehension.source} - detected language: {lang}"
325
+ return f"{comprehension.source} - detected language: {lang} ({comprehension.language_confidence:.2f})"
326
+
327
+
328
+ def _append_entities(lines: list[str], comprehension: ComprehensionRecord) -> None:
329
+ entities = []
330
+ for item in comprehension.data.key_entities[:12]:
331
+ if not isinstance(item, dict):
332
+ continue
333
+ entity_type = str(item.get("type") or "").strip()
334
+ value = str(item.get("value") or "").strip()
335
+ if entity_type and value:
336
+ entities.append(f"{entity_type}={value}")
337
+ if entities:
338
+ lines.append("- Key entities: " + ", ".join(entities))
339
+
340
+
341
+ def _append_list(lines: list[str], label: str, values: list[str]) -> None:
342
+ clean_values = [str(value).strip() for value in values if str(value).strip()]
343
+ if not clean_values:
344
+ return
345
+ lines.append(f"- {label}:")
346
+ for value in clean_values[:8]:
347
+ lines.append(f" - {value}")
348
+
349
+
350
+ def _kind_label(record: AssetRecord) -> str:
351
+ subtype = record.mime.split("/")[-1] if record.mime else "unknown"
352
+ return f"{record.kind} ({subtype}, {_format_size(record.size_bytes)})"
353
+
354
+
355
+ def _format_size(size_bytes: int) -> str:
356
+ size = float(max(size_bytes, 0))
357
+ for unit in ("B", "KB", "MB", "GB"):
358
+ if size < 1024 or unit == "GB":
359
+ return f"{int(size)} {unit}" if unit == "B" else f"{size:.0f} {unit}"
360
+ size /= 1024
361
+ return f"{size_bytes} B"
362
+
363
+
364
+ def _confidence_label(comprehension: ComprehensionRecord) -> str:
365
+ score = float(comprehension.confidence_modifier or 0.0)
366
+ if score >= 0.8:
367
+ return "high"
368
+ if score >= 0.55:
369
+ return "medium"
370
+ return "low"
371
+
372
+
373
+ def _truncate_block(text: str, *, max_chars: int) -> str:
374
+ if len(text) <= max_chars:
375
+ return text
376
+ limit = max(max_chars - len(_TRUNCATION_SUFFIX), 0)
377
+ return text[:limit].rstrip() + _TRUNCATION_SUFFIX
378
+
379
+
380
+ def _readiness_snapshot(
381
+ *,
382
+ surface: AssetSurface,
383
+ policy: AssetReadinessPolicy,
384
+ timed_out: list[str],
385
+ ) -> AssetReadinessReport:
386
+ ready: list[str] = []
387
+ pending: list[str] = []
388
+ failed: list[str] = []
389
+ minimal: list[str] = []
390
+ for entry in surface.list_assets(include_deleted=False):
391
+ status = entry.comprehension_status
392
+ if status == "ready":
393
+ ready.append(entry.record.id)
394
+ elif status in {"pending", "running"}:
395
+ pending.append(entry.record.id)
396
+ elif status == "failed":
397
+ failed.append(entry.record.id)
398
+ elif status == "minimal":
399
+ minimal.append(entry.record.id)
400
+ return AssetReadinessReport(
401
+ ready=ready,
402
+ pending=pending,
403
+ failed=failed,
404
+ minimal=minimal,
405
+ timed_out=timed_out,
406
+ policy=policy,
407
+ )
408
+
409
+
410
+ def _start_pending_comprehensions(surface: AssetSurface) -> list[str]:
411
+ started: list[str] = []
412
+ for entry in surface.list_assets(include_deleted=False):
413
+ if entry.comprehension_status != "pending":
414
+ continue
415
+ try:
416
+ surface.refresh_comprehension(entry.record.id, mode="async")
417
+ except AssetError as exc:
418
+ LOGGER.warning(
419
+ "planner_assets_readiness failed to start comprehension asset_id=%s: %s",
420
+ entry.record.id,
421
+ exc,
422
+ )
423
+ continue
424
+ started.append(entry.record.id)
425
+ return started
426
+
427
+
428
+ def _collect_referenced_asset_ids_best_effort(plan: dict[str, Any]) -> set[str]:
429
+ referenced: set[str] = set()
430
+ for task in plan.get("tasks") or []:
431
+ if not isinstance(task, dict):
432
+ continue
433
+ try:
434
+ briefing = task_asset_briefing(task)
435
+ except AssetError:
436
+ continue
437
+ if briefing is None:
438
+ continue
439
+ referenced.update(entry.asset_id for entry in briefing.primary)
440
+ referenced.update(entry.asset_id for entry in briefing.may_need)
441
+ return referenced
442
+
443
+
444
+ def _drift_block(report: AssetReferenceReport) -> str:
445
+ lines = ["", "", "## Plan-Asset Drift", ""]
446
+ if report.deleted_referenced:
447
+ lines.append("Deleted assets are still referenced by tasks:")
448
+ for task_id, asset_id in report.deleted_referenced:
449
+ lines.append(f"- {task_id}: {asset_id}")
450
+ if report.missing_referenced:
451
+ lines.append("Missing assets are still referenced by tasks:")
452
+ for task_id, asset_id in report.missing_referenced:
453
+ lines.append(f"- {task_id}: {asset_id}")
454
+ lines.append("Revise affected task asset_briefing entries before relying on them.")
455
+ return "\n".join(lines)
456
+
457
+
458
+ def _model_supports_vision(model_registry: ModelRegistry, role_model: str) -> bool:
459
+ try:
460
+ return bool(model_registry.get(role_model).supports_vision)
461
+ except Exception: # noqa: BLE001 - metadata policy handles hard failures elsewhere
462
+ return False
463
+
464
+
465
+ def absolute_asset_path(root: Path, stored_path: str) -> str:
466
+ return str((root / stored_path).resolve())
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from ..config import AppConfig
9
+ from ..forge import RunPaths
10
+ from ..llm.factory import make_llm_client
11
+ from ..model_registry import ModelRegistry
12
+ from .asset_read_core import perform_asset_read
13
+ from .models import AssetError, ComprehensionRecord
14
+ from .surface import AssetSurface
15
+
16
+ LOGGER = logging.getLogger(__name__)
17
+
18
+ ASSET_READ_TOOL: dict[str, Any] = {
19
+ "type": "function",
20
+ "function": {
21
+ "name": "asset_read",
22
+ "description": (
23
+ "Read the extracted text of an asset, optionally focused on a topic the planner cares about."
24
+ ),
25
+ "parameters": {
26
+ "type": "object",
27
+ "additionalProperties": False,
28
+ "required": ["asset_id"],
29
+ "properties": {
30
+ "asset_id": {"type": "string"},
31
+ "focus": {
32
+ "type": "string",
33
+ "description": (
34
+ "Optional topic to focus the extraction on. When provided, runs a small LLM "
35
+ "extraction pass to return only the relevant content."
36
+ ),
37
+ },
38
+ "max_chars": {
39
+ "type": "integer",
40
+ "description": "Cap on returned text. Default 8000.",
41
+ "minimum": 100,
42
+ "maximum": 32000,
43
+ },
44
+ },
45
+ },
46
+ },
47
+ }
48
+
49
+ ASSET_INSPECT_TOOL: dict[str, Any] = {
50
+ "type": "function",
51
+ "function": {
52
+ "name": "asset_inspect",
53
+ "description": "Request a re-comprehension of an asset using a specific angle or framing.",
54
+ "parameters": {
55
+ "type": "object",
56
+ "additionalProperties": False,
57
+ "required": ["asset_id"],
58
+ "properties": {
59
+ "asset_id": {"type": "string"},
60
+ "angle": {
61
+ "type": "string",
62
+ "description": (
63
+ "Optional framing for the re-comprehension, for example security perspective "
64
+ "or data-flow focus."
65
+ ),
66
+ },
67
+ },
68
+ },
69
+ },
70
+ }
71
+
72
+ PLANNER_ASSET_TOOLS: list[dict[str, Any]] = [ASSET_READ_TOOL, ASSET_INSPECT_TOOL]
73
+ _INSPECT_CAP_PER_ASSET = 3
74
+
75
+
76
+ class PlannerAssetToolRunner:
77
+ def __init__(
78
+ self,
79
+ *,
80
+ cfg: AppConfig,
81
+ run_paths: RunPaths,
82
+ surface: AssetSurface,
83
+ model_registry: ModelRegistry,
84
+ api_key: str | None = None,
85
+ transport: httpx.BaseTransport | None = None,
86
+ ) -> None:
87
+ self.cfg = cfg
88
+ self.run_paths = run_paths
89
+ self.surface = surface
90
+ self.model_registry = model_registry
91
+ self.api_key = api_key
92
+ self.transport = transport
93
+ self._inspect_counts: dict[str, int] = {}
94
+
95
+ def dispatch(self, name: str, arguments: dict[str, Any]) -> str:
96
+ if name == "asset_read":
97
+ return self.asset_read(
98
+ asset_id=str(arguments.get("asset_id") or ""),
99
+ focus=_optional_string(arguments.get("focus")),
100
+ max_chars=_optional_int(arguments.get("max_chars")),
101
+ )
102
+ if name == "asset_inspect":
103
+ return self.asset_inspect(
104
+ asset_id=str(arguments.get("asset_id") or ""),
105
+ angle=_optional_string(arguments.get("angle")),
106
+ )
107
+ return f"Unknown planner asset tool: {name}"
108
+
109
+ def asset_read(
110
+ self,
111
+ *,
112
+ asset_id: str,
113
+ focus: str | None = None,
114
+ max_chars: int | None = None,
115
+ ) -> str:
116
+ result = perform_asset_read(
117
+ surface=self.surface,
118
+ asset_id=asset_id,
119
+ focus=focus,
120
+ max_chars=max_chars,
121
+ api_key=self.api_key,
122
+ cfg=self.cfg,
123
+ model_registry=self.model_registry,
124
+ transport=self.transport,
125
+ client_factory=make_llm_client,
126
+ )
127
+ return result.text
128
+
129
+ def asset_inspect(self, *, asset_id: str, angle: str | None = None) -> str:
130
+ count = self._inspect_counts.get(asset_id, 0)
131
+ if count >= _INSPECT_CAP_PER_ASSET:
132
+ return "Inspection cap reached for this asset; proceed with available data or ask the user."
133
+ self._inspect_counts[asset_id] = count + 1
134
+ try:
135
+ handle = self.surface.refresh_comprehension(
136
+ asset_id,
137
+ mode="sync",
138
+ angle=angle,
139
+ )
140
+ record = handle.join()
141
+ except AssetError as exc:
142
+ return f"Asset inspection failed: {exc}"
143
+ if record is None:
144
+ return "Asset inspection did not produce a comprehension record."
145
+ LOGGER.info(
146
+ "asset_inspect asset_id=%s angle=%s result_status=%s",
147
+ asset_id,
148
+ str(bool(angle)).lower(),
149
+ record.status,
150
+ )
151
+ return _comprehension_result(record)
152
+
153
+
154
+ def _comprehension_result(record: ComprehensionRecord) -> str:
155
+ lines = [
156
+ f"Asset inspection complete: {record.asset_id}",
157
+ f"- status: {record.status}",
158
+ f"- source: {record.source}",
159
+ f"- detected_language: {record.detected_language or 'unknown'}",
160
+ f"- summary: {record.data.semantic_summary or '(none)'}",
161
+ ]
162
+ if record.data.stated_facts:
163
+ lines.append("- stated_facts:")
164
+ lines.extend(f" - {fact}" for fact in record.data.stated_facts[:8])
165
+ if record.data.open_questions:
166
+ lines.append("- open_questions:")
167
+ lines.extend(f" - {question}" for question in record.data.open_questions[:8])
168
+ return "\n".join(lines)
169
+
170
+
171
+ def _optional_string(value: Any) -> str | None:
172
+ if value is None:
173
+ return None
174
+ text = str(value).strip()
175
+ return text or None
176
+
177
+
178
+ def _optional_int(value: Any) -> int | None:
179
+ if value is None:
180
+ return None
181
+ try:
182
+ return int(value)
183
+ except (TypeError, ValueError):
184
+ return None