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.
- asi.py +9644 -0
- asicode-0.2.6.dist-info/METADATA +208 -0
- asicode-0.2.6.dist-info/RECORD +340 -0
- asicode-0.2.6.dist-info/WHEEL +5 -0
- asicode-0.2.6.dist-info/entry_points.txt +2 -0
- asicode-0.2.6.dist-info/licenses/LICENSE +21 -0
- asicode-0.2.6.dist-info/top_level.txt +11 -0
- common.py +86 -0
- config.py +162 -0
- context_collector.py +388 -0
- diff_apply.py +1549 -0
- external_llm/__init__.py +53 -0
- external_llm/agent/__init__.py +1 -0
- external_llm/agent/_shared_utils.py +1713 -0
- external_llm/agent/_thread_pool.py +13 -0
- external_llm/agent/_user_intent.py +233 -0
- external_llm/agent/agent_context_manager.py +532 -0
- external_llm/agent/agent_escalation_pipeline.py +161 -0
- external_llm/agent/agent_fast_path.py +63 -0
- external_llm/agent/agent_loop.py +3567 -0
- external_llm/agent/agent_loop_types.py +207 -0
- external_llm/agent/agent_phase_manager.py +352 -0
- external_llm/agent/agent_planner_pipeline.py +1756 -0
- external_llm/agent/agent_profile.py +200 -0
- external_llm/agent/agent_turn_pipeline.py +2592 -0
- external_llm/agent/alignment_scorer.py +333 -0
- external_llm/agent/anchor_shared.py +310 -0
- external_llm/agent/argument_repairer.py +119 -0
- external_llm/agent/ast_cache.py +148 -0
- external_llm/agent/ast_engine.py +1060 -0
- external_llm/agent/async_tool_executor.py +182 -0
- external_llm/agent/auto_correction.py +2268 -0
- external_llm/agent/background_job_manager.py +571 -0
- external_llm/agent/call_graph.py +516 -0
- external_llm/agent/checkpoint_store.py +472 -0
- external_llm/agent/config/__init__.py +0 -0
- external_llm/agent/config/thresholds.py +409 -0
- external_llm/agent/context_budget.py +593 -0
- external_llm/agent/context_contract.py +231 -0
- external_llm/agent/context_manager.py +1301 -0
- external_llm/agent/context_utils.py +29 -0
- external_llm/agent/design_chat_loop.py +2510 -0
- external_llm/agent/enums.py +48 -0
- external_llm/agent/execution_mode_classifier.py +323 -0
- external_llm/agent/execution_spec.py +355 -0
- external_llm/agent/execution_thresholds.py +281 -0
- external_llm/agent/failure_classifier.py +160 -0
- external_llm/agent/failure_context.py +409 -0
- external_llm/agent/failure_pattern_store.py +911 -0
- external_llm/agent/file_cache.py +144 -0
- external_llm/agent/fix_spec_claim_validator.py +747 -0
- external_llm/agent/gsg_safety.py +114 -0
- external_llm/agent/guard_ir.py +985 -0
- external_llm/agent/handoff_observer.py +327 -0
- external_llm/agent/insights_manager.py +1124 -0
- external_llm/agent/intent_models.py +250 -0
- external_llm/agent/intent_resolver.py +861 -0
- external_llm/agent/interrupt_tool_results.py +95 -0
- external_llm/agent/json_repair.py +283 -0
- external_llm/agent/language_backend.py +612 -0
- external_llm/agent/language_hint.py +51 -0
- external_llm/agent/learned_policy.py +826 -0
- external_llm/agent/light_repo_index.py +231 -0
- external_llm/agent/lint_runner.py +397 -0
- external_llm/agent/llm_output_utils.py +52 -0
- external_llm/agent/local_assistant.py +804 -0
- external_llm/agent/message_shapes.py +89 -0
- external_llm/agent/metadata_utils.py +14 -0
- external_llm/agent/operation_models.py +2707 -0
- external_llm/agent/orchestrator.py +5109 -0
- external_llm/agent/output_normalizer.py +415 -0
- external_llm/agent/performance_metrics.py +452 -0
- external_llm/agent/placement_contract.py +2765 -0
- external_llm/agent/plan_state.py +136 -0
- external_llm/agent/planner_lane_facade.py +67 -0
- external_llm/agent/rag_configs.py +168 -0
- external_llm/agent/rag_searcher.py +693 -0
- external_llm/agent/reasoning_utils.py +62 -0
- external_llm/agent/repair_helpers.py +475 -0
- external_llm/agent/request_intent_classifier.py +95 -0
- external_llm/agent/routing_policy.py +157 -0
- external_llm/agent/run_store.py +2356 -0
- external_llm/agent/scanner_registry.py +610 -0
- external_llm/agent/selector_constants.py +26 -0
- external_llm/agent/semantic_intent.py +163 -0
- external_llm/agent/semantic_lint.py +94 -0
- external_llm/agent/session_state.py +80 -0
- external_llm/agent/slash_commands.py +90 -0
- external_llm/agent/subagent_ipc.py +1222 -0
- external_llm/agent/symbol_engine.py +592 -0
- external_llm/agent/symbol_index.py +380 -0
- external_llm/agent/symbol_locator.py +117 -0
- external_llm/agent/symbol_modify_tool.py +1258 -0
- external_llm/agent/symbol_search.py +1881 -0
- external_llm/agent/task_router.py +751 -0
- external_llm/agent/terminal_coordination.py +43 -0
- external_llm/agent/termination_policy.py +284 -0
- external_llm/agent/test_impact_selector.py +411 -0
- external_llm/agent/test_runner.py +450 -0
- external_llm/agent/tool_chain.py +36 -0
- external_llm/agent/tool_dependency_graph.py +169 -0
- external_llm/agent/tool_failure_log.py +497 -0
- external_llm/agent/tool_handlers/__init__.py +25 -0
- external_llm/agent/tool_handlers/agent_tools.py +289 -0
- external_llm/agent/tool_handlers/analysis_tools.py +1343 -0
- external_llm/agent/tool_handlers/ast_op_executor.py +1233 -0
- external_llm/agent/tool_handlers/browser_tools.py +325 -0
- external_llm/agent/tool_handlers/constants.py +10 -0
- external_llm/agent/tool_handlers/git_tools.py +1031 -0
- external_llm/agent/tool_handlers/read_tools.py +530 -0
- external_llm/agent/tool_handlers/shell_policy.py +17 -0
- external_llm/agent/tool_handlers/test_tools.py +228 -0
- external_llm/agent/tool_handlers/web_search_tools.py +769 -0
- external_llm/agent/tool_handlers/write_tools.py +6213 -0
- external_llm/agent/tool_registry.py +2268 -0
- external_llm/agent/tool_result_cache.py +178 -0
- external_llm/agent/tool_safety.py +956 -0
- external_llm/agent/tool_schemas.py +1176 -0
- external_llm/agent/vector_cache.py +614 -0
- external_llm/agent/weight_learning.py +2929 -0
- external_llm/agent/work_state_digest.py +157 -0
- external_llm/analysis/__init__.py +0 -0
- external_llm/analysis/_dead_block_shared.py +848 -0
- external_llm/analysis/ast_similarity_scanner.py +1128 -0
- external_llm/analysis/broken_contract_scanner.py +591 -0
- external_llm/analysis/container_reachability_scanner.py +792 -0
- external_llm/analysis/contradictory_logic_scanner.py +693 -0
- external_llm/analysis/cross_file_refs.py +430 -0
- external_llm/analysis/dead_block_scanner.py +97 -0
- external_llm/analysis/duplicate_definition_scanner.py +353 -0
- external_llm/analysis/parse_cache.py +110 -0
- external_llm/analysis/public_dead_code_scanner.py +106 -0
- external_llm/analysis/unused_import_scanner.py +385 -0
- external_llm/analysis/vulture_scanner.py +445 -0
- external_llm/anthropic_client.py +1235 -0
- external_llm/ast_rewrite.py +322 -0
- external_llm/client.py +485 -0
- external_llm/code_analyzer.py +302 -0
- external_llm/code_structure_utils.py +1554 -0
- external_llm/common/__init__.py +0 -0
- external_llm/common/atomic_io.py +240 -0
- external_llm/common/file_lock.py +134 -0
- external_llm/common/indent_utils.py +839 -0
- external_llm/context/context_packs.py +60 -0
- external_llm/context_builder.py +411 -0
- external_llm/dependency_graph.py +303 -0
- external_llm/design_session.py +629 -0
- external_llm/edit_localization/__init__.py +26 -0
- external_llm/edit_localization/dataflow_extractor.py +472 -0
- external_llm/edit_localization/graph_propagator.py +241 -0
- external_llm/edit_localization/relevance_scorer.py +310 -0
- external_llm/edit_localization/request_analyzer.py +89 -0
- external_llm/editor/_editor_core/__init__.py +0 -0
- external_llm/editor/_editor_core/common/__init__.py +0 -0
- external_llm/editor/_editor_core/common/import_normalizer.py +376 -0
- external_llm/editor/_editor_core/ts_vm/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/ast_rewriter.py +118 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/models.py +27 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/rollback_manager.py +41 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/verifier.py +212 -0
- external_llm/editor/_editor_core/ts_vm/execution_vm/vm.py +193 -0
- external_llm/editor/_editor_core/ts_vm/learning/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/learning/exploration_policy.py +43 -0
- external_llm/editor/_editor_core/ts_vm/primitives/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/primitives/executor.py +66 -0
- external_llm/editor/_editor_core/ts_vm/primitives/models.py +21 -0
- external_llm/editor/_editor_core/ts_vm/repair/__init__.py +0 -0
- external_llm/editor/_editor_core/ts_vm/repair/failure_classifier.py +88 -0
- external_llm/editor/_editor_core/ts_vm/repair/repair_planner.py +94 -0
- external_llm/editor/_editor_core/ts_vm/repair/repair_registry.py +42 -0
- external_llm/editor/_editor_core/ts_vm/repair/repair_strategies.py +113 -0
- external_llm/editor/_editor_core/vm/__init__.py +36 -0
- external_llm/editor/_editor_core/vm/ast_rewriter.py +99 -0
- external_llm/editor/_editor_core/vm/classification.py +70 -0
- external_llm/editor/_editor_core/vm/contracts/__init__.py +3 -0
- external_llm/editor/_editor_core/vm/contracts/contract_diff.py +225 -0
- external_llm/editor/_editor_core/vm/contracts/contract_extractor.py +271 -0
- external_llm/editor/_editor_core/vm/contracts/contract_models.py +93 -0
- external_llm/editor/_editor_core/vm/contracts/project_graph.py +201 -0
- external_llm/editor/_editor_core/vm/contracts/propagation_planner.py +237 -0
- external_llm/editor/_editor_core/vm/failure_classifier.py +390 -0
- external_llm/editor/_editor_core/vm/models.py +28 -0
- external_llm/editor/_editor_core/vm/repair_planner.py +91 -0
- external_llm/editor/_editor_core/vm/repair_registry.py +34 -0
- external_llm/editor/_editor_core/vm/repair_strategies.py +1049 -0
- external_llm/editor/_editor_core/vm/rollback_manager.py +40 -0
- external_llm/editor/_editor_core/vm/verifier.py +381 -0
- external_llm/editor/_editor_core/vm/vm.py +326 -0
- external_llm/editor/agent/autonomous/__init__.py +45 -0
- external_llm/editor/agent/autonomous/proactive_runner.py +386 -0
- external_llm/editor/agent/autonomous/push_manager.py +299 -0
- external_llm/editor/agent/autonomous/task_queue.py +225 -0
- external_llm/editor/agent/autonomous/trigger_engine.py +175 -0
- external_llm/editor/agent/autonomous/trigger_policy.py +294 -0
- external_llm/editor/agent/mcp/__init__.py +13 -0
- external_llm/editor/agent/mcp/server.py +175 -0
- external_llm/editor/agent/prompts/__init__.py +2 -0
- external_llm/editor/agent/prompts/edit_prompts.py +299 -0
- external_llm/editor/cross_language/__init__.py +4 -0
- external_llm/editor/cross_language/models.py +79 -0
- external_llm/editor/cross_language/strategy_abstraction.py +204 -0
- external_llm/editor/cross_language/transfer_engine.py +170 -0
- external_llm/editor/learning/__init__.py +1 -0
- external_llm/editor/learning/experience_store.py +343 -0
- external_llm/editor/learning/exploration_state.py +124 -0
- external_llm/editor/learning/learning_sink.py +131 -0
- external_llm/editor/learning/pattern_extractor.py +158 -0
- external_llm/editor/learning/primitive_learning_models.py +125 -0
- external_llm/editor/learning/primitive_learning_scorer.py +145 -0
- external_llm/editor/learning/primitive_learning_store.py +142 -0
- external_llm/editor/learning/primitive_learning_updater.py +128 -0
- external_llm/editor/learning/primitive_sequence_models.py +58 -0
- external_llm/editor/learning/primitive_sequence_scorer.py +117 -0
- external_llm/editor/learning/primitive_sequence_store.py +115 -0
- external_llm/editor/learning/primitive_sequence_updater.py +74 -0
- external_llm/editor/learning/problem_signature.py +161 -0
- external_llm/editor/learning/strategy_execution_bias.py +345 -0
- external_llm/editor/learning/strategy_prioritizer.py +245 -0
- external_llm/editor/learning/strategy_state.py +128 -0
- external_llm/editor/learning/unified_run_record.py +118 -0
- external_llm/editor/learning/unified_store.py +566 -0
- external_llm/editor/primitives/__init__.py +2 -0
- external_llm/editor/primitives/code_context.py +660 -0
- external_llm/editor/primitives/delete_node.py +41 -0
- external_llm/editor/primitives/executor.py +106 -0
- external_llm/editor/primitives/insert_import.py +90 -0
- external_llm/editor/primitives/insert_statement.py +77 -0
- external_llm/editor/primitives/models.py +87 -0
- external_llm/editor/primitives/registry.py +49 -0
- external_llm/editor/primitives/rename_symbol.py +91 -0
- external_llm/editor/primitives/replace_function.py +260 -0
- external_llm/editor/primitives/update_call.py +76 -0
- external_llm/editor/semantic/__init__.py +1 -0
- external_llm/editor/semantic/ast_transform_utils.py +450 -0
- external_llm/editor/semantic/contract_replan_strategy.py +98 -0
- external_llm/editor/semantic/draft_parser.py +206 -0
- external_llm/editor/semantic/flow_synthesizer.py +165 -0
- external_llm/editor/semantic/fragment_generator.py +21 -0
- external_llm/editor/semantic/fragment_integrator.py +163 -0
- external_llm/editor/semantic/generation_planner.py +19 -0
- external_llm/editor/semantic/libcst_transform_utils.py +254 -0
- external_llm/editor/semantic/output_comparator.py +90 -0
- external_llm/editor/semantic/primitive_code_templates.py +307 -0
- external_llm/editor/semantic/primitive_detector.py +402 -0
- external_llm/editor/semantic/primitive_ir_builder.py +109 -0
- external_llm/editor/semantic/primitive_models.py +108 -0
- external_llm/editor/semantic/primitive_reconstructor.py +264 -0
- external_llm/editor/semantic/primitive_registry.py +328 -0
- external_llm/editor/semantic/semantic_contract_evaluator.py +321 -0
- external_llm/editor/semantic/semantic_contract_models.py +124 -0
- external_llm/editor/semantic/semantic_contract_registry.py +134 -0
- external_llm/editor/semantic/semantic_contracts.py +72 -0
- external_llm/editor/semantic/semantic_corrector.py +75 -0
- external_llm/editor/semantic/semantic_gap_analyzer.py +194 -0
- external_llm/editor/semantic/semantic_rewrite_models.py +62 -0
- external_llm/editor/semantic/semantic_rewrite_planner.py +252 -0
- external_llm/editor/semantic/semantic_rewriter.py +304 -0
- external_llm/editor/semantic/semantic_tracer.py +427 -0
- external_llm/editor/semantic/synthesis_planner.py +99 -0
- external_llm/editor/semantic/ts_ir_models.py +414 -0
- external_llm/editor/semantic/ts_semantic_models.py +112 -0
- external_llm/editor/semantic/ts_semantic_tracer.py +1302 -0
- external_llm/editor/simulator/dependency_traversal.py +172 -0
- external_llm/editor/simulator/impact_models.py +42 -0
- external_llm/editor/simulator/impact_simulator.py +187 -0
- external_llm/editor/simulator/risk_estimator.py +71 -0
- external_llm/editor/verification/__init__.py +7 -0
- external_llm/editor/verification/code_integrity.py +406 -0
- external_llm/editor/verification/impact_propagation.py +401 -0
- external_llm/editor/verification/impact_verification_mapper.py +148 -0
- external_llm/editor/verification/verification_set_builder.py +329 -0
- external_llm/graph/composite_risk.py +226 -0
- external_llm/graph/execution_graph_advisor.py +636 -0
- external_llm/graph/graph_builder.py +31 -0
- external_llm/graph/graph_facade.py +198 -0
- external_llm/graph/models.py +113 -0
- external_llm/graph/repository_graph.py +1061 -0
- external_llm/graph/run_scoped_graph_cache.py +283 -0
- external_llm/graph/spec_graph_enricher.py +855 -0
- external_llm/graph/virtual_graph.py +429 -0
- external_llm/hybrid_parser.py +215 -0
- external_llm/image_utils.py +147 -0
- external_llm/intelligent_service.py +2147 -0
- external_llm/languages/__init__.py +24 -0
- external_llm/languages/base.py +450 -0
- external_llm/languages/bash_provider.py +94 -0
- external_llm/languages/capabilities.py +129 -0
- external_llm/languages/csharp_provider.py +111 -0
- external_llm/languages/css_provider.py +178 -0
- external_llm/languages/dependency_checker.py +753 -0
- external_llm/languages/go_provider.py +452 -0
- external_llm/languages/html_provider.py +136 -0
- external_llm/languages/java_provider.py +485 -0
- external_llm/languages/javascript_provider.py +334 -0
- external_llm/languages/json_provider.py +113 -0
- external_llm/languages/kotlin_provider.py +462 -0
- external_llm/languages/libcst_utils.py +699 -0
- external_llm/languages/models.py +195 -0
- external_llm/languages/php_provider.py +90 -0
- external_llm/languages/python_provider.py +390 -0
- external_llm/languages/registry.py +128 -0
- external_llm/languages/ruby_provider.py +90 -0
- external_llm/languages/rust_provider.py +89 -0
- external_llm/languages/swift_provider.py +95 -0
- external_llm/languages/syntax_validator.py +357 -0
- external_llm/languages/tree_sitter_utils.py +2140 -0
- external_llm/languages/typescript_provider.py +694 -0
- external_llm/model_registry.py +153 -0
- external_llm/multi_planner.py +744 -0
- external_llm/ollama_api.py +126 -0
- external_llm/openai_client.py +1281 -0
- external_llm/output_modes.py +29 -0
- external_llm/output_parser.py +760 -0
- external_llm/patch_engine.py +2482 -0
- external_llm/patch_synthesizer.py +251 -0
- external_llm/project_analyzer.py +1386 -0
- external_llm/providers.py +2112 -0
- external_llm/repl/collaborate/__init__.py +46 -0
- external_llm/repl/collaborate/asi_mcp_adapter.py +550 -0
- external_llm/repl/collaborate/claude_session.py +484 -0
- external_llm/repl/collaborate/cli.py +257 -0
- external_llm/repl/collaborate/collaboration_orchestrator.py +422 -0
- external_llm/repl/collaborate/streaming_display.py +667 -0
- external_llm/repl/collaborate/verdict.py +145 -0
- external_llm/semantic_patch.py +190 -0
- external_llm/service.py +1690 -0
- external_llm/smart_analyzer.py +466 -0
- external_llm/super_context_builder.py +644 -0
- external_llm/testing/__init__.py +0 -0
- external_llm/testing/symbol_aware_test_finder.py +377 -0
- external_llm/testing/test_dependency_graph.py +380 -0
- patch_synth.py +1587 -0
- path_security.py +143 -0
- plan_compiler.py +1058 -0
- services/__init__.py +0 -0
- services/patch_helpers.py +151 -0
- utils/__init__.py +1 -0
- utils/llm_utils.py +22 -0
- 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)
|