klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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 (140) hide show
  1. klaude_code/cli/main.py +9 -4
  2. klaude_code/cli/runtime.py +42 -43
  3. klaude_code/command/__init__.py +7 -5
  4. klaude_code/command/clear_cmd.py +6 -29
  5. klaude_code/command/command_abc.py +44 -8
  6. klaude_code/command/diff_cmd.py +33 -27
  7. klaude_code/command/export_cmd.py +18 -26
  8. klaude_code/command/help_cmd.py +10 -8
  9. klaude_code/command/model_cmd.py +11 -40
  10. klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
  11. klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
  12. klaude_code/command/prompt-init.md +2 -5
  13. klaude_code/command/prompt_command.py +6 -6
  14. klaude_code/command/refresh_cmd.py +4 -5
  15. klaude_code/command/registry.py +16 -19
  16. klaude_code/command/terminal_setup_cmd.py +12 -11
  17. klaude_code/config/__init__.py +4 -0
  18. klaude_code/config/config.py +25 -26
  19. klaude_code/config/list_model.py +8 -3
  20. klaude_code/config/select_model.py +1 -1
  21. klaude_code/const/__init__.py +1 -1
  22. klaude_code/core/__init__.py +0 -3
  23. klaude_code/core/agent.py +25 -50
  24. klaude_code/core/executor.py +268 -101
  25. klaude_code/core/prompt.py +12 -12
  26. klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
  27. klaude_code/core/reminders.py +76 -95
  28. klaude_code/core/task.py +21 -14
  29. klaude_code/core/tool/__init__.py +45 -11
  30. klaude_code/core/tool/file/apply_patch.py +5 -1
  31. klaude_code/core/tool/file/apply_patch_tool.py +11 -13
  32. klaude_code/core/tool/file/edit_tool.py +27 -23
  33. klaude_code/core/tool/file/multi_edit_tool.py +15 -17
  34. klaude_code/core/tool/file/read_tool.py +41 -36
  35. klaude_code/core/tool/file/write_tool.py +13 -15
  36. klaude_code/core/tool/memory/memory_tool.py +85 -68
  37. klaude_code/core/tool/memory/skill_tool.py +10 -12
  38. klaude_code/core/tool/shell/bash_tool.py +24 -22
  39. klaude_code/core/tool/shell/command_safety.py +12 -1
  40. klaude_code/core/tool/sub_agent_tool.py +11 -12
  41. klaude_code/core/tool/todo/todo_write_tool.py +21 -28
  42. klaude_code/core/tool/todo/update_plan_tool.py +14 -24
  43. klaude_code/core/tool/tool_abc.py +3 -4
  44. klaude_code/core/tool/tool_context.py +7 -7
  45. klaude_code/core/tool/tool_registry.py +30 -47
  46. klaude_code/core/tool/tool_runner.py +35 -43
  47. klaude_code/core/tool/truncation.py +14 -20
  48. klaude_code/core/tool/web/mermaid_tool.py +12 -14
  49. klaude_code/core/tool/web/web_fetch_tool.py +15 -17
  50. klaude_code/core/turn.py +19 -7
  51. klaude_code/llm/__init__.py +3 -4
  52. klaude_code/llm/anthropic/client.py +30 -46
  53. klaude_code/llm/anthropic/input.py +4 -11
  54. klaude_code/llm/client.py +29 -8
  55. klaude_code/llm/input_common.py +66 -36
  56. klaude_code/llm/openai_compatible/client.py +42 -84
  57. klaude_code/llm/openai_compatible/input.py +11 -16
  58. klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
  59. klaude_code/llm/openrouter/client.py +40 -289
  60. klaude_code/llm/openrouter/input.py +13 -35
  61. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  62. klaude_code/llm/registry.py +5 -75
  63. klaude_code/llm/responses/client.py +34 -55
  64. klaude_code/llm/responses/input.py +24 -26
  65. klaude_code/llm/usage.py +109 -0
  66. klaude_code/protocol/__init__.py +4 -0
  67. klaude_code/protocol/events.py +3 -2
  68. klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
  69. klaude_code/protocol/model.py +49 -4
  70. klaude_code/protocol/op.py +18 -16
  71. klaude_code/protocol/op_handler.py +28 -0
  72. klaude_code/{core → protocol}/sub_agent.py +7 -0
  73. klaude_code/session/export.py +150 -70
  74. klaude_code/session/session.py +28 -14
  75. klaude_code/session/templates/export_session.html +180 -42
  76. klaude_code/trace/__init__.py +2 -2
  77. klaude_code/trace/log.py +11 -5
  78. klaude_code/ui/__init__.py +91 -8
  79. klaude_code/ui/core/__init__.py +1 -0
  80. klaude_code/ui/core/display.py +103 -0
  81. klaude_code/ui/core/input.py +71 -0
  82. klaude_code/ui/modes/__init__.py +1 -0
  83. klaude_code/ui/modes/debug/__init__.py +1 -0
  84. klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
  85. klaude_code/ui/modes/exec/__init__.py +1 -0
  86. klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
  87. klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
  88. klaude_code/ui/modes/repl/clipboard.py +152 -0
  89. klaude_code/ui/modes/repl/completers.py +429 -0
  90. klaude_code/ui/modes/repl/display.py +60 -0
  91. klaude_code/ui/modes/repl/event_handler.py +375 -0
  92. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  93. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  94. klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
  95. klaude_code/ui/renderers/assistant.py +21 -0
  96. klaude_code/ui/renderers/common.py +0 -16
  97. klaude_code/ui/renderers/developer.py +18 -18
  98. klaude_code/ui/renderers/diffs.py +36 -14
  99. klaude_code/ui/renderers/errors.py +1 -1
  100. klaude_code/ui/renderers/metadata.py +50 -27
  101. klaude_code/ui/renderers/sub_agent.py +43 -9
  102. klaude_code/ui/renderers/thinking.py +33 -1
  103. klaude_code/ui/renderers/tools.py +212 -20
  104. klaude_code/ui/renderers/user_input.py +19 -23
  105. klaude_code/ui/rich/__init__.py +1 -0
  106. klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
  107. klaude_code/ui/{renderers → rich}/status.py +29 -18
  108. klaude_code/ui/{base → rich}/theme.py +8 -2
  109. klaude_code/ui/terminal/__init__.py +1 -0
  110. klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
  111. klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
  112. klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
  113. klaude_code/ui/utils/__init__.py +1 -0
  114. klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
  115. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
  116. klaude_code-1.2.3.dist-info/RECORD +161 -0
  117. klaude_code/core/clipboard_manifest.py +0 -124
  118. klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
  119. klaude_code/ui/base/__init__.py +0 -1
  120. klaude_code/ui/base/display_abc.py +0 -36
  121. klaude_code/ui/base/input_abc.py +0 -20
  122. klaude_code/ui/repl/display.py +0 -36
  123. klaude_code/ui/repl/event_handler.py +0 -247
  124. klaude_code/ui/repl/input.py +0 -773
  125. klaude_code/ui/rich_ext/__init__.py +0 -1
  126. klaude_code-1.2.1.dist-info/RECORD +0 -151
  127. /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
  128. /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
  129. /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
  130. /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
  131. /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
  132. /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
  133. /klaude_code/ui/{base → core}/stage_manager.py +0 -0
  134. /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
  135. /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
  136. /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
  137. /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
  138. /klaude_code/ui/{base → utils}/debouncer.py +0 -0
  139. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
  140. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
@@ -5,30 +5,90 @@ This module implements the submission_loop equivalent for klaude,
5
5
  handling operations submitted from the CLI and coordinating with agents.
6
6
  """
7
7
 
8
+ from __future__ import annotations
9
+
8
10
  import asyncio
9
11
  from dataclasses import dataclass
10
- from uuid import uuid4
12
+ from dataclasses import field as dataclass_field
11
13
 
12
- from klaude_code.command import dispatch_command
14
+ from klaude_code.command import InputAction, InputActionType, dispatch_command
15
+ from klaude_code.config import Config, load_config
13
16
  from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
14
- from klaude_code.llm import LLMClients
15
- from klaude_code.core.sub_agent import SubAgentResult
16
- from klaude_code.core.tool.tool_context import current_run_subtask_callback
17
- from klaude_code.protocol import events, model
18
- from klaude_code.protocol.op import (
19
- EndOperation,
20
- InitAgentOperation,
21
- InterruptOperation,
22
- Operation,
23
- Submission,
24
- UserInputOperation,
25
- )
17
+ from klaude_code.core.tool import current_run_subtask_callback
18
+ from klaude_code.llm.client import LLMClientABC
19
+ from klaude_code.llm.registry import create_llm_client
20
+ from klaude_code.protocol import commands, events, model, op
21
+ from klaude_code.protocol.op_handler import OperationHandler
22
+ from klaude_code.protocol.sub_agent import SubAgentResult, get_sub_agent_profile
23
+ from klaude_code.protocol.tools import SubAgentType
26
24
  from klaude_code.session.session import Session
27
25
  from klaude_code.trace import DebugType, log_debug
28
26
 
29
27
 
28
+ @dataclass
29
+ class LLMClients:
30
+ """Container for LLM clients used by main agent and sub-agents."""
31
+
32
+ main: LLMClientABC
33
+ sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=lambda: {})
34
+
35
+ def get_client(self, sub_agent_type: SubAgentType | None = None) -> LLMClientABC:
36
+ """Get client for given sub-agent type, or main client if None."""
37
+ if sub_agent_type is None:
38
+ return self.main
39
+ return self.sub_clients.get(sub_agent_type) or self.main
40
+
41
+ @classmethod
42
+ def from_config(
43
+ cls,
44
+ config: Config,
45
+ model_override: str | None = None,
46
+ enabled_sub_agents: list[SubAgentType] | None = None,
47
+ ) -> LLMClients:
48
+ """Create LLMClients from application config.
49
+
50
+ Args:
51
+ config: Application configuration
52
+ model_override: Optional model name to override the main model
53
+ enabled_sub_agents: List of sub-agent types to initialize clients for
54
+
55
+ Returns:
56
+ LLMClients instance
57
+ """
58
+ # Resolve main agent LLM config
59
+ if model_override:
60
+ llm_config = config.get_model_config(model_override)
61
+ else:
62
+ llm_config = config.get_main_model_config()
63
+
64
+ log_debug(
65
+ "Main LLM config",
66
+ llm_config.model_dump_json(exclude_none=True),
67
+ style="yellow",
68
+ debug_type=DebugType.LLM_CONFIG,
69
+ )
70
+
71
+ main_client = create_llm_client(llm_config)
72
+ sub_clients: dict[SubAgentType, LLMClientABC] = {}
73
+
74
+ # Initialize sub-agent clients
75
+ for sub_agent_type in enabled_sub_agents or []:
76
+ model_name = config.subagent_models.get(sub_agent_type)
77
+ if not model_name:
78
+ continue
79
+ profile = get_sub_agent_profile(sub_agent_type)
80
+ if not profile.enabled_for_model(main_client.model_name):
81
+ continue
82
+ sub_llm_config = config.get_model_config(model_name)
83
+ sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
84
+
85
+ return cls(main=main_client, sub_clients=sub_clients)
86
+
87
+
30
88
  @dataclass
31
89
  class ActiveTask:
90
+ """Track an in-flight task and its owning session."""
91
+
32
92
  task: asyncio.Task[None]
33
93
  session_id: str
34
94
 
@@ -39,6 +99,8 @@ class ExecutorContext:
39
99
 
40
100
  This context is passed to operations when they execute, allowing them
41
101
  to access shared resources like the event queue and active sessions.
102
+
103
+ Implements the OperationHandler protocol via structural subtyping.
42
104
  """
43
105
 
44
106
  def __init__(
@@ -47,8 +109,8 @@ class ExecutorContext:
47
109
  llm_clients: LLMClients,
48
110
  model_profile_provider: ModelProfileProvider | None = None,
49
111
  ):
50
- self.event_queue = event_queue
51
- self.llm_clients = llm_clients
112
+ self.event_queue: asyncio.Queue[events.Event] = event_queue
113
+ self.llm_clients: LLMClients = llm_clients
52
114
  self.model_profile_provider: ModelProfileProvider = model_profile_provider or DefaultModelProfileProvider()
53
115
 
54
116
  # Track active agents by session ID
@@ -60,56 +122,68 @@ class ExecutorContext:
60
122
  """Emit an event to the UI display system."""
61
123
  await self.event_queue.put(event)
62
124
 
63
- async def handle_init_agent(self, operation: InitAgentOperation) -> None:
125
+ async def _ensure_agent(self, session_id: str) -> Agent:
126
+ """Return an existing agent for the session or create a new one."""
127
+
128
+ agent = self.active_agents.get(session_id)
129
+ if agent is not None:
130
+ return agent
131
+
132
+ session = Session.load(session_id)
133
+ profile = self.model_profile_provider.build_profile(self.llm_clients.main)
134
+ agent = Agent(
135
+ session=session,
136
+ profile=profile,
137
+ )
138
+
139
+ async for evt in agent.replay_history():
140
+ await self.emit_event(evt)
141
+
142
+ await self.emit_event(
143
+ events.WelcomeEvent(
144
+ work_dir=str(session.work_dir),
145
+ llm_config=self.llm_clients.main.get_llm_config(),
146
+ )
147
+ )
148
+
149
+ self.active_agents[session_id] = agent
150
+ log_debug(
151
+ f"Initialized agent for session: {session_id}",
152
+ style="cyan",
153
+ debug_type=DebugType.EXECUTION,
154
+ )
155
+ return agent
156
+
157
+ async def handle_init_agent(self, operation: op.InitAgentOperation) -> None:
64
158
  """Initialize an agent for a session and replay history to UI."""
65
159
  if operation.session_id is None:
66
160
  raise ValueError("session_id cannot be None")
67
161
 
68
- # Load or create session first
69
- session = Session.load(operation.session_id)
70
-
71
- # Create agent if not exists
72
- if operation.session_id not in self.active_agents:
73
- profile = self.model_profile_provider.build_profile(self.llm_clients.main)
74
- agent = Agent(
75
- session=session,
76
- profile=profile,
77
- model_profile_provider=self.model_profile_provider,
78
- )
79
- async for evt in agent.replay_history():
80
- await self.emit_event(evt)
81
- await self.emit_event(
82
- events.WelcomeEvent(
83
- work_dir=str(session.work_dir),
84
- llm_config=self.llm_clients.main.get_llm_config(),
85
- )
86
- )
87
- self.active_agents[operation.session_id] = agent
88
- log_debug(
89
- f"Initialized agent for session: {operation.session_id}",
90
- style="cyan",
91
- debug_type=DebugType.EXECUTION,
92
- )
162
+ await self._ensure_agent(operation.session_id)
93
163
 
94
- async def handle_user_input(self, operation: UserInputOperation) -> None:
164
+ async def handle_user_input(self, operation: op.UserInputOperation) -> None:
95
165
  """Handle a user input operation by running it through an agent."""
96
166
 
97
167
  if operation.session_id is None:
98
168
  raise ValueError("session_id cannot be None")
99
169
 
100
- # Ensure initialized via init_agent
101
- if operation.session_id not in self.active_agents:
102
- await self.handle_init_agent(InitAgentOperation(id=str(uuid4()), session_id=operation.session_id))
103
-
104
- agent = self.active_agents[operation.session_id]
170
+ session_id = operation.session_id
171
+ agent = await self._ensure_agent(session_id)
172
+ user_input = operation.input
105
173
 
106
174
  # emit user input event
107
- await self.emit_event(events.UserMessageEvent(content=operation.content, session_id=operation.session_id))
175
+ await self.emit_event(
176
+ events.UserMessageEvent(content=user_input.text, session_id=session_id, images=user_input.images)
177
+ )
108
178
 
109
- result = await dispatch_command(operation.content, agent)
110
- if not result.agent_input:
111
- # If this command do not need run agent, we should append user message to session history here
112
- agent.session.append_history([model.UserMessageItem(content=operation.content)])
179
+ result = await dispatch_command(user_input.text, agent)
180
+
181
+ actions: list[InputAction] = list(result.actions or [])
182
+
183
+ has_run_agent_action = any(action.type is InputActionType.RUN_AGENT for action in actions)
184
+ if not has_run_agent_action:
185
+ # No async agent task will run, append user message directly
186
+ agent.session.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
113
187
 
114
188
  if result.events:
115
189
  agent.session.append_history(
@@ -118,15 +192,82 @@ class ExecutorContext:
118
192
  for evt in result.events:
119
193
  await self.emit_event(evt)
120
194
 
121
- if result.agent_input:
122
- # Start task to process user input (do NOT await here so the executor loop stays responsive)
195
+ for action in actions:
196
+ await self._run_input_action(action, operation, agent)
197
+
198
+ async def _run_input_action(self, action: InputAction, operation: op.UserInputOperation, agent: Agent) -> None:
199
+ if operation.session_id is None:
200
+ raise ValueError("session_id cannot be None for input actions")
201
+
202
+ session_id = operation.session_id
203
+
204
+ if action.type == InputActionType.RUN_AGENT:
205
+ task_input = model.UserInputPayload(text=action.text, images=operation.input.images)
206
+
207
+ existing_active = self.active_tasks.get(operation.id)
208
+ if existing_active is not None and not existing_active.task.done():
209
+ raise RuntimeError(f"Active task already registered for operation {operation.id}")
210
+
123
211
  task: asyncio.Task[None] = asyncio.create_task(
124
- self._run_agent_task(agent, result.agent_input, operation.id, operation.session_id)
212
+ self._run_agent_task(agent, task_input, operation.id, session_id)
125
213
  )
126
- self.active_tasks[operation.id] = ActiveTask(task=task, session_id=operation.session_id)
127
- # Do not await task here; completion will be tracked by the executor
214
+ self.active_tasks[operation.id] = ActiveTask(task=task, session_id=session_id)
215
+ return
216
+
217
+ if action.type == InputActionType.CHANGE_MODEL:
218
+ if not action.model_name:
219
+ raise ValueError("ChangeModel action requires model_name")
220
+
221
+ await self._apply_model_change(agent, action.model_name)
222
+ return
223
+
224
+ if action.type == InputActionType.CLEAR:
225
+ await self._apply_clear(agent)
226
+ return
227
+
228
+ raise ValueError(f"Unsupported input action type: {action.type}")
229
+
230
+ async def _apply_model_change(self, agent: Agent, model_name: str) -> None:
231
+ config = load_config()
232
+ if config is None:
233
+ raise ValueError("Configuration must be initialized before changing model")
234
+
235
+ llm_config = config.get_model_config(model_name)
236
+ llm_client = create_llm_client(llm_config)
237
+ agent.set_model_profile(self.model_profile_provider.build_profile(llm_client))
238
+
239
+ developer_item = model.DeveloperMessageItem(
240
+ content=f"switched to model: {model_name}",
241
+ command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
242
+ )
243
+ agent.session.append_history([developer_item])
244
+
245
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
246
+ await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
247
+
248
+ async def _apply_clear(self, agent: Agent) -> None:
249
+ old_session_id = agent.session.id
250
+
251
+ # Create a new session instance to replace the current one
252
+ new_session = Session(work_dir=agent.session.work_dir)
253
+ new_session.model_name = agent.session.model_name
128
254
 
129
- async def handle_interrupt(self, operation: InterruptOperation) -> None:
255
+ # Replace the agent's session with the new one
256
+ agent.session = new_session
257
+ agent.session.save()
258
+
259
+ # Update the active_agents mapping
260
+ self.active_agents.pop(old_session_id, None)
261
+ self.active_agents[new_session.id] = agent
262
+
263
+ developer_item = model.DeveloperMessageItem(
264
+ content="started new conversation",
265
+ command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
266
+ )
267
+
268
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
269
+
270
+ async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
130
271
  """Handle an interrupt by invoking agent.cancel() and cancelling tasks."""
131
272
 
132
273
  # Determine affected sessions
@@ -170,7 +311,9 @@ class ExecutorContext:
170
311
  # Remove from active tasks immediately
171
312
  self.active_tasks.pop(task_id, None)
172
313
 
173
- async def _run_agent_task(self, agent: Agent, user_input: str, task_id: str, session_id: str) -> None:
314
+ async def _run_agent_task(
315
+ self, agent: Agent, user_input: model.UserInputPayload, task_id: str, session_id: str
316
+ ) -> None:
174
317
  """
175
318
  Run an agent task and forward all events to the UI.
176
319
 
@@ -225,7 +368,11 @@ class ExecutorContext:
225
368
  finally:
226
369
  # Clean up the task from active tasks
227
370
  self.active_tasks.pop(task_id, None)
228
- log_debug(f"Cleaned up agent task {task_id}", style="cyan", debug_type=DebugType.EXECUTION)
371
+ log_debug(
372
+ f"Cleaned up agent task {task_id}",
373
+ style="cyan",
374
+ debug_type=DebugType.EXECUTION,
375
+ )
229
376
 
230
377
  async def _run_subagent_task(self, parent_agent: Agent, state: model.SubAgentState) -> SubAgentResult:
231
378
  """Run a nested sub-agent task and return the final task_result text.
@@ -246,7 +393,6 @@ class ExecutorContext:
246
393
  child_agent = Agent(
247
394
  session=child_session,
248
395
  profile=child_profile,
249
- model_profile_provider=self.model_profile_provider,
250
396
  )
251
397
 
252
398
  log_debug(
@@ -258,7 +404,8 @@ class ExecutorContext:
258
404
  try:
259
405
  # Not emit the subtask's user input since task tool call is already rendered
260
406
  result: str = ""
261
- async for event in child_agent.run_task(state.sub_agent_prompt):
407
+ sub_agent_input = model.UserInputPayload(text=state.sub_agent_prompt, images=None)
408
+ async for event in child_agent.run_task(sub_agent_input):
262
409
  # Capture TaskFinishEvent content for return
263
410
  if isinstance(event, events.TaskFinishEvent):
264
411
  result = event.task_result
@@ -279,7 +426,9 @@ class ExecutorContext:
279
426
  debug_type=DebugType.EXECUTION,
280
427
  )
281
428
  return SubAgentResult(
282
- task_result=f"Subagent task failed: [{e.__class__.__name__}] {str(e)}", session_id="", error=True
429
+ task_result=f"Subagent task failed: [{e.__class__.__name__}] {str(e)}",
430
+ session_id="",
431
+ error=True,
283
432
  )
284
433
 
285
434
 
@@ -298,11 +447,11 @@ class Executor:
298
447
  model_profile_provider: ModelProfileProvider | None = None,
299
448
  ):
300
449
  self.context = ExecutorContext(event_queue, llm_clients, model_profile_provider)
301
- self.submission_queue: asyncio.Queue[Submission] = asyncio.Queue()
302
- self.running = False
303
- self.task_completion_events: dict[str, asyncio.Event] = {}
450
+ self.submission_queue: asyncio.Queue[op.Submission] = asyncio.Queue()
451
+ # Track completion events for all submissions (not just those with ActiveTask)
452
+ self._completion_events: dict[str, asyncio.Event] = {}
304
453
 
305
- async def submit(self, operation: Operation) -> str:
454
+ async def submit(self, operation: op.Operation) -> str:
306
455
  """
307
456
  Submit an operation to the executor for processing.
308
457
 
@@ -313,12 +462,11 @@ class Executor:
313
462
  Unique submission ID for tracking
314
463
  """
315
464
 
316
- submission = Submission(id=operation.id, operation=operation)
465
+ submission = op.Submission(id=operation.id, operation=operation)
317
466
  await self.submission_queue.put(submission)
318
467
 
319
468
  # Create completion event for tracking
320
- completion_event = asyncio.Event()
321
- self.task_completion_events[operation.id] = completion_event
469
+ self._completion_events[operation.id] = asyncio.Event()
322
470
 
323
471
  log_debug(
324
472
  f"Submitted operation {operation.type} with ID {operation.id}",
@@ -328,12 +476,17 @@ class Executor:
328
476
 
329
477
  return operation.id
330
478
 
331
- async def wait_for_completion(self, submission_id: str) -> None:
479
+ async def wait_for(self, submission_id: str) -> None:
332
480
  """Wait for a specific submission to complete."""
333
- if submission_id in self.task_completion_events:
334
- await self.task_completion_events[submission_id].wait()
335
- # Clean up the completion event
336
- self.task_completion_events.pop(submission_id, None)
481
+ event = self._completion_events.get(submission_id)
482
+ if event is not None:
483
+ await event.wait()
484
+ self._completion_events.pop(submission_id, None)
485
+
486
+ async def submit_and_wait(self, operation: op.Operation) -> None:
487
+ """Submit an operation and wait for it to complete."""
488
+ submission_id = await self.submit(operation)
489
+ await self.wait_for(submission_id)
337
490
 
338
491
  async def start(self) -> None:
339
492
  """
@@ -342,17 +495,15 @@ class Executor:
342
495
  This method runs continuously, processing submissions from the queue
343
496
  until the executor is stopped.
344
497
  """
345
- self.running = True
346
-
347
498
  log_debug("Executor started", style="green", debug_type=DebugType.EXECUTION)
348
499
 
349
- while self.running:
500
+ while True:
350
501
  try:
351
502
  # Wait for next submission
352
503
  submission = await self.submission_queue.get()
353
504
 
354
505
  # Check for end operation to gracefully exit
355
- if isinstance(submission.operation, EndOperation):
506
+ if isinstance(submission.operation, op.EndOperation):
356
507
  log_debug(
357
508
  "Received EndOperation, stopping executor",
358
509
  style="yellow",
@@ -369,15 +520,17 @@ class Executor:
369
520
 
370
521
  except Exception as e:
371
522
  # Handle unexpected errors
372
- log_debug(f"Executor error: {str(e)}", style="red", debug_type=DebugType.EXECUTION)
523
+ log_debug(
524
+ f"Executor error: {str(e)}",
525
+ style="red",
526
+ debug_type=DebugType.EXECUTION,
527
+ )
373
528
  await self.context.emit_event(
374
529
  events.ErrorEvent(error_message=f"Executor error: {str(e)}", can_retry=False)
375
530
  )
376
531
 
377
532
  async def stop(self) -> None:
378
533
  """Stop the executor and clean up resources."""
379
- self.running = False
380
-
381
534
  # Cancel all active tasks and collect them for awaiting
382
535
  tasks_to_await: list[asyncio.Task[None]] = []
383
536
  for active in self.context.active_tasks.values():
@@ -395,15 +548,19 @@ class Executor:
395
548
 
396
549
  # Send EndOperation to wake up the start() loop
397
550
  try:
398
- end_operation = EndOperation()
399
- submission = Submission(id=end_operation.id, operation=end_operation)
551
+ end_operation = op.EndOperation()
552
+ submission = op.Submission(id=end_operation.id, operation=end_operation)
400
553
  await self.submission_queue.put(submission)
401
554
  except Exception as e:
402
- log_debug(f"Failed to send EndOperation: {str(e)}", style="red", debug_type=DebugType.EXECUTION)
555
+ log_debug(
556
+ f"Failed to send EndOperation: {str(e)}",
557
+ style="red",
558
+ debug_type=DebugType.EXECUTION,
559
+ )
403
560
 
404
561
  log_debug("Executor stopped", style="yellow", debug_type=DebugType.EXECUTION)
405
562
 
406
- async def _handle_submission(self, submission: Submission) -> None:
563
+ async def _handle_submission(self, submission: op.Submission) -> None:
407
564
  """
408
565
  Handle a single submission by executing its operation.
409
566
 
@@ -418,23 +575,28 @@ class Executor:
418
575
  )
419
576
 
420
577
  # Execute to spawn the agent task in context
421
- await submission.operation.execute(self.context)
578
+ await submission.operation.execute(handler=self.context)
422
579
 
423
580
  async def _await_agent_and_complete() -> None:
424
- try:
425
- # Wait for the agent task tied to this submission id
426
- active = self.context.active_tasks.get(submission.id)
427
- if active is not None:
581
+ # Wait for the agent task tied to this submission id
582
+ active = self.context.active_tasks.get(submission.id)
583
+ if active is not None:
584
+ try:
428
585
  await active.task
429
- finally:
430
- # Signal completion of this submission when agent task completes
431
- if submission.id in self.task_completion_events:
432
- self.task_completion_events[submission.id].set()
433
-
434
- # Run in background so the submission loop can continue (e.g., to handle interrupts)
586
+ finally:
587
+ event = self._completion_events.get(submission.id)
588
+ if event is not None:
589
+ event.set()
435
590
 
591
+ # Run in background so the submission loop can continue (e.g., to handle interrupts)
436
592
  asyncio.create_task(_await_agent_and_complete())
437
593
 
594
+ # For operations without ActiveTask (e.g., InitAgentOperation), signal completion immediately
595
+ if submission.id not in self.context.active_tasks:
596
+ event = self._completion_events.get(submission.id)
597
+ if event is not None:
598
+ event.set()
599
+
438
600
  except Exception as e:
439
601
  log_debug(
440
602
  f"Failed to handle submission {submission.id}: {str(e)}",
@@ -445,6 +607,11 @@ class Executor:
445
607
  events.ErrorEvent(error_message=f"Operation failed: {str(e)}", can_retry=False)
446
608
  )
447
609
  # Set completion event even on error to prevent wait_for_completion from hanging
448
- completion_event = self.task_completion_events.get(submission.id)
449
- if completion_event is not None:
450
- completion_event.set()
610
+ event = self._completion_events.get(submission.id)
611
+ if event is not None:
612
+ event.set()
613
+
614
+
615
+ # Static type check: ExecutorContext must satisfy OperationHandler protocol.
616
+ # If this line causes a type error, ExecutorContext is missing required methods.
617
+ _: type[OperationHandler] = ExecutorContext # pyright: ignore[reportUnusedVariable]
@@ -13,20 +13,20 @@ COMMAND_DESCRIPTIONS: dict[str, str] = {
13
13
 
14
14
  # Mapping from logical prompt keys to resource file paths under the core/prompt directory.
15
15
  PROMPT_FILES: dict[str, str] = {
16
- "main_codex": "prompt/prompt-codex.md",
17
- "main_claude": "prompt/prompt-claude-code.md",
18
- "main_gemini": "prompt/prompt-gemini.md", # https://ai.google.dev/gemini-api/docs/prompting-strategies?hl=zh-cn#agentic-si-template
16
+ "main_codex": "prompts/prompt-codex.md",
17
+ "main_claude": "prompts/prompt-claude-code.md",
18
+ "main_gemini": "prompts/prompt-gemini.md", # https://ai.google.dev/gemini-api/docs/prompting-strategies?hl=zh-cn#agentic-si-template
19
19
  # Sub-agent prompts keyed by their name
20
- "Task": "prompt/prompt-subagent.md",
21
- "Oracle": "prompt/prompt-subagent-oracle.md",
22
- "Explore": "prompt/prompt-subagent-explore.md",
23
- "WebFetchAgent": "prompt/prompt-subagent-webfetch.md",
20
+ "Task": "prompts/prompt-subagent.md",
21
+ "Oracle": "prompts/prompt-subagent-oracle.md",
22
+ "Explore": "prompts/prompt-subagent-explore.md",
23
+ "WebFetchAgent": "prompts/prompt-subagent-webfetch.md",
24
24
  }
25
25
 
26
26
 
27
27
  @lru_cache(maxsize=None)
28
- def get_system_prompt(model_name: str, key: str = "main") -> str:
29
- """Get system prompt content for the given model and prompt key."""
28
+ def get_system_prompt(model_name: str, sub_agent_type: str | None = None) -> str:
29
+ """Get system prompt content for the given model and sub-agent type."""
30
30
 
31
31
  cwd = Path.cwd()
32
32
  today = datetime.datetime.now().strftime("%Y-%m-%d")
@@ -37,7 +37,7 @@ def get_system_prompt(model_name: str, key: str = "main") -> str:
37
37
  if shutil.which(command) is not None:
38
38
  available_tools.append(f"{command}: {desc}")
39
39
 
40
- if key == "main":
40
+ if sub_agent_type is None:
41
41
  match model_name:
42
42
  case name if "gpt-5" in name:
43
43
  file_key = "main_codex"
@@ -46,12 +46,12 @@ def get_system_prompt(model_name: str, key: str = "main") -> str:
46
46
  case _:
47
47
  file_key = "main_claude"
48
48
  else:
49
- file_key = key
49
+ file_key = sub_agent_type
50
50
 
51
51
  try:
52
52
  prompt_path = PROMPT_FILES[file_key]
53
53
  except KeyError as exc:
54
- raise ValueError(f"Unknown prompt key: {key}") from exc
54
+ raise ValueError(f"Unknown prompt key: {file_key}") from exc
55
55
 
56
56
  base_prompt = (
57
57
  files(__package__)
@@ -1,4 +1,4 @@
1
- You are a very strong reasoner and planner. Use these critical instructions to structure your plans, thoughts, and responses.
1
+ You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
2
2
 
3
3
  Before taking any action (either tool calls *or* responses to the user), you must proactively, methodically, and independently plan and reason about:
4
4