kimi-cli 0.40__py3-none-any.whl → 0.41__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kimi-cli might be problematic. Click here for more details.
- kimi_cli/CHANGELOG.md +12 -0
- kimi_cli/__init__.py +18 -280
- kimi_cli/agents/koder/system.md +1 -1
- kimi_cli/agentspec.py +104 -0
- kimi_cli/cli.py +235 -0
- kimi_cli/constant.py +4 -0
- kimi_cli/llm.py +69 -0
- kimi_cli/prompts/__init__.py +2 -2
- kimi_cli/soul/__init__.py +102 -6
- kimi_cli/soul/agent.py +157 -0
- kimi_cli/soul/approval.py +1 -1
- kimi_cli/soul/compaction.py +4 -4
- kimi_cli/soul/context.py +5 -0
- kimi_cli/soul/globals.py +92 -0
- kimi_cli/soul/kimisoul.py +21 -26
- kimi_cli/tools/dmail/__init__.py +1 -1
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +2 -2
- kimi_cli/tools/file/grep.py +1 -1
- kimi_cli/tools/file/patch.py +2 -2
- kimi_cli/tools/file/read.py +1 -1
- kimi_cli/tools/file/replace.py +2 -2
- kimi_cli/tools/file/write.py +2 -2
- kimi_cli/tools/task/__init__.py +23 -22
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +1 -1
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/search.py +2 -2
- kimi_cli/ui/__init__.py +0 -69
- kimi_cli/ui/acp/__init__.py +8 -9
- kimi_cli/ui/print/__init__.py +17 -35
- kimi_cli/ui/shell/__init__.py +5 -13
- kimi_cli/ui/shell/liveview.py +1 -1
- kimi_cli/ui/shell/metacmd.py +3 -3
- kimi_cli/ui/shell/setup.py +5 -5
- kimi_cli/ui/shell/update.py +2 -2
- kimi_cli/ui/shell/visualize.py +10 -7
- kimi_cli/utils/changelog.py +3 -1
- kimi_cli/wire/__init__.py +57 -0
- kimi_cli/{soul/wire.py → wire/message.py} +4 -39
- {kimi_cli-0.40.dist-info → kimi_cli-0.41.dist-info}/METADATA +34 -1
- kimi_cli-0.41.dist-info/RECORD +85 -0
- kimi_cli-0.41.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/utils/provider.py +0 -70
- kimi_cli-0.40.dist-info/RECORD +0 -81
- kimi_cli-0.40.dist-info/entry_points.txt +0 -3
- {kimi_cli-0.40.dist-info → kimi_cli-0.41.dist-info}/WHEEL +0 -0
kimi_cli/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,18 @@ 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.41] - 2025-10-26
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Fix a bug for Glob tool when no matching files are found
|
|
17
|
+
- Ensure reading files with UTF-8 encoding
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Disable reading command/query from stdin in shell mode
|
|
22
|
+
- Clarify the API platform selection in `/setup` meta command
|
|
23
|
+
|
|
12
24
|
## [0.40] - 2025-10-24
|
|
13
25
|
|
|
14
26
|
### Added
|
kimi_cli/__init__.py
CHANGED
|
@@ -1,258 +1,28 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import contextlib
|
|
3
|
-
import importlib.metadata
|
|
4
|
-
import json
|
|
5
2
|
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
import sys
|
|
8
|
-
import textwrap
|
|
9
3
|
import warnings
|
|
10
|
-
from datetime import datetime
|
|
11
4
|
from pathlib import Path
|
|
12
5
|
from typing import Any, Literal
|
|
13
6
|
|
|
14
7
|
import click
|
|
15
8
|
from pydantic import SecretStr
|
|
16
9
|
|
|
17
|
-
from kimi_cli.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
10
|
+
from kimi_cli.agentspec import DEFAULT_AGENT_FILE
|
|
11
|
+
from kimi_cli.config import Config, LLMModel, LLMProvider
|
|
12
|
+
from kimi_cli.llm import augment_provider_with_env_vars, create_llm
|
|
13
|
+
from kimi_cli.metadata import Session
|
|
14
|
+
from kimi_cli.soul.agent import load_agent_with_mcp
|
|
34
15
|
from kimi_cli.soul.context import Context
|
|
35
|
-
from kimi_cli.soul.
|
|
16
|
+
from kimi_cli.soul.globals import AgentGlobals
|
|
36
17
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
37
18
|
from kimi_cli.ui.acp import ACPServer
|
|
38
19
|
from kimi_cli.ui.print import InputFormat, OutputFormat, PrintApp
|
|
39
|
-
from kimi_cli.ui.shell import
|
|
20
|
+
from kimi_cli.ui.shell import ShellApp
|
|
40
21
|
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
22
|
|
|
46
23
|
UIMode = Literal["shell", "print", "acp"]
|
|
47
24
|
|
|
48
25
|
|
|
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
|
-
@click.option(
|
|
156
|
-
"--yolo",
|
|
157
|
-
"--yes",
|
|
158
|
-
"-y",
|
|
159
|
-
"--auto-approve",
|
|
160
|
-
"yolo",
|
|
161
|
-
is_flag=True,
|
|
162
|
-
default=False,
|
|
163
|
-
help="Automatically approve all actions. Default: no.",
|
|
164
|
-
)
|
|
165
|
-
def kimi(
|
|
166
|
-
verbose: bool,
|
|
167
|
-
debug: bool,
|
|
168
|
-
agent_file: Path,
|
|
169
|
-
model_name: str | None,
|
|
170
|
-
work_dir: Path,
|
|
171
|
-
continue_: bool,
|
|
172
|
-
command: str | None,
|
|
173
|
-
ui: UIMode,
|
|
174
|
-
input_format: InputFormat | None,
|
|
175
|
-
output_format: OutputFormat | None,
|
|
176
|
-
mcp_config_file: list[Path],
|
|
177
|
-
mcp_config: list[str],
|
|
178
|
-
yolo: bool,
|
|
179
|
-
):
|
|
180
|
-
"""Kimi, your next CLI agent."""
|
|
181
|
-
echo = click.echo if verbose else lambda *args, **kwargs: None
|
|
182
|
-
|
|
183
|
-
logger.add(
|
|
184
|
-
get_share_dir() / "logs" / "kimi.log",
|
|
185
|
-
level="DEBUG" if debug else "INFO",
|
|
186
|
-
rotation="06:00",
|
|
187
|
-
retention="10 days",
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
work_dir = work_dir.absolute()
|
|
191
|
-
|
|
192
|
-
if continue_:
|
|
193
|
-
session = continue_session(work_dir)
|
|
194
|
-
if session is None:
|
|
195
|
-
raise click.BadOptionUsage(
|
|
196
|
-
"--continue", "No previous session found for the working directory"
|
|
197
|
-
)
|
|
198
|
-
echo(f"✓ Continuing previous session: {session.id}")
|
|
199
|
-
else:
|
|
200
|
-
session = new_session(work_dir)
|
|
201
|
-
echo(f"✓ Created new session: {session.id}")
|
|
202
|
-
echo(f"✓ Session history file: {session.history_file}")
|
|
203
|
-
|
|
204
|
-
if input_format is not None and ui != "print":
|
|
205
|
-
raise click.BadOptionUsage(
|
|
206
|
-
"--input-format",
|
|
207
|
-
"Input format is only supported for print UI",
|
|
208
|
-
)
|
|
209
|
-
if output_format is not None and ui != "print":
|
|
210
|
-
raise click.BadOptionUsage(
|
|
211
|
-
"--output-format",
|
|
212
|
-
"Output format is only supported for print UI",
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
try:
|
|
216
|
-
mcp_configs = [json.loads(conf.read_text()) for conf in mcp_config_file]
|
|
217
|
-
except json.JSONDecodeError as e:
|
|
218
|
-
raise click.BadOptionUsage("--mcp-config-file", f"Invalid JSON: {e}") from e
|
|
219
|
-
|
|
220
|
-
try:
|
|
221
|
-
mcp_configs += [json.loads(conf) for conf in mcp_config]
|
|
222
|
-
except json.JSONDecodeError as e:
|
|
223
|
-
raise click.BadOptionUsage("--mcp-config", f"Invalid JSON: {e}") from e
|
|
224
|
-
|
|
225
|
-
while True:
|
|
226
|
-
try:
|
|
227
|
-
try:
|
|
228
|
-
config = load_config()
|
|
229
|
-
except ConfigError as e:
|
|
230
|
-
raise click.ClickException(f"Failed to load config: {e}") from e
|
|
231
|
-
echo(f"✓ Loaded config: {config}")
|
|
232
|
-
|
|
233
|
-
succeeded = asyncio.run(
|
|
234
|
-
kimi_run(
|
|
235
|
-
config=config,
|
|
236
|
-
model_name=model_name,
|
|
237
|
-
work_dir=work_dir,
|
|
238
|
-
session=session,
|
|
239
|
-
command=command,
|
|
240
|
-
agent_file=agent_file,
|
|
241
|
-
verbose=verbose,
|
|
242
|
-
ui=ui,
|
|
243
|
-
input_format=input_format,
|
|
244
|
-
output_format=output_format,
|
|
245
|
-
mcp_configs=mcp_configs,
|
|
246
|
-
yolo=yolo,
|
|
247
|
-
)
|
|
248
|
-
)
|
|
249
|
-
if not succeeded:
|
|
250
|
-
sys.exit(1)
|
|
251
|
-
break
|
|
252
|
-
except Reload:
|
|
253
|
-
continue
|
|
254
|
-
|
|
255
|
-
|
|
256
26
|
async def kimi_run(
|
|
257
27
|
*,
|
|
258
28
|
config: Config,
|
|
@@ -261,7 +31,6 @@ async def kimi_run(
|
|
|
261
31
|
session: Session,
|
|
262
32
|
command: str | None = None,
|
|
263
33
|
agent_file: Path = DEFAULT_AGENT_FILE,
|
|
264
|
-
verbose: bool = True,
|
|
265
34
|
ui: UIMode = "shell",
|
|
266
35
|
input_format: InputFormat | None = None,
|
|
267
36
|
output_format: OutputFormat | None = None,
|
|
@@ -269,8 +38,6 @@ async def kimi_run(
|
|
|
269
38
|
yolo: bool = False,
|
|
270
39
|
) -> bool:
|
|
271
40
|
"""Run Kimi CLI."""
|
|
272
|
-
echo = click.echo if verbose else lambda *args, **kwargs: None
|
|
273
|
-
|
|
274
41
|
model: LLMModel | None = None
|
|
275
42
|
provider: LLMProvider | None = None
|
|
276
43
|
|
|
@@ -296,37 +63,16 @@ async def kimi_run(
|
|
|
296
63
|
if not provider.base_url or not model.model:
|
|
297
64
|
llm = None
|
|
298
65
|
else:
|
|
299
|
-
|
|
300
|
-
|
|
66
|
+
logger.info("Using LLM provider: {provider}", provider=provider)
|
|
67
|
+
logger.info("Using LLM model: {model}", model=model)
|
|
301
68
|
stream = ui != "print" # use non-streaming mode only for print UI
|
|
302
69
|
llm = create_llm(provider, model, stream=stream, session_id=session.id)
|
|
303
70
|
|
|
304
|
-
|
|
305
|
-
ls = subprocess.run(["ls", "-la"], capture_output=True, text=True)
|
|
306
|
-
agents_md = load_agents_md(work_dir) or ""
|
|
307
|
-
if agents_md:
|
|
308
|
-
echo(f"✓ Loaded agents.md: {textwrap.shorten(agents_md, width=100)}")
|
|
309
|
-
|
|
310
|
-
agent_globals = AgentGlobals(
|
|
311
|
-
config=config,
|
|
312
|
-
llm=llm,
|
|
313
|
-
builtin_args=BuiltinSystemPromptArgs(
|
|
314
|
-
KIMI_NOW=datetime.now().astimezone().isoformat(),
|
|
315
|
-
KIMI_WORK_DIR=work_dir,
|
|
316
|
-
KIMI_WORK_DIR_LS=ls.stdout,
|
|
317
|
-
KIMI_AGENTS_MD=agents_md,
|
|
318
|
-
),
|
|
319
|
-
denwa_renji=DenwaRenji(),
|
|
320
|
-
session=session,
|
|
321
|
-
approval=Approval(yolo=yolo),
|
|
322
|
-
)
|
|
71
|
+
agent_globals = await AgentGlobals.create(config, llm, session, yolo)
|
|
323
72
|
try:
|
|
324
73
|
agent = await load_agent_with_mcp(agent_file, agent_globals, mcp_configs or [])
|
|
325
74
|
except ValueError as e:
|
|
326
75
|
raise click.BadParameter(f"Failed to load agent: {e}") from e
|
|
327
|
-
echo(f"✓ Loaded agent: {agent.name}")
|
|
328
|
-
echo(f"✓ Loaded system prompt: {textwrap.shorten(agent.system_prompt, width=100)}")
|
|
329
|
-
echo(f"✓ Loaded tools: {[tool.name for tool in agent.toolset.tools]}")
|
|
330
76
|
|
|
331
77
|
if command is not None:
|
|
332
78
|
command = command.strip()
|
|
@@ -334,9 +80,7 @@ async def kimi_run(
|
|
|
334
80
|
raise click.BadParameter("Command cannot be empty")
|
|
335
81
|
|
|
336
82
|
context = Context(session.history_file)
|
|
337
|
-
|
|
338
|
-
if restored:
|
|
339
|
-
echo(f"✓ Restored history from {session.history_file}")
|
|
83
|
+
await context.restore()
|
|
340
84
|
|
|
341
85
|
soul = KimiSoul(
|
|
342
86
|
agent,
|
|
@@ -350,10 +94,6 @@ async def kimi_run(
|
|
|
350
94
|
|
|
351
95
|
try:
|
|
352
96
|
if ui == "shell":
|
|
353
|
-
if command is None and not sys.stdin.isatty():
|
|
354
|
-
command = sys.stdin.read().strip()
|
|
355
|
-
echo(f"✓ Read command from stdin: {command}")
|
|
356
|
-
|
|
357
97
|
app = ShellApp(
|
|
358
98
|
soul,
|
|
359
99
|
welcome_info={
|
|
@@ -366,7 +106,13 @@ async def kimi_run(
|
|
|
366
106
|
with contextlib.redirect_stderr(StreamToLogger()):
|
|
367
107
|
return await app.run(command)
|
|
368
108
|
elif ui == "print":
|
|
369
|
-
|
|
109
|
+
soul._approval.set_yolo(True) # print mode implies yolo mode
|
|
110
|
+
app = PrintApp(
|
|
111
|
+
soul,
|
|
112
|
+
input_format or "text",
|
|
113
|
+
output_format or "text",
|
|
114
|
+
context.file_backend,
|
|
115
|
+
)
|
|
370
116
|
return await app.run(command)
|
|
371
117
|
elif ui == "acp":
|
|
372
118
|
if command is not None:
|
|
@@ -377,11 +123,3 @@ async def kimi_run(
|
|
|
377
123
|
raise click.BadParameter(f"Invalid UI mode: {ui}")
|
|
378
124
|
finally:
|
|
379
125
|
os.chdir(original_cwd)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def main():
|
|
383
|
-
kimi()
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if __name__ == "__main__":
|
|
387
|
-
main()
|
kimi_cli/agents/koder/system.md
CHANGED
|
@@ -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
|
|
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,104 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, NamedTuple
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_agents_dir() -> Path:
|
|
9
|
+
return Path(__file__).parent / "agents"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_AGENT_FILE = get_agents_dir() / "koder" / "agent.yaml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentSpec(BaseModel):
|
|
16
|
+
"""Agent specification."""
|
|
17
|
+
|
|
18
|
+
extend: str | None = Field(default=None, description="Agent file to extend")
|
|
19
|
+
name: str | None = Field(default=None, description="Agent name") # required
|
|
20
|
+
system_prompt_path: Path | None = Field(
|
|
21
|
+
default=None, description="System prompt path"
|
|
22
|
+
) # required
|
|
23
|
+
system_prompt_args: dict[str, str] = Field(
|
|
24
|
+
default_factory=dict, description="System prompt arguments"
|
|
25
|
+
)
|
|
26
|
+
tools: list[str] | None = Field(default=None, description="Tools") # required
|
|
27
|
+
exclude_tools: list[str] | None = Field(default=None, description="Tools to exclude")
|
|
28
|
+
subagents: dict[str, "SubagentSpec"] | None = Field(default=None, description="Subagents")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SubagentSpec(BaseModel):
|
|
32
|
+
"""Subagent specification."""
|
|
33
|
+
|
|
34
|
+
path: Path = Field(description="Subagent file path")
|
|
35
|
+
description: str = Field(description="Subagent description")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ResolvedAgentSpec(NamedTuple):
|
|
39
|
+
"""Resolved agent specification."""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
system_prompt_path: Path
|
|
43
|
+
system_prompt_args: dict[str, str]
|
|
44
|
+
tools: list[str]
|
|
45
|
+
exclude_tools: list[str]
|
|
46
|
+
subagents: dict[str, "SubagentSpec"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
|
|
50
|
+
"""Load agent specification from file."""
|
|
51
|
+
agent_spec = _load_agent_spec(agent_file)
|
|
52
|
+
assert agent_spec.extend is None, "agent extension should be recursively resolved"
|
|
53
|
+
if agent_spec.name is None:
|
|
54
|
+
raise ValueError("Agent name is required")
|
|
55
|
+
if agent_spec.system_prompt_path is None:
|
|
56
|
+
raise ValueError("System prompt path is required")
|
|
57
|
+
if agent_spec.tools is None:
|
|
58
|
+
raise ValueError("Tools are required")
|
|
59
|
+
return ResolvedAgentSpec(
|
|
60
|
+
name=agent_spec.name,
|
|
61
|
+
system_prompt_path=agent_spec.system_prompt_path,
|
|
62
|
+
system_prompt_args=agent_spec.system_prompt_args,
|
|
63
|
+
tools=agent_spec.tools,
|
|
64
|
+
exclude_tools=agent_spec.exclude_tools or [],
|
|
65
|
+
subagents=agent_spec.subagents or {},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_agent_spec(agent_file: Path) -> AgentSpec:
|
|
70
|
+
assert agent_file.is_file(), "expect agent file to exist"
|
|
71
|
+
with open(agent_file, encoding="utf-8") as f:
|
|
72
|
+
data: dict[str, Any] = yaml.safe_load(f)
|
|
73
|
+
|
|
74
|
+
version = data.get("version", 1)
|
|
75
|
+
if version != 1:
|
|
76
|
+
raise ValueError(f"Unsupported agent spec version: {version}")
|
|
77
|
+
|
|
78
|
+
agent_spec = AgentSpec(**data.get("agent", {}))
|
|
79
|
+
if agent_spec.system_prompt_path is not None:
|
|
80
|
+
agent_spec.system_prompt_path = agent_file.parent / agent_spec.system_prompt_path
|
|
81
|
+
if agent_spec.subagents is not None:
|
|
82
|
+
for v in agent_spec.subagents.values():
|
|
83
|
+
v.path = agent_file.parent / v.path
|
|
84
|
+
if agent_spec.extend:
|
|
85
|
+
if agent_spec.extend == "default":
|
|
86
|
+
base_agent_file = DEFAULT_AGENT_FILE
|
|
87
|
+
else:
|
|
88
|
+
base_agent_file = agent_file.parent / agent_spec.extend
|
|
89
|
+
base_agent_spec = _load_agent_spec(base_agent_file)
|
|
90
|
+
if agent_spec.name is not None:
|
|
91
|
+
base_agent_spec.name = agent_spec.name
|
|
92
|
+
if agent_spec.system_prompt_path is not None:
|
|
93
|
+
base_agent_spec.system_prompt_path = agent_spec.system_prompt_path
|
|
94
|
+
for k, v in agent_spec.system_prompt_args.items():
|
|
95
|
+
# system prompt args should be merged instead of overwritten
|
|
96
|
+
base_agent_spec.system_prompt_args[k] = v
|
|
97
|
+
if agent_spec.tools is not None:
|
|
98
|
+
base_agent_spec.tools = agent_spec.tools
|
|
99
|
+
if agent_spec.exclude_tools is not None:
|
|
100
|
+
base_agent_spec.exclude_tools = agent_spec.exclude_tools
|
|
101
|
+
if agent_spec.subagents is not None:
|
|
102
|
+
base_agent_spec.subagents = agent_spec.subagents
|
|
103
|
+
agent_spec = base_agent_spec
|
|
104
|
+
return agent_spec
|