kimi-cli 0.35__py3-none-any.whl → 0.52__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.
Files changed (88) hide show
  1. kimi_cli/CHANGELOG.md +165 -0
  2. kimi_cli/__init__.py +0 -374
  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/app.py +208 -0
  7. kimi_cli/cli.py +321 -0
  8. kimi_cli/config.py +33 -16
  9. kimi_cli/constant.py +4 -0
  10. kimi_cli/exception.py +16 -0
  11. kimi_cli/llm.py +144 -3
  12. kimi_cli/metadata.py +6 -69
  13. kimi_cli/prompts/__init__.py +4 -0
  14. kimi_cli/session.py +103 -0
  15. kimi_cli/soul/__init__.py +130 -9
  16. kimi_cli/soul/agent.py +159 -0
  17. kimi_cli/soul/approval.py +5 -6
  18. kimi_cli/soul/compaction.py +106 -0
  19. kimi_cli/soul/context.py +1 -1
  20. kimi_cli/soul/kimisoul.py +180 -80
  21. kimi_cli/soul/message.py +6 -6
  22. kimi_cli/soul/runtime.py +96 -0
  23. kimi_cli/soul/toolset.py +3 -2
  24. kimi_cli/tools/__init__.py +35 -31
  25. kimi_cli/tools/bash/__init__.py +25 -9
  26. kimi_cli/tools/bash/cmd.md +31 -0
  27. kimi_cli/tools/dmail/__init__.py +5 -4
  28. kimi_cli/tools/file/__init__.py +8 -0
  29. kimi_cli/tools/file/glob.md +1 -1
  30. kimi_cli/tools/file/glob.py +4 -4
  31. kimi_cli/tools/file/grep.py +36 -19
  32. kimi_cli/tools/file/patch.py +52 -10
  33. kimi_cli/tools/file/read.py +6 -5
  34. kimi_cli/tools/file/replace.py +16 -4
  35. kimi_cli/tools/file/write.py +16 -4
  36. kimi_cli/tools/mcp.py +7 -4
  37. kimi_cli/tools/task/__init__.py +60 -41
  38. kimi_cli/tools/task/task.md +1 -1
  39. kimi_cli/tools/todo/__init__.py +4 -2
  40. kimi_cli/tools/utils.py +1 -1
  41. kimi_cli/tools/web/fetch.py +2 -1
  42. kimi_cli/tools/web/search.py +13 -12
  43. kimi_cli/ui/__init__.py +0 -68
  44. kimi_cli/ui/acp/__init__.py +67 -38
  45. kimi_cli/ui/print/__init__.py +46 -69
  46. kimi_cli/ui/shell/__init__.py +145 -154
  47. kimi_cli/ui/shell/console.py +27 -1
  48. kimi_cli/ui/shell/debug.py +187 -0
  49. kimi_cli/ui/shell/keyboard.py +183 -0
  50. kimi_cli/ui/shell/metacmd.py +34 -81
  51. kimi_cli/ui/shell/prompt.py +245 -28
  52. kimi_cli/ui/shell/replay.py +104 -0
  53. kimi_cli/ui/shell/setup.py +19 -19
  54. kimi_cli/ui/shell/update.py +11 -5
  55. kimi_cli/ui/shell/visualize.py +576 -0
  56. kimi_cli/ui/wire/README.md +109 -0
  57. kimi_cli/ui/wire/__init__.py +340 -0
  58. kimi_cli/ui/wire/jsonrpc.py +48 -0
  59. kimi_cli/utils/__init__.py +0 -0
  60. kimi_cli/utils/aiohttp.py +10 -0
  61. kimi_cli/utils/changelog.py +6 -2
  62. kimi_cli/utils/clipboard.py +10 -0
  63. kimi_cli/utils/message.py +15 -1
  64. kimi_cli/utils/rich/__init__.py +33 -0
  65. kimi_cli/utils/rich/markdown.py +959 -0
  66. kimi_cli/utils/rich/markdown_sample.md +108 -0
  67. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  68. kimi_cli/utils/signals.py +41 -0
  69. kimi_cli/utils/string.py +8 -0
  70. kimi_cli/utils/term.py +114 -0
  71. kimi_cli/wire/__init__.py +73 -0
  72. kimi_cli/wire/message.py +191 -0
  73. kimi_cli-0.52.dist-info/METADATA +186 -0
  74. kimi_cli-0.52.dist-info/RECORD +99 -0
  75. kimi_cli-0.52.dist-info/entry_points.txt +3 -0
  76. kimi_cli/agent.py +0 -261
  77. kimi_cli/agents/koder/README.md +0 -3
  78. kimi_cli/prompts/metacmds/__init__.py +0 -4
  79. kimi_cli/soul/wire.py +0 -101
  80. kimi_cli/ui/shell/liveview.py +0 -158
  81. kimi_cli/utils/provider.py +0 -64
  82. kimi_cli-0.35.dist-info/METADATA +0 -24
  83. kimi_cli-0.35.dist-info/RECORD +0 -76
  84. kimi_cli-0.35.dist-info/entry_points.txt +0 -3
  85. /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
  86. /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
  87. /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
  88. {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
kimi_cli/CHANGELOG.md CHANGED
@@ -9,6 +9,171 @@ 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.52] - 2025-11-10
13
+
14
+ - CLI: Remove `--ui` option in favor of `--print`, `--acp`, and `--wire` flags (shell is still the default)
15
+ - CLI: More intuitive session continuation behavior
16
+ - Core: Add retry for LLM empty responses
17
+ - Tool: Change `Bash` tool to `CMD` tool on Windows
18
+ - UI: Fix completion after backspacing
19
+ - UI: Fix code block rendering issues on light background colors
20
+
21
+ ## [0.51] - 2025-11-8
22
+
23
+ - Lib: Rename `Soul.model` to `Soul.model_name`
24
+ - Lib: Rename `LLMModelCapability` to `ModelCapability` and move to `kimi_cli.llm`
25
+ - Lib: Add `"thinking"` to `ModelCapability`
26
+ - Lib: Remove `LLM.supports_image_in` property
27
+ - Lib: Add required `Soul.model_capabilities` property
28
+ - Lib: Rename `KimiSoul.set_thinking_mode` to `KimiSoul.set_thinking`
29
+ - Lib: Add `KimiSoul.thinking` property
30
+ - UI: Better checks and notices for LLM model capabilities
31
+ - UI: Clear the screen for `/clear` meta command
32
+ - Tool: Support auto-downloading ripgrep on Windows
33
+ - CLI: Add `--thinking` option to start in thinking mode
34
+ - ACP: Support thinking content in ACP mode
35
+
36
+ ## [0.50] - 2025-11-07
37
+
38
+ ### Changed
39
+
40
+ - Improve UI look and feel
41
+ - Improve Task tool observability
42
+
43
+ ## [0.49] - 2025-11-06
44
+
45
+ ### Fixed
46
+
47
+ - Minor UX improvements
48
+
49
+ ## [0.48] - 2025-11-06
50
+
51
+ ### Added
52
+
53
+ - Support Kimi K2 thinking mode
54
+
55
+ ## [0.47] - 2025-11-05
56
+
57
+ ### Fixed
58
+
59
+ - Fix Ctrl-W not working in some environments
60
+ - Do not load SearchWeb tool when the search service is not configured
61
+
62
+ ## [0.46] - 2025-11-03
63
+
64
+ ### Added
65
+
66
+ - Introduce Wire over stdio for local IPC (experimental, subject to change)
67
+ - Support Anthropic provider type
68
+
69
+ ### Fixed
70
+
71
+ - Fix binary packed by PyInstaller not working due to wrong entrypoint
72
+
73
+ ## [0.45] - 2025-10-31
74
+
75
+ ### Added
76
+
77
+ - Allow `KIMI_MODEL_CAPABILITIES` environment variable to override model capabilities
78
+ - Add `--no-markdown` option to disable markdown rendering
79
+ - Support `openai_responses` LLM provider type
80
+
81
+ ### Fixed
82
+
83
+ - Fix crash when continuing a session
84
+
85
+ ## [0.44] - 2025-10-30
86
+
87
+ ### Changed
88
+
89
+ - Improve startup time
90
+
91
+ ### Fixed
92
+
93
+ - Fix potential invalid bytes in user input
94
+
95
+ ## [0.43] - 2025-10-30
96
+
97
+ ### Added
98
+
99
+ - Basic Windows support (experimental)
100
+ - Display warnings when base URL or API key is overridden in environment variables
101
+ - Support image input if the LLM model supports it
102
+ - Replay recent context history when continuing a session
103
+
104
+ ### Fixed
105
+
106
+ - Ensure new line after executing shell commands
107
+
108
+ ## [0.42] - 2025-10-28
109
+
110
+ ### Added
111
+
112
+ - Support Ctrl-J or Alt-Enter to insert a new line
113
+
114
+ ### Changed
115
+
116
+ - Change mode switch shortcut from Ctrl-K to Ctrl-X
117
+ - Improve overall robustness
118
+
119
+ ### Fixed
120
+
121
+ - Fix ACP server `no attribute` error
122
+
123
+ ## [0.41] - 2025-10-26
124
+
125
+ ### Fixed
126
+
127
+ - Fix a bug for Glob tool when no matching files are found
128
+ - Ensure reading files with UTF-8 encoding
129
+
130
+ ### Changed
131
+
132
+ - Disable reading command/query from stdin in shell mode
133
+ - Clarify the API platform selection in `/setup` meta command
134
+
135
+ ## [0.40] - 2025-10-24
136
+
137
+ ### Added
138
+
139
+ - Support `ESC` key to interrupt the agent loop
140
+
141
+ ### Fixed
142
+
143
+ - Fix SSL certificate verification error in some rare cases
144
+ - Fix possible decoding error in Bash tool
145
+
146
+ ## [0.39] - 2025-10-24
147
+
148
+ ### Fixed
149
+
150
+ - Fix context compaction threshold check
151
+ - Fix panic when SOCKS proxy is set in the shell session
152
+
153
+ ## [0.38] - 2025-10-24
154
+
155
+ - Minor UX improvements
156
+
157
+ ## [0.37] - 2025-10-24
158
+
159
+ ### Fixed
160
+
161
+ - Fix update checking
162
+
163
+ ## [0.36] - 2025-10-24
164
+
165
+ ### Added
166
+
167
+ - Add `/debug` meta command to debug the context
168
+ - Add auto context compaction
169
+ - Add approval request mechanism
170
+ - Add `--yolo` option to automatically approve all actions
171
+ - Render markdown content for better readability
172
+
173
+ ### Fixed
174
+
175
+ - Fix "unknown error" message when interrupting a meta command
176
+
12
177
  ## [0.35] - 2025-10-22
13
178
 
14
179
  ### Changed
kimi_cli/__init__.py CHANGED
@@ -1,374 +0,0 @@
1
- import asyncio
2
- import contextlib
3
- import importlib.metadata
4
- import json
5
- import os
6
- import subprocess
7
- import sys
8
- import textwrap
9
- import warnings
10
- from datetime import datetime
11
- from pathlib import Path
12
- from typing import Any, Literal
13
-
14
- import click
15
- from pydantic import SecretStr
16
-
17
- from kimi_cli.agent import (
18
- DEFAULT_AGENT_FILE,
19
- AgentGlobals,
20
- BuiltinSystemPromptArgs,
21
- load_agent_with_mcp,
22
- load_agents_md,
23
- )
24
- from kimi_cli.config import (
25
- Config,
26
- ConfigError,
27
- LLMModel,
28
- LLMProvider,
29
- load_config,
30
- )
31
- from kimi_cli.metadata import Session, continue_session, new_session
32
- from kimi_cli.share import get_share_dir
33
- from kimi_cli.soul.approval import Approval
34
- from kimi_cli.soul.context import Context
35
- from kimi_cli.soul.denwarenji import DenwaRenji
36
- from kimi_cli.soul.kimisoul import KimiSoul
37
- from kimi_cli.ui.acp import ACPServer
38
- from kimi_cli.ui.print import InputFormat, OutputFormat, PrintApp
39
- from kimi_cli.ui.shell import Reload, ShellApp
40
- from kimi_cli.utils.logging import StreamToLogger, logger
41
- from kimi_cli.utils.provider import augment_provider_with_env_vars, create_llm
42
-
43
- __version__ = importlib.metadata.version("kimi-cli")
44
- USER_AGENT = f"KimiCLI/{__version__}"
45
-
46
- UIMode = Literal["shell", "print", "acp"]
47
-
48
-
49
- @click.command(context_settings=dict(help_option_names=["-h", "--help"]))
50
- @click.version_option(__version__)
51
- @click.option(
52
- "--verbose",
53
- is_flag=True,
54
- default=False,
55
- help="Print verbose information. Default: no.",
56
- )
57
- @click.option(
58
- "--debug",
59
- is_flag=True,
60
- default=False,
61
- help="Log debug information. Default: no.",
62
- )
63
- @click.option(
64
- "--agent-file",
65
- type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
66
- default=DEFAULT_AGENT_FILE,
67
- help="Custom agent specification file. Default: builtin Kimi Koder.",
68
- )
69
- @click.option(
70
- "--model",
71
- "-m",
72
- "model_name",
73
- type=str,
74
- default=None,
75
- help="LLM model to use. Default: default model set in config file.",
76
- )
77
- @click.option(
78
- "--work-dir",
79
- "-w",
80
- type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
81
- default=Path.cwd(),
82
- help="Working directory for the agent. Default: current directory.",
83
- )
84
- @click.option(
85
- "--continue",
86
- "-C",
87
- "continue_",
88
- is_flag=True,
89
- default=False,
90
- help="Continue the previous session for the working directory. Default: no.",
91
- )
92
- @click.option(
93
- "--command",
94
- "-c",
95
- "--query",
96
- "-q",
97
- "command",
98
- type=str,
99
- default=None,
100
- help="User query to the agent. Default: prompt interactively.",
101
- )
102
- @click.option(
103
- "--ui",
104
- "ui",
105
- type=click.Choice(["shell", "print", "acp"]),
106
- default="shell",
107
- help="UI mode to use. Default: shell.",
108
- )
109
- @click.option(
110
- "--print",
111
- "ui",
112
- flag_value="print",
113
- help="Run in print mode. Shortcut for `--ui print`.",
114
- )
115
- @click.option(
116
- "--acp",
117
- "ui",
118
- flag_value="acp",
119
- help="Start ACP server. Shortcut for `--ui acp`.",
120
- )
121
- @click.option(
122
- "--input-format",
123
- type=click.Choice(["text", "stream-json"]),
124
- default=None,
125
- help=(
126
- "Input format to use. Must be used with `--print` "
127
- "and the input must be piped in via stdin. "
128
- "Default: text."
129
- ),
130
- )
131
- @click.option(
132
- "--output-format",
133
- type=click.Choice(["text", "stream-json"]),
134
- default=None,
135
- help="Output format to use. Must be used with `--print`. Default: text.",
136
- )
137
- @click.option(
138
- "--mcp-config-file",
139
- type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
140
- multiple=True,
141
- help=(
142
- "MCP config file to load. Add this option multiple times to specify multiple MCP configs. "
143
- "Default: none."
144
- ),
145
- )
146
- @click.option(
147
- "--mcp-config",
148
- type=str,
149
- multiple=True,
150
- help=(
151
- "MCP config JSON to load. Add this option multiple times to specify multiple MCP configs. "
152
- "Default: none."
153
- ),
154
- )
155
- def kimi(
156
- verbose: bool,
157
- debug: bool,
158
- agent_file: Path,
159
- model_name: str | None,
160
- work_dir: Path,
161
- continue_: bool,
162
- command: str | None,
163
- ui: UIMode,
164
- input_format: InputFormat | None,
165
- output_format: OutputFormat | None,
166
- mcp_config_file: list[Path],
167
- mcp_config: list[str],
168
- ):
169
- """Kimi, your next CLI agent."""
170
- echo = click.echo if verbose else lambda *args, **kwargs: None
171
-
172
- logger.add(
173
- get_share_dir() / "logs" / "kimi.log",
174
- level="DEBUG" if debug else "INFO",
175
- rotation="06:00",
176
- retention="10 days",
177
- )
178
-
179
- work_dir = work_dir.absolute()
180
-
181
- if continue_:
182
- session = continue_session(work_dir)
183
- if session is None:
184
- raise click.BadOptionUsage(
185
- "--continue", "No previous session found for the working directory"
186
- )
187
- echo(f"✓ Continuing previous session: {session.id}")
188
- else:
189
- session = new_session(work_dir)
190
- echo(f"✓ Created new session: {session.id}")
191
- echo(f"✓ Session history file: {session.history_file}")
192
-
193
- if input_format is not None and ui != "print":
194
- raise click.BadOptionUsage(
195
- "--input-format",
196
- "Input format is only supported for print UI",
197
- )
198
- if output_format is not None and ui != "print":
199
- raise click.BadOptionUsage(
200
- "--output-format",
201
- "Output format is only supported for print UI",
202
- )
203
-
204
- try:
205
- mcp_configs = [json.loads(conf.read_text()) for conf in mcp_config_file]
206
- except json.JSONDecodeError as e:
207
- raise click.BadOptionUsage("--mcp-config-file", f"Invalid JSON: {e}") from e
208
-
209
- try:
210
- mcp_configs += [json.loads(conf) for conf in mcp_config]
211
- except json.JSONDecodeError as e:
212
- raise click.BadOptionUsage("--mcp-config", f"Invalid JSON: {e}") from e
213
-
214
- while True:
215
- try:
216
- try:
217
- config = load_config()
218
- except ConfigError as e:
219
- raise click.ClickException(f"Failed to load config: {e}") from e
220
- echo(f"✓ Loaded config: {config}")
221
-
222
- succeeded = asyncio.run(
223
- kimi_run(
224
- config=config,
225
- model_name=model_name,
226
- work_dir=work_dir,
227
- session=session,
228
- command=command,
229
- agent_file=agent_file,
230
- verbose=verbose,
231
- ui=ui,
232
- input_format=input_format,
233
- output_format=output_format,
234
- mcp_configs=mcp_configs,
235
- )
236
- )
237
- if not succeeded:
238
- sys.exit(1)
239
- break
240
- except Reload:
241
- continue
242
-
243
-
244
- async def kimi_run(
245
- *,
246
- config: Config,
247
- model_name: str | None,
248
- work_dir: Path,
249
- session: Session,
250
- command: str | None = None,
251
- agent_file: Path = DEFAULT_AGENT_FILE,
252
- verbose: bool = True,
253
- ui: UIMode = "shell",
254
- input_format: InputFormat | None = None,
255
- output_format: OutputFormat | None = None,
256
- mcp_configs: list[dict[str, Any]] | None = None,
257
- ) -> bool:
258
- """Run Kimi CLI."""
259
- echo = click.echo if verbose else lambda *args, **kwargs: None
260
-
261
- model: LLMModel | None = None
262
- provider: LLMProvider | None = None
263
-
264
- # try to use config file
265
- if not model_name and config.default_model:
266
- # no --model specified && default model is set in config
267
- model = config.models[config.default_model]
268
- provider = config.providers[model.provider]
269
- if model_name and model_name in config.models:
270
- # --model specified && model is set in config
271
- model = config.models[model_name]
272
- provider = config.providers[model.provider]
273
-
274
- if not model:
275
- model = LLMModel(provider="", model="", max_context_size=100_000)
276
- provider = LLMProvider(type="kimi", base_url="", api_key=SecretStr(""))
277
-
278
- # try overwrite with environment variables
279
- assert provider is not None
280
- assert model is not None
281
- augment_provider_with_env_vars(provider, model)
282
-
283
- if not provider.base_url or not model.model:
284
- llm = None
285
- else:
286
- echo(f"✓ Using LLM provider: {provider}")
287
- echo(f"✓ Using LLM model: {model}")
288
- stream = ui != "print" # use non-streaming mode only for print UI
289
- llm = create_llm(provider, model, stream=stream)
290
-
291
- # TODO: support Windows
292
- ls = subprocess.run(["ls", "-la"], capture_output=True, text=True)
293
- agents_md = load_agents_md(work_dir) or ""
294
- if agents_md:
295
- echo(f"✓ Loaded agents.md: {textwrap.shorten(agents_md, width=100)}")
296
-
297
- agent_globals = AgentGlobals(
298
- config=config,
299
- llm=llm,
300
- builtin_args=BuiltinSystemPromptArgs(
301
- KIMI_NOW=datetime.now().astimezone().isoformat(),
302
- KIMI_WORK_DIR=work_dir,
303
- KIMI_WORK_DIR_LS=ls.stdout,
304
- KIMI_AGENTS_MD=agents_md,
305
- ),
306
- denwa_renji=DenwaRenji(),
307
- session=session,
308
- approval=Approval(),
309
- )
310
- try:
311
- agent = await load_agent_with_mcp(agent_file, agent_globals, mcp_configs or [])
312
- except ValueError as e:
313
- raise click.BadParameter(f"Failed to load agent: {e}") from e
314
- echo(f"✓ Loaded agent: {agent.name}")
315
- echo(f"✓ Loaded system prompt: {textwrap.shorten(agent.system_prompt, width=100)}")
316
- echo(f"✓ Loaded tools: {[tool.name for tool in agent.toolset.tools]}")
317
-
318
- if command is not None:
319
- command = command.strip()
320
- if not command:
321
- raise click.BadParameter("Command cannot be empty")
322
-
323
- context = Context(session.history_file)
324
- restored = await context.restore()
325
- if restored:
326
- echo(f"✓ Restored history from {session.history_file}")
327
-
328
- soul = KimiSoul(
329
- agent,
330
- agent_globals,
331
- context=context,
332
- loop_control=config.loop_control,
333
- )
334
-
335
- original_cwd = Path.cwd()
336
- os.chdir(work_dir)
337
-
338
- try:
339
- if ui == "shell":
340
- if command is None and not sys.stdin.isatty():
341
- command = sys.stdin.read().strip()
342
- echo(f"✓ Read command from stdin: {command}")
343
-
344
- app = ShellApp(
345
- soul,
346
- welcome_info={
347
- "Directory": str(work_dir),
348
- "Session": session.id,
349
- },
350
- )
351
- # to ignore possible warnings from dateparser
352
- warnings.filterwarnings("ignore", category=DeprecationWarning)
353
- with contextlib.redirect_stderr(StreamToLogger()):
354
- return await app.run(command)
355
- elif ui == "print":
356
- app = PrintApp(soul, input_format or "text", output_format or "text")
357
- return await app.run(command)
358
- elif ui == "acp":
359
- if command is not None:
360
- logger.warning("ACP server ignores command argument")
361
- app = ACPServer(soul)
362
- return await app.run()
363
- else:
364
- raise click.BadParameter(f"Invalid UI mode: {ui}")
365
- finally:
366
- os.chdir(original_cwd)
367
-
368
-
369
- def main():
370
- kimi()
371
-
372
-
373
- if __name__ == "__main__":
374
- main()
@@ -19,6 +19,6 @@ agent:
19
19
  - "kimi_cli.tools.web:SearchWeb"
20
20
  - "kimi_cli.tools.web:FetchURL"
21
21
  subagents:
22
- koder:
22
+ coder:
23
23
  path: ./sub.yaml
24
24
  description: "Good at general software engineering tasks."
@@ -47,7 +47,7 @@ The operating environment is not in a sandbox. Any action especially mutation yo
47
47
 
48
48
  The current working directory is `${KIMI_WORK_DIR}`. This should be considered as the project root if you are instructed to perform tasks on the project. Every file system operation will be relative to the working directory if you do not explicitly specify the absolute path. Tools may require absolute paths for some parameters, if so, you should strictly follow the requirements.
49
49
 
50
- The `ls -la` output of current working directory is:
50
+ The directory listing of current working directory is:
51
51
 
52
52
  ```
53
53
  ${KIMI_WORK_DIR_LS}
kimi_cli/agentspec.py ADDED
@@ -0,0 +1,115 @@
1
+ from pathlib import Path
2
+ from typing import Any, NamedTuple
3
+
4
+ import yaml
5
+ from pydantic import BaseModel, Field
6
+
7
+ from kimi_cli.exception import AgentSpecError
8
+
9
+
10
+ def get_agents_dir() -> Path:
11
+ return Path(__file__).parent / "agents"
12
+
13
+
14
+ DEFAULT_AGENT_FILE = get_agents_dir() / "default" / "agent.yaml"
15
+
16
+
17
+ class AgentSpec(BaseModel):
18
+ """Agent specification."""
19
+
20
+ extend: str | None = Field(default=None, description="Agent file to extend")
21
+ name: str | None = Field(default=None, description="Agent name") # required
22
+ system_prompt_path: Path | None = Field(
23
+ default=None, description="System prompt path"
24
+ ) # required
25
+ system_prompt_args: dict[str, str] = Field(
26
+ default_factory=dict, description="System prompt arguments"
27
+ )
28
+ tools: list[str] | None = Field(default=None, description="Tools") # required
29
+ exclude_tools: list[str] | None = Field(default=None, description="Tools to exclude")
30
+ subagents: dict[str, "SubagentSpec"] | None = Field(default=None, description="Subagents")
31
+
32
+
33
+ class SubagentSpec(BaseModel):
34
+ """Subagent specification."""
35
+
36
+ path: Path = Field(description="Subagent file path")
37
+ description: str = Field(description="Subagent description")
38
+
39
+
40
+ class ResolvedAgentSpec(NamedTuple):
41
+ """Resolved agent specification."""
42
+
43
+ name: str
44
+ system_prompt_path: Path
45
+ system_prompt_args: dict[str, str]
46
+ tools: list[str]
47
+ exclude_tools: list[str]
48
+ subagents: dict[str, "SubagentSpec"]
49
+
50
+
51
+ def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
52
+ """
53
+ Load agent specification from file.
54
+
55
+ Raises:
56
+ FileNotFoundError: If the agent spec file is not found.
57
+ AgentSpecError: If the agent spec is not valid.
58
+ """
59
+ agent_spec = _load_agent_spec(agent_file)
60
+ assert agent_spec.extend is None, "agent extension should be recursively resolved"
61
+ if agent_spec.name is None:
62
+ raise AgentSpecError("Agent name is required")
63
+ if agent_spec.system_prompt_path is None:
64
+ raise AgentSpecError("System prompt path is required")
65
+ if agent_spec.tools is None:
66
+ raise AgentSpecError("Tools are required")
67
+ return ResolvedAgentSpec(
68
+ name=agent_spec.name,
69
+ system_prompt_path=agent_spec.system_prompt_path,
70
+ system_prompt_args=agent_spec.system_prompt_args,
71
+ tools=agent_spec.tools,
72
+ exclude_tools=agent_spec.exclude_tools or [],
73
+ subagents=agent_spec.subagents or {},
74
+ )
75
+
76
+
77
+ def _load_agent_spec(agent_file: Path) -> AgentSpec:
78
+ assert agent_file.is_file(), "expect agent file to exist"
79
+ try:
80
+ with open(agent_file, encoding="utf-8") as f:
81
+ data: dict[str, Any] = yaml.safe_load(f)
82
+ except yaml.YAMLError as e:
83
+ raise AgentSpecError(f"Invalid YAML in agent spec file: {e}") from e
84
+
85
+ version = data.get("version", 1)
86
+ if version != 1:
87
+ raise AgentSpecError(f"Unsupported agent spec version: {version}")
88
+
89
+ agent_spec = AgentSpec(**data.get("agent", {}))
90
+ if agent_spec.system_prompt_path is not None:
91
+ agent_spec.system_prompt_path = agent_file.parent / agent_spec.system_prompt_path
92
+ if agent_spec.subagents is not None:
93
+ for v in agent_spec.subagents.values():
94
+ v.path = agent_file.parent / v.path
95
+ if agent_spec.extend:
96
+ if agent_spec.extend == "default":
97
+ base_agent_file = DEFAULT_AGENT_FILE
98
+ else:
99
+ base_agent_file = agent_file.parent / agent_spec.extend
100
+ base_agent_spec = _load_agent_spec(base_agent_file)
101
+ if agent_spec.name is not None:
102
+ base_agent_spec.name = agent_spec.name
103
+ if agent_spec.system_prompt_path is not None:
104
+ base_agent_spec.system_prompt_path = agent_spec.system_prompt_path
105
+ for k, v in agent_spec.system_prompt_args.items():
106
+ # system prompt args should be merged instead of overwritten
107
+ base_agent_spec.system_prompt_args[k] = v
108
+ if agent_spec.tools is not None:
109
+ base_agent_spec.tools = agent_spec.tools
110
+ if agent_spec.exclude_tools is not None:
111
+ base_agent_spec.exclude_tools = agent_spec.exclude_tools
112
+ if agent_spec.subagents is not None:
113
+ base_agent_spec.subagents = agent_spec.subagents
114
+ agent_spec = base_agent_spec
115
+ return agent_spec