bhu-cli 1.46.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 (258) hide show
  1. bhu_cli-1.46.0.dist-info/METADATA +210 -0
  2. bhu_cli-1.46.0.dist-info/RECORD +258 -0
  3. bhu_cli-1.46.0.dist-info/WHEEL +4 -0
  4. bhu_cli-1.46.0.dist-info/entry_points.txt +4 -0
  5. kimi_cli/CHANGELOG.md +1281 -0
  6. kimi_cli/__init__.py +29 -0
  7. kimi_cli/__main__.py +43 -0
  8. kimi_cli/_build_info.py +1 -0
  9. kimi_cli/acp/AGENTS.md +92 -0
  10. kimi_cli/acp/__init__.py +13 -0
  11. kimi_cli/acp/convert.py +128 -0
  12. kimi_cli/acp/kaos.py +291 -0
  13. kimi_cli/acp/mcp.py +46 -0
  14. kimi_cli/acp/server.py +468 -0
  15. kimi_cli/acp/session.py +499 -0
  16. kimi_cli/acp/tools.py +167 -0
  17. kimi_cli/acp/types.py +13 -0
  18. kimi_cli/acp/version.py +45 -0
  19. kimi_cli/agents/default/agent.yaml +36 -0
  20. kimi_cli/agents/default/coder.yaml +25 -0
  21. kimi_cli/agents/default/explore.yaml +46 -0
  22. kimi_cli/agents/default/plan.yaml +30 -0
  23. kimi_cli/agents/default/system.md +160 -0
  24. kimi_cli/agents/okabe/agent.yaml +22 -0
  25. kimi_cli/agentspec.py +160 -0
  26. kimi_cli/app.py +807 -0
  27. kimi_cli/approval_runtime/__init__.py +29 -0
  28. kimi_cli/approval_runtime/models.py +42 -0
  29. kimi_cli/approval_runtime/runtime.py +235 -0
  30. kimi_cli/auth/__init__.py +5 -0
  31. kimi_cli/auth/oauth.py +1092 -0
  32. kimi_cli/auth/platforms.py +374 -0
  33. kimi_cli/background/__init__.py +36 -0
  34. kimi_cli/background/agent_runner.py +231 -0
  35. kimi_cli/background/ids.py +19 -0
  36. kimi_cli/background/manager.py +725 -0
  37. kimi_cli/background/models.py +105 -0
  38. kimi_cli/background/store.py +237 -0
  39. kimi_cli/background/summary.py +66 -0
  40. kimi_cli/background/worker.py +209 -0
  41. kimi_cli/cli/__init__.py +1081 -0
  42. kimi_cli/cli/__main__.py +34 -0
  43. kimi_cli/cli/_lazy_group.py +238 -0
  44. kimi_cli/cli/export.py +322 -0
  45. kimi_cli/cli/info.py +62 -0
  46. kimi_cli/cli/mcp.py +353 -0
  47. kimi_cli/cli/plugin.py +347 -0
  48. kimi_cli/cli/toad.py +73 -0
  49. kimi_cli/cli/vis.py +38 -0
  50. kimi_cli/cli/web.py +80 -0
  51. kimi_cli/config.py +429 -0
  52. kimi_cli/constant.py +116 -0
  53. kimi_cli/exception.py +43 -0
  54. kimi_cli/hooks/__init__.py +24 -0
  55. kimi_cli/hooks/config.py +34 -0
  56. kimi_cli/hooks/defaults/rtk.py +48 -0
  57. kimi_cli/hooks/engine.py +394 -0
  58. kimi_cli/hooks/events.py +190 -0
  59. kimi_cli/hooks/runner.py +103 -0
  60. kimi_cli/llm.py +333 -0
  61. kimi_cli/mcp_oauth.py +72 -0
  62. kimi_cli/metadata.py +79 -0
  63. kimi_cli/notifications/__init__.py +33 -0
  64. kimi_cli/notifications/llm.py +77 -0
  65. kimi_cli/notifications/manager.py +145 -0
  66. kimi_cli/notifications/models.py +50 -0
  67. kimi_cli/notifications/notifier.py +41 -0
  68. kimi_cli/notifications/store.py +118 -0
  69. kimi_cli/notifications/wire.py +21 -0
  70. kimi_cli/plugin/__init__.py +124 -0
  71. kimi_cli/plugin/manager.py +153 -0
  72. kimi_cli/plugin/tool.py +173 -0
  73. kimi_cli/prompts/__init__.py +6 -0
  74. kimi_cli/prompts/compact.md +73 -0
  75. kimi_cli/prompts/init.md +21 -0
  76. kimi_cli/py.typed +0 -0
  77. kimi_cli/session.py +319 -0
  78. kimi_cli/session_fork.py +325 -0
  79. kimi_cli/session_state.py +132 -0
  80. kimi_cli/share.py +14 -0
  81. kimi_cli/skill/__init__.py +727 -0
  82. kimi_cli/skill/flow/__init__.py +99 -0
  83. kimi_cli/skill/flow/d2.py +482 -0
  84. kimi_cli/skill/flow/mermaid.py +266 -0
  85. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  86. kimi_cli/skills/skill-creator/SKILL.md +367 -0
  87. kimi_cli/soul/__init__.py +304 -0
  88. kimi_cli/soul/agent.py +519 -0
  89. kimi_cli/soul/approval.py +267 -0
  90. kimi_cli/soul/btw.py +237 -0
  91. kimi_cli/soul/compaction.py +189 -0
  92. kimi_cli/soul/context.py +339 -0
  93. kimi_cli/soul/denwarenji.py +39 -0
  94. kimi_cli/soul/dynamic_injection.py +84 -0
  95. kimi_cli/soul/dynamic_injections/__init__.py +0 -0
  96. kimi_cli/soul/dynamic_injections/afk_mode.py +74 -0
  97. kimi_cli/soul/dynamic_injections/plan_mode.py +245 -0
  98. kimi_cli/soul/kimisoul.py +1710 -0
  99. kimi_cli/soul/message.py +92 -0
  100. kimi_cli/soul/slash.py +341 -0
  101. kimi_cli/soul/toolset.py +891 -0
  102. kimi_cli/subagents/__init__.py +21 -0
  103. kimi_cli/subagents/builder.py +42 -0
  104. kimi_cli/subagents/core.py +86 -0
  105. kimi_cli/subagents/git_context.py +170 -0
  106. kimi_cli/subagents/models.py +54 -0
  107. kimi_cli/subagents/output.py +71 -0
  108. kimi_cli/subagents/registry.py +28 -0
  109. kimi_cli/subagents/runner.py +428 -0
  110. kimi_cli/subagents/store.py +196 -0
  111. kimi_cli/telemetry/__init__.py +197 -0
  112. kimi_cli/telemetry/crash.py +149 -0
  113. kimi_cli/telemetry/sink.py +143 -0
  114. kimi_cli/telemetry/transport.py +318 -0
  115. kimi_cli/tools/AGENTS.md +5 -0
  116. kimi_cli/tools/__init__.py +105 -0
  117. kimi_cli/tools/agent/__init__.py +277 -0
  118. kimi_cli/tools/agent/description.md +41 -0
  119. kimi_cli/tools/ask_user/__init__.py +154 -0
  120. kimi_cli/tools/ask_user/description.md +19 -0
  121. kimi_cli/tools/background/__init__.py +318 -0
  122. kimi_cli/tools/background/list.md +10 -0
  123. kimi_cli/tools/background/output.md +11 -0
  124. kimi_cli/tools/background/stop.md +8 -0
  125. kimi_cli/tools/display.py +46 -0
  126. kimi_cli/tools/dmail/__init__.py +38 -0
  127. kimi_cli/tools/dmail/dmail.md +17 -0
  128. kimi_cli/tools/file/__init__.py +30 -0
  129. kimi_cli/tools/file/glob.md +21 -0
  130. kimi_cli/tools/file/glob.py +178 -0
  131. kimi_cli/tools/file/grep.md +6 -0
  132. kimi_cli/tools/file/grep_local.py +590 -0
  133. kimi_cli/tools/file/plan_mode.py +45 -0
  134. kimi_cli/tools/file/read.md +16 -0
  135. kimi_cli/tools/file/read.py +300 -0
  136. kimi_cli/tools/file/read_media.md +24 -0
  137. kimi_cli/tools/file/read_media.py +217 -0
  138. kimi_cli/tools/file/replace.md +7 -0
  139. kimi_cli/tools/file/replace.py +195 -0
  140. kimi_cli/tools/file/utils.py +257 -0
  141. kimi_cli/tools/file/write.md +5 -0
  142. kimi_cli/tools/file/write.py +177 -0
  143. kimi_cli/tools/plan/__init__.py +339 -0
  144. kimi_cli/tools/plan/description.md +29 -0
  145. kimi_cli/tools/plan/enter.py +197 -0
  146. kimi_cli/tools/plan/enter_description.md +35 -0
  147. kimi_cli/tools/plan/heroes.py +277 -0
  148. kimi_cli/tools/shell/__init__.py +260 -0
  149. kimi_cli/tools/shell/bash.md +35 -0
  150. kimi_cli/tools/test.py +55 -0
  151. kimi_cli/tools/think/__init__.py +21 -0
  152. kimi_cli/tools/think/think.md +1 -0
  153. kimi_cli/tools/todo/__init__.py +168 -0
  154. kimi_cli/tools/todo/set_todo_list.md +23 -0
  155. kimi_cli/tools/utils.py +199 -0
  156. kimi_cli/tools/web/__init__.py +4 -0
  157. kimi_cli/tools/web/fetch.md +1 -0
  158. kimi_cli/tools/web/fetch.py +189 -0
  159. kimi_cli/tools/web/search.md +1 -0
  160. kimi_cli/tools/web/search.py +163 -0
  161. kimi_cli/ui/__init__.py +0 -0
  162. kimi_cli/ui/acp/__init__.py +99 -0
  163. kimi_cli/ui/print/__init__.py +474 -0
  164. kimi_cli/ui/print/visualize.py +194 -0
  165. kimi_cli/ui/shell/__init__.py +1540 -0
  166. kimi_cli/ui/shell/console.py +109 -0
  167. kimi_cli/ui/shell/debug.py +190 -0
  168. kimi_cli/ui/shell/echo.py +17 -0
  169. kimi_cli/ui/shell/export_import.py +117 -0
  170. kimi_cli/ui/shell/keyboard.py +300 -0
  171. kimi_cli/ui/shell/mcp_status.py +111 -0
  172. kimi_cli/ui/shell/oauth.py +149 -0
  173. kimi_cli/ui/shell/placeholders.py +531 -0
  174. kimi_cli/ui/shell/prompt.py +2259 -0
  175. kimi_cli/ui/shell/replay.py +215 -0
  176. kimi_cli/ui/shell/session_picker.py +227 -0
  177. kimi_cli/ui/shell/setup.py +212 -0
  178. kimi_cli/ui/shell/slash.py +893 -0
  179. kimi_cli/ui/shell/startup.py +32 -0
  180. kimi_cli/ui/shell/task_browser.py +486 -0
  181. kimi_cli/ui/shell/update.py +349 -0
  182. kimi_cli/ui/shell/usage.py +295 -0
  183. kimi_cli/ui/shell/visualize/__init__.py +165 -0
  184. kimi_cli/ui/shell/visualize/_approval_panel.py +505 -0
  185. kimi_cli/ui/shell/visualize/_blocks.py +640 -0
  186. kimi_cli/ui/shell/visualize/_btw_panel.py +224 -0
  187. kimi_cli/ui/shell/visualize/_input_router.py +48 -0
  188. kimi_cli/ui/shell/visualize/_interactive.py +524 -0
  189. kimi_cli/ui/shell/visualize/_live_view.py +902 -0
  190. kimi_cli/ui/shell/visualize/_question_panel.py +586 -0
  191. kimi_cli/ui/theme.py +241 -0
  192. kimi_cli/utils/__init__.py +0 -0
  193. kimi_cli/utils/aiohttp.py +24 -0
  194. kimi_cli/utils/aioqueue.py +72 -0
  195. kimi_cli/utils/broadcast.py +37 -0
  196. kimi_cli/utils/changelog.py +108 -0
  197. kimi_cli/utils/clipboard.py +246 -0
  198. kimi_cli/utils/datetime.py +64 -0
  199. kimi_cli/utils/diff.py +135 -0
  200. kimi_cli/utils/editor.py +91 -0
  201. kimi_cli/utils/environment.py +225 -0
  202. kimi_cli/utils/envvar.py +22 -0
  203. kimi_cli/utils/export.py +696 -0
  204. kimi_cli/utils/file_filter.py +367 -0
  205. kimi_cli/utils/frontmatter.py +70 -0
  206. kimi_cli/utils/io.py +27 -0
  207. kimi_cli/utils/logging.py +124 -0
  208. kimi_cli/utils/media_tags.py +29 -0
  209. kimi_cli/utils/message.py +24 -0
  210. kimi_cli/utils/path.py +245 -0
  211. kimi_cli/utils/proctitle.py +33 -0
  212. kimi_cli/utils/proxy.py +31 -0
  213. kimi_cli/utils/pyinstaller.py +43 -0
  214. kimi_cli/utils/rich/__init__.py +33 -0
  215. kimi_cli/utils/rich/columns.py +99 -0
  216. kimi_cli/utils/rich/diff_render.py +481 -0
  217. kimi_cli/utils/rich/markdown.py +898 -0
  218. kimi_cli/utils/rich/markdown_sample.md +108 -0
  219. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  220. kimi_cli/utils/rich/syntax.py +114 -0
  221. kimi_cli/utils/sensitive.py +54 -0
  222. kimi_cli/utils/server.py +121 -0
  223. kimi_cli/utils/shell_quoting.py +38 -0
  224. kimi_cli/utils/signals.py +43 -0
  225. kimi_cli/utils/slashcmd.py +145 -0
  226. kimi_cli/utils/string.py +41 -0
  227. kimi_cli/utils/subprocess_env.py +73 -0
  228. kimi_cli/utils/term.py +168 -0
  229. kimi_cli/utils/typing.py +20 -0
  230. kimi_cli/utils/windows_paths.py +48 -0
  231. kimi_cli/vis/__init__.py +0 -0
  232. kimi_cli/vis/api/__init__.py +5 -0
  233. kimi_cli/vis/api/sessions.py +687 -0
  234. kimi_cli/vis/api/statistics.py +209 -0
  235. kimi_cli/vis/api/system.py +19 -0
  236. kimi_cli/vis/app.py +175 -0
  237. kimi_cli/web/__init__.py +5 -0
  238. kimi_cli/web/api/__init__.py +15 -0
  239. kimi_cli/web/api/config.py +208 -0
  240. kimi_cli/web/api/open_in.py +197 -0
  241. kimi_cli/web/api/sessions.py +1223 -0
  242. kimi_cli/web/app.py +451 -0
  243. kimi_cli/web/auth.py +191 -0
  244. kimi_cli/web/models.py +98 -0
  245. kimi_cli/web/runner/__init__.py +5 -0
  246. kimi_cli/web/runner/messages.py +57 -0
  247. kimi_cli/web/runner/process.py +754 -0
  248. kimi_cli/web/runner/worker.py +95 -0
  249. kimi_cli/web/store/__init__.py +1 -0
  250. kimi_cli/web/store/sessions.py +432 -0
  251. kimi_cli/wire/__init__.py +148 -0
  252. kimi_cli/wire/file.py +151 -0
  253. kimi_cli/wire/jsonrpc.py +263 -0
  254. kimi_cli/wire/protocol.py +2 -0
  255. kimi_cli/wire/root_hub.py +27 -0
  256. kimi_cli/wire/serde.py +26 -0
  257. kimi_cli/wire/server.py +1060 -0
  258. kimi_cli/wire/types.py +717 -0
kimi_cli/wire/types.py ADDED
@@ -0,0 +1,717 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, Literal, TypeGuard, cast
5
+
6
+ from kosong.chat_provider import TokenUsage
7
+ from kosong.message import (
8
+ AudioURLPart,
9
+ ContentPart,
10
+ ImageURLPart,
11
+ TextPart,
12
+ ThinkPart,
13
+ ToolCall,
14
+ ToolCallPart,
15
+ VideoURLPart,
16
+ )
17
+ from kosong.tooling import (
18
+ BriefDisplayBlock,
19
+ DisplayBlock,
20
+ ToolResult,
21
+ ToolReturnValue,
22
+ UnknownDisplayBlock,
23
+ )
24
+ from kosong.utils.typing import JsonType
25
+ from pydantic import BaseModel, Field, field_serializer, field_validator, model_validator
26
+
27
+ from kimi_cli.tools.display import (
28
+ BackgroundTaskDisplayBlock,
29
+ DiffDisplayBlock,
30
+ ShellDisplayBlock,
31
+ TodoDisplayBlock,
32
+ TodoDisplayItem,
33
+ )
34
+ from kimi_cli.utils.typing import flatten_union
35
+
36
+
37
+ class TurnBegin(BaseModel):
38
+ """
39
+ Indicates the beginning of a new agent turn.
40
+ This event must be sent before any other event in the turn.
41
+ """
42
+
43
+ user_input: str | list[ContentPart]
44
+
45
+
46
+ class SteerInput(BaseModel):
47
+ """
48
+ Indicates that the user appended follow-up input to the current running turn.
49
+ This event is emitted after the current step finishes and the input is appended
50
+ to context, before the next step begins.
51
+ """
52
+
53
+ user_input: str | list[ContentPart]
54
+
55
+
56
+ class TurnEnd(BaseModel):
57
+ """
58
+ Indicates the end of the current agent turn.
59
+ This event must be sent after all other events in the turn.
60
+ If the turn is interrupted, this event may be omitted.
61
+ """
62
+
63
+ pass
64
+
65
+
66
+ class StepBegin(BaseModel):
67
+ """
68
+ Indicates the beginning of a new agent step.
69
+ This event must be sent before any other event in the step.
70
+ """
71
+
72
+ n: int
73
+ """The step number."""
74
+
75
+
76
+ class StepInterrupted(BaseModel):
77
+ """Indicates the current step was interrupted, either by user intervention or an error."""
78
+
79
+ pass
80
+
81
+
82
+ class StepRetry(BaseModel):
83
+ """Indicates that the current step attempt failed and will be retried."""
84
+
85
+ n: int
86
+ """The step number."""
87
+ next_attempt: int
88
+ """The next attempt number, 1-based."""
89
+ max_attempts: int
90
+ """The maximum number of attempts for this step."""
91
+ wait_s: float
92
+ """Seconds to wait before retrying."""
93
+ error_type: str
94
+ """The exception class name that triggered the retry."""
95
+ status_code: int | None = None
96
+ """HTTP status code when available."""
97
+
98
+
99
+ class CompactionBegin(BaseModel):
100
+ """
101
+ Indicates that a compaction just began.
102
+ This event must be sent during a step, which means, between `StepBegin` and the next
103
+ `StepBegin` or `StepInterrupted`. And, there must be a `CompactionEnd` directly following
104
+ this event.
105
+ """
106
+
107
+ pass
108
+
109
+
110
+ class CompactionEnd(BaseModel):
111
+ """
112
+ Indicates that a compaction just ended.
113
+ This event must be sent directly after a `CompactionBegin` event.
114
+ """
115
+
116
+ pass
117
+
118
+
119
+ class HookTriggered(BaseModel):
120
+ """A batch of hooks has been triggered and is now executing."""
121
+
122
+ event: str
123
+ """The hook event type, e.g. 'PreToolUse', 'Stop'."""
124
+ target: str = ""
125
+ """What the hooks are targeting: tool name for tool hooks,
126
+ agent name for subagent hooks, etc."""
127
+ hook_count: int = 1
128
+ """Number of matched hooks running in parallel."""
129
+
130
+
131
+ class HookResolved(BaseModel):
132
+ """A batch of hooks has finished executing."""
133
+
134
+ event: str
135
+ """The hook event type, e.g. 'PreToolUse', 'Stop'."""
136
+ target: str = ""
137
+ """Same as HookTriggered.target."""
138
+ action: Literal["allow", "block"] = "allow"
139
+ """Aggregate decision: 'block' if any hook blocked, 'allow' otherwise."""
140
+ reason: str = ""
141
+ """Reason for blocking. Empty if allowed."""
142
+ duration_ms: int = 0
143
+ """Wall-clock time for the entire batch, in milliseconds."""
144
+
145
+
146
+ class MCPLoadingBegin(BaseModel):
147
+ """Indicates that MCP tool loading is in progress."""
148
+
149
+ pass
150
+
151
+
152
+ class MCPLoadingEnd(BaseModel):
153
+ """Indicates that MCP tool loading has finished."""
154
+
155
+ pass
156
+
157
+
158
+ class MCPServerSnapshot(BaseModel):
159
+ """A snapshot of one MCP server during startup."""
160
+
161
+ name: str
162
+ status: Literal["pending", "connecting", "connected", "failed", "unauthorized"]
163
+ tools: tuple[str, ...] = ()
164
+
165
+
166
+ class MCPStatusSnapshot(BaseModel):
167
+ """A snapshot of MCP startup progress."""
168
+
169
+ loading: bool
170
+ connected: int
171
+ total: int
172
+ tools: int
173
+ servers: tuple[MCPServerSnapshot, ...] = ()
174
+
175
+
176
+ class StatusUpdate(BaseModel):
177
+ """
178
+ An update on the current status of the soul.
179
+ None fields indicate no change from the previous status.
180
+ """
181
+
182
+ context_usage: float | None = None
183
+ """The usage of the context, in percentage."""
184
+ context_tokens: int | None = None
185
+ """The number of tokens currently in the context."""
186
+ max_context_tokens: int | None = None
187
+ """The maximum number of tokens the context can hold."""
188
+ token_usage: TokenUsage | None = None
189
+ """The token usage statistics of the current step."""
190
+ message_id: str | None = None
191
+ """The message ID of the current step."""
192
+ plan_mode: bool | None = None
193
+ """Whether plan mode (read-only) is active. None means no change."""
194
+ mcp_status: MCPStatusSnapshot | None = None
195
+ """The current MCP startup snapshot. None means no change."""
196
+
197
+
198
+ class Notification(BaseModel):
199
+ """A generic system notification for UI and client consumption."""
200
+
201
+ id: str
202
+ category: str
203
+ type: str
204
+ source_kind: str
205
+ source_id: str
206
+ title: str
207
+ body: str
208
+ severity: str
209
+ created_at: float
210
+ payload: dict[str, JsonType] = Field(default_factory=dict)
211
+
212
+
213
+ class PlanDisplay(BaseModel):
214
+ """Displays a plan's content inline in the chat with special formatting."""
215
+
216
+ content: str
217
+ """The full markdown content of the plan."""
218
+ file_path: str
219
+ """The path to the plan file for reference."""
220
+
221
+
222
+ class BtwBegin(BaseModel):
223
+ """Indicates that a side question (/btw) has started processing."""
224
+
225
+ id: str
226
+ """Unique ID to pair with the corresponding BtwEnd."""
227
+ question: str
228
+ """The user's original side question text."""
229
+
230
+
231
+ class BtwEnd(BaseModel):
232
+ """Indicates that a side question (/btw) has finished."""
233
+
234
+ id: str
235
+ """Unique ID matching the BtwBegin."""
236
+ response: str | None = None
237
+ """The LLM's response text, or None if it failed."""
238
+ error: str | None = None
239
+ """Error message if the side question failed."""
240
+
241
+
242
+ class SubagentEvent(BaseModel):
243
+ """
244
+ An event from a subagent.
245
+ """
246
+
247
+ parent_tool_call_id: str | None = None
248
+ """The ID of the parent Agent tool call associated with this subagent."""
249
+ agent_id: str | None = None
250
+ """The subagent instance ID."""
251
+ subagent_type: str | None = None
252
+ """The built-in subagent type used by this instance."""
253
+ event: Event
254
+ """The event from the subagent."""
255
+ # TODO: maybe restrict the event types? to exclude approval request, etc.
256
+
257
+ @model_validator(mode="before")
258
+ @classmethod
259
+ def _compat_legacy_fields(cls, value: Any) -> Any:
260
+ if not isinstance(value, dict):
261
+ return value
262
+ data = dict(cast(dict[str, Any], value))
263
+ if "parent_tool_call_id" not in data and "task_tool_call_id" in data:
264
+ data["parent_tool_call_id"] = data["task_tool_call_id"]
265
+ return data
266
+
267
+ @field_serializer("event", when_used="json")
268
+ def _serialize_event(self, event: Event) -> dict[str, Any]:
269
+ envelope = WireMessageEnvelope.from_wire_message(event)
270
+ return envelope.model_dump(mode="json")
271
+
272
+ @field_validator("event", mode="before")
273
+ @classmethod
274
+ def _validate_event(cls, value: Any) -> Event:
275
+ if is_wire_message(value):
276
+ if is_event(value):
277
+ return value
278
+ raise ValueError("SubagentEvent event must be an Event")
279
+
280
+ if not isinstance(value, dict):
281
+ raise ValueError("SubagentEvent event must be a dict")
282
+ event_type = cast(dict[str, Any], value).get("type")
283
+ event_payload = cast(dict[str, Any], value).get("payload")
284
+ envelope = WireMessageEnvelope.model_validate(
285
+ {"type": event_type, "payload": event_payload}
286
+ )
287
+ event = envelope.to_wire_message()
288
+ if not is_event(event):
289
+ raise ValueError("SubagentEvent event must be an Event")
290
+ return event
291
+
292
+
293
+ class ApprovalResponse(BaseModel):
294
+ """
295
+ Indicates that an approval request has been resolved.
296
+ """
297
+
298
+ type Kind = Literal["approve", "approve_for_session", "reject"]
299
+
300
+ request_id: str
301
+ """The ID of the resolved approval request."""
302
+ response: Kind
303
+ """The response to the approval request."""
304
+ feedback: str = ""
305
+ """Optional user feedback when rejecting (e.g. instructions for the model)."""
306
+
307
+
308
+ class ApprovalRequest(BaseModel):
309
+ """
310
+ A request for user approval before proceeding with an action.
311
+ """
312
+
313
+ id: str
314
+ tool_call_id: str
315
+ sender: str
316
+ action: str
317
+ description: str
318
+ source_kind: Literal["foreground_turn", "background_agent"] | None = None
319
+ source_id: str | None = None
320
+ agent_id: str | None = None
321
+ subagent_type: str | None = None
322
+ source_description: str | None = None
323
+ display: list[DisplayBlock] = Field(default_factory=list[DisplayBlock])
324
+ """Defaults to an empty list for backwards-compatible wire.jsonl loading."""
325
+
326
+ # Note that the above fields are just a copy of `kimi_cli.soul.approval.Request`, but
327
+ # we cannot directly use that class here because we want to avoid dependency from Wire
328
+ # to Soul.
329
+
330
+ def __init__(self, **kwargs: Any) -> None:
331
+ super().__init__(**kwargs)
332
+ self._future: asyncio.Future[ApprovalResponse.Kind] | None = None
333
+ self._feedback: str = ""
334
+
335
+ def _get_future(self) -> asyncio.Future[ApprovalResponse.Kind]:
336
+ if self._future is None:
337
+ self._future = asyncio.get_event_loop().create_future()
338
+ return self._future
339
+
340
+ async def wait(self) -> ApprovalResponse.Kind:
341
+ """
342
+ Wait for the request to be resolved or cancelled.
343
+
344
+ Returns:
345
+ ApprovalResponse.Kind: The response to the approval request.
346
+ """
347
+ return await self._get_future()
348
+
349
+ def resolve(self, response: ApprovalResponse.Kind, feedback: str = "") -> None:
350
+ """
351
+ Resolve the approval request with the given response.
352
+ This will cause the `wait()` method to return the response.
353
+ """
354
+ self._feedback = feedback
355
+ future = self._get_future()
356
+ if not future.done():
357
+ future.set_result(response)
358
+
359
+ @property
360
+ def feedback(self) -> str:
361
+ """User feedback text provided with a rejection, if any."""
362
+ return self._feedback
363
+
364
+ @property
365
+ def resolved(self) -> bool:
366
+ """Whether the request is resolved."""
367
+ return self._future is not None and self._future.done()
368
+
369
+
370
+ class QuestionOption(BaseModel):
371
+ """A single option for a question."""
372
+
373
+ label: str
374
+ """The display text for this option."""
375
+ description: str = ""
376
+ """Explanation of what this option means."""
377
+
378
+
379
+ class QuestionItem(BaseModel):
380
+ """A single question to ask the user."""
381
+
382
+ question: str
383
+ """The complete question text."""
384
+ header: str = ""
385
+ """Short label displayed as a tag (max 12 chars)."""
386
+ options: list[QuestionOption]
387
+ """The available choices for this question (2-4 options)."""
388
+ multi_select: bool = False
389
+ """Whether multiple options can be selected."""
390
+ body: str = ""
391
+ """Optional body content (markdown) displayed above options."""
392
+ other_label: str = ""
393
+ """Custom label for the synthetic 'Other' free-text option. Empty uses default."""
394
+ other_description: str = ""
395
+ """Custom description for the synthetic 'Other' option. Empty uses default."""
396
+
397
+
398
+ class QuestionResponse(BaseModel):
399
+ """Response to a question request."""
400
+
401
+ request_id: str
402
+ """The ID of the resolved question request."""
403
+ answers: dict[str, str]
404
+ """Mapping from question text to selected option label(s). Multi-select answers are
405
+ comma-separated."""
406
+
407
+
408
+ class QuestionNotSupported(Exception):
409
+ """Raised when the connected client does not support interactive questions."""
410
+
411
+
412
+ class QuestionRequest(BaseModel):
413
+ """
414
+ A request to ask the user structured questions during execution.
415
+ """
416
+
417
+ id: str
418
+ """The unique request ID."""
419
+ tool_call_id: str
420
+ """The ID of the tool call that initiated this question."""
421
+ questions: list[QuestionItem]
422
+ """The questions to ask the user (1-4 questions)."""
423
+
424
+ def __init__(self, **kwargs: Any) -> None:
425
+ super().__init__(**kwargs)
426
+ self._future: asyncio.Future[dict[str, str]] | None = None
427
+
428
+ def _get_future(self) -> asyncio.Future[dict[str, str]]:
429
+ if self._future is None:
430
+ self._future = asyncio.get_event_loop().create_future()
431
+ return self._future
432
+
433
+ async def wait(self) -> dict[str, str]:
434
+ """
435
+ Wait for the question to be answered.
436
+
437
+ Returns:
438
+ dict[str, str]: Mapping from question text to answer.
439
+ """
440
+ return await self._get_future()
441
+
442
+ def resolve(self, answers: dict[str, str]) -> None:
443
+ """
444
+ Resolve the question request with the given answers.
445
+ This will cause the `wait()` method to return the answers.
446
+ """
447
+ future = self._get_future()
448
+ if not future.done():
449
+ future.set_result(answers)
450
+
451
+ def set_exception(self, exc: BaseException) -> None:
452
+ """Resolve the question request with an exception."""
453
+ future = self._get_future()
454
+ if not future.done():
455
+ future.set_exception(exc)
456
+
457
+ @property
458
+ def resolved(self) -> bool:
459
+ """Whether the question request is resolved."""
460
+ return self._future is not None and self._future.done()
461
+
462
+
463
+ class ToolCallRequest(BaseModel):
464
+ """
465
+ A tool call request routed to the Wire client for execution.
466
+ """
467
+
468
+ id: str
469
+ """The ID of the tool call."""
470
+ name: str
471
+ """The name of the tool to call."""
472
+ arguments: str | None
473
+ """Arguments of the tool call in JSON string format."""
474
+
475
+ def __init__(self, **kwargs: Any) -> None:
476
+ super().__init__(**kwargs)
477
+ self._future: asyncio.Future[ToolReturnValue] | None = None
478
+
479
+ def _get_future(self) -> asyncio.Future[ToolReturnValue]:
480
+ if self._future is None:
481
+ self._future = asyncio.get_event_loop().create_future()
482
+ return self._future
483
+
484
+ @staticmethod
485
+ def from_tool_call(tool_call: ToolCall) -> ToolCallRequest:
486
+ return ToolCallRequest(
487
+ id=tool_call.id,
488
+ name=tool_call.function.name,
489
+ arguments=tool_call.function.arguments,
490
+ )
491
+
492
+ async def wait(self) -> ToolReturnValue:
493
+ """
494
+ Wait for the tool call to be resolved or cancelled.
495
+
496
+ Returns:
497
+ ToolReturnValue: The tool execution result.
498
+ """
499
+ return await self._get_future()
500
+
501
+ def resolve(self, result: ToolReturnValue) -> None:
502
+ """
503
+ Resolve the tool call with the given result.
504
+ This will cause the `wait()` method to return the result.
505
+ """
506
+ future = self._get_future()
507
+ if not future.done():
508
+ future.set_result(result)
509
+
510
+ @property
511
+ def resolved(self) -> bool:
512
+ """Whether the tool call is resolved."""
513
+ return self._future is not None and self._future.done()
514
+
515
+
516
+ type Event = (
517
+ TurnBegin
518
+ | SteerInput
519
+ | TurnEnd
520
+ | StepBegin
521
+ | StepInterrupted
522
+ | StepRetry
523
+ | HookTriggered
524
+ | HookResolved
525
+ | CompactionBegin
526
+ | CompactionEnd
527
+ | MCPLoadingBegin
528
+ | MCPLoadingEnd
529
+ | StatusUpdate
530
+ | Notification
531
+ | ContentPart
532
+ | ToolCall
533
+ | ToolCallPart
534
+ | ToolResult
535
+ | ApprovalResponse
536
+ | SubagentEvent
537
+ | PlanDisplay
538
+ | BtwBegin
539
+ | BtwEnd
540
+ )
541
+ """Any event, including control flow and content/tooling events."""
542
+
543
+
544
+ class HookResponse(BaseModel):
545
+ """
546
+ Client response to a HookRequest.
547
+ """
548
+
549
+ request_id: str
550
+ """The ID of the HookRequest being responded to."""
551
+ action: Literal["allow", "block"] = "allow"
552
+ """The decision: allow the action or block it."""
553
+ reason: str = ""
554
+ """Reason for blocking. Empty if allowed."""
555
+
556
+
557
+ class HookRequest(BaseModel):
558
+ """
559
+ A request for the wire client to handle a hook event.
560
+ The client runs its own logic and responds with allow/block.
561
+ """
562
+
563
+ type Action = Literal["allow", "block"]
564
+
565
+ id: str
566
+ """Unique request ID."""
567
+ subscription_id: str = ""
568
+ """Which subscription triggered this request."""
569
+ event: str
570
+ """The hook event type, e.g. 'PreToolUse', 'Stop'."""
571
+ target: str = ""
572
+ """What triggered the hook: tool name, agent name, etc."""
573
+ input_data: dict[str, Any] = Field(default_factory=dict)
574
+ """Full event payload (same as what shell hooks get on stdin)."""
575
+
576
+ def __init__(self, **kwargs: Any) -> None:
577
+ super().__init__(**kwargs)
578
+ self._future: asyncio.Future[tuple[HookRequest.Action, str]] | None = None
579
+
580
+ def _get_future(self) -> asyncio.Future[tuple[HookRequest.Action, str]]:
581
+ if self._future is None:
582
+ self._future = asyncio.get_event_loop().create_future()
583
+ return self._future
584
+
585
+ async def wait(self) -> tuple[Action, str]:
586
+ """Wait for client response. Returns (action, reason)."""
587
+ return await self._get_future()
588
+
589
+ def resolve(self, action: Action, reason: str = "") -> None:
590
+ """Resolve with client's decision."""
591
+ future = self._get_future()
592
+ if not future.done():
593
+ future.set_result((action, reason))
594
+
595
+ @property
596
+ def resolved(self) -> bool:
597
+ return self._future is not None and self._future.done()
598
+
599
+
600
+ type Request = ApprovalRequest | ToolCallRequest | QuestionRequest | HookRequest
601
+ """Any request. Request is a message that expects a response."""
602
+
603
+ type WireMessage = Event | Request
604
+ """Any message sent over the `Wire`."""
605
+
606
+
607
+ _EVENT_TYPES = cast(tuple[type[Event], ...], flatten_union(Event))
608
+ _REQUEST_TYPES = cast(tuple[type[Request], ...], flatten_union(Request))
609
+ _WIRE_MESSAGE_TYPES = cast(tuple[type[WireMessage], ...], flatten_union(WireMessage))
610
+
611
+
612
+ def is_event(msg: Any) -> TypeGuard[Event]:
613
+ """Check if the message is an Event."""
614
+ return isinstance(msg, _EVENT_TYPES)
615
+
616
+
617
+ def is_request(msg: Any) -> TypeGuard[Request]:
618
+ """Check if the message is a Request."""
619
+ return isinstance(msg, _REQUEST_TYPES)
620
+
621
+
622
+ def is_wire_message(msg: Any) -> TypeGuard[WireMessage]:
623
+ """Check if the message is a WireMessage."""
624
+ return isinstance(msg, _WIRE_MESSAGE_TYPES)
625
+
626
+
627
+ _NAME_TO_WIRE_MESSAGE_TYPE: dict[str, type[WireMessage]] = {
628
+ cls.__name__: cls for cls in _WIRE_MESSAGE_TYPES
629
+ }
630
+ # for backwards compatibility with Wire v1
631
+ _NAME_TO_WIRE_MESSAGE_TYPE["ApprovalRequestResolved"] = ApprovalResponse
632
+
633
+
634
+ class WireMessageEnvelope(BaseModel):
635
+ type: str
636
+ payload: dict[str, JsonType]
637
+
638
+ @classmethod
639
+ def from_wire_message(cls, msg: WireMessage) -> WireMessageEnvelope:
640
+ typename: str | None = None
641
+ for name, typ in _NAME_TO_WIRE_MESSAGE_TYPE.items():
642
+ if issubclass(type(msg), typ):
643
+ typename = name
644
+ break
645
+ assert typename is not None, f"Unknown wire message type: {type(msg)}"
646
+ return cls(
647
+ type=typename,
648
+ payload=msg.model_dump(mode="json"),
649
+ )
650
+
651
+ def to_wire_message(self) -> WireMessage:
652
+ """
653
+ Convert the envelope back into a `WireMessage`.
654
+
655
+ Raises:
656
+ ValueError: If the message type is unknown or the payload is invalid.
657
+ """
658
+ msg_type = _NAME_TO_WIRE_MESSAGE_TYPE.get(self.type)
659
+ if msg_type is None:
660
+ raise ValueError(f"Unknown wire message type: {self.type}")
661
+ return msg_type.model_validate(self.payload)
662
+
663
+
664
+ __all__ = [
665
+ # `WireMessage` variants
666
+ "TurnBegin",
667
+ "SteerInput",
668
+ "TurnEnd",
669
+ "StepBegin",
670
+ "StepInterrupted",
671
+ "StepRetry",
672
+ "CompactionBegin",
673
+ "CompactionEnd",
674
+ "MCPLoadingBegin",
675
+ "MCPLoadingEnd",
676
+ "StatusUpdate",
677
+ "MCPServerSnapshot",
678
+ "MCPStatusSnapshot",
679
+ "Notification",
680
+ "ContentPart",
681
+ "ToolCall",
682
+ "ToolCallPart",
683
+ "ToolResult",
684
+ "ApprovalResponse",
685
+ "SubagentEvent",
686
+ "PlanDisplay",
687
+ "BtwBegin",
688
+ "BtwEnd",
689
+ "ApprovalRequest",
690
+ "ToolCallRequest",
691
+ "QuestionOption",
692
+ "QuestionItem",
693
+ "QuestionResponse",
694
+ "QuestionRequest",
695
+ "QuestionNotSupported",
696
+ # helpers
697
+ "WireMessageEnvelope",
698
+ # `StatusUpdate`-related
699
+ "TokenUsage",
700
+ # `ContentPart` types
701
+ "TextPart",
702
+ "ThinkPart",
703
+ "ImageURLPart",
704
+ "AudioURLPart",
705
+ "VideoURLPart",
706
+ # `ToolResult`-related
707
+ "ToolReturnValue",
708
+ # `DisplayBlock` types
709
+ "DisplayBlock",
710
+ "UnknownDisplayBlock",
711
+ "BriefDisplayBlock",
712
+ "DiffDisplayBlock",
713
+ "TodoDisplayBlock",
714
+ "TodoDisplayItem",
715
+ "ShellDisplayBlock",
716
+ "BackgroundTaskDisplayBlock",
717
+ ]