kimi-cli 0.40__py3-none-any.whl → 0.42__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 +27 -0
- kimi_cli/__init__.py +127 -359
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/cli.py +249 -0
- kimi_cli/config.py +28 -14
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +70 -0
- kimi_cli/metadata.py +5 -68
- kimi_cli/prompts/__init__.py +2 -2
- kimi_cli/session.py +81 -0
- kimi_cli/soul/__init__.py +102 -6
- kimi_cli/soul/agent.py +152 -0
- kimi_cli/soul/approval.py +1 -1
- kimi_cli/soul/compaction.py +4 -4
- kimi_cli/soul/kimisoul.py +39 -46
- kimi_cli/soul/runtime.py +94 -0
- 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 +48 -40
- 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 +5 -2
- kimi_cli/ui/__init__.py +0 -69
- kimi_cli/ui/acp/__init__.py +8 -9
- kimi_cli/ui/print/__init__.py +21 -37
- kimi_cli/ui/shell/__init__.py +8 -19
- kimi_cli/ui/shell/liveview.py +1 -1
- kimi_cli/ui/shell/metacmd.py +5 -10
- kimi_cli/ui/shell/prompt.py +10 -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 +69 -0
- kimi_cli/{soul/wire.py → wire/message.py} +4 -39
- {kimi_cli-0.40.dist-info → kimi_cli-0.42.dist-info}/METADATA +51 -18
- kimi_cli-0.42.dist-info/RECORD +86 -0
- kimi_cli-0.42.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- 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/agents/{koder → default}/sub.yaml +0 -0
- {kimi_cli-0.40.dist-info → kimi_cli-0.42.dist-info}/WHEEL +0 -0
kimi_cli/soul/__init__.py
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
from collections.abc import Callable, Coroutine
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
from typing import Any, NamedTuple, Protocol, runtime_checkable
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
from kimi_cli.utils.logging import logger
|
|
8
|
+
from kimi_cli.wire import Wire, WireUISide
|
|
9
|
+
from kimi_cli.wire.message import WireMessage
|
|
5
10
|
|
|
6
11
|
|
|
7
12
|
class LLMNotSet(Exception):
|
|
@@ -42,13 +47,12 @@ class Soul(Protocol):
|
|
|
42
47
|
"""The current status of the soul. The returned value is immutable."""
|
|
43
48
|
...
|
|
44
49
|
|
|
45
|
-
async def run(self, user_input: str
|
|
50
|
+
async def run(self, user_input: str):
|
|
46
51
|
"""
|
|
47
|
-
Run the agent with the given user input.
|
|
52
|
+
Run the agent with the given user input until the max steps or no more tool calls.
|
|
48
53
|
|
|
49
54
|
Args:
|
|
50
55
|
user_input (str): The user input to the agent.
|
|
51
|
-
wire (Wire): The wire to send events and requests to the UI loop.
|
|
52
56
|
|
|
53
57
|
Raises:
|
|
54
58
|
LLMNotSet: When the LLM is not set.
|
|
@@ -57,3 +61,95 @@ class Soul(Protocol):
|
|
|
57
61
|
asyncio.CancelledError: When the run is cancelled by user.
|
|
58
62
|
"""
|
|
59
63
|
...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
type UILoopFn = Callable[[WireUISide], Coroutine[Any, Any, None]]
|
|
67
|
+
"""A long-running async function to visualize the agent behavior."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RunCancelled(Exception):
|
|
71
|
+
"""The run was cancelled by the cancel event."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def run_soul(
|
|
75
|
+
soul: "Soul",
|
|
76
|
+
user_input: str,
|
|
77
|
+
ui_loop_fn: UILoopFn,
|
|
78
|
+
cancel_event: asyncio.Event,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Run the soul with the given user input, connecting it to the UI loop with a wire.
|
|
82
|
+
|
|
83
|
+
`cancel_event` is a outside handle that can be used to cancel the run. When the
|
|
84
|
+
event is set, the run will be gracefully stopped and a `RunCancelled` will be raised.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
LLMNotSet: When the LLM is not set.
|
|
88
|
+
ChatProviderError: When the LLM provider returns an error.
|
|
89
|
+
MaxStepsReached: When the maximum number of steps is reached.
|
|
90
|
+
RunCancelled: When the run is cancelled by the cancel event.
|
|
91
|
+
"""
|
|
92
|
+
wire = Wire()
|
|
93
|
+
wire_token = _current_wire.set(wire)
|
|
94
|
+
|
|
95
|
+
logger.debug("Starting UI loop with function: {ui_loop_fn}", ui_loop_fn=ui_loop_fn)
|
|
96
|
+
ui_task = asyncio.create_task(ui_loop_fn(wire.ui_side))
|
|
97
|
+
|
|
98
|
+
logger.debug("Starting soul run")
|
|
99
|
+
soul_task = asyncio.create_task(soul.run(user_input))
|
|
100
|
+
|
|
101
|
+
cancel_event_task = asyncio.create_task(cancel_event.wait())
|
|
102
|
+
await asyncio.wait(
|
|
103
|
+
[soul_task, cancel_event_task],
|
|
104
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
if cancel_event.is_set():
|
|
109
|
+
logger.debug("Cancelling the run task")
|
|
110
|
+
soul_task.cancel()
|
|
111
|
+
try:
|
|
112
|
+
await soul_task
|
|
113
|
+
except asyncio.CancelledError:
|
|
114
|
+
raise RunCancelled from None
|
|
115
|
+
else:
|
|
116
|
+
assert soul_task.done() # either stop event is set or the run task is done
|
|
117
|
+
cancel_event_task.cancel()
|
|
118
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
119
|
+
await cancel_event_task
|
|
120
|
+
soul_task.result() # this will raise if any exception was raised in the run task
|
|
121
|
+
finally:
|
|
122
|
+
logger.debug("Shutting down the UI loop")
|
|
123
|
+
# shutting down the wire should break the UI loop
|
|
124
|
+
wire.shutdown()
|
|
125
|
+
try:
|
|
126
|
+
await asyncio.wait_for(ui_task, timeout=0.5)
|
|
127
|
+
except asyncio.QueueShutDown:
|
|
128
|
+
# expected
|
|
129
|
+
pass
|
|
130
|
+
except TimeoutError:
|
|
131
|
+
logger.warning("UI loop timed out")
|
|
132
|
+
|
|
133
|
+
_current_wire.reset(wire_token)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_current_wire = ContextVar[Wire | None]("current_wire", default=None)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_wire_or_none() -> Wire | None:
|
|
140
|
+
"""
|
|
141
|
+
Get the current wire or None.
|
|
142
|
+
Expect to be not None when called from anywhere in the agent loop.
|
|
143
|
+
"""
|
|
144
|
+
return _current_wire.get()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def wire_send(msg: WireMessage) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Send a wire message to the current wire.
|
|
150
|
+
Take this as `print` and `input` for souls.
|
|
151
|
+
Souls should always use this function to send wire messages.
|
|
152
|
+
"""
|
|
153
|
+
wire = get_wire_or_none()
|
|
154
|
+
assert wire is not None, "Wire is expected to be set when soul is running"
|
|
155
|
+
wire.soul_side.send(msg)
|
kimi_cli/soul/agent.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import string
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, NamedTuple
|
|
6
|
+
|
|
7
|
+
import fastmcp
|
|
8
|
+
from kosong.tooling import CallableTool, CallableTool2, Toolset
|
|
9
|
+
|
|
10
|
+
from kimi_cli.agentspec import ResolvedAgentSpec, load_agent_spec
|
|
11
|
+
from kimi_cli.config import Config
|
|
12
|
+
from kimi_cli.session import Session
|
|
13
|
+
from kimi_cli.soul.approval import Approval
|
|
14
|
+
from kimi_cli.soul.denwarenji import DenwaRenji
|
|
15
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs, Runtime
|
|
16
|
+
from kimi_cli.soul.toolset import CustomToolset
|
|
17
|
+
from kimi_cli.tools.mcp import MCPTool
|
|
18
|
+
from kimi_cli.utils.logging import logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Agent(NamedTuple):
|
|
22
|
+
"""The loaded agent."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
system_prompt: str
|
|
26
|
+
toolset: Toolset
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def load_agent(
|
|
30
|
+
agent_file: Path,
|
|
31
|
+
runtime: Runtime,
|
|
32
|
+
*,
|
|
33
|
+
mcp_configs: list[dict[str, Any]],
|
|
34
|
+
) -> Agent:
|
|
35
|
+
"""
|
|
36
|
+
Load agent from specification file.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
FileNotFoundError: If the agent spec file does not exist.
|
|
40
|
+
AgentSpecError: If the agent spec is not valid.
|
|
41
|
+
"""
|
|
42
|
+
logger.info("Loading agent: {agent_file}", agent_file=agent_file)
|
|
43
|
+
agent_spec = load_agent_spec(agent_file)
|
|
44
|
+
|
|
45
|
+
system_prompt = _load_system_prompt(
|
|
46
|
+
agent_spec.system_prompt_path,
|
|
47
|
+
agent_spec.system_prompt_args,
|
|
48
|
+
runtime.builtin_args,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
tool_deps = {
|
|
52
|
+
ResolvedAgentSpec: agent_spec,
|
|
53
|
+
Runtime: runtime,
|
|
54
|
+
Config: runtime.config,
|
|
55
|
+
BuiltinSystemPromptArgs: runtime.builtin_args,
|
|
56
|
+
Session: runtime.session,
|
|
57
|
+
DenwaRenji: runtime.denwa_renji,
|
|
58
|
+
Approval: runtime.approval,
|
|
59
|
+
}
|
|
60
|
+
tools = agent_spec.tools
|
|
61
|
+
if agent_spec.exclude_tools:
|
|
62
|
+
logger.debug("Excluding tools: {tools}", tools=agent_spec.exclude_tools)
|
|
63
|
+
tools = [tool for tool in tools if tool not in agent_spec.exclude_tools]
|
|
64
|
+
toolset = CustomToolset()
|
|
65
|
+
bad_tools = _load_tools(toolset, tools, tool_deps)
|
|
66
|
+
if bad_tools:
|
|
67
|
+
raise ValueError(f"Invalid tools: {bad_tools}")
|
|
68
|
+
|
|
69
|
+
assert isinstance(toolset, CustomToolset)
|
|
70
|
+
if mcp_configs:
|
|
71
|
+
await _load_mcp_tools(toolset, mcp_configs)
|
|
72
|
+
|
|
73
|
+
return Agent(
|
|
74
|
+
name=agent_spec.name,
|
|
75
|
+
system_prompt=system_prompt,
|
|
76
|
+
toolset=toolset,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _load_system_prompt(
|
|
81
|
+
path: Path, args: dict[str, str], builtin_args: BuiltinSystemPromptArgs
|
|
82
|
+
) -> str:
|
|
83
|
+
logger.info("Loading system prompt: {path}", path=path)
|
|
84
|
+
system_prompt = path.read_text(encoding="utf-8").strip()
|
|
85
|
+
logger.debug(
|
|
86
|
+
"Substituting system prompt with builtin args: {builtin_args}, spec args: {spec_args}",
|
|
87
|
+
builtin_args=builtin_args,
|
|
88
|
+
spec_args=args,
|
|
89
|
+
)
|
|
90
|
+
return string.Template(system_prompt).substitute(builtin_args._asdict(), **args)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
type ToolType = CallableTool | CallableTool2[Any]
|
|
94
|
+
# TODO: move this to kosong.tooling.simple
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _load_tools(
|
|
98
|
+
toolset: CustomToolset,
|
|
99
|
+
tool_paths: list[str],
|
|
100
|
+
dependencies: dict[type[Any], Any],
|
|
101
|
+
) -> list[str]:
|
|
102
|
+
bad_tools: list[str] = []
|
|
103
|
+
for tool_path in tool_paths:
|
|
104
|
+
tool = _load_tool(tool_path, dependencies)
|
|
105
|
+
if tool:
|
|
106
|
+
toolset += tool
|
|
107
|
+
else:
|
|
108
|
+
bad_tools.append(tool_path)
|
|
109
|
+
logger.info("Loaded tools: {tools}", tools=[tool.name for tool in toolset.tools])
|
|
110
|
+
if bad_tools:
|
|
111
|
+
logger.error("Bad tools: {bad_tools}", bad_tools=bad_tools)
|
|
112
|
+
return bad_tools
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _load_tool(tool_path: str, dependencies: dict[type[Any], Any]) -> ToolType | None:
|
|
116
|
+
logger.debug("Loading tool: {tool_path}", tool_path=tool_path)
|
|
117
|
+
module_name, class_name = tool_path.rsplit(":", 1)
|
|
118
|
+
try:
|
|
119
|
+
module = importlib.import_module(module_name)
|
|
120
|
+
except ImportError:
|
|
121
|
+
return None
|
|
122
|
+
cls = getattr(module, class_name, None)
|
|
123
|
+
if cls is None:
|
|
124
|
+
return None
|
|
125
|
+
args: list[type[Any]] = []
|
|
126
|
+
for param in inspect.signature(cls).parameters.values():
|
|
127
|
+
if param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
128
|
+
# once we encounter a keyword-only parameter, we stop injecting dependencies
|
|
129
|
+
break
|
|
130
|
+
# all positional parameters should be dependencies to be injected
|
|
131
|
+
if param.annotation not in dependencies:
|
|
132
|
+
raise ValueError(f"Tool dependency not found: {param.annotation}")
|
|
133
|
+
args.append(dependencies[param.annotation])
|
|
134
|
+
return cls(*args)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def _load_mcp_tools(
|
|
138
|
+
toolset: CustomToolset,
|
|
139
|
+
mcp_configs: list[dict[str, Any]],
|
|
140
|
+
):
|
|
141
|
+
"""
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If the MCP config is not valid.
|
|
144
|
+
RuntimeError: If the MCP server cannot be connected.
|
|
145
|
+
"""
|
|
146
|
+
for mcp_config in mcp_configs:
|
|
147
|
+
logger.info("Loading MCP tools from: {mcp_config}", mcp_config=mcp_config)
|
|
148
|
+
client = fastmcp.Client(mcp_config)
|
|
149
|
+
async with client:
|
|
150
|
+
for tool in await client.list_tools():
|
|
151
|
+
toolset += MCPTool(tool, client)
|
|
152
|
+
return toolset
|
kimi_cli/soul/approval.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
3
|
from kimi_cli.soul.toolset import get_current_tool_call_or_none
|
|
4
|
-
from kimi_cli.soul.wire import ApprovalRequest, ApprovalResponse
|
|
5
4
|
from kimi_cli.utils.logging import logger
|
|
5
|
+
from kimi_cli.wire.message import ApprovalRequest, ApprovalResponse
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Approval:
|
kimi_cli/soul/compaction.py
CHANGED
|
@@ -10,8 +10,6 @@ from kimi_cli.llm import LLM
|
|
|
10
10
|
from kimi_cli.soul.message import system
|
|
11
11
|
from kimi_cli.utils.logging import logger
|
|
12
12
|
|
|
13
|
-
MAX_PRESERVED_MESSAGES = 2
|
|
14
|
-
|
|
15
13
|
|
|
16
14
|
@runtime_checkable
|
|
17
15
|
class Compaction(Protocol):
|
|
@@ -33,6 +31,8 @@ class Compaction(Protocol):
|
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
class SimpleCompaction(Compaction):
|
|
34
|
+
MAX_PRESERVED_MESSAGES = 2
|
|
35
|
+
|
|
36
36
|
async def compact(self, messages: Sequence[Message], llm: LLM) -> Sequence[Message]:
|
|
37
37
|
history = list(messages)
|
|
38
38
|
if not history:
|
|
@@ -43,11 +43,11 @@ class SimpleCompaction(Compaction):
|
|
|
43
43
|
for index in range(len(history) - 1, -1, -1):
|
|
44
44
|
if history[index].role in {"user", "assistant"}:
|
|
45
45
|
n_preserved += 1
|
|
46
|
-
if n_preserved == MAX_PRESERVED_MESSAGES:
|
|
46
|
+
if n_preserved == self.MAX_PRESERVED_MESSAGES:
|
|
47
47
|
preserve_start_index = index
|
|
48
48
|
break
|
|
49
49
|
|
|
50
|
-
if n_preserved < MAX_PRESERVED_MESSAGES:
|
|
50
|
+
if n_preserved < self.MAX_PRESERVED_MESSAGES:
|
|
51
51
|
return history
|
|
52
52
|
|
|
53
53
|
to_compact = history[:preserve_start_index]
|
kimi_cli/soul/kimisoul.py
CHANGED
|
@@ -16,58 +16,55 @@ 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.
|
|
20
|
-
from kimi_cli.
|
|
21
|
-
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot
|
|
19
|
+
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot, wire_send
|
|
20
|
+
from kimi_cli.soul.agent import Agent
|
|
22
21
|
from kimi_cli.soul.compaction import SimpleCompaction
|
|
23
22
|
from kimi_cli.soul.context import Context
|
|
24
23
|
from kimi_cli.soul.message import system, tool_result_to_messages
|
|
25
|
-
from kimi_cli.soul.
|
|
24
|
+
from kimi_cli.soul.runtime import Runtime
|
|
25
|
+
from kimi_cli.tools.dmail import NAME as SendDMail_NAME
|
|
26
|
+
from kimi_cli.tools.utils import ToolRejectedError
|
|
27
|
+
from kimi_cli.utils.logging import logger
|
|
28
|
+
from kimi_cli.wire.message import (
|
|
26
29
|
CompactionBegin,
|
|
27
30
|
CompactionEnd,
|
|
28
31
|
StatusUpdate,
|
|
29
32
|
StepBegin,
|
|
30
33
|
StepInterrupted,
|
|
31
|
-
Wire,
|
|
32
|
-
current_wire,
|
|
33
34
|
)
|
|
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
35
|
|
|
38
36
|
RESERVED_TOKENS = 50_000
|
|
39
37
|
|
|
40
38
|
|
|
41
|
-
class KimiSoul:
|
|
39
|
+
class KimiSoul(Soul):
|
|
42
40
|
"""The soul of Kimi CLI."""
|
|
43
41
|
|
|
44
42
|
def __init__(
|
|
45
43
|
self,
|
|
46
44
|
agent: Agent,
|
|
47
|
-
|
|
45
|
+
runtime: Runtime,
|
|
48
46
|
*,
|
|
49
47
|
context: Context,
|
|
50
|
-
loop_control: LoopControl,
|
|
51
48
|
):
|
|
52
49
|
"""
|
|
53
50
|
Initialize the soul.
|
|
54
51
|
|
|
55
52
|
Args:
|
|
56
53
|
agent (Agent): The agent to run.
|
|
57
|
-
|
|
54
|
+
runtime (Runtime): Runtime parameters and states.
|
|
58
55
|
context (Context): The context of the agent.
|
|
59
56
|
loop_control (LoopControl): The control parameters for the agent loop.
|
|
60
57
|
"""
|
|
61
58
|
self._agent = agent
|
|
62
|
-
self.
|
|
63
|
-
self._denwa_renji =
|
|
64
|
-
self._approval =
|
|
59
|
+
self._runtime = runtime
|
|
60
|
+
self._denwa_renji = runtime.denwa_renji
|
|
61
|
+
self._approval = runtime.approval
|
|
65
62
|
self._context = context
|
|
66
|
-
self._loop_control = loop_control
|
|
63
|
+
self._loop_control = runtime.config.loop_control
|
|
67
64
|
self._compaction = SimpleCompaction() # TODO: maybe configurable and composable
|
|
68
65
|
self._reserved_tokens = RESERVED_TOKENS
|
|
69
|
-
if self.
|
|
70
|
-
assert self._reserved_tokens <= self.
|
|
66
|
+
if self._runtime.llm is not None:
|
|
67
|
+
assert self._reserved_tokens <= self._runtime.llm.max_context_size
|
|
71
68
|
|
|
72
69
|
for tool in agent.toolset.tools:
|
|
73
70
|
if tool.name == SendDMail_NAME:
|
|
@@ -82,7 +79,7 @@ class KimiSoul:
|
|
|
82
79
|
|
|
83
80
|
@property
|
|
84
81
|
def model(self) -> str:
|
|
85
|
-
return self.
|
|
82
|
+
return self._runtime.llm.chat_provider.model_name if self._runtime.llm else ""
|
|
86
83
|
|
|
87
84
|
@property
|
|
88
85
|
def status(self) -> StatusSnapshot:
|
|
@@ -90,38 +87,34 @@ class KimiSoul:
|
|
|
90
87
|
|
|
91
88
|
@property
|
|
92
89
|
def _context_usage(self) -> float:
|
|
93
|
-
if self.
|
|
94
|
-
return self._context.token_count / self.
|
|
90
|
+
if self._runtime.llm is not None:
|
|
91
|
+
return self._context.token_count / self._runtime.llm.max_context_size
|
|
95
92
|
return 0.0
|
|
96
93
|
|
|
97
94
|
async def _checkpoint(self):
|
|
98
95
|
await self._context.checkpoint(self._checkpoint_with_user_message)
|
|
99
96
|
|
|
100
|
-
async def run(self, user_input: str
|
|
101
|
-
if self.
|
|
97
|
+
async def run(self, user_input: str):
|
|
98
|
+
if self._runtime.llm is None:
|
|
102
99
|
raise LLMNotSet()
|
|
103
100
|
|
|
104
101
|
await self._checkpoint() # this creates the checkpoint 0 on first run
|
|
105
102
|
await self._context.append_message(Message(role="user", content=user_input))
|
|
106
103
|
logger.debug("Appended user message to context")
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
await self._agent_loop(wire)
|
|
110
|
-
finally:
|
|
111
|
-
current_wire.reset(wire_token)
|
|
104
|
+
await self._agent_loop()
|
|
112
105
|
|
|
113
|
-
async def _agent_loop(self
|
|
106
|
+
async def _agent_loop(self):
|
|
114
107
|
"""The main agent loop for one run."""
|
|
115
|
-
assert self.
|
|
108
|
+
assert self._runtime.llm is not None
|
|
116
109
|
|
|
117
110
|
async def _pipe_approval_to_wire():
|
|
118
111
|
while True:
|
|
119
112
|
request = await self._approval.fetch_request()
|
|
120
|
-
|
|
113
|
+
wire_send(request)
|
|
121
114
|
|
|
122
115
|
step_no = 1
|
|
123
116
|
while True:
|
|
124
|
-
|
|
117
|
+
wire_send(StepBegin(step_no))
|
|
125
118
|
approval_task = asyncio.create_task(_pipe_approval_to_wire())
|
|
126
119
|
# FIXME: It's possible that a subagent's approval task steals approval request
|
|
127
120
|
# from the main agent. We must ensure that the Task tool will redirect them
|
|
@@ -131,24 +124,24 @@ class KimiSoul:
|
|
|
131
124
|
# compact the context if needed
|
|
132
125
|
if (
|
|
133
126
|
self._context.token_count + self._reserved_tokens
|
|
134
|
-
>= self.
|
|
127
|
+
>= self._runtime.llm.max_context_size
|
|
135
128
|
):
|
|
136
129
|
logger.info("Context too long, compacting...")
|
|
137
|
-
|
|
130
|
+
wire_send(CompactionBegin())
|
|
138
131
|
await self.compact_context()
|
|
139
|
-
|
|
132
|
+
wire_send(CompactionEnd())
|
|
140
133
|
|
|
141
134
|
logger.debug("Beginning step {step_no}", step_no=step_no)
|
|
142
135
|
await self._checkpoint()
|
|
143
136
|
self._denwa_renji.set_n_checkpoints(self._context.n_checkpoints)
|
|
144
|
-
finished = await self._step(
|
|
137
|
+
finished = await self._step()
|
|
145
138
|
except BackToTheFuture as e:
|
|
146
139
|
await self._context.revert_to(e.checkpoint_id)
|
|
147
140
|
await self._checkpoint()
|
|
148
141
|
await self._context.append_message(e.messages)
|
|
149
142
|
continue
|
|
150
143
|
except (ChatProviderError, asyncio.CancelledError):
|
|
151
|
-
|
|
144
|
+
wire_send(StepInterrupted())
|
|
152
145
|
# break the agent loop
|
|
153
146
|
raise
|
|
154
147
|
finally:
|
|
@@ -161,11 +154,11 @@ class KimiSoul:
|
|
|
161
154
|
if step_no > self._loop_control.max_steps_per_run:
|
|
162
155
|
raise MaxStepsReached(self._loop_control.max_steps_per_run)
|
|
163
156
|
|
|
164
|
-
async def _step(self
|
|
157
|
+
async def _step(self) -> bool:
|
|
165
158
|
"""Run an single step and return whether the run should be stopped."""
|
|
166
159
|
# already checked in `run`
|
|
167
|
-
assert self.
|
|
168
|
-
chat_provider = self.
|
|
160
|
+
assert self._runtime.llm is not None
|
|
161
|
+
chat_provider = self._runtime.llm.chat_provider
|
|
169
162
|
|
|
170
163
|
@tenacity.retry(
|
|
171
164
|
retry=retry_if_exception(self._is_retryable_error),
|
|
@@ -181,8 +174,8 @@ class KimiSoul:
|
|
|
181
174
|
self._agent.system_prompt,
|
|
182
175
|
self._agent.toolset,
|
|
183
176
|
self._context.history,
|
|
184
|
-
on_message_part=
|
|
185
|
-
on_tool_result=
|
|
177
|
+
on_message_part=wire_send,
|
|
178
|
+
on_tool_result=wire_send,
|
|
186
179
|
)
|
|
187
180
|
|
|
188
181
|
result = await _kosong_step_with_retry()
|
|
@@ -190,7 +183,7 @@ class KimiSoul:
|
|
|
190
183
|
if result.usage is not None:
|
|
191
184
|
# mark the token count for the context before the step
|
|
192
185
|
await self._context.update_token_count(result.usage.input)
|
|
193
|
-
|
|
186
|
+
wire_send(StatusUpdate(status=self.status))
|
|
194
187
|
|
|
195
188
|
# wait for all tool results (may be interrupted)
|
|
196
189
|
results = await result.tool_results()
|
|
@@ -260,9 +253,9 @@ class KimiSoul:
|
|
|
260
253
|
reraise=True,
|
|
261
254
|
)
|
|
262
255
|
async def _compact_with_retry() -> Sequence[Message]:
|
|
263
|
-
if self.
|
|
256
|
+
if self._runtime.llm is None:
|
|
264
257
|
raise LLMNotSet()
|
|
265
|
-
return await self._compaction.compact(self._context.history, self.
|
|
258
|
+
return await self._compaction.compact(self._context.history, self._runtime.llm)
|
|
266
259
|
|
|
267
260
|
compacted_messages = await _compact_with_retry()
|
|
268
261
|
await self._context.revert_to(0)
|
kimi_cli/soul/runtime.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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.session 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 Runtime(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
|
+
@staticmethod
|
|
72
|
+
async def create(
|
|
73
|
+
config: Config,
|
|
74
|
+
llm: LLM | None,
|
|
75
|
+
session: Session,
|
|
76
|
+
yolo: bool,
|
|
77
|
+
) -> "Runtime":
|
|
78
|
+
# FIXME: do these asynchronously
|
|
79
|
+
ls_output = _list_work_dir(session.work_dir)
|
|
80
|
+
agents_md = load_agents_md(session.work_dir) or ""
|
|
81
|
+
|
|
82
|
+
return Runtime(
|
|
83
|
+
config=config,
|
|
84
|
+
llm=llm,
|
|
85
|
+
session=session,
|
|
86
|
+
builtin_args=BuiltinSystemPromptArgs(
|
|
87
|
+
KIMI_NOW=datetime.now().astimezone().isoformat(),
|
|
88
|
+
KIMI_WORK_DIR=session.work_dir,
|
|
89
|
+
KIMI_WORK_DIR_LS=ls_output,
|
|
90
|
+
KIMI_AGENTS_MD=agents_md,
|
|
91
|
+
),
|
|
92
|
+
denwa_renji=DenwaRenji(),
|
|
93
|
+
approval=Approval(yolo=yolo),
|
|
94
|
+
)
|
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.runtime 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
|