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,460 @@
1
+ """Durable ownership records for background terminal processes.
2
+
3
+ The in-memory terminal manager can terminate everything during an orderly shutdown. These records
4
+ cover the harder case where the bridge is killed before teardown runs. A later Alysis Code process
5
+ only signals an exact PID/process-group whose creation token still matches a record written at
6
+ spawn time; it never enumerates or matches processes by name or command line.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import ctypes
13
+ import json
14
+ import os
15
+ import re
16
+ import signal
17
+ import stat
18
+ import subprocess
19
+ import time
20
+ import uuid
21
+ from ctypes import wintypes
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+ from typing import Any, Literal
25
+
26
+ from .branding import canonical_user_data_dir, env_get
27
+
28
+ _SCHEMA_VERSION = 1
29
+ _MAX_RECORD_BYTES = 8192
30
+ _MAX_RECORDS = 256
31
+ _TERMINATION_CONFIRM_TIMEOUT_S = 1.0
32
+ _TERMINATION_CONFIRM_INTERVAL_S = 0.02
33
+ _DOCKER_CONTAINER_PATTERN = re.compile(r"^alysis-bgsbx-[0-9a-f]{12}$")
34
+ _ProcessState = Literal["matching", "dead", "reused", "unknown"]
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class TerminalOwnershipHandle:
39
+ record_path: Path
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class TerminalScavengeResult:
44
+ record: str
45
+ action: Literal["preserved", "terminated", "absent", "refused", "invalid"]
46
+
47
+
48
+ class TerminalOwnershipLedger:
49
+ def __init__(self, state_dir: Path | None = None) -> None:
50
+ configured = str(env_get("ALYSIS_TERMINAL_OWNERSHIP_DIR") or "").strip()
51
+ configured_data = str(env_get("ALYSIS_DATA_DIR") or "").strip()
52
+ data_root = (
53
+ Path(configured_data).expanduser() if configured_data else canonical_user_data_dir()
54
+ )
55
+ default = Path(configured).expanduser() if configured else data_root / "terminal-processes"
56
+ self.state_dir = (state_dir or default).expanduser().resolve()
57
+
58
+ def scavenge_stale(self) -> tuple[TerminalScavengeResult, ...]:
59
+ try:
60
+ self.state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
61
+ except OSError:
62
+ return ()
63
+ results: list[TerminalScavengeResult] = []
64
+ try:
65
+ candidates = sorted(self.state_dir.glob("*.json"))[:_MAX_RECORDS]
66
+ except OSError:
67
+ return ()
68
+ for path in candidates:
69
+ payload = _read_record(path)
70
+ if payload is None:
71
+ _unlink_record(path)
72
+ results.append(TerminalScavengeResult(path.name, "invalid"))
73
+ continue
74
+ owner_state = _process_state(payload["owner_pid"], payload["owner_start_token"])
75
+ if owner_state in {"matching", "unknown"}:
76
+ results.append(TerminalScavengeResult(path.name, "preserved"))
77
+ continue
78
+ child_state = _process_state(payload["child_pid"], payload["child_start_token"])
79
+ if child_state == "matching":
80
+ terminated = _terminate_owned_process(payload)
81
+ action: TerminalScavengeResult = TerminalScavengeResult(
82
+ path.name, "terminated" if terminated else "refused"
83
+ )
84
+ consume_record = terminated
85
+ elif child_state == "dead":
86
+ _cleanup_owned_resource(payload)
87
+ action = TerminalScavengeResult(path.name, "absent")
88
+ consume_record = True
89
+ elif child_state == "reused":
90
+ # The recorded process no longer exists. Never signal its recycled PID, but the
91
+ # stale ownership record can safely be consumed.
92
+ action = TerminalScavengeResult(path.name, "refused")
93
+ consume_record = True
94
+ else:
95
+ # An unverifiable identity may still be the owned process. Keep its record so a
96
+ # later scavenger can retry after a transient inspection or access failure.
97
+ action = TerminalScavengeResult(path.name, "refused")
98
+ consume_record = False
99
+ if consume_record:
100
+ _unlink_record(path)
101
+ results.append(action)
102
+ return tuple(results)
103
+
104
+ def track(
105
+ self,
106
+ *,
107
+ child_pid: int,
108
+ termination_mode: str,
109
+ posix_pgid: int | None,
110
+ cleanup_kind: str | None = None,
111
+ cleanup_id: str | None = None,
112
+ ) -> TerminalOwnershipHandle | None:
113
+ owner_pid = os.getpid()
114
+ owner_token = _pid_start_token(owner_pid)
115
+ child_token = _pid_start_token(child_pid)
116
+ if owner_pid <= 1 or child_pid <= 1 or owner_token is None or child_token is None:
117
+ return None
118
+ if termination_mode not in {"process_group", "direct"}:
119
+ return None
120
+ pgid: int | None = None
121
+ if termination_mode == "process_group" and os.name != "nt":
122
+ if not isinstance(posix_pgid, int) or posix_pgid <= 1:
123
+ return None
124
+ pgid = posix_pgid
125
+ if cleanup_kind is not None:
126
+ if cleanup_kind != "docker-container" or not _valid_cleanup_id(cleanup_id):
127
+ return None
128
+ payload = {
129
+ "schema_version": _SCHEMA_VERSION,
130
+ "record_id": uuid.uuid4().hex,
131
+ "owner_pid": owner_pid,
132
+ "owner_start_token": owner_token,
133
+ "child_pid": child_pid,
134
+ "child_start_token": child_token,
135
+ "termination_mode": termination_mode,
136
+ "posix_pgid": pgid,
137
+ "cleanup_kind": cleanup_kind,
138
+ "cleanup_id": cleanup_id,
139
+ "created_at": time.time(),
140
+ }
141
+ try:
142
+ self.state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
143
+ path = self.state_dir / f"{payload['record_id']}.json"
144
+ temporary = self.state_dir / f".{payload['record_id']}.{uuid.uuid4().hex}.tmp"
145
+ raw = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode()
146
+ descriptor = os.open(temporary, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
147
+ try:
148
+ with os.fdopen(descriptor, "wb", closefd=True) as stream:
149
+ stream.write(raw)
150
+ stream.flush()
151
+ os.fsync(stream.fileno())
152
+ os.replace(temporary, path)
153
+ _fsync_directory(self.state_dir)
154
+ finally:
155
+ with contextlib.suppress(OSError):
156
+ temporary.unlink()
157
+ return TerminalOwnershipHandle(path)
158
+ except OSError:
159
+ return None
160
+
161
+ def release(self, handle: TerminalOwnershipHandle | None) -> None:
162
+ if handle is None:
163
+ return
164
+ try:
165
+ resolved = handle.record_path.resolve()
166
+ if resolved.parent != self.state_dir or resolved.suffix != ".json":
167
+ return
168
+ except OSError:
169
+ return
170
+ _unlink_record(handle.record_path)
171
+
172
+
173
+ def _read_record(path: Path) -> dict[str, Any] | None:
174
+ try:
175
+ info = path.lstat()
176
+ if not stat.S_ISREG(info.st_mode) or info.st_size <= 0 or info.st_size > _MAX_RECORD_BYTES:
177
+ return None
178
+ flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
179
+ descriptor = os.open(path, flags)
180
+ with os.fdopen(descriptor, "r", encoding="utf-8", closefd=True) as stream:
181
+ payload = json.load(stream)
182
+ except (OSError, UnicodeError, json.JSONDecodeError):
183
+ return None
184
+ if not isinstance(payload, dict) or set(payload) != {
185
+ "schema_version",
186
+ "record_id",
187
+ "owner_pid",
188
+ "owner_start_token",
189
+ "child_pid",
190
+ "child_start_token",
191
+ "termination_mode",
192
+ "posix_pgid",
193
+ "cleanup_kind",
194
+ "cleanup_id",
195
+ "created_at",
196
+ }:
197
+ return None
198
+ if payload.get("schema_version") != _SCHEMA_VERSION:
199
+ return None
200
+ if (
201
+ not isinstance(payload.get("record_id"), str)
202
+ or re.fullmatch(r"[0-9a-f]{32}", payload["record_id"]) is None
203
+ ):
204
+ return None
205
+ if path.name != f"{payload['record_id']}.json":
206
+ return None
207
+ for key in ("owner_pid", "child_pid"):
208
+ if (
209
+ not isinstance(payload.get(key), int)
210
+ or isinstance(payload[key], bool)
211
+ or payload[key] <= 1
212
+ ):
213
+ return None
214
+ for key in ("owner_start_token", "child_start_token"):
215
+ if not isinstance(payload.get(key), str) or not payload[key] or len(payload[key]) > 256:
216
+ return None
217
+ if payload.get("termination_mode") not in {"process_group", "direct"}:
218
+ return None
219
+ pgid = payload.get("posix_pgid")
220
+ if pgid is not None and (not isinstance(pgid, int) or isinstance(pgid, bool) or pgid <= 1):
221
+ return None
222
+ cleanup_kind = payload.get("cleanup_kind")
223
+ cleanup_id = payload.get("cleanup_id")
224
+ if cleanup_kind is not None and (
225
+ cleanup_kind != "docker-container" or not _valid_cleanup_id(cleanup_id)
226
+ ):
227
+ return None
228
+ if cleanup_kind is None and cleanup_id is not None:
229
+ return None
230
+ if not isinstance(payload.get("created_at"), (int, float)):
231
+ return None
232
+ return payload
233
+
234
+
235
+ def _valid_cleanup_id(value: Any) -> bool:
236
+ return isinstance(value, str) and _DOCKER_CONTAINER_PATTERN.fullmatch(value) is not None
237
+
238
+
239
+ def _unlink_record(path: Path) -> None:
240
+ with contextlib.suppress(OSError):
241
+ path.unlink()
242
+
243
+
244
+ def _process_state(pid: int, expected_token: str) -> _ProcessState:
245
+ existence = _pid_exists(pid)
246
+ if existence is False:
247
+ return "dead"
248
+ token = _pid_start_token(pid)
249
+ if token is not None:
250
+ return "matching" if token == expected_token else "reused"
251
+ return "unknown"
252
+
253
+
254
+ def _pid_start_token(pid: int) -> str | None:
255
+ if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 1:
256
+ return None
257
+ if os.name == "nt": # pragma: no cover - exercised on Windows CI
258
+ return _windows_process_start_token(pid)
259
+ stat_path = Path("/proc") / str(pid) / "stat"
260
+ try:
261
+ text = stat_path.read_text(encoding="utf-8")
262
+ after_comm = text.rsplit(") ", 1)[1]
263
+ return f"linux:{after_comm.split()[19]}"
264
+ except (OSError, IndexError, ValueError):
265
+ pass
266
+ try:
267
+ completed = subprocess.run(
268
+ ["ps", "-o", "lstart=", "-p", str(pid)],
269
+ check=False,
270
+ capture_output=True,
271
+ text=True,
272
+ timeout=2.0,
273
+ )
274
+ except (OSError, subprocess.SubprocessError):
275
+ return None
276
+ value = completed.stdout.strip()
277
+ return f"ps:{value}" if completed.returncode == 0 and value else None
278
+
279
+
280
+ def _pid_exists(pid: int) -> bool | None:
281
+ if os.name == "nt": # pragma: no cover - exercised on Windows CI
282
+ handle = _windows_open_process(pid, 0x00100000)
283
+ if not handle:
284
+ # OpenProcess uses ERROR_INVALID_PARAMETER for a PID that no longer exists. Access
285
+ # denied remains unknown and is deliberately preserved rather than guessed dead.
286
+ if ctypes.get_last_error() == 87:
287
+ return False
288
+ return None
289
+ try:
290
+ result = _windows_kernel32().WaitForSingleObject(handle, 0)
291
+ return result == 0x00000102
292
+ finally:
293
+ _windows_kernel32().CloseHandle(handle)
294
+ try:
295
+ os.kill(pid, 0)
296
+ except ProcessLookupError:
297
+ return False
298
+ except PermissionError:
299
+ return True
300
+ except OSError:
301
+ return None
302
+ return True
303
+
304
+
305
+ def _windows_open_process(pid: int, access: int) -> Any:
306
+ try:
307
+ return _windows_kernel32().OpenProcess(access, False, pid)
308
+ except (AttributeError, OSError):
309
+ return None
310
+
311
+
312
+ def _windows_kernel32() -> Any:
313
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
314
+ kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
315
+ kernel32.OpenProcess.restype = wintypes.HANDLE
316
+ kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
317
+ kernel32.WaitForSingleObject.restype = wintypes.DWORD
318
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
319
+ kernel32.CloseHandle.restype = wintypes.BOOL
320
+ kernel32.GetProcessTimes.argtypes = [
321
+ wintypes.HANDLE,
322
+ ctypes.POINTER(wintypes.FILETIME),
323
+ ctypes.POINTER(wintypes.FILETIME),
324
+ ctypes.POINTER(wintypes.FILETIME),
325
+ ctypes.POINTER(wintypes.FILETIME),
326
+ ]
327
+ kernel32.GetProcessTimes.restype = wintypes.BOOL
328
+ return kernel32
329
+
330
+
331
+ def _windows_process_start_token(pid: int) -> str | None:
332
+ handle = _windows_open_process(pid, 0x1000) # PROCESS_QUERY_LIMITED_INFORMATION
333
+ if not handle:
334
+ return None
335
+ try:
336
+ created = wintypes.FILETIME()
337
+ exited = wintypes.FILETIME()
338
+ kernel = wintypes.FILETIME()
339
+ user = wintypes.FILETIME()
340
+ if not _windows_kernel32().GetProcessTimes(
341
+ handle,
342
+ ctypes.byref(created),
343
+ ctypes.byref(exited),
344
+ ctypes.byref(kernel),
345
+ ctypes.byref(user),
346
+ ):
347
+ return None
348
+ ticks = (int(created.dwHighDateTime) << 32) | int(created.dwLowDateTime)
349
+ return f"windows:{ticks}"
350
+ except (AttributeError, OSError):
351
+ return None
352
+ finally:
353
+ _windows_kernel32().CloseHandle(handle)
354
+
355
+
356
+ def _fsync_directory(path: Path) -> None:
357
+ if os.name == "nt":
358
+ return
359
+ try:
360
+ descriptor = os.open(path, os.O_RDONLY)
361
+ except OSError:
362
+ return
363
+ try:
364
+ os.fsync(descriptor)
365
+ except OSError:
366
+ pass
367
+ finally:
368
+ os.close(descriptor)
369
+
370
+
371
+ def _terminate_owned_process(payload: dict[str, Any]) -> bool:
372
+ # Re-check immediately before signalling to narrow the PID-reuse race.
373
+ if _process_state(payload["child_pid"], payload["child_start_token"]) != "matching":
374
+ return False
375
+ _cleanup_owned_resource(payload)
376
+ child_pid = payload["child_pid"]
377
+ if os.name == "nt": # pragma: no cover - exercised on Windows CI
378
+ return _terminate_windows_process_tree(child_pid, payload["child_start_token"])
379
+ if payload["termination_mode"] == "process_group":
380
+ pgid = payload.get("posix_pgid")
381
+ if pgid != child_pid or pgid == os.getpgrp():
382
+ return False
383
+ try:
384
+ if os.getpgid(child_pid) != pgid:
385
+ return False
386
+ except ProcessLookupError:
387
+ return True
388
+ return _terminate_posix_group(pgid)
389
+ try:
390
+ os.kill(child_pid, signal.SIGTERM)
391
+ except ProcessLookupError:
392
+ return True
393
+ except OSError:
394
+ return False
395
+ return True
396
+
397
+
398
+ def _terminate_windows_process_tree(child_pid: int, child_start_token: str) -> bool:
399
+ try:
400
+ subprocess.run(
401
+ ["taskkill", "/PID", str(child_pid), "/T", "/F"],
402
+ check=False,
403
+ stdin=subprocess.DEVNULL,
404
+ stdout=subprocess.DEVNULL,
405
+ stderr=subprocess.DEVNULL,
406
+ timeout=5.0,
407
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
408
+ )
409
+ except (OSError, subprocess.SubprocessError):
410
+ pass
411
+
412
+ # taskkill can return a non-zero status when the exact target exits during its own tree walk.
413
+ # Conversely, a zero status is not sufficient reason to discard durable ownership state until
414
+ # the recorded PID/token identity is confirmed gone. This also prevents a recycled PID from
415
+ # being mistaken for the process we attempted to terminate.
416
+ deadline = time.monotonic() + _TERMINATION_CONFIRM_TIMEOUT_S
417
+ while True:
418
+ state = _process_state(child_pid, child_start_token)
419
+ if state in {"dead", "reused"}:
420
+ return True
421
+ if time.monotonic() >= deadline:
422
+ return False
423
+ time.sleep(_TERMINATION_CONFIRM_INTERVAL_S)
424
+
425
+
426
+ def _terminate_posix_group(pgid: int) -> bool:
427
+ try:
428
+ os.killpg(pgid, signal.SIGTERM)
429
+ except ProcessLookupError:
430
+ return True
431
+ except OSError:
432
+ return False
433
+ deadline = time.monotonic() + 1.0
434
+ while time.monotonic() < deadline:
435
+ try:
436
+ os.killpg(pgid, 0)
437
+ except ProcessLookupError:
438
+ return True
439
+ except OSError:
440
+ break
441
+ time.sleep(0.02)
442
+ with contextlib.suppress(ProcessLookupError):
443
+ os.killpg(pgid, signal.SIGKILL)
444
+ return True
445
+
446
+
447
+ def _cleanup_owned_resource(payload: dict[str, Any]) -> None:
448
+ if payload.get("cleanup_kind") != "docker-container" or not _valid_cleanup_id(
449
+ payload.get("cleanup_id")
450
+ ):
451
+ return
452
+ with contextlib.suppress(OSError, subprocess.SubprocessError):
453
+ subprocess.run(
454
+ ["docker", "rm", "-f", payload["cleanup_id"]],
455
+ check=False,
456
+ stdin=subprocess.DEVNULL,
457
+ stdout=subprocess.DEVNULL,
458
+ stderr=subprocess.DEVNULL,
459
+ timeout=10.0,
460
+ )
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import unicodedata
4
+
5
+ _TYPOGRAPHIC_QUOTE_TRANSLATION = str.maketrans(
6
+ {
7
+ "\u2018": "'",
8
+ "\u2019": "'",
9
+ "\u201a": "'",
10
+ "\u201b": "'",
11
+ "\u201c": '"',
12
+ "\u201d": '"',
13
+ "\u201e": '"',
14
+ "\u201f": '"',
15
+ }
16
+ )
17
+
18
+
19
+ def normalize_for_matching(text: str) -> str:
20
+ """Return a language-neutral Unicode/whitespace/quote normalization.
21
+
22
+ This helper is for comparing controller-owned markers and fingerprints. It
23
+ deliberately contains no vocabulary, stemming, translation, or
24
+ language-specific character rewriting and must not be used as an intent
25
+ classifier.
26
+ """
27
+
28
+ normalized = unicodedata.normalize("NFKC", str(text or ""))
29
+ normalized = normalized.translate(_TYPOGRAPHIC_QUOTE_TRANSLATION).casefold()
30
+ return " ".join(normalized.split())
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+ from functools import lru_cache
6
+
7
+ _TOKEN_ENCODING_NAME = "cl100k_base"
8
+ _TOKEN_LIKE_RE = re.compile(r"\w+|[^\w\s]", re.UNICODE)
9
+ _WORD_RE = re.compile(r"\w+", re.UNICODE)
10
+
11
+
12
+ @lru_cache(maxsize=1)
13
+ def _token_encoder() -> object | None:
14
+ try:
15
+ import tiktoken # type: ignore[import-not-found]
16
+
17
+ return tiktoken.get_encoding(_TOKEN_ENCODING_NAME)
18
+ except Exception:
19
+ return None
20
+
21
+
22
+ def _fallback_estimate_tokens(text: str) -> int:
23
+ pieces = _TOKEN_LIKE_RE.findall(text)
24
+ if not pieces:
25
+ return 1 if text else 0
26
+
27
+ total = 0
28
+ for piece in pieces:
29
+ if _WORD_RE.fullmatch(piece):
30
+ total += max(1, math.ceil(len(piece.encode("utf-8")) / 4))
31
+ else:
32
+ total += 1
33
+ return total
34
+
35
+
36
+ def estimate_tokens(text: str) -> int:
37
+ if not text:
38
+ return 0
39
+
40
+ encoder = _token_encoder()
41
+ if encoder is not None:
42
+ try:
43
+ return len(encoder.encode(text)) # type: ignore[attr-defined]
44
+ except Exception:
45
+ pass
46
+
47
+ return _fallback_estimate_tokens(text)
48
+
49
+
50
+ def trim_text_to_budget(text: str, max_tokens: int) -> tuple[str, bool]:
51
+ if max_tokens <= 0:
52
+ return "", bool(text)
53
+
54
+ current = estimate_tokens(text)
55
+ if current <= max_tokens:
56
+ return text, False
57
+
58
+ marker = "\n\n...[TRUNCATED FOR TOKEN BUDGET]...\n\n"
59
+ marker_tokens = max(1, estimate_tokens(marker))
60
+
61
+ if max_tokens <= marker_tokens + 2:
62
+ return marker, True
63
+
64
+ avg_chars_per_token = max(1, len(text) // max(1, current))
65
+ available = max_tokens - marker_tokens
66
+ head_tokens = available // 2
67
+ tail_tokens = available - head_tokens
68
+ head_chars = max(32, head_tokens * avg_chars_per_token)
69
+ tail_chars = max(32, tail_tokens * avg_chars_per_token)
70
+
71
+ def _build() -> str:
72
+ if head_chars + tail_chars >= len(text):
73
+ return text
74
+ return text[:head_chars] + marker + text[-tail_chars:]
75
+
76
+ candidate = _build()
77
+ while estimate_tokens(candidate) > max_tokens and (head_chars > 32 or tail_chars > 32):
78
+ head_chars = max(32, int(head_chars * 0.9))
79
+ tail_chars = max(32, int(tail_chars * 0.9))
80
+ candidate = _build()
81
+
82
+ return candidate, True
83
+
84
+
85
+ def compute_input_budget(cap: object, safety_margin: int = 512) -> int:
86
+ context = int(getattr(cap, "context_window_tokens", 8192))
87
+ output = int(getattr(cap, "max_output_tokens", 2048))
88
+ if output >= context > 0:
89
+ # Shared-window metadata (e.g. the Kimi Code ids, where max_tokens may
90
+ # be raised up to the full context): there is no fixed output
91
+ # reservation, so subtracting the whole window would leave the 512-token
92
+ # floor as the input budget on a 1M-context model — the fresh-session
93
+ # "context: 0% left" footer bug. Reserve a conservative response
94
+ # allowance instead of the entire window.
95
+ output = min(output, max(4096, context // 8))
96
+ budget = context - output - max(0, int(safety_margin))
97
+ return max(512, budget)
@@ -0,0 +1,34 @@
1
+ # Built-In Tools
2
+
3
+ This package contains Alysis Code's built-in tool implementations. These are
4
+ registered by the host at session startup after mode, workspace, sandbox, and
5
+ configuration checks have been applied.
6
+
7
+ ## Contents
8
+
9
+ - `fs.py`, `search.py`, `symbols.py`, `git.py`, and `history.py` cover local
10
+ workspace inspection and edits.
11
+ - `shell.py` delegates command execution to the configured shell runner.
12
+ - `web.py` and `web_search.py` provide bounded web access when configured.
13
+ - `registry.py` keeps the tool metadata, schemas, previews, and result summaries
14
+ used by the session runtime.
15
+ - `availability.py` tracks optional tools without making missing optional
16
+ backends a startup failure.
17
+
18
+ ## Scope
19
+
20
+ Tool exposure is decided in `../agent/tools_assembly.py`; defining a tool here
21
+ does not make it available in every runtime or mode.
22
+
23
+ Shell execution policy is owned by the session and sandbox layers. Networked
24
+ tools should continue to use the safe HTTP path where applicable.
25
+
26
+ ## Development
27
+
28
+ Keep changes narrow and run the tests for the tool family you touch. If a change
29
+ affects which tools are exposed, also cover the session assembly path.
30
+
31
+ ## See Also
32
+
33
+ - [Security model](../../../docs/security_model.md)
34
+ - [Shell sandbox](../../../docs/shell_sandbox.md)
@@ -0,0 +1 @@
1
+ __all__ = []