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.
- klaude_code/auth/base.py +2 -6
- klaude_code/cli/auth_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -1
- klaude_code/cli/main.py +1 -1
- klaude_code/cli/runtime.py +7 -5
- klaude_code/cli/self_update.py +1 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +2 -2
- klaude_code/command/debug_cmd.py +4 -4
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +12 -12
- klaude_code/command/fork_session_cmd.py +29 -23
- klaude_code/command/help_cmd.py +4 -4
- klaude_code/command/model_cmd.py +4 -4
- klaude_code/command/model_select.py +1 -1
- klaude_code/command/prompt-commit.md +11 -2
- klaude_code/command/prompt_command.py +3 -3
- klaude_code/command/refresh_cmd.py +2 -2
- klaude_code/command/registry.py +7 -5
- klaude_code/command/release_notes_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +15 -11
- klaude_code/command/status_cmd.py +4 -4
- klaude_code/command/terminal_setup_cmd.py +8 -8
- klaude_code/command/thinking_cmd.py +4 -4
- klaude_code/config/assets/builtin_config.yaml +16 -0
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +7 -2
- klaude_code/const.py +146 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +21 -13
- klaude_code/core/manager/sub_agent_manager.py +71 -7
- klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
- klaude_code/core/reminders.py +88 -69
- klaude_code/core/task.py +44 -45
- klaude_code/core/tool/file/apply_patch_tool.py +9 -9
- klaude_code/core/tool/file/diff_builder.py +3 -5
- klaude_code/core/tool/file/edit_tool.py +23 -23
- klaude_code/core/tool/file/move_tool.py +43 -43
- klaude_code/core/tool/file/read_tool.py +44 -39
- klaude_code/core/tool/file/write_tool.py +14 -14
- klaude_code/core/tool/report_back_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +23 -23
- klaude_code/core/tool/skill/skill_tool.py +7 -7
- klaude_code/core/tool/sub_agent_tool.py +38 -9
- klaude_code/core/tool/todo/todo_write_tool.py +8 -8
- klaude_code/core/tool/todo/update_plan_tool.py +6 -6
- klaude_code/core/tool/tool_abc.py +2 -2
- klaude_code/core/tool/tool_context.py +27 -0
- klaude_code/core/tool/tool_runner.py +88 -42
- klaude_code/core/tool/truncation.py +38 -20
- klaude_code/core/tool/web/mermaid_tool.py +6 -7
- klaude_code/core/tool/web/web_fetch_tool.py +68 -30
- klaude_code/core/tool/web/web_search_tool.py +15 -17
- klaude_code/core/turn.py +120 -73
- klaude_code/llm/anthropic/client.py +79 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/client.py +18 -8
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +15 -9
- klaude_code/llm/google/client.py +122 -60
- klaude_code/llm/google/input.py +94 -108
- klaude_code/llm/image.py +123 -0
- klaude_code/llm/input_common.py +136 -189
- klaude_code/llm/openai_compatible/client.py +17 -7
- klaude_code/llm/openai_compatible/input.py +36 -66
- klaude_code/llm/openai_compatible/stream.py +119 -67
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
- klaude_code/llm/openrouter/client.py +34 -9
- klaude_code/llm/openrouter/input.py +63 -64
- klaude_code/llm/openrouter/reasoning.py +22 -24
- klaude_code/llm/registry.py +20 -17
- klaude_code/llm/responses/client.py +107 -45
- klaude_code/llm/responses/input.py +115 -98
- klaude_code/llm/usage.py +52 -25
- klaude_code/protocol/__init__.py +1 -0
- klaude_code/protocol/events.py +16 -12
- klaude_code/protocol/llm_param.py +20 -2
- klaude_code/protocol/message.py +250 -0
- klaude_code/protocol/model.py +94 -281
- klaude_code/protocol/op.py +2 -2
- klaude_code/protocol/sub_agent/__init__.py +1 -0
- klaude_code/protocol/sub_agent/explore.py +10 -0
- klaude_code/protocol/sub_agent/image_gen.py +119 -0
- klaude_code/protocol/sub_agent/task.py +10 -0
- klaude_code/protocol/sub_agent/web.py +10 -0
- klaude_code/session/codec.py +6 -6
- klaude_code/session/export.py +261 -62
- klaude_code/session/selector.py +7 -24
- klaude_code/session/session.py +126 -54
- klaude_code/session/store.py +5 -32
- klaude_code/session/templates/export_session.html +1 -1
- klaude_code/session/templates/mermaid_viewer.html +1 -1
- klaude_code/trace/log.py +11 -6
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +1 -8
- klaude_code/ui/modes/debug/display.py +2 -2
- klaude_code/ui/modes/repl/clipboard.py +2 -2
- klaude_code/ui/modes/repl/completers.py +18 -10
- klaude_code/ui/modes/repl/event_handler.py +136 -127
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/common.py +65 -7
- klaude_code/ui/renderers/developer.py +7 -6
- klaude_code/ui/renderers/diffs.py +11 -11
- klaude_code/ui/renderers/mermaid_viewer.py +49 -2
- klaude_code/ui/renderers/metadata.py +33 -5
- klaude_code/ui/renderers/sub_agent.py +57 -16
- klaude_code/ui/renderers/thinking.py +37 -2
- klaude_code/ui/renderers/tools.py +180 -165
- klaude_code/ui/rich/live.py +3 -1
- klaude_code/ui/rich/markdown.py +39 -7
- klaude_code/ui/rich/quote.py +76 -1
- klaude_code/ui/rich/status.py +14 -8
- klaude_code/ui/rich/theme.py +8 -2
- klaude_code/ui/terminal/image.py +34 -0
- klaude_code/ui/terminal/notifier.py +2 -1
- klaude_code/ui/terminal/progress_bar.py +4 -4
- klaude_code/ui/terminal/selector.py +22 -4
- klaude_code/ui/utils/common.py +11 -2
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +4 -2
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code-1.9.0.dist-info/RECORD +0 -224
- {klaude_code-1.9.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {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
|
|
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,
|
|
21
|
+
from klaude_code.protocol import llm_param, message, tools
|
|
17
22
|
|
|
18
|
-
|
|
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[:
|
|
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}
|
|
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 =
|
|
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,
|
|
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":
|
|
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
|
-
|
|
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) ->
|
|
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
|
|
220
|
+
return message.ToolResultMessage(
|
|
199
221
|
status="error",
|
|
200
|
-
|
|
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) ->
|
|
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
|
|
232
|
+
return message.ToolResultMessage(
|
|
211
233
|
status="error",
|
|
212
|
-
|
|
234
|
+
output_text=f"Invalid URL: must start with http:// or https:// (url={url})",
|
|
213
235
|
)
|
|
214
236
|
|
|
215
237
|
try:
|
|
216
|
-
content_type,
|
|
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
|
|
263
|
+
return message.ToolResultMessage(
|
|
226
264
|
status="success",
|
|
227
|
-
|
|
265
|
+
output_text=output,
|
|
228
266
|
)
|
|
229
267
|
|
|
230
268
|
except urllib.error.HTTPError as e:
|
|
231
|
-
return
|
|
269
|
+
return message.ToolResultMessage(
|
|
232
270
|
status="error",
|
|
233
|
-
|
|
271
|
+
output_text=f"HTTP error {e.code}: {e.reason} (url={url})",
|
|
234
272
|
)
|
|
235
273
|
except urllib.error.URLError as e:
|
|
236
|
-
return
|
|
274
|
+
return message.ToolResultMessage(
|
|
237
275
|
status="error",
|
|
238
|
-
|
|
276
|
+
output_text=f"URL error: {e.reason} (url={url})",
|
|
239
277
|
)
|
|
240
278
|
except TimeoutError:
|
|
241
|
-
return
|
|
279
|
+
return message.ToolResultMessage(
|
|
242
280
|
status="error",
|
|
243
|
-
|
|
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
|
|
284
|
+
return message.ToolResultMessage(
|
|
247
285
|
status="error",
|
|
248
|
-
|
|
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,
|
|
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: {
|
|
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 =
|
|
91
|
+
max_results: int = WEB_SEARCH_DEFAULT_MAX_RESULTS
|
|
94
92
|
|
|
95
93
|
@classmethod
|
|
96
|
-
async def call(cls, arguments: str) ->
|
|
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
|
|
98
|
+
return message.ToolResultMessage(
|
|
101
99
|
status="error",
|
|
102
|
-
|
|
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) ->
|
|
105
|
+
async def call_with_args(cls, args: WebSearchArguments) -> message.ToolResultMessage:
|
|
108
106
|
query = args.query.strip()
|
|
109
107
|
if not query:
|
|
110
|
-
return
|
|
108
|
+
return message.ToolResultMessage(
|
|
111
109
|
status="error",
|
|
112
|
-
|
|
110
|
+
output_text="Query cannot be empty",
|
|
113
111
|
)
|
|
114
112
|
|
|
115
|
-
max_results = min(max(args.max_results, 1),
|
|
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
|
|
119
|
+
return message.ToolResultMessage(
|
|
122
120
|
status="success",
|
|
123
|
-
|
|
121
|
+
output_text=formatted,
|
|
124
122
|
)
|
|
125
123
|
|
|
126
124
|
except Exception as e:
|
|
127
|
-
return
|
|
125
|
+
return message.ToolResultMessage(
|
|
128
126
|
status="error",
|
|
129
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
65
|
-
arguments=tool_call.
|
|
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.
|
|
75
|
-
result=tool_result.
|
|
78
|
+
tool_name=tool_call.tool_name,
|
|
79
|
+
result=tool_result.output_text,
|
|
76
80
|
ui_extra=tool_result.ui_extra,
|
|
77
|
-
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
|
-
|
|
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.
|
|
192
|
-
turn_result.report_back_result = tool_call.
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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"[{
|
|
210
|
-
|
|
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
|
|
215
|
-
case
|
|
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=
|
|
229
|
-
response_id=
|
|
251
|
+
content=delta.content,
|
|
252
|
+
response_id=delta.response_id,
|
|
230
253
|
session_id=session_ctx.session_id,
|
|
231
254
|
)
|
|
232
|
-
case
|
|
233
|
-
if
|
|
234
|
-
self._assistant_response_id =
|
|
235
|
-
self._assistant_delta_buffer.append(
|
|
236
|
-
yield events.
|
|
237
|
-
content=
|
|
238
|
-
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
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
249
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
307
|
+
msg.error,
|
|
258
308
|
style="red",
|
|
259
309
|
debug_type=DebugType.RESPONSE,
|
|
260
310
|
)
|
|
261
|
-
case
|
|
311
|
+
case message.ToolCallStartItem() as msg:
|
|
262
312
|
yield events.TurnToolCallStartEvent(
|
|
263
313
|
session_id=session_ctx.session_id,
|
|
264
|
-
response_id=
|
|
265
|
-
tool_call_id=
|
|
266
|
-
tool_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[
|
|
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) +
|
|
360
|
+
partial_text = "".join(self._assistant_delta_buffer) + INTERRUPT_MARKER
|
|
315
361
|
if not partial_text:
|
|
316
362
|
return
|
|
317
|
-
|
|
318
|
-
|
|
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([
|
|
368
|
+
self._context.session_ctx.append_history([partial_message])
|
|
322
369
|
self._assistant_delta_buffer.clear()
|