klaude-code 2.4.1__py3-none-any.whl → 2.5.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 (58) hide show
  1. klaude_code/app/runtime.py +2 -6
  2. klaude_code/cli/main.py +0 -1
  3. klaude_code/config/assets/builtin_config.yaml +7 -0
  4. klaude_code/const.py +7 -4
  5. klaude_code/core/agent.py +10 -1
  6. klaude_code/core/agent_profile.py +47 -35
  7. klaude_code/core/executor.py +6 -21
  8. klaude_code/core/manager/sub_agent_manager.py +17 -1
  9. klaude_code/core/prompts/prompt-sub-agent-web.md +4 -4
  10. klaude_code/core/task.py +65 -4
  11. klaude_code/core/tool/__init__.py +0 -5
  12. klaude_code/core/tool/context.py +12 -1
  13. klaude_code/core/tool/offload.py +311 -0
  14. klaude_code/core/tool/shell/bash_tool.md +1 -43
  15. klaude_code/core/tool/sub_agent_tool.py +1 -0
  16. klaude_code/core/tool/todo/todo_write_tool.md +0 -23
  17. klaude_code/core/tool/tool_runner.py +14 -9
  18. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  19. klaude_code/core/tool/web/web_fetch_tool.py +14 -39
  20. klaude_code/core/turn.py +128 -138
  21. klaude_code/llm/anthropic/client.py +176 -82
  22. klaude_code/llm/bedrock/client.py +8 -12
  23. klaude_code/llm/claude/client.py +11 -15
  24. klaude_code/llm/client.py +31 -4
  25. klaude_code/llm/codex/client.py +7 -11
  26. klaude_code/llm/google/client.py +150 -69
  27. klaude_code/llm/openai_compatible/client.py +10 -15
  28. klaude_code/llm/openai_compatible/stream.py +68 -6
  29. klaude_code/llm/openrouter/client.py +9 -15
  30. klaude_code/llm/partial_message.py +35 -0
  31. klaude_code/llm/responses/client.py +134 -68
  32. klaude_code/llm/usage.py +30 -0
  33. klaude_code/protocol/commands.py +0 -4
  34. klaude_code/protocol/events/metadata.py +1 -0
  35. klaude_code/protocol/events/streaming.py +1 -0
  36. klaude_code/protocol/events/system.py +0 -4
  37. klaude_code/protocol/model.py +2 -15
  38. klaude_code/protocol/sub_agent/explore.py +0 -10
  39. klaude_code/protocol/sub_agent/image_gen.py +0 -7
  40. klaude_code/protocol/sub_agent/task.py +0 -10
  41. klaude_code/protocol/sub_agent/web.py +4 -12
  42. klaude_code/session/templates/export_session.html +4 -4
  43. klaude_code/skill/manager.py +2 -1
  44. klaude_code/tui/components/metadata.py +41 -49
  45. klaude_code/tui/components/rich/markdown.py +1 -3
  46. klaude_code/tui/components/rich/theme.py +2 -2
  47. klaude_code/tui/components/sub_agent.py +9 -1
  48. klaude_code/tui/components/tools.py +0 -31
  49. klaude_code/tui/components/welcome.py +1 -32
  50. klaude_code/tui/input/prompt_toolkit.py +25 -9
  51. klaude_code/tui/machine.py +40 -8
  52. klaude_code/tui/renderer.py +1 -0
  53. {klaude_code-2.4.1.dist-info → klaude_code-2.5.0.dist-info}/METADATA +2 -2
  54. {klaude_code-2.4.1.dist-info → klaude_code-2.5.0.dist-info}/RECORD +56 -56
  55. klaude_code/core/prompts/prompt-nano-banana.md +0 -1
  56. klaude_code/core/tool/truncation.py +0 -203
  57. {klaude_code-2.4.1.dist-info → klaude_code-2.5.0.dist-info}/WHEEL +0 -0
  58. {klaude_code-2.4.1.dist-info → klaude_code-2.5.0.dist-info}/entry_points.txt +0 -0
@@ -11,7 +11,6 @@ from klaude_code.config import Config, load_config
11
11
  from klaude_code.core.agent import Agent
12
12
  from klaude_code.core.agent_profile import (
13
13
  DefaultModelProfileProvider,
14
- NanoBananaModelProfileProvider,
15
14
  VanillaModelProfileProvider,
16
15
  )
17
16
  from klaude_code.core.executor import Executor
@@ -28,7 +27,6 @@ class AppInitConfig:
28
27
  model: str | None
29
28
  debug: bool
30
29
  vanilla: bool
31
- banana: bool
32
30
  debug_filters: set[DebugType] | None = None
33
31
 
34
32
 
@@ -59,7 +57,7 @@ async def initialize_app_components(
59
57
  llm_clients = build_llm_clients(
60
58
  config,
61
59
  model_override=init_config.model,
62
- skip_sub_agents=init_config.vanilla or init_config.banana,
60
+ skip_sub_agents=init_config.vanilla,
63
61
  )
64
62
  except ValueError as exc:
65
63
  if init_config.model:
@@ -74,9 +72,7 @@ async def initialize_app_components(
74
72
  log((f"Error: failed to load the default model configuration: {exc}", "red"))
75
73
  raise typer.Exit(2) from None
76
74
 
77
- if init_config.banana:
78
- model_profile_provider = NanoBananaModelProfileProvider()
79
- elif init_config.vanilla:
75
+ if init_config.vanilla:
80
76
  model_profile_provider = VanillaModelProfileProvider()
81
77
  else:
82
78
  model_profile_provider = DefaultModelProfileProvider(config=config)
klaude_code/cli/main.py CHANGED
@@ -222,7 +222,6 @@ def main_callback(
222
222
  model=chosen_model,
223
223
  debug=debug_enabled,
224
224
  vanilla=vanilla,
225
- banana=banana,
226
225
  debug_filters=debug_filters,
227
226
  )
228
227
 
@@ -152,6 +152,13 @@ provider_list:
152
152
  modalities:
153
153
  - image
154
154
  - text
155
+ - model_name: flux
156
+ model_id: black-forest-labs/flux.2-max
157
+ context_limit: 47000
158
+ cost: {input: 7.32, output: 7.32, image: 7.32}
159
+ modalities:
160
+ - image
161
+ - text
155
162
  - provider_name: google
156
163
  protocol: google
157
164
  api_key: ${GOOGLE_API_KEY}
klaude_code/const.py CHANGED
@@ -47,7 +47,6 @@ THROUGHPUT_MIN_DURATION_SEC = 0.15 # Minimum duration (seconds) for throughput
47
47
  INITIAL_RETRY_DELAY_S = 1.0 # Initial delay before retrying a failed turn (seconds)
48
48
  MAX_RETRY_DELAY_S = 30.0 # Maximum delay between retries (seconds)
49
49
  CANCEL_OUTPUT = "[Request interrupted by user for tool use]" # Message shown when tool call is cancelled
50
- INTERRUPT_MARKER = " <system>interrupted by user</system>" # Marker appended when assistant is interrupted
51
50
  EMPTY_TOOL_OUTPUT_MESSAGE = (
52
51
  "<system-reminder>Tool ran without output or errors</system-reminder>" # Tool output placeholder
53
52
  )
@@ -112,7 +111,10 @@ DIFF_DEFAULT_CONTEXT_LINES = 3 # Default number of context lines in diff output
112
111
  TOOL_OUTPUT_MAX_LENGTH = 40000 # Maximum length for tool output before truncation
113
112
  TOOL_OUTPUT_DISPLAY_HEAD = 10000 # Characters to show from the beginning of truncated output
114
113
  TOOL_OUTPUT_DISPLAY_TAIL = 10000 # Characters to show from the end of truncated output
115
- TOOL_OUTPUT_TRUNCATION_DIR = "/tmp/klaude" # Directory for saving full truncated output
114
+ TOOL_OUTPUT_MAX_LINES = 2000 # Maximum lines for tool output before truncation
115
+ TOOL_OUTPUT_DISPLAY_HEAD_LINES = 1000 # Lines to show from the beginning of truncated output
116
+ TOOL_OUTPUT_DISPLAY_TAIL_LINES = 1000 # Lines to show from the end of truncated output
117
+ TOOL_OUTPUT_TRUNCATION_DIR = "/tmp" # Directory for saving full truncated output
116
118
 
117
119
 
118
120
  # =============================================================================
@@ -156,8 +158,8 @@ STATUS_HINT_TEXT = " (esc to interrupt)" # Status hint text shown after spinner
156
158
 
157
159
  # Spinner status texts
158
160
  STATUS_WAITING_TEXT = "Connecting …"
159
- STATUS_THINKING_TEXT = "Reasoning …"
160
- STATUS_COMPOSING_TEXT = "Generating"
161
+ STATUS_THINKING_TEXT = "Thinking …"
162
+ STATUS_COMPOSING_TEXT = "Composing …"
161
163
 
162
164
  # Backwards-compatible alias for the default spinner status text.
163
165
  STATUS_DEFAULT_TEXT = STATUS_WAITING_TEXT
@@ -166,6 +168,7 @@ SPINNER_BREATH_PERIOD_SECONDS: float = 2.0 # Spinner breathing animation period
166
168
  STATUS_SHIMMER_PADDING = 10 # Horizontal padding for shimmer band position
167
169
  STATUS_SHIMMER_BAND_HALF_WIDTH = 5.0 # Half-width of shimmer band in characters
168
170
  STATUS_SHIMMER_ALPHA_SCALE = 0.7 # Scale factor for shimmer intensity
171
+ STATUS_SHOW_BUFFER_LENGTH = False # Show character count (e.g., "(213)") during text generation
169
172
 
170
173
 
171
174
  # =============================================================================
klaude_code/core/agent.py CHANGED
@@ -8,7 +8,7 @@ from klaude_code.core.tool import build_todo_context, get_registry
8
8
  from klaude_code.core.tool.context import RunSubtask
9
9
  from klaude_code.llm import LLMClientABC
10
10
  from klaude_code.log import DebugType, log_debug
11
- from klaude_code.protocol import events
11
+ from klaude_code.protocol import events, model
12
12
  from klaude_code.protocol.message import UserInputPayload
13
13
  from klaude_code.session import Session
14
14
 
@@ -93,3 +93,12 @@ class Agent:
93
93
 
94
94
  def get_llm_client(self) -> LLMClientABC:
95
95
  return self.profile.llm_client
96
+
97
+ def get_partial_metadata(self) -> model.TaskMetadata | None:
98
+ """Get partial metadata from the currently running task.
99
+
100
+ Returns None if no task is running or no usage data has been accumulated.
101
+ """
102
+ if self._current_task is None:
103
+ return None
104
+ return self._current_task.get_partial_metadata()
@@ -62,10 +62,7 @@ PROMPT_FILES: dict[str, str] = {
62
62
  }
63
63
 
64
64
 
65
- NANO_BANANA_SYSTEM_PROMPT_PATH = "prompts/prompt-nano-banana.md"
66
-
67
-
68
- STRUCTURED_OUTPUT_PROMPT = """\
65
+ STRUCTURED_OUTPUT_PROMPT_FOR_SUB_AGENT = """\
69
66
 
70
67
  # Structured Output
71
68
  You have a `report_back` tool available. When you complete the task,\
@@ -74,6 +71,20 @@ Only the content passed to `report_back` will be returned to user.\
74
71
  """
75
72
 
76
73
 
74
+ SUB_AGENT_COMMON_PROMPT_FOR_MAIN_AGENT = """\
75
+
76
+ # Sub-agent capabilities
77
+ You have sub-agents (e.g. Task, Explore, WebAgent, ImageGen) with structured output and resume capabilites:
78
+ - Agents can be provided with an `output_format` (JSON Schema) parameter for structured output
79
+ - Example: `output_format={"type": "object", "properties": {"files": {"type": "array", "items": {"type": "string"}, "description": "List of file paths that match the search criteria, e.g. ['src/main.py', 'src/utils/helper.py']"}}, "required": ["files"]}`
80
+ - Agents can be resumed using the `resume` parameter by passing the agent ID from a previous invocation. \
81
+ When resumed, the agent continues with its full previous context preserved. \
82
+ When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
83
+ - When the agent is done, it will return a single message back to you along with its agent ID. \
84
+ You can use this ID to resume the agent later if needed for follow-up work.
85
+ """
86
+
87
+
77
88
  @cache
78
89
  def _load_prompt_by_path(prompt_path: str) -> str:
79
90
  """Load and cache prompt content from a file path relative to core package."""
@@ -142,10 +153,24 @@ def _build_env_info(model_name: str) -> str:
142
153
  return "\n".join(env_lines)
143
154
 
144
155
 
156
+ def _has_sub_agents(config: Config | None) -> bool:
157
+ """Check if there are any sub-agent tools available for the main agent."""
158
+ if config is not None:
159
+ from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
160
+
161
+ helper = SubAgentModelHelper(config)
162
+ return bool(helper.get_enabled_sub_agent_tool_names())
163
+
164
+ from klaude_code.protocol.sub_agent import sub_agent_tool_names
165
+
166
+ return bool(sub_agent_tool_names(enabled_only=True))
167
+
168
+
145
169
  def load_system_prompt(
146
170
  model_name: str,
147
171
  protocol: llm_param.LLMClientProtocol,
148
172
  sub_agent_type: str | None = None,
173
+ config: Config | None = None,
149
174
  ) -> str:
150
175
  """Get system prompt content for the given model and sub-agent type."""
151
176
 
@@ -161,13 +186,18 @@ def load_system_prompt(
161
186
  return base_prompt
162
187
 
163
188
  skills_prompt = ""
189
+ sub_agent_prompt = ""
164
190
  if sub_agent_type is None:
165
191
  # Skills are progressive-disclosure: keep only metadata in the system prompt.
166
192
  from klaude_code.skill.manager import format_available_skills_for_system_prompt
167
193
 
168
194
  skills_prompt = format_available_skills_for_system_prompt()
169
195
 
170
- return base_prompt + _build_env_info(model_name) + skills_prompt
196
+ # Add sub-agent resume instructions if there are sub-agent tools available.
197
+ if _has_sub_agents(config):
198
+ sub_agent_prompt = "\n" + SUB_AGENT_COMMON_PROMPT_FOR_MAIN_AGENT
199
+
200
+ return base_prompt + _build_env_info(model_name) + skills_prompt + sub_agent_prompt
171
201
 
172
202
 
173
203
  def load_agent_tools(
@@ -245,7 +275,7 @@ def with_structured_output(profile: AgentProfile, output_schema: dict[str, Any])
245
275
  base_prompt = profile.system_prompt or ""
246
276
  return AgentProfile(
247
277
  llm_client=profile.llm_client,
248
- system_prompt=base_prompt + STRUCTURED_OUTPUT_PROMPT,
278
+ system_prompt=base_prompt + STRUCTURED_OUTPUT_PROMPT_FOR_SUB_AGENT,
249
279
  tools=[*profile.tools, report_back_tool_class.schema()],
250
280
  reminders=profile.reminders,
251
281
  )
@@ -279,17 +309,24 @@ class DefaultModelProfileProvider(ModelProfileProvider):
279
309
  model_name = llm_client.model_name
280
310
  llm_config = llm_client.get_llm_config()
281
311
 
282
- # Image generation models should not have tools
283
- if llm_config.modalities and "image" in llm_config.modalities:
312
+ # Image generation models should not have system prompt, tools, or reminders
313
+ is_image_model = llm_config.modalities and "image" in llm_config.modalities
314
+ if is_image_model:
315
+ agent_system_prompt: str | None = None
284
316
  agent_tools: list[llm_param.ToolSchema] = []
317
+ agent_reminders: list[Reminder] = []
285
318
  else:
319
+ agent_system_prompt = load_system_prompt(
320
+ model_name, llm_client.protocol, sub_agent_type, config=self._config
321
+ )
286
322
  agent_tools = load_agent_tools(model_name, sub_agent_type, config=self._config)
323
+ agent_reminders = load_agent_reminders(model_name, sub_agent_type)
287
324
 
288
325
  profile = AgentProfile(
289
326
  llm_client=llm_client,
290
- system_prompt=load_system_prompt(model_name, llm_client.protocol, sub_agent_type),
327
+ system_prompt=agent_system_prompt,
291
328
  tools=agent_tools,
292
- reminders=load_agent_reminders(model_name, sub_agent_type),
329
+ reminders=agent_reminders,
293
330
  )
294
331
  if output_schema:
295
332
  return with_structured_output(profile, output_schema)
@@ -316,28 +353,3 @@ class VanillaModelProfileProvider(ModelProfileProvider):
316
353
  if output_schema:
317
354
  return with_structured_output(profile, output_schema)
318
355
  return profile
319
-
320
-
321
- class NanoBananaModelProfileProvider(ModelProfileProvider):
322
- """Provider for the Nano Banana image generation model.
323
-
324
- This mode uses a dedicated system prompt and strips all tools/reminders.
325
- """
326
-
327
- def build_profile(
328
- self,
329
- llm_client: LLMClientABC,
330
- sub_agent_type: tools.SubAgentType | None = None,
331
- *,
332
- output_schema: dict[str, Any] | None = None,
333
- ) -> AgentProfile:
334
- del sub_agent_type
335
- profile = AgentProfile(
336
- llm_client=llm_client,
337
- system_prompt=_load_prompt_by_path(NANO_BANANA_SYSTEM_PROMPT_PATH),
338
- tools=[],
339
- reminders=[],
340
- )
341
- if output_schema:
342
- return with_structured_output(profile, output_schema)
343
- return profile
@@ -17,14 +17,14 @@ from pathlib import Path
17
17
  from klaude_code.config import load_config
18
18
  from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
19
19
  from klaude_code.core.agent import Agent
20
- from klaude_code.core.agent_profile import AgentProfile, DefaultModelProfileProvider, ModelProfileProvider
20
+ from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
21
21
  from klaude_code.core.manager import LLMClients, SubAgentManager
22
22
  from klaude_code.llm.registry import create_llm_client
23
23
  from klaude_code.log import DebugType, log_debug
24
24
  from klaude_code.protocol import commands, events, message, model, op
25
25
  from klaude_code.protocol.llm_param import LLMConfigParameter, Thinking
26
26
  from klaude_code.protocol.op_handler import OperationHandler
27
- from klaude_code.protocol.sub_agent import SubAgentResult, get_sub_agent_profile_by_tool
27
+ from klaude_code.protocol.sub_agent import SubAgentResult
28
28
  from klaude_code.session.export import build_export_html, get_default_export_path
29
29
  from klaude_code.session.session import Session
30
30
 
@@ -110,19 +110,6 @@ class AgentRuntime:
110
110
  def current_agent(self) -> Agent | None:
111
111
  return self._agent
112
112
 
113
- def _get_sub_agent_models(self, profile: AgentProfile) -> dict[str, LLMConfigParameter]:
114
- """Build a dict of sub-agent type to LLMConfigParameter based on profile tools."""
115
- enabled_types: set[str] = set()
116
- for tool in profile.tools:
117
- sub_profile = get_sub_agent_profile_by_tool(tool.name)
118
- if sub_profile is not None:
119
- enabled_types.add(sub_profile.name)
120
- return {
121
- sub_agent_type: client.get_llm_config()
122
- for sub_agent_type, client in self._llm_clients.sub_clients.items()
123
- if sub_agent_type in enabled_types
124
- }
125
-
126
113
  async def ensure_agent(self, session_id: str | None = None) -> Agent:
127
114
  """Return the active agent, creating or loading a session as needed."""
128
115
 
@@ -149,7 +136,6 @@ class AgentRuntime:
149
136
  session_id=session.id,
150
137
  work_dir=str(session.work_dir),
151
138
  llm_config=self._llm_clients.main.get_llm_config(),
152
- sub_agent_models=self._get_sub_agent_models(profile),
153
139
  )
154
140
  )
155
141
 
@@ -206,7 +192,6 @@ class AgentRuntime:
206
192
  session_id=agent.session.id,
207
193
  work_dir=str(agent.session.work_dir),
208
194
  llm_config=self._llm_clients.main.get_llm_config(),
209
- sub_agent_models=self._get_sub_agent_models(agent.profile),
210
195
  )
211
196
  )
212
197
 
@@ -230,7 +215,6 @@ class AgentRuntime:
230
215
  session_id=target_session.id,
231
216
  work_dir=str(target_session.work_dir),
232
217
  llm_config=self._llm_clients.main.get_llm_config(),
233
- sub_agent_models=self._get_sub_agent_models(profile),
234
218
  )
235
219
  )
236
220
 
@@ -291,8 +275,11 @@ class AgentRuntime:
291
275
  async def _runner(
292
276
  state: model.SubAgentState,
293
277
  record_session_id: Callable[[str], None] | None,
278
+ register_metadata_getter: Callable[[Callable[[], model.TaskMetadata | None]], None] | None,
294
279
  ) -> SubAgentResult:
295
- return await self._sub_agent_manager.run_sub_agent(agent, state, record_session_id=record_session_id)
280
+ return await self._sub_agent_manager.run_sub_agent(
281
+ agent, state, record_session_id=record_session_id, register_metadata_getter=register_metadata_getter
282
+ )
296
283
 
297
284
  async for event in agent.run_task(user_input, run_subtask=_runner):
298
285
  await self._emit_event(event)
@@ -464,7 +451,6 @@ class ExecutorContext:
464
451
  llm_config=llm_config,
465
452
  work_dir=str(agent.session.work_dir),
466
453
  show_klaude_code_info=False,
467
- show_sub_agent_models=False,
468
454
  )
469
455
  )
470
456
 
@@ -512,7 +498,6 @@ class ExecutorContext:
512
498
  work_dir=str(agent.session.work_dir),
513
499
  llm_config=agent.profile.llm_client.get_llm_config(),
514
500
  show_klaude_code_info=False,
515
- show_sub_agent_models=False,
516
501
  )
517
502
  )
518
503
 
@@ -44,6 +44,7 @@ class SubAgentManager:
44
44
  state: model.SubAgentState,
45
45
  *,
46
46
  record_session_id: Callable[[str], None] | None = None,
47
+ register_metadata_getter: Callable[[Callable[[], model.TaskMetadata | None]], None] | None = None,
47
48
  ) -> SubAgentResult:
48
49
  """Run a nested sub-agent task and return its result."""
49
50
 
@@ -114,6 +115,16 @@ class SubAgentManager:
114
115
  debug_type=DebugType.EXECUTION,
115
116
  )
116
117
 
118
+ # Register metadata getter so parent can retrieve partial metadata on cancel
119
+ def _get_partial_metadata() -> model.TaskMetadata | None:
120
+ metadata = child_agent.get_partial_metadata()
121
+ if metadata is not None:
122
+ metadata.description = state.sub_agent_desc or None
123
+ return metadata
124
+
125
+ if register_metadata_getter is not None:
126
+ register_metadata_getter(_get_partial_metadata)
127
+
117
128
  try:
118
129
  # Not emit the subtask's user input since task tool call is already rendered
119
130
  result: str = ""
@@ -138,6 +149,7 @@ class SubAgentManager:
138
149
  # Capture TaskMetadataEvent for metadata propagation
139
150
  elif isinstance(event, events.TaskMetadataEvent):
140
151
  task_metadata = event.metadata.main_agent
152
+ task_metadata.description = state.sub_agent_desc or None
141
153
  await self.emit_event(event)
142
154
 
143
155
  # Ensure the sub-agent session is persisted before returning its id for resume.
@@ -148,7 +160,11 @@ class SubAgentManager:
148
160
  task_metadata=task_metadata,
149
161
  )
150
162
  except asyncio.CancelledError:
151
- # Propagate cancellation so tooling can treat it as user interrupt
163
+ # Call cancel() on child agent to emit cleanup events
164
+ # Note: Parent retrieves partial metadata via registered getter before this runs
165
+ for evt in child_agent.cancel():
166
+ await self.emit_event(evt)
167
+
152
168
  log_debug(
153
169
  f"Sub-agent task for {state.sub_agent_type} was cancelled",
154
170
  style="yellow",
@@ -17,7 +17,7 @@ You are a web research subagent that searches and fetches web content to provide
17
17
  - HTML pages are automatically converted to Markdown
18
18
  - JSON responses are auto-formatted with indentation
19
19
  - Other text content returned as-is
20
- - **Content is always saved to a local file** - check `<file_saved>` tag for the path
20
+ - **Content is always saved to a local file** - path shown in `[Web content saved to ...]` at output start
21
21
 
22
22
  ## Tool Usage Strategy
23
23
 
@@ -54,9 +54,9 @@ Balance efficiency with thoroughness. For open-ended questions (e.g., "recommend
54
54
  ## Response Guidelines
55
55
 
56
56
  - Only your last message is returned to the main agent
57
+ - Include the file path from `[Web content saved to ...]` so the main agent can access full content
57
58
  - **DO NOT copy full web page content** - the main agent can read the saved files directly
58
59
  - Provide a concise summary/analysis of key findings
59
- - Include the file path from `<file_saved>` so the main agent can access full content if needed
60
60
  - Lead with the most recent info for evolving topics
61
61
  - Favor original sources (company blogs, papers, gov sites) over aggregators
62
62
  - When sources conflict, explain the discrepancy and which source is more authoritative
@@ -73,5 +73,5 @@ Stop only when all are true:
73
73
  You MUST end every response with a "Sources:" section listing all URLs with their saved file paths:
74
74
 
75
75
  Sources:
76
- - [Source Title](https://example.com) -> /tmp/klaude/web/example_com-123456.md
77
- - [Another Source](https://example.com/page) -> /tmp/klaude/web/example_com_page-123456.md
76
+ - [Source Title](https://example.com) -> /tmp/klaude-webfetch-example_com.txt
77
+ - [Another Source](https://example.com/page) -> /tmp/klaude-webfetch-example_com_page.txt
klaude_code/core/task.py CHANGED
@@ -66,6 +66,49 @@ class MetadataAccumulator:
66
66
  """Add sub-agent task metadata to the accumulated state."""
67
67
  self._sub_agent_metadata.append(sub_agent_metadata)
68
68
 
69
+ def get_partial(self, task_duration_s: float) -> model.TaskMetadata | None:
70
+ """Return a snapshot of main agent metadata without modifying accumulator state.
71
+
72
+ Returns None if no usage data has been accumulated yet.
73
+ """
74
+ if self._main_agent.usage is None:
75
+ return None
76
+
77
+ # Create a copy to avoid modifying the original
78
+ usage_copy = self._main_agent.usage.model_copy(deep=True)
79
+
80
+ if self._throughput_tracked_tokens > 0:
81
+ usage_copy.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
82
+ else:
83
+ usage_copy.throughput_tps = None
84
+
85
+ if self._first_token_latency_count > 0:
86
+ usage_copy.first_token_latency_ms = self._first_token_latency_sum / self._first_token_latency_count
87
+ else:
88
+ usage_copy.first_token_latency_ms = None
89
+
90
+ return model.TaskMetadata(
91
+ model_name=self._main_agent.model_name,
92
+ provider=self._main_agent.provider,
93
+ usage=usage_copy,
94
+ task_duration_s=task_duration_s,
95
+ turn_count=self._turn_count,
96
+ )
97
+
98
+ def get_partial_item(self, task_duration_s: float) -> model.TaskMetadataItem | None:
99
+ """Return a snapshot of full metadata (main + sub-agents) without modifying state.
100
+
101
+ Returns None if no usage data has been accumulated yet.
102
+ """
103
+ main_agent = self.get_partial(task_duration_s)
104
+ if main_agent is None:
105
+ return None
106
+
107
+ return model.TaskMetadataItem(
108
+ main_agent=main_agent,
109
+ sub_agent_task_metadata=list(self._sub_agent_metadata),
110
+ )
111
+
69
112
  def finalize(self, task_duration_s: float) -> model.TaskMetadataItem:
70
113
  """Return the final accumulated metadata with computed throughput and duration."""
71
114
  if self._main_agent.usage is not None:
@@ -129,20 +172,38 @@ class TaskExecutor:
129
172
  def current_turn(self) -> TurnExecutor | None:
130
173
  return self._current_turn
131
174
 
175
+ def get_partial_metadata(self) -> model.TaskMetadata | None:
176
+ """Get the currently accumulated metadata without finalizing.
177
+
178
+ Returns partial metadata that can be used if the task is interrupted.
179
+ """
180
+ if self._metadata_accumulator is None or self._started_at <= 0:
181
+ return None
182
+ task_duration_s = time.perf_counter() - self._started_at
183
+ return self._metadata_accumulator.get_partial(task_duration_s)
184
+
132
185
  def cancel(self) -> list[events.Event]:
133
186
  """Cancel the current turn and return any resulting events including metadata."""
134
187
  ui_events: list[events.Event] = []
135
188
  if self._current_turn is not None:
136
- ui_events.extend(self._current_turn.cancel())
189
+ for evt in self._current_turn.cancel():
190
+ # Collect sub-agent task metadata from cancelled tool results
191
+ if (
192
+ isinstance(evt, events.ToolResultEvent)
193
+ and evt.task_metadata is not None
194
+ and self._metadata_accumulator is not None
195
+ ):
196
+ self._metadata_accumulator.add_sub_agent_metadata(evt.task_metadata)
197
+ ui_events.append(evt)
137
198
  self._current_turn = None
138
199
 
139
200
  # Emit partial metadata on cancellation
140
201
  if self._metadata_accumulator is not None and self._started_at > 0:
141
202
  task_duration_s = time.perf_counter() - self._started_at
142
- accumulated = self._metadata_accumulator.finalize(task_duration_s)
143
- if accumulated.main_agent.usage is not None:
203
+ accumulated = self._metadata_accumulator.get_partial_item(task_duration_s)
204
+ if accumulated is not None:
144
205
  session_id = self._context.session_ctx.session_id
145
- ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id))
206
+ ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id, cancelled=True))
146
207
  self._context.session_ctx.append_history([accumulated])
147
208
 
148
209
  return ui_events
@@ -13,7 +13,6 @@ from .todo.update_plan_tool import UpdatePlanTool
13
13
  from .tool_abc import ToolABC
14
14
  from .tool_registry import get_registry, get_tool_schemas
15
15
  from .tool_runner import run_tool
16
- from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
17
16
  from .web.mermaid_tool import MermaidTool
18
17
  from .web.web_fetch_tool import WebFetchTool
19
18
  from .web.web_search_tool import WebSearchTool
@@ -29,14 +28,12 @@ __all__ = [
29
28
  "ReportBackTool",
30
29
  "RunSubtask",
31
30
  "SafetyCheckResult",
32
- "SimpleTruncationStrategy",
33
31
  "SubAgentResumeClaims",
34
32
  "SubAgentTool",
35
33
  "TodoContext",
36
34
  "TodoWriteTool",
37
35
  "ToolABC",
38
36
  "ToolContext",
39
- "TruncationStrategy",
40
37
  "UpdatePlanTool",
41
38
  "WebFetchTool",
42
39
  "WebSearchTool",
@@ -44,9 +41,7 @@ __all__ = [
44
41
  "build_todo_context",
45
42
  "get_registry",
46
43
  "get_tool_schemas",
47
- "get_truncation_strategy",
48
44
  "is_safe_command",
49
45
  "process_patch",
50
46
  "run_tool",
51
- "set_truncation_strategy",
52
47
  ]
@@ -10,7 +10,12 @@ from klaude_code.session.session import Session
10
10
 
11
11
  type FileTracker = MutableMapping[str, model.FileStatus]
12
12
 
13
- RunSubtask = Callable[[model.SubAgentState, Callable[[str], None] | None], Awaitable[SubAgentResult]]
13
+ GetMetadataFn = Callable[[], model.TaskMetadata | None]
14
+
15
+ RunSubtask = Callable[
16
+ [model.SubAgentState, Callable[[str], None] | None, Callable[[GetMetadataFn], None] | None],
17
+ Awaitable[SubAgentResult],
18
+ ]
14
19
 
15
20
 
16
21
  @dataclass
@@ -79,6 +84,12 @@ class ToolContext:
79
84
  run_subtask: RunSubtask | None = None
80
85
  sub_agent_resume_claims: SubAgentResumeClaims | None = None
81
86
  record_sub_agent_session_id: Callable[[str], None] | None = None
87
+ register_sub_agent_metadata_getter: Callable[[GetMetadataFn], None] | None = None
82
88
 
83
89
  def with_record_sub_agent_session_id(self, callback: Callable[[str], None] | None) -> ToolContext:
84
90
  return replace(self, record_sub_agent_session_id=callback)
91
+
92
+ def with_register_sub_agent_metadata_getter(
93
+ self, callback: Callable[[GetMetadataFn], None] | None
94
+ ) -> ToolContext:
95
+ return replace(self, register_sub_agent_metadata_getter=callback)