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/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.runtime 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.runtime 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.runtime 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.runtime 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, SubagentSpec
|
|
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
|
|
10
12
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
11
|
-
from kimi_cli.soul.
|
|
13
|
+
from kimi_cli.soul.runtime import Runtime
|
|
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,29 +49,36 @@ class Task(CallableTool2[Params]):
|
|
|
45
49
|
name: str = "Task"
|
|
46
50
|
params: type[Params] = Params
|
|
47
51
|
|
|
48
|
-
def __init__(self, agent_spec:
|
|
49
|
-
subagents: dict[str, Agent] = {}
|
|
50
|
-
descs = []
|
|
51
|
-
|
|
52
|
-
# load all subagents
|
|
53
|
-
assert agent_spec.subagents is not None, "Task tool expects subagents"
|
|
54
|
-
for name, spec in agent_spec.subagents.items():
|
|
55
|
-
subagents[name] = load_agent(spec.path, agent_globals)
|
|
56
|
-
descs.append(f"- `{name}`: {spec.description}")
|
|
57
|
-
|
|
52
|
+
def __init__(self, agent_spec: ResolvedAgentSpec, runtime: Runtime, **kwargs):
|
|
58
53
|
super().__init__(
|
|
59
54
|
description=load_desc(
|
|
60
55
|
Path(__file__).parent / "task.md",
|
|
61
56
|
{
|
|
62
|
-
"SUBAGENTS_MD": "\n".join(
|
|
57
|
+
"SUBAGENTS_MD": "\n".join(
|
|
58
|
+
f"- `{name}`: {spec.description}"
|
|
59
|
+
for name, spec in agent_spec.subagents.items()
|
|
60
|
+
),
|
|
63
61
|
},
|
|
64
62
|
),
|
|
65
63
|
**kwargs,
|
|
66
64
|
)
|
|
67
65
|
|
|
68
|
-
self.
|
|
69
|
-
self._session =
|
|
70
|
-
self._subagents =
|
|
66
|
+
self._runtime = runtime
|
|
67
|
+
self._session = runtime.session
|
|
68
|
+
self._subagents: dict[str, Agent] = {}
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
self._load_task = asyncio.create_task(self._load_subagents(agent_spec.subagents))
|
|
72
|
+
except RuntimeError:
|
|
73
|
+
# In case there's no running event loop, e.g., during synchronous tests
|
|
74
|
+
self._load_task = None
|
|
75
|
+
asyncio.run(self._load_subagents(agent_spec.subagents))
|
|
76
|
+
|
|
77
|
+
async def _load_subagents(self, subagent_specs: dict[str, SubagentSpec]) -> None:
|
|
78
|
+
"""Load all subagents specified in the agent spec."""
|
|
79
|
+
for name, spec in subagent_specs.items():
|
|
80
|
+
agent = await load_agent(spec.path, self._runtime, mcp_configs=[])
|
|
81
|
+
self._subagents[name] = agent
|
|
71
82
|
|
|
72
83
|
async def _get_subagent_history_file(self) -> Path:
|
|
73
84
|
"""Generate a unique history file path for subagent."""
|
|
@@ -82,6 +93,10 @@ class Task(CallableTool2[Params]):
|
|
|
82
93
|
|
|
83
94
|
@override
|
|
84
95
|
async def __call__(self, params: Params) -> ToolReturnType:
|
|
96
|
+
if self._load_task is not None:
|
|
97
|
+
await self._load_task
|
|
98
|
+
self._load_task = None
|
|
99
|
+
|
|
85
100
|
if params.subagent_name not in self._subagents:
|
|
86
101
|
return ToolError(
|
|
87
102
|
message=f"Subagent not found: {params.subagent_name}",
|
|
@@ -99,20 +114,25 @@ class Task(CallableTool2[Params]):
|
|
|
99
114
|
|
|
100
115
|
async def _run_subagent(self, agent: Agent, prompt: str) -> ToolReturnType:
|
|
101
116
|
"""Run subagent with optional continuation for task summary."""
|
|
117
|
+
super_wire = get_wire_or_none()
|
|
118
|
+
assert super_wire is not None
|
|
119
|
+
|
|
120
|
+
def _super_wire_send(msg: WireMessage) -> None:
|
|
121
|
+
if isinstance(msg, ApprovalRequest):
|
|
122
|
+
super_wire.soul_side.send(msg)
|
|
123
|
+
# TODO: visualize subagent behavior by sending other messages in some way
|
|
124
|
+
|
|
125
|
+
async def _ui_loop_fn(wire: WireUISide) -> None:
|
|
126
|
+
while True:
|
|
127
|
+
msg = await wire.receive()
|
|
128
|
+
_super_wire_send(msg)
|
|
129
|
+
|
|
102
130
|
subagent_history_file = await self._get_subagent_history_file()
|
|
103
131
|
context = Context(file_backend=subagent_history_file)
|
|
104
|
-
soul = KimiSoul(
|
|
105
|
-
agent,
|
|
106
|
-
agent_globals=self._agent_globals,
|
|
107
|
-
context=context,
|
|
108
|
-
loop_control=self._agent_globals.config.loop_control,
|
|
109
|
-
)
|
|
110
|
-
wire = get_wire_or_none()
|
|
111
|
-
assert wire is not None, "Wire is expected to be set"
|
|
112
|
-
sub_wire = _SubWire(wire)
|
|
132
|
+
soul = KimiSoul(agent, runtime=self._runtime, context=context)
|
|
113
133
|
|
|
114
134
|
try:
|
|
115
|
-
await soul
|
|
135
|
+
await run_soul(soul, prompt, _ui_loop_fn, asyncio.Event())
|
|
116
136
|
except MaxStepsReached as e:
|
|
117
137
|
return ToolError(
|
|
118
138
|
message=(
|
|
@@ -135,22 +155,10 @@ class Task(CallableTool2[Params]):
|
|
|
135
155
|
# Check if response is too brief, if so, run again with continuation prompt
|
|
136
156
|
n_attempts_remaining = MAX_CONTINUE_ATTEMPTS
|
|
137
157
|
if len(final_response) < 200 and n_attempts_remaining > 0:
|
|
138
|
-
await soul
|
|
158
|
+
await run_soul(soul, CONTINUE_PROMPT, _ui_loop_fn, asyncio.Event())
|
|
139
159
|
|
|
140
160
|
if len(context.history) == 0 or context.history[-1].role != "assistant":
|
|
141
161
|
return ToolError(message=_error_msg, brief="Failed to run subagent")
|
|
142
162
|
final_response = message_extract_text(context.history[-1])
|
|
143
163
|
|
|
144
164
|
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
|
|
@@ -44,9 +44,11 @@ class SearchWeb(CallableTool2[Params]):
|
|
|
44
44
|
if config.services.moonshot_search is not None:
|
|
45
45
|
self._base_url = config.services.moonshot_search.base_url
|
|
46
46
|
self._api_key = config.services.moonshot_search.api_key.get_secret_value()
|
|
47
|
+
self._custom_headers = config.services.moonshot_search.custom_headers
|
|
47
48
|
else:
|
|
48
49
|
self._base_url = ""
|
|
49
50
|
self._api_key = ""
|
|
51
|
+
self._custom_headers = {}
|
|
50
52
|
|
|
51
53
|
@override
|
|
52
54
|
async def __call__(self, params: Params) -> ToolReturnType:
|
|
@@ -66,9 +68,10 @@ class SearchWeb(CallableTool2[Params]):
|
|
|
66
68
|
session.post(
|
|
67
69
|
self._base_url,
|
|
68
70
|
headers={
|
|
69
|
-
"User-Agent":
|
|
71
|
+
"User-Agent": USER_AGENT,
|
|
70
72
|
"Authorization": f"Bearer {self._api_key}",
|
|
71
73
|
"X-Msh-Tool-Call-Id": tool_call.id,
|
|
74
|
+
**self._custom_headers,
|
|
72
75
|
},
|
|
73
76
|
json={
|
|
74
77
|
"text_query": params.query,
|
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
|
)
|
kimi_cli/ui/print/__init__.py
CHANGED
|
@@ -3,18 +3,18 @@ import json
|
|
|
3
3
|
import signal
|
|
4
4
|
import sys
|
|
5
5
|
from functools import partial
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Literal
|
|
7
8
|
|
|
8
9
|
import aiofiles
|
|
9
10
|
from kosong.base.message import Message
|
|
10
11
|
from kosong.chat_provider import ChatProviderError
|
|
11
12
|
|
|
12
|
-
from kimi_cli.soul import LLMNotSet, MaxStepsReached
|
|
13
|
-
from kimi_cli.soul.kimisoul import KimiSoul
|
|
14
|
-
from kimi_cli.soul.wire import StepInterrupted, Wire
|
|
15
|
-
from kimi_cli.ui import RunCancelled, run_soul
|
|
13
|
+
from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
16
14
|
from kimi_cli.utils.logging import logger
|
|
17
15
|
from kimi_cli.utils.message import message_extract_text
|
|
16
|
+
from kimi_cli.wire import WireUISide
|
|
17
|
+
from kimi_cli.wire.message import StepInterrupted
|
|
18
18
|
|
|
19
19
|
InputFormat = Literal["text", "stream-json"]
|
|
20
20
|
OutputFormat = Literal["text", "stream-json"]
|
|
@@ -25,17 +25,23 @@ class PrintApp:
|
|
|
25
25
|
An app implementation that prints the agent behavior to the console.
|
|
26
26
|
|
|
27
27
|
Args:
|
|
28
|
-
soul (
|
|
28
|
+
soul (Soul): The soul to run.
|
|
29
29
|
input_format (InputFormat): The input format to use.
|
|
30
30
|
output_format (OutputFormat): The output format to use.
|
|
31
|
+
context_file (Path): The file to store the context.
|
|
31
32
|
"""
|
|
32
33
|
|
|
33
|
-
def __init__(
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
soul: Soul,
|
|
37
|
+
input_format: InputFormat,
|
|
38
|
+
output_format: OutputFormat,
|
|
39
|
+
context_file: Path,
|
|
40
|
+
):
|
|
34
41
|
self.soul = soul
|
|
35
42
|
self.input_format = input_format
|
|
36
43
|
self.output_format = output_format
|
|
37
|
-
self.
|
|
38
|
-
# TODO(approval): proper approval request handling
|
|
44
|
+
self.context_file = context_file
|
|
39
45
|
|
|
40
46
|
async def run(self, command: str | None = None) -> bool:
|
|
41
47
|
cancel_event = asyncio.Event()
|
|
@@ -95,30 +101,6 @@ class PrintApp:
|
|
|
95
101
|
loop.remove_signal_handler(signal.SIGINT)
|
|
96
102
|
return False
|
|
97
103
|
|
|
98
|
-
# TODO: unify with `_soul_run` in `ShellApp` and `ACPAgentImpl`
|
|
99
|
-
async def _soul_run(self, user_input: str):
|
|
100
|
-
wire = Wire()
|
|
101
|
-
logger.debug("Starting visualization loop")
|
|
102
|
-
|
|
103
|
-
if self.output_format == "text":
|
|
104
|
-
vis_task = asyncio.create_task(self._visualize_text(wire))
|
|
105
|
-
else:
|
|
106
|
-
assert self.output_format == "stream-json"
|
|
107
|
-
if not self.soul._context._file_backend.exists():
|
|
108
|
-
self.soul._context._file_backend.touch()
|
|
109
|
-
start_position = self.soul._context._file_backend.stat().st_size
|
|
110
|
-
vis_task = asyncio.create_task(self._visualize_stream_json(wire, start_position))
|
|
111
|
-
|
|
112
|
-
try:
|
|
113
|
-
await self.soul.run(user_input, wire)
|
|
114
|
-
finally:
|
|
115
|
-
wire.shutdown()
|
|
116
|
-
# shutting down the event queue should break the visualization loop
|
|
117
|
-
try:
|
|
118
|
-
await asyncio.wait_for(vis_task, timeout=0.5)
|
|
119
|
-
except TimeoutError:
|
|
120
|
-
logger.warning("Visualization loop timed out")
|
|
121
|
-
|
|
122
104
|
def _read_next_command(self) -> str | None:
|
|
123
105
|
while True:
|
|
124
106
|
json_line = sys.stdin.readline()
|
|
@@ -144,7 +126,7 @@ class PrintApp:
|
|
|
144
126
|
except Exception:
|
|
145
127
|
logger.warning("Ignoring invalid user message: {json_line}", json_line=json_line)
|
|
146
128
|
|
|
147
|
-
async def _visualize_text(self, wire:
|
|
129
|
+
async def _visualize_text(self, wire: WireUISide):
|
|
148
130
|
try:
|
|
149
131
|
while True:
|
|
150
132
|
msg = await wire.receive()
|
|
@@ -154,15 +136,17 @@ class PrintApp:
|
|
|
154
136
|
except asyncio.QueueShutDown:
|
|
155
137
|
logger.debug("Visualization loop shutting down")
|
|
156
138
|
|
|
157
|
-
async def _visualize_stream_json(self, wire:
|
|
139
|
+
async def _visualize_stream_json(self, wire: WireUISide, start_position: int):
|
|
158
140
|
# TODO: be aware of context compaction
|
|
141
|
+
# FIXME: this is only a temporary impl, may miss the last lines of the context file
|
|
142
|
+
if not self.context_file.exists():
|
|
143
|
+
self.context_file.touch()
|
|
159
144
|
try:
|
|
160
|
-
async with aiofiles.open(self.
|
|
145
|
+
async with aiofiles.open(self.context_file, encoding="utf-8") as f:
|
|
161
146
|
await f.seek(start_position)
|
|
162
147
|
while True:
|
|
163
148
|
should_end = False
|
|
164
|
-
while wire.
|
|
165
|
-
msg = wire._queue.get_nowait()
|
|
149
|
+
while (msg := wire.receive_nowait()) is not None:
|
|
166
150
|
if isinstance(msg, StepInterrupted):
|
|
167
151
|
should_end = True
|
|
168
152
|
|
kimi_cli/ui/shell/__init__.py
CHANGED
|
@@ -9,9 +9,8 @@ from rich.panel import Panel
|
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
from rich.text import Text
|
|
11
11
|
|
|
12
|
-
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul
|
|
12
|
+
from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
13
13
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
14
|
-
from kimi_cli.ui import RunCancelled, run_soul
|
|
15
14
|
from kimi_cli.ui.shell.console import console
|
|
16
15
|
from kimi_cli.ui.shell.metacmd import get_meta_command
|
|
17
16
|
from kimi_cli.ui.shell.prompt import CustomPromptSession, PromptMode, toast
|
|
@@ -20,12 +19,6 @@ from kimi_cli.ui.shell.visualize import visualize
|
|
|
20
19
|
from kimi_cli.utils.logging import logger
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
class Reload(Exception):
|
|
24
|
-
"""Reload configuration."""
|
|
25
|
-
|
|
26
|
-
pass
|
|
27
|
-
|
|
28
|
-
|
|
29
22
|
class ShellApp:
|
|
30
23
|
def __init__(self, soul: Soul, welcome_info: dict[str, str] | None = None):
|
|
31
24
|
self.soul = soul
|
|
@@ -38,7 +31,7 @@ class ShellApp:
|
|
|
38
31
|
logger.info("Running agent with command: {command}", command=command)
|
|
39
32
|
return await self._run_soul_command(command)
|
|
40
33
|
|
|
41
|
-
self.
|
|
34
|
+
self._start_background_task(self._auto_update())
|
|
42
35
|
|
|
43
36
|
_print_welcome_info(self.soul.name or "Kimi CLI", self.soul.model, self.welcome_info)
|
|
44
37
|
|
|
@@ -106,6 +99,8 @@ class ShellApp:
|
|
|
106
99
|
loop.remove_signal_handler(signal.SIGINT)
|
|
107
100
|
|
|
108
101
|
async def _run_meta_command(self, command_str: str):
|
|
102
|
+
from kimi_cli.cli import Reload
|
|
103
|
+
|
|
109
104
|
parts = command_str.split(" ")
|
|
110
105
|
command_name = parts[0]
|
|
111
106
|
command_args = parts[1:]
|
|
@@ -188,9 +183,6 @@ class ShellApp:
|
|
|
188
183
|
except RunCancelled:
|
|
189
184
|
logger.info("Cancelled by user")
|
|
190
185
|
console.print("[red]Interrupted by user[/red]")
|
|
191
|
-
except Reload:
|
|
192
|
-
# just propagate
|
|
193
|
-
raise
|
|
194
186
|
except BaseException as e:
|
|
195
187
|
logger.exception("Unknown error:")
|
|
196
188
|
console.print(f"[red]Unknown error: {e}[/red]")
|
|
@@ -199,10 +191,7 @@ class ShellApp:
|
|
|
199
191
|
loop.remove_signal_handler(signal.SIGINT)
|
|
200
192
|
return False
|
|
201
193
|
|
|
202
|
-
def
|
|
203
|
-
self._add_background_task(self._auto_update_background())
|
|
204
|
-
|
|
205
|
-
async def _auto_update_background(self) -> None:
|
|
194
|
+
async def _auto_update(self) -> None:
|
|
206
195
|
toast("checking for updates...", duration=2.0)
|
|
207
196
|
result = await do_update(print=False, check_only=True)
|
|
208
197
|
if result == UpdateResult.UPDATE_AVAILABLE:
|
|
@@ -212,7 +201,7 @@ class ShellApp:
|
|
|
212
201
|
elif result == UpdateResult.UPDATED:
|
|
213
202
|
toast("auto updated, restart to use the new version", duration=5.0)
|
|
214
203
|
|
|
215
|
-
def
|
|
204
|
+
def _start_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
|
|
216
205
|
task = asyncio.create_task(coro)
|
|
217
206
|
self._background_tasks.add(task)
|
|
218
207
|
|
|
@@ -265,9 +254,9 @@ def _print_welcome_info(name: str, model: str, info_items: dict[str, str]) -> No
|
|
|
265
254
|
)
|
|
266
255
|
|
|
267
256
|
if LATEST_VERSION_FILE.exists():
|
|
268
|
-
from kimi_cli import
|
|
257
|
+
from kimi_cli.constant import VERSION as current_version
|
|
269
258
|
|
|
270
|
-
latest_version = LATEST_VERSION_FILE.read_text().strip()
|
|
259
|
+
latest_version = LATEST_VERSION_FILE.read_text(encoding="utf-8").strip()
|
|
271
260
|
if semver_tuple(latest_version) > semver_tuple(current_version):
|
|
272
261
|
rows.append(
|
|
273
262
|
Text.from_markup(
|
kimi_cli/ui/shell/liveview.py
CHANGED
|
@@ -15,10 +15,10 @@ from rich.status import Status
|
|
|
15
15
|
from rich.text import Text
|
|
16
16
|
|
|
17
17
|
from kimi_cli.soul import StatusSnapshot
|
|
18
|
-
from kimi_cli.soul.wire import ApprovalRequest, ApprovalResponse
|
|
19
18
|
from kimi_cli.tools import extract_subtitle
|
|
20
19
|
from kimi_cli.ui.shell.console import console
|
|
21
20
|
from kimi_cli.ui.shell.keyboard import KeyEvent
|
|
21
|
+
from kimi_cli.wire.message import ApprovalRequest, ApprovalResponse
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class _ToolCallDisplay:
|
kimi_cli/ui/shell/metacmd.py
CHANGED
|
@@ -8,10 +8,10 @@ from kosong.base.message import Message
|
|
|
8
8
|
from rich.panel import Panel
|
|
9
9
|
|
|
10
10
|
import kimi_cli.prompts as prompts
|
|
11
|
-
from kimi_cli.agent import load_agents_md
|
|
12
11
|
from kimi_cli.soul.context import Context
|
|
13
12
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
14
13
|
from kimi_cli.soul.message import system
|
|
14
|
+
from kimi_cli.soul.runtime import load_agents_md
|
|
15
15
|
from kimi_cli.ui.shell.console import console
|
|
16
16
|
from kimi_cli.utils.changelog import CHANGELOG, format_release_notes
|
|
17
17
|
from kimi_cli.utils.logging import logger
|
|
@@ -173,9 +173,9 @@ def help(app: "ShellApp", args: list[str]):
|
|
|
173
173
|
@meta_command
|
|
174
174
|
def version(app: "ShellApp", args: list[str]):
|
|
175
175
|
"""Show version information"""
|
|
176
|
-
from kimi_cli import
|
|
176
|
+
from kimi_cli.constant import VERSION
|
|
177
177
|
|
|
178
|
-
console.print(f"kimi, version {
|
|
178
|
+
console.print(f"kimi, version {VERSION}")
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
@meta_command(name="release-notes")
|
|
@@ -206,12 +206,7 @@ async def init(app: "ShellApp", args: list[str]):
|
|
|
206
206
|
logger.info("Running `/init`")
|
|
207
207
|
console.print("Analyzing the codebase...")
|
|
208
208
|
tmp_context = Context(file_backend=Path(temp_dir) / "context.jsonl")
|
|
209
|
-
app.soul = KimiSoul(
|
|
210
|
-
soul_bak._agent,
|
|
211
|
-
soul_bak._agent_globals,
|
|
212
|
-
context=tmp_context,
|
|
213
|
-
loop_control=soul_bak._loop_control,
|
|
214
|
-
)
|
|
209
|
+
app.soul = KimiSoul(soul_bak._agent, soul_bak._runtime, context=tmp_context)
|
|
215
210
|
ok = await app._run_soul_command(prompts.INIT)
|
|
216
211
|
|
|
217
212
|
if ok:
|
|
@@ -223,7 +218,7 @@ async def init(app: "ShellApp", args: list[str]):
|
|
|
223
218
|
console.print("[red]Failed to analyze the codebase.[/red]")
|
|
224
219
|
|
|
225
220
|
app.soul = soul_bak
|
|
226
|
-
agents_md = load_agents_md(soul_bak.
|
|
221
|
+
agents_md = load_agents_md(soul_bak._runtime.builtin_args.KIMI_WORK_DIR)
|
|
227
222
|
system_message = system(
|
|
228
223
|
"The user just ran `/init` meta command. "
|
|
229
224
|
"The system has analyzed the codebase and generated an `AGENTS.md` file. "
|