asicode 0.2.6__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 (340) hide show
  1. asi.py +9644 -0
  2. asicode-0.2.6.dist-info/METADATA +208 -0
  3. asicode-0.2.6.dist-info/RECORD +340 -0
  4. asicode-0.2.6.dist-info/WHEEL +5 -0
  5. asicode-0.2.6.dist-info/entry_points.txt +2 -0
  6. asicode-0.2.6.dist-info/licenses/LICENSE +21 -0
  7. asicode-0.2.6.dist-info/top_level.txt +11 -0
  8. common.py +86 -0
  9. config.py +162 -0
  10. context_collector.py +388 -0
  11. diff_apply.py +1549 -0
  12. external_llm/__init__.py +53 -0
  13. external_llm/agent/__init__.py +1 -0
  14. external_llm/agent/_shared_utils.py +1713 -0
  15. external_llm/agent/_thread_pool.py +13 -0
  16. external_llm/agent/_user_intent.py +233 -0
  17. external_llm/agent/agent_context_manager.py +532 -0
  18. external_llm/agent/agent_escalation_pipeline.py +161 -0
  19. external_llm/agent/agent_fast_path.py +63 -0
  20. external_llm/agent/agent_loop.py +3567 -0
  21. external_llm/agent/agent_loop_types.py +207 -0
  22. external_llm/agent/agent_phase_manager.py +352 -0
  23. external_llm/agent/agent_planner_pipeline.py +1756 -0
  24. external_llm/agent/agent_profile.py +200 -0
  25. external_llm/agent/agent_turn_pipeline.py +2592 -0
  26. external_llm/agent/alignment_scorer.py +333 -0
  27. external_llm/agent/anchor_shared.py +310 -0
  28. external_llm/agent/argument_repairer.py +119 -0
  29. external_llm/agent/ast_cache.py +148 -0
  30. external_llm/agent/ast_engine.py +1060 -0
  31. external_llm/agent/async_tool_executor.py +182 -0
  32. external_llm/agent/auto_correction.py +2268 -0
  33. external_llm/agent/background_job_manager.py +571 -0
  34. external_llm/agent/call_graph.py +516 -0
  35. external_llm/agent/checkpoint_store.py +472 -0
  36. external_llm/agent/config/__init__.py +0 -0
  37. external_llm/agent/config/thresholds.py +409 -0
  38. external_llm/agent/context_budget.py +593 -0
  39. external_llm/agent/context_contract.py +231 -0
  40. external_llm/agent/context_manager.py +1301 -0
  41. external_llm/agent/context_utils.py +29 -0
  42. external_llm/agent/design_chat_loop.py +2510 -0
  43. external_llm/agent/enums.py +48 -0
  44. external_llm/agent/execution_mode_classifier.py +323 -0
  45. external_llm/agent/execution_spec.py +355 -0
  46. external_llm/agent/execution_thresholds.py +281 -0
  47. external_llm/agent/failure_classifier.py +160 -0
  48. external_llm/agent/failure_context.py +409 -0
  49. external_llm/agent/failure_pattern_store.py +911 -0
  50. external_llm/agent/file_cache.py +144 -0
  51. external_llm/agent/fix_spec_claim_validator.py +747 -0
  52. external_llm/agent/gsg_safety.py +114 -0
  53. external_llm/agent/guard_ir.py +985 -0
  54. external_llm/agent/handoff_observer.py +327 -0
  55. external_llm/agent/insights_manager.py +1124 -0
  56. external_llm/agent/intent_models.py +250 -0
  57. external_llm/agent/intent_resolver.py +861 -0
  58. external_llm/agent/interrupt_tool_results.py +95 -0
  59. external_llm/agent/json_repair.py +283 -0
  60. external_llm/agent/language_backend.py +612 -0
  61. external_llm/agent/language_hint.py +51 -0
  62. external_llm/agent/learned_policy.py +826 -0
  63. external_llm/agent/light_repo_index.py +231 -0
  64. external_llm/agent/lint_runner.py +397 -0
  65. external_llm/agent/llm_output_utils.py +52 -0
  66. external_llm/agent/local_assistant.py +804 -0
  67. external_llm/agent/message_shapes.py +89 -0
  68. external_llm/agent/metadata_utils.py +14 -0
  69. external_llm/agent/operation_models.py +2707 -0
  70. external_llm/agent/orchestrator.py +5109 -0
  71. external_llm/agent/output_normalizer.py +415 -0
  72. external_llm/agent/performance_metrics.py +452 -0
  73. external_llm/agent/placement_contract.py +2765 -0
  74. external_llm/agent/plan_state.py +136 -0
  75. external_llm/agent/planner_lane_facade.py +67 -0
  76. external_llm/agent/rag_configs.py +168 -0
  77. external_llm/agent/rag_searcher.py +693 -0
  78. external_llm/agent/reasoning_utils.py +62 -0
  79. external_llm/agent/repair_helpers.py +475 -0
  80. external_llm/agent/request_intent_classifier.py +95 -0
  81. external_llm/agent/routing_policy.py +157 -0
  82. external_llm/agent/run_store.py +2356 -0
  83. external_llm/agent/scanner_registry.py +610 -0
  84. external_llm/agent/selector_constants.py +26 -0
  85. external_llm/agent/semantic_intent.py +163 -0
  86. external_llm/agent/semantic_lint.py +94 -0
  87. external_llm/agent/session_state.py +80 -0
  88. external_llm/agent/slash_commands.py +90 -0
  89. external_llm/agent/subagent_ipc.py +1222 -0
  90. external_llm/agent/symbol_engine.py +592 -0
  91. external_llm/agent/symbol_index.py +380 -0
  92. external_llm/agent/symbol_locator.py +117 -0
  93. external_llm/agent/symbol_modify_tool.py +1258 -0
  94. external_llm/agent/symbol_search.py +1881 -0
  95. external_llm/agent/task_router.py +751 -0
  96. external_llm/agent/terminal_coordination.py +43 -0
  97. external_llm/agent/termination_policy.py +284 -0
  98. external_llm/agent/test_impact_selector.py +411 -0
  99. external_llm/agent/test_runner.py +450 -0
  100. external_llm/agent/tool_chain.py +36 -0
  101. external_llm/agent/tool_dependency_graph.py +169 -0
  102. external_llm/agent/tool_failure_log.py +497 -0
  103. external_llm/agent/tool_handlers/__init__.py +25 -0
  104. external_llm/agent/tool_handlers/agent_tools.py +289 -0
  105. external_llm/agent/tool_handlers/analysis_tools.py +1343 -0
  106. external_llm/agent/tool_handlers/ast_op_executor.py +1233 -0
  107. external_llm/agent/tool_handlers/browser_tools.py +325 -0
  108. external_llm/agent/tool_handlers/constants.py +10 -0
  109. external_llm/agent/tool_handlers/git_tools.py +1031 -0
  110. external_llm/agent/tool_handlers/read_tools.py +530 -0
  111. external_llm/agent/tool_handlers/shell_policy.py +17 -0
  112. external_llm/agent/tool_handlers/test_tools.py +228 -0
  113. external_llm/agent/tool_handlers/web_search_tools.py +769 -0
  114. external_llm/agent/tool_handlers/write_tools.py +6213 -0
  115. external_llm/agent/tool_registry.py +2268 -0
  116. external_llm/agent/tool_result_cache.py +178 -0
  117. external_llm/agent/tool_safety.py +956 -0
  118. external_llm/agent/tool_schemas.py +1176 -0
  119. external_llm/agent/vector_cache.py +614 -0
  120. external_llm/agent/weight_learning.py +2929 -0
  121. external_llm/agent/work_state_digest.py +157 -0
  122. external_llm/analysis/__init__.py +0 -0
  123. external_llm/analysis/_dead_block_shared.py +848 -0
  124. external_llm/analysis/ast_similarity_scanner.py +1128 -0
  125. external_llm/analysis/broken_contract_scanner.py +591 -0
  126. external_llm/analysis/container_reachability_scanner.py +792 -0
  127. external_llm/analysis/contradictory_logic_scanner.py +693 -0
  128. external_llm/analysis/cross_file_refs.py +430 -0
  129. external_llm/analysis/dead_block_scanner.py +97 -0
  130. external_llm/analysis/duplicate_definition_scanner.py +353 -0
  131. external_llm/analysis/parse_cache.py +110 -0
  132. external_llm/analysis/public_dead_code_scanner.py +106 -0
  133. external_llm/analysis/unused_import_scanner.py +385 -0
  134. external_llm/analysis/vulture_scanner.py +445 -0
  135. external_llm/anthropic_client.py +1235 -0
  136. external_llm/ast_rewrite.py +322 -0
  137. external_llm/client.py +485 -0
  138. external_llm/code_analyzer.py +302 -0
  139. external_llm/code_structure_utils.py +1554 -0
  140. external_llm/common/__init__.py +0 -0
  141. external_llm/common/atomic_io.py +240 -0
  142. external_llm/common/file_lock.py +134 -0
  143. external_llm/common/indent_utils.py +839 -0
  144. external_llm/context/context_packs.py +60 -0
  145. external_llm/context_builder.py +411 -0
  146. external_llm/dependency_graph.py +303 -0
  147. external_llm/design_session.py +629 -0
  148. external_llm/edit_localization/__init__.py +26 -0
  149. external_llm/edit_localization/dataflow_extractor.py +472 -0
  150. external_llm/edit_localization/graph_propagator.py +241 -0
  151. external_llm/edit_localization/relevance_scorer.py +310 -0
  152. external_llm/edit_localization/request_analyzer.py +89 -0
  153. external_llm/editor/_editor_core/__init__.py +0 -0
  154. external_llm/editor/_editor_core/common/__init__.py +0 -0
  155. external_llm/editor/_editor_core/common/import_normalizer.py +376 -0
  156. external_llm/editor/_editor_core/ts_vm/__init__.py +0 -0
  157. external_llm/editor/_editor_core/ts_vm/execution_vm/__init__.py +0 -0
  158. external_llm/editor/_editor_core/ts_vm/execution_vm/ast_rewriter.py +118 -0
  159. external_llm/editor/_editor_core/ts_vm/execution_vm/models.py +27 -0
  160. external_llm/editor/_editor_core/ts_vm/execution_vm/rollback_manager.py +41 -0
  161. external_llm/editor/_editor_core/ts_vm/execution_vm/verifier.py +212 -0
  162. external_llm/editor/_editor_core/ts_vm/execution_vm/vm.py +193 -0
  163. external_llm/editor/_editor_core/ts_vm/learning/__init__.py +0 -0
  164. external_llm/editor/_editor_core/ts_vm/learning/exploration_policy.py +43 -0
  165. external_llm/editor/_editor_core/ts_vm/primitives/__init__.py +0 -0
  166. external_llm/editor/_editor_core/ts_vm/primitives/executor.py +66 -0
  167. external_llm/editor/_editor_core/ts_vm/primitives/models.py +21 -0
  168. external_llm/editor/_editor_core/ts_vm/repair/__init__.py +0 -0
  169. external_llm/editor/_editor_core/ts_vm/repair/failure_classifier.py +88 -0
  170. external_llm/editor/_editor_core/ts_vm/repair/repair_planner.py +94 -0
  171. external_llm/editor/_editor_core/ts_vm/repair/repair_registry.py +42 -0
  172. external_llm/editor/_editor_core/ts_vm/repair/repair_strategies.py +113 -0
  173. external_llm/editor/_editor_core/vm/__init__.py +36 -0
  174. external_llm/editor/_editor_core/vm/ast_rewriter.py +99 -0
  175. external_llm/editor/_editor_core/vm/classification.py +70 -0
  176. external_llm/editor/_editor_core/vm/contracts/__init__.py +3 -0
  177. external_llm/editor/_editor_core/vm/contracts/contract_diff.py +225 -0
  178. external_llm/editor/_editor_core/vm/contracts/contract_extractor.py +271 -0
  179. external_llm/editor/_editor_core/vm/contracts/contract_models.py +93 -0
  180. external_llm/editor/_editor_core/vm/contracts/project_graph.py +201 -0
  181. external_llm/editor/_editor_core/vm/contracts/propagation_planner.py +237 -0
  182. external_llm/editor/_editor_core/vm/failure_classifier.py +390 -0
  183. external_llm/editor/_editor_core/vm/models.py +28 -0
  184. external_llm/editor/_editor_core/vm/repair_planner.py +91 -0
  185. external_llm/editor/_editor_core/vm/repair_registry.py +34 -0
  186. external_llm/editor/_editor_core/vm/repair_strategies.py +1049 -0
  187. external_llm/editor/_editor_core/vm/rollback_manager.py +40 -0
  188. external_llm/editor/_editor_core/vm/verifier.py +381 -0
  189. external_llm/editor/_editor_core/vm/vm.py +326 -0
  190. external_llm/editor/agent/autonomous/__init__.py +45 -0
  191. external_llm/editor/agent/autonomous/proactive_runner.py +386 -0
  192. external_llm/editor/agent/autonomous/push_manager.py +299 -0
  193. external_llm/editor/agent/autonomous/task_queue.py +225 -0
  194. external_llm/editor/agent/autonomous/trigger_engine.py +175 -0
  195. external_llm/editor/agent/autonomous/trigger_policy.py +294 -0
  196. external_llm/editor/agent/mcp/__init__.py +13 -0
  197. external_llm/editor/agent/mcp/server.py +175 -0
  198. external_llm/editor/agent/prompts/__init__.py +2 -0
  199. external_llm/editor/agent/prompts/edit_prompts.py +299 -0
  200. external_llm/editor/cross_language/__init__.py +4 -0
  201. external_llm/editor/cross_language/models.py +79 -0
  202. external_llm/editor/cross_language/strategy_abstraction.py +204 -0
  203. external_llm/editor/cross_language/transfer_engine.py +170 -0
  204. external_llm/editor/learning/__init__.py +1 -0
  205. external_llm/editor/learning/experience_store.py +343 -0
  206. external_llm/editor/learning/exploration_state.py +124 -0
  207. external_llm/editor/learning/learning_sink.py +131 -0
  208. external_llm/editor/learning/pattern_extractor.py +158 -0
  209. external_llm/editor/learning/primitive_learning_models.py +125 -0
  210. external_llm/editor/learning/primitive_learning_scorer.py +145 -0
  211. external_llm/editor/learning/primitive_learning_store.py +142 -0
  212. external_llm/editor/learning/primitive_learning_updater.py +128 -0
  213. external_llm/editor/learning/primitive_sequence_models.py +58 -0
  214. external_llm/editor/learning/primitive_sequence_scorer.py +117 -0
  215. external_llm/editor/learning/primitive_sequence_store.py +115 -0
  216. external_llm/editor/learning/primitive_sequence_updater.py +74 -0
  217. external_llm/editor/learning/problem_signature.py +161 -0
  218. external_llm/editor/learning/strategy_execution_bias.py +345 -0
  219. external_llm/editor/learning/strategy_prioritizer.py +245 -0
  220. external_llm/editor/learning/strategy_state.py +128 -0
  221. external_llm/editor/learning/unified_run_record.py +118 -0
  222. external_llm/editor/learning/unified_store.py +566 -0
  223. external_llm/editor/primitives/__init__.py +2 -0
  224. external_llm/editor/primitives/code_context.py +660 -0
  225. external_llm/editor/primitives/delete_node.py +41 -0
  226. external_llm/editor/primitives/executor.py +106 -0
  227. external_llm/editor/primitives/insert_import.py +90 -0
  228. external_llm/editor/primitives/insert_statement.py +77 -0
  229. external_llm/editor/primitives/models.py +87 -0
  230. external_llm/editor/primitives/registry.py +49 -0
  231. external_llm/editor/primitives/rename_symbol.py +91 -0
  232. external_llm/editor/primitives/replace_function.py +260 -0
  233. external_llm/editor/primitives/update_call.py +76 -0
  234. external_llm/editor/semantic/__init__.py +1 -0
  235. external_llm/editor/semantic/ast_transform_utils.py +450 -0
  236. external_llm/editor/semantic/contract_replan_strategy.py +98 -0
  237. external_llm/editor/semantic/draft_parser.py +206 -0
  238. external_llm/editor/semantic/flow_synthesizer.py +165 -0
  239. external_llm/editor/semantic/fragment_generator.py +21 -0
  240. external_llm/editor/semantic/fragment_integrator.py +163 -0
  241. external_llm/editor/semantic/generation_planner.py +19 -0
  242. external_llm/editor/semantic/libcst_transform_utils.py +254 -0
  243. external_llm/editor/semantic/output_comparator.py +90 -0
  244. external_llm/editor/semantic/primitive_code_templates.py +307 -0
  245. external_llm/editor/semantic/primitive_detector.py +402 -0
  246. external_llm/editor/semantic/primitive_ir_builder.py +109 -0
  247. external_llm/editor/semantic/primitive_models.py +108 -0
  248. external_llm/editor/semantic/primitive_reconstructor.py +264 -0
  249. external_llm/editor/semantic/primitive_registry.py +328 -0
  250. external_llm/editor/semantic/semantic_contract_evaluator.py +321 -0
  251. external_llm/editor/semantic/semantic_contract_models.py +124 -0
  252. external_llm/editor/semantic/semantic_contract_registry.py +134 -0
  253. external_llm/editor/semantic/semantic_contracts.py +72 -0
  254. external_llm/editor/semantic/semantic_corrector.py +75 -0
  255. external_llm/editor/semantic/semantic_gap_analyzer.py +194 -0
  256. external_llm/editor/semantic/semantic_rewrite_models.py +62 -0
  257. external_llm/editor/semantic/semantic_rewrite_planner.py +252 -0
  258. external_llm/editor/semantic/semantic_rewriter.py +304 -0
  259. external_llm/editor/semantic/semantic_tracer.py +427 -0
  260. external_llm/editor/semantic/synthesis_planner.py +99 -0
  261. external_llm/editor/semantic/ts_ir_models.py +414 -0
  262. external_llm/editor/semantic/ts_semantic_models.py +112 -0
  263. external_llm/editor/semantic/ts_semantic_tracer.py +1302 -0
  264. external_llm/editor/simulator/dependency_traversal.py +172 -0
  265. external_llm/editor/simulator/impact_models.py +42 -0
  266. external_llm/editor/simulator/impact_simulator.py +187 -0
  267. external_llm/editor/simulator/risk_estimator.py +71 -0
  268. external_llm/editor/verification/__init__.py +7 -0
  269. external_llm/editor/verification/code_integrity.py +406 -0
  270. external_llm/editor/verification/impact_propagation.py +401 -0
  271. external_llm/editor/verification/impact_verification_mapper.py +148 -0
  272. external_llm/editor/verification/verification_set_builder.py +329 -0
  273. external_llm/graph/composite_risk.py +226 -0
  274. external_llm/graph/execution_graph_advisor.py +636 -0
  275. external_llm/graph/graph_builder.py +31 -0
  276. external_llm/graph/graph_facade.py +198 -0
  277. external_llm/graph/models.py +113 -0
  278. external_llm/graph/repository_graph.py +1061 -0
  279. external_llm/graph/run_scoped_graph_cache.py +283 -0
  280. external_llm/graph/spec_graph_enricher.py +855 -0
  281. external_llm/graph/virtual_graph.py +429 -0
  282. external_llm/hybrid_parser.py +215 -0
  283. external_llm/image_utils.py +147 -0
  284. external_llm/intelligent_service.py +2147 -0
  285. external_llm/languages/__init__.py +24 -0
  286. external_llm/languages/base.py +450 -0
  287. external_llm/languages/bash_provider.py +94 -0
  288. external_llm/languages/capabilities.py +129 -0
  289. external_llm/languages/csharp_provider.py +111 -0
  290. external_llm/languages/css_provider.py +178 -0
  291. external_llm/languages/dependency_checker.py +753 -0
  292. external_llm/languages/go_provider.py +452 -0
  293. external_llm/languages/html_provider.py +136 -0
  294. external_llm/languages/java_provider.py +485 -0
  295. external_llm/languages/javascript_provider.py +334 -0
  296. external_llm/languages/json_provider.py +113 -0
  297. external_llm/languages/kotlin_provider.py +462 -0
  298. external_llm/languages/libcst_utils.py +699 -0
  299. external_llm/languages/models.py +195 -0
  300. external_llm/languages/php_provider.py +90 -0
  301. external_llm/languages/python_provider.py +390 -0
  302. external_llm/languages/registry.py +128 -0
  303. external_llm/languages/ruby_provider.py +90 -0
  304. external_llm/languages/rust_provider.py +89 -0
  305. external_llm/languages/swift_provider.py +95 -0
  306. external_llm/languages/syntax_validator.py +357 -0
  307. external_llm/languages/tree_sitter_utils.py +2140 -0
  308. external_llm/languages/typescript_provider.py +694 -0
  309. external_llm/model_registry.py +153 -0
  310. external_llm/multi_planner.py +744 -0
  311. external_llm/ollama_api.py +126 -0
  312. external_llm/openai_client.py +1281 -0
  313. external_llm/output_modes.py +29 -0
  314. external_llm/output_parser.py +760 -0
  315. external_llm/patch_engine.py +2482 -0
  316. external_llm/patch_synthesizer.py +251 -0
  317. external_llm/project_analyzer.py +1386 -0
  318. external_llm/providers.py +2112 -0
  319. external_llm/repl/collaborate/__init__.py +46 -0
  320. external_llm/repl/collaborate/asi_mcp_adapter.py +550 -0
  321. external_llm/repl/collaborate/claude_session.py +484 -0
  322. external_llm/repl/collaborate/cli.py +257 -0
  323. external_llm/repl/collaborate/collaboration_orchestrator.py +422 -0
  324. external_llm/repl/collaborate/streaming_display.py +667 -0
  325. external_llm/repl/collaborate/verdict.py +145 -0
  326. external_llm/semantic_patch.py +190 -0
  327. external_llm/service.py +1690 -0
  328. external_llm/smart_analyzer.py +466 -0
  329. external_llm/super_context_builder.py +644 -0
  330. external_llm/testing/__init__.py +0 -0
  331. external_llm/testing/symbol_aware_test_finder.py +377 -0
  332. external_llm/testing/test_dependency_graph.py +380 -0
  333. patch_synth.py +1587 -0
  334. path_security.py +143 -0
  335. plan_compiler.py +1058 -0
  336. services/__init__.py +0 -0
  337. services/patch_helpers.py +151 -0
  338. utils/__init__.py +1 -0
  339. utils/llm_utils.py +22 -0
  340. utils/string_helper.py +274 -0
@@ -0,0 +1,484 @@
1
+ """
2
+ ClaudeSession — ClaudeSDKClient wrapper with streaming, hooks, and interrupts.
3
+
4
+ Provides a robust async context manager for Claude Code Agent communication.
5
+ Handles streaming events, permission hooks, and session lifecycle.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import time
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Optional
14
+
15
+ from .verdict import CollaborationVerdict
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class SessionEvent:
22
+ """A single event from a Claude Code Agent session."""
23
+ type: str = "unknown" # "text", "tool_call", "tool_result", "error", "verdict", "status"
24
+ content: str = ""
25
+ metadata: dict[str, Any] = field(default_factory=dict)
26
+ timestamp: float = field(default_factory=time.time)
27
+
28
+
29
+ @dataclass
30
+ class SessionResult:
31
+ """Complete result of a Claude Code Agent session."""
32
+ verdict: CollaborationVerdict
33
+ events: list[SessionEvent] = field(default_factory=list)
34
+ tool_calls_count: int = 0
35
+ total_tokens: int = 0
36
+ total_cost_usd: float = 0.0
37
+ duration_seconds: float = 0.0
38
+ error: Optional[str] = None
39
+
40
+
41
+ class ClaudeSession:
42
+ """Wrapper around ClaudeSDKClient for asicode collaboration.
43
+
44
+ Manages the full lifecycle of a Claude Code Agent conversation:
45
+ - Connection and disconnection
46
+ - Streaming message handling
47
+ - Tool call event capture
48
+ - Permission control via hooks
49
+ - Interrupt support
50
+ - Verdict extraction
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ options: Any = None, # ClaudeAgentOptions
56
+ event_callback: Optional[Callable[[SessionEvent], None]] = None,
57
+ include_partial: bool = True,
58
+ ):
59
+ self._options = options
60
+ self._event_callback = event_callback
61
+ self._include_partial = include_partial
62
+ self._client: Any = None # ClaudeSDKClient
63
+ self._events: list[SessionEvent] = []
64
+ self._start_time: float = 0.0
65
+ self._tool_calls_count: int = 0
66
+ self._tool_names_by_id: dict[str, str] = {}
67
+ self._last_cost_usd: float = 0.0
68
+ self._last_total_tokens: int = 0
69
+ self._structured_candidate: Optional[dict[str, Any]] = None
70
+
71
+ async def __aenter__(self) -> "ClaudeSession":
72
+ """Enter context: create and connect the SDK client."""
73
+ from claude_agent_sdk import ClaudeSDKClient
74
+
75
+ # SDK INFO logs ("Using bundled Claude Code CLI" etc.) would intrude into the
76
+ # terminal UI, so only pass through WARNING and above — unrelated to file handlers.
77
+ logging.getLogger("claude_agent_sdk").setLevel(logging.WARNING)
78
+
79
+ # Set include_partial_messages for streaming text
80
+ if self._options is not None and self._include_partial:
81
+ self._options.include_partial_messages = True
82
+
83
+ self._client = ClaudeSDKClient(options=self._options)
84
+ self._start_time = time.time()
85
+
86
+ try:
87
+ await self._client.connect()
88
+ logger.debug("ClaudeSession connected")
89
+ except Exception as ex:
90
+ # connect() failed — clean up the client to avoid leaking
91
+ # SDK resources (subprocess, file descriptors). disconnect()
92
+ # on a not-yet-connected client is a no-op or raises; both
93
+ # are safe to ignore.
94
+ try:
95
+ await self._client.disconnect()
96
+ except Exception:
97
+ pass
98
+ self._client = None
99
+ logger.error("Failed to connect ClaudeSession: %s", ex)
100
+ raise
101
+
102
+ return self
103
+
104
+ async def __aexit__(self, *args) -> None:
105
+ """Exit context: disconnect the SDK client."""
106
+ if self._client is not None:
107
+ try:
108
+ await self._client.disconnect()
109
+ except Exception as ex:
110
+ logger.debug("Disconnect error (ignored): %s", ex)
111
+ self._client = None
112
+ logger.debug("ClaudeSession disconnected")
113
+
114
+ async def query(self, prompt: str) -> SessionResult:
115
+ """Send a query and process all responses into a SessionResult.
116
+
117
+ Args:
118
+ prompt: The prompt to send to Claude Code Agent.
119
+
120
+ Returns:
121
+ SessionResult containing the verdict and all session events.
122
+ """
123
+ if self._client is None:
124
+ raise RuntimeError("ClaudeSession not connected; use async with")
125
+
126
+ self._events = []
127
+ self._tool_calls_count = 0
128
+ self._tool_names_by_id = {}
129
+ self._last_cost_usd = 0.0
130
+ self._last_total_tokens = 0
131
+ self._structured_candidate = None
132
+ start = time.monotonic()
133
+
134
+ try:
135
+ # Send the query
136
+ await self._client.query(prompt)
137
+
138
+ # Process streaming response
139
+ verdict = await self._process_response_stream()
140
+
141
+ duration = time.monotonic() - start
142
+
143
+ return SessionResult(
144
+ verdict=verdict,
145
+ events=self._events,
146
+ tool_calls_count=self._tool_calls_count,
147
+ total_tokens=self._last_total_tokens,
148
+ total_cost_usd=self._last_cost_usd,
149
+ duration_seconds=duration,
150
+ )
151
+
152
+ except Exception as ex:
153
+ duration = time.monotonic() - start
154
+ logger.exception("ClaudeSession.query failed")
155
+ return SessionResult(
156
+ verdict=CollaborationVerdict(
157
+ status="failure",
158
+ summary="Session error",
159
+ details=str(ex),
160
+ confidence=0.0,
161
+ ),
162
+ events=self._events,
163
+ tool_calls_count=self._tool_calls_count,
164
+ duration_seconds=duration,
165
+ error=str(ex),
166
+ )
167
+
168
+ async def _process_response_stream(self) -> CollaborationVerdict:
169
+ """Process all messages from the streaming response.
170
+
171
+ Handles AssistantMessage (text + tool calls), ResultMessage (verdict),
172
+ StreamEvent (partial tokens), and error messages.
173
+
174
+ Returns:
175
+ CollaborationVerdict extracted from the final ResultMessage, or
176
+ a fallback if no structured verdict is returned.
177
+ """
178
+ from claude_agent_sdk import (
179
+ AssistantMessage,
180
+ ResultMessage,
181
+ StreamEvent,
182
+ SystemMessage,
183
+ UserMessage,
184
+ )
185
+
186
+ final_verdict: Optional[CollaborationVerdict] = None
187
+ accumulated_text: list[str] = []
188
+ has_error = False
189
+
190
+ async for message in self._client.receive_response():
191
+ if isinstance(message, StreamEvent):
192
+ self._handle_stream_event(message)
193
+
194
+ elif isinstance(message, AssistantMessage):
195
+ # Check for protocol-level error on the assistant message
196
+ error = getattr(message, "error", None)
197
+ if error:
198
+ has_error = True
199
+ self._handle_assistant_message(message, accumulated_text)
200
+
201
+ elif isinstance(message, UserMessage):
202
+ # Tool results arrive as ToolResultBlocks inside UserMessage —
203
+ # NOT inside AssistantMessage. Without this branch the display
204
+ # never sees tool completion (✓) events.
205
+ self._handle_user_message(message)
206
+
207
+ elif isinstance(message, ResultMessage):
208
+ # Check for errors in the result
209
+ msg_errors = getattr(message, "errors", None)
210
+ api_error = getattr(message, "api_error_status", None)
211
+ if (msg_errors or api_error) and not has_error:
212
+ has_error = True
213
+ self._emit_event(SessionEvent(
214
+ type="error",
215
+ content=f"Result error: {msg_errors or api_error}",
216
+ ))
217
+ final_verdict = self._handle_result_message(
218
+ message, accumulated_text,
219
+ )
220
+
221
+ elif isinstance(message, SystemMessage):
222
+ pass # system messages are informational
223
+
224
+ # Note: TaskUsage (TypedDict) is not yielded by receive_response()
225
+
226
+ # Fallback if no structured verdict
227
+ if final_verdict is None:
228
+ full_text = "\n".join(accumulated_text)
229
+ final_verdict = CollaborationVerdict(
230
+ status="error" if has_error else ("success" if full_text else "insufficient_info"),
231
+ summary="No structured verdict returned",
232
+ details=full_text,
233
+ confidence=0.5,
234
+ )
235
+
236
+ return final_verdict
237
+
238
+ def _handle_stream_event(self, event: Any) -> None:
239
+ """Process a StreamEvent (partial messages)."""
240
+ # StreamEvent has an .event dict attribute; a raw dict is also
241
+ # accepted (pass-through). Normalize to a dict ONCE: the body calls
242
+ # .get() unconditionally, and an .event that is None or a typed object
243
+ # (has .type but no .get) would otherwise raise AttributeError deep in
244
+ # the stream loop and abort the whole query.
245
+ raw = getattr(event, "event", None)
246
+ ev: dict = raw if isinstance(raw, dict) else (event if isinstance(event, dict) else {})
247
+ event_type = ev.get("type", "")
248
+
249
+ if event_type == "content_block_delta":
250
+ delta = ev.get("delta", {})
251
+ delta_type = delta.get("type", "")
252
+ if delta_type == "text_delta":
253
+ text = delta.get("text", "")
254
+ if text:
255
+ self._emit_event(SessionEvent(
256
+ type="text",
257
+ content=text,
258
+ metadata={"partial": True},
259
+ ))
260
+ elif delta_type == "input_json_delta":
261
+ partial_json = delta.get("partial_json", "")
262
+ if partial_json:
263
+ self._emit_event(SessionEvent(
264
+ type="tool_call",
265
+ content=partial_json,
266
+ metadata={"partial": True, "delta_type": "input_json"},
267
+ ))
268
+
269
+ elif event_type == "content_block_start":
270
+ block = ev.get("content_block", {})
271
+ if block.get("type") == "tool_use":
272
+ # Count increments only on ToolUseBlock in AssistantMessage (dedup)
273
+ tool_id = block.get("id", "")
274
+ tool_name = block.get("name", "?")
275
+ if tool_id:
276
+ self._tool_names_by_id[tool_id] = tool_name
277
+ self._emit_event(SessionEvent(
278
+ type="tool_call",
279
+ content=f"Starting tool: {tool_name}",
280
+ metadata={
281
+ "tool_name": tool_name,
282
+ "tool_id": tool_id,
283
+ "event": "start",
284
+ },
285
+ ))
286
+
287
+ elif event_type == "content_block_stop":
288
+ pass # block completed
289
+
290
+ def _handle_assistant_message(
291
+ self, message: Any, accumulated_text: list[str]
292
+ ) -> None:
293
+ """Process an AssistantMessage containing content blocks."""
294
+ from claude_agent_sdk import TextBlock, ToolResultBlock, ToolUseBlock
295
+
296
+ for block in getattr(message, "content", []) or []:
297
+ if isinstance(block, TextBlock):
298
+ text = getattr(block, "text", str(block))
299
+ if text:
300
+ accumulated_text.append(text)
301
+ self._emit_event(SessionEvent(
302
+ type="text",
303
+ content=text,
304
+ ))
305
+
306
+ elif isinstance(block, ToolUseBlock):
307
+ tool_name = getattr(block, "name", "?")
308
+ tool_input = getattr(block, "input", {})
309
+ tool_id = getattr(block, "id", "")
310
+ # SDK internal tools (StructuredOutput from output_format) are
311
+ # not user-facing tool calls — but their input IS the verdict.
312
+ # Keep it: if the final ResultMessage arrives as an error
313
+ # (e.g. budget exceeded), structured_output is dropped by the
314
+ # SDK and this captured copy is the only surviving verdict.
315
+ if tool_name in {"StructuredOutput", "output", "output_json"}:
316
+ if isinstance(tool_input, dict) and tool_input:
317
+ self._structured_candidate = tool_input
318
+ continue
319
+ if tool_id:
320
+ self._tool_names_by_id[tool_id] = tool_name
321
+ self._tool_calls_count += 1
322
+ self._emit_event(SessionEvent(
323
+ type="tool_call",
324
+ content=f"Tool: {tool_name}",
325
+ metadata={
326
+ "tool_name": tool_name,
327
+ "tool_id": tool_id,
328
+ "input": tool_input,
329
+ "event": "complete",
330
+ },
331
+ ))
332
+
333
+ elif isinstance(block, ToolResultBlock):
334
+ self._emit_tool_result(block)
335
+
336
+ def _handle_user_message(self, message: Any) -> None:
337
+ """Process a UserMessage — extracts ToolResultBlocks (tool completions)."""
338
+ from claude_agent_sdk import ToolResultBlock
339
+
340
+ content = getattr(message, "content", None)
341
+ if not isinstance(content, list):
342
+ return
343
+ for block in content:
344
+ if isinstance(block, ToolResultBlock):
345
+ self._emit_tool_result(block)
346
+
347
+ def _emit_tool_result(self, block: Any) -> None:
348
+ """Emit a tool_result event from a ToolResultBlock."""
349
+ tool_use_id = getattr(block, "tool_use_id", "?")
350
+ content = getattr(block, "content", "")
351
+ is_error = bool(getattr(block, "is_error", False))
352
+ # content can be str | list[{"type": "text", "text": ...}] | None
353
+ if isinstance(content, list):
354
+ content = "\n".join(
355
+ str(part.get("text", "")) for part in content
356
+ if isinstance(part, dict)
357
+ )
358
+ self._emit_event(SessionEvent(
359
+ type="tool_result",
360
+ content=str(content or "")[:500],
361
+ metadata={
362
+ "tool_use_id": tool_use_id,
363
+ "tool_name": self._tool_names_by_id.get(tool_use_id, "?"),
364
+ "is_error": is_error,
365
+ },
366
+ ))
367
+
368
+ def _handle_result_message(
369
+ self, message: Any, accumulated_text: Optional[list[str]] = None,
370
+ ) -> CollaborationVerdict:
371
+ """Extract a CollaborationVerdict from a ResultMessage.
372
+
373
+ Salvage order: structured_output → captured StructuredOutput tool
374
+ input → result text → accumulated assistant text → failure.
375
+ A late error (budget exceeded, max turns) arrives AFTER the
376
+ analysis is complete — the completed work must survive it.
377
+ """
378
+ # Capture usage/cost for the session summary
379
+ self._last_cost_usd = float(getattr(message, "total_cost_usd", 0.0) or 0.0)
380
+ usage = getattr(message, "usage", None)
381
+ if isinstance(usage, dict):
382
+ # Claude Code Agent is Anthropic-backed: input_tokens EXCLUDES
383
+ # cache_creation_input_tokens and cache_read_input_tokens
384
+ # (separate accounting — see _shared_utils._CACHE_TOKENS_SEPARATE).
385
+ # Summing only input+output silently underreports total consumption
386
+ # for cache-heavy collaboration sessions.
387
+ self._last_total_tokens = (
388
+ int(usage.get("input_tokens", 0) or 0)
389
+ + int(usage.get("cache_creation_input_tokens", 0) or 0)
390
+ + int(usage.get("cache_read_input_tokens", 0) or 0)
391
+ + int(usage.get("output_tokens", 0) or 0)
392
+ )
393
+
394
+ errors = getattr(message, "errors", []) or []
395
+ error_note = "; ".join(str(e) for e in errors)
396
+
397
+ def _emit(verdict: CollaborationVerdict) -> CollaborationVerdict:
398
+ self._emit_event(SessionEvent(
399
+ type="verdict",
400
+ content=f"Status: {verdict.status} | {verdict.summary}",
401
+ metadata={"verdict": verdict.to_dict()},
402
+ ))
403
+ return verdict
404
+
405
+ # 1. Structured output from output_format option
406
+ structured = getattr(message, "structured_output", None)
407
+ if isinstance(structured, dict):
408
+ verdict = CollaborationVerdict.from_result_message(structured)
409
+ if error_note:
410
+ # Preserve a late error (e.g. budget exceeded) for parity with
411
+ # the structured_candidate salvage path below — otherwise the
412
+ # error context is silently dropped on the success path.
413
+ verdict.metadata["result_error"] = error_note
414
+ return _emit(verdict)
415
+
416
+ # 2. StructuredOutput tool input captured from the stream —
417
+ # the SDK drops structured_output on error ResultMessages
418
+ # (e.g. 'Reached maximum budget') even though the verdict
419
+ # was already produced.
420
+ if self._structured_candidate:
421
+ verdict = CollaborationVerdict.from_result_message(
422
+ self._structured_candidate,
423
+ )
424
+ if error_note:
425
+ verdict.metadata["result_error"] = error_note
426
+ return _emit(verdict)
427
+
428
+ # 3. Result text (from unstructured output)
429
+ result_text = str(getattr(message, "result", "") or "")
430
+ first_line = result_text.split("\n")[0][:80] if result_text else ""
431
+ if result_text:
432
+ return _emit(CollaborationVerdict(
433
+ status="needs_review" if not getattr(message, 'is_error', False) else "failure",
434
+ summary=first_line or "Unstructured result",
435
+ details=result_text,
436
+ confidence=0.5,
437
+ ))
438
+
439
+ # 4. No result, but the assistant already streamed its analysis —
440
+ # return it for review instead of discarding the whole session.
441
+ full_text = "\n".join(accumulated_text or []).strip()
442
+ if full_text:
443
+ return _emit(CollaborationVerdict(
444
+ status="needs_review",
445
+ summary=(
446
+ f"Session ended with error after analysis: {error_note}"
447
+ if error_note else "Analysis text without structured verdict"
448
+ ),
449
+ details=full_text,
450
+ confidence=0.5,
451
+ metadata={"result_error": error_note} if error_note else {},
452
+ ))
453
+
454
+ # 5. Truly empty — report the error
455
+ is_error = getattr(message, 'is_error', False)
456
+ if is_error:
457
+ verdict = CollaborationVerdict(
458
+ status="failure",
459
+ summary="Execution failed",
460
+ details=error_note or "Unknown error",
461
+ confidence=1.0,
462
+ )
463
+ else:
464
+ verdict = CollaborationVerdict(
465
+ status="insufficient_info",
466
+ summary="No result returned",
467
+ confidence=0.0,
468
+ )
469
+ return _emit(verdict)
470
+
471
+ async def interrupt(self) -> None:
472
+ """Interrupt the current Claude Code Agent task."""
473
+ if self._client is not None:
474
+ await self._client.interrupt()
475
+ self._emit_event(SessionEvent(type="status", content="INTERRUPTED"))
476
+
477
+ def _emit_event(self, event: SessionEvent) -> None:
478
+ """Emit an event to internal list and optional callback."""
479
+ self._events.append(event)
480
+ if self._event_callback:
481
+ try:
482
+ self._event_callback(event)
483
+ except Exception as ex:
484
+ logger.debug("Event callback error: %s", ex)