kimi-cli 0.44__py3-none-any.whl → 0.78__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 +349 -40
- kimi_cli/__init__.py +6 -0
- kimi_cli/acp/AGENTS.md +91 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +111 -0
- kimi_cli/acp/kaos.py +270 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +335 -0
- kimi_cli/acp/session.py +445 -0
- kimi_cli/acp/tools.py +158 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/agents/default/agent.yaml +4 -4
- kimi_cli/agents/default/sub.yaml +2 -1
- kimi_cli/agents/default/system.md +79 -21
- kimi_cli/agents/okabe/agent.yaml +17 -0
- kimi_cli/agentspec.py +53 -25
- kimi_cli/app.py +180 -52
- kimi_cli/cli/__init__.py +595 -0
- kimi_cli/cli/__main__.py +8 -0
- kimi_cli/cli/info.py +63 -0
- kimi_cli/cli/mcp.py +349 -0
- kimi_cli/config.py +153 -17
- kimi_cli/constant.py +3 -0
- kimi_cli/exception.py +23 -2
- kimi_cli/flow/__init__.py +117 -0
- kimi_cli/flow/d2.py +376 -0
- kimi_cli/flow/mermaid.py +218 -0
- kimi_cli/llm.py +129 -23
- kimi_cli/metadata.py +32 -7
- kimi_cli/platforms.py +262 -0
- kimi_cli/prompts/__init__.py +2 -0
- kimi_cli/prompts/compact.md +4 -5
- kimi_cli/session.py +223 -31
- kimi_cli/share.py +2 -0
- kimi_cli/skill.py +145 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +351 -0
- kimi_cli/soul/__init__.py +51 -20
- kimi_cli/soul/agent.py +213 -85
- kimi_cli/soul/approval.py +86 -17
- kimi_cli/soul/compaction.py +64 -53
- kimi_cli/soul/context.py +38 -5
- kimi_cli/soul/denwarenji.py +2 -0
- kimi_cli/soul/kimisoul.py +442 -60
- kimi_cli/soul/message.py +54 -54
- kimi_cli/soul/slash.py +72 -0
- kimi_cli/soul/toolset.py +387 -6
- kimi_cli/toad.py +74 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +42 -34
- kimi_cli/tools/display.py +25 -0
- kimi_cli/tools/dmail/__init__.py +10 -10
- kimi_cli/tools/dmail/dmail.md +11 -9
- kimi_cli/tools/file/__init__.py +1 -3
- kimi_cli/tools/file/glob.py +20 -23
- kimi_cli/tools/file/grep.md +1 -1
- kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
- kimi_cli/tools/file/read.md +24 -6
- kimi_cli/tools/file/read.py +134 -50
- kimi_cli/tools/file/replace.md +1 -1
- kimi_cli/tools/file/replace.py +36 -29
- kimi_cli/tools/file/utils.py +282 -0
- kimi_cli/tools/file/write.py +43 -22
- kimi_cli/tools/multiagent/__init__.py +7 -0
- kimi_cli/tools/multiagent/create.md +11 -0
- kimi_cli/tools/multiagent/create.py +50 -0
- kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
- kimi_cli/tools/shell/__init__.py +120 -0
- kimi_cli/tools/{bash → shell}/bash.md +1 -2
- kimi_cli/tools/shell/powershell.md +25 -0
- kimi_cli/tools/test.py +4 -4
- kimi_cli/tools/think/__init__.py +2 -2
- kimi_cli/tools/todo/__init__.py +14 -8
- kimi_cli/tools/utils.py +64 -24
- kimi_cli/tools/web/fetch.py +68 -13
- kimi_cli/tools/web/search.py +10 -12
- kimi_cli/ui/acp/__init__.py +65 -412
- kimi_cli/ui/print/__init__.py +37 -49
- kimi_cli/ui/print/visualize.py +179 -0
- kimi_cli/ui/shell/__init__.py +141 -84
- kimi_cli/ui/shell/console.py +2 -0
- kimi_cli/ui/shell/debug.py +28 -23
- kimi_cli/ui/shell/keyboard.py +5 -1
- kimi_cli/ui/shell/prompt.py +220 -194
- kimi_cli/ui/shell/replay.py +111 -46
- kimi_cli/ui/shell/setup.py +89 -82
- kimi_cli/ui/shell/slash.py +422 -0
- kimi_cli/ui/shell/update.py +4 -2
- kimi_cli/ui/shell/usage.py +271 -0
- kimi_cli/ui/shell/visualize.py +574 -72
- kimi_cli/ui/wire/__init__.py +267 -0
- kimi_cli/ui/wire/jsonrpc.py +142 -0
- kimi_cli/ui/wire/protocol.py +1 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +2 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +12 -7
- kimi_cli/utils/clipboard.py +12 -0
- kimi_cli/utils/datetime.py +37 -0
- kimi_cli/utils/environment.py +58 -0
- kimi_cli/utils/envvar.py +12 -0
- kimi_cli/utils/frontmatter.py +44 -0
- kimi_cli/utils/logging.py +7 -6
- kimi_cli/utils/message.py +9 -14
- kimi_cli/utils/path.py +99 -9
- kimi_cli/utils/pyinstaller.py +6 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/markdown.py +961 -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 +2 -0
- kimi_cli/utils/slashcmd.py +124 -0
- kimi_cli/utils/string.py +2 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/wire/__init__.py +98 -29
- kimi_cli/wire/serde.py +45 -0
- kimi_cli/wire/types.py +299 -0
- kimi_cli-0.78.dist-info/METADATA +200 -0
- kimi_cli-0.78.dist-info/RECORD +135 -0
- kimi_cli-0.78.dist-info/entry_points.txt +4 -0
- kimi_cli/cli.py +0 -250
- kimi_cli/soul/runtime.py +0 -96
- kimi_cli/tools/bash/__init__.py +0 -99
- kimi_cli/tools/file/patch.md +0 -8
- kimi_cli/tools/file/patch.py +0 -143
- kimi_cli/tools/mcp.py +0 -85
- kimi_cli/ui/shell/liveview.py +0 -386
- kimi_cli/ui/shell/metacmd.py +0 -262
- kimi_cli/wire/message.py +0 -91
- kimi_cli-0.44.dist-info/METADATA +0 -188
- kimi_cli-0.44.dist-info/RECORD +0 -89
- kimi_cli-0.44.dist-info/entry_points.txt +0 -3
- /kimi_cli/tools/{task → multiagent}/task.md +0 -0
- {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
kimi_cli/soul/agent.py
CHANGED
|
@@ -1,41 +1,211 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
3
4
|
import string
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from datetime import datetime
|
|
4
8
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
import pydantic
|
|
12
|
+
from kaos.path import KaosPath
|
|
13
|
+
from kosong.tooling import Toolset
|
|
8
14
|
|
|
9
|
-
from kimi_cli.agentspec import
|
|
15
|
+
from kimi_cli.agentspec import load_agent_spec
|
|
10
16
|
from kimi_cli.config import Config
|
|
17
|
+
from kimi_cli.exception import MCPConfigError
|
|
18
|
+
from kimi_cli.llm import LLM
|
|
11
19
|
from kimi_cli.session import Session
|
|
20
|
+
from kimi_cli.skill import (
|
|
21
|
+
Skill,
|
|
22
|
+
discover_skills_from_roots,
|
|
23
|
+
get_builtin_skills_dir,
|
|
24
|
+
get_claude_skills_dir,
|
|
25
|
+
get_skills_dir,
|
|
26
|
+
index_skills,
|
|
27
|
+
)
|
|
12
28
|
from kimi_cli.soul.approval import Approval
|
|
13
29
|
from kimi_cli.soul.denwarenji import DenwaRenji
|
|
14
|
-
from kimi_cli.soul.
|
|
15
|
-
from kimi_cli.
|
|
30
|
+
from kimi_cli.soul.toolset import KimiToolset
|
|
31
|
+
from kimi_cli.utils.environment import Environment
|
|
16
32
|
from kimi_cli.utils.logging import logger
|
|
33
|
+
from kimi_cli.utils.path import list_directory
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from fastmcp.mcp_config import MCPConfig
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
40
|
+
class BuiltinSystemPromptArgs:
|
|
41
|
+
"""Builtin system prompt arguments."""
|
|
42
|
+
|
|
43
|
+
KIMI_NOW: str
|
|
44
|
+
"""The current datetime."""
|
|
45
|
+
KIMI_WORK_DIR: KaosPath
|
|
46
|
+
"""The absolute path of current working directory."""
|
|
47
|
+
KIMI_WORK_DIR_LS: str
|
|
48
|
+
"""The directory listing of current working directory."""
|
|
49
|
+
KIMI_AGENTS_MD: str # TODO: move to first message from system prompt
|
|
50
|
+
"""The content of AGENTS.md."""
|
|
51
|
+
KIMI_SKILLS: str
|
|
52
|
+
"""Formatted information about available skills."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def load_agents_md(work_dir: KaosPath) -> str | None:
|
|
56
|
+
paths = [
|
|
57
|
+
work_dir / "AGENTS.md",
|
|
58
|
+
work_dir / "agents.md",
|
|
59
|
+
]
|
|
60
|
+
for path in paths:
|
|
61
|
+
if await path.is_file():
|
|
62
|
+
logger.info("Loaded agents.md: {path}", path=path)
|
|
63
|
+
return (await path.read_text()).strip()
|
|
64
|
+
logger.info("No AGENTS.md found in {work_dir}", work_dir=work_dir)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(slots=True, kw_only=True)
|
|
69
|
+
class Runtime:
|
|
70
|
+
"""Agent runtime."""
|
|
71
|
+
|
|
72
|
+
config: Config
|
|
73
|
+
llm: LLM | None # we do not freeze the `Runtime` dataclass because LLM can be changed
|
|
74
|
+
session: Session
|
|
75
|
+
builtin_args: BuiltinSystemPromptArgs
|
|
76
|
+
denwa_renji: DenwaRenji
|
|
77
|
+
approval: Approval
|
|
78
|
+
labor_market: LaborMarket
|
|
79
|
+
environment: Environment
|
|
80
|
+
skills: dict[str, Skill]
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
async def create(
|
|
84
|
+
config: Config,
|
|
85
|
+
llm: LLM | None,
|
|
86
|
+
session: Session,
|
|
87
|
+
yolo: bool,
|
|
88
|
+
skills_dir: Path | None = None,
|
|
89
|
+
) -> Runtime:
|
|
90
|
+
ls_output, agents_md, environment = await asyncio.gather(
|
|
91
|
+
list_directory(session.work_dir),
|
|
92
|
+
load_agents_md(session.work_dir),
|
|
93
|
+
Environment.detect(),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Discover and format skills
|
|
97
|
+
builtin_skills_dir = get_builtin_skills_dir()
|
|
98
|
+
if skills_dir is None:
|
|
99
|
+
skills_dir = get_skills_dir()
|
|
100
|
+
if not skills_dir.is_dir() and (claude_skills_dir := get_claude_skills_dir()).is_dir():
|
|
101
|
+
skills_dir = claude_skills_dir
|
|
102
|
+
skills_roots = [builtin_skills_dir, skills_dir]
|
|
103
|
+
skills = discover_skills_from_roots(skills_roots)
|
|
104
|
+
skills_by_name = index_skills(skills)
|
|
105
|
+
logger.info("Discovered {count} skill(s)", count=len(skills))
|
|
106
|
+
skills_formatted = "\n".join(
|
|
107
|
+
(
|
|
108
|
+
f"- {skill.name}\n"
|
|
109
|
+
f" - Path: {skill.skill_md_file}\n"
|
|
110
|
+
f" - Description: {skill.description}"
|
|
111
|
+
)
|
|
112
|
+
for skill in skills
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return Runtime(
|
|
116
|
+
config=config,
|
|
117
|
+
llm=llm,
|
|
118
|
+
session=session,
|
|
119
|
+
builtin_args=BuiltinSystemPromptArgs(
|
|
120
|
+
KIMI_NOW=datetime.now().astimezone().isoformat(),
|
|
121
|
+
KIMI_WORK_DIR=session.work_dir,
|
|
122
|
+
KIMI_WORK_DIR_LS=ls_output,
|
|
123
|
+
KIMI_AGENTS_MD=agents_md or "",
|
|
124
|
+
KIMI_SKILLS=skills_formatted or "No skills found.",
|
|
125
|
+
),
|
|
126
|
+
denwa_renji=DenwaRenji(),
|
|
127
|
+
approval=Approval(yolo=yolo),
|
|
128
|
+
labor_market=LaborMarket(),
|
|
129
|
+
environment=environment,
|
|
130
|
+
skills=skills_by_name,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def copy_for_fixed_subagent(self) -> Runtime:
|
|
134
|
+
"""Clone runtime for fixed subagent."""
|
|
135
|
+
return Runtime(
|
|
136
|
+
config=self.config,
|
|
137
|
+
llm=self.llm,
|
|
138
|
+
session=self.session,
|
|
139
|
+
builtin_args=self.builtin_args,
|
|
140
|
+
denwa_renji=DenwaRenji(), # subagent must have its own DenwaRenji
|
|
141
|
+
approval=self.approval,
|
|
142
|
+
labor_market=LaborMarket(), # fixed subagent has its own LaborMarket
|
|
143
|
+
environment=self.environment,
|
|
144
|
+
skills=self.skills,
|
|
145
|
+
)
|
|
17
146
|
|
|
147
|
+
def copy_for_dynamic_subagent(self) -> Runtime:
|
|
148
|
+
"""Clone runtime for dynamic subagent."""
|
|
149
|
+
return Runtime(
|
|
150
|
+
config=self.config,
|
|
151
|
+
llm=self.llm,
|
|
152
|
+
session=self.session,
|
|
153
|
+
builtin_args=self.builtin_args,
|
|
154
|
+
denwa_renji=DenwaRenji(), # subagent must have its own DenwaRenji
|
|
155
|
+
approval=self.approval,
|
|
156
|
+
labor_market=self.labor_market, # dynamic subagent shares LaborMarket with main agent
|
|
157
|
+
environment=self.environment,
|
|
158
|
+
skills=self.skills,
|
|
159
|
+
)
|
|
18
160
|
|
|
19
|
-
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
163
|
+
class Agent:
|
|
20
164
|
"""The loaded agent."""
|
|
21
165
|
|
|
22
166
|
name: str
|
|
23
167
|
system_prompt: str
|
|
24
168
|
toolset: Toolset
|
|
169
|
+
runtime: Runtime
|
|
170
|
+
"""Each agent has its own runtime, which should be derived from its main agent."""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class LaborMarket:
|
|
174
|
+
def __init__(self):
|
|
175
|
+
self.fixed_subagents: dict[str, Agent] = {}
|
|
176
|
+
self.fixed_subagent_descs: dict[str, str] = {}
|
|
177
|
+
self.dynamic_subagents: dict[str, Agent] = {}
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def subagents(self) -> Mapping[str, Agent]:
|
|
181
|
+
"""Get all subagents in the labor market."""
|
|
182
|
+
return {**self.fixed_subagents, **self.dynamic_subagents}
|
|
183
|
+
|
|
184
|
+
def add_fixed_subagent(self, name: str, agent: Agent, description: str):
|
|
185
|
+
"""Add a fixed subagent."""
|
|
186
|
+
self.fixed_subagents[name] = agent
|
|
187
|
+
self.fixed_subagent_descs[name] = description
|
|
188
|
+
|
|
189
|
+
def add_dynamic_subagent(self, name: str, agent: Agent):
|
|
190
|
+
"""Add a dynamic subagent."""
|
|
191
|
+
self.dynamic_subagents[name] = agent
|
|
25
192
|
|
|
26
193
|
|
|
27
194
|
async def load_agent(
|
|
28
195
|
agent_file: Path,
|
|
29
196
|
runtime: Runtime,
|
|
30
197
|
*,
|
|
31
|
-
mcp_configs: list[dict[str, Any]],
|
|
198
|
+
mcp_configs: list[MCPConfig] | list[dict[str, Any]],
|
|
32
199
|
) -> Agent:
|
|
33
200
|
"""
|
|
34
201
|
Load agent from specification file.
|
|
35
202
|
|
|
36
203
|
Raises:
|
|
37
|
-
FileNotFoundError:
|
|
38
|
-
AgentSpecError:
|
|
204
|
+
FileNotFoundError: When the agent file is not found.
|
|
205
|
+
AgentSpecError(KimiCLIException, ValueError): When the agent specification is invalid.
|
|
206
|
+
InvalidToolError(KimiCLIException, ValueError): When any tool cannot be loaded.
|
|
207
|
+
MCPConfigError(KimiCLIException, ValueError): When any MCP configuration is invalid.
|
|
208
|
+
MCPRuntimeError(KimiCLIException, RuntimeError): When any MCP server cannot be connected.
|
|
39
209
|
"""
|
|
40
210
|
logger.info("Loading agent: {agent_file}", agent_file=agent_file)
|
|
41
211
|
agent_spec = load_agent_spec(agent_file)
|
|
@@ -46,32 +216,56 @@ async def load_agent(
|
|
|
46
216
|
runtime.builtin_args,
|
|
47
217
|
)
|
|
48
218
|
|
|
219
|
+
# load subagents before loading tools because Task tool depends on LaborMarket on initialization
|
|
220
|
+
for subagent_name, subagent_spec in agent_spec.subagents.items():
|
|
221
|
+
logger.debug("Loading subagent: {subagent_name}", subagent_name=subagent_name)
|
|
222
|
+
subagent = await load_agent(
|
|
223
|
+
subagent_spec.path,
|
|
224
|
+
runtime.copy_for_fixed_subagent(),
|
|
225
|
+
mcp_configs=mcp_configs,
|
|
226
|
+
)
|
|
227
|
+
runtime.labor_market.add_fixed_subagent(subagent_name, subagent, subagent_spec.description)
|
|
228
|
+
|
|
229
|
+
toolset = KimiToolset()
|
|
49
230
|
tool_deps = {
|
|
50
|
-
|
|
231
|
+
KimiToolset: toolset,
|
|
51
232
|
Runtime: runtime,
|
|
233
|
+
# TODO: remove all the following dependencies and use Runtime instead
|
|
52
234
|
Config: runtime.config,
|
|
53
235
|
BuiltinSystemPromptArgs: runtime.builtin_args,
|
|
54
236
|
Session: runtime.session,
|
|
55
237
|
DenwaRenji: runtime.denwa_renji,
|
|
56
238
|
Approval: runtime.approval,
|
|
239
|
+
LaborMarket: runtime.labor_market,
|
|
240
|
+
Environment: runtime.environment,
|
|
57
241
|
}
|
|
58
242
|
tools = agent_spec.tools
|
|
59
243
|
if agent_spec.exclude_tools:
|
|
60
244
|
logger.debug("Excluding tools: {tools}", tools=agent_spec.exclude_tools)
|
|
61
245
|
tools = [tool for tool in tools if tool not in agent_spec.exclude_tools]
|
|
62
|
-
toolset
|
|
63
|
-
bad_tools = _load_tools(toolset, tools, tool_deps)
|
|
64
|
-
if bad_tools:
|
|
65
|
-
raise ValueError(f"Invalid tools: {bad_tools}")
|
|
246
|
+
toolset.load_tools(tools, tool_deps)
|
|
66
247
|
|
|
67
|
-
assert isinstance(toolset, CustomToolset)
|
|
68
248
|
if mcp_configs:
|
|
69
|
-
|
|
249
|
+
validated_mcp_configs: list[MCPConfig] = []
|
|
250
|
+
if mcp_configs:
|
|
251
|
+
from fastmcp.mcp_config import MCPConfig
|
|
252
|
+
|
|
253
|
+
for mcp_config in mcp_configs:
|
|
254
|
+
try:
|
|
255
|
+
validated_mcp_configs.append(
|
|
256
|
+
mcp_config
|
|
257
|
+
if isinstance(mcp_config, MCPConfig)
|
|
258
|
+
else MCPConfig.model_validate(mcp_config)
|
|
259
|
+
)
|
|
260
|
+
except pydantic.ValidationError as e:
|
|
261
|
+
raise MCPConfigError(f"Invalid MCP config: {e}") from e
|
|
262
|
+
await toolset.load_mcp_tools(validated_mcp_configs, runtime)
|
|
70
263
|
|
|
71
264
|
return Agent(
|
|
72
265
|
name=agent_spec.name,
|
|
73
266
|
system_prompt=system_prompt,
|
|
74
267
|
toolset=toolset,
|
|
268
|
+
runtime=runtime,
|
|
75
269
|
)
|
|
76
270
|
|
|
77
271
|
|
|
@@ -85,70 +279,4 @@ def _load_system_prompt(
|
|
|
85
279
|
builtin_args=builtin_args,
|
|
86
280
|
spec_args=args,
|
|
87
281
|
)
|
|
88
|
-
return string.Template(system_prompt).substitute(builtin_args
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
type ToolType = CallableTool | CallableTool2[Any]
|
|
92
|
-
# TODO: move this to kosong.tooling.simple
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _load_tools(
|
|
96
|
-
toolset: CustomToolset,
|
|
97
|
-
tool_paths: list[str],
|
|
98
|
-
dependencies: dict[type[Any], Any],
|
|
99
|
-
) -> list[str]:
|
|
100
|
-
bad_tools: list[str] = []
|
|
101
|
-
for tool_path in tool_paths:
|
|
102
|
-
tool = _load_tool(tool_path, dependencies)
|
|
103
|
-
if tool:
|
|
104
|
-
toolset += tool
|
|
105
|
-
else:
|
|
106
|
-
bad_tools.append(tool_path)
|
|
107
|
-
logger.info("Loaded tools: {tools}", tools=[tool.name for tool in toolset.tools])
|
|
108
|
-
if bad_tools:
|
|
109
|
-
logger.error("Bad tools: {bad_tools}", bad_tools=bad_tools)
|
|
110
|
-
return bad_tools
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _load_tool(tool_path: str, dependencies: dict[type[Any], Any]) -> ToolType | None:
|
|
114
|
-
logger.debug("Loading tool: {tool_path}", tool_path=tool_path)
|
|
115
|
-
module_name, class_name = tool_path.rsplit(":", 1)
|
|
116
|
-
try:
|
|
117
|
-
module = importlib.import_module(module_name)
|
|
118
|
-
except ImportError:
|
|
119
|
-
return None
|
|
120
|
-
cls = getattr(module, class_name, None)
|
|
121
|
-
if cls is None:
|
|
122
|
-
return None
|
|
123
|
-
args: list[type[Any]] = []
|
|
124
|
-
for param in inspect.signature(cls).parameters.values():
|
|
125
|
-
if param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
126
|
-
# once we encounter a keyword-only parameter, we stop injecting dependencies
|
|
127
|
-
break
|
|
128
|
-
# all positional parameters should be dependencies to be injected
|
|
129
|
-
if param.annotation not in dependencies:
|
|
130
|
-
raise ValueError(f"Tool dependency not found: {param.annotation}")
|
|
131
|
-
args.append(dependencies[param.annotation])
|
|
132
|
-
return cls(*args)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
async def _load_mcp_tools(
|
|
136
|
-
toolset: CustomToolset,
|
|
137
|
-
mcp_configs: list[dict[str, Any]],
|
|
138
|
-
):
|
|
139
|
-
"""
|
|
140
|
-
Raises:
|
|
141
|
-
ValueError: If the MCP config is not valid.
|
|
142
|
-
RuntimeError: If the MCP server cannot be connected.
|
|
143
|
-
"""
|
|
144
|
-
import fastmcp
|
|
145
|
-
|
|
146
|
-
from kimi_cli.tools.mcp import MCPTool
|
|
147
|
-
|
|
148
|
-
for mcp_config in mcp_configs:
|
|
149
|
-
logger.info("Loading MCP tools from: {mcp_config}", mcp_config=mcp_config)
|
|
150
|
-
client = fastmcp.Client(mcp_config)
|
|
151
|
-
async with client:
|
|
152
|
-
for tool in await client.list_tools():
|
|
153
|
-
toolset += MCPTool(tool, client)
|
|
154
|
-
return toolset
|
|
282
|
+
return string.Template(system_prompt).substitute(asdict(builtin_args), **args)
|
kimi_cli/soul/approval.py
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
2
7
|
|
|
3
8
|
from kimi_cli.soul.toolset import get_current_tool_call_or_none
|
|
9
|
+
from kimi_cli.utils.aioqueue import Queue
|
|
4
10
|
from kimi_cli.utils.logging import logger
|
|
5
|
-
from kimi_cli.wire.
|
|
11
|
+
from kimi_cli.wire.types import DisplayBlock
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
15
|
+
class Request:
|
|
16
|
+
id: str
|
|
17
|
+
tool_call_id: str
|
|
18
|
+
sender: str
|
|
19
|
+
action: str
|
|
20
|
+
description: str
|
|
21
|
+
display: list[DisplayBlock]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
type Response = Literal["approve", "approve_for_session", "reject"]
|
|
6
25
|
|
|
7
26
|
|
|
8
27
|
class Approval:
|
|
9
28
|
def __init__(self, yolo: bool = False):
|
|
10
|
-
self._request_queue =
|
|
29
|
+
self._request_queue = Queue[Request]()
|
|
30
|
+
self._requests: dict[str, tuple[Request, asyncio.Future[bool]]] = {}
|
|
11
31
|
self._yolo = yolo
|
|
12
32
|
self._auto_approve_actions: set[str] = set() # TODO: persist across sessions
|
|
13
33
|
"""Set of action names that should automatically be approved."""
|
|
@@ -15,7 +35,16 @@ class Approval:
|
|
|
15
35
|
def set_yolo(self, yolo: bool) -> None:
|
|
16
36
|
self._yolo = yolo
|
|
17
37
|
|
|
18
|
-
|
|
38
|
+
def is_yolo(self) -> bool:
|
|
39
|
+
return self._yolo
|
|
40
|
+
|
|
41
|
+
async def request(
|
|
42
|
+
self,
|
|
43
|
+
sender: str,
|
|
44
|
+
action: str,
|
|
45
|
+
description: str,
|
|
46
|
+
display: list[DisplayBlock] | None = None,
|
|
47
|
+
) -> bool:
|
|
19
48
|
"""
|
|
20
49
|
Request approval for the given action. Intended to be called by tools.
|
|
21
50
|
|
|
@@ -48,21 +77,61 @@ class Approval:
|
|
|
48
77
|
if action in self._auto_approve_actions:
|
|
49
78
|
return True
|
|
50
79
|
|
|
51
|
-
request =
|
|
80
|
+
request = Request(
|
|
81
|
+
id=str(uuid.uuid4()),
|
|
82
|
+
tool_call_id=tool_call.id,
|
|
83
|
+
sender=sender,
|
|
84
|
+
action=action,
|
|
85
|
+
description=description,
|
|
86
|
+
display=display or [],
|
|
87
|
+
)
|
|
88
|
+
approved_future = asyncio.Future[bool]()
|
|
52
89
|
self._request_queue.put_nowait(request)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return True
|
|
58
|
-
case ApprovalResponse.APPROVE_FOR_SESSION:
|
|
59
|
-
self._auto_approve_actions.add(action)
|
|
60
|
-
return True
|
|
61
|
-
case ApprovalResponse.REJECT:
|
|
62
|
-
return False
|
|
63
|
-
|
|
64
|
-
async def fetch_request(self) -> ApprovalRequest:
|
|
90
|
+
self._requests[request.id] = (request, approved_future)
|
|
91
|
+
return await approved_future
|
|
92
|
+
|
|
93
|
+
async def fetch_request(self) -> Request:
|
|
65
94
|
"""
|
|
66
95
|
Fetch an approval request from the queue. Intended to be called by the soul.
|
|
67
96
|
"""
|
|
68
|
-
|
|
97
|
+
while True:
|
|
98
|
+
request = await self._request_queue.get()
|
|
99
|
+
if request.action in self._auto_approve_actions:
|
|
100
|
+
# the action is not auto-approved when the request was created, but now it should be
|
|
101
|
+
logger.debug(
|
|
102
|
+
"Auto-approving previously requested action: {action}", action=request.action
|
|
103
|
+
)
|
|
104
|
+
self.resolve_request(request.id, "approve")
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
return request
|
|
108
|
+
|
|
109
|
+
def resolve_request(self, request_id: str, response: Response) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Resolve an approval request with the given response. Intended to be called by the soul.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
request_id (str): The ID of the request to resolve.
|
|
115
|
+
response (Response): The response to the request.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
KeyError: If there is no pending request with the given ID.
|
|
119
|
+
"""
|
|
120
|
+
request_tuple = self._requests.pop(request_id, None)
|
|
121
|
+
if request_tuple is None:
|
|
122
|
+
raise KeyError(f"No pending request with ID {request_id}")
|
|
123
|
+
request, future = request_tuple
|
|
124
|
+
|
|
125
|
+
logger.debug(
|
|
126
|
+
"Received approval response for request {request_id}: {response}",
|
|
127
|
+
request_id=request_id,
|
|
128
|
+
response=response,
|
|
129
|
+
)
|
|
130
|
+
match response:
|
|
131
|
+
case "approve":
|
|
132
|
+
future.set_result(True)
|
|
133
|
+
case "approve_for_session":
|
|
134
|
+
self._auto_approve_actions.add(request.action)
|
|
135
|
+
future.set_result(True)
|
|
136
|
+
case "reject":
|
|
137
|
+
future.set_result(False)
|
kimi_cli/soul/compaction.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections.abc import Sequence
|
|
2
|
-
from
|
|
3
|
-
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
4
|
+
from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
from kosong.
|
|
6
|
+
import kosong
|
|
7
|
+
from kosong.message import Message
|
|
8
|
+
from kosong.tooling.empty import EmptyToolset
|
|
7
9
|
|
|
8
10
|
import kimi_cli.prompts as prompts
|
|
9
11
|
from kimi_cli.llm import LLM
|
|
10
12
|
from kimi_cli.soul.message import system
|
|
11
13
|
from kimi_cli.utils.logging import logger
|
|
14
|
+
from kimi_cli.wire.types import ContentPart, TextPart, ThinkPart
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
@runtime_checkable
|
|
@@ -30,76 +33,84 @@ class Compaction(Protocol):
|
|
|
30
33
|
...
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
|
|
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
|
|
36
|
+
if TYPE_CHECKING:
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
38
|
+
def type_check(simple: SimpleCompaction):
|
|
39
|
+
_: Compaction = simple
|
|
49
40
|
|
|
50
|
-
if n_preserved < self.MAX_PRESERVED_MESSAGES:
|
|
51
|
-
return history
|
|
52
41
|
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
class SimpleCompaction:
|
|
43
|
+
def __init__(self, max_preserved_messages: int = 2) -> None:
|
|
44
|
+
self.max_preserved_messages = max_preserved_messages
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
async def compact(self, messages: Sequence[Message], llm: LLM) -> Sequence[Message]:
|
|
47
|
+
compact_message, to_preserve = self.prepare(messages)
|
|
48
|
+
if compact_message is None:
|
|
58
49
|
return to_preserve
|
|
59
50
|
|
|
60
|
-
#
|
|
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
|
|
51
|
+
# Call kosong.step to get the compacted context
|
|
74
52
|
# TODO: set max completion tokens
|
|
75
53
|
logger.debug("Compacting context...")
|
|
76
|
-
|
|
54
|
+
result = await kosong.step(
|
|
77
55
|
chat_provider=llm.chat_provider,
|
|
78
56
|
system_prompt="You are a helpful assistant that compacts conversation context.",
|
|
79
|
-
|
|
57
|
+
toolset=EmptyToolset(),
|
|
80
58
|
history=[compact_message],
|
|
81
59
|
)
|
|
82
|
-
if usage:
|
|
60
|
+
if result.usage:
|
|
83
61
|
logger.debug(
|
|
84
62
|
"Compaction used {input} input tokens and {output} output tokens",
|
|
85
|
-
input=usage.input,
|
|
86
|
-
output=usage.output,
|
|
63
|
+
input=result.usage.input,
|
|
64
|
+
output=result.usage.output,
|
|
87
65
|
)
|
|
88
66
|
|
|
89
67
|
content: list[ContentPart] = [
|
|
90
68
|
system("Previous context has been compacted. Here is the compaction output:")
|
|
91
69
|
]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
compacted_messages: list[Message] = [Message(role="assistant", content=content)]
|
|
70
|
+
compacted_msg = result.message
|
|
71
|
+
|
|
72
|
+
# drop thinking parts if any
|
|
73
|
+
content.extend(part for part in compacted_msg.content if not isinstance(part, ThinkPart))
|
|
74
|
+
compacted_messages: list[Message] = [Message(role="user", content=content)]
|
|
98
75
|
compacted_messages.extend(to_preserve)
|
|
99
76
|
return compacted_messages
|
|
100
77
|
|
|
78
|
+
class PrepareResult(NamedTuple):
|
|
79
|
+
compact_message: Message | None
|
|
80
|
+
to_preserve: Sequence[Message]
|
|
101
81
|
|
|
102
|
-
|
|
82
|
+
def prepare(self, messages: Sequence[Message]) -> PrepareResult:
|
|
83
|
+
if not messages or self.max_preserved_messages <= 0:
|
|
84
|
+
return self.PrepareResult(compact_message=None, to_preserve=messages)
|
|
103
85
|
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
history = list(messages)
|
|
87
|
+
preserve_start_index = len(history)
|
|
88
|
+
n_preserved = 0
|
|
89
|
+
for index in range(len(history) - 1, -1, -1):
|
|
90
|
+
if history[index].role in {"user", "assistant"}:
|
|
91
|
+
n_preserved += 1
|
|
92
|
+
if n_preserved == self.max_preserved_messages:
|
|
93
|
+
preserve_start_index = index
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
if n_preserved < self.max_preserved_messages:
|
|
97
|
+
return self.PrepareResult(compact_message=None, to_preserve=messages)
|
|
98
|
+
|
|
99
|
+
to_compact = history[:preserve_start_index]
|
|
100
|
+
to_preserve = history[preserve_start_index:]
|
|
101
|
+
|
|
102
|
+
if not to_compact:
|
|
103
|
+
# Let's hope this won't exceed the context size limit
|
|
104
|
+
return self.PrepareResult(compact_message=None, to_preserve=to_preserve)
|
|
105
|
+
|
|
106
|
+
# Create input message for compaction
|
|
107
|
+
compact_message = Message(role="user", content=[])
|
|
108
|
+
for i, msg in enumerate(to_compact):
|
|
109
|
+
compact_message.content.append(
|
|
110
|
+
TextPart(text=f"## Message {i + 1}\nRole: {msg.role}\nContent:\n")
|
|
111
|
+
)
|
|
112
|
+
compact_message.content.extend(
|
|
113
|
+
part for part in msg.content if not isinstance(part, ThinkPart)
|
|
114
|
+
)
|
|
115
|
+
compact_message.content.append(TextPart(text="\n" + prompts.COMPACT))
|
|
116
|
+
return self.PrepareResult(compact_message=compact_message, to_preserve=to_preserve)
|