kimi-cli 0.35__py3-none-any.whl → 0.36__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.

@@ -1,11 +1,10 @@
1
1
  import asyncio
2
2
  import signal
3
3
  from collections.abc import Awaitable, Coroutine
4
+ from functools import partial
4
5
  from typing import Any
5
6
 
6
- from kosong.base.message import ContentPart, TextPart, ToolCall, ToolCallPart
7
7
  from kosong.chat_provider import APIStatusError, ChatProviderError
8
- from kosong.tooling import ToolResult
9
8
  from rich.console import Group, RenderableType
10
9
  from rich.panel import Panel
11
10
  from rich.table import Table
@@ -13,20 +12,12 @@ from rich.text import Text
13
12
 
14
13
  from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul
15
14
  from kimi_cli.soul.kimisoul import KimiSoul
16
- from kimi_cli.soul.wire import (
17
- ApprovalRequest,
18
- ApprovalResponse,
19
- StatusUpdate,
20
- StepBegin,
21
- StepInterrupted,
22
- Wire,
23
- )
24
15
  from kimi_cli.ui import RunCancelled, run_soul
25
16
  from kimi_cli.ui.shell.console import console
26
- from kimi_cli.ui.shell.liveview import StepLiveView
27
17
  from kimi_cli.ui.shell.metacmd import get_meta_command
28
18
  from kimi_cli.ui.shell.prompt import CustomPromptSession, PromptMode, toast
29
19
  from kimi_cli.ui.shell.update import UpdateResult, do_update
20
+ from kimi_cli.ui.shell.visualize import visualize
30
21
  from kimi_cli.utils.logging import logger
31
22
 
32
23
 
@@ -46,7 +37,7 @@ class ShellApp:
46
37
  if command is not None:
47
38
  # run single command and exit
48
39
  logger.info("Running agent with command: {command}", command=command)
49
- return await self._run(command)
40
+ return await self._run_soul_command(command)
50
41
 
51
42
  self._start_auto_update_task()
52
43
 
@@ -86,7 +77,7 @@ class ShellApp:
86
77
  continue
87
78
 
88
79
  logger.info("Running agent command: {command}", command=command)
89
- await self._run(command)
80
+ await self._run_soul_command(command)
90
81
 
91
82
  return True
92
83
 
@@ -115,7 +106,44 @@ class ShellApp:
115
106
  finally:
116
107
  loop.remove_signal_handler(signal.SIGINT)
117
108
 
118
- async def _run(self, command: str) -> bool:
109
+ async def _run_meta_command(self, command_str: str):
110
+ parts = command_str.split(" ")
111
+ command_name = parts[0]
112
+ command_args = parts[1:]
113
+ command = get_meta_command(command_name)
114
+ if command is None:
115
+ console.print(f"Meta command /{command_name} not found")
116
+ return
117
+ if command.kimi_soul_only and not isinstance(self.soul, KimiSoul):
118
+ console.print(f"Meta command /{command_name} not supported")
119
+ return
120
+ logger.debug(
121
+ "Running meta command: {command_name} with args: {command_args}",
122
+ command_name=command_name,
123
+ command_args=command_args,
124
+ )
125
+ try:
126
+ ret = command.func(self, command_args)
127
+ if isinstance(ret, Awaitable):
128
+ await ret
129
+ except LLMNotSet:
130
+ logger.error("LLM not set")
131
+ console.print("[red]LLM not set, send /setup to configure[/red]")
132
+ except ChatProviderError as e:
133
+ logger.exception("LLM provider error:")
134
+ console.print(f"[red]LLM provider error: {e}[/red]")
135
+ except asyncio.CancelledError:
136
+ logger.info("Interrupted by user")
137
+ console.print("[red]Interrupted by user[/red]")
138
+ except Reload:
139
+ # just propagate
140
+ raise
141
+ except BaseException as e:
142
+ logger.exception("Unknown error:")
143
+ console.print(f"[red]Unknown error: {e}[/red]")
144
+ raise # re-raise unknown error
145
+
146
+ async def _run_soul_command(self, command: str) -> bool:
119
147
  """
120
148
  Run the soul and handle any known exceptions.
121
149
 
@@ -132,7 +160,12 @@ class ShellApp:
132
160
  loop.add_signal_handler(signal.SIGINT, _handler)
133
161
 
134
162
  try:
135
- await run_soul(self.soul, command, self._visualize, cancel_event)
163
+ await run_soul(
164
+ self.soul,
165
+ command,
166
+ partial(visualize, initial_status=self.soul.status),
167
+ cancel_event,
168
+ )
136
169
  return True
137
170
  except LLMNotSet:
138
171
  logger.error("LLM not set")
@@ -193,93 +226,6 @@ class ShellApp:
193
226
  task.add_done_callback(_cleanup)
194
227
  return task
195
228
 
196
- async def _visualize(self, wire: Wire):
197
- """
198
- A loop to consume agent events and visualize the agent behavior.
199
- This loop never raise any exception except asyncio.CancelledError.
200
- """
201
- try:
202
- # expect a StepBegin
203
- assert isinstance(await wire.receive(), StepBegin)
204
-
205
- while True:
206
- # spin the moon at the beginning of each step
207
- with console.status("", spinner="moon"):
208
- msg = await wire.receive()
209
-
210
- with StepLiveView(self.soul.status) as step:
211
- # visualization loop for one step
212
- while True:
213
- match msg:
214
- case TextPart(text=text):
215
- step.append_text(text)
216
- case ContentPart():
217
- # TODO: support more content parts
218
- step.append_text(f"[{msg.__class__.__name__}]")
219
- case ToolCall():
220
- step.append_tool_call(msg)
221
- case ToolCallPart():
222
- step.append_tool_call_part(msg)
223
- case ToolResult():
224
- step.append_tool_result(msg)
225
- case ApprovalRequest():
226
- msg.resolve(ApprovalResponse.APPROVE)
227
- # TODO(approval): handle approval request
228
- case StatusUpdate(status=status):
229
- step.update_status(status)
230
- case _:
231
- break # break the step loop
232
- msg = await wire.receive()
233
-
234
- # cleanup the step live view
235
- if isinstance(msg, StepInterrupted):
236
- step.interrupt()
237
- else:
238
- step.finish()
239
-
240
- if isinstance(msg, StepInterrupted):
241
- # for StepInterrupted, the visualization loop should end immediately
242
- break
243
-
244
- assert isinstance(msg, StepBegin), "expect a StepBegin"
245
- # start a new step
246
- except asyncio.QueueShutDown:
247
- logger.debug("Visualization loop shutting down")
248
-
249
- async def _run_meta_command(self, command_str: str):
250
- parts = command_str.split(" ")
251
- command_name = parts[0]
252
- command_args = parts[1:]
253
- command = get_meta_command(command_name)
254
- if command is None:
255
- console.print(f"Meta command /{command_name} not found")
256
- return
257
- if command.kimi_soul_only and not isinstance(self.soul, KimiSoul):
258
- console.print(f"Meta command /{command_name} not supported")
259
- return
260
- logger.debug(
261
- "Running meta command: {command_name} with args: {command_args}",
262
- command_name=command_name,
263
- command_args=command_args,
264
- )
265
- try:
266
- ret = command.func(self, command_args)
267
- if isinstance(ret, Awaitable):
268
- await ret
269
- except LLMNotSet:
270
- logger.error("LLM not set")
271
- console.print("[red]LLM not set, send /setup to configure[/red]")
272
- except ChatProviderError as e:
273
- logger.exception("LLM provider error:")
274
- console.print(f"[red]LLM provider error: {e}[/red]")
275
- except Reload:
276
- # just propagate
277
- raise
278
- except BaseException as e:
279
- logger.exception("Unknown error:")
280
- console.print(f"[red]Unknown error: {e}[/red]")
281
- raise # re-raise unknown error
282
-
283
229
 
284
230
  _KIMI_BLUE = "dodger_blue1"
285
231
  _LOGO = f"""\
@@ -1,3 +1,29 @@
1
1
  from rich.console import Console
2
+ from rich.theme import Theme
2
3
 
3
- console = Console(highlight=False)
4
+ _NEUTRAL_MARKDOWN_THEME = Theme(
5
+ {
6
+ "markdown.paragraph": "none",
7
+ "markdown.block_quote": "none",
8
+ "markdown.hr": "none",
9
+ "markdown.item": "none",
10
+ "markdown.item.bullet": "none",
11
+ "markdown.item.number": "none",
12
+ "markdown.link": "none",
13
+ "markdown.link_url": "none",
14
+ "markdown.h1": "none",
15
+ "markdown.h1.border": "none",
16
+ "markdown.h2": "none",
17
+ "markdown.h3": "none",
18
+ "markdown.h4": "none",
19
+ "markdown.h5": "none",
20
+ "markdown.h6": "none",
21
+ "markdown.em": "none",
22
+ "markdown.strong": "none",
23
+ "markdown.s": "none",
24
+ "status.spinner": "none",
25
+ },
26
+ inherit=True,
27
+ )
28
+
29
+ console = Console(highlight=False, theme=_NEUTRAL_MARKDOWN_THEME)
@@ -0,0 +1,187 @@
1
+ import json
2
+ from typing import TYPE_CHECKING
3
+
4
+ from kosong.base.message import (
5
+ AudioURLPart,
6
+ ContentPart,
7
+ ImageURLPart,
8
+ Message,
9
+ TextPart,
10
+ ThinkPart,
11
+ ToolCall,
12
+ )
13
+ from rich.console import Group
14
+ from rich.panel import Panel
15
+ from rich.rule import Rule
16
+ from rich.syntax import Syntax
17
+ from rich.text import Text
18
+
19
+ from kimi_cli.soul.kimisoul import KimiSoul
20
+ from kimi_cli.ui.shell.console import console
21
+ from kimi_cli.ui.shell.metacmd import meta_command
22
+
23
+ if TYPE_CHECKING:
24
+ from kimi_cli.ui.shell import ShellApp
25
+
26
+
27
+ def _format_content_part(part: ContentPart) -> Text | Panel | Group:
28
+ """Format a single content part."""
29
+ match part:
30
+ case TextPart(text=text):
31
+ # Check if it looks like a system tag
32
+ if text.strip().startswith("<system>") and text.strip().endswith("</system>"):
33
+ return Panel(
34
+ text.strip()[8:-9].strip(),
35
+ title="[dim]system[/dim]",
36
+ border_style="dim yellow",
37
+ padding=(0, 1),
38
+ )
39
+ return Text(text, style="white")
40
+
41
+ case ThinkPart(think=think):
42
+ return Panel(
43
+ think,
44
+ title="[dim]thinking[/dim]",
45
+ border_style="dim cyan",
46
+ padding=(0, 1),
47
+ )
48
+
49
+ case ImageURLPart(image_url=img):
50
+ url_display = img.url[:80] + "..." if len(img.url) > 80 else img.url
51
+ id_text = f" (id: {img.id})" if img.id else ""
52
+ return Text(f"[Image{id_text}] {url_display}", style="blue")
53
+
54
+ case AudioURLPart(audio_url=audio):
55
+ url_display = audio.url[:80] + "..." if len(audio.url) > 80 else audio.url
56
+ id_text = f" (id: {audio.id})" if audio.id else ""
57
+ return Text(f"[Audio{id_text}] {url_display}", style="blue")
58
+
59
+ case _:
60
+ return Text(f"[Unknown content type: {type(part).__name__}]", style="red")
61
+
62
+
63
+ def _format_tool_call(tool_call: ToolCall) -> Panel:
64
+ """Format a tool call."""
65
+ args = tool_call.function.arguments or "{}"
66
+ try:
67
+ args_formatted = json.dumps(json.loads(args), indent=2)
68
+ args_syntax = Syntax(args_formatted, "json", theme="monokai", padding=(0, 1))
69
+ except json.JSONDecodeError:
70
+ args_syntax = Text(args, style="red")
71
+
72
+ content = Group(
73
+ Text(f"Function: {tool_call.function.name}", style="bold cyan"),
74
+ Text(f"Call ID: {tool_call.id}", style="dim"),
75
+ Text("Arguments:", style="bold"),
76
+ args_syntax,
77
+ )
78
+
79
+ return Panel(
80
+ content,
81
+ title="[bold yellow]Tool Call[/bold yellow]",
82
+ border_style="yellow",
83
+ padding=(0, 1),
84
+ )
85
+
86
+
87
+ def _format_message(msg: Message, index: int) -> Panel:
88
+ """Format a single message."""
89
+ # Role styling
90
+ role_colors = {
91
+ "system": "magenta",
92
+ "developer": "magenta",
93
+ "user": "green",
94
+ "assistant": "blue",
95
+ "tool": "yellow",
96
+ }
97
+ role_color = role_colors.get(msg.role, "white")
98
+ role_text = f"[bold {role_color}]{msg.role.upper()}[/bold {role_color}]"
99
+
100
+ # Add name if present
101
+ if msg.name:
102
+ role_text += f" [dim]({msg.name})[/dim]"
103
+
104
+ # Add tool call ID for tool messages
105
+ if msg.tool_call_id:
106
+ role_text += f" [dim]→ {msg.tool_call_id}[/dim]"
107
+
108
+ # Format content
109
+ content_items: list = []
110
+
111
+ if isinstance(msg.content, str):
112
+ content_items.append(Text(msg.content, style="white"))
113
+ else:
114
+ for part in msg.content:
115
+ formatted = _format_content_part(part)
116
+ content_items.append(formatted)
117
+
118
+ # Add tool calls if present
119
+ if msg.tool_calls:
120
+ if content_items:
121
+ content_items.append(Text()) # Empty line
122
+ for tool_call in msg.tool_calls:
123
+ content_items.append(_format_tool_call(tool_call))
124
+
125
+ # Combine all content
126
+ if not content_items:
127
+ content_items.append(Text("[empty message]", style="dim italic"))
128
+
129
+ group = Group(*content_items)
130
+
131
+ # Create panel
132
+ title = f"#{index + 1} {role_text}"
133
+ if msg.partial:
134
+ title += " [dim italic](partial)[/dim italic]"
135
+
136
+ return Panel(
137
+ group,
138
+ title=title,
139
+ border_style=role_color,
140
+ padding=(0, 1),
141
+ )
142
+
143
+
144
+ @meta_command(kimi_soul_only=True)
145
+ def debug(app: "ShellApp", args: list[str]):
146
+ """Debug the context"""
147
+ assert isinstance(app.soul, KimiSoul)
148
+
149
+ context = app.soul._context
150
+ history = context.history
151
+
152
+ if not history:
153
+ console.print(
154
+ Panel(
155
+ "Context is empty - no messages yet",
156
+ border_style="yellow",
157
+ padding=(1, 2),
158
+ )
159
+ )
160
+ return
161
+
162
+ # Build the debug output
163
+ output_items = [
164
+ Panel(
165
+ Group(
166
+ Text(f"Total messages: {len(history)}", style="bold"),
167
+ Text(f"Token count: {context.token_count:,}", style="bold"),
168
+ Text(f"Checkpoints: {context.n_checkpoints}", style="bold"),
169
+ Text(f"Trajectory: {context._file_backend}", style="dim"),
170
+ ),
171
+ title="[bold]Context Info[/bold]",
172
+ border_style="cyan",
173
+ padding=(0, 1),
174
+ ),
175
+ Rule(style="dim"),
176
+ ]
177
+
178
+ # Add all messages
179
+ for idx, msg in enumerate(history):
180
+ output_items.append(_format_message(msg, idx))
181
+
182
+ # Display using rich pager
183
+ display_group = Group(*output_items)
184
+
185
+ # Use pager to display
186
+ with console.pager(styles=True):
187
+ console.print(display_group)
@@ -0,0 +1,115 @@
1
+ import asyncio
2
+ import sys
3
+ import termios
4
+ import threading
5
+ import time
6
+ from collections.abc import AsyncGenerator, Callable
7
+ from enum import Enum, auto
8
+
9
+
10
+ class KeyEvent(Enum):
11
+ UP = auto()
12
+ DOWN = auto()
13
+ LEFT = auto()
14
+ RIGHT = auto()
15
+ ENTER = auto()
16
+ ESCAPE = auto()
17
+ TAB = auto()
18
+
19
+
20
+ async def listen_for_keyboard() -> AsyncGenerator[KeyEvent]:
21
+ loop = asyncio.get_running_loop()
22
+ queue = asyncio.Queue[KeyEvent]()
23
+ cancel_event = threading.Event()
24
+
25
+ def emit(event: KeyEvent) -> None:
26
+ # print(f"emit: {event}")
27
+ loop.call_soon_threadsafe(queue.put_nowait, event)
28
+
29
+ listener = threading.Thread(
30
+ target=_listen_for_keyboard_thread,
31
+ args=(cancel_event, emit),
32
+ name="kimi-cli-keyboard-listener",
33
+ daemon=True,
34
+ )
35
+ listener.start()
36
+
37
+ try:
38
+ while True:
39
+ yield await queue.get()
40
+ finally:
41
+ cancel_event.set()
42
+ if listener.is_alive():
43
+ await asyncio.to_thread(listener.join)
44
+
45
+
46
+ def _listen_for_keyboard_thread(
47
+ cancel: threading.Event,
48
+ emit: Callable[[KeyEvent], None],
49
+ ) -> None:
50
+ # make stdin raw and non-blocking
51
+ fd = sys.stdin.fileno()
52
+ oldterm = termios.tcgetattr(fd)
53
+ newattr = termios.tcgetattr(fd)
54
+ newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
55
+ newattr[6][termios.VMIN] = 0
56
+ newattr[6][termios.VTIME] = 0
57
+ termios.tcsetattr(fd, termios.TCSANOW, newattr)
58
+
59
+ try:
60
+ while not cancel.is_set():
61
+ try:
62
+ c = sys.stdin.read(1)
63
+ except (OSError, ValueError):
64
+ c = ""
65
+
66
+ if not c:
67
+ if cancel.is_set():
68
+ break
69
+ time.sleep(0.01)
70
+ continue
71
+
72
+ if c == "\x1b":
73
+ sequence = c
74
+ for _ in range(2):
75
+ if cancel.is_set():
76
+ break
77
+ try:
78
+ fragment = sys.stdin.read(1)
79
+ except (OSError, ValueError):
80
+ fragment = ""
81
+ if not fragment:
82
+ break
83
+ sequence += fragment
84
+ if sequence in _ARROW_KEY_MAP:
85
+ break
86
+
87
+ event = _ARROW_KEY_MAP.get(sequence)
88
+ if event is not None:
89
+ emit(event)
90
+ elif sequence == "\x1b":
91
+ emit(KeyEvent.ESCAPE)
92
+ elif c in ("\r", "\n"):
93
+ emit(KeyEvent.ENTER)
94
+ elif c == "\t":
95
+ emit(KeyEvent.TAB)
96
+ finally:
97
+ # restore the terminal settings
98
+ termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
99
+
100
+
101
+ _ARROW_KEY_MAP: dict[str, KeyEvent] = {
102
+ "\x1b[A": KeyEvent.UP,
103
+ "\x1b[B": KeyEvent.DOWN,
104
+ "\x1b[C": KeyEvent.RIGHT,
105
+ "\x1b[D": KeyEvent.LEFT,
106
+ }
107
+
108
+
109
+ if __name__ == "__main__":
110
+
111
+ async def dev_main():
112
+ async for event in listen_for_keyboard():
113
+ print(event)
114
+
115
+ asyncio.run(dev_main())