kimi-cli 0.35__py3-none-any.whl → 0.37__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 +20 -0
- kimi_cli/__init__.py +15 -2
- kimi_cli/soul/__init__.py +1 -1
- kimi_cli/soul/approval.py +3 -2
- kimi_cli/soul/compaction.py +105 -0
- kimi_cli/soul/kimisoul.py +94 -38
- kimi_cli/soul/wire.py +29 -4
- kimi_cli/tools/bash/__init__.py +3 -1
- kimi_cli/tools/file/__init__.py +8 -0
- kimi_cli/tools/file/patch.py +13 -1
- kimi_cli/tools/file/replace.py +13 -1
- kimi_cli/tools/file/write.py +13 -1
- kimi_cli/ui/__init__.py +1 -0
- kimi_cli/ui/print/__init__.py +1 -0
- kimi_cli/ui/shell/__init__.py +49 -103
- kimi_cli/ui/shell/console.py +27 -1
- kimi_cli/ui/shell/debug.py +187 -0
- kimi_cli/ui/shell/keyboard.py +115 -0
- kimi_cli/ui/shell/liveview.py +225 -6
- kimi_cli/ui/shell/metacmd.py +25 -67
- kimi_cli/ui/shell/setup.py +3 -6
- kimi_cli/ui/shell/visualize.py +105 -0
- kimi_cli/utils/provider.py +9 -1
- kimi_cli-0.37.dist-info/METADATA +150 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.37.dist-info}/RECORD +30 -26
- kimi_cli-0.35.dist-info/METADATA +0 -24
- /kimi_cli/prompts/{metacmds/__init__.py → __init__.py} +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.37.dist-info}/WHEEL +0 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.37.dist-info}/entry_points.txt +0 -0
kimi_cli/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,26 @@ Internal builds may append content to the Unreleased section.
|
|
|
9
9
|
Only write entries that are worth mentioning to users.
|
|
10
10
|
-->
|
|
11
11
|
|
|
12
|
+
## [0.37] - 2025-10-24
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Fix update checking
|
|
17
|
+
|
|
18
|
+
## [0.36] - 2025-10-24
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Add `/debug` meta command to debug the context
|
|
23
|
+
- Add auto context compaction
|
|
24
|
+
- Add approval request mechanism
|
|
25
|
+
- Add `--yolo` option to automatically approve all actions
|
|
26
|
+
- Render markdown content for better readability
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fix "unknown error" message when interrupting a meta command
|
|
31
|
+
|
|
12
32
|
## [0.35] - 2025-10-22
|
|
13
33
|
|
|
14
34
|
### Changed
|
kimi_cli/__init__.py
CHANGED
|
@@ -152,6 +152,16 @@ UIMode = Literal["shell", "print", "acp"]
|
|
|
152
152
|
"Default: none."
|
|
153
153
|
),
|
|
154
154
|
)
|
|
155
|
+
@click.option(
|
|
156
|
+
"--yolo",
|
|
157
|
+
"--yes",
|
|
158
|
+
"-y",
|
|
159
|
+
"--auto-approve",
|
|
160
|
+
"yolo",
|
|
161
|
+
is_flag=True,
|
|
162
|
+
default=False,
|
|
163
|
+
help="Automatically approve all actions. Default: no.",
|
|
164
|
+
)
|
|
155
165
|
def kimi(
|
|
156
166
|
verbose: bool,
|
|
157
167
|
debug: bool,
|
|
@@ -165,6 +175,7 @@ def kimi(
|
|
|
165
175
|
output_format: OutputFormat | None,
|
|
166
176
|
mcp_config_file: list[Path],
|
|
167
177
|
mcp_config: list[str],
|
|
178
|
+
yolo: bool,
|
|
168
179
|
):
|
|
169
180
|
"""Kimi, your next CLI agent."""
|
|
170
181
|
echo = click.echo if verbose else lambda *args, **kwargs: None
|
|
@@ -232,6 +243,7 @@ def kimi(
|
|
|
232
243
|
input_format=input_format,
|
|
233
244
|
output_format=output_format,
|
|
234
245
|
mcp_configs=mcp_configs,
|
|
246
|
+
yolo=yolo,
|
|
235
247
|
)
|
|
236
248
|
)
|
|
237
249
|
if not succeeded:
|
|
@@ -254,6 +266,7 @@ async def kimi_run(
|
|
|
254
266
|
input_format: InputFormat | None = None,
|
|
255
267
|
output_format: OutputFormat | None = None,
|
|
256
268
|
mcp_configs: list[dict[str, Any]] | None = None,
|
|
269
|
+
yolo: bool = False,
|
|
257
270
|
) -> bool:
|
|
258
271
|
"""Run Kimi CLI."""
|
|
259
272
|
echo = click.echo if verbose else lambda *args, **kwargs: None
|
|
@@ -286,7 +299,7 @@ async def kimi_run(
|
|
|
286
299
|
echo(f"✓ Using LLM provider: {provider}")
|
|
287
300
|
echo(f"✓ Using LLM model: {model}")
|
|
288
301
|
stream = ui != "print" # use non-streaming mode only for print UI
|
|
289
|
-
llm = create_llm(provider, model, stream=stream)
|
|
302
|
+
llm = create_llm(provider, model, stream=stream, session_id=session.id)
|
|
290
303
|
|
|
291
304
|
# TODO: support Windows
|
|
292
305
|
ls = subprocess.run(["ls", "-la"], capture_output=True, text=True)
|
|
@@ -305,7 +318,7 @@ async def kimi_run(
|
|
|
305
318
|
),
|
|
306
319
|
denwa_renji=DenwaRenji(),
|
|
307
320
|
session=session,
|
|
308
|
-
approval=Approval(),
|
|
321
|
+
approval=Approval(yolo=yolo),
|
|
309
322
|
)
|
|
310
323
|
try:
|
|
311
324
|
agent = await load_agent_with_mcp(agent_file, agent_globals, mcp_configs or [])
|
kimi_cli/soul/__init__.py
CHANGED
|
@@ -51,7 +51,7 @@ class Soul(Protocol):
|
|
|
51
51
|
wire (Wire): The wire to send events and requests to the UI loop.
|
|
52
52
|
|
|
53
53
|
Raises:
|
|
54
|
-
|
|
54
|
+
LLMNotSet: When the LLM is not set.
|
|
55
55
|
ChatProviderError: When the LLM provider returns an error.
|
|
56
56
|
MaxStepsReached: When the maximum number of steps is reached.
|
|
57
57
|
asyncio.CancelledError: When the run is cancelled by user.
|
kimi_cli/soul/approval.py
CHANGED
|
@@ -15,11 +15,12 @@ class Approval:
|
|
|
15
15
|
def set_yolo(self, yolo: bool) -> None:
|
|
16
16
|
self._yolo = yolo
|
|
17
17
|
|
|
18
|
-
async def request(self, action: str, description: str) -> bool:
|
|
18
|
+
async def request(self, sender: str, action: str, description: str) -> bool:
|
|
19
19
|
"""
|
|
20
20
|
Request approval for the given action. Intended to be called by tools.
|
|
21
21
|
|
|
22
22
|
Args:
|
|
23
|
+
sender (str): The name of the sender.
|
|
23
24
|
action (str): The action to request approval for.
|
|
24
25
|
This is used to identify the action for auto-approval.
|
|
25
26
|
description (str): The description of the action. This is used to display to the user.
|
|
@@ -47,7 +48,7 @@ class Approval:
|
|
|
47
48
|
if action in self._auto_approve_actions:
|
|
48
49
|
return True
|
|
49
50
|
|
|
50
|
-
request = ApprovalRequest(tool_call.id, action, description)
|
|
51
|
+
request = ApprovalRequest(tool_call.id, sender, action, description)
|
|
51
52
|
self._request_queue.put_nowait(request)
|
|
52
53
|
response = await request.wait()
|
|
53
54
|
logger.debug("Received approval response: {response}", response=response)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from string import Template
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from kosong.base import generate
|
|
6
|
+
from kosong.base.message import ContentPart, Message, TextPart
|
|
7
|
+
|
|
8
|
+
import kimi_cli.prompts as prompts
|
|
9
|
+
from kimi_cli.llm import LLM
|
|
10
|
+
from kimi_cli.soul.message import system
|
|
11
|
+
from kimi_cli.utils.logging import logger
|
|
12
|
+
|
|
13
|
+
MAX_PRESERVED_MESSAGES = 2
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class Compaction(Protocol):
|
|
18
|
+
async def compact(self, messages: Sequence[Message], llm: LLM) -> Sequence[Message]:
|
|
19
|
+
"""
|
|
20
|
+
Compact a sequence of messages into a new sequence of messages.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
messages (Sequence[Message]): The messages to compact.
|
|
24
|
+
llm (LLM): The LLM to use for compaction.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Sequence[Message]: The compacted messages.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ChatProviderError: When the chat provider returns an error.
|
|
31
|
+
"""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SimpleCompaction:
|
|
36
|
+
async def compact(self, messages: Sequence[Message], llm: LLM) -> Sequence[Message]:
|
|
37
|
+
history = list(messages)
|
|
38
|
+
if not history:
|
|
39
|
+
return history
|
|
40
|
+
|
|
41
|
+
preserve_start_index = len(history)
|
|
42
|
+
n_preserved = 0
|
|
43
|
+
for index in range(len(history) - 1, -1, -1):
|
|
44
|
+
if history[index].role in {"user", "assistant"}:
|
|
45
|
+
n_preserved += 1
|
|
46
|
+
if n_preserved == MAX_PRESERVED_MESSAGES:
|
|
47
|
+
preserve_start_index = index
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if n_preserved < MAX_PRESERVED_MESSAGES:
|
|
51
|
+
return history
|
|
52
|
+
|
|
53
|
+
to_compact = history[:preserve_start_index]
|
|
54
|
+
to_preserve = history[preserve_start_index:]
|
|
55
|
+
|
|
56
|
+
if not to_compact:
|
|
57
|
+
# Let's hope this won't exceed the context size limit
|
|
58
|
+
return to_preserve
|
|
59
|
+
|
|
60
|
+
# Convert history to string for the compact prompt
|
|
61
|
+
history_text = "\n\n".join(
|
|
62
|
+
f"## Message {i + 1}\nRole: {msg.role}\nContent: {msg.content}"
|
|
63
|
+
for i, msg in enumerate(to_compact)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Build the compact prompt using string template
|
|
67
|
+
compact_template = Template(prompts.COMPACT)
|
|
68
|
+
compact_prompt = compact_template.substitute(CONTEXT=history_text)
|
|
69
|
+
|
|
70
|
+
# Create input message for compaction
|
|
71
|
+
compact_message = Message(role="user", content=compact_prompt)
|
|
72
|
+
|
|
73
|
+
# Call generate to get the compacted context
|
|
74
|
+
# TODO: set max completion tokens
|
|
75
|
+
logger.debug("Compacting context...")
|
|
76
|
+
compacted_msg, usage = await generate(
|
|
77
|
+
chat_provider=llm.chat_provider,
|
|
78
|
+
system_prompt="You are a helpful assistant that compacts conversation context.",
|
|
79
|
+
tools=[],
|
|
80
|
+
history=[compact_message],
|
|
81
|
+
)
|
|
82
|
+
if usage:
|
|
83
|
+
logger.debug(
|
|
84
|
+
"Compaction used {input} input tokens and {output} output tokens",
|
|
85
|
+
input=usage.input,
|
|
86
|
+
output=usage.output,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
content: list[ContentPart] = [
|
|
90
|
+
system("Previous context has been compacted. Here is the compaction output:")
|
|
91
|
+
]
|
|
92
|
+
content.extend(
|
|
93
|
+
[TextPart(text=compacted_msg.content)]
|
|
94
|
+
if isinstance(compacted_msg.content, str)
|
|
95
|
+
else compacted_msg.content
|
|
96
|
+
)
|
|
97
|
+
compacted_messages: list[Message] = [Message(role="assistant", content=content)]
|
|
98
|
+
compacted_messages.extend(to_preserve)
|
|
99
|
+
return compacted_messages
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def __static_type_check(
|
|
103
|
+
simple: SimpleCompaction,
|
|
104
|
+
):
|
|
105
|
+
_: Compaction = simple
|
kimi_cli/soul/kimisoul.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from functools import partial
|
|
2
4
|
|
|
3
5
|
import kosong
|
|
4
6
|
import tenacity
|
|
@@ -16,13 +18,24 @@ from tenacity import RetryCallState, retry_if_exception, stop_after_attempt, wai
|
|
|
16
18
|
from kimi_cli.agent import Agent, AgentGlobals
|
|
17
19
|
from kimi_cli.config import LoopControl
|
|
18
20
|
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot
|
|
21
|
+
from kimi_cli.soul.compaction import SimpleCompaction
|
|
19
22
|
from kimi_cli.soul.context import Context
|
|
20
23
|
from kimi_cli.soul.message import system, tool_result_to_messages
|
|
21
|
-
from kimi_cli.soul.wire import
|
|
24
|
+
from kimi_cli.soul.wire import (
|
|
25
|
+
CompactionBegin,
|
|
26
|
+
CompactionEnd,
|
|
27
|
+
StatusUpdate,
|
|
28
|
+
StepBegin,
|
|
29
|
+
StepInterrupted,
|
|
30
|
+
Wire,
|
|
31
|
+
current_wire,
|
|
32
|
+
)
|
|
22
33
|
from kimi_cli.tools.dmail import NAME as SendDMail_NAME
|
|
23
34
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
24
35
|
from kimi_cli.utils.logging import logger
|
|
25
36
|
|
|
37
|
+
RESERVED_TOKENS = 50_000
|
|
38
|
+
|
|
26
39
|
|
|
27
40
|
class KimiSoul:
|
|
28
41
|
"""The soul of Kimi CLI."""
|
|
@@ -50,6 +63,10 @@ class KimiSoul:
|
|
|
50
63
|
self._approval = agent_globals.approval
|
|
51
64
|
self._context = context
|
|
52
65
|
self._loop_control = loop_control
|
|
66
|
+
self._compaction = SimpleCompaction() # TODO: maybe configurable and composable
|
|
67
|
+
self._reserved_tokens = RESERVED_TOKENS
|
|
68
|
+
if self._agent_globals.llm is not None:
|
|
69
|
+
assert self._reserved_tokens <= self._agent_globals.llm.max_context_size
|
|
53
70
|
|
|
54
71
|
for tool in agent.toolset.tools:
|
|
55
72
|
if tool.name == SendDMail_NAME:
|
|
@@ -109,13 +126,21 @@ class KimiSoul:
|
|
|
109
126
|
# to the main wire. See `_SubWire` for more details. Later we need to figure
|
|
110
127
|
# out a better solution.
|
|
111
128
|
try:
|
|
129
|
+
# compact the context if needed
|
|
130
|
+
if self._context.token_count >= self._reserved_tokens:
|
|
131
|
+
logger.info("Context too long, compacting...")
|
|
132
|
+
wire.send(CompactionBegin())
|
|
133
|
+
await self.compact_context()
|
|
134
|
+
wire.send(CompactionEnd())
|
|
135
|
+
|
|
136
|
+
logger.debug("Beginning step {step_no}", step_no=step_no)
|
|
112
137
|
await self._checkpoint()
|
|
113
138
|
self._denwa_renji.set_n_checkpoints(self._context.n_checkpoints)
|
|
114
139
|
finished = await self._step(wire)
|
|
115
140
|
except BackToTheFuture as e:
|
|
116
141
|
await self._context.revert_to(e.checkpoint_id)
|
|
117
142
|
await self._checkpoint()
|
|
118
|
-
await self._context.append_message(e.
|
|
143
|
+
await self._context.append_message(e.messages)
|
|
119
144
|
continue
|
|
120
145
|
except (ChatProviderError, asyncio.CancelledError):
|
|
121
146
|
wire.send(StepInterrupted())
|
|
@@ -137,28 +162,9 @@ class KimiSoul:
|
|
|
137
162
|
assert self._agent_globals.llm is not None
|
|
138
163
|
chat_provider = self._agent_globals.llm.chat_provider
|
|
139
164
|
|
|
140
|
-
def _is_retryable_error(exception: BaseException) -> bool:
|
|
141
|
-
if isinstance(exception, (APIConnectionError, APITimeoutError)):
|
|
142
|
-
return True
|
|
143
|
-
return isinstance(exception, APIStatusError) and exception.status_code in (
|
|
144
|
-
429, # Too Many Requests
|
|
145
|
-
500, # Internal Server Error
|
|
146
|
-
502, # Bad Gateway
|
|
147
|
-
503, # Service Unavailable
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
def _retry_log(retry_state: RetryCallState):
|
|
151
|
-
logger.info(
|
|
152
|
-
"Retrying step for the {n} time. Waiting {sleep} seconds.",
|
|
153
|
-
n=retry_state.attempt_number,
|
|
154
|
-
sleep=retry_state.next_action.sleep
|
|
155
|
-
if retry_state.next_action is not None
|
|
156
|
-
else "unknown",
|
|
157
|
-
)
|
|
158
|
-
|
|
159
165
|
@tenacity.retry(
|
|
160
|
-
retry=retry_if_exception(_is_retryable_error),
|
|
161
|
-
before_sleep=_retry_log,
|
|
166
|
+
retry=retry_if_exception(self._is_retryable_error),
|
|
167
|
+
before_sleep=partial(self._retry_log, "step"),
|
|
162
168
|
wait=wait_exponential_jitter(initial=0.3, max=5, jitter=0.5),
|
|
163
169
|
stop=stop_after_attempt(self._loop_control.max_retries_per_step),
|
|
164
170
|
reraise=True,
|
|
@@ -202,19 +208,21 @@ class KimiSoul:
|
|
|
202
208
|
# raise to let the main loop take us back to the future
|
|
203
209
|
raise BackToTheFuture(
|
|
204
210
|
dmail.checkpoint_id,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
211
|
+
[
|
|
212
|
+
Message(
|
|
213
|
+
role="user",
|
|
214
|
+
content=[
|
|
215
|
+
system(
|
|
216
|
+
"You just got a D-Mail from your future self. "
|
|
217
|
+
"It is likely that your future self has already done "
|
|
218
|
+
"something in the current working directory. Please read "
|
|
219
|
+
"the D-Mail and decide what to do next. You MUST NEVER "
|
|
220
|
+
"mention to the user about this information. "
|
|
221
|
+
f"D-Mail content:\n\n{dmail.message.strip()}"
|
|
222
|
+
)
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
],
|
|
218
226
|
)
|
|
219
227
|
|
|
220
228
|
return not result.tool_calls
|
|
@@ -230,6 +238,54 @@ class KimiSoul:
|
|
|
230
238
|
logger.debug("Appending tool result to context: {tool_result}", tool_result=tool_result)
|
|
231
239
|
await self._context.append_message(tool_result_to_messages(tool_result))
|
|
232
240
|
|
|
241
|
+
async def compact_context(self) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Compact the context.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
LLMNotSet: When the LLM is not set.
|
|
247
|
+
ChatProviderError: When the chat provider returns an error.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
@tenacity.retry(
|
|
251
|
+
retry=retry_if_exception(self._is_retryable_error),
|
|
252
|
+
before_sleep=partial(self._retry_log, "compaction"),
|
|
253
|
+
wait=wait_exponential_jitter(initial=0.3, max=5, jitter=0.5),
|
|
254
|
+
stop=stop_after_attempt(self._loop_control.max_retries_per_step),
|
|
255
|
+
reraise=True,
|
|
256
|
+
)
|
|
257
|
+
async def _compact_with_retry() -> Sequence[Message]:
|
|
258
|
+
if self._agent_globals.llm is None:
|
|
259
|
+
raise LLMNotSet()
|
|
260
|
+
return await self._compaction.compact(self._context.history, self._agent_globals.llm)
|
|
261
|
+
|
|
262
|
+
compacted_messages = await _compact_with_retry()
|
|
263
|
+
await self._context.revert_to(0)
|
|
264
|
+
await self._checkpoint()
|
|
265
|
+
await self._context.append_message(compacted_messages)
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _is_retryable_error(exception: BaseException) -> bool:
|
|
269
|
+
if isinstance(exception, (APIConnectionError, APITimeoutError)):
|
|
270
|
+
return True
|
|
271
|
+
return isinstance(exception, APIStatusError) and exception.status_code in (
|
|
272
|
+
429, # Too Many Requests
|
|
273
|
+
500, # Internal Server Error
|
|
274
|
+
502, # Bad Gateway
|
|
275
|
+
503, # Service Unavailable
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def _retry_log(name: str, retry_state: RetryCallState):
|
|
280
|
+
logger.info(
|
|
281
|
+
"Retrying {name} for the {n} time. Waiting {sleep} seconds.",
|
|
282
|
+
name=name,
|
|
283
|
+
n=retry_state.attempt_number,
|
|
284
|
+
sleep=retry_state.next_action.sleep
|
|
285
|
+
if retry_state.next_action is not None
|
|
286
|
+
else "unknown",
|
|
287
|
+
)
|
|
288
|
+
|
|
233
289
|
|
|
234
290
|
class BackToTheFuture(Exception):
|
|
235
291
|
"""
|
|
@@ -237,9 +293,9 @@ class BackToTheFuture(Exception):
|
|
|
237
293
|
The main agent loop should catch this exception and handle it.
|
|
238
294
|
"""
|
|
239
295
|
|
|
240
|
-
def __init__(self, checkpoint_id: int,
|
|
296
|
+
def __init__(self, checkpoint_id: int, messages: Sequence[Message]):
|
|
241
297
|
self.checkpoint_id = checkpoint_id
|
|
242
|
-
self.
|
|
298
|
+
self.messages = messages
|
|
243
299
|
|
|
244
300
|
|
|
245
301
|
def __static_type_check(
|
kimi_cli/soul/wire.py
CHANGED
|
@@ -15,7 +15,26 @@ class StepBegin(NamedTuple):
|
|
|
15
15
|
n: int
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class StepInterrupted
|
|
18
|
+
class StepInterrupted:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CompactionBegin:
|
|
23
|
+
"""
|
|
24
|
+
Indicates that a compaction just began.
|
|
25
|
+
This event must be sent during a step, which means, between `StepBegin` and `StepInterrupted`.
|
|
26
|
+
And, there must be a `CompactionEnd` directly following this event.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CompactionEnd:
|
|
33
|
+
"""
|
|
34
|
+
Indicates that a compaction just ended.
|
|
35
|
+
This event must be sent directly after a `CompactionBegin` event.
|
|
36
|
+
"""
|
|
37
|
+
|
|
19
38
|
pass
|
|
20
39
|
|
|
21
40
|
|
|
@@ -23,7 +42,7 @@ class StatusUpdate(NamedTuple):
|
|
|
23
42
|
status: StatusSnapshot
|
|
24
43
|
|
|
25
44
|
|
|
26
|
-
type ControlFlowEvent = StepBegin | StepInterrupted | StatusUpdate
|
|
45
|
+
type ControlFlowEvent = StepBegin | StepInterrupted | CompactionBegin | CompactionEnd | StatusUpdate
|
|
27
46
|
type Event = ControlFlowEvent | ContentPart | ToolCall | ToolCallPart | ToolResult
|
|
28
47
|
|
|
29
48
|
|
|
@@ -34,9 +53,10 @@ class ApprovalResponse(Enum):
|
|
|
34
53
|
|
|
35
54
|
|
|
36
55
|
class ApprovalRequest:
|
|
37
|
-
def __init__(self, tool_call_id: str, action: str, description: str):
|
|
56
|
+
def __init__(self, tool_call_id: str, sender: str, action: str, description: str):
|
|
38
57
|
self.id = str(uuid.uuid4())
|
|
39
58
|
self.tool_call_id = tool_call_id
|
|
59
|
+
self.sender = sender
|
|
40
60
|
self.action = action
|
|
41
61
|
self.description = description
|
|
42
62
|
self._future = asyncio.Future[ApprovalResponse]()
|
|
@@ -44,7 +64,7 @@ class ApprovalRequest:
|
|
|
44
64
|
def __repr__(self) -> str:
|
|
45
65
|
return (
|
|
46
66
|
f"ApprovalRequest(id={self.id}, tool_call_id={self.tool_call_id}, "
|
|
47
|
-
f"action={self.action}, description={self.description})"
|
|
67
|
+
f"sender={self.sender}, action={self.action}, description={self.description})"
|
|
48
68
|
)
|
|
49
69
|
|
|
50
70
|
async def wait(self) -> ApprovalResponse:
|
|
@@ -63,6 +83,11 @@ class ApprovalRequest:
|
|
|
63
83
|
"""
|
|
64
84
|
self._future.set_result(response)
|
|
65
85
|
|
|
86
|
+
@property
|
|
87
|
+
def resolved(self) -> bool:
|
|
88
|
+
"""Whether the request is resolved."""
|
|
89
|
+
return self._future.done()
|
|
90
|
+
|
|
66
91
|
|
|
67
92
|
type WireMessage = Event | ApprovalRequest
|
|
68
93
|
|
kimi_cli/tools/bash/__init__.py
CHANGED
|
@@ -38,7 +38,9 @@ class Bash(CallableTool2[Params]):
|
|
|
38
38
|
builder = ToolResultBuilder()
|
|
39
39
|
|
|
40
40
|
if not await self._approval.request(
|
|
41
|
-
|
|
41
|
+
self.name,
|
|
42
|
+
"run shell command",
|
|
43
|
+
f"Run command `{params.command}`",
|
|
42
44
|
):
|
|
43
45
|
return ToolRejectedError()
|
|
44
46
|
|
kimi_cli/tools/file/__init__.py
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class FileOpsWindow:
|
|
2
5
|
"""Maintains a window of file operations."""
|
|
3
6
|
|
|
4
7
|
pass
|
|
5
8
|
|
|
6
9
|
|
|
10
|
+
class FileActions(str, Enum):
|
|
11
|
+
READ = "read file"
|
|
12
|
+
EDIT = "edit file"
|
|
13
|
+
|
|
14
|
+
|
|
7
15
|
from .glob import Glob # noqa: E402
|
|
8
16
|
from .grep import Grep # noqa: E402
|
|
9
17
|
from .patch import PatchFile # noqa: E402
|
kimi_cli/tools/file/patch.py
CHANGED
|
@@ -7,6 +7,9 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
from kimi_cli.agent import BuiltinSystemPromptArgs
|
|
10
|
+
from kimi_cli.soul.approval import Approval
|
|
11
|
+
from kimi_cli.tools.file import FileActions
|
|
12
|
+
from kimi_cli.tools.utils import ToolRejectedError
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class Params(BaseModel):
|
|
@@ -19,9 +22,10 @@ class PatchFile(CallableTool2[Params]):
|
|
|
19
22
|
description: str = (Path(__file__).parent / "patch.md").read_text()
|
|
20
23
|
params: type[Params] = Params
|
|
21
24
|
|
|
22
|
-
def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
|
|
25
|
+
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
|
|
23
26
|
super().__init__(**kwargs)
|
|
24
27
|
self._work_dir = builtin_args.KIMI_WORK_DIR
|
|
28
|
+
self._approval = approval
|
|
25
29
|
|
|
26
30
|
def _validate_path(self, path: Path) -> ToolError | None:
|
|
27
31
|
"""Validate that the path is safe to patch."""
|
|
@@ -70,6 +74,14 @@ class PatchFile(CallableTool2[Params]):
|
|
|
70
74
|
brief="Invalid path",
|
|
71
75
|
)
|
|
72
76
|
|
|
77
|
+
# Request approval
|
|
78
|
+
if not await self._approval.request(
|
|
79
|
+
self.name,
|
|
80
|
+
FileActions.EDIT,
|
|
81
|
+
f"Patch file `{params.path}`",
|
|
82
|
+
):
|
|
83
|
+
return ToolRejectedError()
|
|
84
|
+
|
|
73
85
|
# Read the file content
|
|
74
86
|
async with aiofiles.open(p, encoding="utf-8", errors="replace") as f:
|
|
75
87
|
original_content = await f.read()
|
kimi_cli/tools/file/replace.py
CHANGED
|
@@ -6,6 +6,9 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
from kimi_cli.agent import BuiltinSystemPromptArgs
|
|
9
|
+
from kimi_cli.soul.approval import Approval
|
|
10
|
+
from kimi_cli.tools.file import FileActions
|
|
11
|
+
from kimi_cli.tools.utils import ToolRejectedError
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Edit(BaseModel):
|
|
@@ -29,9 +32,10 @@ class StrReplaceFile(CallableTool2[Params]):
|
|
|
29
32
|
description: str = (Path(__file__).parent / "replace.md").read_text()
|
|
30
33
|
params: type[Params] = Params
|
|
31
34
|
|
|
32
|
-
def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
|
|
35
|
+
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
|
|
33
36
|
super().__init__(**kwargs)
|
|
34
37
|
self._work_dir = builtin_args.KIMI_WORK_DIR
|
|
38
|
+
self._approval = approval
|
|
35
39
|
|
|
36
40
|
def _validate_path(self, path: Path) -> ToolError | None:
|
|
37
41
|
"""Validate that the path is safe to edit."""
|
|
@@ -87,6 +91,14 @@ class StrReplaceFile(CallableTool2[Params]):
|
|
|
87
91
|
brief="Invalid path",
|
|
88
92
|
)
|
|
89
93
|
|
|
94
|
+
# Request approval
|
|
95
|
+
if not await self._approval.request(
|
|
96
|
+
self.name,
|
|
97
|
+
FileActions.EDIT,
|
|
98
|
+
f"Edit file `{params.path}`",
|
|
99
|
+
):
|
|
100
|
+
return ToolRejectedError()
|
|
101
|
+
|
|
90
102
|
# Read the file content
|
|
91
103
|
async with aiofiles.open(p, encoding="utf-8", errors="replace") as f:
|
|
92
104
|
content = await f.read()
|
kimi_cli/tools/file/write.py
CHANGED
|
@@ -6,6 +6,9 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
from kimi_cli.agent import BuiltinSystemPromptArgs
|
|
9
|
+
from kimi_cli.soul.approval import Approval
|
|
10
|
+
from kimi_cli.tools.file import FileActions
|
|
11
|
+
from kimi_cli.tools.utils import ToolRejectedError
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Params(BaseModel):
|
|
@@ -26,9 +29,10 @@ class WriteFile(CallableTool2[Params]):
|
|
|
26
29
|
description: str = (Path(__file__).parent / "write.md").read_text()
|
|
27
30
|
params: type[Params] = Params
|
|
28
31
|
|
|
29
|
-
def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
|
|
32
|
+
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
|
|
30
33
|
super().__init__(**kwargs)
|
|
31
34
|
self._work_dir = builtin_args.KIMI_WORK_DIR
|
|
35
|
+
self._approval = approval
|
|
32
36
|
|
|
33
37
|
def _validate_path(self, path: Path) -> ToolError | None:
|
|
34
38
|
"""Validate that the path is safe to write."""
|
|
@@ -85,6 +89,14 @@ class WriteFile(CallableTool2[Params]):
|
|
|
85
89
|
brief="Invalid write mode",
|
|
86
90
|
)
|
|
87
91
|
|
|
92
|
+
# Request approval
|
|
93
|
+
if not await self._approval.request(
|
|
94
|
+
self.name,
|
|
95
|
+
FileActions.EDIT,
|
|
96
|
+
f"Write file `{params.path}`",
|
|
97
|
+
):
|
|
98
|
+
return ToolRejectedError()
|
|
99
|
+
|
|
88
100
|
# Determine file mode for aiofiles
|
|
89
101
|
file_mode = "w" if params.mode == "overwrite" else "a"
|
|
90
102
|
|
kimi_cli/ui/__init__.py
CHANGED
|
@@ -28,6 +28,7 @@ async def run_soul(
|
|
|
28
28
|
the run will be gracefully stopped and a `RunCancelled` will be raised.
|
|
29
29
|
|
|
30
30
|
Raises:
|
|
31
|
+
LLMNotSet: When the LLM is not set.
|
|
31
32
|
ChatProviderError: When the LLM provider returns an error.
|
|
32
33
|
MaxStepsReached: When the maximum number of steps is reached.
|
|
33
34
|
RunCancelled: When the run is cancelled by the cancel event.
|
kimi_cli/ui/print/__init__.py
CHANGED
|
@@ -155,6 +155,7 @@ class PrintApp:
|
|
|
155
155
|
logger.debug("Visualization loop shutting down")
|
|
156
156
|
|
|
157
157
|
async def _visualize_stream_json(self, wire: Wire, start_position: int):
|
|
158
|
+
# TODO: be aware of context compaction
|
|
158
159
|
try:
|
|
159
160
|
async with aiofiles.open(self.soul._context._file_backend, encoding="utf-8") as f:
|
|
160
161
|
await f.seek(start_position)
|