kimi-cli 0.40__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 +12 -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 +1 -1
- kimi_cli/soul/compaction.py +4 -4
- kimi_cli/soul/context.py +5 -0
- kimi_cli/soul/globals.py +92 -0
- kimi_cli/soul/kimisoul.py +21 -26
- 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 +1 -1
- 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/search.py +2 -2
- 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 +5 -13
- kimi_cli/ui/shell/liveview.py +1 -1
- kimi_cli/ui/shell/metacmd.py +3 -3
- kimi_cli/ui/shell/setup.py +5 -5
- kimi_cli/ui/shell/update.py +2 -2
- kimi_cli/ui/shell/visualize.py +10 -7
- 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.40.dist-info → kimi_cli-0.41.dist-info}/METADATA +34 -1
- 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 -70
- kimi_cli-0.40.dist-info/RECORD +0 -81
- kimi_cli-0.40.dist-info/entry_points.txt +0 -3
- {kimi_cli-0.40.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
|
@@ -16,29 +16,28 @@ from kosong.chat_provider import (
|
|
|
16
16
|
from kosong.tooling import ToolResult
|
|
17
17
|
from tenacity import RetryCallState, retry_if_exception, stop_after_attempt, wait_exponential_jitter
|
|
18
18
|
|
|
19
|
-
from kimi_cli.agent import Agent, AgentGlobals
|
|
20
19
|
from kimi_cli.config import LoopControl
|
|
21
|
-
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
|
|
22
22
|
from kimi_cli.soul.compaction import SimpleCompaction
|
|
23
23
|
from kimi_cli.soul.context import Context
|
|
24
|
+
from kimi_cli.soul.globals import AgentGlobals
|
|
24
25
|
from kimi_cli.soul.message import system, tool_result_to_messages
|
|
25
|
-
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 (
|
|
26
30
|
CompactionBegin,
|
|
27
31
|
CompactionEnd,
|
|
28
32
|
StatusUpdate,
|
|
29
33
|
StepBegin,
|
|
30
34
|
StepInterrupted,
|
|
31
|
-
Wire,
|
|
32
|
-
current_wire,
|
|
33
35
|
)
|
|
34
|
-
from kimi_cli.tools.dmail import NAME as SendDMail_NAME
|
|
35
|
-
from kimi_cli.tools.utils import ToolRejectedError
|
|
36
|
-
from kimi_cli.utils.logging import logger
|
|
37
36
|
|
|
38
37
|
RESERVED_TOKENS = 50_000
|
|
39
38
|
|
|
40
39
|
|
|
41
|
-
class KimiSoul:
|
|
40
|
+
class KimiSoul(Soul):
|
|
42
41
|
"""The soul of Kimi CLI."""
|
|
43
42
|
|
|
44
43
|
def __init__(
|
|
@@ -97,31 +96,27 @@ class KimiSoul:
|
|
|
97
96
|
async def _checkpoint(self):
|
|
98
97
|
await self._context.checkpoint(self._checkpoint_with_user_message)
|
|
99
98
|
|
|
100
|
-
async def run(self, user_input: str
|
|
99
|
+
async def run(self, user_input: str):
|
|
101
100
|
if self._agent_globals.llm is None:
|
|
102
101
|
raise LLMNotSet()
|
|
103
102
|
|
|
104
103
|
await self._checkpoint() # this creates the checkpoint 0 on first run
|
|
105
104
|
await self._context.append_message(Message(role="user", content=user_input))
|
|
106
105
|
logger.debug("Appended user message to context")
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
await self._agent_loop(wire)
|
|
110
|
-
finally:
|
|
111
|
-
current_wire.reset(wire_token)
|
|
106
|
+
await self._agent_loop()
|
|
112
107
|
|
|
113
|
-
async def _agent_loop(self
|
|
108
|
+
async def _agent_loop(self):
|
|
114
109
|
"""The main agent loop for one run."""
|
|
115
110
|
assert self._agent_globals.llm is not None
|
|
116
111
|
|
|
117
112
|
async def _pipe_approval_to_wire():
|
|
118
113
|
while True:
|
|
119
114
|
request = await self._approval.fetch_request()
|
|
120
|
-
|
|
115
|
+
wire_send(request)
|
|
121
116
|
|
|
122
117
|
step_no = 1
|
|
123
118
|
while True:
|
|
124
|
-
|
|
119
|
+
wire_send(StepBegin(step_no))
|
|
125
120
|
approval_task = asyncio.create_task(_pipe_approval_to_wire())
|
|
126
121
|
# FIXME: It's possible that a subagent's approval task steals approval request
|
|
127
122
|
# from the main agent. We must ensure that the Task tool will redirect them
|
|
@@ -134,21 +129,21 @@ class KimiSoul:
|
|
|
134
129
|
>= self._agent_globals.llm.max_context_size
|
|
135
130
|
):
|
|
136
131
|
logger.info("Context too long, compacting...")
|
|
137
|
-
|
|
132
|
+
wire_send(CompactionBegin())
|
|
138
133
|
await self.compact_context()
|
|
139
|
-
|
|
134
|
+
wire_send(CompactionEnd())
|
|
140
135
|
|
|
141
136
|
logger.debug("Beginning step {step_no}", step_no=step_no)
|
|
142
137
|
await self._checkpoint()
|
|
143
138
|
self._denwa_renji.set_n_checkpoints(self._context.n_checkpoints)
|
|
144
|
-
finished = await self._step(
|
|
139
|
+
finished = await self._step()
|
|
145
140
|
except BackToTheFuture as e:
|
|
146
141
|
await self._context.revert_to(e.checkpoint_id)
|
|
147
142
|
await self._checkpoint()
|
|
148
143
|
await self._context.append_message(e.messages)
|
|
149
144
|
continue
|
|
150
145
|
except (ChatProviderError, asyncio.CancelledError):
|
|
151
|
-
|
|
146
|
+
wire_send(StepInterrupted())
|
|
152
147
|
# break the agent loop
|
|
153
148
|
raise
|
|
154
149
|
finally:
|
|
@@ -161,7 +156,7 @@ class KimiSoul:
|
|
|
161
156
|
if step_no > self._loop_control.max_steps_per_run:
|
|
162
157
|
raise MaxStepsReached(self._loop_control.max_steps_per_run)
|
|
163
158
|
|
|
164
|
-
async def _step(self
|
|
159
|
+
async def _step(self) -> bool:
|
|
165
160
|
"""Run an single step and return whether the run should be stopped."""
|
|
166
161
|
# already checked in `run`
|
|
167
162
|
assert self._agent_globals.llm is not None
|
|
@@ -181,8 +176,8 @@ class KimiSoul:
|
|
|
181
176
|
self._agent.system_prompt,
|
|
182
177
|
self._agent.toolset,
|
|
183
178
|
self._context.history,
|
|
184
|
-
on_message_part=
|
|
185
|
-
on_tool_result=
|
|
179
|
+
on_message_part=wire_send,
|
|
180
|
+
on_tool_result=wire_send,
|
|
186
181
|
)
|
|
187
182
|
|
|
188
183
|
result = await _kosong_step_with_retry()
|
|
@@ -190,7 +185,7 @@ class KimiSoul:
|
|
|
190
185
|
if result.usage is not None:
|
|
191
186
|
# mark the token count for the context before the step
|
|
192
187
|
await self._context.update_token_count(result.usage.input)
|
|
193
|
-
|
|
188
|
+
wire_send(StatusUpdate(status=self.status))
|
|
194
189
|
|
|
195
190
|
# wait for all tool results (may be interrupted)
|
|
196
191
|
results = await result.tool_results()
|
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
|
@@ -220,7 +220,7 @@ async def _ensure_rg_path() -> str:
|
|
|
220
220
|
|
|
221
221
|
class Grep(CallableTool2[Params]):
|
|
222
222
|
name: str = "Grep"
|
|
223
|
-
description: str = (Path(__file__).parent / "grep.md").read_text()
|
|
223
|
+
description: str = (Path(__file__).parent / "grep.md").read_text(encoding="utf-8")
|
|
224
224
|
params: type[Params] = Params
|
|
225
225
|
|
|
226
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/search.py
CHANGED
|
@@ -4,8 +4,8 @@ from typing import override
|
|
|
4
4
|
from kosong.tooling import CallableTool2, ToolReturnType
|
|
5
5
|
from pydantic import BaseModel, Field, ValidationError
|
|
6
6
|
|
|
7
|
-
import kimi_cli
|
|
8
7
|
from kimi_cli.config import Config
|
|
8
|
+
from kimi_cli.constant import USER_AGENT
|
|
9
9
|
from kimi_cli.soul.toolset import get_current_tool_call_or_none
|
|
10
10
|
from kimi_cli.tools.utils import ToolResultBuilder, load_desc
|
|
11
11
|
from kimi_cli.utils.aiohttp import new_client_session
|
|
@@ -66,7 +66,7 @@ class SearchWeb(CallableTool2[Params]):
|
|
|
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")
|
kimi_cli/ui/acp/__init__.py
CHANGED
|
@@ -12,18 +12,17 @@ from kosong.base.message import (
|
|
|
12
12
|
from kosong.chat_provider import ChatProviderError
|
|
13
13
|
from kosong.tooling import ToolError, ToolOk, ToolResult
|
|
14
14
|
|
|
15
|
-
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul
|
|
16
|
-
from kimi_cli.
|
|
15
|
+
from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
16
|
+
from kimi_cli.tools import extract_subtitle
|
|
17
|
+
from kimi_cli.utils.logging import logger
|
|
18
|
+
from kimi_cli.wire import WireUISide
|
|
19
|
+
from kimi_cli.wire.message import (
|
|
17
20
|
ApprovalRequest,
|
|
18
21
|
ApprovalResponse,
|
|
19
22
|
StatusUpdate,
|
|
20
23
|
StepBegin,
|
|
21
24
|
StepInterrupted,
|
|
22
|
-
Wire,
|
|
23
25
|
)
|
|
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
26
|
|
|
28
27
|
|
|
29
28
|
class _ToolCallState:
|
|
@@ -64,7 +63,7 @@ class _RunState:
|
|
|
64
63
|
self.cancel_event = asyncio.Event()
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
class
|
|
66
|
+
class ACPAgent:
|
|
68
67
|
"""Implementation of the ACP Agent protocol."""
|
|
69
68
|
|
|
70
69
|
def __init__(self, soul: Soul, connection: acp.AgentSideConnection):
|
|
@@ -172,7 +171,7 @@ class ACPAgentImpl:
|
|
|
172
171
|
logger.info("Cancelling running prompt")
|
|
173
172
|
self.run_state.cancel_event.set()
|
|
174
173
|
|
|
175
|
-
async def _stream_events(self, wire:
|
|
174
|
+
async def _stream_events(self, wire: WireUISide):
|
|
176
175
|
try:
|
|
177
176
|
# expect a StepBegin
|
|
178
177
|
assert isinstance(await wire.receive(), StepBegin)
|
|
@@ -428,7 +427,7 @@ class ACPServer:
|
|
|
428
427
|
|
|
429
428
|
# Create connection - the library handles all JSON-RPC details!
|
|
430
429
|
_ = acp.AgentSideConnection(
|
|
431
|
-
lambda conn:
|
|
430
|
+
lambda conn: ACPAgent(self.soul, conn),
|
|
432
431
|
writer,
|
|
433
432
|
reader,
|
|
434
433
|
)
|