klaude-code 1.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 (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncGenerator, Iterable
4
+ from dataclasses import dataclass
5
+ from typing import Protocol
6
+
7
+ from klaude_code.core.prompt import get_system_prompt as load_system_prompt
8
+ from klaude_code.core.reminders import Reminder, load_agent_reminders
9
+ from klaude_code.core.task import TaskExecutionContext, TaskExecutor
10
+ from klaude_code.core.tool import TodoContext, get_registry, load_agent_tools
11
+ from klaude_code.llm import LLMClientABC
12
+ from klaude_code.protocol import events, llm_param, model, tools
13
+ from klaude_code.protocol.model import UserInputPayload
14
+ from klaude_code.session import Session
15
+ from klaude_code.trace import DebugType, log_debug
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class AgentProfile:
20
+ """Encapsulates the active LLM client plus prompts/tools/reminders."""
21
+
22
+ llm_client: LLMClientABC
23
+ system_prompt: str | None
24
+ tools: list[llm_param.ToolSchema]
25
+ reminders: list[Reminder]
26
+
27
+
28
+ class ModelProfileProvider(Protocol):
29
+ """Strategy interface for constructing agent profiles."""
30
+
31
+ def build_profile(
32
+ self,
33
+ llm_client: LLMClientABC,
34
+ sub_agent_type: tools.SubAgentType | None = None,
35
+ ) -> AgentProfile: ...
36
+
37
+
38
+ class DefaultModelProfileProvider(ModelProfileProvider):
39
+ """Default provider backed by global prompts/tool/reminder registries."""
40
+
41
+ def build_profile(
42
+ self,
43
+ llm_client: LLMClientABC,
44
+ sub_agent_type: tools.SubAgentType | None = None,
45
+ ) -> AgentProfile:
46
+ model_name = llm_client.model_name
47
+ return AgentProfile(
48
+ llm_client=llm_client,
49
+ system_prompt=load_system_prompt(model_name, sub_agent_type),
50
+ tools=load_agent_tools(model_name, sub_agent_type),
51
+ reminders=load_agent_reminders(model_name, sub_agent_type),
52
+ )
53
+
54
+
55
+ class VanillaModelProfileProvider(ModelProfileProvider):
56
+ """Provider that strips prompts, reminders, and tools for vanilla mode."""
57
+
58
+ def build_profile(
59
+ self,
60
+ llm_client: LLMClientABC,
61
+ sub_agent_type: tools.SubAgentType | None = None,
62
+ ) -> AgentProfile:
63
+ model_name = llm_client.model_name
64
+ return AgentProfile(
65
+ llm_client=llm_client,
66
+ system_prompt=None,
67
+ tools=load_agent_tools(model_name, vanilla=True),
68
+ reminders=load_agent_reminders(model_name, vanilla=True),
69
+ )
70
+
71
+
72
+ class Agent:
73
+ def __init__(
74
+ self,
75
+ session: Session,
76
+ profile: AgentProfile,
77
+ ):
78
+ self.session: Session = session
79
+ self.profile: AgentProfile | None = None
80
+ # Active task executor, if any
81
+ self._current_task: TaskExecutor | None = None
82
+ # Ensure runtime configuration matches the active model on initialization
83
+ self.set_model_profile(profile)
84
+
85
+ def cancel(self) -> Iterable[events.Event]:
86
+ """Handle agent cancellation and persist an interrupt marker and tool cancellations.
87
+
88
+ - Appends an `InterruptItem` into the session history so interruptions are reflected
89
+ in persisted conversation logs.
90
+ - For any tool calls that are pending or in-progress in the current task, delegate to
91
+ the active TaskExecutor to append synthetic ToolResultItem entries with error status
92
+ to indicate cancellation.
93
+ """
94
+ # First, cancel any running task so it stops emitting events.
95
+ if self._current_task is not None:
96
+ for ui_event in self._current_task.cancel():
97
+ yield ui_event
98
+ self._current_task = None
99
+
100
+ # Record an interrupt marker in the session history
101
+ self.session.append_history([model.InterruptItem()])
102
+ log_debug(
103
+ f"Session {self.session.id} interrupted",
104
+ style="yellow",
105
+ debug_type=DebugType.EXECUTION,
106
+ )
107
+
108
+ async def run_task(self, user_input: UserInputPayload) -> AsyncGenerator[events.Event, None]:
109
+ context = TaskExecutionContext(
110
+ session_id=self.session.id,
111
+ profile=self._require_profile(),
112
+ get_conversation_history=lambda: self.session.conversation_history,
113
+ append_history=self.session.append_history,
114
+ tool_registry=get_registry(),
115
+ file_tracker=self.session.file_tracker,
116
+ todo_context=TodoContext(
117
+ get_todos=lambda: self.session.todos,
118
+ set_todos=lambda todos: setattr(self.session, "todos", todos),
119
+ ),
120
+ process_reminder=self._process_reminder,
121
+ sub_agent_state=self.session.sub_agent_state,
122
+ )
123
+
124
+ task = TaskExecutor(context)
125
+ self._current_task = task
126
+
127
+ try:
128
+ async for event in task.run(user_input):
129
+ yield event
130
+ finally:
131
+ self._current_task = None
132
+
133
+ async def replay_history(self) -> AsyncGenerator[events.Event, None]:
134
+ """Yield UI events reconstructed from saved conversation history."""
135
+
136
+ if len(self.session.conversation_history) == 0:
137
+ return
138
+
139
+ yield events.ReplayHistoryEvent(
140
+ events=list(self.session.get_history_item()),
141
+ updated_at=self.session.updated_at,
142
+ session_id=self.session.id,
143
+ )
144
+
145
+ async def _process_reminder(self, reminder: Reminder) -> AsyncGenerator[events.DeveloperMessageEvent, None]:
146
+ """Process a single reminder and yield events if it produces output."""
147
+ item = await reminder(self.session)
148
+ if item is not None:
149
+ self.session.append_history([item])
150
+ yield events.DeveloperMessageEvent(session_id=self.session.id, item=item)
151
+
152
+ def set_model_profile(self, profile: AgentProfile) -> None:
153
+ """Apply a fully constructed profile to the agent."""
154
+
155
+ self.profile = profile
156
+ if not self.session.model_name:
157
+ self.session.model_name = profile.llm_client.model_name
158
+
159
+ def get_llm_client(self) -> LLMClientABC:
160
+ return self._require_profile().llm_client
161
+
162
+ def _require_profile(self) -> AgentProfile:
163
+ if self.profile is None:
164
+ raise RuntimeError("Agent profile is not initialized")
165
+ return self.profile
@@ -0,0 +1,485 @@
1
+ """
2
+ Executor module providing the core event loop and task management.
3
+
4
+ This module implements the submission_loop equivalent for klaude,
5
+ handling operations submitted from the CLI and coordinating with agents.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from dataclasses import dataclass
12
+
13
+ from klaude_code.command import InputAction, InputActionType, dispatch_command
14
+ from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
15
+ from klaude_code.core.manager import AgentManager, LLMClients, SubAgentManager
16
+ from klaude_code.core.tool import current_run_subtask_callback
17
+ from klaude_code.protocol import events, model, op
18
+ from klaude_code.protocol.op_handler import OperationHandler
19
+ from klaude_code.protocol.sub_agent import SubAgentResult
20
+ from klaude_code.trace import DebugType, log_debug
21
+
22
+
23
+ @dataclass
24
+ class ActiveTask:
25
+ """Track an in-flight task and its owning session."""
26
+
27
+ task: asyncio.Task[None]
28
+ session_id: str
29
+
30
+
31
+ class TaskManager:
32
+ """Manager that tracks active tasks keyed by submission id."""
33
+
34
+ def __init__(self) -> None:
35
+ self._tasks: dict[str, ActiveTask] = {}
36
+
37
+ def register(self, submission_id: str, task: asyncio.Task[None], session_id: str) -> None:
38
+ """Register a new active task for a submission id."""
39
+
40
+ self._tasks[submission_id] = ActiveTask(task=task, session_id=session_id)
41
+
42
+ def get(self, submission_id: str) -> ActiveTask | None:
43
+ """Return the active task for a submission id if present."""
44
+
45
+ return self._tasks.get(submission_id)
46
+
47
+ def remove(self, submission_id: str) -> None:
48
+ """Remove the active task associated with a submission id if present."""
49
+
50
+ self._tasks.pop(submission_id, None)
51
+
52
+ def values(self) -> list[ActiveTask]:
53
+ """Return a snapshot list of all active tasks."""
54
+
55
+ return list(self._tasks.values())
56
+
57
+ def cancel_tasks_for_sessions(self, session_ids: set[str] | None = None) -> list[tuple[str, asyncio.Task[None]]]:
58
+ """Collect tasks that should be cancelled for given sessions."""
59
+
60
+ tasks_to_cancel: list[tuple[str, asyncio.Task[None]]] = []
61
+ for task_id, active in list(self._tasks.items()):
62
+ task = active.task
63
+ if task.done():
64
+ continue
65
+ if session_ids is None or active.session_id in session_ids:
66
+ tasks_to_cancel.append((task_id, task))
67
+ return tasks_to_cancel
68
+
69
+ def clear(self) -> None:
70
+ """Remove all tracked tasks from the manager."""
71
+
72
+ self._tasks.clear()
73
+
74
+
75
+ class ExecutorContext:
76
+ """
77
+ Context object providing shared state and operations for the executor.
78
+
79
+ This context is passed to operations when they execute, allowing them
80
+ to access shared resources like the event queue and active sessions.
81
+
82
+ Implements the OperationHandler protocol via structural subtyping.
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ event_queue: asyncio.Queue[events.Event],
88
+ llm_clients: LLMClients,
89
+ model_profile_provider: ModelProfileProvider | None = None,
90
+ ):
91
+ self.event_queue: asyncio.Queue[events.Event] = event_queue
92
+
93
+ resolved_profile_provider = model_profile_provider or DefaultModelProfileProvider()
94
+ self.model_profile_provider: ModelProfileProvider = resolved_profile_provider
95
+
96
+ # Delegate responsibilities to helper components
97
+ self.agent_manager = AgentManager(event_queue, llm_clients, resolved_profile_provider)
98
+ self.task_manager = TaskManager()
99
+ self.subagent_manager = SubAgentManager(event_queue, llm_clients, resolved_profile_provider)
100
+
101
+ async def emit_event(self, event: events.Event) -> None:
102
+ """Emit an event to the UI display system."""
103
+ await self.event_queue.put(event)
104
+
105
+ @property
106
+ def active_agents(self) -> dict[str, Agent]:
107
+ """Expose currently active agents keyed by session id.
108
+
109
+ This property preserves the previous public attribute used by the
110
+ CLI status provider while delegating storage to :class:`AgentManager`.
111
+ """
112
+
113
+ return self.agent_manager.all_active_agents()
114
+
115
+ async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
116
+ """Initialize an agent for a session and replay history to UI."""
117
+ if operation.session_id is None:
118
+ raise ValueError("session_id cannot be None")
119
+
120
+ await self.agent_manager.ensure_agent(operation.session_id)
121
+
122
+ async def handle_user_input(self, operation: op.UserInputOperation) -> None:
123
+ """Handle a user input operation by running it through an agent."""
124
+
125
+ if operation.session_id is None:
126
+ raise ValueError("session_id cannot be None")
127
+
128
+ session_id = operation.session_id
129
+ agent = await self.agent_manager.ensure_agent(session_id)
130
+ user_input = operation.input
131
+
132
+ # emit user input event
133
+ await self.emit_event(
134
+ events.UserMessageEvent(content=user_input.text, session_id=session_id, images=user_input.images)
135
+ )
136
+
137
+ result = await dispatch_command(user_input.text, agent)
138
+
139
+ actions: list[InputAction] = list(result.actions or [])
140
+
141
+ has_run_agent_action = any(action.type is InputActionType.RUN_AGENT for action in actions)
142
+ if not has_run_agent_action:
143
+ # No async agent task will run, append user message directly
144
+ agent.session.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
145
+
146
+ if result.events:
147
+ agent.session.append_history(
148
+ [evt.item for evt in result.events if isinstance(evt, events.DeveloperMessageEvent)]
149
+ )
150
+ for evt in result.events:
151
+ await self.emit_event(evt)
152
+
153
+ for action in actions:
154
+ await self._run_input_action(action, operation, agent)
155
+
156
+ async def _run_input_action(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
157
+ if operation.session_id is None:
158
+ raise ValueError("session_id cannot be None for input actions")
159
+
160
+ session_id = operation.session_id
161
+
162
+ if action.type == InputActionType.RUN_AGENT:
163
+ task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
164
+
165
+ existing_active = self.task_manager.get(operation.id)
166
+ if existing_active is not None and not existing_active.task.done():
167
+ raise RuntimeError(f"Active task already registered for operation {operation.id}")
168
+
169
+ task: asyncio.Task[None] = asyncio.create_task(
170
+ self._run_agent_task(agent, task_input, operation.id, session_id)
171
+ )
172
+ self.task_manager.register(operation.id, task, session_id)
173
+ return
174
+
175
+ if action.type == InputActionType.CHANGE_MODEL:
176
+ if not action.model_name:
177
+ raise ValueError("ChangeModel action requires model_name")
178
+
179
+ await self.agent_manager.apply_model_change(agent, action.model_name)
180
+ return
181
+
182
+ if action.type == InputActionType.CLEAR:
183
+ await self.agent_manager.apply_clear(agent)
184
+ return
185
+
186
+ raise ValueError(f"Unsupported input action type: {action.type}")
187
+
188
+ async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
189
+ """Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
190
+
191
+ # Determine affected sessions
192
+ if operation.target_session_id is not None:
193
+ session_ids: list[str] = [operation.target_session_id]
194
+ else:
195
+ session_ids = self.agent_manager.active_session_ids()
196
+
197
+ # Call cancel() on each affected agent to persist an interrupt marker
198
+ for sid in session_ids:
199
+ agent = self.agent_manager.get_active_agent(sid)
200
+ if agent is not None:
201
+ for evt in agent.cancel():
202
+ await self.emit_event(evt)
203
+
204
+ # emit interrupt event
205
+ await self.emit_event(events.InterruptEvent(session_id=operation.target_session_id or "all"))
206
+
207
+ # Find tasks to cancel (filter by target sessions if provided)
208
+ if operation.target_session_id is None:
209
+ session_filter: set[str] | None = None
210
+ else:
211
+ session_filter = {operation.target_session_id}
212
+
213
+ tasks_to_cancel = self.task_manager.cancel_tasks_for_sessions(session_filter)
214
+
215
+ scope = operation.target_session_id or "all"
216
+ log_debug(
217
+ f"Interrupting {len(tasks_to_cancel)} task(s) for: {scope}",
218
+ style="yellow",
219
+ debug_type=DebugType.EXECUTION,
220
+ )
221
+
222
+ # Cancel the tasks
223
+ for task_id, task in tasks_to_cancel:
224
+ task.cancel()
225
+ # Remove from active tasks immediately
226
+ self.task_manager.remove(task_id)
227
+
228
+ async def _run_agent_task(
229
+ self, agent: Agent, user_input: model.UserInputPayload, task_id: str, session_id: str
230
+ ) -> None:
231
+ """
232
+ Run an agent task and forward all events to the UI.
233
+
234
+ This method wraps the agent's run_task method and handles any exceptions
235
+ that might occur during execution.
236
+ """
237
+ try:
238
+ log_debug(
239
+ f"Starting agent task {task_id} for session {session_id}",
240
+ style="green",
241
+ debug_type=DebugType.EXECUTION,
242
+ )
243
+
244
+ # Inject subtask runner into tool context for nested Task tool usage
245
+ async def _runner(state: model.SubAgentState) -> SubAgentResult:
246
+ return await self.subagent_manager.run_subagent(agent, state)
247
+
248
+ token = current_run_subtask_callback.set(_runner)
249
+ try:
250
+ # Forward all events from the agent to the UI
251
+ async for event in agent.run_task(user_input):
252
+ await self.emit_event(event)
253
+ finally:
254
+ current_run_subtask_callback.reset(token)
255
+
256
+ except asyncio.CancelledError:
257
+ # Task was cancelled (likely due to interrupt)
258
+ log_debug(
259
+ f"Agent task {task_id} was cancelled",
260
+ style="yellow",
261
+ debug_type=DebugType.EXECUTION,
262
+ )
263
+ await self.emit_event(events.TaskFinishEvent(session_id=session_id, task_result="task cancelled"))
264
+
265
+ except Exception as e:
266
+ # Handle any other exceptions
267
+ import traceback
268
+
269
+ log_debug(
270
+ f"Agent task {task_id} failed: {str(e)}",
271
+ style="red",
272
+ debug_type=DebugType.EXECUTION,
273
+ )
274
+ log_debug(traceback.format_exc(), style="red", debug_type=DebugType.EXECUTION)
275
+ await self.emit_event(
276
+ events.ErrorEvent(
277
+ error_message=f"Agent task failed: [{e.__class__.__name__}] {str(e)}",
278
+ can_retry=False,
279
+ )
280
+ )
281
+
282
+ finally:
283
+ # Clean up the task from active tasks
284
+ self.task_manager.remove(task_id)
285
+ log_debug(
286
+ f"Cleaned up agent task {task_id}",
287
+ style="cyan",
288
+ debug_type=DebugType.EXECUTION,
289
+ )
290
+
291
+ def get_active_task(self, submission_id: str) -> asyncio.Task[None] | None:
292
+ """Return the asyncio.Task for a submission id if one is registered."""
293
+
294
+ active = self.task_manager.get(submission_id)
295
+ if active is None:
296
+ return None
297
+ return active.task
298
+
299
+ def has_active_task(self, submission_id: str) -> bool:
300
+ """Return True if a task is registered for the submission id."""
301
+
302
+ return self.task_manager.get(submission_id) is not None
303
+
304
+
305
+ class Executor:
306
+ """
307
+ Core executor that processes operations submitted from the CLI.
308
+
309
+ This class implements a message loop similar to Codex-rs's submission_loop,
310
+ processing operations asynchronously and coordinating with agents.
311
+ """
312
+
313
+ def __init__(
314
+ self,
315
+ event_queue: asyncio.Queue[events.Event],
316
+ llm_clients: LLMClients,
317
+ model_profile_provider: ModelProfileProvider | None = None,
318
+ ):
319
+ self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider)
320
+ self.submission_queue: asyncio.Queue[op.Submission] = asyncio.Queue()
321
+ # Track completion events for all submissions (not just those with ActiveTask)
322
+ self._completion_events: dict[str, asyncio.Event] = {}
323
+
324
+ async def submit(self, operation: op.Operation) -> str:
325
+ """
326
+ Submit an operation to the executor for processing.
327
+
328
+ Args:
329
+ operation: Operation to submit
330
+
331
+ Returns:
332
+ Unique submission ID for tracking
333
+ """
334
+
335
+ submission = op.Submission(id=operation.id, operation=operation)
336
+ await self.submission_queue.put(submission)
337
+
338
+ # Create completion event for tracking
339
+ self._completion_events[operation.id] = asyncio.Event()
340
+
341
+ log_debug(
342
+ f"Submitted operation {operation.type} with ID {operation.id}",
343
+ style="blue",
344
+ debug_type=DebugType.EXECUTION,
345
+ )
346
+
347
+ return operation.id
348
+
349
+ async def wait_for(self, submission_id: str) -> None:
350
+ """Wait for a specific submission to complete."""
351
+ event = self._completion_events.get(submission_id)
352
+ if event is not None:
353
+ await event.wait()
354
+ self._completion_events.pop(submission_id, None)
355
+
356
+ async def submit_and_wait(self, operation: op.Operation) -> None:
357
+ """Submit an operation and wait for it to complete."""
358
+ submission_id = await self.submit(operation)
359
+ await self.wait_for(submission_id)
360
+
361
+ async def start(self) -> None:
362
+ """
363
+ Start the executor main loop.
364
+
365
+ This method runs continuously, processing submissions from the queue
366
+ until the executor is stopped.
367
+ """
368
+ log_debug("Executor started", style="green", debug_type=DebugType.EXECUTION)
369
+
370
+ while True:
371
+ try:
372
+ # Wait for next submission
373
+ submission = await self.submission_queue.get()
374
+
375
+ # Check for end operation to gracefully exit
376
+ if isinstance(submission.operation, op.EndOperation):
377
+ log_debug(
378
+ "Received EndOperation, stopping executor",
379
+ style="yellow",
380
+ debug_type=DebugType.EXECUTION,
381
+ )
382
+ break
383
+
384
+ await self._handle_submission(submission)
385
+
386
+ except asyncio.CancelledError:
387
+ # Executor was cancelled
388
+ log_debug("Executor cancelled", style="yellow", debug_type=DebugType.EXECUTION)
389
+ break
390
+
391
+ except Exception as e:
392
+ # Handle unexpected errors
393
+ log_debug(
394
+ f"Executor error: {str(e)}",
395
+ style="red",
396
+ debug_type=DebugType.EXECUTION,
397
+ )
398
+ await self.context.emit_event(
399
+ events.ErrorEvent(error_message=f"Executor error: {str(e)}", can_retry=False)
400
+ )
401
+
402
+ async def stop(self) -> None:
403
+ """Stop the executor and clean up resources."""
404
+ # Cancel all active tasks and collect them for awaiting
405
+ tasks_to_await: list[asyncio.Task[None]] = []
406
+ for active in self.context.task_manager.values():
407
+ task = active.task
408
+ if not task.done():
409
+ task.cancel()
410
+ tasks_to_await.append(task)
411
+
412
+ # Wait for all cancelled tasks to complete
413
+ if tasks_to_await:
414
+ await asyncio.gather(*tasks_to_await, return_exceptions=True)
415
+
416
+ # Clear the active task manager
417
+ self.context.task_manager.clear()
418
+
419
+ # Send EndOperation to wake up the start() loop
420
+ try:
421
+ end_operation = op.EndOperation()
422
+ submission = op.Submission(id=end_operation.id, operation=end_operation)
423
+ await self.submission_queue.put(submission)
424
+ except Exception as e:
425
+ log_debug(
426
+ f"Failed to send EndOperation: {str(e)}",
427
+ style="red",
428
+ debug_type=DebugType.EXECUTION,
429
+ )
430
+
431
+ log_debug("Executor stopped", style="yellow", debug_type=DebugType.EXECUTION)
432
+
433
+ async def _handle_submission(self, submission: op.Submission) -> None:
434
+ """
435
+ Handle a single submission by executing its operation.
436
+
437
+ This method delegates to the operation's execute method, which
438
+ can access shared resources through the executor context.
439
+ """
440
+ try:
441
+ log_debug(
442
+ f"Handling submission {submission.id} of type {submission.operation.type.value}",
443
+ style="cyan",
444
+ debug_type=DebugType.EXECUTION,
445
+ )
446
+
447
+ # Execute to spawn the agent task in context
448
+ await submission.operation.execute(handler=self.context)
449
+
450
+ task = self.context.get_active_task(submission.id)
451
+
452
+ async def _await_agent_and_complete(captured_task: asyncio.Task[None]) -> None:
453
+ try:
454
+ await captured_task
455
+ finally:
456
+ event = self._completion_events.get(submission.id)
457
+ if event is not None:
458
+ event.set()
459
+
460
+ if task is None:
461
+ event = self._completion_events.get(submission.id)
462
+ if event is not None:
463
+ event.set()
464
+ else:
465
+ # Run in background so the submission loop can continue (e.g., to handle interrupts)
466
+ asyncio.create_task(_await_agent_and_complete(task))
467
+
468
+ except Exception as e:
469
+ log_debug(
470
+ f"Failed to handle submission {submission.id}: {str(e)}",
471
+ style="red",
472
+ debug_type=DebugType.EXECUTION,
473
+ )
474
+ await self.context.emit_event(
475
+ events.ErrorEvent(error_message=f"Operation failed: {str(e)}", can_retry=False)
476
+ )
477
+ # Set completion event even on error to prevent wait_for_completion from hanging
478
+ event = self._completion_events.get(submission.id)
479
+ if event is not None:
480
+ event.set()
481
+
482
+
483
+ # Static type check: ExecutorContext must satisfy OperationHandler protocol.
484
+ # If this line causes a type error, ExecutorContext is missing required methods.
485
+ _: type[OperationHandler] = ExecutorContext # pyright: ignore[reportUnusedVariable]
@@ -0,0 +1,19 @@
1
+ """Core runtime and state management components.
2
+
3
+ Expose the manager layer via package imports to reduce module churn in
4
+ callers. This keeps long-lived runtime state helpers (agents, tasks,
5
+ LLM clients, sub-agents) distinct from per-session execution logic in
6
+ ``klaude_code.core``.
7
+ """
8
+
9
+ from klaude_code.core.manager.agent_manager import AgentManager
10
+ from klaude_code.core.manager.llm_clients import LLMClients
11
+ from klaude_code.core.manager.llm_clients_builder import build_llm_clients
12
+ from klaude_code.core.manager.sub_agent_manager import SubAgentManager
13
+
14
+ __all__ = [
15
+ "AgentManager",
16
+ "LLMClients",
17
+ "SubAgentManager",
18
+ "build_llm_clients",
19
+ ]