kimi-cli 0.41__py3-none-any.whl → 0.43__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 +28 -0
- kimi_cli/__init__.py +169 -102
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agentspec.py +19 -8
- kimi_cli/cli.py +51 -37
- kimi_cli/config.py +33 -14
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +31 -3
- kimi_cli/metadata.py +5 -68
- kimi_cli/session.py +81 -0
- kimi_cli/soul/__init__.py +22 -4
- kimi_cli/soul/agent.py +18 -23
- kimi_cli/soul/context.py +0 -5
- kimi_cli/soul/kimisoul.py +40 -25
- kimi_cli/soul/message.py +1 -1
- kimi_cli/soul/{globals.py → runtime.py} +13 -11
- kimi_cli/tools/file/glob.py +1 -1
- kimi_cli/tools/file/patch.py +1 -1
- kimi_cli/tools/file/read.py +1 -1
- kimi_cli/tools/file/replace.py +1 -1
- kimi_cli/tools/file/write.py +1 -1
- kimi_cli/tools/task/__init__.py +29 -21
- kimi_cli/tools/web/search.py +3 -0
- kimi_cli/ui/acp/__init__.py +24 -28
- kimi_cli/ui/print/__init__.py +27 -30
- kimi_cli/ui/shell/__init__.py +58 -42
- kimi_cli/ui/shell/keyboard.py +82 -14
- kimi_cli/ui/shell/metacmd.py +3 -8
- kimi_cli/ui/shell/prompt.py +208 -6
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/visualize.py +54 -57
- kimi_cli/utils/message.py +14 -0
- kimi_cli/utils/signals.py +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/wire/__init__.py +13 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/METADATA +21 -20
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/RECORD +41 -38
- kimi_cli/agents/koder/README.md +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/agents/{koder → default}/system.md +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/WHEEL +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/entry_points.txt +0 -0
kimi_cli/config.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import Literal, Self
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field, SecretStr, ValidationError, field_serializer, model_validator
|
|
6
6
|
|
|
7
|
+
from kimi_cli.exception import ConfigError
|
|
7
8
|
from kimi_cli.share import get_share_dir
|
|
8
9
|
from kimi_cli.utils.logging import logger
|
|
9
10
|
|
|
@@ -17,12 +18,17 @@ class LLMProvider(BaseModel):
|
|
|
17
18
|
"""API base URL"""
|
|
18
19
|
api_key: SecretStr
|
|
19
20
|
"""API key"""
|
|
21
|
+
custom_headers: dict[str, str] | None = None
|
|
22
|
+
"""Custom headers to include in API requests"""
|
|
20
23
|
|
|
21
24
|
@field_serializer("api_key", when_used="json")
|
|
22
25
|
def dump_secret(self, v: SecretStr):
|
|
23
26
|
return v.get_secret_value()
|
|
24
27
|
|
|
25
28
|
|
|
29
|
+
LLMModelCapability = Literal["image_in"]
|
|
30
|
+
|
|
31
|
+
|
|
26
32
|
class LLMModel(BaseModel):
|
|
27
33
|
"""LLM model configuration."""
|
|
28
34
|
|
|
@@ -32,6 +38,8 @@ class LLMModel(BaseModel):
|
|
|
32
38
|
"""Model name"""
|
|
33
39
|
max_context_size: int
|
|
34
40
|
"""Maximum context size (unit: tokens)"""
|
|
41
|
+
capabilities: set[LLMModelCapability] | None = None
|
|
42
|
+
"""Model capabilities"""
|
|
35
43
|
|
|
36
44
|
|
|
37
45
|
class LoopControl(BaseModel):
|
|
@@ -50,6 +58,8 @@ class MoonshotSearchConfig(BaseModel):
|
|
|
50
58
|
"""Base URL for Moonshot Search service."""
|
|
51
59
|
api_key: SecretStr
|
|
52
60
|
"""API key for Moonshot Search service."""
|
|
61
|
+
custom_headers: dict[str, str] | None = None
|
|
62
|
+
"""Custom headers to include in API requests."""
|
|
53
63
|
|
|
54
64
|
@field_serializer("api_key", when_used="json")
|
|
55
65
|
def dump_secret(self, v: SecretStr):
|
|
@@ -99,13 +109,21 @@ def get_default_config() -> Config:
|
|
|
99
109
|
)
|
|
100
110
|
|
|
101
111
|
|
|
102
|
-
def load_config() -> Config:
|
|
103
|
-
"""
|
|
112
|
+
def load_config(config_file: Path | None = None) -> Config:
|
|
113
|
+
"""
|
|
114
|
+
Load configuration from config file.
|
|
115
|
+
If the config file does not exist, create it with default configuration.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
config_file (Path | None): Path to the configuration file. If None, use default path.
|
|
104
119
|
|
|
105
120
|
Returns:
|
|
106
121
|
Validated Config object.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ConfigError: If the configuration file is invalid.
|
|
107
125
|
"""
|
|
108
|
-
config_file = get_config_file()
|
|
126
|
+
config_file = config_file or get_config_file()
|
|
109
127
|
logger.debug("Loading config from file: {file}", file=config_file)
|
|
110
128
|
|
|
111
129
|
if not config_file.exists():
|
|
@@ -119,20 +137,21 @@ def load_config() -> Config:
|
|
|
119
137
|
with open(config_file, encoding="utf-8") as f:
|
|
120
138
|
data = json.load(f)
|
|
121
139
|
return Config(**data)
|
|
122
|
-
except
|
|
123
|
-
raise ConfigError(f"Invalid configuration file: {
|
|
140
|
+
except json.JSONDecodeError as e:
|
|
141
|
+
raise ConfigError(f"Invalid JSON in configuration file: {e}") from e
|
|
142
|
+
except ValidationError as e:
|
|
143
|
+
raise ConfigError(f"Invalid configuration file: {e}") from e
|
|
124
144
|
|
|
125
145
|
|
|
126
|
-
|
|
127
|
-
"""
|
|
128
|
-
|
|
129
|
-
def __init__(self, message: str):
|
|
130
|
-
super().__init__(message)
|
|
131
|
-
|
|
146
|
+
def save_config(config: Config, config_file: Path | None = None):
|
|
147
|
+
"""
|
|
148
|
+
Save configuration to config file.
|
|
132
149
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
Args:
|
|
151
|
+
config (Config): Config object to save.
|
|
152
|
+
config_file (Path | None): Path to the configuration file. If None, use default path.
|
|
153
|
+
"""
|
|
154
|
+
config_file = config_file or get_config_file()
|
|
136
155
|
logger.debug("Saving config to file: {file}", file=config_file)
|
|
137
156
|
with open(config_file, "w", encoding="utf-8") as f:
|
|
138
157
|
f.write(config.model_dump_json(indent=2, exclude_none=True))
|
kimi_cli/exception.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class KimiCLIException(Exception):
|
|
2
|
+
"""Base exception class for Kimi CLI."""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConfigError(KimiCLIException):
|
|
8
|
+
"""Configuration error."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentSpecError(KimiCLIException):
|
|
14
|
+
"""Agent specification error."""
|
|
15
|
+
|
|
16
|
+
pass
|
kimi_cli/llm.py
CHANGED
|
@@ -7,26 +7,47 @@ from kosong.chat_provider.kimi import Kimi
|
|
|
7
7
|
from kosong.chat_provider.openai_legacy import OpenAILegacy
|
|
8
8
|
from pydantic import SecretStr
|
|
9
9
|
|
|
10
|
-
from kimi_cli.config import LLMModel, LLMProvider
|
|
10
|
+
from kimi_cli.config import LLMModel, LLMModelCapability, LLMProvider
|
|
11
11
|
from kimi_cli.constant import USER_AGENT
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class LLM(NamedTuple):
|
|
15
15
|
chat_provider: ChatProvider
|
|
16
16
|
max_context_size: int
|
|
17
|
+
capabilities: set[LLMModelCapability]
|
|
18
|
+
# TODO: these additional fields should be moved to ChatProvider
|
|
17
19
|
|
|
20
|
+
@property
|
|
21
|
+
def model_name(self) -> str:
|
|
22
|
+
return self.chat_provider.model_name
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def supports_image_in(self) -> bool:
|
|
26
|
+
return "image_in" in self.capabilities
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel) -> dict[str, str]:
|
|
30
|
+
"""Override provider/model settings from environment variables.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Mapping of environment variables that were applied.
|
|
34
|
+
"""
|
|
35
|
+
applied: dict[str, str] = {}
|
|
18
36
|
|
|
19
|
-
def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel):
|
|
20
37
|
match provider.type:
|
|
21
38
|
case "kimi":
|
|
22
39
|
if base_url := os.getenv("KIMI_BASE_URL"):
|
|
23
40
|
provider.base_url = base_url
|
|
41
|
+
applied["KIMI_BASE_URL"] = base_url
|
|
24
42
|
if api_key := os.getenv("KIMI_API_KEY"):
|
|
25
43
|
provider.api_key = SecretStr(api_key)
|
|
44
|
+
applied["KIMI_API_KEY"] = "******"
|
|
26
45
|
if model_name := os.getenv("KIMI_MODEL_NAME"):
|
|
27
46
|
model.model = model_name
|
|
47
|
+
applied["KIMI_MODEL_NAME"] = model.model
|
|
28
48
|
if max_context_size := os.getenv("KIMI_MODEL_MAX_CONTEXT_SIZE"):
|
|
29
49
|
model.max_context_size = int(max_context_size)
|
|
50
|
+
applied["KIMI_MODEL_MAX_CONTEXT_SIZE"] = str(model.max_context_size)
|
|
30
51
|
case "openai_legacy":
|
|
31
52
|
if base_url := os.getenv("OPENAI_BASE_URL"):
|
|
32
53
|
provider.base_url = base_url
|
|
@@ -35,6 +56,8 @@ def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel):
|
|
|
35
56
|
case _:
|
|
36
57
|
pass
|
|
37
58
|
|
|
59
|
+
return applied
|
|
60
|
+
|
|
38
61
|
|
|
39
62
|
def create_llm(
|
|
40
63
|
provider: LLMProvider,
|
|
@@ -52,6 +75,7 @@ def create_llm(
|
|
|
52
75
|
stream=stream,
|
|
53
76
|
default_headers={
|
|
54
77
|
"User-Agent": USER_AGENT,
|
|
78
|
+
**(provider.custom_headers or {}),
|
|
55
79
|
},
|
|
56
80
|
)
|
|
57
81
|
if session_id:
|
|
@@ -74,4 +98,8 @@ def create_llm(
|
|
|
74
98
|
),
|
|
75
99
|
)
|
|
76
100
|
|
|
77
|
-
return LLM(
|
|
101
|
+
return LLM(
|
|
102
|
+
chat_provider=chat_provider,
|
|
103
|
+
max_context_size=model.max_context_size,
|
|
104
|
+
capabilities=model.capabilities or set(),
|
|
105
|
+
)
|
kimi_cli/metadata.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import uuid
|
|
3
2
|
from hashlib import md5
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import NamedTuple
|
|
6
4
|
|
|
7
5
|
from pydantic import BaseModel, Field
|
|
8
6
|
|
|
@@ -33,10 +31,12 @@ class WorkDirMeta(BaseModel):
|
|
|
33
31
|
class Metadata(BaseModel):
|
|
34
32
|
"""Kimi metadata structure."""
|
|
35
33
|
|
|
36
|
-
work_dirs: list[WorkDirMeta] = Field(
|
|
34
|
+
work_dirs: list[WorkDirMeta] = Field(
|
|
35
|
+
default_factory=list[WorkDirMeta], description="Work directory list"
|
|
36
|
+
)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def
|
|
39
|
+
def load_metadata() -> Metadata:
|
|
40
40
|
metadata_file = get_metadata_file()
|
|
41
41
|
logger.debug("Loading metadata from file: {file}", file=metadata_file)
|
|
42
42
|
if not metadata_file.exists():
|
|
@@ -47,71 +47,8 @@ def _load_metadata() -> Metadata:
|
|
|
47
47
|
return Metadata(**data)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def
|
|
50
|
+
def save_metadata(metadata: Metadata):
|
|
51
51
|
metadata_file = get_metadata_file()
|
|
52
52
|
logger.debug("Saving metadata to file: {file}", file=metadata_file)
|
|
53
53
|
with open(metadata_file, "w", encoding="utf-8") as f:
|
|
54
54
|
json.dump(metadata.model_dump(), f, indent=2, ensure_ascii=False)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class Session(NamedTuple):
|
|
58
|
-
"""A session of a work directory."""
|
|
59
|
-
|
|
60
|
-
id: str
|
|
61
|
-
work_dir: WorkDirMeta
|
|
62
|
-
history_file: Path
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def new_session(work_dir: Path, _history_file: Path | None = None) -> Session:
|
|
66
|
-
"""Create a new session for a work directory."""
|
|
67
|
-
logger.debug("Creating new session for work directory: {work_dir}", work_dir=work_dir)
|
|
68
|
-
|
|
69
|
-
metadata = _load_metadata()
|
|
70
|
-
work_dir_meta = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
|
|
71
|
-
if work_dir_meta is None:
|
|
72
|
-
work_dir_meta = WorkDirMeta(path=str(work_dir))
|
|
73
|
-
metadata.work_dirs.append(work_dir_meta)
|
|
74
|
-
|
|
75
|
-
session_id = str(uuid.uuid4())
|
|
76
|
-
if _history_file is None:
|
|
77
|
-
history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
78
|
-
work_dir_meta.last_session_id = session_id
|
|
79
|
-
else:
|
|
80
|
-
logger.warning("Using provided history file: {history_file}", history_file=_history_file)
|
|
81
|
-
_history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
-
if _history_file.exists():
|
|
83
|
-
assert _history_file.is_file()
|
|
84
|
-
history_file = _history_file
|
|
85
|
-
|
|
86
|
-
if history_file.exists():
|
|
87
|
-
# truncate if exists
|
|
88
|
-
logger.warning(
|
|
89
|
-
"History file already exists, truncating: {history_file}", history_file=history_file
|
|
90
|
-
)
|
|
91
|
-
history_file.unlink()
|
|
92
|
-
history_file.touch()
|
|
93
|
-
|
|
94
|
-
_save_metadata(metadata)
|
|
95
|
-
return Session(id=session_id, work_dir=work_dir_meta, history_file=history_file)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def continue_session(work_dir: Path) -> Session | None:
|
|
99
|
-
"""Get the last session for a work directory."""
|
|
100
|
-
logger.debug("Continuing session for work directory: {work_dir}", work_dir=work_dir)
|
|
101
|
-
|
|
102
|
-
metadata = _load_metadata()
|
|
103
|
-
work_dir_meta = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
|
|
104
|
-
if work_dir_meta is None:
|
|
105
|
-
logger.debug("Work directory never been used")
|
|
106
|
-
return None
|
|
107
|
-
if work_dir_meta.last_session_id is None:
|
|
108
|
-
logger.debug("Work directory never had a session")
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
logger.debug(
|
|
112
|
-
"Found last session for work directory: {session_id}",
|
|
113
|
-
session_id=work_dir_meta.last_session_id,
|
|
114
|
-
)
|
|
115
|
-
session_id = work_dir_meta.last_session_id
|
|
116
|
-
history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
117
|
-
return Session(id=session_id, work_dir=work_dir_meta, history_file=history_file)
|
kimi_cli/session.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import NamedTuple
|
|
4
|
+
|
|
5
|
+
from kimi_cli.metadata import WorkDirMeta, load_metadata, save_metadata
|
|
6
|
+
from kimi_cli.utils.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Session(NamedTuple):
|
|
10
|
+
"""A session of a work directory."""
|
|
11
|
+
|
|
12
|
+
id: str
|
|
13
|
+
work_dir: Path
|
|
14
|
+
history_file: Path
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def create(work_dir: Path, _history_file: Path | None = None) -> "Session":
|
|
18
|
+
"""Create a new session for a work directory."""
|
|
19
|
+
logger.debug("Creating new session for work directory: {work_dir}", work_dir=work_dir)
|
|
20
|
+
|
|
21
|
+
metadata = load_metadata()
|
|
22
|
+
work_dir_meta = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
|
|
23
|
+
if work_dir_meta is None:
|
|
24
|
+
work_dir_meta = WorkDirMeta(path=str(work_dir))
|
|
25
|
+
metadata.work_dirs.append(work_dir_meta)
|
|
26
|
+
|
|
27
|
+
session_id = str(uuid.uuid4())
|
|
28
|
+
if _history_file is None:
|
|
29
|
+
history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
30
|
+
work_dir_meta.last_session_id = session_id
|
|
31
|
+
else:
|
|
32
|
+
logger.warning(
|
|
33
|
+
"Using provided history file: {history_file}", history_file=_history_file
|
|
34
|
+
)
|
|
35
|
+
_history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
if _history_file.exists():
|
|
37
|
+
assert _history_file.is_file()
|
|
38
|
+
history_file = _history_file
|
|
39
|
+
|
|
40
|
+
if history_file.exists():
|
|
41
|
+
# truncate if exists
|
|
42
|
+
logger.warning(
|
|
43
|
+
"History file already exists, truncating: {history_file}", history_file=history_file
|
|
44
|
+
)
|
|
45
|
+
history_file.unlink()
|
|
46
|
+
history_file.touch()
|
|
47
|
+
|
|
48
|
+
save_metadata(metadata)
|
|
49
|
+
|
|
50
|
+
return Session(
|
|
51
|
+
id=session_id,
|
|
52
|
+
work_dir=work_dir,
|
|
53
|
+
history_file=history_file,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def continue_(work_dir: Path) -> "Session | None":
|
|
58
|
+
"""Get the last session for a work directory."""
|
|
59
|
+
logger.debug("Continuing session for work directory: {work_dir}", work_dir=work_dir)
|
|
60
|
+
|
|
61
|
+
metadata = load_metadata()
|
|
62
|
+
work_dir_meta = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
|
|
63
|
+
if work_dir_meta is None:
|
|
64
|
+
logger.debug("Work directory never been used")
|
|
65
|
+
return None
|
|
66
|
+
if work_dir_meta.last_session_id is None:
|
|
67
|
+
logger.debug("Work directory never had a session")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
logger.debug(
|
|
71
|
+
"Found last session for work directory: {session_id}",
|
|
72
|
+
session_id=work_dir_meta.last_session_id,
|
|
73
|
+
)
|
|
74
|
+
session_id = work_dir_meta.last_session_id
|
|
75
|
+
history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
76
|
+
|
|
77
|
+
return Session(
|
|
78
|
+
id=session_id,
|
|
79
|
+
work_dir=work_dir,
|
|
80
|
+
history_file=history_file,
|
|
81
|
+
)
|
kimi_cli/soul/__init__.py
CHANGED
|
@@ -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
|
-
|
|
146
|
+
logger.debug("UI loop shut down")
|
|
129
147
|
pass
|
|
130
148
|
except TimeoutError:
|
|
131
149
|
logger.warning("UI loop timed out")
|
kimi_cli/soul/agent.py
CHANGED
|
@@ -9,10 +9,10 @@ from kosong.tooling import CallableTool, CallableTool2, Toolset
|
|
|
9
9
|
|
|
10
10
|
from kimi_cli.agentspec import ResolvedAgentSpec, load_agent_spec
|
|
11
11
|
from kimi_cli.config import Config
|
|
12
|
-
from kimi_cli.
|
|
12
|
+
from kimi_cli.session import Session
|
|
13
13
|
from kimi_cli.soul.approval import Approval
|
|
14
14
|
from kimi_cli.soul.denwarenji import DenwaRenji
|
|
15
|
-
from kimi_cli.soul.
|
|
15
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs, Runtime
|
|
16
16
|
from kimi_cli.soul.toolset import CustomToolset
|
|
17
17
|
from kimi_cli.tools.mcp import MCPTool
|
|
18
18
|
from kimi_cli.utils.logging import logger
|
|
@@ -26,27 +26,18 @@ class Agent(NamedTuple):
|
|
|
26
26
|
toolset: Toolset
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
async def
|
|
29
|
+
async def load_agent(
|
|
30
30
|
agent_file: Path,
|
|
31
|
-
|
|
31
|
+
runtime: Runtime,
|
|
32
|
+
*,
|
|
32
33
|
mcp_configs: list[dict[str, Any]],
|
|
33
|
-
) -> Agent:
|
|
34
|
-
agent = load_agent(agent_file, globals_)
|
|
35
|
-
assert isinstance(agent.toolset, CustomToolset)
|
|
36
|
-
if mcp_configs:
|
|
37
|
-
await _load_mcp_tools(agent.toolset, mcp_configs)
|
|
38
|
-
return agent
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def load_agent(
|
|
42
|
-
agent_file: Path,
|
|
43
|
-
globals_: AgentGlobals,
|
|
44
34
|
) -> Agent:
|
|
45
35
|
"""
|
|
46
36
|
Load agent from specification file.
|
|
47
37
|
|
|
48
38
|
Raises:
|
|
49
|
-
|
|
39
|
+
FileNotFoundError: If the agent spec file does not exist.
|
|
40
|
+
AgentSpecError: If the agent spec is not valid.
|
|
50
41
|
"""
|
|
51
42
|
logger.info("Loading agent: {agent_file}", agent_file=agent_file)
|
|
52
43
|
agent_spec = load_agent_spec(agent_file)
|
|
@@ -54,17 +45,17 @@ def load_agent(
|
|
|
54
45
|
system_prompt = _load_system_prompt(
|
|
55
46
|
agent_spec.system_prompt_path,
|
|
56
47
|
agent_spec.system_prompt_args,
|
|
57
|
-
|
|
48
|
+
runtime.builtin_args,
|
|
58
49
|
)
|
|
59
50
|
|
|
60
51
|
tool_deps = {
|
|
61
52
|
ResolvedAgentSpec: agent_spec,
|
|
62
|
-
|
|
63
|
-
Config:
|
|
64
|
-
BuiltinSystemPromptArgs:
|
|
65
|
-
Session:
|
|
66
|
-
DenwaRenji:
|
|
67
|
-
Approval:
|
|
53
|
+
Runtime: runtime,
|
|
54
|
+
Config: runtime.config,
|
|
55
|
+
BuiltinSystemPromptArgs: runtime.builtin_args,
|
|
56
|
+
Session: runtime.session,
|
|
57
|
+
DenwaRenji: runtime.denwa_renji,
|
|
58
|
+
Approval: runtime.approval,
|
|
68
59
|
}
|
|
69
60
|
tools = agent_spec.tools
|
|
70
61
|
if agent_spec.exclude_tools:
|
|
@@ -75,6 +66,10 @@ def load_agent(
|
|
|
75
66
|
if bad_tools:
|
|
76
67
|
raise ValueError(f"Invalid tools: {bad_tools}")
|
|
77
68
|
|
|
69
|
+
assert isinstance(toolset, CustomToolset)
|
|
70
|
+
if mcp_configs:
|
|
71
|
+
await _load_mcp_tools(toolset, mcp_configs)
|
|
72
|
+
|
|
78
73
|
return Agent(
|
|
79
74
|
name=agent_spec.name,
|
|
80
75
|
system_prompt=system_prompt,
|
kimi_cli/soul/context.py
CHANGED
|
@@ -19,11 +19,6 @@ class Context:
|
|
|
19
19
|
self._next_checkpoint_id: int = 0
|
|
20
20
|
"""The ID of the next checkpoint, starting from 0, incremented after each checkpoint."""
|
|
21
21
|
|
|
22
|
-
@property
|
|
23
|
-
def file_backend(self) -> Path:
|
|
24
|
-
"""The JSONL file backend of the context."""
|
|
25
|
-
return self._file_backend
|
|
26
|
-
|
|
27
22
|
async def restore(self) -> bool:
|
|
28
23
|
logger.debug("Restoring context from file: {file_backend}", file_backend=self._file_backend)
|
|
29
24
|
if self._history:
|