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.

Files changed (55) hide show
  1. kimi_cli/CHANGELOG.md +27 -0
  2. kimi_cli/__init__.py +127 -359
  3. kimi_cli/agents/{koder → default}/agent.yaml +1 -1
  4. kimi_cli/agents/{koder → default}/system.md +1 -1
  5. kimi_cli/agentspec.py +115 -0
  6. kimi_cli/cli.py +249 -0
  7. kimi_cli/config.py +28 -14
  8. kimi_cli/constant.py +4 -0
  9. kimi_cli/exception.py +16 -0
  10. kimi_cli/llm.py +70 -0
  11. kimi_cli/metadata.py +5 -68
  12. kimi_cli/prompts/__init__.py +2 -2
  13. kimi_cli/session.py +81 -0
  14. kimi_cli/soul/__init__.py +102 -6
  15. kimi_cli/soul/agent.py +152 -0
  16. kimi_cli/soul/approval.py +1 -1
  17. kimi_cli/soul/compaction.py +4 -4
  18. kimi_cli/soul/kimisoul.py +39 -46
  19. kimi_cli/soul/runtime.py +94 -0
  20. kimi_cli/tools/dmail/__init__.py +1 -1
  21. kimi_cli/tools/file/glob.md +1 -1
  22. kimi_cli/tools/file/glob.py +2 -2
  23. kimi_cli/tools/file/grep.py +1 -1
  24. kimi_cli/tools/file/patch.py +2 -2
  25. kimi_cli/tools/file/read.py +1 -1
  26. kimi_cli/tools/file/replace.py +2 -2
  27. kimi_cli/tools/file/write.py +2 -2
  28. kimi_cli/tools/task/__init__.py +48 -40
  29. kimi_cli/tools/task/task.md +1 -1
  30. kimi_cli/tools/todo/__init__.py +1 -1
  31. kimi_cli/tools/utils.py +1 -1
  32. kimi_cli/tools/web/search.py +5 -2
  33. kimi_cli/ui/__init__.py +0 -69
  34. kimi_cli/ui/acp/__init__.py +8 -9
  35. kimi_cli/ui/print/__init__.py +21 -37
  36. kimi_cli/ui/shell/__init__.py +8 -19
  37. kimi_cli/ui/shell/liveview.py +1 -1
  38. kimi_cli/ui/shell/metacmd.py +5 -10
  39. kimi_cli/ui/shell/prompt.py +10 -3
  40. kimi_cli/ui/shell/setup.py +5 -5
  41. kimi_cli/ui/shell/update.py +2 -2
  42. kimi_cli/ui/shell/visualize.py +10 -7
  43. kimi_cli/utils/changelog.py +3 -1
  44. kimi_cli/wire/__init__.py +69 -0
  45. kimi_cli/{soul/wire.py → wire/message.py} +4 -39
  46. {kimi_cli-0.40.dist-info → kimi_cli-0.42.dist-info}/METADATA +51 -18
  47. kimi_cli-0.42.dist-info/RECORD +86 -0
  48. kimi_cli-0.42.dist-info/entry_points.txt +3 -0
  49. kimi_cli/agent.py +0 -261
  50. kimi_cli/agents/koder/README.md +0 -3
  51. kimi_cli/utils/provider.py +0 -70
  52. kimi_cli-0.40.dist-info/RECORD +0 -81
  53. kimi_cli-0.40.dist-info/entry_points.txt +0 -3
  54. /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
  55. {kimi_cli-0.40.dist-info → kimi_cli-0.42.dist-info}/WHEEL +0 -0
@@ -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):
@@ -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.runtime 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.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):
@@ -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):
@@ -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, 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.wire import ApprovalRequest, Wire, WireMessage, get_wire_or_none
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: AgentSpec, agent_globals: AgentGlobals, **kwargs):
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(descs),
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._agent_globals = agent_globals
69
- self._session = agent_globals.session
70
- self._subagents = 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.run(prompt, sub_wire)
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.run(CONTINUE_PROMPT, sub_wire)
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
@@ -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
@@ -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": kimi_cli.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")
@@ -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
  )
@@ -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 (KimiSoul): The soul to run. Only `KimiSoul` is supported.
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__(self, soul: KimiSoul, input_format: InputFormat, output_format: OutputFormat):
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.soul._approval.set_yolo(True)
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: 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: Wire, start_position: int):
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.soul._context._file_backend, encoding="utf-8") as f:
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._queue.qsize() > 0:
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
 
@@ -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._start_auto_update_task()
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 _start_auto_update_task(self) -> None:
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 _add_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
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 __version__ as current_version
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(
@@ -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:
@@ -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 __version__
176
+ from kimi_cli.constant import VERSION
177
177
 
178
- console.print(f"kimi, version {__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._agent_globals.builtin_args.KIMI_WORK_DIR)
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. "