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.
- bhu_cli-1.46.0.dist-info/METADATA +210 -0
- bhu_cli-1.46.0.dist-info/RECORD +258 -0
- bhu_cli-1.46.0.dist-info/WHEEL +4 -0
- bhu_cli-1.46.0.dist-info/entry_points.txt +4 -0
- kimi_cli/CHANGELOG.md +1281 -0
- kimi_cli/__init__.py +29 -0
- kimi_cli/__main__.py +43 -0
- kimi_cli/_build_info.py +1 -0
- kimi_cli/acp/AGENTS.md +92 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +128 -0
- kimi_cli/acp/kaos.py +291 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +468 -0
- kimi_cli/acp/session.py +499 -0
- kimi_cli/acp/tools.py +167 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/acp/version.py +45 -0
- kimi_cli/agents/default/agent.yaml +36 -0
- kimi_cli/agents/default/coder.yaml +25 -0
- kimi_cli/agents/default/explore.yaml +46 -0
- kimi_cli/agents/default/plan.yaml +30 -0
- kimi_cli/agents/default/system.md +160 -0
- kimi_cli/agents/okabe/agent.yaml +22 -0
- kimi_cli/agentspec.py +160 -0
- kimi_cli/app.py +807 -0
- kimi_cli/approval_runtime/__init__.py +29 -0
- kimi_cli/approval_runtime/models.py +42 -0
- kimi_cli/approval_runtime/runtime.py +235 -0
- kimi_cli/auth/__init__.py +5 -0
- kimi_cli/auth/oauth.py +1092 -0
- kimi_cli/auth/platforms.py +374 -0
- kimi_cli/background/__init__.py +36 -0
- kimi_cli/background/agent_runner.py +231 -0
- kimi_cli/background/ids.py +19 -0
- kimi_cli/background/manager.py +725 -0
- kimi_cli/background/models.py +105 -0
- kimi_cli/background/store.py +237 -0
- kimi_cli/background/summary.py +66 -0
- kimi_cli/background/worker.py +209 -0
- kimi_cli/cli/__init__.py +1081 -0
- kimi_cli/cli/__main__.py +34 -0
- kimi_cli/cli/_lazy_group.py +238 -0
- kimi_cli/cli/export.py +322 -0
- kimi_cli/cli/info.py +62 -0
- kimi_cli/cli/mcp.py +353 -0
- kimi_cli/cli/plugin.py +347 -0
- kimi_cli/cli/toad.py +73 -0
- kimi_cli/cli/vis.py +38 -0
- kimi_cli/cli/web.py +80 -0
- kimi_cli/config.py +429 -0
- kimi_cli/constant.py +116 -0
- kimi_cli/exception.py +43 -0
- kimi_cli/hooks/__init__.py +24 -0
- kimi_cli/hooks/config.py +34 -0
- kimi_cli/hooks/defaults/rtk.py +48 -0
- kimi_cli/hooks/engine.py +394 -0
- kimi_cli/hooks/events.py +190 -0
- kimi_cli/hooks/runner.py +103 -0
- kimi_cli/llm.py +333 -0
- kimi_cli/mcp_oauth.py +72 -0
- kimi_cli/metadata.py +79 -0
- kimi_cli/notifications/__init__.py +33 -0
- kimi_cli/notifications/llm.py +77 -0
- kimi_cli/notifications/manager.py +145 -0
- kimi_cli/notifications/models.py +50 -0
- kimi_cli/notifications/notifier.py +41 -0
- kimi_cli/notifications/store.py +118 -0
- kimi_cli/notifications/wire.py +21 -0
- kimi_cli/plugin/__init__.py +124 -0
- kimi_cli/plugin/manager.py +153 -0
- kimi_cli/plugin/tool.py +173 -0
- kimi_cli/prompts/__init__.py +6 -0
- kimi_cli/prompts/compact.md +73 -0
- kimi_cli/prompts/init.md +21 -0
- kimi_cli/py.typed +0 -0
- kimi_cli/session.py +319 -0
- kimi_cli/session_fork.py +325 -0
- kimi_cli/session_state.py +132 -0
- kimi_cli/share.py +14 -0
- kimi_cli/skill/__init__.py +727 -0
- kimi_cli/skill/flow/__init__.py +99 -0
- kimi_cli/skill/flow/d2.py +482 -0
- kimi_cli/skill/flow/mermaid.py +266 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +367 -0
- kimi_cli/soul/__init__.py +304 -0
- kimi_cli/soul/agent.py +519 -0
- kimi_cli/soul/approval.py +267 -0
- kimi_cli/soul/btw.py +237 -0
- kimi_cli/soul/compaction.py +189 -0
- kimi_cli/soul/context.py +339 -0
- kimi_cli/soul/denwarenji.py +39 -0
- kimi_cli/soul/dynamic_injection.py +84 -0
- kimi_cli/soul/dynamic_injections/__init__.py +0 -0
- kimi_cli/soul/dynamic_injections/afk_mode.py +74 -0
- kimi_cli/soul/dynamic_injections/plan_mode.py +245 -0
- kimi_cli/soul/kimisoul.py +1710 -0
- kimi_cli/soul/message.py +92 -0
- kimi_cli/soul/slash.py +341 -0
- kimi_cli/soul/toolset.py +891 -0
- kimi_cli/subagents/__init__.py +21 -0
- kimi_cli/subagents/builder.py +42 -0
- kimi_cli/subagents/core.py +86 -0
- kimi_cli/subagents/git_context.py +170 -0
- kimi_cli/subagents/models.py +54 -0
- kimi_cli/subagents/output.py +71 -0
- kimi_cli/subagents/registry.py +28 -0
- kimi_cli/subagents/runner.py +428 -0
- kimi_cli/subagents/store.py +196 -0
- kimi_cli/telemetry/__init__.py +197 -0
- kimi_cli/telemetry/crash.py +149 -0
- kimi_cli/telemetry/sink.py +143 -0
- kimi_cli/telemetry/transport.py +318 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +105 -0
- kimi_cli/tools/agent/__init__.py +277 -0
- kimi_cli/tools/agent/description.md +41 -0
- kimi_cli/tools/ask_user/__init__.py +154 -0
- kimi_cli/tools/ask_user/description.md +19 -0
- kimi_cli/tools/background/__init__.py +318 -0
- kimi_cli/tools/background/list.md +10 -0
- kimi_cli/tools/background/output.md +11 -0
- kimi_cli/tools/background/stop.md +8 -0
- kimi_cli/tools/display.py +46 -0
- kimi_cli/tools/dmail/__init__.py +38 -0
- kimi_cli/tools/dmail/dmail.md +17 -0
- kimi_cli/tools/file/__init__.py +30 -0
- kimi_cli/tools/file/glob.md +21 -0
- kimi_cli/tools/file/glob.py +178 -0
- kimi_cli/tools/file/grep.md +6 -0
- kimi_cli/tools/file/grep_local.py +590 -0
- kimi_cli/tools/file/plan_mode.py +45 -0
- kimi_cli/tools/file/read.md +16 -0
- kimi_cli/tools/file/read.py +300 -0
- kimi_cli/tools/file/read_media.md +24 -0
- kimi_cli/tools/file/read_media.py +217 -0
- kimi_cli/tools/file/replace.md +7 -0
- kimi_cli/tools/file/replace.py +195 -0
- kimi_cli/tools/file/utils.py +257 -0
- kimi_cli/tools/file/write.md +5 -0
- kimi_cli/tools/file/write.py +177 -0
- kimi_cli/tools/plan/__init__.py +339 -0
- kimi_cli/tools/plan/description.md +29 -0
- kimi_cli/tools/plan/enter.py +197 -0
- kimi_cli/tools/plan/enter_description.md +35 -0
- kimi_cli/tools/plan/heroes.py +277 -0
- kimi_cli/tools/shell/__init__.py +260 -0
- kimi_cli/tools/shell/bash.md +35 -0
- kimi_cli/tools/test.py +55 -0
- kimi_cli/tools/think/__init__.py +21 -0
- kimi_cli/tools/think/think.md +1 -0
- kimi_cli/tools/todo/__init__.py +168 -0
- kimi_cli/tools/todo/set_todo_list.md +23 -0
- kimi_cli/tools/utils.py +199 -0
- kimi_cli/tools/web/__init__.py +4 -0
- kimi_cli/tools/web/fetch.md +1 -0
- kimi_cli/tools/web/fetch.py +189 -0
- kimi_cli/tools/web/search.md +1 -0
- kimi_cli/tools/web/search.py +163 -0
- kimi_cli/ui/__init__.py +0 -0
- kimi_cli/ui/acp/__init__.py +99 -0
- kimi_cli/ui/print/__init__.py +474 -0
- kimi_cli/ui/print/visualize.py +194 -0
- kimi_cli/ui/shell/__init__.py +1540 -0
- kimi_cli/ui/shell/console.py +109 -0
- kimi_cli/ui/shell/debug.py +190 -0
- kimi_cli/ui/shell/echo.py +17 -0
- kimi_cli/ui/shell/export_import.py +117 -0
- kimi_cli/ui/shell/keyboard.py +300 -0
- kimi_cli/ui/shell/mcp_status.py +111 -0
- kimi_cli/ui/shell/oauth.py +149 -0
- kimi_cli/ui/shell/placeholders.py +531 -0
- kimi_cli/ui/shell/prompt.py +2259 -0
- kimi_cli/ui/shell/replay.py +215 -0
- kimi_cli/ui/shell/session_picker.py +227 -0
- kimi_cli/ui/shell/setup.py +212 -0
- kimi_cli/ui/shell/slash.py +893 -0
- kimi_cli/ui/shell/startup.py +32 -0
- kimi_cli/ui/shell/task_browser.py +486 -0
- kimi_cli/ui/shell/update.py +349 -0
- kimi_cli/ui/shell/usage.py +295 -0
- kimi_cli/ui/shell/visualize/__init__.py +165 -0
- kimi_cli/ui/shell/visualize/_approval_panel.py +505 -0
- kimi_cli/ui/shell/visualize/_blocks.py +640 -0
- kimi_cli/ui/shell/visualize/_btw_panel.py +224 -0
- kimi_cli/ui/shell/visualize/_input_router.py +48 -0
- kimi_cli/ui/shell/visualize/_interactive.py +524 -0
- kimi_cli/ui/shell/visualize/_live_view.py +902 -0
- kimi_cli/ui/shell/visualize/_question_panel.py +586 -0
- kimi_cli/ui/theme.py +241 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +24 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +108 -0
- kimi_cli/utils/clipboard.py +246 -0
- kimi_cli/utils/datetime.py +64 -0
- kimi_cli/utils/diff.py +135 -0
- kimi_cli/utils/editor.py +91 -0
- kimi_cli/utils/environment.py +225 -0
- kimi_cli/utils/envvar.py +22 -0
- kimi_cli/utils/export.py +696 -0
- kimi_cli/utils/file_filter.py +367 -0
- kimi_cli/utils/frontmatter.py +70 -0
- kimi_cli/utils/io.py +27 -0
- kimi_cli/utils/logging.py +124 -0
- kimi_cli/utils/media_tags.py +29 -0
- kimi_cli/utils/message.py +24 -0
- kimi_cli/utils/path.py +245 -0
- kimi_cli/utils/proctitle.py +33 -0
- kimi_cli/utils/proxy.py +31 -0
- kimi_cli/utils/pyinstaller.py +43 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/diff_render.py +481 -0
- kimi_cli/utils/rich/markdown.py +898 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/rich/syntax.py +114 -0
- kimi_cli/utils/sensitive.py +54 -0
- kimi_cli/utils/server.py +121 -0
- kimi_cli/utils/shell_quoting.py +38 -0
- kimi_cli/utils/signals.py +43 -0
- kimi_cli/utils/slashcmd.py +145 -0
- kimi_cli/utils/string.py +41 -0
- kimi_cli/utils/subprocess_env.py +73 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/utils/windows_paths.py +48 -0
- kimi_cli/vis/__init__.py +0 -0
- kimi_cli/vis/api/__init__.py +5 -0
- kimi_cli/vis/api/sessions.py +687 -0
- kimi_cli/vis/api/statistics.py +209 -0
- kimi_cli/vis/api/system.py +19 -0
- kimi_cli/vis/app.py +175 -0
- kimi_cli/web/__init__.py +5 -0
- kimi_cli/web/api/__init__.py +15 -0
- kimi_cli/web/api/config.py +208 -0
- kimi_cli/web/api/open_in.py +197 -0
- kimi_cli/web/api/sessions.py +1223 -0
- kimi_cli/web/app.py +451 -0
- kimi_cli/web/auth.py +191 -0
- kimi_cli/web/models.py +98 -0
- kimi_cli/web/runner/__init__.py +5 -0
- kimi_cli/web/runner/messages.py +57 -0
- kimi_cli/web/runner/process.py +754 -0
- kimi_cli/web/runner/worker.py +95 -0
- kimi_cli/web/store/__init__.py +1 -0
- kimi_cli/web/store/sessions.py +432 -0
- kimi_cli/wire/__init__.py +148 -0
- kimi_cli/wire/file.py +151 -0
- kimi_cli/wire/jsonrpc.py +263 -0
- kimi_cli/wire/protocol.py +2 -0
- kimi_cli/wire/root_hub.py +27 -0
- kimi_cli/wire/serde.py +26 -0
- kimi_cli/wire/server.py +1060 -0
- 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
|