klaude-code 1.2.6__py3-none-any.whl → 1.8.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 (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -12,6 +12,9 @@ class LLMClientProtocol(Enum):
12
12
  RESPONSES = "responses"
13
13
  OPENROUTER = "openrouter"
14
14
  ANTHROPIC = "anthropic"
15
+ BEDROCK = "bedrock"
16
+ CODEX = "codex"
17
+ GOOGLE = "google"
15
18
 
16
19
 
17
20
  class ToolSchema(BaseModel):
@@ -27,7 +30,7 @@ class Thinking(BaseModel):
27
30
  """
28
31
 
29
32
  # OpenAI Reasoning Style
30
- reasoning_effort: Literal["high", "medium", "low", "minimal", "none"] | None = None
33
+ reasoning_effort: Literal["high", "medium", "low", "minimal", "none", "xhigh"] | None = None
31
34
  reasoning_summary: Literal["auto", "concise", "detailed"] | None = None
32
35
 
33
36
  # Claude/Gemini Thinking Style
@@ -36,12 +39,13 @@ class Thinking(BaseModel):
36
39
 
37
40
 
38
41
  class Cost(BaseModel):
39
- """Cost configuration per million tokens (USD)."""
42
+ """Cost configuration per million tokens."""
40
43
 
41
44
  input: float # Input token price per million tokens
42
45
  output: float # Output token price per million tokens
43
46
  cache_read: float = 0.0 # Cache read price per million tokens
44
47
  cache_write: float = 0.0 # Cache write price per million tokens (ignored in calculation for now)
48
+ currency: Literal["USD", "CNY"] = "USD" # Currency for cost display
45
49
 
46
50
 
47
51
  class OpenRouterProviderRouting(BaseModel):
@@ -89,8 +93,15 @@ class LLMConfigProviderParameter(BaseModel):
89
93
  protocol: LLMClientProtocol
90
94
  base_url: str | None = None
91
95
  api_key: str | None = None
96
+ # Azure OpenAI
92
97
  is_azure: bool = False
93
98
  azure_api_version: str | None = None
99
+ # AWS Bedrock configuration
100
+ aws_access_key: str | None = None
101
+ aws_secret_key: str | None = None
102
+ aws_region: str | None = None
103
+ aws_session_token: str | None = None
104
+ aws_profile: str | None = None
94
105
 
95
106
 
96
107
  class LLMConfigModelParameter(BaseModel):
@@ -136,12 +147,4 @@ class LLMCallParameter(LLMConfigModelParameter):
136
147
  input: list[ConversationItem]
137
148
  system: str | None = None
138
149
  tools: list[ToolSchema] | None = None
139
-
140
- stream: Literal[True] = True # Always True
141
-
142
- # OpenAI Responses
143
- include: list[str] | None = None
144
- store: bool = True
145
- previous_response_id: str | None = None
146
-
147
150
  session_id: str | None = None
@@ -1,9 +1,10 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum
3
- from typing import Literal
3
+ from typing import Annotated, Any, Literal
4
4
 
5
- from pydantic import BaseModel, Field
5
+ from pydantic import BaseModel, ConfigDict, Field, computed_field
6
6
 
7
+ from klaude_code import const
7
8
  from klaude_code.protocol.commands import CommandName
8
9
  from klaude_code.protocol.tools import SubAgentType
9
10
 
@@ -12,26 +13,73 @@ TodoStatusType = Literal["pending", "in_progress", "completed"]
12
13
 
13
14
 
14
15
  class Usage(BaseModel):
16
+ # Token Usage (primary state)
15
17
  input_tokens: int = 0
16
18
  cached_tokens: int = 0
17
19
  reasoning_tokens: int = 0
18
20
  output_tokens: int = 0
19
- total_tokens: int = 0
20
- context_usage_percent: float | None = None
21
+
22
+ # Context window tracking
23
+ context_size: int | None = None # Peak total_tokens seen (for context usage display)
24
+ context_limit: int | None = None # Model's context limit
25
+ max_tokens: int | None = None # Max output tokens for this request
26
+
21
27
  throughput_tps: float | None = None
22
28
  first_token_latency_ms: float | None = None
23
29
 
24
- # Cost in USD (calculated from token counts and cost config)
30
+ # Cost (calculated from token counts and cost config)
25
31
  input_cost: float | None = None # Cost for non-cached input tokens
26
32
  output_cost: float | None = None # Cost for output tokens (including reasoning)
27
33
  cache_read_cost: float | None = None # Cost for cached tokens
28
- total_cost: float | None = None # Total cost (input + output + cache_read)
34
+ currency: str = "USD" # Currency for cost display (USD or CNY)
35
+
36
+ @computed_field
37
+ @property
38
+ def total_tokens(self) -> int:
39
+ """Total tokens computed from input + output tokens."""
40
+ return self.input_tokens + self.output_tokens
41
+
42
+ @computed_field
43
+ @property
44
+ def total_cost(self) -> float | None:
45
+ """Total cost computed from input + output + cache_read costs."""
46
+ costs = [self.input_cost, self.output_cost, self.cache_read_cost]
47
+ non_none = [c for c in costs if c is not None]
48
+ return sum(non_none) if non_none else None
49
+
50
+ @computed_field
51
+ @property
52
+ def context_usage_percent(self) -> float | None:
53
+ """Context usage percentage computed from context_token / (context_limit - max_tokens)."""
54
+ if self.context_limit is None or self.context_limit <= 0:
55
+ return None
56
+ if self.context_size is None:
57
+ return None
58
+ effective_limit = self.context_limit - (self.max_tokens or const.DEFAULT_MAX_TOKENS)
59
+ if effective_limit <= 0:
60
+ return None
61
+ return (self.context_size / effective_limit) * 100
29
62
 
30
63
 
31
64
  class TodoItem(BaseModel):
65
+ model_config = ConfigDict(populate_by_name=True)
66
+
32
67
  content: str
33
68
  status: TodoStatusType
34
- activeForm: str = ""
69
+ active_form: str = Field(default="", alias="activeForm")
70
+
71
+
72
+ class FileStatus(BaseModel):
73
+ """Tracks file state including modification time and content hash.
74
+
75
+ Notes:
76
+ - `mtime` is a cheap heuristic and may miss changes on some filesystems.
77
+ - `content_sha256` provides an explicit content-based change detector.
78
+ """
79
+
80
+ mtime: float
81
+ content_sha256: str | None = None
82
+ is_memory: bool = False
35
83
 
36
84
 
37
85
  class TodoUIExtra(BaseModel):
@@ -39,43 +87,104 @@ class TodoUIExtra(BaseModel):
39
87
  new_completed: list[str]
40
88
 
41
89
 
42
- class ToolResultUIExtraType(str, Enum):
43
- DIFF_TEXT = "diff_text"
44
- TODO_LIST = "todo_list"
45
- SESSION_ID = "session_id"
46
- MERMAID_LINK = "mermaid_link"
47
- TRUNCATION = "truncation"
48
- SESSION_STATUS = "session_status"
49
-
50
-
51
90
  class ToolSideEffect(str, Enum):
52
91
  TODO_CHANGE = "todo_change"
53
92
 
54
93
 
94
+ # Discriminated union types for ToolResultUIExtra
95
+ class DiffSpan(BaseModel):
96
+ op: Literal["equal", "insert", "delete"]
97
+ text: str
98
+
99
+
100
+ class DiffLine(BaseModel):
101
+ kind: Literal["ctx", "add", "remove", "gap"]
102
+ new_line_no: int | None = None
103
+ spans: list[DiffSpan]
104
+
105
+
106
+ class DiffFileDiff(BaseModel):
107
+ file_path: str
108
+ lines: list[DiffLine]
109
+ stats_add: int = 0
110
+ stats_remove: int = 0
111
+
112
+
113
+ class DiffUIExtra(BaseModel):
114
+ type: Literal["diff"] = "diff"
115
+ files: list[DiffFileDiff]
116
+
117
+
118
+ class TodoListUIExtra(BaseModel):
119
+ type: Literal["todo_list"] = "todo_list"
120
+ todo_list: TodoUIExtra
121
+
122
+
123
+ class SessionIdUIExtra(BaseModel):
124
+ type: Literal["session_id"] = "session_id"
125
+ session_id: str
126
+
127
+
55
128
  class MermaidLinkUIExtra(BaseModel):
129
+ type: Literal["mermaid_link"] = "mermaid_link"
130
+ code: str = ""
56
131
  link: str
57
132
  line_count: int
58
133
 
59
134
 
60
135
  class TruncationUIExtra(BaseModel):
136
+ type: Literal["truncation"] = "truncation"
61
137
  saved_file_path: str
62
138
  original_length: int
63
139
  truncated_length: int
64
140
 
65
141
 
142
+ class MarkdownDocUIExtra(BaseModel):
143
+ type: Literal["markdown_doc"] = "markdown_doc"
144
+ file_path: str
145
+ content: str
146
+
147
+
66
148
  class SessionStatusUIExtra(BaseModel):
149
+ type: Literal["session_status"] = "session_status"
67
150
  usage: "Usage"
68
151
  task_count: int
152
+ by_model: list["TaskMetadata"] = []
153
+
154
+
155
+ MultiUIExtraItem = (
156
+ DiffUIExtra
157
+ | TodoListUIExtra
158
+ | SessionIdUIExtra
159
+ | MermaidLinkUIExtra
160
+ | TruncationUIExtra
161
+ | MarkdownDocUIExtra
162
+ | SessionStatusUIExtra
163
+ )
164
+
69
165
 
166
+ class MultiUIExtra(BaseModel):
167
+ """A container UIExtra that can render multiple UI blocks for a single tool result.
168
+
169
+ This is primarily used by tools like apply_patch which can perform multiple
170
+ operations in one invocation.
171
+ """
172
+
173
+ type: Literal["multi"] = "multi"
174
+ items: list[MultiUIExtraItem]
70
175
 
71
- class ToolResultUIExtra(BaseModel):
72
- type: ToolResultUIExtraType
73
- diff_text: str | None = None
74
- todo_list: TodoUIExtra | None = None
75
- session_id: str | None = None
76
- mermaid_link: MermaidLinkUIExtra | None = None
77
- truncation: TruncationUIExtra | None = None
78
- session_status: SessionStatusUIExtra | None = None
176
+
177
+ ToolResultUIExtra = Annotated[
178
+ DiffUIExtra
179
+ | TodoListUIExtra
180
+ | SessionIdUIExtra
181
+ | MermaidLinkUIExtra
182
+ | TruncationUIExtra
183
+ | MarkdownDocUIExtra
184
+ | SessionStatusUIExtra
185
+ | MultiUIExtra,
186
+ Field(discriminator="type"),
187
+ ]
79
188
 
80
189
 
81
190
  class AtPatternParseResult(BaseModel):
@@ -85,6 +194,7 @@ class AtPatternParseResult(BaseModel):
85
194
  tool_args: str
86
195
  operation: Literal["Read", "List"]
87
196
  images: list["ImageURLPart"] | None = None
197
+ mentioned_in: str | None = None # Parent file that referenced this file
88
198
 
89
199
 
90
200
  class CommandOutput(BaseModel):
@@ -97,6 +207,7 @@ class SubAgentState(BaseModel):
97
207
  sub_agent_type: SubAgentType
98
208
  sub_agent_desc: str
99
209
  sub_agent_prompt: str
210
+ output_schema: dict[str, Any] | None = None
100
211
 
101
212
 
102
213
  """
@@ -121,7 +232,7 @@ A conversation history input contains:
121
232
  - [DeveloperMessageItem]
122
233
 
123
234
  When adding a new item, please also modify the following:
124
- - session.py#_TypeMap
235
+ - session/codec.py (ConversationItem registry derived from ConversationItem union)
125
236
  """
126
237
 
127
238
 
@@ -150,11 +261,13 @@ class DeveloperMessageItem(BaseModel):
150
261
 
151
262
  # Special fields for reminders UI
152
263
  memory_paths: list[str] | None = None
264
+ memory_mentioned: dict[str, list[str]] | None = None # memory_path -> list of @ patterns mentioned in it
153
265
  external_file_changes: list[str] | None = None
154
266
  todo_use: bool | None = None
155
267
  at_files: list[AtPatternParseResult] | None = None
156
268
  command_output: CommandOutput | None = None
157
269
  user_image_count: int | None = None
270
+ skill_name: str | None = None # Skill name activated via $skill syntax
158
271
 
159
272
 
160
273
  class ImageURLPart(BaseModel):
@@ -239,6 +352,7 @@ class ToolResultItem(BaseModel):
239
352
  ui_extra: ToolResultUIExtra | None = None # Extra data for UI display, e.g. diff render
240
353
  images: list[ImageURLPart] | None = None
241
354
  side_effects: list[ToolSideEffect] | None = None
355
+ task_metadata: "TaskMetadata | None" = None # Sub-agent task metadata for propagation to main agent
242
356
  created_at: datetime = Field(default_factory=datetime.now)
243
357
 
244
358
 
@@ -248,19 +362,101 @@ class AssistantMessageDelta(BaseModel):
248
362
  created_at: datetime = Field(default_factory=datetime.now)
249
363
 
250
364
 
365
+ class ReasoningTextDelta(BaseModel):
366
+ response_id: str | None = None
367
+ content: str
368
+ created_at: datetime = Field(default_factory=datetime.now)
369
+
370
+
251
371
  class StreamErrorItem(BaseModel):
252
372
  error: str
253
373
  created_at: datetime = Field(default_factory=datetime.now)
254
374
 
255
375
 
256
376
  class ResponseMetadataItem(BaseModel):
377
+ """Metadata for a single LLM response (turn-level)."""
378
+
257
379
  response_id: str | None = None
258
380
  usage: Usage | None = None
259
381
  model_name: str = ""
260
382
  provider: str | None = None # OpenRouter's provider name
261
383
  task_duration_s: float | None = None
262
- status: str | None = None
263
- error_reason: str | None = None
384
+ created_at: datetime = Field(default_factory=datetime.now)
385
+
386
+
387
+ class TaskMetadata(BaseModel):
388
+ """Base metadata for a task execution (used by both main and sub-agents)."""
389
+
390
+ usage: Usage | None = None
391
+ model_name: str = ""
392
+ provider: str | None = None
393
+ task_duration_s: float | None = None
394
+ turn_count: int = 0
395
+
396
+ @staticmethod
397
+ def merge_usage(dst: Usage, src: Usage) -> None:
398
+ """Merge src usage into dst usage (in-place).
399
+
400
+ Accumulates token counts and cost components. Does not handle
401
+ special fields like throughput_tps, first_token_latency_ms,
402
+ context_size, or context_limit - those require custom logic.
403
+ """
404
+ dst.input_tokens += src.input_tokens
405
+ dst.cached_tokens += src.cached_tokens
406
+ dst.reasoning_tokens += src.reasoning_tokens
407
+ dst.output_tokens += src.output_tokens
408
+
409
+ if src.input_cost is not None:
410
+ dst.input_cost = (dst.input_cost or 0.0) + src.input_cost
411
+ if src.output_cost is not None:
412
+ dst.output_cost = (dst.output_cost or 0.0) + src.output_cost
413
+ if src.cache_read_cost is not None:
414
+ dst.cache_read_cost = (dst.cache_read_cost or 0.0) + src.cache_read_cost
415
+
416
+ @staticmethod
417
+ def aggregate_by_model(metadata_list: list["TaskMetadata"]) -> list["TaskMetadata"]:
418
+ """Aggregate multiple TaskMetadata by (model_name, provider).
419
+
420
+ Returns a list sorted by total_cost descending.
421
+
422
+ Note: total_tokens and total_cost are now computed fields,
423
+ so we only accumulate the primary state fields here.
424
+ """
425
+ aggregated: dict[tuple[str, str | None], TaskMetadata] = {}
426
+
427
+ for meta in metadata_list:
428
+ if not meta.usage:
429
+ continue
430
+
431
+ key = (meta.model_name, meta.provider)
432
+ usage = meta.usage
433
+
434
+ if key not in aggregated:
435
+ aggregated[key] = TaskMetadata(
436
+ model_name=meta.model_name,
437
+ provider=meta.provider,
438
+ usage=Usage(currency=usage.currency),
439
+ )
440
+
441
+ agg = aggregated[key]
442
+ if agg.usage is None:
443
+ continue
444
+
445
+ TaskMetadata.merge_usage(agg.usage, usage)
446
+
447
+ # Sort by total_cost descending
448
+ return sorted(
449
+ aggregated.values(),
450
+ key=lambda m: m.usage.total_cost if m.usage and m.usage.total_cost else 0.0,
451
+ reverse=True,
452
+ )
453
+
454
+
455
+ class TaskMetadataItem(BaseModel):
456
+ """Aggregated metadata for a complete task, stored in conversation history."""
457
+
458
+ main_agent: TaskMetadata = Field(default_factory=TaskMetadata) # Main agent metadata
459
+ sub_agent_task_metadata: list[TaskMetadata] = Field(default_factory=lambda: list[TaskMetadata]())
264
460
  created_at: datetime = Field(default_factory=datetime.now)
265
461
 
266
462
 
@@ -276,10 +472,17 @@ MessageItem = (
276
472
  )
277
473
 
278
474
 
279
- StreamItem = AssistantMessageDelta
475
+ StreamItem = AssistantMessageDelta | ReasoningTextDelta
280
476
 
281
477
  ConversationItem = (
282
- StartItem | InterruptItem | StreamErrorItem | StreamItem | MessageItem | ResponseMetadataItem | ToolCallStartItem
478
+ StartItem
479
+ | InterruptItem
480
+ | StreamErrorItem
481
+ | StreamItem
482
+ | MessageItem
483
+ | ResponseMetadataItem
484
+ | TaskMetadataItem
485
+ | ToolCallStartItem
283
486
  )
284
487
 
285
488
 
@@ -13,6 +13,7 @@ from uuid import uuid4
13
13
 
14
14
  from pydantic import BaseModel, Field
15
15
 
16
+ from klaude_code.protocol.llm_param import Thinking
16
17
  from klaude_code.protocol.model import UserInputPayload
17
18
 
18
19
  if TYPE_CHECKING:
@@ -23,6 +24,12 @@ class OperationType(Enum):
23
24
  """Enumeration of supported operation types."""
24
25
 
25
26
  USER_INPUT = "user_input"
27
+ RUN_AGENT = "run_agent"
28
+ CHANGE_MODEL = "change_model"
29
+ CHANGE_THINKING = "change_thinking"
30
+ CLEAR_SESSION = "clear_session"
31
+ RESUME_SESSION = "resume_session"
32
+ EXPORT_SESSION = "export_session"
26
33
  INTERRUPT = "interrupt"
27
34
  INIT_AGENT = "init_agent"
28
35
  END = "end"
@@ -51,6 +58,84 @@ class UserInputOperation(Operation):
51
58
  await handler.handle_user_input(self)
52
59
 
53
60
 
61
+ class RunAgentOperation(Operation):
62
+ """Operation for launching an agent task for a given session."""
63
+
64
+ type: OperationType = OperationType.RUN_AGENT
65
+ session_id: str
66
+ input: UserInputPayload
67
+
68
+ async def execute(self, handler: OperationHandler) -> None:
69
+ await handler.handle_run_agent(self)
70
+
71
+
72
+ class ChangeModelOperation(Operation):
73
+ """Operation for changing the model used by the active agent session."""
74
+
75
+ type: OperationType = OperationType.CHANGE_MODEL
76
+ session_id: str
77
+ model_name: str
78
+ save_as_default: bool = False
79
+ # When True, the executor must not auto-trigger an interactive thinking selector.
80
+ # This is required for in-prompt model switching where the terminal is already
81
+ # controlled by a prompt_toolkit PromptSession.
82
+ defer_thinking_selection: bool = False
83
+ # When False, do not emit WelcomeEvent (which renders a banner/panel).
84
+ # This is useful for in-prompt model switching where extra output is noisy.
85
+ emit_welcome_event: bool = True
86
+
87
+ # When False, do not emit the "Switched to: ..." developer message.
88
+ # This is useful for in-prompt model switching where extra output is noisy.
89
+ emit_switch_message: bool = True
90
+
91
+ async def execute(self, handler: OperationHandler) -> None:
92
+ await handler.handle_change_model(self)
93
+
94
+
95
+ class ChangeThinkingOperation(Operation):
96
+ """Operation for changing the thinking/reasoning configuration."""
97
+
98
+ type: OperationType = OperationType.CHANGE_THINKING
99
+ session_id: str
100
+ thinking: Thinking | None = None
101
+ emit_welcome_event: bool = True
102
+ emit_switch_message: bool = True
103
+
104
+ async def execute(self, handler: OperationHandler) -> None:
105
+ await handler.handle_change_thinking(self)
106
+
107
+
108
+ class ClearSessionOperation(Operation):
109
+ """Operation for clearing the active session and starting a new one."""
110
+
111
+ type: OperationType = OperationType.CLEAR_SESSION
112
+ session_id: str
113
+
114
+ async def execute(self, handler: OperationHandler) -> None:
115
+ await handler.handle_clear_session(self)
116
+
117
+
118
+ class ResumeSessionOperation(Operation):
119
+ """Operation for resuming a different session."""
120
+
121
+ type: OperationType = OperationType.RESUME_SESSION
122
+ target_session_id: str
123
+
124
+ async def execute(self, handler: OperationHandler) -> None:
125
+ await handler.handle_resume_session(self)
126
+
127
+
128
+ class ExportSessionOperation(Operation):
129
+ """Operation for exporting a session transcript to HTML."""
130
+
131
+ type: OperationType = OperationType.EXPORT_SESSION
132
+ session_id: str
133
+ output_path: str | None = None
134
+
135
+ async def execute(self, handler: OperationHandler) -> None:
136
+ await handler.handle_export_session(self)
137
+
138
+
54
139
  class InterruptOperation(Operation):
55
140
  """Operation for interrupting currently running tasks."""
56
141
 
@@ -63,7 +148,11 @@ class InterruptOperation(Operation):
63
148
 
64
149
 
65
150
  class InitAgentOperation(Operation):
66
- """Operation for initializing an agent and replaying history if any."""
151
+ """Operation for initializing an agent and replaying history if any.
152
+
153
+ If session_id is None, a new session is created with an auto-generated ID.
154
+ If session_id is provided, attempts to load existing session or creates new one.
155
+ """
67
156
 
68
157
  type: OperationType = OperationType.INIT_AGENT
69
158
  session_id: str | None = None
@@ -9,7 +9,17 @@ from __future__ import annotations
9
9
  from typing import TYPE_CHECKING, Protocol
10
10
 
11
11
  if TYPE_CHECKING:
12
- from klaude_code.protocol.op import InitAgentOperation, InterruptOperation, UserInputOperation
12
+ from klaude_code.protocol.op import (
13
+ ChangeModelOperation,
14
+ ChangeThinkingOperation,
15
+ ClearSessionOperation,
16
+ ExportSessionOperation,
17
+ InitAgentOperation,
18
+ InterruptOperation,
19
+ ResumeSessionOperation,
20
+ RunAgentOperation,
21
+ UserInputOperation,
22
+ )
13
23
 
14
24
 
15
25
  class OperationHandler(Protocol):
@@ -19,6 +29,30 @@ class OperationHandler(Protocol):
19
29
  """Handle a user input operation."""
20
30
  ...
21
31
 
32
+ async def handle_run_agent(self, operation: RunAgentOperation) -> None:
33
+ """Handle a run agent operation."""
34
+ ...
35
+
36
+ async def handle_change_model(self, operation: ChangeModelOperation) -> None:
37
+ """Handle a change model operation."""
38
+ ...
39
+
40
+ async def handle_change_thinking(self, operation: ChangeThinkingOperation) -> None:
41
+ """Handle a change thinking operation."""
42
+ ...
43
+
44
+ async def handle_clear_session(self, operation: ClearSessionOperation) -> None:
45
+ """Handle a clear session operation."""
46
+ ...
47
+
48
+ async def handle_resume_session(self, operation: ResumeSessionOperation) -> None:
49
+ """Handle a resume session operation."""
50
+ ...
51
+
52
+ async def handle_export_session(self, operation: ExportSessionOperation) -> None:
53
+ """Handle an export session operation."""
54
+ ...
55
+
22
56
  async def handle_interrupt(self, operation: InterruptOperation) -> None:
23
57
  """Handle an interrupt operation."""
24
58
  ...