kimi-cli 0.41__py3-none-any.whl → 0.42__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 +15 -0
- kimi_cli/__init__.py +131 -101
- 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 +28 -14
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +1 -0
- kimi_cli/metadata.py +5 -68
- kimi_cli/session.py +81 -0
- kimi_cli/soul/agent.py +18 -23
- kimi_cli/soul/context.py +0 -5
- kimi_cli/soul/kimisoul.py +19 -21
- kimi_cli/soul/{globals.py → runtime.py} +12 -10
- 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 +28 -21
- kimi_cli/tools/web/search.py +3 -0
- kimi_cli/ui/print/__init__.py +4 -2
- kimi_cli/ui/shell/__init__.py +3 -6
- kimi_cli/ui/shell/metacmd.py +3 -8
- kimi_cli/ui/shell/prompt.py +10 -3
- kimi_cli/wire/__init__.py +12 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.42.dist-info}/METADATA +18 -18
- {kimi_cli-0.41.dist-info → kimi_cli-0.42.dist-info}/RECORD +32 -31
- 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.42.dist-info}/WHEEL +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.42.dist-info}/entry_points.txt +0 -0
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/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:
|
kimi_cli/soul/kimisoul.py
CHANGED
|
@@ -16,13 +16,12 @@ 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.config import LoopControl
|
|
20
19
|
from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul, StatusSnapshot, wire_send
|
|
21
20
|
from kimi_cli.soul.agent import Agent
|
|
22
21
|
from kimi_cli.soul.compaction import SimpleCompaction
|
|
23
22
|
from kimi_cli.soul.context import Context
|
|
24
|
-
from kimi_cli.soul.globals import AgentGlobals
|
|
25
23
|
from kimi_cli.soul.message import system, tool_result_to_messages
|
|
24
|
+
from kimi_cli.soul.runtime import Runtime
|
|
26
25
|
from kimi_cli.tools.dmail import NAME as SendDMail_NAME
|
|
27
26
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
28
27
|
from kimi_cli.utils.logging import logger
|
|
@@ -43,30 +42,29 @@ class KimiSoul(Soul):
|
|
|
43
42
|
def __init__(
|
|
44
43
|
self,
|
|
45
44
|
agent: Agent,
|
|
46
|
-
|
|
45
|
+
runtime: Runtime,
|
|
47
46
|
*,
|
|
48
47
|
context: Context,
|
|
49
|
-
loop_control: LoopControl,
|
|
50
48
|
):
|
|
51
49
|
"""
|
|
52
50
|
Initialize the soul.
|
|
53
51
|
|
|
54
52
|
Args:
|
|
55
53
|
agent (Agent): The agent to run.
|
|
56
|
-
|
|
54
|
+
runtime (Runtime): Runtime parameters and states.
|
|
57
55
|
context (Context): The context of the agent.
|
|
58
56
|
loop_control (LoopControl): The control parameters for the agent loop.
|
|
59
57
|
"""
|
|
60
58
|
self._agent = agent
|
|
61
|
-
self.
|
|
62
|
-
self._denwa_renji =
|
|
63
|
-
self._approval =
|
|
59
|
+
self._runtime = runtime
|
|
60
|
+
self._denwa_renji = runtime.denwa_renji
|
|
61
|
+
self._approval = runtime.approval
|
|
64
62
|
self._context = context
|
|
65
|
-
self._loop_control = loop_control
|
|
63
|
+
self._loop_control = runtime.config.loop_control
|
|
66
64
|
self._compaction = SimpleCompaction() # TODO: maybe configurable and composable
|
|
67
65
|
self._reserved_tokens = RESERVED_TOKENS
|
|
68
|
-
if self.
|
|
69
|
-
assert self._reserved_tokens <= self.
|
|
66
|
+
if self._runtime.llm is not None:
|
|
67
|
+
assert self._reserved_tokens <= self._runtime.llm.max_context_size
|
|
70
68
|
|
|
71
69
|
for tool in agent.toolset.tools:
|
|
72
70
|
if tool.name == SendDMail_NAME:
|
|
@@ -81,7 +79,7 @@ class KimiSoul(Soul):
|
|
|
81
79
|
|
|
82
80
|
@property
|
|
83
81
|
def model(self) -> str:
|
|
84
|
-
return self.
|
|
82
|
+
return self._runtime.llm.chat_provider.model_name if self._runtime.llm else ""
|
|
85
83
|
|
|
86
84
|
@property
|
|
87
85
|
def status(self) -> StatusSnapshot:
|
|
@@ -89,15 +87,15 @@ class KimiSoul(Soul):
|
|
|
89
87
|
|
|
90
88
|
@property
|
|
91
89
|
def _context_usage(self) -> float:
|
|
92
|
-
if self.
|
|
93
|
-
return self._context.token_count / self.
|
|
90
|
+
if self._runtime.llm is not None:
|
|
91
|
+
return self._context.token_count / self._runtime.llm.max_context_size
|
|
94
92
|
return 0.0
|
|
95
93
|
|
|
96
94
|
async def _checkpoint(self):
|
|
97
95
|
await self._context.checkpoint(self._checkpoint_with_user_message)
|
|
98
96
|
|
|
99
97
|
async def run(self, user_input: str):
|
|
100
|
-
if self.
|
|
98
|
+
if self._runtime.llm is None:
|
|
101
99
|
raise LLMNotSet()
|
|
102
100
|
|
|
103
101
|
await self._checkpoint() # this creates the checkpoint 0 on first run
|
|
@@ -107,7 +105,7 @@ class KimiSoul(Soul):
|
|
|
107
105
|
|
|
108
106
|
async def _agent_loop(self):
|
|
109
107
|
"""The main agent loop for one run."""
|
|
110
|
-
assert self.
|
|
108
|
+
assert self._runtime.llm is not None
|
|
111
109
|
|
|
112
110
|
async def _pipe_approval_to_wire():
|
|
113
111
|
while True:
|
|
@@ -126,7 +124,7 @@ class KimiSoul(Soul):
|
|
|
126
124
|
# compact the context if needed
|
|
127
125
|
if (
|
|
128
126
|
self._context.token_count + self._reserved_tokens
|
|
129
|
-
>= self.
|
|
127
|
+
>= self._runtime.llm.max_context_size
|
|
130
128
|
):
|
|
131
129
|
logger.info("Context too long, compacting...")
|
|
132
130
|
wire_send(CompactionBegin())
|
|
@@ -159,8 +157,8 @@ class KimiSoul(Soul):
|
|
|
159
157
|
async def _step(self) -> bool:
|
|
160
158
|
"""Run an single step and return whether the run should be stopped."""
|
|
161
159
|
# already checked in `run`
|
|
162
|
-
assert self.
|
|
163
|
-
chat_provider = self.
|
|
160
|
+
assert self._runtime.llm is not None
|
|
161
|
+
chat_provider = self._runtime.llm.chat_provider
|
|
164
162
|
|
|
165
163
|
@tenacity.retry(
|
|
166
164
|
retry=retry_if_exception(self._is_retryable_error),
|
|
@@ -255,9 +253,9 @@ class KimiSoul(Soul):
|
|
|
255
253
|
reraise=True,
|
|
256
254
|
)
|
|
257
255
|
async def _compact_with_retry() -> Sequence[Message]:
|
|
258
|
-
if self.
|
|
256
|
+
if self._runtime.llm is None:
|
|
259
257
|
raise LLMNotSet()
|
|
260
|
-
return await self._compaction.compact(self._context.history, self.
|
|
258
|
+
return await self._compaction.compact(self._context.history, self._runtime.llm)
|
|
261
259
|
|
|
262
260
|
compacted_messages = await _compact_with_retry()
|
|
263
261
|
await self._context.revert_to(0)
|
|
@@ -6,7 +6,7 @@ from typing import NamedTuple
|
|
|
6
6
|
|
|
7
7
|
from kimi_cli.config import Config
|
|
8
8
|
from kimi_cli.llm import LLM
|
|
9
|
-
from kimi_cli.
|
|
9
|
+
from kimi_cli.session import Session
|
|
10
10
|
from kimi_cli.soul.approval import Approval
|
|
11
11
|
from kimi_cli.soul.denwarenji import DenwaRenji
|
|
12
12
|
from kimi_cli.utils.logging import logger
|
|
@@ -58,7 +58,7 @@ def _list_work_dir(work_dir: Path) -> str:
|
|
|
58
58
|
return ls.stdout.strip()
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
class
|
|
61
|
+
class Runtime(NamedTuple):
|
|
62
62
|
"""Agent globals."""
|
|
63
63
|
|
|
64
64
|
config: Config
|
|
@@ -68,22 +68,24 @@ class AgentGlobals(NamedTuple):
|
|
|
68
68
|
denwa_renji: DenwaRenji
|
|
69
69
|
approval: Approval
|
|
70
70
|
|
|
71
|
-
@
|
|
71
|
+
@staticmethod
|
|
72
72
|
async def create(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
config: Config,
|
|
74
|
+
llm: LLM | None,
|
|
75
|
+
session: Session,
|
|
76
|
+
yolo: bool,
|
|
77
|
+
) -> "Runtime":
|
|
76
78
|
# FIXME: do these asynchronously
|
|
77
|
-
ls_output = _list_work_dir(work_dir)
|
|
78
|
-
agents_md = load_agents_md(work_dir) or ""
|
|
79
|
+
ls_output = _list_work_dir(session.work_dir)
|
|
80
|
+
agents_md = load_agents_md(session.work_dir) or ""
|
|
79
81
|
|
|
80
|
-
return
|
|
82
|
+
return Runtime(
|
|
81
83
|
config=config,
|
|
82
84
|
llm=llm,
|
|
83
85
|
session=session,
|
|
84
86
|
builtin_args=BuiltinSystemPromptArgs(
|
|
85
87
|
KIMI_NOW=datetime.now().astimezone().isoformat(),
|
|
86
|
-
KIMI_WORK_DIR=work_dir,
|
|
88
|
+
KIMI_WORK_DIR=session.work_dir,
|
|
87
89
|
KIMI_WORK_DIR_LS=ls_output,
|
|
88
90
|
KIMI_AGENTS_MD=agents_md,
|
|
89
91
|
),
|
kimi_cli/tools/file/glob.py
CHANGED
|
@@ -8,7 +8,7 @@ import aiofiles.os
|
|
|
8
8
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
|
-
from kimi_cli.soul.
|
|
11
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs
|
|
12
12
|
from kimi_cli.tools.utils import load_desc
|
|
13
13
|
|
|
14
14
|
MAX_MATCHES = 1000
|
kimi_cli/tools/file/patch.py
CHANGED
|
@@ -7,7 +7,7 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
from kimi_cli.soul.approval import Approval
|
|
10
|
-
from kimi_cli.soul.
|
|
10
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs
|
|
11
11
|
from kimi_cli.tools.file import FileActions
|
|
12
12
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
13
13
|
|
kimi_cli/tools/file/read.py
CHANGED
|
@@ -5,7 +5,7 @@ import aiofiles
|
|
|
5
5
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
-
from kimi_cli.soul.
|
|
8
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs
|
|
9
9
|
from kimi_cli.tools.utils import load_desc, truncate_line
|
|
10
10
|
|
|
11
11
|
MAX_LINES = 1000
|
kimi_cli/tools/file/replace.py
CHANGED
|
@@ -6,7 +6,7 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
from kimi_cli.soul.approval import Approval
|
|
9
|
-
from kimi_cli.soul.
|
|
9
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs
|
|
10
10
|
from kimi_cli.tools.file import FileActions
|
|
11
11
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
12
12
|
|
kimi_cli/tools/file/write.py
CHANGED
|
@@ -6,7 +6,7 @@ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
from kimi_cli.soul.approval import Approval
|
|
9
|
-
from kimi_cli.soul.
|
|
9
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs
|
|
10
10
|
from kimi_cli.tools.file import FileActions
|
|
11
11
|
from kimi_cli.tools.utils import ToolRejectedError
|
|
12
12
|
|
kimi_cli/tools/task/__init__.py
CHANGED
|
@@ -5,12 +5,12 @@ from typing import override
|
|
|
5
5
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
-
from kimi_cli.agentspec import ResolvedAgentSpec
|
|
8
|
+
from kimi_cli.agentspec import ResolvedAgentSpec, SubagentSpec
|
|
9
9
|
from kimi_cli.soul import MaxStepsReached, get_wire_or_none, run_soul
|
|
10
10
|
from kimi_cli.soul.agent import Agent, load_agent
|
|
11
11
|
from kimi_cli.soul.context import Context
|
|
12
|
-
from kimi_cli.soul.globals import AgentGlobals
|
|
13
12
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
13
|
+
from kimi_cli.soul.runtime import Runtime
|
|
14
14
|
from kimi_cli.tools.utils import load_desc
|
|
15
15
|
from kimi_cli.utils.message import message_extract_text
|
|
16
16
|
from kimi_cli.utils.path import next_available_rotation
|
|
@@ -49,28 +49,36 @@ class Task(CallableTool2[Params]):
|
|
|
49
49
|
name: str = "Task"
|
|
50
50
|
params: type[Params] = Params
|
|
51
51
|
|
|
52
|
-
def __init__(self, agent_spec: ResolvedAgentSpec,
|
|
53
|
-
subagents: dict[str, Agent] = {}
|
|
54
|
-
descs = []
|
|
55
|
-
|
|
56
|
-
# load all subagents
|
|
57
|
-
for name, spec in agent_spec.subagents.items():
|
|
58
|
-
subagents[name] = load_agent(spec.path, agent_globals)
|
|
59
|
-
descs.append(f"- `{name}`: {spec.description}")
|
|
60
|
-
|
|
52
|
+
def __init__(self, agent_spec: ResolvedAgentSpec, runtime: Runtime, **kwargs):
|
|
61
53
|
super().__init__(
|
|
62
54
|
description=load_desc(
|
|
63
55
|
Path(__file__).parent / "task.md",
|
|
64
56
|
{
|
|
65
|
-
"SUBAGENTS_MD": "\n".join(
|
|
57
|
+
"SUBAGENTS_MD": "\n".join(
|
|
58
|
+
f"- `{name}`: {spec.description}"
|
|
59
|
+
for name, spec in agent_spec.subagents.items()
|
|
60
|
+
),
|
|
66
61
|
},
|
|
67
62
|
),
|
|
68
63
|
**kwargs,
|
|
69
64
|
)
|
|
70
65
|
|
|
71
|
-
self.
|
|
72
|
-
self._session =
|
|
73
|
-
self._subagents =
|
|
66
|
+
self._runtime = runtime
|
|
67
|
+
self._session = runtime.session
|
|
68
|
+
self._subagents: dict[str, Agent] = {}
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
self._load_task = asyncio.create_task(self._load_subagents(agent_spec.subagents))
|
|
72
|
+
except RuntimeError:
|
|
73
|
+
# In case there's no running event loop, e.g., during synchronous tests
|
|
74
|
+
self._load_task = None
|
|
75
|
+
asyncio.run(self._load_subagents(agent_spec.subagents))
|
|
76
|
+
|
|
77
|
+
async def _load_subagents(self, subagent_specs: dict[str, SubagentSpec]) -> None:
|
|
78
|
+
"""Load all subagents specified in the agent spec."""
|
|
79
|
+
for name, spec in subagent_specs.items():
|
|
80
|
+
agent = await load_agent(spec.path, self._runtime, mcp_configs=[])
|
|
81
|
+
self._subagents[name] = agent
|
|
74
82
|
|
|
75
83
|
async def _get_subagent_history_file(self) -> Path:
|
|
76
84
|
"""Generate a unique history file path for subagent."""
|
|
@@ -85,6 +93,10 @@ class Task(CallableTool2[Params]):
|
|
|
85
93
|
|
|
86
94
|
@override
|
|
87
95
|
async def __call__(self, params: Params) -> ToolReturnType:
|
|
96
|
+
if self._load_task is not None:
|
|
97
|
+
await self._load_task
|
|
98
|
+
self._load_task = None
|
|
99
|
+
|
|
88
100
|
if params.subagent_name not in self._subagents:
|
|
89
101
|
return ToolError(
|
|
90
102
|
message=f"Subagent not found: {params.subagent_name}",
|
|
@@ -117,12 +129,7 @@ class Task(CallableTool2[Params]):
|
|
|
117
129
|
|
|
118
130
|
subagent_history_file = await self._get_subagent_history_file()
|
|
119
131
|
context = Context(file_backend=subagent_history_file)
|
|
120
|
-
soul = KimiSoul(
|
|
121
|
-
agent,
|
|
122
|
-
agent_globals=self._agent_globals,
|
|
123
|
-
context=context,
|
|
124
|
-
loop_control=self._agent_globals.config.loop_control,
|
|
125
|
-
)
|
|
132
|
+
soul = KimiSoul(agent, runtime=self._runtime, context=context)
|
|
126
133
|
|
|
127
134
|
try:
|
|
128
135
|
await run_soul(soul, prompt, _ui_loop_fn, asyncio.Event())
|
kimi_cli/tools/web/search.py
CHANGED
|
@@ -44,9 +44,11 @@ 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
48
|
else:
|
|
48
49
|
self._base_url = ""
|
|
49
50
|
self._api_key = ""
|
|
51
|
+
self._custom_headers = {}
|
|
50
52
|
|
|
51
53
|
@override
|
|
52
54
|
async def __call__(self, params: Params) -> ToolReturnType:
|
|
@@ -69,6 +71,7 @@ class SearchWeb(CallableTool2[Params]):
|
|
|
69
71
|
"User-Agent": USER_AGENT,
|
|
70
72
|
"Authorization": f"Bearer {self._api_key}",
|
|
71
73
|
"X-Msh-Tool-Call-Id": tool_call.id,
|
|
74
|
+
**self._custom_headers,
|
|
72
75
|
},
|
|
73
76
|
json={
|
|
74
77
|
"text_query": params.query,
|
kimi_cli/ui/print/__init__.py
CHANGED
|
@@ -138,13 +138,15 @@ class PrintApp:
|
|
|
138
138
|
|
|
139
139
|
async def _visualize_stream_json(self, wire: WireUISide, start_position: int):
|
|
140
140
|
# TODO: be aware of context compaction
|
|
141
|
+
# FIXME: this is only a temporary impl, may miss the last lines of the context file
|
|
142
|
+
if not self.context_file.exists():
|
|
143
|
+
self.context_file.touch()
|
|
141
144
|
try:
|
|
142
145
|
async with aiofiles.open(self.context_file, encoding="utf-8") as f:
|
|
143
146
|
await f.seek(start_position)
|
|
144
147
|
while True:
|
|
145
148
|
should_end = False
|
|
146
|
-
while wire.
|
|
147
|
-
msg = wire._queue.get_nowait()
|
|
149
|
+
while (msg := wire.receive_nowait()) is not None:
|
|
148
150
|
if isinstance(msg, StepInterrupted):
|
|
149
151
|
should_end = True
|
|
150
152
|
|
kimi_cli/ui/shell/__init__.py
CHANGED
|
@@ -31,7 +31,7 @@ class ShellApp:
|
|
|
31
31
|
logger.info("Running agent with command: {command}", command=command)
|
|
32
32
|
return await self._run_soul_command(command)
|
|
33
33
|
|
|
34
|
-
self.
|
|
34
|
+
self._start_background_task(self._auto_update())
|
|
35
35
|
|
|
36
36
|
_print_welcome_info(self.soul.name or "Kimi CLI", self.soul.model, self.welcome_info)
|
|
37
37
|
|
|
@@ -191,10 +191,7 @@ class ShellApp:
|
|
|
191
191
|
loop.remove_signal_handler(signal.SIGINT)
|
|
192
192
|
return False
|
|
193
193
|
|
|
194
|
-
def
|
|
195
|
-
self._add_background_task(self._auto_update_background())
|
|
196
|
-
|
|
197
|
-
async def _auto_update_background(self) -> None:
|
|
194
|
+
async def _auto_update(self) -> None:
|
|
198
195
|
toast("checking for updates...", duration=2.0)
|
|
199
196
|
result = await do_update(print=False, check_only=True)
|
|
200
197
|
if result == UpdateResult.UPDATE_AVAILABLE:
|
|
@@ -204,7 +201,7 @@ class ShellApp:
|
|
|
204
201
|
elif result == UpdateResult.UPDATED:
|
|
205
202
|
toast("auto updated, restart to use the new version", duration=5.0)
|
|
206
203
|
|
|
207
|
-
def
|
|
204
|
+
def _start_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
|
|
208
205
|
task = asyncio.create_task(coro)
|
|
209
206
|
self._background_tasks.add(task)
|
|
210
207
|
|