kimi-cli 0.40__py3-none-any.whl → 0.41__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kimi-cli might be problematic. Click here for more details.

Files changed (48) hide show
  1. kimi_cli/CHANGELOG.md +12 -0
  2. kimi_cli/__init__.py +18 -280
  3. kimi_cli/agents/koder/system.md +1 -1
  4. kimi_cli/agentspec.py +104 -0
  5. kimi_cli/cli.py +235 -0
  6. kimi_cli/constant.py +4 -0
  7. kimi_cli/llm.py +69 -0
  8. kimi_cli/prompts/__init__.py +2 -2
  9. kimi_cli/soul/__init__.py +102 -6
  10. kimi_cli/soul/agent.py +157 -0
  11. kimi_cli/soul/approval.py +1 -1
  12. kimi_cli/soul/compaction.py +4 -4
  13. kimi_cli/soul/context.py +5 -0
  14. kimi_cli/soul/globals.py +92 -0
  15. kimi_cli/soul/kimisoul.py +21 -26
  16. kimi_cli/tools/dmail/__init__.py +1 -1
  17. kimi_cli/tools/file/glob.md +1 -1
  18. kimi_cli/tools/file/glob.py +2 -2
  19. kimi_cli/tools/file/grep.py +1 -1
  20. kimi_cli/tools/file/patch.py +2 -2
  21. kimi_cli/tools/file/read.py +1 -1
  22. kimi_cli/tools/file/replace.py +2 -2
  23. kimi_cli/tools/file/write.py +2 -2
  24. kimi_cli/tools/task/__init__.py +23 -22
  25. kimi_cli/tools/task/task.md +1 -1
  26. kimi_cli/tools/todo/__init__.py +1 -1
  27. kimi_cli/tools/utils.py +1 -1
  28. kimi_cli/tools/web/search.py +2 -2
  29. kimi_cli/ui/__init__.py +0 -69
  30. kimi_cli/ui/acp/__init__.py +8 -9
  31. kimi_cli/ui/print/__init__.py +17 -35
  32. kimi_cli/ui/shell/__init__.py +5 -13
  33. kimi_cli/ui/shell/liveview.py +1 -1
  34. kimi_cli/ui/shell/metacmd.py +3 -3
  35. kimi_cli/ui/shell/setup.py +5 -5
  36. kimi_cli/ui/shell/update.py +2 -2
  37. kimi_cli/ui/shell/visualize.py +10 -7
  38. kimi_cli/utils/changelog.py +3 -1
  39. kimi_cli/wire/__init__.py +57 -0
  40. kimi_cli/{soul/wire.py → wire/message.py} +4 -39
  41. {kimi_cli-0.40.dist-info → kimi_cli-0.41.dist-info}/METADATA +34 -1
  42. kimi_cli-0.41.dist-info/RECORD +85 -0
  43. kimi_cli-0.41.dist-info/entry_points.txt +3 -0
  44. kimi_cli/agent.py +0 -261
  45. kimi_cli/utils/provider.py +0 -70
  46. kimi_cli-0.40.dist-info/RECORD +0 -81
  47. kimi_cli-0.40.dist-info/entry_points.txt +0 -3
  48. {kimi_cli-0.40.dist-info → kimi_cli-0.41.dist-info}/WHEEL +0 -0
@@ -0,0 +1,92 @@
1
+ import subprocess
2
+ import sys
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import NamedTuple
6
+
7
+ from kimi_cli.config import Config
8
+ from kimi_cli.llm import LLM
9
+ from kimi_cli.metadata import Session
10
+ from kimi_cli.soul.approval import Approval
11
+ from kimi_cli.soul.denwarenji import DenwaRenji
12
+ from kimi_cli.utils.logging import logger
13
+
14
+
15
+ class BuiltinSystemPromptArgs(NamedTuple):
16
+ """Builtin system prompt arguments."""
17
+
18
+ KIMI_NOW: str
19
+ """The current datetime."""
20
+ KIMI_WORK_DIR: Path
21
+ """The current working directory."""
22
+ KIMI_WORK_DIR_LS: str
23
+ """The directory listing of current working directory."""
24
+ KIMI_AGENTS_MD: str # TODO: move to first message from system prompt
25
+ """The content of AGENTS.md."""
26
+
27
+
28
+ def load_agents_md(work_dir: Path) -> str | None:
29
+ paths = [
30
+ work_dir / "AGENTS.md",
31
+ work_dir / "agents.md",
32
+ ]
33
+ for path in paths:
34
+ if path.is_file():
35
+ logger.info("Loaded agents.md: {path}", path=path)
36
+ return path.read_text(encoding="utf-8").strip()
37
+ logger.info("No AGENTS.md found in {work_dir}", work_dir=work_dir)
38
+ return None
39
+
40
+
41
+ def _list_work_dir(work_dir: Path) -> str:
42
+ if sys.platform == "win32":
43
+ ls = subprocess.run(
44
+ ["cmd", "/c", "dir", work_dir],
45
+ capture_output=True,
46
+ text=True,
47
+ encoding="utf-8",
48
+ errors="replace",
49
+ )
50
+ else:
51
+ ls = subprocess.run(
52
+ ["ls", "-la", work_dir],
53
+ capture_output=True,
54
+ text=True,
55
+ encoding="utf-8",
56
+ errors="replace",
57
+ )
58
+ return ls.stdout.strip()
59
+
60
+
61
+ class AgentGlobals(NamedTuple):
62
+ """Agent globals."""
63
+
64
+ config: Config
65
+ llm: LLM | None
66
+ session: Session
67
+ builtin_args: BuiltinSystemPromptArgs
68
+ denwa_renji: DenwaRenji
69
+ approval: Approval
70
+
71
+ @classmethod
72
+ async def create(
73
+ cls, config: Config, llm: LLM | None, session: Session, yolo: bool
74
+ ) -> "AgentGlobals":
75
+ work_dir = Path(session.work_dir.path)
76
+ # FIXME: do these asynchronously
77
+ ls_output = _list_work_dir(work_dir)
78
+ agents_md = load_agents_md(work_dir) or ""
79
+
80
+ return cls(
81
+ config=config,
82
+ llm=llm,
83
+ session=session,
84
+ builtin_args=BuiltinSystemPromptArgs(
85
+ KIMI_NOW=datetime.now().astimezone().isoformat(),
86
+ KIMI_WORK_DIR=work_dir,
87
+ KIMI_WORK_DIR_LS=ls_output,
88
+ KIMI_AGENTS_MD=agents_md,
89
+ ),
90
+ denwa_renji=DenwaRenji(),
91
+ approval=Approval(yolo=yolo),
92
+ )
kimi_cli/soul/kimisoul.py CHANGED
@@ -16,29 +16,28 @@ from kosong.chat_provider import (
16
16
  from kosong.tooling import ToolResult
17
17
  from tenacity import RetryCallState, retry_if_exception, stop_after_attempt, wait_exponential_jitter
18
18
 
19
- from kimi_cli.agent import Agent, AgentGlobals
20
19
  from kimi_cli.config import LoopControl
21
- from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot
20
+ from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot, wire_send
21
+ from kimi_cli.soul.agent import Agent
22
22
  from kimi_cli.soul.compaction import SimpleCompaction
23
23
  from kimi_cli.soul.context import Context
24
+ from kimi_cli.soul.globals import AgentGlobals
24
25
  from kimi_cli.soul.message import system, tool_result_to_messages
25
- from kimi_cli.soul.wire import (
26
+ from kimi_cli.tools.dmail import NAME as SendDMail_NAME
27
+ from kimi_cli.tools.utils import ToolRejectedError
28
+ from kimi_cli.utils.logging import logger
29
+ from kimi_cli.wire.message import (
26
30
  CompactionBegin,
27
31
  CompactionEnd,
28
32
  StatusUpdate,
29
33
  StepBegin,
30
34
  StepInterrupted,
31
- Wire,
32
- current_wire,
33
35
  )
34
- from kimi_cli.tools.dmail import NAME as SendDMail_NAME
35
- from kimi_cli.tools.utils import ToolRejectedError
36
- from kimi_cli.utils.logging import logger
37
36
 
38
37
  RESERVED_TOKENS = 50_000
39
38
 
40
39
 
41
- class KimiSoul:
40
+ class KimiSoul(Soul):
42
41
  """The soul of Kimi CLI."""
43
42
 
44
43
  def __init__(
@@ -97,31 +96,27 @@ class KimiSoul:
97
96
  async def _checkpoint(self):
98
97
  await self._context.checkpoint(self._checkpoint_with_user_message)
99
98
 
100
- async def run(self, user_input: str, wire: Wire):
99
+ async def run(self, user_input: str):
101
100
  if self._agent_globals.llm is None:
102
101
  raise LLMNotSet()
103
102
 
104
103
  await self._checkpoint() # this creates the checkpoint 0 on first run
105
104
  await self._context.append_message(Message(role="user", content=user_input))
106
105
  logger.debug("Appended user message to context")
107
- wire_token = current_wire.set(wire)
108
- try:
109
- await self._agent_loop(wire)
110
- finally:
111
- current_wire.reset(wire_token)
106
+ await self._agent_loop()
112
107
 
113
- async def _agent_loop(self, wire: Wire):
108
+ async def _agent_loop(self):
114
109
  """The main agent loop for one run."""
115
110
  assert self._agent_globals.llm is not None
116
111
 
117
112
  async def _pipe_approval_to_wire():
118
113
  while True:
119
114
  request = await self._approval.fetch_request()
120
- wire.send(request)
115
+ wire_send(request)
121
116
 
122
117
  step_no = 1
123
118
  while True:
124
- wire.send(StepBegin(step_no))
119
+ wire_send(StepBegin(step_no))
125
120
  approval_task = asyncio.create_task(_pipe_approval_to_wire())
126
121
  # FIXME: It's possible that a subagent's approval task steals approval request
127
122
  # from the main agent. We must ensure that the Task tool will redirect them
@@ -134,21 +129,21 @@ class KimiSoul:
134
129
  >= self._agent_globals.llm.max_context_size
135
130
  ):
136
131
  logger.info("Context too long, compacting...")
137
- wire.send(CompactionBegin())
132
+ wire_send(CompactionBegin())
138
133
  await self.compact_context()
139
- wire.send(CompactionEnd())
134
+ wire_send(CompactionEnd())
140
135
 
141
136
  logger.debug("Beginning step {step_no}", step_no=step_no)
142
137
  await self._checkpoint()
143
138
  self._denwa_renji.set_n_checkpoints(self._context.n_checkpoints)
144
- finished = await self._step(wire)
139
+ finished = await self._step()
145
140
  except BackToTheFuture as e:
146
141
  await self._context.revert_to(e.checkpoint_id)
147
142
  await self._checkpoint()
148
143
  await self._context.append_message(e.messages)
149
144
  continue
150
145
  except (ChatProviderError, asyncio.CancelledError):
151
- wire.send(StepInterrupted())
146
+ wire_send(StepInterrupted())
152
147
  # break the agent loop
153
148
  raise
154
149
  finally:
@@ -161,7 +156,7 @@ class KimiSoul:
161
156
  if step_no > self._loop_control.max_steps_per_run:
162
157
  raise MaxStepsReached(self._loop_control.max_steps_per_run)
163
158
 
164
- async def _step(self, wire: Wire) -> bool:
159
+ async def _step(self) -> bool:
165
160
  """Run an single step and return whether the run should be stopped."""
166
161
  # already checked in `run`
167
162
  assert self._agent_globals.llm is not None
@@ -181,8 +176,8 @@ class KimiSoul:
181
176
  self._agent.system_prompt,
182
177
  self._agent.toolset,
183
178
  self._context.history,
184
- on_message_part=wire.send,
185
- on_tool_result=wire.send,
179
+ on_message_part=wire_send,
180
+ on_tool_result=wire_send,
186
181
  )
187
182
 
188
183
  result = await _kosong_step_with_retry()
@@ -190,7 +185,7 @@ class KimiSoul:
190
185
  if result.usage is not None:
191
186
  # mark the token count for the context before the step
192
187
  await self._context.update_token_count(result.usage.input)
193
- wire.send(StatusUpdate(status=self.status))
188
+ wire_send(StatusUpdate(status=self.status))
194
189
 
195
190
  # wait for all tool results (may be interrupted)
196
191
  results = await result.tool_results()
@@ -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):
@@ -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 recursivelly 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.
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.
@@ -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.agent import BuiltinSystemPromptArgs
11
+ from kimi_cli.soul.globals import BuiltinSystemPromptArgs
12
12
  from kimi_cli.tools.utils import load_desc
13
13
 
14
14
  MAX_MATCHES = 1000
@@ -128,7 +128,7 @@ class Glob(CallableTool2[Params]):
128
128
  message = (
129
129
  f"Found {len(matches)} matches for pattern `{params.pattern}`."
130
130
  if len(matches) > 0
131
- else "No matches found for pattern `{params.pattern}`."
131
+ else f"No matches found for pattern `{params.pattern}`."
132
132
  )
133
133
  if len(matches) > MAX_MATCHES:
134
134
  matches = matches[:MAX_MATCHES]
@@ -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
@@ -6,8 +6,8 @@ import patch_ng
6
6
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
7
7
  from pydantic import BaseModel, Field
8
8
 
9
- from kimi_cli.agent import BuiltinSystemPromptArgs
10
9
  from kimi_cli.soul.approval import Approval
10
+ from kimi_cli.soul.globals import BuiltinSystemPromptArgs
11
11
  from kimi_cli.tools.file import FileActions
12
12
  from kimi_cli.tools.utils import ToolRejectedError
13
13
 
@@ -19,7 +19,7 @@ class Params(BaseModel):
19
19
 
20
20
  class PatchFile(CallableTool2[Params]):
21
21
  name: str = "PatchFile"
22
- description: str = (Path(__file__).parent / "patch.md").read_text()
22
+ description: str = (Path(__file__).parent / "patch.md").read_text(encoding="utf-8")
23
23
  params: type[Params] = Params
24
24
 
25
25
  def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
@@ -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.agent import BuiltinSystemPromptArgs
8
+ from kimi_cli.soul.globals import BuiltinSystemPromptArgs
9
9
  from kimi_cli.tools.utils import load_desc, truncate_line
10
10
 
11
11
  MAX_LINES = 1000
@@ -5,8 +5,8 @@ import aiofiles
5
5
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
6
6
  from pydantic import BaseModel, Field
7
7
 
8
- from kimi_cli.agent import BuiltinSystemPromptArgs
9
8
  from kimi_cli.soul.approval import Approval
9
+ from kimi_cli.soul.globals import BuiltinSystemPromptArgs
10
10
  from kimi_cli.tools.file import FileActions
11
11
  from kimi_cli.tools.utils import ToolRejectedError
12
12
 
@@ -29,7 +29,7 @@ class Params(BaseModel):
29
29
 
30
30
  class StrReplaceFile(CallableTool2[Params]):
31
31
  name: str = "StrReplaceFile"
32
- description: str = (Path(__file__).parent / "replace.md").read_text()
32
+ description: str = (Path(__file__).parent / "replace.md").read_text(encoding="utf-8")
33
33
  params: type[Params] = Params
34
34
 
35
35
  def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
@@ -5,8 +5,8 @@ import aiofiles
5
5
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
6
6
  from pydantic import BaseModel, Field
7
7
 
8
- from kimi_cli.agent import BuiltinSystemPromptArgs
9
8
  from kimi_cli.soul.approval import Approval
9
+ from kimi_cli.soul.globals import BuiltinSystemPromptArgs
10
10
  from kimi_cli.tools.file import FileActions
11
11
  from kimi_cli.tools.utils import ToolRejectedError
12
12
 
@@ -26,7 +26,7 @@ class Params(BaseModel):
26
26
 
27
27
  class WriteFile(CallableTool2[Params]):
28
28
  name: str = "WriteFile"
29
- description: str = (Path(__file__).parent / "write.md").read_text()
29
+ description: str = (Path(__file__).parent / "write.md").read_text(encoding="utf-8")
30
30
  params: type[Params] = Params
31
31
 
32
32
  def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
@@ -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.agent import Agent, AgentGlobals, AgentSpec, load_agent
8
- from kimi_cli.soul import MaxStepsReached
8
+ from kimi_cli.agentspec import ResolvedAgentSpec
9
+ from kimi_cli.soul import MaxStepsReached, get_wire_or_none, run_soul
10
+ from kimi_cli.soul.agent import Agent, load_agent
9
11
  from kimi_cli.soul.context import Context
12
+ from kimi_cli.soul.globals import AgentGlobals
10
13
  from kimi_cli.soul.kimisoul import KimiSoul
11
- from kimi_cli.soul.wire import ApprovalRequest, Wire, WireMessage, get_wire_or_none
12
14
  from kimi_cli.tools.utils import load_desc
13
15
  from kimi_cli.utils.message import message_extract_text
14
16
  from kimi_cli.utils.path import next_available_rotation
17
+ from kimi_cli.wire import WireUISide
18
+ from kimi_cli.wire.message import ApprovalRequest, WireMessage
15
19
 
16
20
  # Maximum continuation attempts for task summary
17
21
  MAX_CONTINUE_ATTEMPTS = 1
@@ -45,12 +49,11 @@ class Task(CallableTool2[Params]):
45
49
  name: str = "Task"
46
50
  params: type[Params] = Params
47
51
 
48
- def __init__(self, agent_spec: AgentSpec, agent_globals: AgentGlobals, **kwargs):
52
+ def __init__(self, agent_spec: ResolvedAgentSpec, agent_globals: AgentGlobals, **kwargs):
49
53
  subagents: dict[str, Agent] = {}
50
54
  descs = []
51
55
 
52
56
  # load all subagents
53
- assert agent_spec.subagents is not None, "Task tool expects subagents"
54
57
  for name, spec in agent_spec.subagents.items():
55
58
  subagents[name] = load_agent(spec.path, agent_globals)
56
59
  descs.append(f"- `{name}`: {spec.description}")
@@ -99,6 +102,19 @@ class Task(CallableTool2[Params]):
99
102
 
100
103
  async def _run_subagent(self, agent: Agent, prompt: str) -> ToolReturnType:
101
104
  """Run subagent with optional continuation for task summary."""
105
+ super_wire = get_wire_or_none()
106
+ assert super_wire is not None
107
+
108
+ def _super_wire_send(msg: WireMessage) -> None:
109
+ if isinstance(msg, ApprovalRequest):
110
+ super_wire.soul_side.send(msg)
111
+ # TODO: visualize subagent behavior by sending other messages in some way
112
+
113
+ async def _ui_loop_fn(wire: WireUISide) -> None:
114
+ while True:
115
+ msg = await wire.receive()
116
+ _super_wire_send(msg)
117
+
102
118
  subagent_history_file = await self._get_subagent_history_file()
103
119
  context = Context(file_backend=subagent_history_file)
104
120
  soul = KimiSoul(
@@ -107,12 +123,9 @@ class Task(CallableTool2[Params]):
107
123
  context=context,
108
124
  loop_control=self._agent_globals.config.loop_control,
109
125
  )
110
- wire = get_wire_or_none()
111
- assert wire is not None, "Wire is expected to be set"
112
- sub_wire = _SubWire(wire)
113
126
 
114
127
  try:
115
- await soul.run(prompt, sub_wire)
128
+ await run_soul(soul, prompt, _ui_loop_fn, asyncio.Event())
116
129
  except MaxStepsReached as e:
117
130
  return ToolError(
118
131
  message=(
@@ -135,22 +148,10 @@ class Task(CallableTool2[Params]):
135
148
  # Check if response is too brief, if so, run again with continuation prompt
136
149
  n_attempts_remaining = MAX_CONTINUE_ATTEMPTS
137
150
  if len(final_response) < 200 and n_attempts_remaining > 0:
138
- await soul.run(CONTINUE_PROMPT, sub_wire)
151
+ await run_soul(soul, CONTINUE_PROMPT, _ui_loop_fn, asyncio.Event())
139
152
 
140
153
  if len(context.history) == 0 or context.history[-1].role != "assistant":
141
154
  return ToolError(message=_error_msg, brief="Failed to run subagent")
142
155
  final_response = message_extract_text(context.history[-1])
143
156
 
144
157
  return ToolOk(output=final_response)
145
-
146
-
147
- class _SubWire(Wire):
148
- def __init__(self, super_wire: Wire):
149
- super().__init__()
150
- self._super_wire = super_wire
151
-
152
- @override
153
- def send(self, msg: WireMessage):
154
- if isinstance(msg, ApprovalRequest):
155
- self._super_wire.send(msg)
156
- # TODO: visualize subagent behavior by sending other messages in some way
@@ -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 scenerios you may want this tool for context isolation:
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.
@@ -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
@@ -4,8 +4,8 @@ from typing import override
4
4
  from kosong.tooling import CallableTool2, ToolReturnType
5
5
  from pydantic import BaseModel, Field, ValidationError
6
6
 
7
- import kimi_cli
8
7
  from kimi_cli.config import Config
8
+ from kimi_cli.constant import USER_AGENT
9
9
  from kimi_cli.soul.toolset import get_current_tool_call_or_none
10
10
  from kimi_cli.tools.utils import ToolResultBuilder, load_desc
11
11
  from kimi_cli.utils.aiohttp import new_client_session
@@ -66,7 +66,7 @@ class SearchWeb(CallableTool2[Params]):
66
66
  session.post(
67
67
  self._base_url,
68
68
  headers={
69
- "User-Agent": kimi_cli.USER_AGENT,
69
+ "User-Agent": USER_AGENT,
70
70
  "Authorization": f"Bearer {self._api_key}",
71
71
  "X-Msh-Tool-Call-Id": tool_call.id,
72
72
  },
kimi_cli/ui/__init__.py CHANGED
@@ -1,69 +0,0 @@
1
- import asyncio
2
- import contextlib
3
- from collections.abc import Callable, Coroutine
4
- from typing import Any
5
-
6
- from kimi_cli.soul import Soul
7
- from kimi_cli.soul.wire import Wire
8
- from kimi_cli.utils.logging import logger
9
-
10
- type UILoopFn = Callable[[Wire], Coroutine[Any, Any, None]]
11
- """A long-running async function to visualize the agent behavior."""
12
-
13
-
14
- class RunCancelled(Exception):
15
- """The run was cancelled by the cancel event."""
16
-
17
-
18
- async def run_soul(
19
- soul: Soul,
20
- user_input: str,
21
- ui_loop_fn: UILoopFn,
22
- cancel_event: asyncio.Event,
23
- ):
24
- """
25
- Run the soul with the given user input.
26
-
27
- `cancel_event` is a outside handle that can be used to cancel the run. When the event is set,
28
- the run will be gracefully stopped and a `RunCancelled` will be raised.
29
-
30
- Raises:
31
- LLMNotSet: When the LLM is not set.
32
- ChatProviderError: When the LLM provider returns an error.
33
- MaxStepsReached: When the maximum number of steps is reached.
34
- RunCancelled: When the run is cancelled by the cancel event.
35
- """
36
- wire = Wire()
37
- logger.debug("Starting UI loop with function: {ui_loop_fn}", ui_loop_fn=ui_loop_fn)
38
-
39
- ui_task = asyncio.create_task(ui_loop_fn(wire))
40
- soul_task = asyncio.create_task(soul.run(user_input, wire))
41
-
42
- cancel_event_task = asyncio.create_task(cancel_event.wait())
43
- await asyncio.wait(
44
- [soul_task, cancel_event_task],
45
- return_when=asyncio.FIRST_COMPLETED,
46
- )
47
-
48
- try:
49
- if cancel_event.is_set():
50
- logger.debug("Cancelling the run task")
51
- soul_task.cancel()
52
- try:
53
- await soul_task
54
- except asyncio.CancelledError:
55
- raise RunCancelled from None
56
- else:
57
- assert soul_task.done() # either stop event is set or the run task is done
58
- cancel_event_task.cancel()
59
- with contextlib.suppress(asyncio.CancelledError):
60
- await cancel_event_task
61
- soul_task.result() # this will raise if any exception was raised in the run task
62
- finally:
63
- logger.debug("Shutting down the visualization loop")
64
- # shutting down the event queue should break the visualization loop
65
- wire.shutdown()
66
- try:
67
- await asyncio.wait_for(ui_task, timeout=0.5)
68
- except TimeoutError:
69
- logger.warning("Visualization loop timed out")
@@ -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.soul.wire import (
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 ACPAgentImpl:
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: 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: ACPAgentImpl(self.soul, conn),
430
+ lambda conn: ACPAgent(self.soul, conn),
432
431
  writer,
433
432
  reader,
434
433
  )