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/acp/server.py ADDED
@@ -0,0 +1,468 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import sys
5
+ import time
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any, NamedTuple
9
+
10
+ import acp
11
+ from kaos.path import KaosPath
12
+
13
+ from kimi_cli.acp.kaos import ACPKaos
14
+ from kimi_cli.acp.mcp import acp_mcp_servers_to_mcp_config
15
+ from kimi_cli.acp.session import ACPSession
16
+ from kimi_cli.acp.tools import replace_tools
17
+ from kimi_cli.acp.types import ACPContentBlock, MCPServer
18
+ from kimi_cli.acp.version import ACPVersionSpec, negotiate_version
19
+ from kimi_cli.app import KimiCLI
20
+ from kimi_cli.auth.oauth import KIMI_CODE_OAUTH_KEY, load_tokens
21
+ from kimi_cli.config import LLMModel, OAuthRef, load_config, save_config
22
+ from kimi_cli.constant import NAME, VERSION
23
+ from kimi_cli.llm import create_llm, derive_model_capabilities
24
+ from kimi_cli.session import Session
25
+ from kimi_cli.soul.slash import registry as soul_slash_registry
26
+ from kimi_cli.soul.toolset import KimiToolset
27
+ from kimi_cli.utils.logging import logger
28
+
29
+
30
+ class ACPServer:
31
+ def __init__(self) -> None:
32
+ self.client_capabilities: acp.schema.ClientCapabilities | None = None
33
+ self.conn: acp.Client | None = None
34
+ self.sessions: dict[str, tuple[ACPSession, _ModelIDConv]] = {}
35
+ self.negotiated_version: ACPVersionSpec | None = None
36
+ self._auth_methods: list[acp.schema.AuthMethod] = []
37
+
38
+ def on_connect(self, conn: acp.Client) -> None:
39
+ logger.info("ACP client connected")
40
+ self.conn = conn
41
+
42
+ async def initialize(
43
+ self,
44
+ protocol_version: int,
45
+ client_capabilities: acp.schema.ClientCapabilities | None = None,
46
+ client_info: acp.schema.Implementation | None = None,
47
+ **kwargs: Any,
48
+ ) -> acp.InitializeResponse:
49
+ self.negotiated_version = negotiate_version(protocol_version)
50
+ logger.info(
51
+ "ACP server initialized with client protocol version: {version}, "
52
+ "negotiated version: {negotiated}, "
53
+ "client capabilities: {capabilities}, client info: {info}",
54
+ version=protocol_version,
55
+ negotiated=self.negotiated_version,
56
+ capabilities=client_capabilities,
57
+ info=client_info,
58
+ )
59
+ self.client_capabilities = client_capabilities
60
+
61
+ if client_info is not None:
62
+ from kimi_cli.telemetry import set_client_info
63
+
64
+ set_client_info(
65
+ name=client_info.name,
66
+ version=getattr(client_info, "version", None),
67
+ )
68
+
69
+ # get command and args of current process for terminal-auth
70
+ command = sys.argv[0]
71
+ args: list[str] = []
72
+
73
+ # Build terminal auth data for error response
74
+ terminal_args = args + ["login"]
75
+
76
+ # Build and cache auth methods for reuse in AUTH_REQUIRED errors
77
+ self._auth_methods = [
78
+ acp.schema.AuthMethod(
79
+ id="login",
80
+ name="Login with Kimi account",
81
+ description=(
82
+ "Run `kimi login` command in the terminal, "
83
+ "then follow the instructions to finish login."
84
+ ),
85
+ # Store auth data in field_meta for building AUTH_REQUIRED error
86
+ field_meta={
87
+ "terminal-auth": {
88
+ "command": command,
89
+ "args": terminal_args,
90
+ "label": "Kimi Code Login",
91
+ "env": {},
92
+ "type": "terminal",
93
+ }
94
+ },
95
+ ),
96
+ ]
97
+
98
+ return acp.InitializeResponse(
99
+ protocol_version=self.negotiated_version.protocol_version,
100
+ agent_capabilities=acp.schema.AgentCapabilities(
101
+ load_session=True,
102
+ prompt_capabilities=acp.schema.PromptCapabilities(
103
+ embedded_context=True, image=True, audio=False
104
+ ),
105
+ mcp_capabilities=acp.schema.McpCapabilities(http=True, sse=False),
106
+ session_capabilities=acp.schema.SessionCapabilities(
107
+ list=acp.schema.SessionListCapabilities(),
108
+ resume=acp.schema.SessionResumeCapabilities(),
109
+ ),
110
+ ),
111
+ auth_methods=self._auth_methods,
112
+ agent_info=acp.schema.Implementation(name=NAME, version=VERSION),
113
+ )
114
+
115
+ @staticmethod
116
+ def _check_token_usable() -> str | None:
117
+ """Return ``None`` if the persisted OAuth token is usable, else a reason string."""
118
+ ref = OAuthRef(storage="file", key=KIMI_CODE_OAUTH_KEY)
119
+ token = load_tokens(ref)
120
+
121
+ if token is None or not token.access_token:
122
+ return "no valid token found"
123
+ if token.expires_at and token.expires_at < time.time() and not token.refresh_token:
124
+ # Token expired and no refresh token — background refresh cannot help.
125
+ return "token expired and no refresh token available"
126
+ return None
127
+
128
+ def _check_auth(self) -> None:
129
+ """Check if Kimi Code authentication is complete. Raise AUTH_REQUIRED if not."""
130
+ reason = self._check_token_usable()
131
+ if reason:
132
+ auth_methods_data: list[dict[str, Any]] = []
133
+ for m in self._auth_methods:
134
+ if m.field_meta and "terminal-auth" in m.field_meta:
135
+ terminal_auth = m.field_meta["terminal-auth"]
136
+ auth_methods_data.append(
137
+ {
138
+ "id": m.id,
139
+ "name": m.name,
140
+ "description": m.description,
141
+ "type": terminal_auth.get("type", "terminal"),
142
+ "args": terminal_auth.get("args", []),
143
+ "env": terminal_auth.get("env", {}),
144
+ }
145
+ )
146
+
147
+ logger.warning("Authentication required, {reason}", reason=reason)
148
+ raise acp.RequestError.auth_required({"authMethods": auth_methods_data})
149
+
150
+ async def new_session(
151
+ self, cwd: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
152
+ ) -> acp.NewSessionResponse:
153
+ logger.info("Creating new session for working directory: {cwd}", cwd=cwd)
154
+ assert self.conn is not None, "ACP client not connected"
155
+ assert self.client_capabilities is not None, "ACP connection not initialized"
156
+
157
+ # Check authentication before creating session
158
+ self._check_auth()
159
+
160
+ session = await Session.create(KaosPath.unsafe_from_local_path(Path(cwd)))
161
+
162
+ mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers or [])
163
+ cli_instance = await KimiCLI.create(
164
+ session,
165
+ mcp_configs=[mcp_config],
166
+ ui_mode="acp",
167
+ )
168
+ config = cli_instance.soul.runtime.config
169
+ acp_kaos = ACPKaos(self.conn, session.id, self.client_capabilities)
170
+ acp_session = ACPSession(session.id, cli_instance, self.conn, kaos=acp_kaos)
171
+ model_id_conv = _ModelIDConv(config.default_model, config.default_thinking)
172
+ self.sessions[session.id] = (acp_session, model_id_conv)
173
+
174
+ if isinstance(cli_instance.soul.agent.toolset, KimiToolset):
175
+ replace_tools(
176
+ self.client_capabilities,
177
+ self.conn,
178
+ session.id,
179
+ cli_instance.soul.agent.toolset,
180
+ cli_instance.soul.runtime,
181
+ )
182
+
183
+ available_commands = [
184
+ acp.schema.AvailableCommand(name=cmd.name, description=cmd.description)
185
+ for cmd in soul_slash_registry.list_commands()
186
+ ]
187
+ asyncio.create_task(
188
+ self.conn.session_update(
189
+ session_id=session.id,
190
+ update=acp.schema.AvailableCommandsUpdate(
191
+ session_update="available_commands_update",
192
+ available_commands=available_commands,
193
+ ),
194
+ )
195
+ )
196
+ return acp.NewSessionResponse(
197
+ session_id=session.id,
198
+ modes=acp.schema.SessionModeState(
199
+ available_modes=[
200
+ acp.schema.SessionMode(
201
+ id="default",
202
+ name="Default",
203
+ description="The default mode.",
204
+ ),
205
+ ],
206
+ current_mode_id="default",
207
+ ),
208
+ models=acp.schema.SessionModelState(
209
+ available_models=_expand_llm_models(config.models),
210
+ current_model_id=model_id_conv.to_acp_model_id(),
211
+ ),
212
+ )
213
+
214
+ async def _setup_session(
215
+ self,
216
+ cwd: str,
217
+ session_id: str,
218
+ mcp_servers: list[MCPServer] | None = None,
219
+ ) -> tuple[ACPSession, _ModelIDConv]:
220
+ """Load or resume a session. Shared by load_session and resume_session."""
221
+ assert self.conn is not None, "ACP client not connected"
222
+ assert self.client_capabilities is not None, "ACP connection not initialized"
223
+
224
+ work_dir = KaosPath.unsafe_from_local_path(Path(cwd))
225
+ session = await Session.find(work_dir, session_id)
226
+ if session is None:
227
+ logger.error(
228
+ "Session not found: {id} for working directory: {cwd}", id=session_id, cwd=cwd
229
+ )
230
+ raise acp.RequestError.invalid_params({"session_id": "Session not found"})
231
+
232
+ mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers or [])
233
+ cli_instance = await KimiCLI.create(
234
+ session,
235
+ mcp_configs=[mcp_config],
236
+ resumed=True, # _setup_session loads existing sessions
237
+ ui_mode="acp",
238
+ )
239
+ config = cli_instance.soul.runtime.config
240
+ acp_kaos = ACPKaos(self.conn, session.id, self.client_capabilities)
241
+ acp_session = ACPSession(session.id, cli_instance, self.conn, kaos=acp_kaos)
242
+ model_id_conv = _ModelIDConv(config.default_model, config.default_thinking)
243
+ self.sessions[session.id] = (acp_session, model_id_conv)
244
+
245
+ if isinstance(cli_instance.soul.agent.toolset, KimiToolset):
246
+ replace_tools(
247
+ self.client_capabilities,
248
+ self.conn,
249
+ session.id,
250
+ cli_instance.soul.agent.toolset,
251
+ cli_instance.soul.runtime,
252
+ )
253
+
254
+ return acp_session, model_id_conv
255
+
256
+ async def load_session(
257
+ self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
258
+ ) -> None:
259
+ logger.info("Loading session: {id} for working directory: {cwd}", id=session_id, cwd=cwd)
260
+
261
+ if session_id in self.sessions:
262
+ logger.warning("Session already loaded: {id}", id=session_id)
263
+ return
264
+
265
+ # Check authentication before loading session
266
+ self._check_auth()
267
+
268
+ await self._setup_session(cwd, session_id, mcp_servers)
269
+ # TODO: replay session history?
270
+
271
+ async def resume_session(
272
+ self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
273
+ ) -> acp.schema.ResumeSessionResponse:
274
+ logger.info("Resuming session: {id} for working directory: {cwd}", id=session_id, cwd=cwd)
275
+
276
+ if session_id not in self.sessions:
277
+ await self._setup_session(cwd, session_id, mcp_servers)
278
+
279
+ acp_session, model_id_conv = self.sessions[session_id]
280
+ config = acp_session.cli.soul.runtime.config
281
+ return acp.schema.ResumeSessionResponse(
282
+ modes=acp.schema.SessionModeState(
283
+ available_modes=[
284
+ acp.schema.SessionMode(
285
+ id="default",
286
+ name="Default",
287
+ description="The default mode.",
288
+ ),
289
+ ],
290
+ current_mode_id="default",
291
+ ),
292
+ models=acp.schema.SessionModelState(
293
+ available_models=_expand_llm_models(config.models),
294
+ current_model_id=model_id_conv.to_acp_model_id(),
295
+ ),
296
+ )
297
+
298
+ async def fork_session(
299
+ self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
300
+ ) -> acp.schema.ForkSessionResponse:
301
+ raise NotImplementedError
302
+
303
+ async def list_sessions(
304
+ self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
305
+ ) -> acp.schema.ListSessionsResponse:
306
+ logger.info("Listing sessions for working directory: {cwd}", cwd=cwd)
307
+ if cwd is None:
308
+ return acp.schema.ListSessionsResponse(sessions=[], next_cursor=None)
309
+ work_dir = KaosPath.unsafe_from_local_path(Path(cwd))
310
+ sessions = await Session.list(work_dir)
311
+ return acp.schema.ListSessionsResponse(
312
+ sessions=[
313
+ acp.schema.SessionInfo(
314
+ cwd=cwd,
315
+ session_id=s.id,
316
+ title=s.title,
317
+ updated_at=datetime.fromtimestamp(s.updated_at).astimezone().isoformat(),
318
+ )
319
+ for s in sessions
320
+ ],
321
+ next_cursor=None,
322
+ )
323
+
324
+ async def set_session_mode(self, mode_id: str, session_id: str, **kwargs: Any) -> None:
325
+ assert mode_id == "default", "Only default mode is supported"
326
+
327
+ async def set_session_model(self, model_id: str, session_id: str, **kwargs: Any) -> None:
328
+ logger.info(
329
+ "Setting session model to {model_id} for session: {id}",
330
+ model_id=model_id,
331
+ id=session_id,
332
+ )
333
+ if session_id not in self.sessions:
334
+ logger.error("Session not found: {id}", id=session_id)
335
+ raise acp.RequestError.invalid_params({"session_id": "Session not found"})
336
+
337
+ acp_session, current_model_id = self.sessions[session_id]
338
+ cli_instance = acp_session.cli
339
+ model_id_conv = _ModelIDConv.from_acp_model_id(model_id)
340
+ if model_id_conv == current_model_id:
341
+ return
342
+
343
+ config = cli_instance.soul.runtime.config
344
+ new_model = config.models.get(model_id_conv.model_key)
345
+ if new_model is None:
346
+ logger.error("Model not found: {model_key}", model_key=model_id_conv.model_key)
347
+ raise acp.RequestError.invalid_params({"model_id": "Model not found"})
348
+ new_provider = config.providers.get(new_model.provider)
349
+ if new_provider is None:
350
+ logger.error(
351
+ "Provider not found: {provider} for model: {model_key}",
352
+ provider=new_model.provider,
353
+ model_key=model_id_conv.model_key,
354
+ )
355
+ raise acp.RequestError.invalid_params({"model_id": "Model's provider not found"})
356
+
357
+ new_llm = create_llm(
358
+ new_provider,
359
+ new_model,
360
+ session_id=acp_session.id,
361
+ thinking=model_id_conv.thinking,
362
+ oauth=cli_instance.soul.runtime.oauth,
363
+ )
364
+ cli_instance.soul.runtime.llm = new_llm
365
+
366
+ config.default_model = model_id_conv.model_key
367
+ config.default_thinking = model_id_conv.thinking
368
+ assert config.is_from_default_location, "`kimi acp` must use the default config location"
369
+ config_for_save = load_config()
370
+ config_for_save.default_model = model_id_conv.model_key
371
+ config_for_save.default_thinking = model_id_conv.thinking
372
+ save_config(config_for_save)
373
+
374
+ async def authenticate(self, method_id: str, **kwargs: Any) -> acp.AuthenticateResponse | None:
375
+ """
376
+ For Terminal Auth, this method is typically not called directly
377
+ (user completes auth in terminal). Implement for completeness.
378
+ """
379
+ if method_id == "login":
380
+ reason = self._check_token_usable()
381
+ if reason is None:
382
+ logger.info("Authentication successful for method: {id}", id=method_id)
383
+ return acp.AuthenticateResponse()
384
+ else:
385
+ logger.warning(
386
+ "Authentication not complete for method: {id} ({reason})",
387
+ id=method_id,
388
+ reason=reason,
389
+ )
390
+ raise acp.RequestError.auth_required(
391
+ {
392
+ "message": "Please complete login in terminal first",
393
+ "authMethods": self._auth_methods,
394
+ }
395
+ )
396
+
397
+ logger.error("Unknown auth method: {method_id}", method_id=method_id)
398
+ raise acp.RequestError.invalid_params({"method_id": "Unknown auth method"})
399
+
400
+ async def prompt(
401
+ self, prompt: list[ACPContentBlock], session_id: str, **kwargs: Any
402
+ ) -> acp.PromptResponse:
403
+ logger.info("Received prompt request for session: {id}", id=session_id)
404
+ if session_id not in self.sessions:
405
+ logger.error("Session not found: {id}", id=session_id)
406
+ raise acp.RequestError.invalid_params({"session_id": "Session not found"})
407
+ acp_session, *_ = self.sessions[session_id]
408
+ return await acp_session.prompt(prompt)
409
+
410
+ async def cancel(self, session_id: str, **kwargs: Any) -> None:
411
+ logger.info("Received cancel request for session: {id}", id=session_id)
412
+ if session_id not in self.sessions:
413
+ logger.error("Session not found: {id}", id=session_id)
414
+ raise acp.RequestError.invalid_params({"session_id": "Session not found"})
415
+ acp_session, *_ = self.sessions[session_id]
416
+ await acp_session.cancel()
417
+
418
+ async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
419
+ raise NotImplementedError
420
+
421
+ async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
422
+ raise NotImplementedError
423
+
424
+
425
+ class _ModelIDConv(NamedTuple):
426
+ model_key: str
427
+ thinking: bool
428
+
429
+ @classmethod
430
+ def from_acp_model_id(cls, model_id: str) -> _ModelIDConv:
431
+ if model_id.endswith(",thinking"):
432
+ return _ModelIDConv(model_id[: -len(",thinking")], True)
433
+ return _ModelIDConv(model_id, False)
434
+
435
+ def to_acp_model_id(self) -> str:
436
+ if self.thinking:
437
+ return f"{self.model_key},thinking"
438
+ return self.model_key
439
+
440
+
441
+ def _expand_llm_models(models: dict[str, LLMModel]) -> list[acp.schema.ModelInfo]:
442
+ expanded_models: list[acp.schema.ModelInfo] = []
443
+ for model_key, model in models.items():
444
+ capabilities = derive_model_capabilities(model)
445
+ if "thinking" in model.model or "reason" in model.model:
446
+ # always-thinking models
447
+ expanded_models.append(
448
+ acp.schema.ModelInfo(
449
+ model_id=_ModelIDConv(model_key, True).to_acp_model_id(),
450
+ name=f"{model.model}",
451
+ )
452
+ )
453
+ else:
454
+ expanded_models.append(
455
+ acp.schema.ModelInfo(
456
+ model_id=model_key,
457
+ name=model.model,
458
+ )
459
+ )
460
+ if "thinking" in capabilities:
461
+ # add thinking variant
462
+ expanded_models.append(
463
+ acp.schema.ModelInfo(
464
+ model_id=_ModelIDConv(model_key, True).to_acp_model_id(),
465
+ name=f"{model.model} (thinking)",
466
+ )
467
+ )
468
+ return expanded_models