klaude-code 1.9.0__py3-none-any.whl → 2.0.0__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 (129) hide show
  1. klaude_code/auth/base.py +2 -6
  2. klaude_code/cli/auth_cmd.py +4 -4
  3. klaude_code/cli/list_model.py +1 -1
  4. klaude_code/cli/main.py +1 -1
  5. klaude_code/cli/runtime.py +7 -5
  6. klaude_code/cli/self_update.py +1 -1
  7. klaude_code/cli/session_cmd.py +1 -1
  8. klaude_code/command/clear_cmd.py +6 -2
  9. klaude_code/command/command_abc.py +2 -2
  10. klaude_code/command/debug_cmd.py +4 -4
  11. klaude_code/command/export_cmd.py +2 -2
  12. klaude_code/command/export_online_cmd.py +12 -12
  13. klaude_code/command/fork_session_cmd.py +29 -23
  14. klaude_code/command/help_cmd.py +4 -4
  15. klaude_code/command/model_cmd.py +4 -4
  16. klaude_code/command/model_select.py +1 -1
  17. klaude_code/command/prompt-commit.md +11 -2
  18. klaude_code/command/prompt_command.py +3 -3
  19. klaude_code/command/refresh_cmd.py +2 -2
  20. klaude_code/command/registry.py +7 -5
  21. klaude_code/command/release_notes_cmd.py +4 -4
  22. klaude_code/command/resume_cmd.py +15 -11
  23. klaude_code/command/status_cmd.py +4 -4
  24. klaude_code/command/terminal_setup_cmd.py +8 -8
  25. klaude_code/command/thinking_cmd.py +4 -4
  26. klaude_code/config/assets/builtin_config.yaml +16 -0
  27. klaude_code/config/builtin_config.py +16 -5
  28. klaude_code/config/config.py +7 -2
  29. klaude_code/const.py +146 -91
  30. klaude_code/core/agent.py +3 -12
  31. klaude_code/core/executor.py +21 -13
  32. klaude_code/core/manager/sub_agent_manager.py +71 -7
  33. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  34. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  35. klaude_code/core/reminders.py +88 -69
  36. klaude_code/core/task.py +44 -45
  37. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  38. klaude_code/core/tool/file/diff_builder.py +3 -5
  39. klaude_code/core/tool/file/edit_tool.py +23 -23
  40. klaude_code/core/tool/file/move_tool.py +43 -43
  41. klaude_code/core/tool/file/read_tool.py +44 -39
  42. klaude_code/core/tool/file/write_tool.py +14 -14
  43. klaude_code/core/tool/report_back_tool.py +4 -4
  44. klaude_code/core/tool/shell/bash_tool.py +23 -23
  45. klaude_code/core/tool/skill/skill_tool.py +7 -7
  46. klaude_code/core/tool/sub_agent_tool.py +38 -9
  47. klaude_code/core/tool/todo/todo_write_tool.py +8 -8
  48. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  49. klaude_code/core/tool/tool_abc.py +2 -2
  50. klaude_code/core/tool/tool_context.py +27 -0
  51. klaude_code/core/tool/tool_runner.py +88 -42
  52. klaude_code/core/tool/truncation.py +38 -20
  53. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  54. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  55. klaude_code/core/tool/web/web_search_tool.py +15 -17
  56. klaude_code/core/turn.py +120 -73
  57. klaude_code/llm/anthropic/client.py +79 -44
  58. klaude_code/llm/anthropic/input.py +116 -108
  59. klaude_code/llm/bedrock/client.py +8 -5
  60. klaude_code/llm/claude/client.py +18 -8
  61. klaude_code/llm/client.py +4 -3
  62. klaude_code/llm/codex/client.py +15 -9
  63. klaude_code/llm/google/client.py +122 -60
  64. klaude_code/llm/google/input.py +94 -108
  65. klaude_code/llm/image.py +123 -0
  66. klaude_code/llm/input_common.py +136 -189
  67. klaude_code/llm/openai_compatible/client.py +17 -7
  68. klaude_code/llm/openai_compatible/input.py +36 -66
  69. klaude_code/llm/openai_compatible/stream.py +119 -67
  70. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  71. klaude_code/llm/openrouter/client.py +34 -9
  72. klaude_code/llm/openrouter/input.py +63 -64
  73. klaude_code/llm/openrouter/reasoning.py +22 -24
  74. klaude_code/llm/registry.py +20 -17
  75. klaude_code/llm/responses/client.py +107 -45
  76. klaude_code/llm/responses/input.py +115 -98
  77. klaude_code/llm/usage.py +52 -25
  78. klaude_code/protocol/__init__.py +1 -0
  79. klaude_code/protocol/events.py +16 -12
  80. klaude_code/protocol/llm_param.py +20 -2
  81. klaude_code/protocol/message.py +250 -0
  82. klaude_code/protocol/model.py +94 -281
  83. klaude_code/protocol/op.py +2 -2
  84. klaude_code/protocol/sub_agent/__init__.py +1 -0
  85. klaude_code/protocol/sub_agent/explore.py +10 -0
  86. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  87. klaude_code/protocol/sub_agent/task.py +10 -0
  88. klaude_code/protocol/sub_agent/web.py +10 -0
  89. klaude_code/session/codec.py +6 -6
  90. klaude_code/session/export.py +261 -62
  91. klaude_code/session/selector.py +7 -24
  92. klaude_code/session/session.py +126 -54
  93. klaude_code/session/store.py +5 -32
  94. klaude_code/session/templates/export_session.html +1 -1
  95. klaude_code/session/templates/mermaid_viewer.html +1 -1
  96. klaude_code/trace/log.py +11 -6
  97. klaude_code/ui/core/input.py +1 -1
  98. klaude_code/ui/core/stage_manager.py +1 -8
  99. klaude_code/ui/modes/debug/display.py +2 -2
  100. klaude_code/ui/modes/repl/clipboard.py +2 -2
  101. klaude_code/ui/modes/repl/completers.py +18 -10
  102. klaude_code/ui/modes/repl/event_handler.py +136 -127
  103. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  104. klaude_code/ui/modes/repl/key_bindings.py +1 -1
  105. klaude_code/ui/modes/repl/renderer.py +107 -15
  106. klaude_code/ui/renderers/assistant.py +2 -2
  107. klaude_code/ui/renderers/common.py +65 -7
  108. klaude_code/ui/renderers/developer.py +7 -6
  109. klaude_code/ui/renderers/diffs.py +11 -11
  110. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  111. klaude_code/ui/renderers/metadata.py +33 -5
  112. klaude_code/ui/renderers/sub_agent.py +57 -16
  113. klaude_code/ui/renderers/thinking.py +37 -2
  114. klaude_code/ui/renderers/tools.py +180 -165
  115. klaude_code/ui/rich/live.py +3 -1
  116. klaude_code/ui/rich/markdown.py +39 -7
  117. klaude_code/ui/rich/quote.py +76 -1
  118. klaude_code/ui/rich/status.py +14 -8
  119. klaude_code/ui/rich/theme.py +8 -2
  120. klaude_code/ui/terminal/image.py +34 -0
  121. klaude_code/ui/terminal/notifier.py +2 -1
  122. klaude_code/ui/terminal/progress_bar.py +4 -4
  123. klaude_code/ui/terminal/selector.py +22 -4
  124. klaude_code/ui/utils/common.py +11 -2
  125. {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
  126. klaude_code-2.0.0.dist-info/RECORD +229 -0
  127. klaude_code-1.9.0.dist-info/RECORD +0 -224
  128. {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
  129. {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -10,14 +10,17 @@ from urllib.parse import quote, urlparse, urlunparse
10
10
 
11
11
  from pydantic import BaseModel
12
12
 
13
- from klaude_code import const
13
+ from klaude_code.const import (
14
+ TOOL_OUTPUT_TRUNCATION_DIR,
15
+ URL_FILENAME_MAX_LENGTH,
16
+ WEB_FETCH_DEFAULT_TIMEOUT_SEC,
17
+ WEB_FETCH_USER_AGENT,
18
+ )
14
19
  from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolMetadata, load_desc
15
20
  from klaude_code.core.tool.tool_registry import register
16
- from klaude_code.protocol import llm_param, model, tools
21
+ from klaude_code.protocol import llm_param, message, tools
17
22
 
18
- DEFAULT_TIMEOUT_SEC = 30
19
- DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; KlaudeCode/1.0)"
20
- WEB_FETCH_SAVE_DIR = Path(const.TOOL_OUTPUT_TRUNCATION_DIR) / "web"
23
+ WEB_FETCH_SAVE_DIR = Path(TOOL_OUTPUT_TRUNCATION_DIR) / "web"
21
24
 
22
25
 
23
26
  def _encode_url(url: str) -> str:
@@ -110,16 +113,16 @@ def _extract_url_filename(url: str) -> str:
110
113
  path = parsed.path.strip("/").replace("/", "_")
111
114
  name = f"{host}_{path}" if path else host
112
115
  name = re.sub(r"[^a-zA-Z0-9_\-]", "_", name)
113
- return name[:80] if len(name) > 80 else name
116
+ return name[:URL_FILENAME_MAX_LENGTH] if len(name) > URL_FILENAME_MAX_LENGTH else name
114
117
 
115
118
 
116
- def _save_web_content(url: str, content: str) -> str | None:
119
+ def _save_web_content(url: str, content: str, extension: str = ".md") -> str | None:
117
120
  """Save web content to file. Returns file path or None on failure."""
118
121
  try:
119
122
  WEB_FETCH_SAVE_DIR.mkdir(parents=True, exist_ok=True)
120
123
  timestamp = int(time.time())
121
124
  identifier = _extract_url_filename(url)
122
- filename = f"{identifier}-{timestamp}.md"
125
+ filename = f"{identifier}-{timestamp}{extension}"
123
126
  file_path = WEB_FETCH_SAVE_DIR / filename
124
127
  file_path.write_text(content, encoding="utf-8")
125
128
  return str(file_path)
@@ -127,6 +130,26 @@ def _save_web_content(url: str, content: str) -> str | None:
127
130
  return None
128
131
 
129
132
 
133
+ def _save_binary_content(url: str, data: bytes, extension: str = ".bin") -> str | None:
134
+ """Save binary content to file. Returns file path or None on failure."""
135
+ try:
136
+ WEB_FETCH_SAVE_DIR.mkdir(parents=True, exist_ok=True)
137
+ timestamp = int(time.time())
138
+ identifier = _extract_url_filename(url)
139
+ filename = f"{identifier}-{timestamp}{extension}"
140
+ file_path = WEB_FETCH_SAVE_DIR / filename
141
+ file_path.write_bytes(data)
142
+ return str(file_path)
143
+ except OSError:
144
+ return None
145
+
146
+
147
+ def _is_pdf_url(url: str) -> bool:
148
+ """Check if URL points to a PDF file."""
149
+ parsed = urlparse(url)
150
+ return parsed.path.lower().endswith(".pdf") or "/pdf/" in parsed.path.lower()
151
+
152
+
130
153
  def _process_content(content_type: str, text: str) -> str:
131
154
  """Process content based on Content-Type header."""
132
155
  if content_type == "text/html":
@@ -139,19 +162,19 @@ def _process_content(content_type: str, text: str) -> str:
139
162
  return text
140
163
 
141
164
 
142
- def _fetch_url(url: str, timeout: int = DEFAULT_TIMEOUT_SEC) -> tuple[str, str]:
165
+ def _fetch_url(url: str, timeout: int = WEB_FETCH_DEFAULT_TIMEOUT_SEC) -> tuple[str, bytes, str | None]:
143
166
  """
144
167
  Fetch URL content synchronously.
145
168
 
146
169
  Returns:
147
- Tuple of (content_type, response_text)
170
+ Tuple of (content_type, raw_data, charset)
148
171
 
149
172
  Raises:
150
173
  Various exceptions on failure
151
174
  """
152
175
  headers = {
153
176
  "Accept": "text/markdown, */*",
154
- "User-Agent": DEFAULT_USER_AGENT,
177
+ "User-Agent": WEB_FETCH_USER_AGENT,
155
178
  }
156
179
  encoded_url = _encode_url(url)
157
180
  request = urllib.request.Request(encoded_url, headers=headers)
@@ -159,8 +182,7 @@ def _fetch_url(url: str, timeout: int = DEFAULT_TIMEOUT_SEC) -> tuple[str, str]:
159
182
  with urllib.request.urlopen(request, timeout=timeout) as response:
160
183
  content_type, charset = _extract_content_type_and_charset(response)
161
184
  data = response.read()
162
- text = _decode_content(data, charset)
163
- return content_type, text
185
+ return content_type, data, charset
164
186
 
165
187
 
166
188
  @register(tools.WEB_FETCH)
@@ -191,29 +213,45 @@ class WebFetchTool(ToolABC):
191
213
  url: str
192
214
 
193
215
  @classmethod
194
- async def call(cls, arguments: str) -> model.ToolResultItem:
216
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
195
217
  try:
196
218
  args = WebFetchTool.WebFetchArguments.model_validate_json(arguments)
197
219
  except ValueError as e:
198
- return model.ToolResultItem(
220
+ return message.ToolResultMessage(
199
221
  status="error",
200
- output=f"Invalid arguments: {e}",
222
+ output_text=f"Invalid arguments: {e}",
201
223
  )
202
224
  return await cls.call_with_args(args)
203
225
 
204
226
  @classmethod
205
- async def call_with_args(cls, args: WebFetchArguments) -> model.ToolResultItem:
227
+ async def call_with_args(cls, args: WebFetchArguments) -> message.ToolResultMessage:
206
228
  url = args.url
207
229
 
208
230
  # Basic URL validation
209
231
  if not url.startswith(("http://", "https://")):
210
- return model.ToolResultItem(
232
+ return message.ToolResultMessage(
211
233
  status="error",
212
- output=f"Invalid URL: must start with http:// or https:// (url={url})",
234
+ output_text=f"Invalid URL: must start with http:// or https:// (url={url})",
213
235
  )
214
236
 
215
237
  try:
216
- content_type, text = await asyncio.to_thread(_fetch_url, url)
238
+ content_type, data, charset = await asyncio.to_thread(_fetch_url, url)
239
+
240
+ # Handle PDF files
241
+ if content_type == "application/pdf" or _is_pdf_url(url):
242
+ saved_path = _save_binary_content(url, data, ".pdf")
243
+ if saved_path:
244
+ return message.ToolResultMessage(
245
+ status="success",
246
+ output_text=f"PDF file saved to: {saved_path}\n\nTo read the PDF content, use the Read tool on this file path.",
247
+ )
248
+ return message.ToolResultMessage(
249
+ status="error",
250
+ output_text=f"Failed to save PDF file (url={url})",
251
+ )
252
+
253
+ # Handle text content
254
+ text = _decode_content(data, charset)
217
255
  processed = _process_content(content_type, text)
218
256
 
219
257
  # Always save content to file
@@ -222,28 +260,28 @@ class WebFetchTool(ToolABC):
222
260
  # Build output with file path info
223
261
  output = f"<file_saved>{saved_path}</file_saved>\n\n{processed}" if saved_path else processed
224
262
 
225
- return model.ToolResultItem(
263
+ return message.ToolResultMessage(
226
264
  status="success",
227
- output=output,
265
+ output_text=output,
228
266
  )
229
267
 
230
268
  except urllib.error.HTTPError as e:
231
- return model.ToolResultItem(
269
+ return message.ToolResultMessage(
232
270
  status="error",
233
- output=f"HTTP error {e.code}: {e.reason} (url={url})",
271
+ output_text=f"HTTP error {e.code}: {e.reason} (url={url})",
234
272
  )
235
273
  except urllib.error.URLError as e:
236
- return model.ToolResultItem(
274
+ return message.ToolResultMessage(
237
275
  status="error",
238
- output=f"URL error: {e.reason} (url={url})",
276
+ output_text=f"URL error: {e.reason} (url={url})",
239
277
  )
240
278
  except TimeoutError:
241
- return model.ToolResultItem(
279
+ return message.ToolResultMessage(
242
280
  status="error",
243
- output=f"Request timed out after {DEFAULT_TIMEOUT_SEC} seconds (url={url})",
281
+ output_text=f"Request timed out after {WEB_FETCH_DEFAULT_TIMEOUT_SEC} seconds (url={url})",
244
282
  )
245
283
  except Exception as e:
246
- return model.ToolResultItem(
284
+ return message.ToolResultMessage(
247
285
  status="error",
248
- output=f"Failed to fetch URL: {e} (url={url})",
286
+ output_text=f"Failed to fetch URL: {e} (url={url})",
249
287
  )
@@ -4,12 +4,10 @@ from pathlib import Path
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
+ from klaude_code.const import WEB_SEARCH_DEFAULT_MAX_RESULTS, WEB_SEARCH_MAX_RESULTS_LIMIT
7
8
  from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolMetadata, load_desc
8
9
  from klaude_code.core.tool.tool_registry import register
9
- from klaude_code.protocol import llm_param, model, tools
10
-
11
- DEFAULT_MAX_RESULTS = 10
12
- MAX_RESULTS_LIMIT = 20
10
+ from klaude_code.protocol import llm_param, message, tools
13
11
 
14
12
 
15
13
  @dataclass
@@ -81,7 +79,7 @@ class WebSearchTool(ToolABC):
81
79
  },
82
80
  "max_results": {
83
81
  "type": "integer",
84
- "description": f"Maximum number of results to return (default: {DEFAULT_MAX_RESULTS}, max: {MAX_RESULTS_LIMIT})",
82
+ "description": f"Maximum number of results to return (default: {WEB_SEARCH_DEFAULT_MAX_RESULTS}, max: {WEB_SEARCH_MAX_RESULTS_LIMIT})",
85
83
  },
86
84
  },
87
85
  "required": ["query"],
@@ -90,41 +88,41 @@ class WebSearchTool(ToolABC):
90
88
 
91
89
  class WebSearchArguments(BaseModel):
92
90
  query: str
93
- max_results: int = DEFAULT_MAX_RESULTS
91
+ max_results: int = WEB_SEARCH_DEFAULT_MAX_RESULTS
94
92
 
95
93
  @classmethod
96
- async def call(cls, arguments: str) -> model.ToolResultItem:
94
+ async def call(cls, arguments: str) -> message.ToolResultMessage:
97
95
  try:
98
96
  args = WebSearchTool.WebSearchArguments.model_validate_json(arguments)
99
97
  except ValueError as e:
100
- return model.ToolResultItem(
98
+ return message.ToolResultMessage(
101
99
  status="error",
102
- output=f"Invalid arguments: {e}",
100
+ output_text=f"Invalid arguments: {e}",
103
101
  )
104
102
  return await cls.call_with_args(args)
105
103
 
106
104
  @classmethod
107
- async def call_with_args(cls, args: WebSearchArguments) -> model.ToolResultItem:
105
+ async def call_with_args(cls, args: WebSearchArguments) -> message.ToolResultMessage:
108
106
  query = args.query.strip()
109
107
  if not query:
110
- return model.ToolResultItem(
108
+ return message.ToolResultMessage(
111
109
  status="error",
112
- output="Query cannot be empty",
110
+ output_text="Query cannot be empty",
113
111
  )
114
112
 
115
- max_results = min(max(args.max_results, 1), MAX_RESULTS_LIMIT)
113
+ max_results = min(max(args.max_results, 1), WEB_SEARCH_MAX_RESULTS_LIMIT)
116
114
 
117
115
  try:
118
116
  results = await asyncio.to_thread(_search_duckduckgo, query, max_results)
119
117
  formatted = _format_results(results)
120
118
 
121
- return model.ToolResultItem(
119
+ return message.ToolResultMessage(
122
120
  status="success",
123
- output=formatted,
121
+ output_text=formatted,
124
122
  )
125
123
 
126
124
  except Exception as e:
127
- return model.ToolResultItem(
125
+ return message.ToolResultMessage(
128
126
  status="error",
129
- output=f"Search failed: {e}",
127
+ output_text=f"Search failed: {e}",
130
128
  )
klaude_code/core/turn.py CHANGED
@@ -4,12 +4,15 @@ from collections.abc import AsyncGenerator
4
4
  from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING
6
6
 
7
+ from klaude_code.const import INTERRUPT_MARKER, SUPPORTED_IMAGE_SIZES
7
8
  from klaude_code.core.tool import ToolABC, tool_context
9
+ from klaude_code.core.tool.tool_context import current_sub_agent_resume_claims
8
10
 
9
11
  if TYPE_CHECKING:
10
12
  from klaude_code.core.task import SessionContext
11
13
 
12
14
  from klaude_code.core.tool.tool_runner import (
15
+ ToolCallRequest,
13
16
  ToolExecutionCallStarted,
14
17
  ToolExecutionResult,
15
18
  ToolExecutionTodoChange,
@@ -17,7 +20,7 @@ from klaude_code.core.tool.tool_runner import (
17
20
  ToolExecutorEvent,
18
21
  )
19
22
  from klaude_code.llm import LLMClientABC
20
- from klaude_code.protocol import events, llm_param, model, tools
23
+ from klaude_code.protocol import events, llm_param, message, model, tools
21
24
  from klaude_code.trace import DebugType, log_debug
22
25
 
23
26
 
@@ -36,16 +39,16 @@ class TurnExecutionContext:
36
39
  system_prompt: str | None
37
40
  tools: list[llm_param.ToolSchema]
38
41
  tool_registry: dict[str, type[ToolABC]]
42
+ sub_agent_state: model.SubAgentState | None = None
39
43
 
40
44
 
41
45
  @dataclass
42
46
  class TurnResult:
43
47
  """Aggregated state produced while executing a turn."""
44
48
 
45
- reasoning_items: list[model.ReasoningTextItem | model.ReasoningEncryptedItem]
46
- assistant_message: model.AssistantMessageItem | None
47
- tool_calls: list[model.ToolCallItem]
48
- stream_error: model.StreamErrorItem | None
49
+ assistant_message: message.AssistantMessage | None
50
+ tool_calls: list[ToolCallRequest]
51
+ stream_error: message.StreamErrorItem | None
49
52
  report_back_result: str | None = field(default=None)
50
53
 
51
54
 
@@ -61,23 +64,27 @@ def build_events_from_tool_executor_event(session_id: str, event: ToolExecutorEv
61
64
  session_id=session_id,
62
65
  response_id=tool_call.response_id,
63
66
  tool_call_id=tool_call.call_id,
64
- tool_name=tool_call.name,
65
- arguments=tool_call.arguments,
67
+ tool_name=tool_call.tool_name,
68
+ arguments=tool_call.arguments_json,
66
69
  )
67
70
  )
68
- case ToolExecutionResult(tool_call=tool_call, tool_result=tool_result):
71
+ case ToolExecutionResult(tool_call=tool_call, tool_result=tool_result, is_last_in_turn=is_last_in_turn):
72
+ status = "success" if tool_result.status == "success" else "error"
69
73
  ui_events.append(
70
74
  events.ToolResultEvent(
71
75
  session_id=session_id,
72
76
  response_id=tool_call.response_id,
73
77
  tool_call_id=tool_call.call_id,
74
- tool_name=tool_call.name,
75
- result=tool_result.output or "",
78
+ tool_name=tool_call.tool_name,
79
+ result=tool_result.output_text,
76
80
  ui_extra=tool_result.ui_extra,
77
- status=tool_result.status,
81
+ status=status,
78
82
  task_metadata=tool_result.task_metadata,
83
+ is_last_in_turn=is_last_in_turn,
79
84
  )
80
85
  )
86
+ if tool_result.status == "aborted":
87
+ ui_events.append(events.InterruptEvent(session_id=session_id))
81
88
  case ToolExecutionTodoChange(todos=todos):
82
89
  ui_events.append(
83
90
  events.TodoChangeEvent(
@@ -129,7 +136,10 @@ class TurnExecutor:
129
136
  if self._turn_result is not None and self._turn_result.report_back_result is not None:
130
137
  return self._turn_result.report_back_result
131
138
  if self._turn_result is not None and self._turn_result.assistant_message is not None:
132
- return self._turn_result.assistant_message.content or ""
139
+ assistant_message = self._turn_result.assistant_message
140
+ text = message.join_text_parts(assistant_message.parts)
141
+ images = [part for part in assistant_message.parts if isinstance(part, message.ImageFilePart)]
142
+ return message.format_saved_images(images, text)
133
143
  return ""
134
144
 
135
145
  @property
@@ -160,7 +170,6 @@ class TurnExecutor:
160
170
  yield events.TurnStartEvent(session_id=session_ctx.session_id)
161
171
 
162
172
  self._turn_result = TurnResult(
163
- reasoning_items=[],
164
173
  assistant_message=None,
165
174
  tool_calls=[],
166
175
  stream_error=None,
@@ -188,8 +197,8 @@ class TurnExecutor:
188
197
  def _detect_report_back(self, turn_result: TurnResult) -> None:
189
198
  """Detect report_back tool call and store its arguments as JSON string."""
190
199
  for tool_call in turn_result.tool_calls:
191
- if tool_call.name == tools.REPORT_BACK:
192
- turn_result.report_back_result = tool_call.arguments
200
+ if tool_call.tool_name == tools.REPORT_BACK:
201
+ turn_result.report_back_result = tool_call.arguments_json
193
202
  break
194
203
 
195
204
  async def _consume_llm_stream(self, turn_result: TurnResult) -> AsyncGenerator[events.Event]:
@@ -197,98 +206,134 @@ class TurnExecutor:
197
206
 
198
207
  ctx = self._context
199
208
  session_ctx = ctx.session_ctx
200
- async for response_item in ctx.llm_client.call(
201
- llm_param.LLMCallParameter(
202
- input=session_ctx.get_conversation_history(),
203
- system=ctx.system_prompt,
204
- tools=ctx.tools,
205
- session_id=session_ctx.session_id,
206
- )
207
- ):
209
+ message_types = (
210
+ message.SystemMessage,
211
+ message.DeveloperMessage,
212
+ message.UserMessage,
213
+ message.AssistantMessage,
214
+ message.ToolResultMessage,
215
+ )
216
+ messages = [item for item in session_ctx.get_conversation_history() if isinstance(item, message_types)]
217
+ call_param = llm_param.LLMCallParameter(
218
+ input=messages,
219
+ system=ctx.system_prompt,
220
+ tools=ctx.tools,
221
+ session_id=session_ctx.session_id,
222
+ )
223
+
224
+ # ImageGen per-call overrides (tool-level `generation` parameters)
225
+ if ctx.sub_agent_state is not None and ctx.sub_agent_state.sub_agent_type == "ImageGen":
226
+ call_param.modalities = ["image", "text"]
227
+ generation = ctx.sub_agent_state.generation or {}
228
+ image_config = llm_param.ImageConfig()
229
+ aspect_ratio = generation.get("aspect_ratio")
230
+ if isinstance(aspect_ratio, str) and aspect_ratio.strip():
231
+ image_config.aspect_ratio = aspect_ratio.strip()
232
+ image_size = generation.get("image_size")
233
+ if image_size in SUPPORTED_IMAGE_SIZES:
234
+ image_config.image_size = image_size
235
+ extra = generation.get("extra")
236
+ if isinstance(extra, dict) and extra:
237
+ image_config.extra = extra
238
+ if image_config.model_dump(exclude_none=True):
239
+ call_param.image_config = image_config
240
+
241
+ async for delta in ctx.llm_client.call(call_param):
208
242
  log_debug(
209
- f"[{response_item.__class__.__name__}]",
210
- response_item.model_dump_json(exclude_none=True),
243
+ f"[{delta.__class__.__name__}]",
244
+ delta.model_dump_json(exclude_none=True),
211
245
  style="green",
212
246
  debug_type=DebugType.RESPONSE,
213
247
  )
214
- match response_item:
215
- case model.StartItem():
216
- continue
217
- case model.ReasoningTextItem() as item:
218
- turn_result.reasoning_items.append(item)
219
- yield events.ThinkingEvent(
220
- content=item.content,
221
- response_id=item.response_id,
222
- session_id=session_ctx.session_id,
223
- )
224
- case model.ReasoningEncryptedItem() as item:
225
- turn_result.reasoning_items.append(item)
226
- case model.ReasoningTextDelta() as item:
248
+ match delta:
249
+ case message.ThinkingTextDelta() as delta:
227
250
  yield events.ThinkingDeltaEvent(
228
- content=item.content,
229
- response_id=item.response_id,
251
+ content=delta.content,
252
+ response_id=delta.response_id,
230
253
  session_id=session_ctx.session_id,
231
254
  )
232
- case model.AssistantMessageDelta() as item:
233
- if item.response_id:
234
- self._assistant_response_id = item.response_id
235
- self._assistant_delta_buffer.append(item.content)
236
- yield events.AssistantMessageDeltaEvent(
237
- content=item.content,
238
- response_id=item.response_id,
255
+ case message.AssistantTextDelta() as delta:
256
+ if delta.response_id:
257
+ self._assistant_response_id = delta.response_id
258
+ self._assistant_delta_buffer.append(delta.content)
259
+ yield events.AssistantTextDeltaEvent(
260
+ content=delta.content,
261
+ response_id=delta.response_id,
239
262
  session_id=session_ctx.session_id,
240
263
  )
241
- case model.AssistantMessageItem() as item:
242
- turn_result.assistant_message = item
243
- yield events.AssistantMessageEvent(
244
- content=item.content or "",
245
- response_id=item.response_id,
264
+ case message.AssistantImageDelta() as delta:
265
+ yield events.AssistantImageDeltaEvent(
266
+ file_path=delta.file_path,
267
+ response_id=delta.response_id,
246
268
  session_id=session_ctx.session_id,
247
269
  )
248
- case model.ResponseMetadataItem() as item:
249
- yield events.ResponseMetadataEvent(
270
+ case message.AssistantMessage() as msg:
271
+ if msg.response_id is None and self._assistant_response_id:
272
+ msg.response_id = self._assistant_response_id
273
+ turn_result.assistant_message = msg
274
+ for part in msg.parts:
275
+ if isinstance(part, message.ToolCallPart):
276
+ turn_result.tool_calls.append(
277
+ ToolCallRequest(
278
+ response_id=msg.response_id,
279
+ call_id=part.call_id,
280
+ tool_name=part.tool_name,
281
+ arguments_json=part.arguments_json,
282
+ )
283
+ )
284
+ yield events.AssistantMessageEvent(
285
+ content=message.join_text_parts(msg.parts),
286
+ response_id=msg.response_id,
250
287
  session_id=session_ctx.session_id,
251
- metadata=item,
252
288
  )
253
- case model.StreamErrorItem() as item:
254
- turn_result.stream_error = item
289
+ if msg.stop_reason == "aborted":
290
+ yield events.InterruptEvent(session_id=session_ctx.session_id)
291
+ if msg.usage:
292
+ metadata = msg.usage
293
+ if metadata.response_id is None:
294
+ metadata.response_id = msg.response_id
295
+ if not metadata.model_name:
296
+ metadata.model_name = ctx.llm_client.model_name
297
+ if metadata.provider is None:
298
+ metadata.provider = ctx.llm_client.get_llm_config().provider_name or None
299
+ yield events.ResponseMetadataEvent(
300
+ session_id=session_ctx.session_id,
301
+ metadata=metadata,
302
+ )
303
+ case message.StreamErrorItem() as msg:
304
+ turn_result.stream_error = msg
255
305
  log_debug(
256
306
  "[StreamError]",
257
- item.error,
307
+ msg.error,
258
308
  style="red",
259
309
  debug_type=DebugType.RESPONSE,
260
310
  )
261
- case model.ToolCallStartItem() as item:
311
+ case message.ToolCallStartItem() as msg:
262
312
  yield events.TurnToolCallStartEvent(
263
313
  session_id=session_ctx.session_id,
264
- response_id=item.response_id,
265
- tool_call_id=item.call_id,
266
- tool_name=item.name,
314
+ response_id=msg.response_id,
315
+ tool_call_id=msg.call_id,
316
+ tool_name=msg.name,
267
317
  arguments="",
268
318
  )
269
- case model.ToolCallItem() as item:
270
- turn_result.tool_calls.append(item)
271
319
  case _:
272
320
  continue
273
321
 
274
322
  def _append_success_history(self, turn_result: TurnResult) -> None:
275
323
  """Persist successful turn artifacts to conversation history."""
276
324
  session_ctx = self._context.session_ctx
277
- if turn_result.reasoning_items:
278
- session_ctx.append_history(turn_result.reasoning_items)
279
325
  if turn_result.assistant_message:
280
326
  session_ctx.append_history([turn_result.assistant_message])
281
- if turn_result.tool_calls:
282
- session_ctx.append_history(turn_result.tool_calls)
283
327
  self._assistant_delta_buffer.clear()
284
328
  self._assistant_response_id = None
285
329
 
286
- async def _run_tool_executor(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[events.Event]:
330
+ async def _run_tool_executor(self, tool_calls: list[ToolCallRequest]) -> AsyncGenerator[events.Event]:
287
331
  """Run tools for the turn and translate executor events to UI events."""
288
332
 
289
333
  ctx = self._context
290
334
  session_ctx = ctx.session_ctx
291
335
  with tool_context(session_ctx.file_tracker, session_ctx.todo_context):
336
+ resume_claims_token = current_sub_agent_resume_claims.set(set())
292
337
  executor = ToolExecutor(
293
338
  registry=ctx.tool_registry,
294
339
  append_history=session_ctx.append_history,
@@ -300,6 +345,7 @@ class TurnExecutor:
300
345
  yield ui_event
301
346
  finally:
302
347
  self._tool_executor = None
348
+ current_sub_agent_resume_claims.reset(resume_claims_token)
303
349
 
304
350
  def _persist_partial_assistant_on_cancel(self) -> None:
305
351
  """Persist streamed assistant text when a turn is interrupted.
@@ -311,12 +357,13 @@ class TurnExecutor:
311
357
 
312
358
  if not self._assistant_delta_buffer:
313
359
  return
314
- partial_text = "".join(self._assistant_delta_buffer) + "<system interrupted by user>"
360
+ partial_text = "".join(self._assistant_delta_buffer) + INTERRUPT_MARKER
315
361
  if not partial_text:
316
362
  return
317
- message_item = model.AssistantMessageItem(
318
- content=partial_text,
363
+ partial_message = message.AssistantMessage(
364
+ parts=message.text_parts_from_str(partial_text),
319
365
  response_id=self._assistant_response_id,
366
+ stop_reason="aborted",
320
367
  )
321
- self._context.session_ctx.append_history([message_item])
368
+ self._context.session_ctx.append_history([partial_message])
322
369
  self._assistant_delta_buffer.clear()