kimi-cli 0.41__py3-none-any.whl → 0.43__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 +28 -0
- kimi_cli/__init__.py +169 -102
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agentspec.py +19 -8
- kimi_cli/cli.py +51 -37
- kimi_cli/config.py +33 -14
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +31 -3
- kimi_cli/metadata.py +5 -68
- kimi_cli/session.py +81 -0
- kimi_cli/soul/__init__.py +22 -4
- kimi_cli/soul/agent.py +18 -23
- kimi_cli/soul/context.py +0 -5
- kimi_cli/soul/kimisoul.py +40 -25
- kimi_cli/soul/message.py +1 -1
- kimi_cli/soul/{globals.py → runtime.py} +13 -11
- kimi_cli/tools/file/glob.py +1 -1
- kimi_cli/tools/file/patch.py +1 -1
- kimi_cli/tools/file/read.py +1 -1
- kimi_cli/tools/file/replace.py +1 -1
- kimi_cli/tools/file/write.py +1 -1
- kimi_cli/tools/task/__init__.py +29 -21
- kimi_cli/tools/web/search.py +3 -0
- kimi_cli/ui/acp/__init__.py +24 -28
- kimi_cli/ui/print/__init__.py +27 -30
- kimi_cli/ui/shell/__init__.py +58 -42
- kimi_cli/ui/shell/keyboard.py +82 -14
- kimi_cli/ui/shell/metacmd.py +3 -8
- kimi_cli/ui/shell/prompt.py +208 -6
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/visualize.py +54 -57
- kimi_cli/utils/message.py +14 -0
- kimi_cli/utils/signals.py +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/wire/__init__.py +13 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/METADATA +21 -20
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/RECORD +41 -38
- kimi_cli/agents/koder/README.md +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/agents/{koder → default}/system.md +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/WHEEL +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/entry_points.txt +0 -0
kimi_cli/soul/kimisoul.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
import kosong
|
|
7
7
|
import tenacity
|
|
8
8
|
from kosong import StepResult
|
|
9
|
-
from kosong.base.message import Message
|
|
9
|
+
from kosong.base.message import ContentPart, ImageURLPart, Message
|
|
10
10
|
from kosong.chat_provider import (
|
|
11
11
|
APIConnectionError,
|
|
12
12
|
APIStatusError,
|
|
@@ -16,13 +16,19 @@ 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
|
-
|
|
19
|
+
from kimi_cli.soul import (
|
|
20
|
+
LLMNotSet,
|
|
21
|
+
LLMNotSupported,
|
|
22
|
+
MaxStepsReached,
|
|
23
|
+
Soul,
|
|
24
|
+
StatusSnapshot,
|
|
25
|
+
wire_send,
|
|
26
|
+
)
|
|
21
27
|
from kimi_cli.soul.agent import Agent
|
|
22
28
|
from kimi_cli.soul.compaction import SimpleCompaction
|
|
23
29
|
from kimi_cli.soul.context import Context
|
|
24
|
-
from kimi_cli.soul.globals import AgentGlobals
|
|
25
30
|
from kimi_cli.soul.message import system, tool_result_to_messages
|
|
31
|
+
from kimi_cli.soul.runtime import Runtime
|
|
26
32
|
from kimi_cli.tools.dmail import NAME as SendDMail_NAME
|
|
27
33
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
28
34
|
from kimi_cli.utils.logging import logger
|
|
@@ -43,30 +49,28 @@ class KimiSoul(Soul):
|
|
|
43
49
|
def __init__(
|
|
44
50
|
self,
|
|
45
51
|
agent: Agent,
|
|
46
|
-
|
|
52
|
+
runtime: Runtime,
|
|
47
53
|
*,
|
|
48
54
|
context: Context,
|
|
49
|
-
loop_control: LoopControl,
|
|
50
55
|
):
|
|
51
56
|
"""
|
|
52
57
|
Initialize the soul.
|
|
53
58
|
|
|
54
59
|
Args:
|
|
55
60
|
agent (Agent): The agent to run.
|
|
56
|
-
|
|
61
|
+
runtime (Runtime): Runtime parameters and states.
|
|
57
62
|
context (Context): The context of the agent.
|
|
58
|
-
loop_control (LoopControl): The control parameters for the agent loop.
|
|
59
63
|
"""
|
|
60
64
|
self._agent = agent
|
|
61
|
-
self.
|
|
62
|
-
self._denwa_renji =
|
|
63
|
-
self._approval =
|
|
65
|
+
self._runtime = runtime
|
|
66
|
+
self._denwa_renji = runtime.denwa_renji
|
|
67
|
+
self._approval = runtime.approval
|
|
64
68
|
self._context = context
|
|
65
|
-
self._loop_control = loop_control
|
|
69
|
+
self._loop_control = runtime.config.loop_control
|
|
66
70
|
self._compaction = SimpleCompaction() # TODO: maybe configurable and composable
|
|
67
71
|
self._reserved_tokens = RESERVED_TOKENS
|
|
68
|
-
if self.
|
|
69
|
-
assert self._reserved_tokens <= self.
|
|
72
|
+
if self._runtime.llm is not None:
|
|
73
|
+
assert self._reserved_tokens <= self._runtime.llm.max_context_size
|
|
70
74
|
|
|
71
75
|
for tool in agent.toolset.tools:
|
|
72
76
|
if tool.name == SendDMail_NAME:
|
|
@@ -81,25 +85,36 @@ class KimiSoul(Soul):
|
|
|
81
85
|
|
|
82
86
|
@property
|
|
83
87
|
def model(self) -> str:
|
|
84
|
-
return self.
|
|
88
|
+
return self._runtime.llm.chat_provider.model_name if self._runtime.llm else ""
|
|
85
89
|
|
|
86
90
|
@property
|
|
87
91
|
def status(self) -> StatusSnapshot:
|
|
88
92
|
return StatusSnapshot(context_usage=self._context_usage)
|
|
89
93
|
|
|
94
|
+
@property
|
|
95
|
+
def context(self) -> Context:
|
|
96
|
+
return self._context
|
|
97
|
+
|
|
90
98
|
@property
|
|
91
99
|
def _context_usage(self) -> float:
|
|
92
|
-
if self.
|
|
93
|
-
return self._context.token_count / self.
|
|
100
|
+
if self._runtime.llm is not None:
|
|
101
|
+
return self._context.token_count / self._runtime.llm.max_context_size
|
|
94
102
|
return 0.0
|
|
95
103
|
|
|
96
104
|
async def _checkpoint(self):
|
|
97
105
|
await self._context.checkpoint(self._checkpoint_with_user_message)
|
|
98
106
|
|
|
99
|
-
async def run(self, user_input: str):
|
|
100
|
-
if self.
|
|
107
|
+
async def run(self, user_input: str | list[ContentPart]):
|
|
108
|
+
if self._runtime.llm is None:
|
|
101
109
|
raise LLMNotSet()
|
|
102
110
|
|
|
111
|
+
if (
|
|
112
|
+
isinstance(user_input, list)
|
|
113
|
+
and any(isinstance(part, ImageURLPart) for part in user_input)
|
|
114
|
+
and not self._runtime.llm.supports_image_in
|
|
115
|
+
):
|
|
116
|
+
raise LLMNotSupported(self._runtime.llm, ["image_in"])
|
|
117
|
+
|
|
103
118
|
await self._checkpoint() # this creates the checkpoint 0 on first run
|
|
104
119
|
await self._context.append_message(Message(role="user", content=user_input))
|
|
105
120
|
logger.debug("Appended user message to context")
|
|
@@ -107,7 +122,7 @@ class KimiSoul(Soul):
|
|
|
107
122
|
|
|
108
123
|
async def _agent_loop(self):
|
|
109
124
|
"""The main agent loop for one run."""
|
|
110
|
-
assert self.
|
|
125
|
+
assert self._runtime.llm is not None
|
|
111
126
|
|
|
112
127
|
async def _pipe_approval_to_wire():
|
|
113
128
|
while True:
|
|
@@ -126,7 +141,7 @@ class KimiSoul(Soul):
|
|
|
126
141
|
# compact the context if needed
|
|
127
142
|
if (
|
|
128
143
|
self._context.token_count + self._reserved_tokens
|
|
129
|
-
>= self.
|
|
144
|
+
>= self._runtime.llm.max_context_size
|
|
130
145
|
):
|
|
131
146
|
logger.info("Context too long, compacting...")
|
|
132
147
|
wire_send(CompactionBegin())
|
|
@@ -159,8 +174,8 @@ class KimiSoul(Soul):
|
|
|
159
174
|
async def _step(self) -> bool:
|
|
160
175
|
"""Run an single step and return whether the run should be stopped."""
|
|
161
176
|
# already checked in `run`
|
|
162
|
-
assert self.
|
|
163
|
-
chat_provider = self.
|
|
177
|
+
assert self._runtime.llm is not None
|
|
178
|
+
chat_provider = self._runtime.llm.chat_provider
|
|
164
179
|
|
|
165
180
|
@tenacity.retry(
|
|
166
181
|
retry=retry_if_exception(self._is_retryable_error),
|
|
@@ -255,9 +270,9 @@ class KimiSoul(Soul):
|
|
|
255
270
|
reraise=True,
|
|
256
271
|
)
|
|
257
272
|
async def _compact_with_retry() -> Sequence[Message]:
|
|
258
|
-
if self.
|
|
273
|
+
if self._runtime.llm is None:
|
|
259
274
|
raise LLMNotSet()
|
|
260
|
-
return await self._compaction.compact(self._context.history, self.
|
|
275
|
+
return await self._compaction.compact(self._context.history, self._runtime.llm)
|
|
261
276
|
|
|
262
277
|
compacted_messages = await _compact_with_retry()
|
|
263
278
|
await self._context.revert_to(0)
|
kimi_cli/soul/message.py
CHANGED
|
@@ -14,7 +14,7 @@ def tool_result_to_messages(tool_result: ToolResult) -> list[Message]:
|
|
|
14
14
|
message = tool_result.result.message
|
|
15
15
|
if isinstance(tool_result.result, ToolRuntimeError):
|
|
16
16
|
message += "\nThis is an unexpected error and the tool is probably not working."
|
|
17
|
-
content: list[ContentPart] = [system(message)]
|
|
17
|
+
content: list[ContentPart] = [system(f"ERROR: {message}")]
|
|
18
18
|
if tool_result.result.output:
|
|
19
19
|
content.append(TextPart(text=tool_result.result.output))
|
|
20
20
|
return [
|
|
@@ -6,7 +6,7 @@ from typing import NamedTuple
|
|
|
6
6
|
|
|
7
7
|
from kimi_cli.config import Config
|
|
8
8
|
from kimi_cli.llm import LLM
|
|
9
|
-
from kimi_cli.
|
|
9
|
+
from kimi_cli.session import Session
|
|
10
10
|
from kimi_cli.soul.approval import Approval
|
|
11
11
|
from kimi_cli.soul.denwarenji import DenwaRenji
|
|
12
12
|
from kimi_cli.utils.logging import logger
|
|
@@ -58,8 +58,8 @@ def _list_work_dir(work_dir: Path) -> str:
|
|
|
58
58
|
return ls.stdout.strip()
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
class
|
|
62
|
-
"""Agent
|
|
61
|
+
class Runtime(NamedTuple):
|
|
62
|
+
"""Agent runtime."""
|
|
63
63
|
|
|
64
64
|
config: Config
|
|
65
65
|
llm: LLM | None
|
|
@@ -68,22 +68,24 @@ class AgentGlobals(NamedTuple):
|
|
|
68
68
|
denwa_renji: DenwaRenji
|
|
69
69
|
approval: Approval
|
|
70
70
|
|
|
71
|
-
@
|
|
71
|
+
@staticmethod
|
|
72
72
|
async def create(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
config: Config,
|
|
74
|
+
llm: LLM | None,
|
|
75
|
+
session: Session,
|
|
76
|
+
yolo: bool,
|
|
77
|
+
) -> "Runtime":
|
|
76
78
|
# FIXME: do these asynchronously
|
|
77
|
-
ls_output = _list_work_dir(work_dir)
|
|
78
|
-
agents_md = load_agents_md(work_dir) or ""
|
|
79
|
+
ls_output = _list_work_dir(session.work_dir)
|
|
80
|
+
agents_md = load_agents_md(session.work_dir) or ""
|
|
79
81
|
|
|
80
|
-
return
|
|
82
|
+
return Runtime(
|
|
81
83
|
config=config,
|
|
82
84
|
llm=llm,
|
|
83
85
|
session=session,
|
|
84
86
|
builtin_args=BuiltinSystemPromptArgs(
|
|
85
87
|
KIMI_NOW=datetime.now().astimezone().isoformat(),
|
|
86
|
-
KIMI_WORK_DIR=work_dir,
|
|
88
|
+
KIMI_WORK_DIR=session.work_dir,
|
|
87
89
|
KIMI_WORK_DIR_LS=ls_output,
|
|
88
90
|
KIMI_AGENTS_MD=agents_md,
|
|
89
91
|
),
|
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.soul.
|
|
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
|
kimi_cli/tools/file/patch.py
CHANGED
|
@@ -7,7 +7,7 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
from kimi_cli.soul.approval import Approval
|
|
10
|
-
from kimi_cli.soul.
|
|
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
|
|
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.soul.
|
|
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
|
@@ -6,7 +6,7 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
from kimi_cli.soul.approval import Approval
|
|
9
|
-
from kimi_cli.soul.
|
|
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
|
|
kimi_cli/tools/file/write.py
CHANGED
|
@@ -6,7 +6,7 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
from kimi_cli.soul.approval import Approval
|
|
9
|
-
from kimi_cli.soul.
|
|
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
|
|
kimi_cli/tools/task/__init__.py
CHANGED
|
@@ -5,12 +5,12 @@ from typing import override
|
|
|
5
5
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
-
from kimi_cli.agentspec import ResolvedAgentSpec
|
|
8
|
+
from kimi_cli.agentspec import ResolvedAgentSpec, SubagentSpec
|
|
9
9
|
from kimi_cli.soul import MaxStepsReached, get_wire_or_none, run_soul
|
|
10
10
|
from kimi_cli.soul.agent import Agent, load_agent
|
|
11
11
|
from kimi_cli.soul.context import Context
|
|
12
|
-
from kimi_cli.soul.globals import AgentGlobals
|
|
13
12
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
13
|
+
from kimi_cli.soul.runtime import Runtime
|
|
14
14
|
from kimi_cli.tools.utils import load_desc
|
|
15
15
|
from kimi_cli.utils.message import message_extract_text
|
|
16
16
|
from kimi_cli.utils.path import next_available_rotation
|
|
@@ -49,28 +49,37 @@ class Task(CallableTool2[Params]):
|
|
|
49
49
|
name: str = "Task"
|
|
50
50
|
params: type[Params] = Params
|
|
51
51
|
|
|
52
|
-
def __init__(self, agent_spec: ResolvedAgentSpec,
|
|
53
|
-
subagents: dict[str, Agent] = {}
|
|
54
|
-
descs = []
|
|
55
|
-
|
|
56
|
-
# load all subagents
|
|
57
|
-
for name, spec in agent_spec.subagents.items():
|
|
58
|
-
subagents[name] = load_agent(spec.path, agent_globals)
|
|
59
|
-
descs.append(f"- `{name}`: {spec.description}")
|
|
60
|
-
|
|
52
|
+
def __init__(self, agent_spec: ResolvedAgentSpec, runtime: Runtime, **kwargs):
|
|
61
53
|
super().__init__(
|
|
62
54
|
description=load_desc(
|
|
63
55
|
Path(__file__).parent / "task.md",
|
|
64
56
|
{
|
|
65
|
-
"SUBAGENTS_MD": "\n".join(
|
|
57
|
+
"SUBAGENTS_MD": "\n".join(
|
|
58
|
+
f"- `{name}`: {spec.description}"
|
|
59
|
+
for name, spec in agent_spec.subagents.items()
|
|
60
|
+
),
|
|
66
61
|
},
|
|
67
62
|
),
|
|
68
63
|
**kwargs,
|
|
69
64
|
)
|
|
70
65
|
|
|
71
|
-
self.
|
|
72
|
-
self._session =
|
|
73
|
-
self._subagents =
|
|
66
|
+
self._runtime = runtime
|
|
67
|
+
self._session = runtime.session
|
|
68
|
+
self._subagents: dict[str, Agent] = {}
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
loop = asyncio.get_running_loop()
|
|
72
|
+
self._load_task = loop.create_task(self._load_subagents(agent_spec.subagents))
|
|
73
|
+
except RuntimeError:
|
|
74
|
+
# In case there's no running event loop, e.g., during synchronous tests
|
|
75
|
+
self._load_task = None
|
|
76
|
+
asyncio.run(self._load_subagents(agent_spec.subagents))
|
|
77
|
+
|
|
78
|
+
async def _load_subagents(self, subagent_specs: dict[str, SubagentSpec]) -> None:
|
|
79
|
+
"""Load all subagents specified in the agent spec."""
|
|
80
|
+
for name, spec in subagent_specs.items():
|
|
81
|
+
agent = await load_agent(spec.path, self._runtime, mcp_configs=[])
|
|
82
|
+
self._subagents[name] = agent
|
|
74
83
|
|
|
75
84
|
async def _get_subagent_history_file(self) -> Path:
|
|
76
85
|
"""Generate a unique history file path for subagent."""
|
|
@@ -85,6 +94,10 @@ class Task(CallableTool2[Params]):
|
|
|
85
94
|
|
|
86
95
|
@override
|
|
87
96
|
async def __call__(self, params: Params) -> ToolReturnType:
|
|
97
|
+
if self._load_task is not None:
|
|
98
|
+
await self._load_task
|
|
99
|
+
self._load_task = None
|
|
100
|
+
|
|
88
101
|
if params.subagent_name not in self._subagents:
|
|
89
102
|
return ToolError(
|
|
90
103
|
message=f"Subagent not found: {params.subagent_name}",
|
|
@@ -117,12 +130,7 @@ class Task(CallableTool2[Params]):
|
|
|
117
130
|
|
|
118
131
|
subagent_history_file = await self._get_subagent_history_file()
|
|
119
132
|
context = Context(file_backend=subagent_history_file)
|
|
120
|
-
soul = KimiSoul(
|
|
121
|
-
agent,
|
|
122
|
-
agent_globals=self._agent_globals,
|
|
123
|
-
context=context,
|
|
124
|
-
loop_control=self._agent_globals.config.loop_control,
|
|
125
|
-
)
|
|
133
|
+
soul = KimiSoul(agent, runtime=self._runtime, context=context)
|
|
126
134
|
|
|
127
135
|
try:
|
|
128
136
|
await run_soul(soul, prompt, _ui_loop_fn, asyncio.Event())
|
kimi_cli/tools/web/search.py
CHANGED
|
@@ -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 or {}
|
|
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:
|
|
@@ -69,6 +71,7 @@ class SearchWeb(CallableTool2[Params]):
|
|
|
69
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/acp/__init__.py
CHANGED
|
@@ -172,33 +172,29 @@ class ACPAgent:
|
|
|
172
172
|
self.run_state.cancel_event.set()
|
|
173
173
|
|
|
174
174
|
async def _stream_events(self, wire: WireUISide):
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
elif isinstance(msg, StepInterrupted):
|
|
199
|
-
break
|
|
200
|
-
except asyncio.QueueShutDown:
|
|
201
|
-
logger.debug("Event stream loop shutting down")
|
|
175
|
+
assert isinstance(await wire.receive(), StepBegin)
|
|
176
|
+
|
|
177
|
+
while True:
|
|
178
|
+
msg = await wire.receive()
|
|
179
|
+
|
|
180
|
+
if isinstance(msg, TextPart):
|
|
181
|
+
await self._send_text(msg.text)
|
|
182
|
+
elif isinstance(msg, ContentPart):
|
|
183
|
+
logger.warning("Unsupported content part: {part}", part=msg)
|
|
184
|
+
await self._send_text(f"[{msg.__class__.__name__}]")
|
|
185
|
+
elif isinstance(msg, ToolCall):
|
|
186
|
+
await self._send_tool_call(msg)
|
|
187
|
+
elif isinstance(msg, ToolCallPart):
|
|
188
|
+
await self._send_tool_call_part(msg)
|
|
189
|
+
elif isinstance(msg, ToolResult):
|
|
190
|
+
await self._send_tool_result(msg)
|
|
191
|
+
elif isinstance(msg, ApprovalRequest):
|
|
192
|
+
await self._handle_approval_request(msg)
|
|
193
|
+
elif isinstance(msg, StatusUpdate):
|
|
194
|
+
# TODO: stream status if needed
|
|
195
|
+
pass
|
|
196
|
+
elif isinstance(msg, StepInterrupted):
|
|
197
|
+
break
|
|
202
198
|
|
|
203
199
|
async def _send_text(self, text: str):
|
|
204
200
|
"""Send text chunk to client."""
|
|
@@ -321,7 +317,7 @@ class ACPAgent:
|
|
|
321
317
|
# Create permission request with options
|
|
322
318
|
permission_request = acp.RequestPermissionRequest(
|
|
323
319
|
sessionId=self.session_id,
|
|
324
|
-
toolCall=acp.schema.
|
|
320
|
+
toolCall=acp.schema.ToolCall(
|
|
325
321
|
toolCallId=state.acp_tool_call_id,
|
|
326
322
|
content=[
|
|
327
323
|
acp.schema.ContentToolCallContent(
|
kimi_cli/ui/print/__init__.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
-
import signal
|
|
4
3
|
import sys
|
|
5
4
|
from functools import partial
|
|
6
5
|
from pathlib import Path
|
|
@@ -9,10 +8,12 @@ from typing import Literal
|
|
|
9
8
|
import aiofiles
|
|
10
9
|
from kosong.base.message import Message
|
|
11
10
|
from kosong.chat_provider import ChatProviderError
|
|
11
|
+
from rich import print
|
|
12
12
|
|
|
13
13
|
from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
14
14
|
from kimi_cli.utils.logging import logger
|
|
15
15
|
from kimi_cli.utils.message import message_extract_text
|
|
16
|
+
from kimi_cli.utils.signals import install_sigint_handler
|
|
16
17
|
from kimi_cli.wire import WireUISide
|
|
17
18
|
from kimi_cli.wire.message import StepInterrupted
|
|
18
19
|
|
|
@@ -51,7 +52,7 @@ class PrintApp:
|
|
|
51
52
|
cancel_event.set()
|
|
52
53
|
|
|
53
54
|
loop = asyncio.get_running_loop()
|
|
54
|
-
loop
|
|
55
|
+
remove_sigint = install_sigint_handler(loop, _handler)
|
|
55
56
|
|
|
56
57
|
if command is None and not sys.stdin.isatty() and self.input_format == "text":
|
|
57
58
|
command = sys.stdin.read().strip()
|
|
@@ -98,7 +99,7 @@ class PrintApp:
|
|
|
98
99
|
print(f"Unknown error: {e}")
|
|
99
100
|
raise
|
|
100
101
|
finally:
|
|
101
|
-
|
|
102
|
+
remove_sigint()
|
|
102
103
|
return False
|
|
103
104
|
|
|
104
105
|
def _read_next_command(self) -> str | None:
|
|
@@ -127,33 +128,29 @@ class PrintApp:
|
|
|
127
128
|
logger.warning("Ignoring invalid user message: {json_line}", json_line=json_line)
|
|
128
129
|
|
|
129
130
|
async def _visualize_text(self, wire: WireUISide):
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
break
|
|
136
|
-
except asyncio.QueueShutDown:
|
|
137
|
-
logger.debug("Visualization loop shutting down")
|
|
131
|
+
while True:
|
|
132
|
+
msg = await wire.receive()
|
|
133
|
+
print(msg)
|
|
134
|
+
if isinstance(msg, StepInterrupted):
|
|
135
|
+
break
|
|
138
136
|
|
|
139
137
|
async def _visualize_stream_json(self, wire: WireUISide, start_position: int):
|
|
140
138
|
# TODO: be aware of context compaction
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
logger.debug("Visualization loop shutting down")
|
|
139
|
+
# FIXME: this is only a temporary impl, may miss the last lines of the context file
|
|
140
|
+
if not self.context_file.exists():
|
|
141
|
+
self.context_file.touch()
|
|
142
|
+
async with aiofiles.open(self.context_file, encoding="utf-8") as f:
|
|
143
|
+
await f.seek(start_position)
|
|
144
|
+
while True:
|
|
145
|
+
should_end = False
|
|
146
|
+
while (msg := wire.receive_nowait()) is not None:
|
|
147
|
+
if isinstance(msg, StepInterrupted):
|
|
148
|
+
should_end = True
|
|
149
|
+
|
|
150
|
+
line = await f.readline()
|
|
151
|
+
if not line:
|
|
152
|
+
if should_end:
|
|
153
|
+
break
|
|
154
|
+
await asyncio.sleep(0.1)
|
|
155
|
+
continue
|
|
156
|
+
print(line, end="")
|