kimi-cli 0.42__tar.gz → 0.44__tar.gz

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 (90) hide show
  1. {kimi_cli-0.42 → kimi_cli-0.44}/PKG-INFO +7 -6
  2. {kimi_cli-0.42 → kimi_cli-0.44}/README.md +2 -2
  3. {kimi_cli-0.42 → kimi_cli-0.44}/pyproject.toml +5 -4
  4. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/CHANGELOG.md +23 -0
  5. kimi_cli-0.42/src/kimi_cli/__init__.py → kimi_cli-0.44/src/kimi_cli/app.py +58 -18
  6. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/cli.py +6 -5
  7. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/config.py +7 -2
  8. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/llm.py +37 -7
  9. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/__init__.py +22 -4
  10. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/agent.py +4 -2
  11. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/kimisoul.py +21 -4
  12. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/message.py +1 -1
  13. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/runtime.py +7 -5
  14. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/task/__init__.py +2 -1
  15. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/web/search.py +1 -1
  16. kimi_cli-0.44/src/kimi_cli/ui/__init__.py +0 -0
  17. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/acp/__init__.py +24 -28
  18. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/print/__init__.py +25 -33
  19. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/__init__.py +55 -36
  20. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/keyboard.py +82 -14
  21. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/prompt.py +199 -3
  22. kimi_cli-0.44/src/kimi_cli/ui/shell/replay.py +104 -0
  23. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/setup.py +1 -1
  24. kimi_cli-0.44/src/kimi_cli/ui/shell/visualize.py +111 -0
  25. kimi_cli-0.44/src/kimi_cli/utils/message.py +22 -0
  26. kimi_cli-0.44/src/kimi_cli/utils/signals.py +41 -0
  27. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/utils/string.py +8 -0
  28. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/wire/__init__.py +1 -0
  29. kimi_cli-0.42/src/kimi_cli/ui/shell/visualize.py +0 -114
  30. kimi_cli-0.42/src/kimi_cli/utils/message.py +0 -8
  31. {kimi_cli-0.42/src/kimi_cli/ui → kimi_cli-0.44/src/kimi_cli}/__init__.py +0 -0
  32. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/agents/default/agent.yaml +0 -0
  33. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/agents/default/sub.yaml +0 -0
  34. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/agents/default/system.md +0 -0
  35. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/agentspec.py +0 -0
  36. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/constant.py +0 -0
  37. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/exception.py +0 -0
  38. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/metadata.py +0 -0
  39. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/prompts/__init__.py +0 -0
  40. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/prompts/compact.md +0 -0
  41. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/prompts/init.md +0 -0
  42. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/py.typed +0 -0
  43. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/session.py +0 -0
  44. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/share.py +0 -0
  45. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/approval.py +0 -0
  46. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/compaction.py +0 -0
  47. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/context.py +0 -0
  48. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/denwarenji.py +0 -0
  49. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/soul/toolset.py +0 -0
  50. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/__init__.py +0 -0
  51. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/bash/__init__.py +0 -0
  52. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/bash/bash.md +0 -0
  53. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/dmail/__init__.py +0 -0
  54. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/dmail/dmail.md +0 -0
  55. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/__init__.py +0 -0
  56. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/glob.md +0 -0
  57. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/glob.py +0 -0
  58. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/grep.md +0 -0
  59. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/grep.py +0 -0
  60. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/patch.md +0 -0
  61. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/patch.py +0 -0
  62. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/read.md +0 -0
  63. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/read.py +0 -0
  64. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/replace.md +0 -0
  65. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/replace.py +0 -0
  66. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/write.md +0 -0
  67. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/file/write.py +0 -0
  68. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/mcp.py +0 -0
  69. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/task/task.md +0 -0
  70. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/test.py +0 -0
  71. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/think/__init__.py +0 -0
  72. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/think/think.md +0 -0
  73. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/todo/__init__.py +0 -0
  74. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/todo/set_todo_list.md +0 -0
  75. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/utils.py +0 -0
  76. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/web/__init__.py +0 -0
  77. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/web/fetch.md +0 -0
  78. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/web/fetch.py +0 -0
  79. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/tools/web/search.md +0 -0
  80. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/console.py +0 -0
  81. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/debug.py +0 -0
  82. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/liveview.py +0 -0
  83. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/metacmd.py +0 -0
  84. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/ui/shell/update.py +0 -0
  85. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/utils/aiohttp.py +0 -0
  86. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/utils/changelog.py +0 -0
  87. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/utils/logging.py +0 -0
  88. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/utils/path.py +0 -0
  89. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/utils/pyinstaller.py +0 -0
  90. {kimi_cli-0.42 → kimi_cli-0.44}/src/kimi_cli/wire/message.py +0 -0
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kimi-cli
3
- Version: 0.42
3
+ Version: 0.44
4
4
  Summary: Kimi CLI is your next CLI agent.
5
- Requires-Dist: agent-client-protocol==0.4.9
5
+ Requires-Dist: agent-client-protocol==0.6.2
6
6
  Requires-Dist: aiofiles==25.1.0
7
- Requires-Dist: aiohttp==3.13.1
7
+ Requires-Dist: aiohttp==3.13.2
8
8
  Requires-Dist: click==8.3.0
9
- Requires-Dist: kosong==0.15.0
9
+ Requires-Dist: kosong==0.16.1
10
10
  Requires-Dist: loguru==0.7.3
11
11
  Requires-Dist: patch-ng==1.19.0
12
12
  Requires-Dist: prompt-toolkit==3.0.52
13
+ Requires-Dist: pillow==12.0.0
13
14
  Requires-Dist: pyyaml==6.0.3
14
15
  Requires-Dist: rich==14.2.0
15
16
  Requires-Dist: ripgrepy==2.2.0
@@ -84,7 +85,7 @@ After setup, Kimi CLI will be ready to use. You can send `/help` to get more inf
84
85
 
85
86
  ### Shell mode
86
87
 
87
- Kimi CLI is not only a coding agent, but also a shell. You can switch the mode by pressing `Ctrl-K`. In shell mode, you can directly run shell commands without leaving Kimi CLI.
88
+ Kimi CLI is not only a coding agent, but also a shell. You can switch the mode by pressing `Ctrl-X`. In shell mode, you can directly run shell commands without leaving Kimi CLI.
88
89
 
89
90
  > [!NOTE]
90
91
  > Built-in shell commands like `cd` are not supported yet.
@@ -109,7 +110,7 @@ Then add `kimi-cli` to your Zsh plugin list in `~/.zshrc`:
109
110
  plugins=(... kimi-cli)
110
111
  ```
111
112
 
112
- After restarting Zsh, you can switch to agent mode by pressing `Ctrl-K`.
113
+ After restarting Zsh, you can switch to agent mode by pressing `Ctrl-X`.
113
114
 
114
115
  ### ACP support
115
116
 
@@ -60,7 +60,7 @@ After setup, Kimi CLI will be ready to use. You can send `/help` to get more inf
60
60
 
61
61
  ### Shell mode
62
62
 
63
- Kimi CLI is not only a coding agent, but also a shell. You can switch the mode by pressing `Ctrl-K`. In shell mode, you can directly run shell commands without leaving Kimi CLI.
63
+ Kimi CLI is not only a coding agent, but also a shell. You can switch the mode by pressing `Ctrl-X`. In shell mode, you can directly run shell commands without leaving Kimi CLI.
64
64
 
65
65
  > [!NOTE]
66
66
  > Built-in shell commands like `cd` are not supported yet.
@@ -85,7 +85,7 @@ Then add `kimi-cli` to your Zsh plugin list in `~/.zshrc`:
85
85
  plugins=(... kimi-cli)
86
86
  ```
87
87
 
88
- After restarting Zsh, you can switch to agent mode by pressing `Ctrl-K`.
88
+ After restarting Zsh, you can switch to agent mode by pressing `Ctrl-X`.
89
89
 
90
90
  ### ACP support
91
91
 
@@ -1,18 +1,19 @@
1
1
  [project]
2
2
  name = "kimi-cli"
3
- version = "0.42"
3
+ version = "0.44"
4
4
  description = "Kimi CLI is your next CLI agent."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
7
7
  dependencies = [
8
- "agent-client-protocol==0.4.9",
8
+ "agent-client-protocol==0.6.2",
9
9
  "aiofiles==25.1.0",
10
- "aiohttp==3.13.1",
10
+ "aiohttp==3.13.2",
11
11
  "click==8.3.0",
12
- "kosong==0.15.0",
12
+ "kosong==0.16.1",
13
13
  "loguru==0.7.3",
14
14
  "patch-ng==1.19.0",
15
15
  "prompt-toolkit==3.0.52",
16
+ "pillow==12.0.0",
16
17
  "pyyaml==6.0.3",
17
18
  "rich==14.2.0",
18
19
  "ripgrepy==2.2.0",
@@ -9,6 +9,29 @@ Internal builds may append content to the Unreleased section.
9
9
  Only write entries that are worth mentioning to users.
10
10
  -->
11
11
 
12
+ ## [0.44] - 2025-10-30
13
+
14
+ ### Changed
15
+
16
+ - Improve startup time
17
+
18
+ ### Fixed
19
+
20
+ - Fix potential invalid bytes in user input
21
+
22
+ ## [0.43] - 2025-10-30
23
+
24
+ ### Added
25
+
26
+ - Basic Windows support (experimental)
27
+ - Display warnings when base URL or API key is overridden in environment variables
28
+ - Support image input if the LLM model supports it
29
+ - Replay recent context history when continuing a session
30
+
31
+ ### Fixed
32
+
33
+ - Ensure new line after executing shell commands
34
+
12
35
  ## [0.42] - 2025-10-28
13
36
 
14
37
  ### Added
@@ -8,6 +8,7 @@ from typing import Any
8
8
  from pydantic import SecretStr
9
9
 
10
10
  from kimi_cli.agentspec import DEFAULT_AGENT_FILE
11
+ from kimi_cli.cli import InputFormat, OutputFormat
11
12
  from kimi_cli.config import LLMModel, LLMProvider, load_config
12
13
  from kimi_cli.llm import augment_provider_with_env_vars, create_llm
13
14
  from kimi_cli.session import Session
@@ -15,9 +16,6 @@ from kimi_cli.soul.agent import load_agent
15
16
  from kimi_cli.soul.context import Context
16
17
  from kimi_cli.soul.kimisoul import KimiSoul
17
18
  from kimi_cli.soul.runtime import Runtime
18
- from kimi_cli.ui.acp import ACPServer
19
- from kimi_cli.ui.print import InputFormat, OutputFormat, PrintApp
20
- from kimi_cli.ui.shell import ShellApp
21
19
  from kimi_cli.utils.logging import StreamToLogger, logger
22
20
 
23
21
 
@@ -72,7 +70,7 @@ class KimiCLI:
72
70
  # try overwrite with environment variables
73
71
  assert provider is not None
74
72
  assert model is not None
75
- augment_provider_with_env_vars(provider, model)
73
+ env_overrides = augment_provider_with_env_vars(provider, model)
76
74
 
77
75
  if not provider.base_url or not model.model:
78
76
  llm = None
@@ -95,11 +93,17 @@ class KimiCLI:
95
93
  runtime,
96
94
  context=context,
97
95
  )
98
- return KimiCLI(soul, session)
96
+ return KimiCLI(soul, runtime, env_overrides)
99
97
 
100
- def __init__(self, soul: KimiSoul, session: Session) -> None:
101
- self._soul = soul
102
- self._session = session
98
+ def __init__(
99
+ self,
100
+ _soul: KimiSoul,
101
+ _runtime: Runtime,
102
+ _env_overrides: dict[str, str],
103
+ ) -> None:
104
+ self._soul = _soul
105
+ self._runtime = _runtime
106
+ self._env_overrides = _env_overrides
103
107
 
104
108
  @property
105
109
  def soul(self) -> KimiSoul:
@@ -109,12 +113,12 @@ class KimiCLI:
109
113
  @property
110
114
  def session(self) -> Session:
111
115
  """Get the Session instance."""
112
- return self._session
116
+ return self._runtime.session
113
117
 
114
118
  @contextlib.contextmanager
115
119
  def _app_env(self) -> Generator[None]:
116
120
  original_cwd = Path.cwd()
117
- os.chdir(self._session.work_dir)
121
+ os.chdir(self._runtime.session.work_dir)
118
122
  try:
119
123
  # to ignore possible warnings from dateparser
120
124
  warnings.filterwarnings("ignore", category=DeprecationWarning)
@@ -124,14 +128,46 @@ class KimiCLI:
124
128
  os.chdir(original_cwd)
125
129
 
126
130
  async def run_shell_mode(self, command: str | None = None) -> bool:
127
- with self._app_env():
128
- app = ShellApp(
129
- self._soul,
130
- welcome_info={
131
- "Directory": str(self._session.work_dir),
132
- "Session": self._session.id,
133
- },
131
+ from kimi_cli.ui.shell import ShellApp, WelcomeInfoItem
132
+
133
+ welcome_info = [
134
+ WelcomeInfoItem(name="Directory", value=str(self._runtime.session.work_dir)),
135
+ WelcomeInfoItem(name="Session", value=self._runtime.session.id),
136
+ ]
137
+ if base_url := self._env_overrides.get("KIMI_BASE_URL"):
138
+ welcome_info.append(
139
+ WelcomeInfoItem(
140
+ name="API URL",
141
+ value=f"{base_url} (from KIMI_BASE_URL)",
142
+ level=WelcomeInfoItem.Level.WARN,
143
+ )
144
+ )
145
+ if not self._runtime.llm:
146
+ welcome_info.append(
147
+ WelcomeInfoItem(
148
+ name="Model",
149
+ value="not set, send /setup to configure",
150
+ level=WelcomeInfoItem.Level.WARN,
151
+ )
152
+ )
153
+ elif "KIMI_MODEL_NAME" in self._env_overrides:
154
+ welcome_info.append(
155
+ WelcomeInfoItem(
156
+ name="Model",
157
+ value=f"{self._soul.model} (from KIMI_MODEL_NAME)",
158
+ level=WelcomeInfoItem.Level.WARN,
159
+ )
134
160
  )
161
+ else:
162
+ welcome_info.append(
163
+ WelcomeInfoItem(
164
+ name="Model",
165
+ value=self._soul.model,
166
+ level=WelcomeInfoItem.Level.INFO,
167
+ )
168
+ )
169
+ with self._app_env():
170
+ app = ShellApp(self._soul, welcome_info=welcome_info)
135
171
  return await app.run(command)
136
172
 
137
173
  async def run_print_mode(
@@ -140,16 +176,20 @@ class KimiCLI:
140
176
  output_format: OutputFormat,
141
177
  command: str | None = None,
142
178
  ) -> bool:
179
+ from kimi_cli.ui.print import PrintApp
180
+
143
181
  with self._app_env():
144
182
  app = PrintApp(
145
183
  self._soul,
146
184
  input_format,
147
185
  output_format,
148
- self._session.history_file,
186
+ self._runtime.session.history_file,
149
187
  )
150
188
  return await app.run(command)
151
189
 
152
190
  async def run_acp_server(self) -> bool:
191
+ from kimi_cli.ui.acp import ACPServer
192
+
153
193
  with self._app_env():
154
194
  app = ACPServer(self._soul)
155
195
  return await app.run()
@@ -7,12 +7,7 @@ from typing import Any, Literal, get_args
7
7
 
8
8
  import click
9
9
 
10
- from kimi_cli import KimiCLI
11
10
  from kimi_cli.constant import VERSION
12
- from kimi_cli.session import Session
13
- from kimi_cli.share import get_share_dir
14
- from kimi_cli.ui.print import InputFormat, OutputFormat
15
- from kimi_cli.utils.logging import logger
16
11
 
17
12
 
18
13
  class Reload(Exception):
@@ -22,6 +17,8 @@ class Reload(Exception):
22
17
 
23
18
 
24
19
  UIMode = Literal["shell", "print", "acp"]
20
+ InputFormat = Literal["text", "stream-json"]
21
+ OutputFormat = Literal["text", "stream-json"]
25
22
 
26
23
 
27
24
  @click.command(context_settings=dict(help_option_names=["-h", "--help"]))
@@ -156,6 +153,10 @@ def kimi(
156
153
  yolo: bool,
157
154
  ):
158
155
  """Kimi, your next CLI agent."""
156
+ from kimi_cli.app import KimiCLI
157
+ from kimi_cli.session import Session
158
+ from kimi_cli.share import get_share_dir
159
+ from kimi_cli.utils.logging import logger
159
160
 
160
161
  def _noop_echo(*args: Any, **kwargs: Any):
161
162
  pass
@@ -18,7 +18,7 @@ class LLMProvider(BaseModel):
18
18
  """API base URL"""
19
19
  api_key: SecretStr
20
20
  """API key"""
21
- custom_headers: dict[str, str] = Field(default_factory=dict)
21
+ custom_headers: dict[str, str] | None = None
22
22
  """Custom headers to include in API requests"""
23
23
 
24
24
  @field_serializer("api_key", when_used="json")
@@ -26,6 +26,9 @@ class LLMProvider(BaseModel):
26
26
  return v.get_secret_value()
27
27
 
28
28
 
29
+ LLMModelCapability = Literal["image_in"]
30
+
31
+
29
32
  class LLMModel(BaseModel):
30
33
  """LLM model configuration."""
31
34
 
@@ -35,6 +38,8 @@ class LLMModel(BaseModel):
35
38
  """Model name"""
36
39
  max_context_size: int
37
40
  """Maximum context size (unit: tokens)"""
41
+ capabilities: set[LLMModelCapability] | None = None
42
+ """Model capabilities"""
38
43
 
39
44
 
40
45
  class LoopControl(BaseModel):
@@ -53,7 +58,7 @@ class MoonshotSearchConfig(BaseModel):
53
58
  """Base URL for Moonshot Search service."""
54
59
  api_key: SecretStr
55
60
  """API key for Moonshot Search service."""
56
- custom_headers: dict[str, str] = Field(default_factory=dict)
61
+ custom_headers: dict[str, str] | None = None
57
62
  """Custom headers to include in API requests."""
58
63
 
59
64
  @field_serializer("api_key", when_used="json")
@@ -2,31 +2,49 @@ import os
2
2
  from typing import NamedTuple
3
3
 
4
4
  from kosong.base.chat_provider import ChatProvider
5
- from kosong.chat_provider.chaos import ChaosChatProvider, ChaosConfig
6
- from kosong.chat_provider.kimi import Kimi
7
- from kosong.chat_provider.openai_legacy import OpenAILegacy
8
5
  from pydantic import SecretStr
9
6
 
10
- from kimi_cli.config import LLMModel, LLMProvider
7
+ from kimi_cli.config import LLMModel, LLMModelCapability, LLMProvider
11
8
  from kimi_cli.constant import USER_AGENT
12
9
 
13
10
 
14
11
  class LLM(NamedTuple):
15
12
  chat_provider: ChatProvider
16
13
  max_context_size: int
14
+ capabilities: set[LLMModelCapability]
15
+ # TODO: these additional fields should be moved to ChatProvider
17
16
 
17
+ @property
18
+ def model_name(self) -> str:
19
+ return self.chat_provider.model_name
20
+
21
+ @property
22
+ def supports_image_in(self) -> bool:
23
+ return "image_in" in self.capabilities
24
+
25
+
26
+ def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel) -> dict[str, str]:
27
+ """Override provider/model settings from environment variables.
28
+
29
+ Returns:
30
+ Mapping of environment variables that were applied.
31
+ """
32
+ applied: dict[str, str] = {}
18
33
 
19
- def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel):
20
34
  match provider.type:
21
35
  case "kimi":
22
36
  if base_url := os.getenv("KIMI_BASE_URL"):
23
37
  provider.base_url = base_url
38
+ applied["KIMI_BASE_URL"] = base_url
24
39
  if api_key := os.getenv("KIMI_API_KEY"):
25
40
  provider.api_key = SecretStr(api_key)
41
+ applied["KIMI_API_KEY"] = "******"
26
42
  if model_name := os.getenv("KIMI_MODEL_NAME"):
27
43
  model.model = model_name
44
+ applied["KIMI_MODEL_NAME"] = model.model
28
45
  if max_context_size := os.getenv("KIMI_MODEL_MAX_CONTEXT_SIZE"):
29
46
  model.max_context_size = int(max_context_size)
47
+ applied["KIMI_MODEL_MAX_CONTEXT_SIZE"] = str(model.max_context_size)
30
48
  case "openai_legacy":
31
49
  if base_url := os.getenv("OPENAI_BASE_URL"):
32
50
  provider.base_url = base_url
@@ -35,6 +53,8 @@ def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel):
35
53
  case _:
36
54
  pass
37
55
 
56
+ return applied
57
+
38
58
 
39
59
  def create_llm(
40
60
  provider: LLMProvider,
@@ -45,6 +65,8 @@ def create_llm(
45
65
  ) -> LLM:
46
66
  match provider.type:
47
67
  case "kimi":
68
+ from kosong.chat_provider.kimi import Kimi
69
+
48
70
  chat_provider = Kimi(
49
71
  model=model.model,
50
72
  base_url=provider.base_url,
@@ -52,12 +74,14 @@ def create_llm(
52
74
  stream=stream,
53
75
  default_headers={
54
76
  "User-Agent": USER_AGENT,
55
- **provider.custom_headers,
77
+ **(provider.custom_headers or {}),
56
78
  },
57
79
  )
58
80
  if session_id:
59
81
  chat_provider = chat_provider.with_generation_kwargs(prompt_cache_key=session_id)
60
82
  case "openai_legacy":
83
+ from kosong.chat_provider.openai_legacy import OpenAILegacy
84
+
61
85
  chat_provider = OpenAILegacy(
62
86
  model=model.model,
63
87
  base_url=provider.base_url,
@@ -65,6 +89,8 @@ def create_llm(
65
89
  stream=stream,
66
90
  )
67
91
  case "_chaos":
92
+ from kosong.chat_provider.chaos import ChaosChatProvider, ChaosConfig
93
+
68
94
  chat_provider = ChaosChatProvider(
69
95
  model=model.model,
70
96
  base_url=provider.base_url,
@@ -75,4 +101,8 @@ def create_llm(
75
101
  ),
76
102
  )
77
103
 
78
- return LLM(chat_provider=chat_provider, max_context_size=model.max_context_size)
104
+ return LLM(
105
+ chat_provider=chat_provider,
106
+ max_context_size=model.max_context_size,
107
+ capabilities=model.capabilities or set(),
108
+ )
@@ -4,6 +4,9 @@ from collections.abc import Callable, Coroutine
4
4
  from contextvars import ContextVar
5
5
  from typing import Any, NamedTuple, Protocol, runtime_checkable
6
6
 
7
+ from kosong.base.message import ContentPart
8
+
9
+ from kimi_cli.llm import LLM
7
10
  from kimi_cli.utils.logging import logger
8
11
  from kimi_cli.wire import Wire, WireUISide
9
12
  from kimi_cli.wire.message import WireMessage
@@ -15,6 +18,19 @@ class LLMNotSet(Exception):
15
18
  pass
16
19
 
17
20
 
21
+ class LLMNotSupported(Exception):
22
+ """Raised when the LLM does not have required capabilities."""
23
+
24
+ def __init__(self, llm: LLM, capabilities: list[str]):
25
+ self.llm = llm
26
+ self.capabilities = capabilities
27
+ capabilities_str = "capability" if len(capabilities) == 1 else "capabilities"
28
+ super().__init__(
29
+ f"The LLM model '{llm.model_name}' does not support required {capabilities_str}: "
30
+ f"{', '.join(capabilities)}."
31
+ )
32
+
33
+
18
34
  class MaxStepsReached(Exception):
19
35
  """Raised when the maximum number of steps is reached."""
20
36
 
@@ -47,15 +63,16 @@ class Soul(Protocol):
47
63
  """The current status of the soul. The returned value is immutable."""
48
64
  ...
49
65
 
50
- async def run(self, user_input: str):
66
+ async def run(self, user_input: str | list[ContentPart]):
51
67
  """
52
68
  Run the agent with the given user input until the max steps or no more tool calls.
53
69
 
54
70
  Args:
55
- user_input (str): The user input to the agent.
71
+ user_input (str | list[ContentPart]): The user input to the agent.
56
72
 
57
73
  Raises:
58
74
  LLMNotSet: When the LLM is not set.
75
+ LLMNotSupported: When the LLM does not have required capabilities.
59
76
  ChatProviderError: When the LLM provider returns an error.
60
77
  MaxStepsReached: When the maximum number of steps is reached.
61
78
  asyncio.CancelledError: When the run is cancelled by user.
@@ -73,7 +90,7 @@ class RunCancelled(Exception):
73
90
 
74
91
  async def run_soul(
75
92
  soul: "Soul",
76
- user_input: str,
93
+ user_input: str | list[ContentPart],
77
94
  ui_loop_fn: UILoopFn,
78
95
  cancel_event: asyncio.Event,
79
96
  ) -> None:
@@ -85,6 +102,7 @@ async def run_soul(
85
102
 
86
103
  Raises:
87
104
  LLMNotSet: When the LLM is not set.
105
+ LLMNotSupported: When the LLM does not have required capabilities.
88
106
  ChatProviderError: When the LLM provider returns an error.
89
107
  MaxStepsReached: When the maximum number of steps is reached.
90
108
  RunCancelled: When the run is cancelled by the cancel event.
@@ -125,7 +143,7 @@ async def run_soul(
125
143
  try:
126
144
  await asyncio.wait_for(ui_task, timeout=0.5)
127
145
  except asyncio.QueueShutDown:
128
- # expected
146
+ logger.debug("UI loop shut down")
129
147
  pass
130
148
  except TimeoutError:
131
149
  logger.warning("UI loop timed out")
@@ -4,7 +4,6 @@ import string
4
4
  from pathlib import Path
5
5
  from typing import Any, NamedTuple
6
6
 
7
- import fastmcp
8
7
  from kosong.tooling import CallableTool, CallableTool2, Toolset
9
8
 
10
9
  from kimi_cli.agentspec import ResolvedAgentSpec, load_agent_spec
@@ -14,7 +13,6 @@ from kimi_cli.soul.approval import Approval
14
13
  from kimi_cli.soul.denwarenji import DenwaRenji
15
14
  from kimi_cli.soul.runtime import BuiltinSystemPromptArgs, Runtime
16
15
  from kimi_cli.soul.toolset import CustomToolset
17
- from kimi_cli.tools.mcp import MCPTool
18
16
  from kimi_cli.utils.logging import logger
19
17
 
20
18
 
@@ -143,6 +141,10 @@ async def _load_mcp_tools(
143
141
  ValueError: If the MCP config is not valid.
144
142
  RuntimeError: If the MCP server cannot be connected.
145
143
  """
144
+ import fastmcp
145
+
146
+ from kimi_cli.tools.mcp import MCPTool
147
+
146
148
  for mcp_config in mcp_configs:
147
149
  logger.info("Loading MCP tools from: {mcp_config}", mcp_config=mcp_config)
148
150
  client = fastmcp.Client(mcp_config)
@@ -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,7 +16,14 @@ 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.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot, wire_send
19
+ from kimi_cli.soul import (
20
+ LLMNotSet,
21
+ LLMNotSupported,
22
+ MaxStepsReached,
23
+ Soul,
24
+ StatusSnapshot,
25
+ wire_send,
26
+ )
20
27
  from kimi_cli.soul.agent import Agent
21
28
  from kimi_cli.soul.compaction import SimpleCompaction
22
29
  from kimi_cli.soul.context import Context
@@ -53,7 +60,6 @@ class KimiSoul(Soul):
53
60
  agent (Agent): The agent to run.
54
61
  runtime (Runtime): Runtime parameters and states.
55
62
  context (Context): The context of the agent.
56
- loop_control (LoopControl): The control parameters for the agent loop.
57
63
  """
58
64
  self._agent = agent
59
65
  self._runtime = runtime
@@ -85,6 +91,10 @@ class KimiSoul(Soul):
85
91
  def status(self) -> StatusSnapshot:
86
92
  return StatusSnapshot(context_usage=self._context_usage)
87
93
 
94
+ @property
95
+ def context(self) -> Context:
96
+ return self._context
97
+
88
98
  @property
89
99
  def _context_usage(self) -> float:
90
100
  if self._runtime.llm is not None:
@@ -94,10 +104,17 @@ class KimiSoul(Soul):
94
104
  async def _checkpoint(self):
95
105
  await self._context.checkpoint(self._checkpoint_with_user_message)
96
106
 
97
- async def run(self, user_input: str):
107
+ async def run(self, user_input: str | list[ContentPart]):
98
108
  if self._runtime.llm is None:
99
109
  raise LLMNotSet()
100
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
+
101
118
  await self._checkpoint() # this creates the checkpoint 0 on first run
102
119
  await self._context.append_message(Message(role="user", content=user_input))
103
120
  logger.debug("Appended user message to context")
@@ -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 [
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import subprocess
2
3
  import sys
3
4
  from datetime import datetime
@@ -59,7 +60,7 @@ def _list_work_dir(work_dir: Path) -> str:
59
60
 
60
61
 
61
62
  class Runtime(NamedTuple):
62
- """Agent globals."""
63
+ """Agent runtime."""
63
64
 
64
65
  config: Config
65
66
  llm: LLM | None
@@ -75,9 +76,10 @@ class Runtime(NamedTuple):
75
76
  session: Session,
76
77
  yolo: bool,
77
78
  ) -> "Runtime":
78
- # FIXME: do these asynchronously
79
- ls_output = _list_work_dir(session.work_dir)
80
- agents_md = load_agents_md(session.work_dir) or ""
79
+ ls_output, agents_md = await asyncio.gather(
80
+ asyncio.to_thread(_list_work_dir, session.work_dir),
81
+ asyncio.to_thread(load_agents_md, session.work_dir),
82
+ )
81
83
 
82
84
  return Runtime(
83
85
  config=config,
@@ -87,7 +89,7 @@ class Runtime(NamedTuple):
87
89
  KIMI_NOW=datetime.now().astimezone().isoformat(),
88
90
  KIMI_WORK_DIR=session.work_dir,
89
91
  KIMI_WORK_DIR_LS=ls_output,
90
- KIMI_AGENTS_MD=agents_md,
92
+ KIMI_AGENTS_MD=agents_md or "",
91
93
  ),
92
94
  denwa_renji=DenwaRenji(),
93
95
  approval=Approval(yolo=yolo),
@@ -68,7 +68,8 @@ class Task(CallableTool2[Params]):
68
68
  self._subagents: dict[str, Agent] = {}
69
69
 
70
70
  try:
71
- self._load_task = asyncio.create_task(self._load_subagents(agent_spec.subagents))
71
+ loop = asyncio.get_running_loop()
72
+ self._load_task = loop.create_task(self._load_subagents(agent_spec.subagents))
72
73
  except RuntimeError:
73
74
  # In case there's no running event loop, e.g., during synchronous tests
74
75
  self._load_task = None
@@ -44,7 +44,7 @@ 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
+ self._custom_headers = config.services.moonshot_search.custom_headers or {}
48
48
  else:
49
49
  self._base_url = ""
50
50
  self._api_key = ""
File without changes