kimi-cli 0.39__py3-none-any.whl → 0.41__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 +23 -0
- kimi_cli/__init__.py +18 -280
- kimi_cli/agents/koder/system.md +1 -1
- kimi_cli/agentspec.py +104 -0
- kimi_cli/cli.py +235 -0
- kimi_cli/constant.py +4 -0
- kimi_cli/llm.py +69 -0
- kimi_cli/prompts/__init__.py +2 -2
- kimi_cli/soul/__init__.py +102 -6
- kimi_cli/soul/agent.py +157 -0
- kimi_cli/soul/approval.py +2 -4
- kimi_cli/soul/compaction.py +10 -10
- kimi_cli/soul/context.py +5 -0
- kimi_cli/soul/globals.py +92 -0
- kimi_cli/soul/kimisoul.py +26 -30
- kimi_cli/soul/message.py +5 -5
- kimi_cli/tools/bash/__init__.py +2 -2
- kimi_cli/tools/dmail/__init__.py +1 -1
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +2 -2
- kimi_cli/tools/file/grep.py +3 -2
- kimi_cli/tools/file/patch.py +2 -2
- kimi_cli/tools/file/read.py +1 -1
- kimi_cli/tools/file/replace.py +2 -2
- kimi_cli/tools/file/write.py +2 -2
- kimi_cli/tools/task/__init__.py +23 -22
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +1 -1
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +4 -4
- kimi_cli/ui/__init__.py +0 -69
- kimi_cli/ui/acp/__init__.py +8 -9
- kimi_cli/ui/print/__init__.py +17 -35
- kimi_cli/ui/shell/__init__.py +9 -15
- kimi_cli/ui/shell/liveview.py +13 -4
- kimi_cli/ui/shell/metacmd.py +3 -3
- kimi_cli/ui/shell/setup.py +7 -6
- kimi_cli/ui/shell/update.py +4 -3
- kimi_cli/ui/shell/visualize.py +18 -9
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +3 -1
- kimi_cli/wire/__init__.py +57 -0
- kimi_cli/{soul/wire.py → wire/message.py} +4 -39
- {kimi_cli-0.39.dist-info → kimi_cli-0.41.dist-info}/METADATA +35 -2
- kimi_cli-0.41.dist-info/RECORD +85 -0
- kimi_cli-0.41.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/utils/provider.py +0 -72
- kimi_cli-0.39.dist-info/RECORD +0 -80
- kimi_cli-0.39.dist-info/entry_points.txt +0 -3
- {kimi_cli-0.39.dist-info → kimi_cli-0.41.dist-info}/WHEEL +0 -0
kimi_cli/soul/globals.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
from kimi_cli.config import Config
|
|
8
|
+
from kimi_cli.llm import LLM
|
|
9
|
+
from kimi_cli.metadata import Session
|
|
10
|
+
from kimi_cli.soul.approval import Approval
|
|
11
|
+
from kimi_cli.soul.denwarenji import DenwaRenji
|
|
12
|
+
from kimi_cli.utils.logging import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BuiltinSystemPromptArgs(NamedTuple):
|
|
16
|
+
"""Builtin system prompt arguments."""
|
|
17
|
+
|
|
18
|
+
KIMI_NOW: str
|
|
19
|
+
"""The current datetime."""
|
|
20
|
+
KIMI_WORK_DIR: Path
|
|
21
|
+
"""The current working directory."""
|
|
22
|
+
KIMI_WORK_DIR_LS: str
|
|
23
|
+
"""The directory listing of current working directory."""
|
|
24
|
+
KIMI_AGENTS_MD: str # TODO: move to first message from system prompt
|
|
25
|
+
"""The content of AGENTS.md."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_agents_md(work_dir: Path) -> str | None:
|
|
29
|
+
paths = [
|
|
30
|
+
work_dir / "AGENTS.md",
|
|
31
|
+
work_dir / "agents.md",
|
|
32
|
+
]
|
|
33
|
+
for path in paths:
|
|
34
|
+
if path.is_file():
|
|
35
|
+
logger.info("Loaded agents.md: {path}", path=path)
|
|
36
|
+
return path.read_text(encoding="utf-8").strip()
|
|
37
|
+
logger.info("No AGENTS.md found in {work_dir}", work_dir=work_dir)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _list_work_dir(work_dir: Path) -> str:
|
|
42
|
+
if sys.platform == "win32":
|
|
43
|
+
ls = subprocess.run(
|
|
44
|
+
["cmd", "/c", "dir", work_dir],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
errors="replace",
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
ls = subprocess.run(
|
|
52
|
+
["ls", "-la", work_dir],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
encoding="utf-8",
|
|
56
|
+
errors="replace",
|
|
57
|
+
)
|
|
58
|
+
return ls.stdout.strip()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AgentGlobals(NamedTuple):
|
|
62
|
+
"""Agent globals."""
|
|
63
|
+
|
|
64
|
+
config: Config
|
|
65
|
+
llm: LLM | None
|
|
66
|
+
session: Session
|
|
67
|
+
builtin_args: BuiltinSystemPromptArgs
|
|
68
|
+
denwa_renji: DenwaRenji
|
|
69
|
+
approval: Approval
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
async def create(
|
|
73
|
+
cls, config: Config, llm: LLM | None, session: Session, yolo: bool
|
|
74
|
+
) -> "AgentGlobals":
|
|
75
|
+
work_dir = Path(session.work_dir.path)
|
|
76
|
+
# FIXME: do these asynchronously
|
|
77
|
+
ls_output = _list_work_dir(work_dir)
|
|
78
|
+
agents_md = load_agents_md(work_dir) or ""
|
|
79
|
+
|
|
80
|
+
return cls(
|
|
81
|
+
config=config,
|
|
82
|
+
llm=llm,
|
|
83
|
+
session=session,
|
|
84
|
+
builtin_args=BuiltinSystemPromptArgs(
|
|
85
|
+
KIMI_NOW=datetime.now().astimezone().isoformat(),
|
|
86
|
+
KIMI_WORK_DIR=work_dir,
|
|
87
|
+
KIMI_WORK_DIR_LS=ls_output,
|
|
88
|
+
KIMI_AGENTS_MD=agents_md,
|
|
89
|
+
),
|
|
90
|
+
denwa_renji=DenwaRenji(),
|
|
91
|
+
approval=Approval(yolo=yolo),
|
|
92
|
+
)
|
kimi_cli/soul/kimisoul.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import Sequence
|
|
3
3
|
from functools import partial
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
import kosong
|
|
6
7
|
import tenacity
|
|
@@ -15,29 +16,28 @@ from kosong.chat_provider import (
|
|
|
15
16
|
from kosong.tooling import ToolResult
|
|
16
17
|
from tenacity import RetryCallState, retry_if_exception, stop_after_attempt, wait_exponential_jitter
|
|
17
18
|
|
|
18
|
-
from kimi_cli.agent import Agent, AgentGlobals
|
|
19
19
|
from kimi_cli.config import LoopControl
|
|
20
|
-
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot
|
|
20
|
+
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot, wire_send
|
|
21
|
+
from kimi_cli.soul.agent import Agent
|
|
21
22
|
from kimi_cli.soul.compaction import SimpleCompaction
|
|
22
23
|
from kimi_cli.soul.context import Context
|
|
24
|
+
from kimi_cli.soul.globals import AgentGlobals
|
|
23
25
|
from kimi_cli.soul.message import system, tool_result_to_messages
|
|
24
|
-
from kimi_cli.
|
|
26
|
+
from kimi_cli.tools.dmail import NAME as SendDMail_NAME
|
|
27
|
+
from kimi_cli.tools.utils import ToolRejectedError
|
|
28
|
+
from kimi_cli.utils.logging import logger
|
|
29
|
+
from kimi_cli.wire.message import (
|
|
25
30
|
CompactionBegin,
|
|
26
31
|
CompactionEnd,
|
|
27
32
|
StatusUpdate,
|
|
28
33
|
StepBegin,
|
|
29
34
|
StepInterrupted,
|
|
30
|
-
Wire,
|
|
31
|
-
current_wire,
|
|
32
35
|
)
|
|
33
|
-
from kimi_cli.tools.dmail import NAME as SendDMail_NAME
|
|
34
|
-
from kimi_cli.tools.utils import ToolRejectedError
|
|
35
|
-
from kimi_cli.utils.logging import logger
|
|
36
36
|
|
|
37
37
|
RESERVED_TOKENS = 50_000
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
class KimiSoul:
|
|
40
|
+
class KimiSoul(Soul):
|
|
41
41
|
"""The soul of Kimi CLI."""
|
|
42
42
|
|
|
43
43
|
def __init__(
|
|
@@ -96,31 +96,27 @@ class KimiSoul:
|
|
|
96
96
|
async def _checkpoint(self):
|
|
97
97
|
await self._context.checkpoint(self._checkpoint_with_user_message)
|
|
98
98
|
|
|
99
|
-
async def run(self, user_input: str
|
|
99
|
+
async def run(self, user_input: str):
|
|
100
100
|
if self._agent_globals.llm is None:
|
|
101
101
|
raise LLMNotSet()
|
|
102
102
|
|
|
103
103
|
await self._checkpoint() # this creates the checkpoint 0 on first run
|
|
104
104
|
await self._context.append_message(Message(role="user", content=user_input))
|
|
105
105
|
logger.debug("Appended user message to context")
|
|
106
|
-
|
|
107
|
-
try:
|
|
108
|
-
await self._agent_loop(wire)
|
|
109
|
-
finally:
|
|
110
|
-
current_wire.reset(wire_token)
|
|
106
|
+
await self._agent_loop()
|
|
111
107
|
|
|
112
|
-
async def _agent_loop(self
|
|
108
|
+
async def _agent_loop(self):
|
|
113
109
|
"""The main agent loop for one run."""
|
|
114
110
|
assert self._agent_globals.llm is not None
|
|
115
111
|
|
|
116
112
|
async def _pipe_approval_to_wire():
|
|
117
113
|
while True:
|
|
118
114
|
request = await self._approval.fetch_request()
|
|
119
|
-
|
|
115
|
+
wire_send(request)
|
|
120
116
|
|
|
121
117
|
step_no = 1
|
|
122
118
|
while True:
|
|
123
|
-
|
|
119
|
+
wire_send(StepBegin(step_no))
|
|
124
120
|
approval_task = asyncio.create_task(_pipe_approval_to_wire())
|
|
125
121
|
# FIXME: It's possible that a subagent's approval task steals approval request
|
|
126
122
|
# from the main agent. We must ensure that the Task tool will redirect them
|
|
@@ -133,21 +129,21 @@ class KimiSoul:
|
|
|
133
129
|
>= self._agent_globals.llm.max_context_size
|
|
134
130
|
):
|
|
135
131
|
logger.info("Context too long, compacting...")
|
|
136
|
-
|
|
132
|
+
wire_send(CompactionBegin())
|
|
137
133
|
await self.compact_context()
|
|
138
|
-
|
|
134
|
+
wire_send(CompactionEnd())
|
|
139
135
|
|
|
140
136
|
logger.debug("Beginning step {step_no}", step_no=step_no)
|
|
141
137
|
await self._checkpoint()
|
|
142
138
|
self._denwa_renji.set_n_checkpoints(self._context.n_checkpoints)
|
|
143
|
-
finished = await self._step(
|
|
139
|
+
finished = await self._step()
|
|
144
140
|
except BackToTheFuture as e:
|
|
145
141
|
await self._context.revert_to(e.checkpoint_id)
|
|
146
142
|
await self._checkpoint()
|
|
147
143
|
await self._context.append_message(e.messages)
|
|
148
144
|
continue
|
|
149
145
|
except (ChatProviderError, asyncio.CancelledError):
|
|
150
|
-
|
|
146
|
+
wire_send(StepInterrupted())
|
|
151
147
|
# break the agent loop
|
|
152
148
|
raise
|
|
153
149
|
finally:
|
|
@@ -160,7 +156,7 @@ class KimiSoul:
|
|
|
160
156
|
if step_no > self._loop_control.max_steps_per_run:
|
|
161
157
|
raise MaxStepsReached(self._loop_control.max_steps_per_run)
|
|
162
158
|
|
|
163
|
-
async def _step(self
|
|
159
|
+
async def _step(self) -> bool:
|
|
164
160
|
"""Run an single step and return whether the run should be stopped."""
|
|
165
161
|
# already checked in `run`
|
|
166
162
|
assert self._agent_globals.llm is not None
|
|
@@ -180,8 +176,8 @@ class KimiSoul:
|
|
|
180
176
|
self._agent.system_prompt,
|
|
181
177
|
self._agent.toolset,
|
|
182
178
|
self._context.history,
|
|
183
|
-
on_message_part=
|
|
184
|
-
on_tool_result=
|
|
179
|
+
on_message_part=wire_send,
|
|
180
|
+
on_tool_result=wire_send,
|
|
185
181
|
)
|
|
186
182
|
|
|
187
183
|
result = await _kosong_step_with_retry()
|
|
@@ -189,7 +185,7 @@ class KimiSoul:
|
|
|
189
185
|
if result.usage is not None:
|
|
190
186
|
# mark the token count for the context before the step
|
|
191
187
|
await self._context.update_token_count(result.usage.input)
|
|
192
|
-
|
|
188
|
+
wire_send(StatusUpdate(status=self.status))
|
|
193
189
|
|
|
194
190
|
# wait for all tool results (may be interrupted)
|
|
195
191
|
results = await result.tool_results()
|
|
@@ -302,7 +298,7 @@ class BackToTheFuture(Exception):
|
|
|
302
298
|
self.messages = messages
|
|
303
299
|
|
|
304
300
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
):
|
|
308
|
-
|
|
301
|
+
if TYPE_CHECKING:
|
|
302
|
+
|
|
303
|
+
def type_check(kimi_soul: KimiSoul):
|
|
304
|
+
_: Soul = kimi_soul
|
kimi_cli/soul/message.py
CHANGED
|
@@ -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(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
|
kimi_cli/tools/bash/__init__.py
CHANGED
|
@@ -45,11 +45,11 @@ class Bash(CallableTool2[Params]):
|
|
|
45
45
|
return ToolRejectedError()
|
|
46
46
|
|
|
47
47
|
def stdout_cb(line: bytes):
|
|
48
|
-
line_str = line.decode()
|
|
48
|
+
line_str = line.decode(errors="replace")
|
|
49
49
|
builder.write(line_str)
|
|
50
50
|
|
|
51
51
|
def stderr_cb(line: bytes):
|
|
52
|
-
line_str = line.decode()
|
|
52
|
+
line_str = line.decode(errors="replace")
|
|
53
53
|
builder.write(line_str)
|
|
54
54
|
|
|
55
55
|
try:
|
kimi_cli/tools/dmail/__init__.py
CHANGED
|
@@ -10,7 +10,7 @@ NAME = "SendDMail"
|
|
|
10
10
|
|
|
11
11
|
class SendDMail(CallableTool2):
|
|
12
12
|
name: str = NAME
|
|
13
|
-
description: str = (Path(__file__).parent / "dmail.md").read_text()
|
|
13
|
+
description: str = (Path(__file__).parent / "dmail.md").read_text(encoding="utf-8")
|
|
14
14
|
params: type[DMail] = DMail
|
|
15
15
|
|
|
16
16
|
def __init__(self, denwa_renji: DenwaRenji, **kwargs):
|
kimi_cli/tools/file/glob.md
CHANGED
|
@@ -14,4 +14,4 @@ Find files and directories using glob patterns. This tool supports standard glob
|
|
|
14
14
|
|
|
15
15
|
**Bad example patterns:**
|
|
16
16
|
- `**`, `**/*.py` - Any pattern starting with '**' will be rejected. Because it would recursively search all directories and subdirectories, which is very likely to yield large result that exceeds your context size. Always use more specific patterns like `src/**/*.py` instead.
|
|
17
|
-
- `node_modules/**/*.js` - Although this does not start with '**', it would still highly possible to yield large result because `node_modules` is well-known to contain too many directories and files. Avoid
|
|
17
|
+
- `node_modules/**/*.js` - Although this does not start with '**', it would still highly possible to yield large result because `node_modules` is well-known to contain too many directories and files. Avoid recursively searching in such directories, other examples include `venv`, `.venv`, `__pycache__`, `target`. If you really need to search in a dependency, use more specific patterns like `node_modules/react/src/*` instead.
|
kimi_cli/tools/file/glob.py
CHANGED
|
@@ -8,7 +8,7 @@ import aiofiles.os
|
|
|
8
8
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
|
-
from kimi_cli.
|
|
11
|
+
from kimi_cli.soul.globals import BuiltinSystemPromptArgs
|
|
12
12
|
from kimi_cli.tools.utils import load_desc
|
|
13
13
|
|
|
14
14
|
MAX_MATCHES = 1000
|
|
@@ -128,7 +128,7 @@ class Glob(CallableTool2[Params]):
|
|
|
128
128
|
message = (
|
|
129
129
|
f"Found {len(matches)} matches for pattern `{params.pattern}`."
|
|
130
130
|
if len(matches) > 0
|
|
131
|
-
else "No matches found for pattern `{params.pattern}`."
|
|
131
|
+
else f"No matches found for pattern `{params.pattern}`."
|
|
132
132
|
)
|
|
133
133
|
if len(matches) > MAX_MATCHES:
|
|
134
134
|
matches = matches[:MAX_MATCHES]
|
kimi_cli/tools/file/grep.py
CHANGED
|
@@ -15,6 +15,7 @@ from pydantic import BaseModel, Field
|
|
|
15
15
|
|
|
16
16
|
import kimi_cli
|
|
17
17
|
from kimi_cli.share import get_share_dir
|
|
18
|
+
from kimi_cli.utils.aiohttp import new_client_session
|
|
18
19
|
from kimi_cli.utils.logging import logger
|
|
19
20
|
|
|
20
21
|
|
|
@@ -167,7 +168,7 @@ async def _download_and_install_rg(bin_name: str) -> Path:
|
|
|
167
168
|
share_bin_dir.mkdir(parents=True, exist_ok=True)
|
|
168
169
|
destination = share_bin_dir / bin_name
|
|
169
170
|
|
|
170
|
-
async with
|
|
171
|
+
async with new_client_session() as session:
|
|
171
172
|
with tempfile.TemporaryDirectory(prefix="kimi-rg-") as tmpdir:
|
|
172
173
|
tar_path = Path(tmpdir) / filename
|
|
173
174
|
|
|
@@ -219,7 +220,7 @@ async def _ensure_rg_path() -> str:
|
|
|
219
220
|
|
|
220
221
|
class Grep(CallableTool2[Params]):
|
|
221
222
|
name: str = "Grep"
|
|
222
|
-
description: str = (Path(__file__).parent / "grep.md").read_text()
|
|
223
|
+
description: str = (Path(__file__).parent / "grep.md").read_text(encoding="utf-8")
|
|
223
224
|
params: type[Params] = Params
|
|
224
225
|
|
|
225
226
|
@override
|
kimi_cli/tools/file/patch.py
CHANGED
|
@@ -6,8 +6,8 @@ import patch_ng
|
|
|
6
6
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
|
-
from kimi_cli.agent import BuiltinSystemPromptArgs
|
|
10
9
|
from kimi_cli.soul.approval import Approval
|
|
10
|
+
from kimi_cli.soul.globals import BuiltinSystemPromptArgs
|
|
11
11
|
from kimi_cli.tools.file import FileActions
|
|
12
12
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
13
13
|
|
|
@@ -19,7 +19,7 @@ class Params(BaseModel):
|
|
|
19
19
|
|
|
20
20
|
class PatchFile(CallableTool2[Params]):
|
|
21
21
|
name: str = "PatchFile"
|
|
22
|
-
description: str = (Path(__file__).parent / "patch.md").read_text()
|
|
22
|
+
description: str = (Path(__file__).parent / "patch.md").read_text(encoding="utf-8")
|
|
23
23
|
params: type[Params] = Params
|
|
24
24
|
|
|
25
25
|
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
|
kimi_cli/tools/file/read.py
CHANGED
|
@@ -5,7 +5,7 @@ import aiofiles
|
|
|
5
5
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
-
from kimi_cli.
|
|
8
|
+
from kimi_cli.soul.globals import BuiltinSystemPromptArgs
|
|
9
9
|
from kimi_cli.tools.utils import load_desc, truncate_line
|
|
10
10
|
|
|
11
11
|
MAX_LINES = 1000
|
kimi_cli/tools/file/replace.py
CHANGED
|
@@ -5,8 +5,8 @@ import aiofiles
|
|
|
5
5
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
-
from kimi_cli.agent import BuiltinSystemPromptArgs
|
|
9
8
|
from kimi_cli.soul.approval import Approval
|
|
9
|
+
from kimi_cli.soul.globals import BuiltinSystemPromptArgs
|
|
10
10
|
from kimi_cli.tools.file import FileActions
|
|
11
11
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ class Params(BaseModel):
|
|
|
29
29
|
|
|
30
30
|
class StrReplaceFile(CallableTool2[Params]):
|
|
31
31
|
name: str = "StrReplaceFile"
|
|
32
|
-
description: str = (Path(__file__).parent / "replace.md").read_text()
|
|
32
|
+
description: str = (Path(__file__).parent / "replace.md").read_text(encoding="utf-8")
|
|
33
33
|
params: type[Params] = Params
|
|
34
34
|
|
|
35
35
|
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
|
kimi_cli/tools/file/write.py
CHANGED
|
@@ -5,8 +5,8 @@ import aiofiles
|
|
|
5
5
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
-
from kimi_cli.agent import BuiltinSystemPromptArgs
|
|
9
8
|
from kimi_cli.soul.approval import Approval
|
|
9
|
+
from kimi_cli.soul.globals import BuiltinSystemPromptArgs
|
|
10
10
|
from kimi_cli.tools.file import FileActions
|
|
11
11
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
12
12
|
|
|
@@ -26,7 +26,7 @@ class Params(BaseModel):
|
|
|
26
26
|
|
|
27
27
|
class WriteFile(CallableTool2[Params]):
|
|
28
28
|
name: str = "WriteFile"
|
|
29
|
-
description: str = (Path(__file__).parent / "write.md").read_text()
|
|
29
|
+
description: str = (Path(__file__).parent / "write.md").read_text(encoding="utf-8")
|
|
30
30
|
params: type[Params] = Params
|
|
31
31
|
|
|
32
32
|
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
|
kimi_cli/tools/task/__init__.py
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import override
|
|
3
4
|
|
|
4
5
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
7
|
-
from kimi_cli.
|
|
8
|
-
from kimi_cli.soul import MaxStepsReached
|
|
8
|
+
from kimi_cli.agentspec import ResolvedAgentSpec
|
|
9
|
+
from kimi_cli.soul import MaxStepsReached, get_wire_or_none, run_soul
|
|
10
|
+
from kimi_cli.soul.agent import Agent, load_agent
|
|
9
11
|
from kimi_cli.soul.context import Context
|
|
12
|
+
from kimi_cli.soul.globals import AgentGlobals
|
|
10
13
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
11
|
-
from kimi_cli.soul.wire import ApprovalRequest, Wire, WireMessage, get_wire_or_none
|
|
12
14
|
from kimi_cli.tools.utils import load_desc
|
|
13
15
|
from kimi_cli.utils.message import message_extract_text
|
|
14
16
|
from kimi_cli.utils.path import next_available_rotation
|
|
17
|
+
from kimi_cli.wire import WireUISide
|
|
18
|
+
from kimi_cli.wire.message import ApprovalRequest, WireMessage
|
|
15
19
|
|
|
16
20
|
# Maximum continuation attempts for task summary
|
|
17
21
|
MAX_CONTINUE_ATTEMPTS = 1
|
|
@@ -45,12 +49,11 @@ class Task(CallableTool2[Params]):
|
|
|
45
49
|
name: str = "Task"
|
|
46
50
|
params: type[Params] = Params
|
|
47
51
|
|
|
48
|
-
def __init__(self, agent_spec:
|
|
52
|
+
def __init__(self, agent_spec: ResolvedAgentSpec, agent_globals: AgentGlobals, **kwargs):
|
|
49
53
|
subagents: dict[str, Agent] = {}
|
|
50
54
|
descs = []
|
|
51
55
|
|
|
52
56
|
# load all subagents
|
|
53
|
-
assert agent_spec.subagents is not None, "Task tool expects subagents"
|
|
54
57
|
for name, spec in agent_spec.subagents.items():
|
|
55
58
|
subagents[name] = load_agent(spec.path, agent_globals)
|
|
56
59
|
descs.append(f"- `{name}`: {spec.description}")
|
|
@@ -99,6 +102,19 @@ class Task(CallableTool2[Params]):
|
|
|
99
102
|
|
|
100
103
|
async def _run_subagent(self, agent: Agent, prompt: str) -> ToolReturnType:
|
|
101
104
|
"""Run subagent with optional continuation for task summary."""
|
|
105
|
+
super_wire = get_wire_or_none()
|
|
106
|
+
assert super_wire is not None
|
|
107
|
+
|
|
108
|
+
def _super_wire_send(msg: WireMessage) -> None:
|
|
109
|
+
if isinstance(msg, ApprovalRequest):
|
|
110
|
+
super_wire.soul_side.send(msg)
|
|
111
|
+
# TODO: visualize subagent behavior by sending other messages in some way
|
|
112
|
+
|
|
113
|
+
async def _ui_loop_fn(wire: WireUISide) -> None:
|
|
114
|
+
while True:
|
|
115
|
+
msg = await wire.receive()
|
|
116
|
+
_super_wire_send(msg)
|
|
117
|
+
|
|
102
118
|
subagent_history_file = await self._get_subagent_history_file()
|
|
103
119
|
context = Context(file_backend=subagent_history_file)
|
|
104
120
|
soul = KimiSoul(
|
|
@@ -107,12 +123,9 @@ class Task(CallableTool2[Params]):
|
|
|
107
123
|
context=context,
|
|
108
124
|
loop_control=self._agent_globals.config.loop_control,
|
|
109
125
|
)
|
|
110
|
-
wire = get_wire_or_none()
|
|
111
|
-
assert wire is not None, "Wire is expected to be set"
|
|
112
|
-
sub_wire = _SubWire(wire)
|
|
113
126
|
|
|
114
127
|
try:
|
|
115
|
-
await soul
|
|
128
|
+
await run_soul(soul, prompt, _ui_loop_fn, asyncio.Event())
|
|
116
129
|
except MaxStepsReached as e:
|
|
117
130
|
return ToolError(
|
|
118
131
|
message=(
|
|
@@ -135,22 +148,10 @@ class Task(CallableTool2[Params]):
|
|
|
135
148
|
# Check if response is too brief, if so, run again with continuation prompt
|
|
136
149
|
n_attempts_remaining = MAX_CONTINUE_ATTEMPTS
|
|
137
150
|
if len(final_response) < 200 and n_attempts_remaining > 0:
|
|
138
|
-
await soul
|
|
151
|
+
await run_soul(soul, CONTINUE_PROMPT, _ui_loop_fn, asyncio.Event())
|
|
139
152
|
|
|
140
153
|
if len(context.history) == 0 or context.history[-1].role != "assistant":
|
|
141
154
|
return ToolError(message=_error_msg, brief="Failed to run subagent")
|
|
142
155
|
final_response = message_extract_text(context.history[-1])
|
|
143
156
|
|
|
144
157
|
return ToolOk(output=final_response)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class _SubWire(Wire):
|
|
148
|
-
def __init__(self, super_wire: Wire):
|
|
149
|
-
super().__init__()
|
|
150
|
-
self._super_wire = super_wire
|
|
151
|
-
|
|
152
|
-
@override
|
|
153
|
-
def send(self, msg: WireMessage):
|
|
154
|
-
if isinstance(msg, ApprovalRequest):
|
|
155
|
-
self._super_wire.send(msg)
|
|
156
|
-
# TODO: visualize subagent behavior by sending other messages in some way
|
kimi_cli/tools/task/task.md
CHANGED
|
@@ -4,7 +4,7 @@ Spawn a subagent to perform a specific task. Subagent will be spawned with a fre
|
|
|
4
4
|
|
|
5
5
|
Context isolation is one of the key benefits of using subagents. By delegating tasks to subagents, you can keep your main context clean and focused on the main goal requested by the user.
|
|
6
6
|
|
|
7
|
-
Here are some
|
|
7
|
+
Here are some scenarios you may want this tool for context isolation:
|
|
8
8
|
|
|
9
9
|
- You wrote some code and it did not work as expected. In this case you can spawn a subagent to fix the code, asking the subagent to return how it is fixed. This can potentially benefit because the detailed process of fixing the code may not be relevant to your main goal, and may clutter your context.
|
|
10
10
|
- When you need some latest knowledge of a specific library, framework or technology to proceed with your task, you can spawn a subagent to search on the internet for the needed information and return to you the gathered relevant information, for example code examples, API references, etc. This can avoid ton of irrelevant search results in your own context.
|
kimi_cli/tools/todo/__init__.py
CHANGED
|
@@ -16,7 +16,7 @@ class Params(BaseModel):
|
|
|
16
16
|
|
|
17
17
|
class SetTodoList(CallableTool2[Params]):
|
|
18
18
|
name: str = "SetTodoList"
|
|
19
|
-
description: str = (Path(__file__).parent / "set_todo_list.md").read_text()
|
|
19
|
+
description: str = (Path(__file__).parent / "set_todo_list.md").read_text(encoding="utf-8")
|
|
20
20
|
params: type[Params] = Params
|
|
21
21
|
|
|
22
22
|
@override
|
kimi_cli/tools/utils.py
CHANGED
|
@@ -7,7 +7,7 @@ from kosong.tooling import ToolError, ToolOk
|
|
|
7
7
|
|
|
8
8
|
def load_desc(path: Path, substitutions: dict[str, str] | None = None) -> str:
|
|
9
9
|
"""Load a tool description from a file, with optional substitutions."""
|
|
10
|
-
description = path.read_text()
|
|
10
|
+
description = path.read_text(encoding="utf-8")
|
|
11
11
|
if substitutions:
|
|
12
12
|
description = string.Template(description).substitute(substitutions)
|
|
13
13
|
return description
|
kimi_cli/tools/web/fetch.py
CHANGED
|
@@ -7,6 +7,7 @@ from kosong.tooling import CallableTool2, ToolReturnType
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
from kimi_cli.tools.utils import ToolResultBuilder, load_desc
|
|
10
|
+
from kimi_cli.utils.aiohttp import new_client_session
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class Params(BaseModel):
|
|
@@ -24,7 +25,7 @@ class FetchURL(CallableTool2[Params]):
|
|
|
24
25
|
|
|
25
26
|
try:
|
|
26
27
|
async with (
|
|
27
|
-
|
|
28
|
+
new_client_session() as session,
|
|
28
29
|
session.get(
|
|
29
30
|
params.url,
|
|
30
31
|
headers={
|
kimi_cli/tools/web/search.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import override
|
|
3
3
|
|
|
4
|
-
import aiohttp
|
|
5
4
|
from kosong.tooling import CallableTool2, ToolReturnType
|
|
6
5
|
from pydantic import BaseModel, Field, ValidationError
|
|
7
6
|
|
|
8
|
-
import kimi_cli
|
|
9
7
|
from kimi_cli.config import Config
|
|
8
|
+
from kimi_cli.constant import USER_AGENT
|
|
10
9
|
from kimi_cli.soul.toolset import get_current_tool_call_or_none
|
|
11
10
|
from kimi_cli.tools.utils import ToolResultBuilder, load_desc
|
|
11
|
+
from kimi_cli.utils.aiohttp import new_client_session
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Params(BaseModel):
|
|
@@ -62,11 +62,11 @@ class SearchWeb(CallableTool2[Params]):
|
|
|
62
62
|
assert tool_call is not None, "Tool call is expected to be set"
|
|
63
63
|
|
|
64
64
|
async with (
|
|
65
|
-
|
|
65
|
+
new_client_session() as session,
|
|
66
66
|
session.post(
|
|
67
67
|
self._base_url,
|
|
68
68
|
headers={
|
|
69
|
-
"User-Agent":
|
|
69
|
+
"User-Agent": USER_AGENT,
|
|
70
70
|
"Authorization": f"Bearer {self._api_key}",
|
|
71
71
|
"X-Msh-Tool-Call-Id": tool_call.id,
|
|
72
72
|
},
|
kimi_cli/ui/__init__.py
CHANGED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import contextlib
|
|
3
|
-
from collections.abc import Callable, Coroutine
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from kimi_cli.soul import Soul
|
|
7
|
-
from kimi_cli.soul.wire import Wire
|
|
8
|
-
from kimi_cli.utils.logging import logger
|
|
9
|
-
|
|
10
|
-
type UILoopFn = Callable[[Wire], Coroutine[Any, Any, None]]
|
|
11
|
-
"""A long-running async function to visualize the agent behavior."""
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class RunCancelled(Exception):
|
|
15
|
-
"""The run was cancelled by the cancel event."""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
async def run_soul(
|
|
19
|
-
soul: Soul,
|
|
20
|
-
user_input: str,
|
|
21
|
-
ui_loop_fn: UILoopFn,
|
|
22
|
-
cancel_event: asyncio.Event,
|
|
23
|
-
):
|
|
24
|
-
"""
|
|
25
|
-
Run the soul with the given user input.
|
|
26
|
-
|
|
27
|
-
`cancel_event` is a outside handle that can be used to cancel the run. When the event is set,
|
|
28
|
-
the run will be gracefully stopped and a `RunCancelled` will be raised.
|
|
29
|
-
|
|
30
|
-
Raises:
|
|
31
|
-
LLMNotSet: When the LLM is not set.
|
|
32
|
-
ChatProviderError: When the LLM provider returns an error.
|
|
33
|
-
MaxStepsReached: When the maximum number of steps is reached.
|
|
34
|
-
RunCancelled: When the run is cancelled by the cancel event.
|
|
35
|
-
"""
|
|
36
|
-
wire = Wire()
|
|
37
|
-
logger.debug("Starting UI loop with function: {ui_loop_fn}", ui_loop_fn=ui_loop_fn)
|
|
38
|
-
|
|
39
|
-
ui_task = asyncio.create_task(ui_loop_fn(wire))
|
|
40
|
-
soul_task = asyncio.create_task(soul.run(user_input, wire))
|
|
41
|
-
|
|
42
|
-
cancel_event_task = asyncio.create_task(cancel_event.wait())
|
|
43
|
-
await asyncio.wait(
|
|
44
|
-
[soul_task, cancel_event_task],
|
|
45
|
-
return_when=asyncio.FIRST_COMPLETED,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
try:
|
|
49
|
-
if cancel_event.is_set():
|
|
50
|
-
logger.debug("Cancelling the run task")
|
|
51
|
-
soul_task.cancel()
|
|
52
|
-
try:
|
|
53
|
-
await soul_task
|
|
54
|
-
except asyncio.CancelledError:
|
|
55
|
-
raise RunCancelled from None
|
|
56
|
-
else:
|
|
57
|
-
assert soul_task.done() # either stop event is set or the run task is done
|
|
58
|
-
cancel_event_task.cancel()
|
|
59
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
60
|
-
await cancel_event_task
|
|
61
|
-
soul_task.result() # this will raise if any exception was raised in the run task
|
|
62
|
-
finally:
|
|
63
|
-
logger.debug("Shutting down the visualization loop")
|
|
64
|
-
# shutting down the event queue should break the visualization loop
|
|
65
|
-
wire.shutdown()
|
|
66
|
-
try:
|
|
67
|
-
await asyncio.wait_for(ui_task, timeout=0.5)
|
|
68
|
-
except TimeoutError:
|
|
69
|
-
logger.warning("Visualization loop timed out")
|