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/soul/approval.py
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
3
|
from kimi_cli.soul.toolset import get_current_tool_call_or_none
|
|
4
|
-
from kimi_cli.soul.wire import ApprovalRequest, ApprovalResponse
|
|
5
4
|
from kimi_cli.utils.logging import logger
|
|
5
|
+
from kimi_cli.wire.message import ApprovalRequest, ApprovalResponse
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Approval:
|
|
9
9
|
def __init__(self, yolo: bool = False):
|
|
10
10
|
self._request_queue = asyncio.Queue[ApprovalRequest]()
|
|
11
11
|
self._yolo = yolo
|
|
12
|
-
self._auto_approve_actions = set() # TODO: persist across sessions
|
|
12
|
+
self._auto_approve_actions: set[str] = set() # TODO: persist across sessions
|
|
13
13
|
"""Set of action names that should automatically be approved."""
|
|
14
14
|
|
|
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)
|
|
@@ -59,8 +60,6 @@ class Approval:
|
|
|
59
60
|
return True
|
|
60
61
|
case ApprovalResponse.REJECT:
|
|
61
62
|
return False
|
|
62
|
-
case _:
|
|
63
|
-
raise ValueError(f"Unknown approval response: {response}")
|
|
64
63
|
|
|
65
64
|
async def fetch_request(self) -> ApprovalRequest:
|
|
66
65
|
"""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from string import Template
|
|
3
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from kosong import generate
|
|
6
|
+
from kosong.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
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class Compaction(Protocol):
|
|
16
|
+
async def compact(self, messages: Sequence[Message], llm: LLM) -> Sequence[Message]:
|
|
17
|
+
"""
|
|
18
|
+
Compact a sequence of messages into a new sequence of messages.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
messages (Sequence[Message]): The messages to compact.
|
|
22
|
+
llm (LLM): The LLM to use for compaction.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Sequence[Message]: The compacted messages.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ChatProviderError: When the chat provider returns an error.
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SimpleCompaction(Compaction):
|
|
34
|
+
MAX_PRESERVED_MESSAGES = 2
|
|
35
|
+
|
|
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 == self.MAX_PRESERVED_MESSAGES:
|
|
47
|
+
preserve_start_index = index
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if n_preserved < self.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
|
+
result = 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 result.usage:
|
|
83
|
+
logger.debug(
|
|
84
|
+
"Compaction used {input} input tokens and {output} output tokens",
|
|
85
|
+
input=result.usage.input,
|
|
86
|
+
output=result.usage.output,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
content: list[ContentPart] = [
|
|
90
|
+
system("Previous context has been compacted. Here is the compaction output:")
|
|
91
|
+
]
|
|
92
|
+
compacted_msg = result.message
|
|
93
|
+
content.extend(
|
|
94
|
+
[TextPart(text=compacted_msg.content)]
|
|
95
|
+
if isinstance(compacted_msg.content, str)
|
|
96
|
+
else compacted_msg.content
|
|
97
|
+
)
|
|
98
|
+
compacted_messages: list[Message] = [Message(role="assistant", content=content)]
|
|
99
|
+
compacted_messages.extend(to_preserve)
|
|
100
|
+
return compacted_messages
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if TYPE_CHECKING:
|
|
104
|
+
|
|
105
|
+
def type_check(simple: SimpleCompaction):
|
|
106
|
+
_: Compaction = simple
|
kimi_cli/soul/context.py
CHANGED
kimi_cli/soul/kimisoul.py
CHANGED
|
@@ -1,55 +1,86 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
6
|
import kosong
|
|
4
7
|
import tenacity
|
|
5
8
|
from kosong import StepResult
|
|
6
|
-
from kosong.base.message import Message
|
|
7
9
|
from kosong.chat_provider import (
|
|
8
10
|
APIConnectionError,
|
|
11
|
+
APIEmptyResponseError,
|
|
9
12
|
APIStatusError,
|
|
10
13
|
APITimeoutError,
|
|
11
14
|
ChatProviderError,
|
|
15
|
+
ThinkingEffort,
|
|
12
16
|
)
|
|
17
|
+
from kosong.message import ContentPart, ImageURLPart, Message
|
|
13
18
|
from kosong.tooling import ToolResult
|
|
14
19
|
from tenacity import RetryCallState, retry_if_exception, stop_after_attempt, wait_exponential_jitter
|
|
15
20
|
|
|
16
|
-
from kimi_cli.
|
|
17
|
-
from kimi_cli.
|
|
18
|
-
|
|
21
|
+
from kimi_cli.llm import ModelCapability
|
|
22
|
+
from kimi_cli.soul import (
|
|
23
|
+
LLMNotSet,
|
|
24
|
+
LLMNotSupported,
|
|
25
|
+
MaxStepsReached,
|
|
26
|
+
Soul,
|
|
27
|
+
StatusSnapshot,
|
|
28
|
+
wire_send,
|
|
29
|
+
)
|
|
30
|
+
from kimi_cli.soul.agent import Agent
|
|
31
|
+
from kimi_cli.soul.compaction import SimpleCompaction
|
|
19
32
|
from kimi_cli.soul.context import Context
|
|
20
33
|
from kimi_cli.soul.message import system, tool_result_to_messages
|
|
21
|
-
from kimi_cli.soul.
|
|
34
|
+
from kimi_cli.soul.runtime import Runtime
|
|
22
35
|
from kimi_cli.tools.dmail import NAME as SendDMail_NAME
|
|
23
36
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
24
37
|
from kimi_cli.utils.logging import logger
|
|
38
|
+
from kimi_cli.wire.message import (
|
|
39
|
+
CompactionBegin,
|
|
40
|
+
CompactionEnd,
|
|
41
|
+
StatusUpdate,
|
|
42
|
+
StepBegin,
|
|
43
|
+
StepInterrupted,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
|
|
48
|
+
def type_check(soul: "KimiSoul"):
|
|
49
|
+
_: Soul = soul
|
|
50
|
+
|
|
25
51
|
|
|
52
|
+
RESERVED_TOKENS = 50_000
|
|
26
53
|
|
|
27
|
-
|
|
54
|
+
|
|
55
|
+
class KimiSoul(Soul):
|
|
28
56
|
"""The soul of Kimi CLI."""
|
|
29
57
|
|
|
30
58
|
def __init__(
|
|
31
59
|
self,
|
|
32
60
|
agent: Agent,
|
|
33
|
-
|
|
61
|
+
runtime: Runtime,
|
|
34
62
|
*,
|
|
35
63
|
context: Context,
|
|
36
|
-
loop_control: LoopControl,
|
|
37
64
|
):
|
|
38
65
|
"""
|
|
39
66
|
Initialize the soul.
|
|
40
67
|
|
|
41
68
|
Args:
|
|
42
69
|
agent (Agent): The agent to run.
|
|
43
|
-
|
|
70
|
+
runtime (Runtime): Runtime parameters and states.
|
|
44
71
|
context (Context): The context of the agent.
|
|
45
|
-
loop_control (LoopControl): The control parameters for the agent loop.
|
|
46
72
|
"""
|
|
47
73
|
self._agent = agent
|
|
48
|
-
self.
|
|
49
|
-
self._denwa_renji =
|
|
50
|
-
self._approval =
|
|
74
|
+
self._runtime = runtime
|
|
75
|
+
self._denwa_renji = runtime.denwa_renji
|
|
76
|
+
self._approval = runtime.approval
|
|
51
77
|
self._context = context
|
|
52
|
-
self._loop_control = loop_control
|
|
78
|
+
self._loop_control = runtime.config.loop_control
|
|
79
|
+
self._compaction = SimpleCompaction() # TODO: maybe configurable and composable
|
|
80
|
+
self._reserved_tokens = RESERVED_TOKENS
|
|
81
|
+
if self._runtime.llm is not None:
|
|
82
|
+
assert self._reserved_tokens <= self._runtime.llm.max_context_size
|
|
83
|
+
self._thinking_effort: ThinkingEffort = "off"
|
|
53
84
|
|
|
54
85
|
for tool in agent.toolset.tools:
|
|
55
86
|
if tool.name == SendDMail_NAME:
|
|
@@ -63,62 +94,106 @@ class KimiSoul:
|
|
|
63
94
|
return self._agent.name
|
|
64
95
|
|
|
65
96
|
@property
|
|
66
|
-
def
|
|
67
|
-
return self.
|
|
97
|
+
def model_name(self) -> str:
|
|
98
|
+
return self._runtime.llm.chat_provider.model_name if self._runtime.llm else ""
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def model_capabilities(self) -> set[ModelCapability] | None:
|
|
102
|
+
if self._runtime.llm is None:
|
|
103
|
+
return None
|
|
104
|
+
return self._runtime.llm.capabilities
|
|
68
105
|
|
|
69
106
|
@property
|
|
70
107
|
def status(self) -> StatusSnapshot:
|
|
71
108
|
return StatusSnapshot(context_usage=self._context_usage)
|
|
72
109
|
|
|
110
|
+
@property
|
|
111
|
+
def context(self) -> Context:
|
|
112
|
+
return self._context
|
|
113
|
+
|
|
73
114
|
@property
|
|
74
115
|
def _context_usage(self) -> float:
|
|
75
|
-
if self.
|
|
76
|
-
return self._context.token_count / self.
|
|
116
|
+
if self._runtime.llm is not None:
|
|
117
|
+
return self._context.token_count / self._runtime.llm.max_context_size
|
|
77
118
|
return 0.0
|
|
78
119
|
|
|
120
|
+
@property
|
|
121
|
+
def thinking(self) -> bool:
|
|
122
|
+
"""Whether thinking mode is enabled."""
|
|
123
|
+
return self._thinking_effort != "off"
|
|
124
|
+
|
|
125
|
+
def set_thinking(self, enabled: bool) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Enable/disable thinking mode for the soul.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
LLMNotSet: When the LLM is not set.
|
|
131
|
+
LLMNotSupported: When the LLM does not support thinking mode.
|
|
132
|
+
"""
|
|
133
|
+
if self._runtime.llm is None:
|
|
134
|
+
raise LLMNotSet()
|
|
135
|
+
if enabled and "thinking" not in self._runtime.llm.capabilities:
|
|
136
|
+
raise LLMNotSupported(self._runtime.llm, ["thinking"])
|
|
137
|
+
self._thinking_effort = "high" if enabled else "off"
|
|
138
|
+
|
|
79
139
|
async def _checkpoint(self):
|
|
80
140
|
await self._context.checkpoint(self._checkpoint_with_user_message)
|
|
81
141
|
|
|
82
|
-
async def run(self, user_input: str
|
|
83
|
-
if self.
|
|
142
|
+
async def run(self, user_input: str | list[ContentPart]):
|
|
143
|
+
if self._runtime.llm is None:
|
|
84
144
|
raise LLMNotSet()
|
|
85
145
|
|
|
146
|
+
if (
|
|
147
|
+
isinstance(user_input, list)
|
|
148
|
+
and any(isinstance(part, ImageURLPart) for part in user_input)
|
|
149
|
+
and "image_in" not in self._runtime.llm.capabilities
|
|
150
|
+
):
|
|
151
|
+
raise LLMNotSupported(self._runtime.llm, ["image_in"])
|
|
152
|
+
|
|
86
153
|
await self._checkpoint() # this creates the checkpoint 0 on first run
|
|
87
154
|
await self._context.append_message(Message(role="user", content=user_input))
|
|
88
155
|
logger.debug("Appended user message to context")
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
await self._agent_loop(wire)
|
|
92
|
-
finally:
|
|
93
|
-
current_wire.reset(wire_token)
|
|
156
|
+
await self._agent_loop()
|
|
94
157
|
|
|
95
|
-
async def _agent_loop(self
|
|
158
|
+
async def _agent_loop(self):
|
|
96
159
|
"""The main agent loop for one run."""
|
|
160
|
+
assert self._runtime.llm is not None
|
|
97
161
|
|
|
98
162
|
async def _pipe_approval_to_wire():
|
|
99
163
|
while True:
|
|
100
164
|
request = await self._approval.fetch_request()
|
|
101
|
-
|
|
165
|
+
wire_send(request)
|
|
102
166
|
|
|
103
167
|
step_no = 1
|
|
104
168
|
while True:
|
|
105
|
-
|
|
169
|
+
wire_send(StepBegin(step_no))
|
|
106
170
|
approval_task = asyncio.create_task(_pipe_approval_to_wire())
|
|
107
171
|
# FIXME: It's possible that a subagent's approval task steals approval request
|
|
108
172
|
# from the main agent. We must ensure that the Task tool will redirect them
|
|
109
173
|
# to the main wire. See `_SubWire` for more details. Later we need to figure
|
|
110
174
|
# out a better solution.
|
|
111
175
|
try:
|
|
176
|
+
# compact the context if needed
|
|
177
|
+
if (
|
|
178
|
+
self._context.token_count + self._reserved_tokens
|
|
179
|
+
>= self._runtime.llm.max_context_size
|
|
180
|
+
):
|
|
181
|
+
logger.info("Context too long, compacting...")
|
|
182
|
+
wire_send(CompactionBegin())
|
|
183
|
+
await self.compact_context()
|
|
184
|
+
wire_send(CompactionEnd())
|
|
185
|
+
|
|
186
|
+
logger.debug("Beginning step {step_no}", step_no=step_no)
|
|
112
187
|
await self._checkpoint()
|
|
113
188
|
self._denwa_renji.set_n_checkpoints(self._context.n_checkpoints)
|
|
114
|
-
finished = await self._step(
|
|
189
|
+
finished = await self._step()
|
|
115
190
|
except BackToTheFuture as e:
|
|
116
191
|
await self._context.revert_to(e.checkpoint_id)
|
|
117
192
|
await self._checkpoint()
|
|
118
|
-
await self._context.append_message(e.
|
|
193
|
+
await self._context.append_message(e.messages)
|
|
119
194
|
continue
|
|
120
195
|
except (ChatProviderError, asyncio.CancelledError):
|
|
121
|
-
|
|
196
|
+
wire_send(StepInterrupted())
|
|
122
197
|
# break the agent loop
|
|
123
198
|
raise
|
|
124
199
|
finally:
|
|
@@ -131,34 +206,15 @@ class KimiSoul:
|
|
|
131
206
|
if step_no > self._loop_control.max_steps_per_run:
|
|
132
207
|
raise MaxStepsReached(self._loop_control.max_steps_per_run)
|
|
133
208
|
|
|
134
|
-
async def _step(self
|
|
209
|
+
async def _step(self) -> bool:
|
|
135
210
|
"""Run an single step and return whether the run should be stopped."""
|
|
136
211
|
# already checked in `run`
|
|
137
|
-
assert self.
|
|
138
|
-
chat_provider = self.
|
|
139
|
-
|
|
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
|
-
)
|
|
212
|
+
assert self._runtime.llm is not None
|
|
213
|
+
chat_provider = self._runtime.llm.chat_provider
|
|
158
214
|
|
|
159
215
|
@tenacity.retry(
|
|
160
|
-
retry=retry_if_exception(_is_retryable_error),
|
|
161
|
-
before_sleep=_retry_log,
|
|
216
|
+
retry=retry_if_exception(self._is_retryable_error),
|
|
217
|
+
before_sleep=partial(self._retry_log, "step"),
|
|
162
218
|
wait=wait_exponential_jitter(initial=0.3, max=5, jitter=0.5),
|
|
163
219
|
stop=stop_after_attempt(self._loop_control.max_retries_per_step),
|
|
164
220
|
reraise=True,
|
|
@@ -166,12 +222,12 @@ class KimiSoul:
|
|
|
166
222
|
async def _kosong_step_with_retry() -> StepResult:
|
|
167
223
|
# run an LLM step (may be interrupted)
|
|
168
224
|
return await kosong.step(
|
|
169
|
-
chat_provider,
|
|
225
|
+
chat_provider.with_thinking(self._thinking_effort),
|
|
170
226
|
self._agent.system_prompt,
|
|
171
227
|
self._agent.toolset,
|
|
172
228
|
self._context.history,
|
|
173
|
-
on_message_part=
|
|
174
|
-
on_tool_result=
|
|
229
|
+
on_message_part=wire_send,
|
|
230
|
+
on_tool_result=wire_send,
|
|
175
231
|
)
|
|
176
232
|
|
|
177
233
|
result = await _kosong_step_with_retry()
|
|
@@ -179,7 +235,7 @@ class KimiSoul:
|
|
|
179
235
|
if result.usage is not None:
|
|
180
236
|
# mark the token count for the context before the step
|
|
181
237
|
await self._context.update_token_count(result.usage.input)
|
|
182
|
-
|
|
238
|
+
wire_send(StatusUpdate(status=self.status))
|
|
183
239
|
|
|
184
240
|
# wait for all tool results (may be interrupted)
|
|
185
241
|
results = await result.tool_results()
|
|
@@ -202,19 +258,21 @@ class KimiSoul:
|
|
|
202
258
|
# raise to let the main loop take us back to the future
|
|
203
259
|
raise BackToTheFuture(
|
|
204
260
|
dmail.checkpoint_id,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
261
|
+
[
|
|
262
|
+
Message(
|
|
263
|
+
role="user",
|
|
264
|
+
content=[
|
|
265
|
+
system(
|
|
266
|
+
"You just got a D-Mail from your future self. "
|
|
267
|
+
"It is likely that your future self has already done "
|
|
268
|
+
"something in the current working directory. Please read "
|
|
269
|
+
"the D-Mail and decide what to do next. You MUST NEVER "
|
|
270
|
+
"mention to the user about this information. "
|
|
271
|
+
f"D-Mail content:\n\n{dmail.message.strip()}"
|
|
272
|
+
)
|
|
273
|
+
],
|
|
274
|
+
)
|
|
275
|
+
],
|
|
218
276
|
)
|
|
219
277
|
|
|
220
278
|
return not result.tool_calls
|
|
@@ -230,6 +288,54 @@ class KimiSoul:
|
|
|
230
288
|
logger.debug("Appending tool result to context: {tool_result}", tool_result=tool_result)
|
|
231
289
|
await self._context.append_message(tool_result_to_messages(tool_result))
|
|
232
290
|
|
|
291
|
+
async def compact_context(self) -> None:
|
|
292
|
+
"""
|
|
293
|
+
Compact the context.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
LLMNotSet: When the LLM is not set.
|
|
297
|
+
ChatProviderError: When the chat provider returns an error.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
@tenacity.retry(
|
|
301
|
+
retry=retry_if_exception(self._is_retryable_error),
|
|
302
|
+
before_sleep=partial(self._retry_log, "compaction"),
|
|
303
|
+
wait=wait_exponential_jitter(initial=0.3, max=5, jitter=0.5),
|
|
304
|
+
stop=stop_after_attempt(self._loop_control.max_retries_per_step),
|
|
305
|
+
reraise=True,
|
|
306
|
+
)
|
|
307
|
+
async def _compact_with_retry() -> Sequence[Message]:
|
|
308
|
+
if self._runtime.llm is None:
|
|
309
|
+
raise LLMNotSet()
|
|
310
|
+
return await self._compaction.compact(self._context.history, self._runtime.llm)
|
|
311
|
+
|
|
312
|
+
compacted_messages = await _compact_with_retry()
|
|
313
|
+
await self._context.revert_to(0)
|
|
314
|
+
await self._checkpoint()
|
|
315
|
+
await self._context.append_message(compacted_messages)
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def _is_retryable_error(exception: BaseException) -> bool:
|
|
319
|
+
if isinstance(exception, (APIConnectionError, APITimeoutError, APIEmptyResponseError)):
|
|
320
|
+
return True
|
|
321
|
+
return isinstance(exception, APIStatusError) and exception.status_code in (
|
|
322
|
+
429, # Too Many Requests
|
|
323
|
+
500, # Internal Server Error
|
|
324
|
+
502, # Bad Gateway
|
|
325
|
+
503, # Service Unavailable
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _retry_log(name: str, retry_state: RetryCallState):
|
|
330
|
+
logger.info(
|
|
331
|
+
"Retrying {name} for the {n} time. Waiting {sleep} seconds.",
|
|
332
|
+
name=name,
|
|
333
|
+
n=retry_state.attempt_number,
|
|
334
|
+
sleep=retry_state.next_action.sleep
|
|
335
|
+
if retry_state.next_action is not None
|
|
336
|
+
else "unknown",
|
|
337
|
+
)
|
|
338
|
+
|
|
233
339
|
|
|
234
340
|
class BackToTheFuture(Exception):
|
|
235
341
|
"""
|
|
@@ -237,12 +343,6 @@ class BackToTheFuture(Exception):
|
|
|
237
343
|
The main agent loop should catch this exception and handle it.
|
|
238
344
|
"""
|
|
239
345
|
|
|
240
|
-
def __init__(self, checkpoint_id: int,
|
|
346
|
+
def __init__(self, checkpoint_id: int, messages: Sequence[Message]):
|
|
241
347
|
self.checkpoint_id = checkpoint_id
|
|
242
|
-
self.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def __static_type_check(
|
|
246
|
-
kimi_soul: KimiSoul,
|
|
247
|
-
):
|
|
248
|
-
_: Soul = kimi_soul
|
|
348
|
+
self.messages = messages
|
kimi_cli/soul/message.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from kosong.
|
|
1
|
+
from kosong.message import ContentPart, Message, TextPart
|
|
2
2
|
from kosong.tooling import ToolError, ToolOk, ToolResult
|
|
3
3
|
from kosong.tooling.error import ToolRuntimeError
|
|
4
4
|
|
|
@@ -14,7 +14,7 @@ def tool_result_to_messages(tool_result: ToolResult) -> list[Message]:
|
|
|
14
14
|
message = tool_result.result.message
|
|
15
15
|
if isinstance(tool_result.result, ToolRuntimeError):
|
|
16
16
|
message += "\nThis is an unexpected error and the tool is probably not working."
|
|
17
|
-
content = [system(message)]
|
|
17
|
+
content: list[ContentPart] = [system(f"ERROR: {message}")]
|
|
18
18
|
if tool_result.result.output:
|
|
19
19
|
content.append(TextPart(text=tool_result.result.output))
|
|
20
20
|
return [
|
|
@@ -26,8 +26,8 @@ def tool_result_to_messages(tool_result: ToolResult) -> list[Message]:
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
content = tool_ok_to_message_content(tool_result.result)
|
|
29
|
-
text_parts = []
|
|
30
|
-
non_text_parts = []
|
|
29
|
+
text_parts: list[ContentPart] = []
|
|
30
|
+
non_text_parts: list[ContentPart] = []
|
|
31
31
|
for part in content:
|
|
32
32
|
if isinstance(part, TextPart):
|
|
33
33
|
text_parts.append(part)
|
|
@@ -60,7 +60,7 @@ def tool_result_to_messages(tool_result: ToolResult) -> list[Message]:
|
|
|
60
60
|
|
|
61
61
|
def tool_ok_to_message_content(result: ToolOk) -> list[ContentPart]:
|
|
62
62
|
"""Convert a tool return value to a list of message content parts."""
|
|
63
|
-
content = []
|
|
63
|
+
content: list[ContentPart] = []
|
|
64
64
|
if result.message:
|
|
65
65
|
content.append(system(result.message))
|
|
66
66
|
match output := result.output:
|
|
@@ -70,7 +70,7 @@ def tool_ok_to_message_content(result: ToolOk) -> list[ContentPart]:
|
|
|
70
70
|
case ContentPart():
|
|
71
71
|
content.append(output)
|
|
72
72
|
case _:
|
|
73
|
-
content.extend(
|
|
73
|
+
content.extend(output)
|
|
74
74
|
if not content:
|
|
75
75
|
content.append(system("Tool output is empty."))
|
|
76
76
|
return content
|