kimi-cli 0.35__py3-none-any.whl → 0.52__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.
- kimi_cli/CHANGELOG.md +165 -0
- kimi_cli/__init__.py +0 -374
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/app.py +208 -0
- kimi_cli/cli.py +321 -0
- kimi_cli/config.py +33 -16
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +144 -3
- kimi_cli/metadata.py +6 -69
- kimi_cli/prompts/__init__.py +4 -0
- kimi_cli/session.py +103 -0
- kimi_cli/soul/__init__.py +130 -9
- kimi_cli/soul/agent.py +159 -0
- kimi_cli/soul/approval.py +5 -6
- kimi_cli/soul/compaction.py +106 -0
- kimi_cli/soul/context.py +1 -1
- kimi_cli/soul/kimisoul.py +180 -80
- kimi_cli/soul/message.py +6 -6
- kimi_cli/soul/runtime.py +96 -0
- kimi_cli/soul/toolset.py +3 -2
- kimi_cli/tools/__init__.py +35 -31
- kimi_cli/tools/bash/__init__.py +25 -9
- kimi_cli/tools/bash/cmd.md +31 -0
- kimi_cli/tools/dmail/__init__.py +5 -4
- kimi_cli/tools/file/__init__.py +8 -0
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +4 -4
- kimi_cli/tools/file/grep.py +36 -19
- kimi_cli/tools/file/patch.py +52 -10
- kimi_cli/tools/file/read.py +6 -5
- kimi_cli/tools/file/replace.py +16 -4
- kimi_cli/tools/file/write.py +16 -4
- kimi_cli/tools/mcp.py +7 -4
- kimi_cli/tools/task/__init__.py +60 -41
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +4 -2
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +13 -12
- kimi_cli/ui/__init__.py +0 -68
- kimi_cli/ui/acp/__init__.py +67 -38
- kimi_cli/ui/print/__init__.py +46 -69
- kimi_cli/ui/shell/__init__.py +145 -154
- kimi_cli/ui/shell/console.py +27 -1
- kimi_cli/ui/shell/debug.py +187 -0
- kimi_cli/ui/shell/keyboard.py +183 -0
- kimi_cli/ui/shell/metacmd.py +34 -81
- kimi_cli/ui/shell/prompt.py +245 -28
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/setup.py +19 -19
- kimi_cli/ui/shell/update.py +11 -5
- kimi_cli/ui/shell/visualize.py +576 -0
- kimi_cli/ui/wire/README.md +109 -0
- kimi_cli/ui/wire/__init__.py +340 -0
- kimi_cli/ui/wire/jsonrpc.py +48 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +6 -2
- kimi_cli/utils/clipboard.py +10 -0
- kimi_cli/utils/message.py +15 -1
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/markdown.py +959 -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 +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/utils/term.py +114 -0
- kimi_cli/wire/__init__.py +73 -0
- kimi_cli/wire/message.py +191 -0
- kimi_cli-0.52.dist-info/METADATA +186 -0
- kimi_cli-0.52.dist-info/RECORD +99 -0
- kimi_cli-0.52.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- kimi_cli/prompts/metacmds/__init__.py +0 -4
- kimi_cli/soul/wire.py +0 -101
- kimi_cli/ui/shell/liveview.py +0 -158
- kimi_cli/utils/provider.py +0 -64
- kimi_cli-0.35.dist-info/METADATA +0 -24
- kimi_cli-0.35.dist-info/RECORD +0 -76
- kimi_cli-0.35.dist-info/entry_points.txt +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
- /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
kimi_cli/ui/acp/__init__.py
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import uuid
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
|
-
import acp
|
|
5
|
-
import streamingjson
|
|
6
|
-
from kosong.
|
|
5
|
+
import acp # pyright: ignore[reportMissingTypeStubs]
|
|
6
|
+
import streamingjson # pyright: ignore[reportMissingTypeStubs]
|
|
7
|
+
from kosong.chat_provider import ChatProviderError
|
|
8
|
+
from kosong.message import (
|
|
7
9
|
ContentPart,
|
|
8
10
|
TextPart,
|
|
11
|
+
ThinkPart,
|
|
9
12
|
ToolCall,
|
|
10
13
|
ToolCallPart,
|
|
11
14
|
)
|
|
12
|
-
from kosong.chat_provider import ChatProviderError
|
|
13
15
|
from kosong.tooling import ToolError, ToolOk, ToolResult
|
|
14
16
|
|
|
15
|
-
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul
|
|
16
|
-
from kimi_cli.
|
|
17
|
+
from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
18
|
+
from kimi_cli.tools import extract_key_argument
|
|
19
|
+
from kimi_cli.utils.logging import logger
|
|
20
|
+
from kimi_cli.wire import WireUISide
|
|
21
|
+
from kimi_cli.wire.message import (
|
|
17
22
|
ApprovalRequest,
|
|
18
23
|
ApprovalResponse,
|
|
24
|
+
CompactionBegin,
|
|
25
|
+
CompactionEnd,
|
|
19
26
|
StatusUpdate,
|
|
20
27
|
StepBegin,
|
|
21
28
|
StepInterrupted,
|
|
22
|
-
|
|
29
|
+
SubagentEvent,
|
|
23
30
|
)
|
|
24
|
-
from kimi_cli.tools import extract_subtitle
|
|
25
|
-
from kimi_cli.ui import RunCancelled, run_soul
|
|
26
|
-
from kimi_cli.utils.logging import logger
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class _ToolCallState:
|
|
@@ -50,7 +54,7 @@ class _ToolCallState:
|
|
|
50
54
|
def get_title(self) -> str:
|
|
51
55
|
"""Get the current title with subtitle if available."""
|
|
52
56
|
tool_name = self.tool_call.function.name
|
|
53
|
-
subtitle =
|
|
57
|
+
subtitle = extract_key_argument(self.lexer, tool_name)
|
|
54
58
|
if subtitle:
|
|
55
59
|
return f"{tool_name}: {subtitle}"
|
|
56
60
|
return tool_name
|
|
@@ -62,9 +66,10 @@ class _RunState:
|
|
|
62
66
|
"""Map of tool call ID (LLM-side ID) to tool call state."""
|
|
63
67
|
self.last_tool_call: _ToolCallState | None = None
|
|
64
68
|
self.cancel_event = asyncio.Event()
|
|
69
|
+
self.in_thinking = False
|
|
65
70
|
|
|
66
71
|
|
|
67
|
-
class
|
|
72
|
+
class ACPAgent:
|
|
68
73
|
"""Implementation of the ACP Agent protocol."""
|
|
69
74
|
|
|
70
75
|
def __init__(self, soul: Soul, connection: acp.AgentSideConnection):
|
|
@@ -117,12 +122,12 @@ class ACPAgentImpl:
|
|
|
117
122
|
logger.warning("Set session mode: {mode}", mode=params.modeId)
|
|
118
123
|
return None
|
|
119
124
|
|
|
120
|
-
async def extMethod(self, method: str, params: dict) -> dict:
|
|
125
|
+
async def extMethod(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
121
126
|
"""Handle extension method."""
|
|
122
127
|
logger.warning("Unsupported extension method: {method}", method=method)
|
|
123
128
|
return {}
|
|
124
129
|
|
|
125
|
-
async def extNotification(self, method: str, params: dict) -> None:
|
|
130
|
+
async def extNotification(self, method: str, params: dict[str, Any]) -> None:
|
|
126
131
|
"""Handle extension notification."""
|
|
127
132
|
logger.warning("Unsupported extension notification: {method}", method=method)
|
|
128
133
|
|
|
@@ -172,34 +177,46 @@ class ACPAgentImpl:
|
|
|
172
177
|
logger.info("Cancelling running prompt")
|
|
173
178
|
self.run_state.cancel_event.set()
|
|
174
179
|
|
|
175
|
-
async def _stream_events(self, wire:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
assert isinstance(await wire.receive(), StepBegin)
|
|
180
|
+
async def _stream_events(self, wire: WireUISide):
|
|
181
|
+
while True:
|
|
182
|
+
msg = await wire.receive()
|
|
179
183
|
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
assert self.run_state is not None
|
|
185
|
+
if isinstance(msg, ThinkPart) and not self.run_state.in_thinking:
|
|
186
|
+
await self._send_text("<think>\n")
|
|
187
|
+
self.run_state.in_thinking = True
|
|
188
|
+
if not isinstance(msg, ThinkPart) and self.run_state.in_thinking:
|
|
189
|
+
await self._send_text("\n\n</think>\n\n")
|
|
190
|
+
self.run_state.in_thinking = False
|
|
182
191
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
match msg:
|
|
193
|
+
case StepBegin():
|
|
194
|
+
pass
|
|
195
|
+
case StepInterrupted():
|
|
196
|
+
break
|
|
197
|
+
case CompactionBegin():
|
|
198
|
+
pass
|
|
199
|
+
case CompactionEnd():
|
|
200
|
+
pass
|
|
201
|
+
case StatusUpdate():
|
|
202
|
+
pass
|
|
203
|
+
case ThinkPart(think=think):
|
|
204
|
+
await self._send_text(think)
|
|
205
|
+
case TextPart(text=text):
|
|
206
|
+
await self._send_text(text)
|
|
207
|
+
case ContentPart():
|
|
186
208
|
logger.warning("Unsupported content part: {part}", part=msg)
|
|
187
209
|
await self._send_text(f"[{msg.__class__.__name__}]")
|
|
188
|
-
|
|
210
|
+
case ToolCall():
|
|
189
211
|
await self._send_tool_call(msg)
|
|
190
|
-
|
|
212
|
+
case ToolCallPart():
|
|
191
213
|
await self._send_tool_call_part(msg)
|
|
192
|
-
|
|
214
|
+
case ToolResult():
|
|
193
215
|
await self._send_tool_result(msg)
|
|
194
|
-
|
|
195
|
-
await self._handle_approval_request(msg)
|
|
196
|
-
elif isinstance(msg, StatusUpdate):
|
|
197
|
-
# TODO: stream status if needed
|
|
216
|
+
case SubagentEvent():
|
|
198
217
|
pass
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
except asyncio.QueueShutDown:
|
|
202
|
-
logger.debug("Event stream loop shutting down")
|
|
218
|
+
case ApprovalRequest():
|
|
219
|
+
await self._handle_approval_request(msg)
|
|
203
220
|
|
|
204
221
|
async def _send_text(self, text: str):
|
|
205
222
|
"""Send text chunk to client."""
|
|
@@ -322,7 +339,7 @@ class ACPAgentImpl:
|
|
|
322
339
|
# Create permission request with options
|
|
323
340
|
permission_request = acp.RequestPermissionRequest(
|
|
324
341
|
sessionId=self.session_id,
|
|
325
|
-
toolCall=acp.schema.
|
|
342
|
+
toolCall=acp.schema.ToolCall(
|
|
326
343
|
toolCallId=state.acp_tool_call_id,
|
|
327
344
|
content=[
|
|
328
345
|
acp.schema.ContentToolCallContent(
|
|
@@ -388,7 +405,13 @@ def _tool_result_to_acp_content(
|
|
|
388
405
|
| acp.schema.FileEditToolCallContent
|
|
389
406
|
| acp.schema.TerminalToolCallContent
|
|
390
407
|
]:
|
|
391
|
-
def _to_acp_content(
|
|
408
|
+
def _to_acp_content(
|
|
409
|
+
part: ContentPart,
|
|
410
|
+
) -> (
|
|
411
|
+
acp.schema.ContentToolCallContent
|
|
412
|
+
| acp.schema.FileEditToolCallContent
|
|
413
|
+
| acp.schema.TerminalToolCallContent
|
|
414
|
+
):
|
|
392
415
|
if isinstance(part, TextPart):
|
|
393
416
|
return acp.schema.ContentToolCallContent(
|
|
394
417
|
type="content", content=acp.schema.TextContentBlock(type="text", text=part.text)
|
|
@@ -402,7 +425,13 @@ def _tool_result_to_acp_content(
|
|
|
402
425
|
),
|
|
403
426
|
)
|
|
404
427
|
|
|
405
|
-
content
|
|
428
|
+
content: list[
|
|
429
|
+
(
|
|
430
|
+
acp.schema.ContentToolCallContent
|
|
431
|
+
| acp.schema.FileEditToolCallContent
|
|
432
|
+
| acp.schema.TerminalToolCallContent
|
|
433
|
+
)
|
|
434
|
+
] = []
|
|
406
435
|
if isinstance(tool_result.output, str):
|
|
407
436
|
content.append(_to_acp_content(TextPart(text=tool_result.output)))
|
|
408
437
|
elif isinstance(tool_result.output, ContentPart):
|
|
@@ -428,7 +457,7 @@ class ACPServer:
|
|
|
428
457
|
|
|
429
458
|
# Create connection - the library handles all JSON-RPC details!
|
|
430
459
|
_ = acp.AgentSideConnection(
|
|
431
|
-
lambda conn:
|
|
460
|
+
lambda conn: ACPAgent(self.soul, conn),
|
|
432
461
|
writer,
|
|
433
462
|
reader,
|
|
434
463
|
)
|
kimi_cli/ui/print/__init__.py
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
-
import signal
|
|
4
3
|
import sys
|
|
5
4
|
from functools import partial
|
|
6
|
-
from
|
|
5
|
+
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
import aiofiles
|
|
9
|
-
from kosong.base.message import Message
|
|
10
8
|
from kosong.chat_provider import ChatProviderError
|
|
9
|
+
from kosong.message import Message
|
|
10
|
+
from rich import print
|
|
11
11
|
|
|
12
|
-
from kimi_cli.
|
|
13
|
-
from kimi_cli.soul
|
|
14
|
-
from kimi_cli.soul.wire import StepInterrupted, Wire
|
|
15
|
-
from kimi_cli.ui import RunCancelled, run_soul
|
|
12
|
+
from kimi_cli.cli import InputFormat, OutputFormat
|
|
13
|
+
from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
16
14
|
from kimi_cli.utils.logging import logger
|
|
17
15
|
from kimi_cli.utils.message import message_extract_text
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
from kimi_cli.utils.signals import install_sigint_handler
|
|
17
|
+
from kimi_cli.wire import WireUISide
|
|
18
|
+
from kimi_cli.wire.message import StepInterrupted
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
class PrintApp:
|
|
@@ -25,17 +23,23 @@ class PrintApp:
|
|
|
25
23
|
An app implementation that prints the agent behavior to the console.
|
|
26
24
|
|
|
27
25
|
Args:
|
|
28
|
-
soul (
|
|
26
|
+
soul (Soul): The soul to run.
|
|
29
27
|
input_format (InputFormat): The input format to use.
|
|
30
28
|
output_format (OutputFormat): The output format to use.
|
|
29
|
+
context_file (Path): The file to store the context.
|
|
31
30
|
"""
|
|
32
31
|
|
|
33
|
-
def __init__(
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
soul: Soul,
|
|
35
|
+
input_format: InputFormat,
|
|
36
|
+
output_format: OutputFormat,
|
|
37
|
+
context_file: Path,
|
|
38
|
+
):
|
|
34
39
|
self.soul = soul
|
|
35
40
|
self.input_format = input_format
|
|
36
41
|
self.output_format = output_format
|
|
37
|
-
self.
|
|
38
|
-
# TODO(approval): proper approval request handling
|
|
42
|
+
self.context_file = context_file
|
|
39
43
|
|
|
40
44
|
async def run(self, command: str | None = None) -> bool:
|
|
41
45
|
cancel_event = asyncio.Event()
|
|
@@ -45,7 +49,7 @@ class PrintApp:
|
|
|
45
49
|
cancel_event.set()
|
|
46
50
|
|
|
47
51
|
loop = asyncio.get_running_loop()
|
|
48
|
-
loop
|
|
52
|
+
remove_sigint = install_sigint_handler(loop, _handler)
|
|
49
53
|
|
|
50
54
|
if command is None and not sys.stdin.isatty() and self.input_format == "text":
|
|
51
55
|
command = sys.stdin.read().strip()
|
|
@@ -92,33 +96,9 @@ class PrintApp:
|
|
|
92
96
|
print(f"Unknown error: {e}")
|
|
93
97
|
raise
|
|
94
98
|
finally:
|
|
95
|
-
|
|
99
|
+
remove_sigint()
|
|
96
100
|
return False
|
|
97
101
|
|
|
98
|
-
# TODO: unify with `_soul_run` in `ShellApp` and `ACPAgentImpl`
|
|
99
|
-
async def _soul_run(self, user_input: str):
|
|
100
|
-
wire = Wire()
|
|
101
|
-
logger.debug("Starting visualization loop")
|
|
102
|
-
|
|
103
|
-
if self.output_format == "text":
|
|
104
|
-
vis_task = asyncio.create_task(self._visualize_text(wire))
|
|
105
|
-
else:
|
|
106
|
-
assert self.output_format == "stream-json"
|
|
107
|
-
if not self.soul._context._file_backend.exists():
|
|
108
|
-
self.soul._context._file_backend.touch()
|
|
109
|
-
start_position = self.soul._context._file_backend.stat().st_size
|
|
110
|
-
vis_task = asyncio.create_task(self._visualize_stream_json(wire, start_position))
|
|
111
|
-
|
|
112
|
-
try:
|
|
113
|
-
await self.soul.run(user_input, wire)
|
|
114
|
-
finally:
|
|
115
|
-
wire.shutdown()
|
|
116
|
-
# shutting down the event queue should break the visualization loop
|
|
117
|
-
try:
|
|
118
|
-
await asyncio.wait_for(vis_task, timeout=0.5)
|
|
119
|
-
except TimeoutError:
|
|
120
|
-
logger.warning("Visualization loop timed out")
|
|
121
|
-
|
|
122
102
|
def _read_next_command(self) -> str | None:
|
|
123
103
|
while True:
|
|
124
104
|
json_line = sys.stdin.readline()
|
|
@@ -144,33 +124,30 @@ class PrintApp:
|
|
|
144
124
|
except Exception:
|
|
145
125
|
logger.warning("Ignoring invalid user message: {json_line}", json_line=json_line)
|
|
146
126
|
|
|
147
|
-
async def _visualize_text(self, wire:
|
|
148
|
-
|
|
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)
|
|
149
141
|
while True:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
should_end = False
|
|
163
|
-
while wire._queue.qsize() > 0:
|
|
164
|
-
msg = wire._queue.get_nowait()
|
|
165
|
-
if isinstance(msg, StepInterrupted):
|
|
166
|
-
should_end = True
|
|
167
|
-
|
|
168
|
-
line = await f.readline()
|
|
169
|
-
if not line:
|
|
170
|
-
if should_end:
|
|
171
|
-
break
|
|
172
|
-
await asyncio.sleep(0.1)
|
|
173
|
-
continue
|
|
174
|
-
print(line, end="")
|
|
175
|
-
except asyncio.QueueShutDown:
|
|
176
|
-
logger.debug("Visualization loop shutting down")
|
|
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="")
|