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,521 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import logging
5
+ import threading
6
+ import time
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass, field, replace
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from typing import Any, Literal
12
+
13
+ from ..config import AppConfig
14
+ from ..forge import RunPaths
15
+ from ..model_registry import ModelRegistry
16
+ from .comprehender import AssetComprehender
17
+ from .index import AssetIndex
18
+ from .ingestion import ingest_asset
19
+ from .models import (
20
+ AssetError,
21
+ AssetNotFoundError,
22
+ AssetRecord,
23
+ ComprehensionRecord,
24
+ )
25
+ from .ocr import OcrProvider, TesseractOcrProvider
26
+
27
+ LOGGER = logging.getLogger(__name__)
28
+
29
+ SurfaceComprehensionStatus = Literal["pending", "running", "ready", "failed", "minimal"]
30
+ _ASSET_RUN_PATH_ATTRS: tuple[str, ...] = (
31
+ "asset_store_dir",
32
+ "assets_index_path",
33
+ "assets_index_lock_path",
34
+ "assets_raw_dir",
35
+ "assets_extracted_dir",
36
+ "assets_comprehensions_dir",
37
+ )
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class AssetSurfaceEntry:
42
+ record: AssetRecord
43
+ comprehension_status: SurfaceComprehensionStatus
44
+ comprehension_source: str | None
45
+ comprehension_summary_preview: str
46
+ detected_language: str | None
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class AssetSurfaceDetail:
51
+ record: AssetRecord
52
+ comprehension_status: SurfaceComprehensionStatus
53
+ comprehension: ComprehensionRecord | None
54
+ versions: list[int]
55
+ extracted_text_preview: str
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class AssetSurfaceAddResult:
60
+ record: AssetRecord
61
+ comprehension_handle: ComprehensionRefreshHandle | None
62
+ comprehension_record: ComprehensionRecord | None
63
+
64
+
65
+ @dataclass
66
+ class ComprehensionRefreshHandle:
67
+ asset_id: str
68
+ thread: threading.Thread | None
69
+ started_at: datetime
70
+ cancellation_event: threading.Event
71
+ _done_event: threading.Event = field(default_factory=threading.Event, repr=False)
72
+ _result: ComprehensionRecord | None = field(default=None, repr=False)
73
+
74
+ def join(self, timeout: float | None = None) -> ComprehensionRecord | None:
75
+ if self.thread is not None:
76
+ self.thread.join(timeout)
77
+ if self.thread is not None and self.thread.is_alive():
78
+ return None
79
+ return self._result
80
+
81
+ def is_done(self) -> bool:
82
+ if self.thread is None:
83
+ return True
84
+ return self._done_event.is_set()
85
+
86
+ def _set_result(self, record: ComprehensionRecord) -> None:
87
+ self._result = record
88
+ self._done_event.set()
89
+
90
+ def _mark_done(self) -> None:
91
+ self._done_event.set()
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class AssetSurfaceJoinReport:
96
+ completed: list[str]
97
+ timed_out: list[str]
98
+
99
+
100
+ class AssetSurface:
101
+ def __init__(
102
+ self,
103
+ *,
104
+ cfg: AppConfig,
105
+ run_paths: RunPaths,
106
+ model_registry: ModelRegistry | None = None,
107
+ ocr_provider: OcrProvider | None = None,
108
+ comprehender: AssetComprehender | None = None,
109
+ clock: Callable[[], datetime] = lambda: datetime.now(UTC),
110
+ ) -> None:
111
+ self.cfg = cfg
112
+ self.run_paths = run_paths
113
+ self.index = AssetIndex(run_paths)
114
+ self.model_registry = model_registry or ModelRegistry(cfg=cfg)
115
+ self.ocr_provider = ocr_provider
116
+ self.comprehender = comprehender or AssetComprehender(
117
+ cfg=cfg,
118
+ model_registry=self.model_registry,
119
+ ocr_provider=ocr_provider,
120
+ run_paths=run_paths,
121
+ )
122
+ self.clock = clock
123
+ self._registry_lock = threading.Lock()
124
+ self._in_flight: dict[str, ComprehensionRefreshHandle] = {}
125
+
126
+ def list_assets(self, *, include_deleted: bool = False) -> list[AssetSurfaceEntry]:
127
+ return [
128
+ self._entry_for(record)
129
+ for record in self.index.records(include_deleted=include_deleted)
130
+ ]
131
+
132
+ def show_asset(self, asset_id: str) -> AssetSurfaceDetail:
133
+ record = self.index.get(asset_id, include_deleted=True)
134
+ return AssetSurfaceDetail(
135
+ record=record,
136
+ comprehension_status=self._surface_status(record),
137
+ comprehension=self.comprehension_for(asset_id),
138
+ versions=_comprehension_versions(self.run_paths, asset_id),
139
+ extracted_text_preview=_read_text_preview(
140
+ self.run_paths.root,
141
+ record.extracted_text_path,
142
+ limit=800,
143
+ ),
144
+ )
145
+
146
+ def add_asset(
147
+ self,
148
+ path: Path,
149
+ *,
150
+ title: str,
151
+ description: str = "",
152
+ pinned: bool = False,
153
+ added_by: dict[str, Any] | None = None,
154
+ comprehend: Literal["sync", "async", "skip"] = "async",
155
+ comprehension_callback: Callable[[ComprehensionRecord], None] | None = None,
156
+ dedupe_policy: Literal["reject", "link"] = "reject",
157
+ ) -> AssetSurfaceAddResult:
158
+ existing_for_link = self._existing_asset_for_path(path) if dedupe_policy == "link" else None
159
+ record = ingest_asset(
160
+ path,
161
+ title=title,
162
+ description=description,
163
+ run_paths=self.run_paths,
164
+ pinned=pinned,
165
+ added_by=added_by,
166
+ dedupe_policy=dedupe_policy,
167
+ )
168
+ LOGGER.info(
169
+ "asset_surface op=add_asset asset_id=%s kind=%s size_bytes=%s comprehend=%s",
170
+ record.id,
171
+ record.kind,
172
+ record.size_bytes,
173
+ comprehend,
174
+ )
175
+ if existing_for_link is not None and existing_for_link.id == record.id:
176
+ return AssetSurfaceAddResult(
177
+ record=record,
178
+ comprehension_handle=None,
179
+ comprehension_record=None,
180
+ )
181
+ if comprehend == "skip":
182
+ return AssetSurfaceAddResult(
183
+ record=record,
184
+ comprehension_handle=None,
185
+ comprehension_record=None,
186
+ )
187
+ if comprehend == "sync":
188
+ handle = self.refresh_comprehension(
189
+ record.id,
190
+ mode="sync",
191
+ callback=comprehension_callback,
192
+ )
193
+ return AssetSurfaceAddResult(
194
+ record=self.index.get(record.id, include_deleted=True),
195
+ comprehension_handle=None,
196
+ comprehension_record=handle.join(),
197
+ )
198
+ handle = self.refresh_comprehension(
199
+ record.id,
200
+ mode="async",
201
+ callback=comprehension_callback,
202
+ )
203
+ return AssetSurfaceAddResult(
204
+ record=record,
205
+ comprehension_handle=handle,
206
+ comprehension_record=None,
207
+ )
208
+
209
+ def edit_asset(
210
+ self,
211
+ asset_id: str,
212
+ *,
213
+ title: str | None = None,
214
+ description: str | None = None,
215
+ pinned: bool | None = None,
216
+ retrigger_comprehension: bool = False,
217
+ ) -> AssetSurfaceDetail:
218
+ current = self.index.get(asset_id, include_deleted=True)
219
+ if current.deleted_at is not None:
220
+ raise AssetError(f"Cannot edit deleted asset: {asset_id}")
221
+ changes: list[str] = []
222
+ next_record = current
223
+ if title is not None:
224
+ clean_title = title.strip()
225
+ if not clean_title:
226
+ raise AssetError("Asset title is required.")
227
+ if clean_title != current.title:
228
+ next_record = replace(next_record, title=clean_title)
229
+ changes.append("title")
230
+ if description is not None and description != current.description:
231
+ next_record = replace(next_record, description=description)
232
+ changes.append("description")
233
+ if pinned is not None and bool(pinned) != current.pinned:
234
+ next_record = replace(next_record, pinned=bool(pinned))
235
+ changes.append("pinned")
236
+ if changes:
237
+ self.index.update(next_record)
238
+ LOGGER.info(
239
+ "asset_surface op=edit_asset asset_id=%s changes=%s retrigger_comprehension=%s",
240
+ asset_id,
241
+ f"[{','.join(changes)}]",
242
+ str(bool(retrigger_comprehension)).lower(),
243
+ )
244
+ if retrigger_comprehension:
245
+ self.refresh_comprehension(asset_id, mode="async")
246
+ return self.show_asset(asset_id)
247
+
248
+ def delete_asset(self, asset_id: str) -> AssetRecord:
249
+ self._cancel_existing(asset_id, join=True)
250
+ deleted = self.index.delete(asset_id)
251
+ LOGGER.info("asset_surface op=delete_asset asset_id=%s", asset_id)
252
+ return deleted
253
+
254
+ def refresh_comprehension(
255
+ self,
256
+ asset_id: str,
257
+ *,
258
+ mode: Literal["sync", "async"] = "async",
259
+ callback: Callable[[ComprehensionRecord], None] | None = None,
260
+ angle: str | None = None,
261
+ ) -> ComprehensionRefreshHandle:
262
+ LOGGER.info(
263
+ "asset_surface op=refresh_comprehension asset_id=%s mode=%s",
264
+ asset_id,
265
+ mode,
266
+ )
267
+ self._cancel_existing(asset_id, join=True)
268
+ asset = self.index.get(asset_id, include_deleted=True)
269
+ if asset.deleted_at is not None:
270
+ raise AssetError(f"Cannot refresh deleted asset: {asset_id}")
271
+ cancellation_event = threading.Event()
272
+ handle = ComprehensionRefreshHandle(
273
+ asset_id=asset_id,
274
+ thread=None,
275
+ started_at=self.clock(),
276
+ cancellation_event=cancellation_event,
277
+ )
278
+ if mode == "sync":
279
+ record = self._run_comprehension(
280
+ asset=asset,
281
+ handle=handle,
282
+ callback=callback,
283
+ angle=angle,
284
+ )
285
+ handle._set_result(record)
286
+ return handle
287
+ thread = threading.Thread(
288
+ target=self._run_comprehension_thread,
289
+ args=(asset, handle, callback, angle),
290
+ name=f"asset-comprehension-{asset_id}",
291
+ daemon=True,
292
+ )
293
+ handle.thread = thread
294
+ with self._registry_lock:
295
+ self._in_flight[asset_id] = handle
296
+ thread.start()
297
+ return handle
298
+
299
+ def cancel_pending_comprehensions(self) -> int:
300
+ with self._registry_lock:
301
+ handles = [
302
+ handle
303
+ for handle in self._in_flight.values()
304
+ if handle.thread is not None and handle.thread.is_alive()
305
+ ]
306
+ for handle in handles:
307
+ handle.cancellation_event.set()
308
+ return len(handles)
309
+
310
+ def join_pending(self, *, timeout_seconds: float | None = None) -> AssetSurfaceJoinReport:
311
+ with self._registry_lock:
312
+ handles = list(self._in_flight.values())
313
+ completed: list[str] = []
314
+ timed_out: list[str] = []
315
+ deadline = (
316
+ None if timeout_seconds is None else time.monotonic() + max(float(timeout_seconds), 0.0)
317
+ )
318
+ for handle in handles:
319
+ remaining = None if deadline is None else max(deadline - time.monotonic(), 0.0)
320
+ record = handle.join(remaining)
321
+ if record is None and handle.thread is not None and handle.thread.is_alive():
322
+ timed_out.append(handle.asset_id)
323
+ else:
324
+ completed.append(handle.asset_id)
325
+ return AssetSurfaceJoinReport(
326
+ completed=list(dict.fromkeys(completed)),
327
+ timed_out=list(dict.fromkeys(timed_out)),
328
+ )
329
+
330
+ def comprehension_for(self, asset_id: str) -> ComprehensionRecord | None:
331
+ if self._is_running(asset_id):
332
+ return None
333
+ try:
334
+ record = self.index.get(asset_id, include_deleted=True)
335
+ except AssetNotFoundError:
336
+ return None
337
+ if record.comprehension_status == "pending":
338
+ return None
339
+ try:
340
+ return self.index.read_comprehension(asset_id)
341
+ except AssetError:
342
+ return None
343
+
344
+ def _entry_for(self, record: AssetRecord) -> AssetSurfaceEntry:
345
+ comprehension = self.comprehension_for(record.id)
346
+ status = self._surface_status(record)
347
+ return AssetSurfaceEntry(
348
+ record=record,
349
+ comprehension_status=status,
350
+ comprehension_source=comprehension.source if comprehension is not None else None,
351
+ comprehension_summary_preview=_preview(
352
+ comprehension.data.semantic_summary if comprehension is not None else "",
353
+ 120,
354
+ ),
355
+ detected_language=comprehension.detected_language
356
+ if comprehension is not None
357
+ else None,
358
+ )
359
+
360
+ def _surface_status(self, record: AssetRecord) -> SurfaceComprehensionStatus:
361
+ if self._is_running(record.id):
362
+ return "running"
363
+ return record.comprehension_status
364
+
365
+ def _run_comprehension_thread(
366
+ self,
367
+ asset: AssetRecord,
368
+ handle: ComprehensionRefreshHandle,
369
+ callback: Callable[[ComprehensionRecord], None] | None,
370
+ angle: str | None,
371
+ ) -> None:
372
+ try:
373
+ record = self._run_comprehension(
374
+ asset=asset,
375
+ handle=handle,
376
+ callback=callback,
377
+ angle=angle,
378
+ )
379
+ handle._set_result(record)
380
+ finally:
381
+ handle._mark_done()
382
+ with self._registry_lock:
383
+ if self._in_flight.get(asset.id) is handle:
384
+ self._in_flight.pop(asset.id, None)
385
+
386
+ def _run_comprehension(
387
+ self,
388
+ *,
389
+ asset: AssetRecord,
390
+ handle: ComprehensionRefreshHandle,
391
+ callback: Callable[[ComprehensionRecord], None] | None,
392
+ angle: str | None = None,
393
+ ) -> ComprehensionRecord:
394
+ if angle is None:
395
+ record = self.comprehender.comprehend(asset)
396
+ else:
397
+ record = self.comprehender.comprehend(asset, angle=angle)
398
+ LOGGER.info(
399
+ "asset_comprehension_complete asset_id=%s source=%s status=%s elapsed_ms=%s",
400
+ asset.id,
401
+ record.source,
402
+ record.status,
403
+ record.elapsed_ms,
404
+ )
405
+ if not handle.cancellation_event.is_set() and callback is not None:
406
+ try:
407
+ callback(record)
408
+ except Exception as exc: # noqa: BLE001
409
+ LOGGER.warning(
410
+ "asset_surface comprehension callback failed for asset_id=%s: %s",
411
+ asset.id,
412
+ exc,
413
+ )
414
+ return record
415
+
416
+ def _is_running(self, asset_id: str) -> bool:
417
+ with self._registry_lock:
418
+ handle = self._in_flight.get(asset_id)
419
+ return (
420
+ handle is not None
421
+ and handle.thread is not None
422
+ and handle.thread.is_alive()
423
+ and not handle.is_done()
424
+ )
425
+
426
+ def _cancel_existing(self, asset_id: str, *, join: bool) -> None:
427
+ with self._registry_lock:
428
+ handle = self._in_flight.pop(asset_id, None)
429
+ if handle is None:
430
+ return
431
+ handle.cancellation_event.set()
432
+ if join and handle.thread is not None and handle.thread.is_alive():
433
+ handle.thread.join()
434
+
435
+ def _existing_asset_for_path(self, path: Path) -> AssetRecord | None:
436
+ try:
437
+ digest = hashlib.sha256(path.expanduser().resolve().read_bytes()).hexdigest()
438
+ except OSError:
439
+ return None
440
+ return self.index.find_by_sha256(digest)
441
+
442
+
443
+ def build_asset_surface(
444
+ *,
445
+ cfg: AppConfig,
446
+ run_paths: RunPaths,
447
+ model_registry: ModelRegistry | None = None,
448
+ ) -> AssetSurface:
449
+ if not run_paths_support_asset_surface(run_paths):
450
+ missing = [
451
+ attr
452
+ for attr in _ASSET_RUN_PATH_ATTRS
453
+ if not isinstance(getattr(run_paths, attr, None), Path)
454
+ ]
455
+ raise AssetError(
456
+ "Run paths do not support the asset surface; missing path attributes: "
457
+ + ", ".join(missing)
458
+ )
459
+ registry = model_registry or ModelRegistry(cfg=cfg)
460
+ ocr_provider = _ocr_provider_from_config(cfg)
461
+ return AssetSurface(
462
+ cfg=cfg,
463
+ run_paths=run_paths,
464
+ model_registry=registry,
465
+ ocr_provider=ocr_provider,
466
+ )
467
+
468
+
469
+ def run_paths_support_asset_surface(run_paths: Any) -> bool:
470
+ return all(isinstance(getattr(run_paths, attr, None), Path) for attr in _ASSET_RUN_PATH_ATTRS)
471
+
472
+
473
+ def _ocr_provider_from_config(cfg: AppConfig) -> OcrProvider | None:
474
+ comprehension_cfg = cfg.assets.comprehension
475
+ if comprehension_cfg.ocr_enabled == "never":
476
+ return None
477
+ provider_name = str(comprehension_cfg.ocr_provider or "").strip().lower()
478
+ if provider_name != "tesseract":
479
+ LOGGER.warning("Unsupported OCR provider configured for assets: %s", provider_name)
480
+ return None
481
+ provider = TesseractOcrProvider(timeout_seconds=comprehension_cfg.ocr_timeout_seconds)
482
+ if not provider.is_available():
483
+ return None
484
+ return provider
485
+
486
+
487
+ def _comprehension_versions(run_paths: RunPaths, asset_id: str) -> list[int]:
488
+ directory = run_paths.assets_comprehensions_dir / asset_id
489
+ try:
490
+ paths = list(directory.glob("v*.json"))
491
+ except OSError:
492
+ return []
493
+ versions: list[int] = []
494
+ for path in paths:
495
+ stem = path.stem
496
+ if not stem.startswith("v"):
497
+ continue
498
+ try:
499
+ version = int(stem[1:])
500
+ except ValueError:
501
+ continue
502
+ if version > 0:
503
+ versions.append(version)
504
+ return sorted(set(versions))
505
+
506
+
507
+ def _read_text_preview(root: Path, repo_relative_path: str | None, *, limit: int) -> str:
508
+ if not repo_relative_path:
509
+ return ""
510
+ try:
511
+ path = (root / repo_relative_path).resolve()
512
+ return _preview(path.read_text(encoding="utf-8", errors="replace"), limit)
513
+ except OSError:
514
+ return ""
515
+
516
+
517
+ def _preview(text: str, limit: int) -> str:
518
+ clean = str(text or "")
519
+ if len(clean) <= limit:
520
+ return clean
521
+ return clean[:limit].rstrip() + "..."
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ _ASSET_UNTRUSTED_REPLACEMENTS = (
4
+ ("[ASSET_UNTRUSTED_CONTENT]", "[ASSET_UNTRUSTED_CONTENT (asset literal)]"),
5
+ ("[/ASSET_UNTRUSTED_CONTENT]", "[/ASSET_UNTRUSTED_CONTENT (asset literal)]"),
6
+ (
7
+ "--- BEGIN UNTRUSTED ASSET CONTENT ---",
8
+ "--- BEGIN UNTRUSTED ASSET CONTENT (asset literal) ---",
9
+ ),
10
+ (
11
+ "--- END UNTRUSTED ASSET CONTENT ---",
12
+ "--- END UNTRUSTED ASSET CONTENT (asset literal) ---",
13
+ ),
14
+ ("</asset_content>", "</asset_content (asset literal)>"),
15
+ ("</asset_summary>", "</asset_summary (asset literal)>"),
16
+ )
17
+
18
+
19
+ def build_untrusted_asset_text_block(
20
+ *,
21
+ asset_id: str,
22
+ text: str,
23
+ mime_type: str | None = None,
24
+ original_char_count: int | None = None,
25
+ truncated: bool = False,
26
+ ) -> str:
27
+ body = _neutralize_asset_text_body(text)
28
+ char_count = original_char_count if original_char_count is not None else len(body)
29
+ return "\n".join(
30
+ [
31
+ "[ASSET_UNTRUSTED_CONTENT]",
32
+ f"asset_id: {asset_id}",
33
+ f"mime_type: {mime_type or 'unknown'}",
34
+ f"char_count: {char_count}",
35
+ f"truncated: {'true' if truncated else 'false'}",
36
+ "--- BEGIN UNTRUSTED ASSET CONTENT ---",
37
+ body,
38
+ "--- END UNTRUSTED ASSET CONTENT ---",
39
+ "[/ASSET_UNTRUSTED_CONTENT]",
40
+ ]
41
+ )
42
+
43
+
44
+ def _neutralize_asset_text_body(text: str) -> str:
45
+ body = str(text)
46
+ for raw_marker, escaped_marker in _ASSET_UNTRUSTED_REPLACEMENTS:
47
+ body = body.replace(raw_marker, escaped_marker)
48
+ return body
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import threading
6
+ from collections import Counter
7
+ from typing import Any
8
+
9
+ from ..forge import RunPaths, now_iso
10
+
11
+
12
+ class AssetUsageLogger:
13
+ def __init__(self, *, run_paths: RunPaths, task_id: str) -> None:
14
+ self.run_paths = run_paths
15
+ self.task_id = task_id
16
+ self.path = run_paths.execution_asset_usage_dir / f"{_safe_task(task_id)}.jsonl"
17
+ self._lock = threading.Lock()
18
+ self._counts: Counter[str] = Counter()
19
+
20
+ def event(self, event: str, **payload: Any) -> None:
21
+ clean_event = str(event or "").strip()
22
+ if not clean_event:
23
+ return
24
+ body = {
25
+ "event": clean_event,
26
+ "ts": now_iso(),
27
+ **_sanitize_payload(payload),
28
+ }
29
+ self._append_json_line(body)
30
+ self._counts[clean_event] += 1
31
+
32
+ def mirror(self, *, asset_id: str, kind: str, status: str) -> None:
33
+ self.event("mirror", asset_id=asset_id, kind=kind, status=status)
34
+
35
+ def allocation_decision(self, *, asset_id: str, mode: str) -> None:
36
+ self.event("allocation_decision", asset_id=asset_id, mode=mode)
37
+
38
+ def inline_injection(self, *, asset_id: str, kind: str) -> None:
39
+ self.event("inline_injection", asset_id=asset_id, kind=kind)
40
+
41
+ def asset_read(self, *, asset_id: str, focus: bool, chars: int, cached: bool) -> None:
42
+ self.event(
43
+ "asset_read",
44
+ asset_id=asset_id,
45
+ focus=bool(focus),
46
+ chars=max(0, int(chars)),
47
+ cached=bool(cached),
48
+ )
49
+
50
+ def asset_load(self, *, asset_id: str, kind: str, chars: int) -> None:
51
+ self.event("asset_load", asset_id=asset_id, kind=kind, chars=max(0, int(chars)))
52
+
53
+ def summary(self, *, primary_count: int, may_need_count: int, pinned_count: int) -> None:
54
+ self.event(
55
+ "summary",
56
+ primary_count=max(0, int(primary_count)),
57
+ may_need_count=max(0, int(may_need_count)),
58
+ pinned_count=max(0, int(pinned_count)),
59
+ reads=int(self._counts.get("asset_read", 0)),
60
+ loads=int(self._counts.get("asset_load", 0)),
61
+ )
62
+
63
+ def _append_json_line(self, payload: dict[str, Any]) -> None:
64
+ line = json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str) + "\n"
65
+ encoded = line.encode("utf-8")
66
+ self.path.parent.mkdir(parents=True, exist_ok=True)
67
+ with self._lock:
68
+ fd = os.open(
69
+ self.path,
70
+ os.O_APPEND | os.O_CREAT | os.O_WRONLY,
71
+ 0o600,
72
+ )
73
+ try:
74
+ os.write(fd, encoded)
75
+ finally:
76
+ os.close(fd)
77
+
78
+
79
+ def _sanitize_payload(payload: dict[str, Any]) -> dict[str, Any]:
80
+ sanitized: dict[str, Any] = {}
81
+ for key, value in payload.items():
82
+ clean_key = str(key or "").strip()
83
+ if not clean_key:
84
+ continue
85
+ if isinstance(value, str | int | float | bool) or value is None:
86
+ sanitized[clean_key] = value
87
+ else:
88
+ sanitized[clean_key] = str(value)
89
+ return sanitized
90
+
91
+
92
+ def _safe_task(task_id: str) -> str:
93
+ safe = "".join(ch if ch.isalnum() or ch in {"-", "_"} else "_" for ch in task_id)
94
+ return safe or "task"