kimi-cli 0.44__py3-none-any.whl → 0.78__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.
Potentially problematic release.
This version of kimi-cli might be problematic. Click here for more details.
- kimi_cli/CHANGELOG.md +349 -40
- kimi_cli/__init__.py +6 -0
- kimi_cli/acp/AGENTS.md +91 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +111 -0
- kimi_cli/acp/kaos.py +270 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +335 -0
- kimi_cli/acp/session.py +445 -0
- kimi_cli/acp/tools.py +158 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/agents/default/agent.yaml +4 -4
- kimi_cli/agents/default/sub.yaml +2 -1
- kimi_cli/agents/default/system.md +79 -21
- kimi_cli/agents/okabe/agent.yaml +17 -0
- kimi_cli/agentspec.py +53 -25
- kimi_cli/app.py +180 -52
- kimi_cli/cli/__init__.py +595 -0
- kimi_cli/cli/__main__.py +8 -0
- kimi_cli/cli/info.py +63 -0
- kimi_cli/cli/mcp.py +349 -0
- kimi_cli/config.py +153 -17
- kimi_cli/constant.py +3 -0
- kimi_cli/exception.py +23 -2
- kimi_cli/flow/__init__.py +117 -0
- kimi_cli/flow/d2.py +376 -0
- kimi_cli/flow/mermaid.py +218 -0
- kimi_cli/llm.py +129 -23
- kimi_cli/metadata.py +32 -7
- kimi_cli/platforms.py +262 -0
- kimi_cli/prompts/__init__.py +2 -0
- kimi_cli/prompts/compact.md +4 -5
- kimi_cli/session.py +223 -31
- kimi_cli/share.py +2 -0
- kimi_cli/skill.py +145 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +351 -0
- kimi_cli/soul/__init__.py +51 -20
- kimi_cli/soul/agent.py +213 -85
- kimi_cli/soul/approval.py +86 -17
- kimi_cli/soul/compaction.py +64 -53
- kimi_cli/soul/context.py +38 -5
- kimi_cli/soul/denwarenji.py +2 -0
- kimi_cli/soul/kimisoul.py +442 -60
- kimi_cli/soul/message.py +54 -54
- kimi_cli/soul/slash.py +72 -0
- kimi_cli/soul/toolset.py +387 -6
- kimi_cli/toad.py +74 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +42 -34
- kimi_cli/tools/display.py +25 -0
- kimi_cli/tools/dmail/__init__.py +10 -10
- kimi_cli/tools/dmail/dmail.md +11 -9
- kimi_cli/tools/file/__init__.py +1 -3
- kimi_cli/tools/file/glob.py +20 -23
- kimi_cli/tools/file/grep.md +1 -1
- kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
- kimi_cli/tools/file/read.md +24 -6
- kimi_cli/tools/file/read.py +134 -50
- kimi_cli/tools/file/replace.md +1 -1
- kimi_cli/tools/file/replace.py +36 -29
- kimi_cli/tools/file/utils.py +282 -0
- kimi_cli/tools/file/write.py +43 -22
- kimi_cli/tools/multiagent/__init__.py +7 -0
- kimi_cli/tools/multiagent/create.md +11 -0
- kimi_cli/tools/multiagent/create.py +50 -0
- kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
- kimi_cli/tools/shell/__init__.py +120 -0
- kimi_cli/tools/{bash → shell}/bash.md +1 -2
- kimi_cli/tools/shell/powershell.md +25 -0
- kimi_cli/tools/test.py +4 -4
- kimi_cli/tools/think/__init__.py +2 -2
- kimi_cli/tools/todo/__init__.py +14 -8
- kimi_cli/tools/utils.py +64 -24
- kimi_cli/tools/web/fetch.py +68 -13
- kimi_cli/tools/web/search.py +10 -12
- kimi_cli/ui/acp/__init__.py +65 -412
- kimi_cli/ui/print/__init__.py +37 -49
- kimi_cli/ui/print/visualize.py +179 -0
- kimi_cli/ui/shell/__init__.py +141 -84
- kimi_cli/ui/shell/console.py +2 -0
- kimi_cli/ui/shell/debug.py +28 -23
- kimi_cli/ui/shell/keyboard.py +5 -1
- kimi_cli/ui/shell/prompt.py +220 -194
- kimi_cli/ui/shell/replay.py +111 -46
- kimi_cli/ui/shell/setup.py +89 -82
- kimi_cli/ui/shell/slash.py +422 -0
- kimi_cli/ui/shell/update.py +4 -2
- kimi_cli/ui/shell/usage.py +271 -0
- kimi_cli/ui/shell/visualize.py +574 -72
- kimi_cli/ui/wire/__init__.py +267 -0
- kimi_cli/ui/wire/jsonrpc.py +142 -0
- kimi_cli/ui/wire/protocol.py +1 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +2 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +12 -7
- kimi_cli/utils/clipboard.py +12 -0
- kimi_cli/utils/datetime.py +37 -0
- kimi_cli/utils/environment.py +58 -0
- kimi_cli/utils/envvar.py +12 -0
- kimi_cli/utils/frontmatter.py +44 -0
- kimi_cli/utils/logging.py +7 -6
- kimi_cli/utils/message.py +9 -14
- kimi_cli/utils/path.py +99 -9
- kimi_cli/utils/pyinstaller.py +6 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/markdown.py +961 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +2 -0
- kimi_cli/utils/slashcmd.py +124 -0
- kimi_cli/utils/string.py +2 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/wire/__init__.py +98 -29
- kimi_cli/wire/serde.py +45 -0
- kimi_cli/wire/types.py +299 -0
- kimi_cli-0.78.dist-info/METADATA +200 -0
- kimi_cli-0.78.dist-info/RECORD +135 -0
- kimi_cli-0.78.dist-info/entry_points.txt +4 -0
- kimi_cli/cli.py +0 -250
- kimi_cli/soul/runtime.py +0 -96
- kimi_cli/tools/bash/__init__.py +0 -99
- kimi_cli/tools/file/patch.md +0 -8
- kimi_cli/tools/file/patch.py +0 -143
- kimi_cli/tools/mcp.py +0 -85
- kimi_cli/ui/shell/liveview.py +0 -386
- kimi_cli/ui/shell/metacmd.py +0 -262
- kimi_cli/wire/message.py +0 -91
- kimi_cli-0.44.dist-info/METADATA +0 -188
- kimi_cli-0.44.dist-info/RECORD +0 -89
- kimi_cli-0.44.dist-info/entry_points.txt +0 -3
- /kimi_cli/tools/{task → multiagent}/task.md +0 -0
- {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
kimi_cli/ui/acp/__init__.py
CHANGED
|
@@ -1,436 +1,89 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, NoReturn
|
|
3
4
|
|
|
4
5
|
import acp
|
|
5
|
-
import streamingjson
|
|
6
|
-
from kosong.base.message import (
|
|
7
|
-
ContentPart,
|
|
8
|
-
TextPart,
|
|
9
|
-
ToolCall,
|
|
10
|
-
ToolCallPart,
|
|
11
|
-
)
|
|
12
|
-
from kosong.chat_provider import ChatProviderError
|
|
13
|
-
from kosong.tooling import ToolError, ToolOk, ToolResult
|
|
14
6
|
|
|
15
|
-
from kimi_cli.
|
|
16
|
-
from kimi_cli.
|
|
7
|
+
from kimi_cli.acp.types import ACPContentBlock, MCPServer
|
|
8
|
+
from kimi_cli.soul import Soul
|
|
17
9
|
from kimi_cli.utils.logging import logger
|
|
18
|
-
from kimi_cli.wire import WireUISide
|
|
19
|
-
from kimi_cli.wire.message import (
|
|
20
|
-
ApprovalRequest,
|
|
21
|
-
ApprovalResponse,
|
|
22
|
-
StatusUpdate,
|
|
23
|
-
StepBegin,
|
|
24
|
-
StepInterrupted,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class _ToolCallState:
|
|
29
|
-
"""Manages the state of a single tool call for streaming updates."""
|
|
30
|
-
|
|
31
|
-
def __init__(self, tool_call: ToolCall):
|
|
32
|
-
# When the user rejected or cancelled a tool call, the step result may not
|
|
33
|
-
# be appended to the context. In this case, future step may emit tool call
|
|
34
|
-
# with the same tool call ID (on the LLM side). To avoid confusion of the
|
|
35
|
-
# ACP client, we need to ensure the uniqueness in the ACP connection.
|
|
36
|
-
self.acp_tool_call_id = str(uuid.uuid4())
|
|
37
|
-
|
|
38
|
-
self.tool_call = tool_call
|
|
39
|
-
self.args = tool_call.function.arguments or ""
|
|
40
|
-
self.lexer = streamingjson.Lexer()
|
|
41
|
-
if tool_call.function.arguments is not None:
|
|
42
|
-
self.lexer.append_string(tool_call.function.arguments)
|
|
43
|
-
|
|
44
|
-
def append_args_part(self, args_part: str):
|
|
45
|
-
"""Append a new arguments part to the accumulated args and lexer."""
|
|
46
|
-
self.args += args_part
|
|
47
|
-
self.lexer.append_string(args_part)
|
|
48
|
-
|
|
49
|
-
def get_title(self) -> str:
|
|
50
|
-
"""Get the current title with subtitle if available."""
|
|
51
|
-
tool_name = self.tool_call.function.name
|
|
52
|
-
subtitle = extract_subtitle(self.lexer, tool_name)
|
|
53
|
-
if subtitle:
|
|
54
|
-
return f"{tool_name}: {subtitle}"
|
|
55
|
-
return tool_name
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class _RunState:
|
|
59
|
-
def __init__(self):
|
|
60
|
-
self.tool_calls: dict[str, _ToolCallState] = {}
|
|
61
|
-
"""Map of tool call ID (LLM-side ID) to tool call state."""
|
|
62
|
-
self.last_tool_call: _ToolCallState | None = None
|
|
63
|
-
self.cancel_event = asyncio.Event()
|
|
64
10
|
|
|
11
|
+
_DEPRECATED_MESSAGE = (
|
|
12
|
+
"`kimi --acp` is deprecated. "
|
|
13
|
+
"Update your ACP client settings to use `kimi acp` without any flags or options."
|
|
14
|
+
)
|
|
65
15
|
|
|
66
|
-
class ACPAgent:
|
|
67
|
-
"""Implementation of the ACP Agent protocol."""
|
|
68
16
|
|
|
69
|
-
|
|
17
|
+
class ACPServerSingleSession:
|
|
18
|
+
def __init__(self, soul: Soul):
|
|
70
19
|
self.soul = soul
|
|
71
|
-
self.connection = connection
|
|
72
|
-
self.session_id: str | None = None
|
|
73
|
-
self.run_state: _RunState | None = None
|
|
74
|
-
|
|
75
|
-
async def initialize(self, params: acp.InitializeRequest) -> acp.InitializeResponse:
|
|
76
|
-
"""Handle initialize request."""
|
|
77
|
-
logger.info(
|
|
78
|
-
"ACP server initialized with protocol version: {version}",
|
|
79
|
-
version=params.protocolVersion,
|
|
80
|
-
)
|
|
81
20
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
async def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
async def
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
21
|
+
def on_connect(self, conn: acp.Client) -> None:
|
|
22
|
+
logger.info("ACP client connected")
|
|
23
|
+
|
|
24
|
+
def _raise(self) -> NoReturn:
|
|
25
|
+
logger.error(_DEPRECATED_MESSAGE)
|
|
26
|
+
raise acp.RequestError.invalid_params({"error": _DEPRECATED_MESSAGE})
|
|
27
|
+
|
|
28
|
+
async def initialize(
|
|
29
|
+
self,
|
|
30
|
+
protocol_version: int,
|
|
31
|
+
client_capabilities: acp.schema.ClientCapabilities | None = None,
|
|
32
|
+
client_info: acp.schema.Implementation | None = None,
|
|
33
|
+
**kwargs: Any,
|
|
34
|
+
) -> acp.InitializeResponse:
|
|
35
|
+
self._raise()
|
|
36
|
+
|
|
37
|
+
async def new_session(
|
|
38
|
+
self, cwd: str, mcp_servers: list[MCPServer], **kwargs: Any
|
|
39
|
+
) -> acp.NewSessionResponse:
|
|
40
|
+
self._raise()
|
|
41
|
+
|
|
42
|
+
async def load_session(
|
|
43
|
+
self, cwd: str, mcp_servers: list[MCPServer], session_id: str, **kwargs: Any
|
|
44
|
+
) -> None:
|
|
45
|
+
self._raise()
|
|
46
|
+
|
|
47
|
+
async def list_sessions(
|
|
48
|
+
self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
|
|
49
|
+
) -> acp.schema.ListSessionsResponse:
|
|
50
|
+
self._raise()
|
|
51
|
+
|
|
52
|
+
async def set_session_mode(
|
|
53
|
+
self, mode_id: str, session_id: str, **kwargs: Any
|
|
114
54
|
) -> acp.SetSessionModeResponse | None:
|
|
115
|
-
|
|
116
|
-
logger.warning("Set session mode: {mode}", mode=params.modeId)
|
|
117
|
-
return None
|
|
118
|
-
|
|
119
|
-
async def extMethod(self, method: str, params: dict) -> dict:
|
|
120
|
-
"""Handle extension method."""
|
|
121
|
-
logger.warning("Unsupported extension method: {method}", method=method)
|
|
122
|
-
return {}
|
|
123
|
-
|
|
124
|
-
async def extNotification(self, method: str, params: dict) -> None:
|
|
125
|
-
"""Handle extension notification."""
|
|
126
|
-
logger.warning("Unsupported extension notification: {method}", method=method)
|
|
127
|
-
|
|
128
|
-
async def prompt(self, params: acp.PromptRequest) -> acp.PromptResponse:
|
|
129
|
-
"""Handle prompt request with streaming support."""
|
|
130
|
-
# Extract text from prompt content blocks
|
|
131
|
-
prompt_text = "\n".join(
|
|
132
|
-
block.text for block in params.prompt if isinstance(block, acp.schema.TextContentBlock)
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
if not prompt_text:
|
|
136
|
-
raise acp.RequestError.invalid_params({"reason": "No text in prompt"})
|
|
137
|
-
|
|
138
|
-
logger.info("Processing prompt: {text}", text=prompt_text[:100])
|
|
139
|
-
|
|
140
|
-
self.run_state = _RunState()
|
|
141
|
-
try:
|
|
142
|
-
await run_soul(self.soul, prompt_text, self._stream_events, self.run_state.cancel_event)
|
|
143
|
-
return acp.PromptResponse(stopReason="end_turn")
|
|
144
|
-
except LLMNotSet:
|
|
145
|
-
logger.error("LLM not set")
|
|
146
|
-
raise acp.RequestError.internal_error({"error": "LLM not set"}) from None
|
|
147
|
-
except ChatProviderError as e:
|
|
148
|
-
logger.exception("LLM provider error:")
|
|
149
|
-
raise acp.RequestError.internal_error({"error": f"LLM provider error: {e}"}) from e
|
|
150
|
-
except MaxStepsReached as e:
|
|
151
|
-
logger.warning("Max steps reached: {n}", n=e.n_steps)
|
|
152
|
-
return acp.PromptResponse(stopReason="max_turn_requests")
|
|
153
|
-
except RunCancelled:
|
|
154
|
-
logger.info("Prompt cancelled by user")
|
|
155
|
-
return acp.PromptResponse(stopReason="cancelled")
|
|
156
|
-
except BaseException as e:
|
|
157
|
-
logger.exception("Unknown error:")
|
|
158
|
-
raise acp.RequestError.internal_error({"error": f"Unknown error: {e}"}) from e
|
|
159
|
-
finally:
|
|
160
|
-
self.run_state = None
|
|
161
|
-
|
|
162
|
-
async def cancel(self, params: acp.CancelNotification) -> None:
|
|
163
|
-
"""Handle cancel notification."""
|
|
164
|
-
logger.info("Cancel for session: {id}", id=params.sessionId)
|
|
165
|
-
|
|
166
|
-
if self.run_state is None:
|
|
167
|
-
logger.warning("No running prompt to cancel")
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
if not self.run_state.cancel_event.is_set():
|
|
171
|
-
logger.info("Cancelling running prompt")
|
|
172
|
-
self.run_state.cancel_event.set()
|
|
55
|
+
self._raise()
|
|
173
56
|
|
|
174
|
-
async def
|
|
175
|
-
|
|
57
|
+
async def set_session_model(
|
|
58
|
+
self, model_id: str, session_id: str, **kwargs: Any
|
|
59
|
+
) -> acp.SetSessionModelResponse | None:
|
|
60
|
+
self._raise()
|
|
176
61
|
|
|
177
|
-
|
|
178
|
-
|
|
62
|
+
async def authenticate(self, method_id: str, **kwargs: Any) -> acp.AuthenticateResponse | None:
|
|
63
|
+
self._raise()
|
|
179
64
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
await self._send_text(f"[{msg.__class__.__name__}]")
|
|
185
|
-
elif isinstance(msg, ToolCall):
|
|
186
|
-
await self._send_tool_call(msg)
|
|
187
|
-
elif isinstance(msg, ToolCallPart):
|
|
188
|
-
await self._send_tool_call_part(msg)
|
|
189
|
-
elif isinstance(msg, ToolResult):
|
|
190
|
-
await self._send_tool_result(msg)
|
|
191
|
-
elif isinstance(msg, ApprovalRequest):
|
|
192
|
-
await self._handle_approval_request(msg)
|
|
193
|
-
elif isinstance(msg, StatusUpdate):
|
|
194
|
-
# TODO: stream status if needed
|
|
195
|
-
pass
|
|
196
|
-
elif isinstance(msg, StepInterrupted):
|
|
197
|
-
break
|
|
65
|
+
async def prompt(
|
|
66
|
+
self, prompt: list[ACPContentBlock], session_id: str, **kwargs: Any
|
|
67
|
+
) -> acp.PromptResponse:
|
|
68
|
+
self._raise()
|
|
198
69
|
|
|
199
|
-
async def
|
|
200
|
-
|
|
201
|
-
if not self.session_id:
|
|
202
|
-
return
|
|
70
|
+
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
|
71
|
+
self._raise()
|
|
203
72
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
sessionId=self.session_id,
|
|
207
|
-
update=acp.schema.AgentMessageChunk(
|
|
208
|
-
content=acp.schema.TextContentBlock(type="text", text=text),
|
|
209
|
-
sessionUpdate="agent_message_chunk",
|
|
210
|
-
),
|
|
211
|
-
)
|
|
212
|
-
)
|
|
73
|
+
async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
74
|
+
self._raise()
|
|
213
75
|
|
|
214
|
-
async def
|
|
215
|
-
|
|
216
|
-
assert self.run_state is not None
|
|
217
|
-
if not self.session_id:
|
|
218
|
-
return
|
|
76
|
+
async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
|
|
77
|
+
self._raise()
|
|
219
78
|
|
|
220
|
-
# Create and store tool call state
|
|
221
|
-
state = _ToolCallState(tool_call)
|
|
222
|
-
self.run_state.tool_calls[tool_call.id] = state
|
|
223
|
-
self.run_state.last_tool_call = state
|
|
224
79
|
|
|
225
|
-
|
|
226
|
-
acp.SessionNotification(
|
|
227
|
-
sessionId=self.session_id,
|
|
228
|
-
update=acp.schema.ToolCallStart(
|
|
229
|
-
sessionUpdate="tool_call",
|
|
230
|
-
toolCallId=state.acp_tool_call_id,
|
|
231
|
-
title=state.get_title(),
|
|
232
|
-
status="in_progress",
|
|
233
|
-
content=[
|
|
234
|
-
acp.schema.ContentToolCallContent(
|
|
235
|
-
type="content",
|
|
236
|
-
content=acp.schema.TextContentBlock(type="text", text=state.args),
|
|
237
|
-
)
|
|
238
|
-
],
|
|
239
|
-
),
|
|
240
|
-
)
|
|
241
|
-
)
|
|
242
|
-
logger.debug("Sent tool call: {name}", name=tool_call.function.name)
|
|
243
|
-
|
|
244
|
-
async def _send_tool_call_part(self, part: ToolCallPart):
|
|
245
|
-
"""Send tool call part (streaming arguments)."""
|
|
246
|
-
assert self.run_state is not None
|
|
247
|
-
if not self.session_id or not part.arguments_part or self.run_state.last_tool_call is None:
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
# Append new arguments part to the last tool call
|
|
251
|
-
self.run_state.last_tool_call.append_args_part(part.arguments_part)
|
|
252
|
-
|
|
253
|
-
# Update the tool call with new content and title
|
|
254
|
-
update = acp.schema.ToolCallProgress(
|
|
255
|
-
sessionUpdate="tool_call_update",
|
|
256
|
-
toolCallId=self.run_state.last_tool_call.acp_tool_call_id,
|
|
257
|
-
title=self.run_state.last_tool_call.get_title(),
|
|
258
|
-
status="in_progress",
|
|
259
|
-
content=[
|
|
260
|
-
acp.schema.ContentToolCallContent(
|
|
261
|
-
type="content",
|
|
262
|
-
content=acp.schema.TextContentBlock(
|
|
263
|
-
type="text", text=self.run_state.last_tool_call.args
|
|
264
|
-
),
|
|
265
|
-
)
|
|
266
|
-
],
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
await self.connection.sessionUpdate(
|
|
270
|
-
acp.SessionNotification(sessionId=self.session_id, update=update)
|
|
271
|
-
)
|
|
272
|
-
logger.debug("Sent tool call update: {delta}", delta=part.arguments_part[:50])
|
|
273
|
-
|
|
274
|
-
async def _send_tool_result(self, result: ToolResult):
|
|
275
|
-
"""Send tool result to client."""
|
|
276
|
-
assert self.run_state is not None
|
|
277
|
-
if not self.session_id:
|
|
278
|
-
return
|
|
279
|
-
|
|
280
|
-
tool_result = result.result
|
|
281
|
-
is_error = isinstance(tool_result, ToolError)
|
|
282
|
-
|
|
283
|
-
state = self.run_state.tool_calls.pop(result.tool_call_id, None)
|
|
284
|
-
if state is None:
|
|
285
|
-
logger.warning("Tool call not found: {id}", id=result.tool_call_id)
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
update = acp.schema.ToolCallProgress(
|
|
289
|
-
sessionUpdate="tool_call_update",
|
|
290
|
-
toolCallId=state.acp_tool_call_id,
|
|
291
|
-
status="failed" if is_error else "completed",
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
if state.tool_call.function.name == "SetTodoList" and not is_error:
|
|
295
|
-
update.content = _tool_result_to_acp_content(tool_result)
|
|
296
|
-
|
|
297
|
-
await self.connection.sessionUpdate(
|
|
298
|
-
acp.SessionNotification(sessionId=self.session_id, update=update)
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
logger.debug("Sent tool result: {id}", id=result.tool_call_id)
|
|
302
|
-
|
|
303
|
-
async def _handle_approval_request(self, request: ApprovalRequest):
|
|
304
|
-
"""Handle approval request by sending permission request to client."""
|
|
305
|
-
assert self.run_state is not None
|
|
306
|
-
if not self.session_id:
|
|
307
|
-
logger.warning("No session ID, auto-rejecting approval request")
|
|
308
|
-
request.resolve(ApprovalResponse.REJECT)
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
state = self.run_state.tool_calls.get(request.tool_call_id, None)
|
|
312
|
-
if state is None:
|
|
313
|
-
logger.warning("Tool call not found: {id}", id=request.tool_call_id)
|
|
314
|
-
request.resolve(ApprovalResponse.REJECT)
|
|
315
|
-
return
|
|
316
|
-
|
|
317
|
-
# Create permission request with options
|
|
318
|
-
permission_request = acp.RequestPermissionRequest(
|
|
319
|
-
sessionId=self.session_id,
|
|
320
|
-
toolCall=acp.schema.ToolCall(
|
|
321
|
-
toolCallId=state.acp_tool_call_id,
|
|
322
|
-
content=[
|
|
323
|
-
acp.schema.ContentToolCallContent(
|
|
324
|
-
type="content",
|
|
325
|
-
content=acp.schema.TextContentBlock(
|
|
326
|
-
type="text",
|
|
327
|
-
text=f"Requesting approval to perform: {request.description}",
|
|
328
|
-
),
|
|
329
|
-
),
|
|
330
|
-
],
|
|
331
|
-
),
|
|
332
|
-
options=[
|
|
333
|
-
acp.schema.PermissionOption(
|
|
334
|
-
optionId="approve",
|
|
335
|
-
name="Approve",
|
|
336
|
-
kind="allow_once",
|
|
337
|
-
),
|
|
338
|
-
acp.schema.PermissionOption(
|
|
339
|
-
optionId="approve_for_session",
|
|
340
|
-
name="Approve for this session",
|
|
341
|
-
kind="allow_always",
|
|
342
|
-
),
|
|
343
|
-
acp.schema.PermissionOption(
|
|
344
|
-
optionId="reject",
|
|
345
|
-
name="Reject",
|
|
346
|
-
kind="reject_once",
|
|
347
|
-
),
|
|
348
|
-
],
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
try:
|
|
352
|
-
# Send permission request and wait for response
|
|
353
|
-
logger.debug("Requesting permission for action: {action}", action=request.action)
|
|
354
|
-
response = await self.connection.requestPermission(permission_request)
|
|
355
|
-
logger.debug("Received permission response: {response}", response=response)
|
|
356
|
-
|
|
357
|
-
# Process the outcome
|
|
358
|
-
if isinstance(response.outcome, acp.schema.AllowedOutcome):
|
|
359
|
-
# selected
|
|
360
|
-
if response.outcome.optionId == "approve":
|
|
361
|
-
logger.debug("Permission granted for: {action}", action=request.action)
|
|
362
|
-
request.resolve(ApprovalResponse.APPROVE)
|
|
363
|
-
elif response.outcome.optionId == "approve_for_session":
|
|
364
|
-
logger.debug("Permission granted for session: {action}", action=request.action)
|
|
365
|
-
request.resolve(ApprovalResponse.APPROVE_FOR_SESSION)
|
|
366
|
-
else:
|
|
367
|
-
logger.debug("Permission denied for: {action}", action=request.action)
|
|
368
|
-
request.resolve(ApprovalResponse.REJECT)
|
|
369
|
-
else:
|
|
370
|
-
# cancelled
|
|
371
|
-
logger.debug("Permission request cancelled for: {action}", action=request.action)
|
|
372
|
-
request.resolve(ApprovalResponse.REJECT)
|
|
373
|
-
except Exception:
|
|
374
|
-
logger.exception("Error handling approval request:")
|
|
375
|
-
# On error, reject the request
|
|
376
|
-
request.resolve(ApprovalResponse.REJECT)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def _tool_result_to_acp_content(
|
|
380
|
-
tool_result: ToolOk | ToolError,
|
|
381
|
-
) -> list[
|
|
382
|
-
acp.schema.ContentToolCallContent
|
|
383
|
-
| acp.schema.FileEditToolCallContent
|
|
384
|
-
| acp.schema.TerminalToolCallContent
|
|
385
|
-
]:
|
|
386
|
-
def _to_acp_content(part: ContentPart) -> acp.schema.ContentToolCallContent:
|
|
387
|
-
if isinstance(part, TextPart):
|
|
388
|
-
return acp.schema.ContentToolCallContent(
|
|
389
|
-
type="content", content=acp.schema.TextContentBlock(type="text", text=part.text)
|
|
390
|
-
)
|
|
391
|
-
else:
|
|
392
|
-
logger.warning("Unsupported content part in tool result: {part}", part=part)
|
|
393
|
-
return acp.schema.ContentToolCallContent(
|
|
394
|
-
type="content",
|
|
395
|
-
content=acp.schema.TextContentBlock(
|
|
396
|
-
type="text", text=f"[{part.__class__.__name__}]"
|
|
397
|
-
),
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
content = []
|
|
401
|
-
if isinstance(tool_result.output, str):
|
|
402
|
-
content.append(_to_acp_content(TextPart(text=tool_result.output)))
|
|
403
|
-
elif isinstance(tool_result.output, ContentPart):
|
|
404
|
-
content.append(_to_acp_content(tool_result.output))
|
|
405
|
-
elif isinstance(tool_result.output, list):
|
|
406
|
-
content.extend(_to_acp_content(part) for part in tool_result.output)
|
|
407
|
-
|
|
408
|
-
return content
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
class ACPServer:
|
|
80
|
+
class ACP:
|
|
412
81
|
"""ACP server using the official acp library."""
|
|
413
82
|
|
|
414
83
|
def __init__(self, soul: Soul):
|
|
415
84
|
self.soul = soul
|
|
416
85
|
|
|
417
|
-
async def run(self)
|
|
86
|
+
async def run(self):
|
|
418
87
|
"""Run the ACP server."""
|
|
419
|
-
logger.info("Starting ACP server on stdio")
|
|
420
|
-
|
|
421
|
-
# Get stdio streams
|
|
422
|
-
reader, writer = await acp.stdio_streams()
|
|
423
|
-
|
|
424
|
-
# Create connection - the library handles all JSON-RPC details!
|
|
425
|
-
_ = acp.AgentSideConnection(
|
|
426
|
-
lambda conn: ACPAgent(self.soul, conn),
|
|
427
|
-
writer,
|
|
428
|
-
reader,
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
logger.info("ACP server ready")
|
|
432
|
-
|
|
433
|
-
# Keep running - connection handles everything
|
|
434
|
-
await asyncio.Event().wait()
|
|
435
|
-
|
|
436
|
-
return True
|
|
88
|
+
logger.info("Starting ACP server (single session) on stdio")
|
|
89
|
+
await acp.run_agent(ACPServerSingleSession(self.soul))
|
kimi_cli/ui/print/__init__.py
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import json
|
|
3
5
|
import sys
|
|
4
6
|
from functools import partial
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
7
|
-
import aiofiles
|
|
8
|
-
from kosong.base.message import Message
|
|
9
9
|
from kosong.chat_provider import ChatProviderError
|
|
10
|
+
from kosong.message import Message
|
|
10
11
|
from rich import print
|
|
11
12
|
|
|
12
13
|
from kimi_cli.cli import InputFormat, OutputFormat
|
|
13
|
-
from kimi_cli.soul import
|
|
14
|
+
from kimi_cli.soul import (
|
|
15
|
+
LLMNotSet,
|
|
16
|
+
LLMNotSupported,
|
|
17
|
+
MaxStepsReached,
|
|
18
|
+
RunCancelled,
|
|
19
|
+
Soul,
|
|
20
|
+
run_soul,
|
|
21
|
+
)
|
|
22
|
+
from kimi_cli.soul.kimisoul import KimiSoul
|
|
23
|
+
from kimi_cli.ui.print.visualize import visualize
|
|
14
24
|
from kimi_cli.utils.logging import logger
|
|
15
|
-
from kimi_cli.utils.message import message_extract_text
|
|
16
25
|
from kimi_cli.utils.signals import install_sigint_handler
|
|
17
|
-
from kimi_cli.wire import WireUISide
|
|
18
|
-
from kimi_cli.wire.message import StepInterrupted
|
|
19
26
|
|
|
20
27
|
|
|
21
|
-
class
|
|
28
|
+
class Print:
|
|
22
29
|
"""
|
|
23
30
|
An app implementation that prints the agent behavior to the console.
|
|
24
31
|
|
|
@@ -27,6 +34,7 @@ class PrintApp:
|
|
|
27
34
|
input_format (InputFormat): The input format to use.
|
|
28
35
|
output_format (OutputFormat): The output format to use.
|
|
29
36
|
context_file (Path): The file to store the context.
|
|
37
|
+
final_only (bool): Whether to only print the final assistant message.
|
|
30
38
|
"""
|
|
31
39
|
|
|
32
40
|
def __init__(
|
|
@@ -35,11 +43,14 @@ class PrintApp:
|
|
|
35
43
|
input_format: InputFormat,
|
|
36
44
|
output_format: OutputFormat,
|
|
37
45
|
context_file: Path,
|
|
46
|
+
*,
|
|
47
|
+
final_only: bool = False,
|
|
38
48
|
):
|
|
39
49
|
self.soul = soul
|
|
40
|
-
self.input_format = input_format
|
|
41
|
-
self.output_format = output_format
|
|
50
|
+
self.input_format: InputFormat = input_format
|
|
51
|
+
self.output_format: OutputFormat = output_format
|
|
42
52
|
self.context_file = context_file
|
|
53
|
+
self.final_only = final_only
|
|
43
54
|
|
|
44
55
|
async def run(self, command: str | None = None) -> bool:
|
|
45
56
|
cancel_event = asyncio.Event()
|
|
@@ -68,26 +79,31 @@ class PrintApp:
|
|
|
68
79
|
|
|
69
80
|
if command:
|
|
70
81
|
logger.info("Running agent with command: {command}", command=command)
|
|
71
|
-
if self.output_format == "text":
|
|
72
|
-
visualize_fn = self._visualize_text
|
|
82
|
+
if self.output_format == "text" and not self.final_only:
|
|
73
83
|
print(command)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
await run_soul(
|
|
85
|
+
self.soul,
|
|
86
|
+
command,
|
|
87
|
+
partial(visualize, self.output_format, self.final_only),
|
|
88
|
+
cancel_event,
|
|
89
|
+
self.soul.wire_file if isinstance(self.soul, KimiSoul) else None,
|
|
90
|
+
)
|
|
78
91
|
else:
|
|
79
92
|
logger.info("Empty command, skipping")
|
|
80
93
|
|
|
81
94
|
command = None
|
|
82
|
-
except LLMNotSet:
|
|
83
|
-
logger.
|
|
84
|
-
print(
|
|
95
|
+
except LLMNotSet as e:
|
|
96
|
+
logger.exception("LLM not set:")
|
|
97
|
+
print(str(e))
|
|
98
|
+
except LLMNotSupported as e:
|
|
99
|
+
logger.exception("LLM not supported:")
|
|
100
|
+
print(str(e))
|
|
85
101
|
except ChatProviderError as e:
|
|
86
102
|
logger.exception("LLM provider error:")
|
|
87
|
-
print(
|
|
103
|
+
print(str(e))
|
|
88
104
|
except MaxStepsReached as e:
|
|
89
105
|
logger.warning("Max steps reached: {n_steps}", n_steps=e.n_steps)
|
|
90
|
-
print(
|
|
106
|
+
print(str(e))
|
|
91
107
|
except RunCancelled:
|
|
92
108
|
logger.error("Interrupted by user")
|
|
93
109
|
print("Interrupted by user")
|
|
@@ -115,7 +131,7 @@ class PrintApp:
|
|
|
115
131
|
data = json.loads(json_line)
|
|
116
132
|
message = Message.model_validate(data)
|
|
117
133
|
if message.role == "user":
|
|
118
|
-
return
|
|
134
|
+
return message.extract_text(sep="\n")
|
|
119
135
|
logger.warning(
|
|
120
136
|
"Ignoring message with role `{role}`: {json_line}",
|
|
121
137
|
role=message.role,
|
|
@@ -123,31 +139,3 @@ class PrintApp:
|
|
|
123
139
|
)
|
|
124
140
|
except Exception:
|
|
125
141
|
logger.warning("Ignoring invalid user message: {json_line}", json_line=json_line)
|
|
126
|
-
|
|
127
|
-
async def _visualize_text(self, wire: WireUISide):
|
|
128
|
-
while True:
|
|
129
|
-
msg = await wire.receive()
|
|
130
|
-
print(msg)
|
|
131
|
-
if isinstance(msg, StepInterrupted):
|
|
132
|
-
break
|
|
133
|
-
|
|
134
|
-
async def _visualize_stream_json(self, wire: WireUISide, start_position: int):
|
|
135
|
-
# TODO: be aware of context compaction
|
|
136
|
-
# FIXME: this is only a temporary impl, may miss the last lines of the context file
|
|
137
|
-
if not self.context_file.exists():
|
|
138
|
-
self.context_file.touch()
|
|
139
|
-
async with aiofiles.open(self.context_file, encoding="utf-8") as f:
|
|
140
|
-
await f.seek(start_position)
|
|
141
|
-
while True:
|
|
142
|
-
should_end = False
|
|
143
|
-
while (msg := wire.receive_nowait()) is not None:
|
|
144
|
-
if isinstance(msg, StepInterrupted):
|
|
145
|
-
should_end = True
|
|
146
|
-
|
|
147
|
-
line = await f.readline()
|
|
148
|
-
if not line:
|
|
149
|
-
if should_end:
|
|
150
|
-
break
|
|
151
|
-
await asyncio.sleep(0.1)
|
|
152
|
-
continue
|
|
153
|
-
print(line, end="")
|