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.
- klaude_code/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- 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
|
+
]
|