kimi-cli 0.44__py3-none-any.whl → 0.78__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 +349 -40
- kimi_cli/__init__.py +6 -0
- kimi_cli/acp/AGENTS.md +91 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +111 -0
- kimi_cli/acp/kaos.py +270 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +335 -0
- kimi_cli/acp/session.py +445 -0
- kimi_cli/acp/tools.py +158 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/agents/default/agent.yaml +4 -4
- kimi_cli/agents/default/sub.yaml +2 -1
- kimi_cli/agents/default/system.md +79 -21
- kimi_cli/agents/okabe/agent.yaml +17 -0
- kimi_cli/agentspec.py +53 -25
- kimi_cli/app.py +180 -52
- kimi_cli/cli/__init__.py +595 -0
- kimi_cli/cli/__main__.py +8 -0
- kimi_cli/cli/info.py +63 -0
- kimi_cli/cli/mcp.py +349 -0
- kimi_cli/config.py +153 -17
- kimi_cli/constant.py +3 -0
- kimi_cli/exception.py +23 -2
- kimi_cli/flow/__init__.py +117 -0
- kimi_cli/flow/d2.py +376 -0
- kimi_cli/flow/mermaid.py +218 -0
- kimi_cli/llm.py +129 -23
- kimi_cli/metadata.py +32 -7
- kimi_cli/platforms.py +262 -0
- kimi_cli/prompts/__init__.py +2 -0
- kimi_cli/prompts/compact.md +4 -5
- kimi_cli/session.py +223 -31
- kimi_cli/share.py +2 -0
- kimi_cli/skill.py +145 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +351 -0
- kimi_cli/soul/__init__.py +51 -20
- kimi_cli/soul/agent.py +213 -85
- kimi_cli/soul/approval.py +86 -17
- kimi_cli/soul/compaction.py +64 -53
- kimi_cli/soul/context.py +38 -5
- kimi_cli/soul/denwarenji.py +2 -0
- kimi_cli/soul/kimisoul.py +442 -60
- kimi_cli/soul/message.py +54 -54
- kimi_cli/soul/slash.py +72 -0
- kimi_cli/soul/toolset.py +387 -6
- kimi_cli/toad.py +74 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +42 -34
- kimi_cli/tools/display.py +25 -0
- kimi_cli/tools/dmail/__init__.py +10 -10
- kimi_cli/tools/dmail/dmail.md +11 -9
- kimi_cli/tools/file/__init__.py +1 -3
- kimi_cli/tools/file/glob.py +20 -23
- kimi_cli/tools/file/grep.md +1 -1
- kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
- kimi_cli/tools/file/read.md +24 -6
- kimi_cli/tools/file/read.py +134 -50
- kimi_cli/tools/file/replace.md +1 -1
- kimi_cli/tools/file/replace.py +36 -29
- kimi_cli/tools/file/utils.py +282 -0
- kimi_cli/tools/file/write.py +43 -22
- kimi_cli/tools/multiagent/__init__.py +7 -0
- kimi_cli/tools/multiagent/create.md +11 -0
- kimi_cli/tools/multiagent/create.py +50 -0
- kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
- kimi_cli/tools/shell/__init__.py +120 -0
- kimi_cli/tools/{bash → shell}/bash.md +1 -2
- kimi_cli/tools/shell/powershell.md +25 -0
- kimi_cli/tools/test.py +4 -4
- kimi_cli/tools/think/__init__.py +2 -2
- kimi_cli/tools/todo/__init__.py +14 -8
- kimi_cli/tools/utils.py +64 -24
- kimi_cli/tools/web/fetch.py +68 -13
- kimi_cli/tools/web/search.py +10 -12
- kimi_cli/ui/acp/__init__.py +65 -412
- kimi_cli/ui/print/__init__.py +37 -49
- kimi_cli/ui/print/visualize.py +179 -0
- kimi_cli/ui/shell/__init__.py +141 -84
- kimi_cli/ui/shell/console.py +2 -0
- kimi_cli/ui/shell/debug.py +28 -23
- kimi_cli/ui/shell/keyboard.py +5 -1
- kimi_cli/ui/shell/prompt.py +220 -194
- kimi_cli/ui/shell/replay.py +111 -46
- kimi_cli/ui/shell/setup.py +89 -82
- kimi_cli/ui/shell/slash.py +422 -0
- kimi_cli/ui/shell/update.py +4 -2
- kimi_cli/ui/shell/usage.py +271 -0
- kimi_cli/ui/shell/visualize.py +574 -72
- kimi_cli/ui/wire/__init__.py +267 -0
- kimi_cli/ui/wire/jsonrpc.py +142 -0
- kimi_cli/ui/wire/protocol.py +1 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +2 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +12 -7
- kimi_cli/utils/clipboard.py +12 -0
- kimi_cli/utils/datetime.py +37 -0
- kimi_cli/utils/environment.py +58 -0
- kimi_cli/utils/envvar.py +12 -0
- kimi_cli/utils/frontmatter.py +44 -0
- kimi_cli/utils/logging.py +7 -6
- kimi_cli/utils/message.py +9 -14
- kimi_cli/utils/path.py +99 -9
- kimi_cli/utils/pyinstaller.py +6 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/markdown.py +961 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +2 -0
- kimi_cli/utils/slashcmd.py +124 -0
- kimi_cli/utils/string.py +2 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/wire/__init__.py +98 -29
- kimi_cli/wire/serde.py +45 -0
- kimi_cli/wire/types.py +299 -0
- kimi_cli-0.78.dist-info/METADATA +200 -0
- kimi_cli-0.78.dist-info/RECORD +135 -0
- kimi_cli-0.78.dist-info/entry_points.txt +4 -0
- kimi_cli/cli.py +0 -250
- kimi_cli/soul/runtime.py +0 -96
- kimi_cli/tools/bash/__init__.py +0 -99
- kimi_cli/tools/file/patch.md +0 -8
- kimi_cli/tools/file/patch.py +0 -143
- kimi_cli/tools/mcp.py +0 -85
- kimi_cli/ui/shell/liveview.py +0 -386
- kimi_cli/ui/shell/metacmd.py +0 -262
- kimi_cli/wire/message.py +0 -91
- kimi_cli-0.44.dist-info/METADATA +0 -188
- kimi_cli-0.44.dist-info/RECORD +0 -89
- kimi_cli-0.44.dist-info/entry_points.txt +0 -3
- /kimi_cli/tools/{task → multiagent}/task.md +0 -0
- {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
kimi_cli/tools/file/write.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Literal, override
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
from kosong.tooling import CallableTool2, ToolError,
|
|
4
|
+
from kaos.path import KaosPath
|
|
5
|
+
from kosong.tooling import CallableTool2, ToolError, ToolReturnValue
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
+
from kimi_cli.soul.agent import BuiltinSystemPromptArgs
|
|
8
9
|
from kimi_cli.soul.approval import Approval
|
|
9
|
-
from kimi_cli.
|
|
10
|
+
from kimi_cli.tools.display import DisplayBlock
|
|
10
11
|
from kimi_cli.tools.file import FileActions
|
|
11
|
-
from kimi_cli.tools.utils import
|
|
12
|
+
from kimi_cli.tools.file.utils import build_diff_blocks
|
|
13
|
+
from kimi_cli.tools.utils import ToolRejectedError, load_desc
|
|
14
|
+
from kimi_cli.utils.path import is_within_directory
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class Params(BaseModel):
|
|
@@ -26,22 +29,21 @@ class Params(BaseModel):
|
|
|
26
29
|
|
|
27
30
|
class WriteFile(CallableTool2[Params]):
|
|
28
31
|
name: str = "WriteFile"
|
|
29
|
-
description: str = (Path(__file__).parent / "write.md")
|
|
32
|
+
description: str = load_desc(Path(__file__).parent / "write.md")
|
|
30
33
|
params: type[Params] = Params
|
|
31
34
|
|
|
32
|
-
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval
|
|
33
|
-
super().__init__(
|
|
35
|
+
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval):
|
|
36
|
+
super().__init__()
|
|
34
37
|
self._work_dir = builtin_args.KIMI_WORK_DIR
|
|
35
38
|
self._approval = approval
|
|
36
39
|
|
|
37
|
-
def _validate_path(self, path:
|
|
40
|
+
async def _validate_path(self, path: KaosPath) -> ToolError | None:
|
|
38
41
|
"""Validate that the path is safe to write."""
|
|
39
42
|
# Check for path traversal attempts
|
|
40
|
-
resolved_path = path.
|
|
41
|
-
resolved_work_dir = self._work_dir.resolve()
|
|
43
|
+
resolved_path = path.canonical()
|
|
42
44
|
|
|
43
45
|
# Ensure the path is within work directory
|
|
44
|
-
if not
|
|
46
|
+
if not is_within_directory(resolved_path, self._work_dir):
|
|
45
47
|
return ToolError(
|
|
46
48
|
message=(
|
|
47
49
|
f"`{path}` is outside the working directory. "
|
|
@@ -52,12 +54,12 @@ class WriteFile(CallableTool2[Params]):
|
|
|
52
54
|
return None
|
|
53
55
|
|
|
54
56
|
@override
|
|
55
|
-
async def __call__(self, params: Params) ->
|
|
57
|
+
async def __call__(self, params: Params) -> ToolReturnValue:
|
|
56
58
|
# TODO: checks:
|
|
57
59
|
# - check if the path may contain secrets
|
|
58
60
|
# - check if the file format is writable
|
|
59
61
|
try:
|
|
60
|
-
p =
|
|
62
|
+
p = KaosPath(params.path)
|
|
61
63
|
|
|
62
64
|
if not p.is_absolute():
|
|
63
65
|
return ToolError(
|
|
@@ -69,11 +71,11 @@ class WriteFile(CallableTool2[Params]):
|
|
|
69
71
|
)
|
|
70
72
|
|
|
71
73
|
# Validate path safety
|
|
72
|
-
path_error = self._validate_path(p)
|
|
74
|
+
path_error = await self._validate_path(p)
|
|
73
75
|
if path_error:
|
|
74
76
|
return path_error
|
|
75
77
|
|
|
76
|
-
if not p.parent.exists():
|
|
78
|
+
if not await p.parent.exists():
|
|
77
79
|
return ToolError(
|
|
78
80
|
message=f"`{params.path}` parent directory does not exist.",
|
|
79
81
|
brief="Parent directory not found",
|
|
@@ -89,27 +91,46 @@ class WriteFile(CallableTool2[Params]):
|
|
|
89
91
|
brief="Invalid write mode",
|
|
90
92
|
)
|
|
91
93
|
|
|
94
|
+
file_existed = await p.exists()
|
|
95
|
+
old_text = None
|
|
96
|
+
if file_existed:
|
|
97
|
+
old_text = await p.read_text(errors="replace")
|
|
98
|
+
|
|
99
|
+
new_text = (
|
|
100
|
+
params.content if params.mode == "overwrite" else (old_text or "") + params.content
|
|
101
|
+
)
|
|
102
|
+
diff_blocks: list[DisplayBlock] = list(
|
|
103
|
+
build_diff_blocks(
|
|
104
|
+
params.path,
|
|
105
|
+
old_text or "",
|
|
106
|
+
new_text,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
92
110
|
# Request approval
|
|
93
111
|
if not await self._approval.request(
|
|
94
112
|
self.name,
|
|
95
113
|
FileActions.EDIT,
|
|
96
114
|
f"Write file `{params.path}`",
|
|
115
|
+
display=diff_blocks,
|
|
97
116
|
):
|
|
98
117
|
return ToolRejectedError()
|
|
99
118
|
|
|
100
|
-
# Determine file mode for aiofiles
|
|
101
|
-
file_mode = "w" if params.mode == "overwrite" else "a"
|
|
102
|
-
|
|
103
119
|
# Write content to file
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
match params.mode:
|
|
121
|
+
case "overwrite":
|
|
122
|
+
await p.write_text(params.content)
|
|
123
|
+
case "append":
|
|
124
|
+
await p.append_text(params.content)
|
|
106
125
|
|
|
107
126
|
# Get file info for success message
|
|
108
|
-
file_size = p.stat().st_size
|
|
127
|
+
file_size = (await p.stat()).st_size
|
|
109
128
|
action = "overwritten" if params.mode == "overwrite" else "appended to"
|
|
110
|
-
return
|
|
129
|
+
return ToolReturnValue(
|
|
130
|
+
is_error=False,
|
|
111
131
|
output="",
|
|
112
132
|
message=(f"File successfully {action}. Current size: {file_size} bytes."),
|
|
133
|
+
display=diff_blocks,
|
|
113
134
|
)
|
|
114
135
|
|
|
115
136
|
except Exception as e:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Create a custom subagent with specific system prompt and name for reuse.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
- Define specialized agents with custom roles and boundaries
|
|
5
|
+
- Created agents can be referenced by name in the Task tool
|
|
6
|
+
- Use this when you need a specific agent type not covered by predefined agents
|
|
7
|
+
- The created agent configuration will be saved and can be used immediately
|
|
8
|
+
|
|
9
|
+
Example workflow:
|
|
10
|
+
1. Use CreateSubagent to define a specialized agent (e.g., 'code_reviewer')
|
|
11
|
+
2. Use the Task tool with agent='code_reviewer' to launch the created agent
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnValue
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from kimi_cli.soul.agent import Agent, Runtime
|
|
7
|
+
from kimi_cli.soul.toolset import KimiToolset
|
|
8
|
+
from kimi_cli.tools.utils import load_desc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Params(BaseModel):
|
|
12
|
+
name: str = Field(
|
|
13
|
+
description=(
|
|
14
|
+
"Unique name for this agent configuration (e.g., 'summarizer', 'code_reviewer'). "
|
|
15
|
+
"This name will be used to reference the agent in the Task tool."
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
system_prompt: str = Field(
|
|
19
|
+
description="System prompt defining the agent's role, capabilities, and boundaries."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CreateSubagent(CallableTool2[Params]):
|
|
24
|
+
name: str = "CreateSubagent"
|
|
25
|
+
description: str = load_desc(Path(__file__).parent / "create.md")
|
|
26
|
+
params: type[Params] = Params
|
|
27
|
+
|
|
28
|
+
def __init__(self, toolset: KimiToolset, runtime: Runtime):
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._toolset = toolset
|
|
31
|
+
self._runtime = runtime
|
|
32
|
+
|
|
33
|
+
async def __call__(self, params: Params) -> ToolReturnValue:
|
|
34
|
+
if params.name in self._runtime.labor_market.subagents:
|
|
35
|
+
return ToolError(
|
|
36
|
+
message=f"Subagent with name '{params.name}' already exists.",
|
|
37
|
+
brief="Subagent already exists",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
subagent = Agent(
|
|
41
|
+
name=params.name,
|
|
42
|
+
system_prompt=params.system_prompt,
|
|
43
|
+
toolset=self._toolset, # share the same toolset as the parent agent
|
|
44
|
+
runtime=self._runtime.copy_for_dynamic_subagent(),
|
|
45
|
+
)
|
|
46
|
+
self._runtime.labor_market.add_dynamic_subagent(params.name, subagent)
|
|
47
|
+
return ToolOk(
|
|
48
|
+
output="Available subagents: " + ", ".join(self._runtime.labor_market.subagents.keys()),
|
|
49
|
+
message=f"Subagent '{params.name}' created successfully.",
|
|
50
|
+
)
|
|
@@ -2,20 +2,23 @@ import asyncio
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import override
|
|
4
4
|
|
|
5
|
-
from kosong.tooling import CallableTool2, ToolError, ToolOk,
|
|
5
|
+
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnValue
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
-
from kimi_cli.agentspec import ResolvedAgentSpec, SubagentSpec
|
|
9
8
|
from kimi_cli.soul import MaxStepsReached, get_wire_or_none, run_soul
|
|
10
|
-
from kimi_cli.soul.agent import Agent,
|
|
9
|
+
from kimi_cli.soul.agent import Agent, Runtime
|
|
11
10
|
from kimi_cli.soul.context import Context
|
|
12
11
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
13
|
-
from kimi_cli.soul.
|
|
12
|
+
from kimi_cli.soul.toolset import get_current_tool_call_or_none
|
|
14
13
|
from kimi_cli.tools.utils import load_desc
|
|
15
|
-
from kimi_cli.utils.message import message_extract_text
|
|
16
14
|
from kimi_cli.utils.path import next_available_rotation
|
|
17
|
-
from kimi_cli.wire import
|
|
18
|
-
from kimi_cli.wire.
|
|
15
|
+
from kimi_cli.wire import Wire
|
|
16
|
+
from kimi_cli.wire.types import (
|
|
17
|
+
ApprovalRequest,
|
|
18
|
+
ApprovalRequestResolved,
|
|
19
|
+
SubagentEvent,
|
|
20
|
+
WireMessage,
|
|
21
|
+
)
|
|
19
22
|
|
|
20
23
|
# Maximum continuation attempts for task summary
|
|
21
24
|
MAX_CONTINUE_ATTEMPTS = 1
|
|
@@ -49,61 +52,42 @@ class Task(CallableTool2[Params]):
|
|
|
49
52
|
name: str = "Task"
|
|
50
53
|
params: type[Params] = Params
|
|
51
54
|
|
|
52
|
-
def __init__(self,
|
|
55
|
+
def __init__(self, runtime: Runtime):
|
|
53
56
|
super().__init__(
|
|
54
57
|
description=load_desc(
|
|
55
58
|
Path(__file__).parent / "task.md",
|
|
56
59
|
{
|
|
57
60
|
"SUBAGENTS_MD": "\n".join(
|
|
58
|
-
f"- `{name}`: {
|
|
59
|
-
for name,
|
|
61
|
+
f"- `{name}`: {desc}"
|
|
62
|
+
for name, desc in runtime.labor_market.fixed_subagent_descs.items()
|
|
60
63
|
),
|
|
61
64
|
},
|
|
62
65
|
),
|
|
63
|
-
**kwargs,
|
|
64
66
|
)
|
|
65
|
-
|
|
66
|
-
self._runtime = runtime
|
|
67
|
+
self._labor_market = runtime.labor_market
|
|
67
68
|
self._session = runtime.session
|
|
68
|
-
self._subagents: dict[str, Agent] = {}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
async def _load_subagents(self, subagent_specs: dict[str, SubagentSpec]) -> None:
|
|
79
|
-
"""Load all subagents specified in the agent spec."""
|
|
80
|
-
for name, spec in subagent_specs.items():
|
|
81
|
-
agent = await load_agent(spec.path, self._runtime, mcp_configs=[])
|
|
82
|
-
self._subagents[name] = agent
|
|
83
|
-
|
|
84
|
-
async def _get_subagent_history_file(self) -> Path:
|
|
85
|
-
"""Generate a unique history file path for subagent."""
|
|
86
|
-
main_history_file = self._session.history_file
|
|
87
|
-
subagent_base_name = f"{main_history_file.stem}_sub"
|
|
88
|
-
main_history_file.parent.mkdir(parents=True, exist_ok=True) # just in case
|
|
89
|
-
sub_history_file = await next_available_rotation(
|
|
90
|
-
main_history_file.parent / f"{subagent_base_name}{main_history_file.suffix}"
|
|
70
|
+
async def _get_subagent_context_file(self) -> Path:
|
|
71
|
+
"""Generate a unique context file path for subagent."""
|
|
72
|
+
main_context_file = self._session.context_file
|
|
73
|
+
subagent_base_name = f"{main_context_file.stem}_sub"
|
|
74
|
+
main_context_file.parent.mkdir(parents=True, exist_ok=True) # just in case
|
|
75
|
+
sub_context_file = await next_available_rotation(
|
|
76
|
+
main_context_file.parent / f"{subagent_base_name}{main_context_file.suffix}"
|
|
91
77
|
)
|
|
92
|
-
assert
|
|
93
|
-
return
|
|
78
|
+
assert sub_context_file is not None
|
|
79
|
+
return sub_context_file
|
|
94
80
|
|
|
95
81
|
@override
|
|
96
|
-
async def __call__(self, params: Params) ->
|
|
97
|
-
|
|
98
|
-
await self._load_task
|
|
99
|
-
self._load_task = None
|
|
82
|
+
async def __call__(self, params: Params) -> ToolReturnValue:
|
|
83
|
+
subagents = self._labor_market.subagents
|
|
100
84
|
|
|
101
|
-
if params.subagent_name not in
|
|
85
|
+
if params.subagent_name not in subagents:
|
|
102
86
|
return ToolError(
|
|
103
87
|
message=f"Subagent not found: {params.subagent_name}",
|
|
104
88
|
brief="Subagent not found",
|
|
105
89
|
)
|
|
106
|
-
agent =
|
|
90
|
+
agent = subagents[params.subagent_name]
|
|
107
91
|
try:
|
|
108
92
|
result = await self._run_subagent(agent, params.prompt)
|
|
109
93
|
return result
|
|
@@ -113,24 +97,35 @@ class Task(CallableTool2[Params]):
|
|
|
113
97
|
brief="Failed to run subagent",
|
|
114
98
|
)
|
|
115
99
|
|
|
116
|
-
async def _run_subagent(self, agent: Agent, prompt: str) ->
|
|
100
|
+
async def _run_subagent(self, agent: Agent, prompt: str) -> ToolReturnValue:
|
|
117
101
|
"""Run subagent with optional continuation for task summary."""
|
|
118
102
|
super_wire = get_wire_or_none()
|
|
119
103
|
assert super_wire is not None
|
|
104
|
+
current_tool_call = get_current_tool_call_or_none()
|
|
105
|
+
assert current_tool_call is not None
|
|
106
|
+
current_tool_call_id = current_tool_call.id
|
|
120
107
|
|
|
121
108
|
def _super_wire_send(msg: WireMessage) -> None:
|
|
122
|
-
if isinstance(msg, ApprovalRequest):
|
|
109
|
+
if isinstance(msg, ApprovalRequest | ApprovalRequestResolved):
|
|
110
|
+
# ApprovalRequest and ApprovalRequestResolved should be root level Wire messages
|
|
123
111
|
super_wire.soul_side.send(msg)
|
|
124
|
-
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
event = SubagentEvent(
|
|
115
|
+
task_tool_call_id=current_tool_call_id,
|
|
116
|
+
event=msg,
|
|
117
|
+
)
|
|
118
|
+
super_wire.soul_side.send(event)
|
|
125
119
|
|
|
126
|
-
async def _ui_loop_fn(wire:
|
|
120
|
+
async def _ui_loop_fn(wire: Wire) -> None:
|
|
121
|
+
wire_ui = wire.ui_side(merge=True)
|
|
127
122
|
while True:
|
|
128
|
-
msg = await
|
|
123
|
+
msg = await wire_ui.receive()
|
|
129
124
|
_super_wire_send(msg)
|
|
130
125
|
|
|
131
|
-
|
|
132
|
-
context = Context(file_backend=
|
|
133
|
-
soul = KimiSoul(agent,
|
|
126
|
+
subagent_context_file = await self._get_subagent_context_file()
|
|
127
|
+
context = Context(file_backend=subagent_context_file)
|
|
128
|
+
soul = KimiSoul(agent, context=context)
|
|
134
129
|
|
|
135
130
|
try:
|
|
136
131
|
await run_soul(soul, prompt, _ui_loop_fn, asyncio.Event())
|
|
@@ -151,7 +146,7 @@ class Task(CallableTool2[Params]):
|
|
|
151
146
|
if len(context.history) == 0 or context.history[-1].role != "assistant":
|
|
152
147
|
return ToolError(message=_error_msg, brief="Failed to run subagent")
|
|
153
148
|
|
|
154
|
-
final_response =
|
|
149
|
+
final_response = context.history[-1].extract_text(sep="\n")
|
|
155
150
|
|
|
156
151
|
# Check if response is too brief, if so, run again with continuation prompt
|
|
157
152
|
n_attempts_remaining = MAX_CONTINUE_ATTEMPTS
|
|
@@ -160,6 +155,6 @@ class Task(CallableTool2[Params]):
|
|
|
160
155
|
|
|
161
156
|
if len(context.history) == 0 or context.history[-1].role != "assistant":
|
|
162
157
|
return ToolError(message=_error_msg, brief="Failed to run subagent")
|
|
163
|
-
final_response =
|
|
158
|
+
final_response = context.history[-1].extract_text(sep="\n")
|
|
164
159
|
|
|
165
160
|
return ToolOk(output=final_response)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import override
|
|
5
|
+
|
|
6
|
+
import kaos
|
|
7
|
+
from kaos import AsyncReadable
|
|
8
|
+
from kosong.tooling import CallableTool2, ToolReturnValue
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from kimi_cli.soul.approval import Approval
|
|
12
|
+
from kimi_cli.tools.utils import ToolRejectedError, ToolResultBuilder, load_desc
|
|
13
|
+
from kimi_cli.utils.environment import Environment
|
|
14
|
+
|
|
15
|
+
MAX_TIMEOUT = 5 * 60
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Params(BaseModel):
|
|
19
|
+
command: str = Field(description="The bash command to execute.")
|
|
20
|
+
timeout: int = Field(
|
|
21
|
+
description=(
|
|
22
|
+
"The timeout in seconds for the command to execute. "
|
|
23
|
+
"If the command takes longer than this, it will be killed."
|
|
24
|
+
),
|
|
25
|
+
default=60,
|
|
26
|
+
ge=1,
|
|
27
|
+
le=MAX_TIMEOUT,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Shell(CallableTool2[Params]):
|
|
32
|
+
name: str = "Shell"
|
|
33
|
+
params: type[Params] = Params
|
|
34
|
+
|
|
35
|
+
def __init__(self, approval: Approval, environment: Environment):
|
|
36
|
+
is_powershell = environment.shell_name == "Windows PowerShell"
|
|
37
|
+
super().__init__(
|
|
38
|
+
description=load_desc(
|
|
39
|
+
Path(__file__).parent / ("powershell.md" if is_powershell else "bash.md"),
|
|
40
|
+
{"SHELL": f"{environment.shell_name} (`{environment.shell_path}`)"},
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
self._approval = approval
|
|
44
|
+
self._is_powershell = is_powershell
|
|
45
|
+
self._shell_path = environment.shell_path
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
async def __call__(self, params: Params) -> ToolReturnValue:
|
|
49
|
+
builder = ToolResultBuilder()
|
|
50
|
+
|
|
51
|
+
if not params.command:
|
|
52
|
+
return builder.error("Command cannot be empty.", brief="Empty command")
|
|
53
|
+
|
|
54
|
+
if not await self._approval.request(
|
|
55
|
+
self.name,
|
|
56
|
+
"run shell command",
|
|
57
|
+
f"Run command `{params.command}`",
|
|
58
|
+
):
|
|
59
|
+
return ToolRejectedError()
|
|
60
|
+
|
|
61
|
+
def stdout_cb(line: bytes):
|
|
62
|
+
line_str = line.decode(encoding="utf-8", errors="replace")
|
|
63
|
+
builder.write(line_str)
|
|
64
|
+
|
|
65
|
+
def stderr_cb(line: bytes):
|
|
66
|
+
line_str = line.decode(encoding="utf-8", errors="replace")
|
|
67
|
+
builder.write(line_str)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
exitcode = await self._run_shell_command(
|
|
71
|
+
params.command, stdout_cb, stderr_cb, params.timeout
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if exitcode == 0:
|
|
75
|
+
return builder.ok("Command executed successfully.")
|
|
76
|
+
else:
|
|
77
|
+
return builder.error(
|
|
78
|
+
f"Command failed with exit code: {exitcode}.",
|
|
79
|
+
brief=f"Failed with exit code: {exitcode}",
|
|
80
|
+
)
|
|
81
|
+
except TimeoutError:
|
|
82
|
+
return builder.error(
|
|
83
|
+
f"Command killed by timeout ({params.timeout}s)",
|
|
84
|
+
brief=f"Killed by timeout ({params.timeout}s)",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def _run_shell_command(
|
|
88
|
+
self,
|
|
89
|
+
command: str,
|
|
90
|
+
stdout_cb: Callable[[bytes], None],
|
|
91
|
+
stderr_cb: Callable[[bytes], None],
|
|
92
|
+
timeout: int,
|
|
93
|
+
) -> int:
|
|
94
|
+
async def _read_stream(stream: AsyncReadable, cb: Callable[[bytes], None]):
|
|
95
|
+
while True:
|
|
96
|
+
line = await stream.readline()
|
|
97
|
+
if line:
|
|
98
|
+
cb(line)
|
|
99
|
+
else:
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
process = await kaos.exec(*self._shell_args(command))
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
await asyncio.wait_for(
|
|
106
|
+
asyncio.gather(
|
|
107
|
+
_read_stream(process.stdout, stdout_cb),
|
|
108
|
+
_read_stream(process.stderr, stderr_cb),
|
|
109
|
+
),
|
|
110
|
+
timeout,
|
|
111
|
+
)
|
|
112
|
+
return await process.wait()
|
|
113
|
+
except TimeoutError:
|
|
114
|
+
await process.kill()
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
def _shell_args(self, command: str) -> tuple[str, ...]:
|
|
118
|
+
if self._is_powershell:
|
|
119
|
+
return (str(self._shell_path), "-command", command)
|
|
120
|
+
return (str(self._shell_path), "-c", command)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Execute a
|
|
1
|
+
Execute a ${SHELL} command. Use this tool to explore the filesystem, edit files, run scripts, get system information, etc.
|
|
2
2
|
|
|
3
3
|
**Output:**
|
|
4
4
|
The stdout and stderr will be combined and returned as a string. The output may be truncated if it is too long. If the command failed, the exit code will be provided in a system tag.
|
|
@@ -25,7 +25,6 @@ The stdout and stderr will be combined and returned as a string. The output may
|
|
|
25
25
|
- File viewing/editing: cat, grep, head, tail, diff, patch
|
|
26
26
|
- Text processing: awk, sed, sort, uniq, wc
|
|
27
27
|
- System information/operations: ps, kill, top, df, free, uname, whoami, id, date
|
|
28
|
-
- Package management: pip, uv, npm, yarn, bun, cargo
|
|
29
28
|
- Network operations: curl, wget, ping, telnet, ssh
|
|
30
29
|
- Archive operations: tar, zip, unzip
|
|
31
30
|
- Other: Other commands available in the shell environment. Check the existence of a command by running `which <command>` before using it.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Execute a ${SHELL} command. Use this tool to explore the filesystem, inspect or edit files, run Windows scripts, collect system information, etc., whenever the agent is running on Windows.
|
|
2
|
+
|
|
3
|
+
Note that you are running on Windows, so make sure to use Windows commands, paths, and conventions.
|
|
4
|
+
|
|
5
|
+
**Output:**
|
|
6
|
+
The stdout and stderr streams are combined and returned as a single string. Extremely long output may be truncated. When a command fails, the exit code is provided in a system tag.
|
|
7
|
+
|
|
8
|
+
**Guidelines for safety and security:**
|
|
9
|
+
- Every tool call starts a fresh ${SHELL} session. Environment variables, `cd` changes, and command history do not persist between calls.
|
|
10
|
+
- Do not launch interactive programs or anything that is expected to block indefinitely; ensure each command finishes promptly. Provide a `timeout` argument for potentially long runs.
|
|
11
|
+
- Avoid using `..` to leave the working directory, and never touch files outside that directory unless explicitly instructed.
|
|
12
|
+
- Never attempt commands that require elevated (Administrator) privileges unless explicitly authorized.
|
|
13
|
+
|
|
14
|
+
**Guidelines for efficiency:**
|
|
15
|
+
- Chain related commands with `;` and use `if ($?)` or `if (-not $?)` to conditionally execute commands based on the success or failure of previous ones.
|
|
16
|
+
- Redirect or pipe output with `>`, `>>`, `|`, and leverage `for /f`, `if`, and `set` to build richer one-liners instead of multiple tool calls.
|
|
17
|
+
- Reuse built-in utilities (e.g., `findstr`, `where`) to filter, transform, or locate data in a single invocation.
|
|
18
|
+
|
|
19
|
+
**Commands available:**
|
|
20
|
+
- Shell environment: `cd`, `dir`, `set`, `setlocal`, `echo`, `call`, `where`
|
|
21
|
+
- File operations: `type`, `copy`, `move`, `del`, `erase`, `mkdir`, `rmdir`, `attrib`, `mklink`
|
|
22
|
+
- Text/search: `find`, `findstr`, `more`, `sort`, `Get-Content`
|
|
23
|
+
- System info: `ver`, `systeminfo`, `tasklist`, `wmic`, `hostname`
|
|
24
|
+
- Archives/scripts: `tar`, `Compress-Archive`, `powershell`, `python`, `node`
|
|
25
|
+
- Other: Any other binaries available on the system PATH; run `where <command>` first if unsure.
|
kimi_cli/tools/test.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from typing import override
|
|
3
3
|
|
|
4
|
-
from kosong.tooling import CallableTool2, ToolOk,
|
|
4
|
+
from kosong.tooling import CallableTool2, ToolOk, ToolReturnValue
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
7
|
|
|
@@ -16,7 +16,7 @@ class Plus(CallableTool2[PlusParams]):
|
|
|
16
16
|
params: type[PlusParams] = PlusParams
|
|
17
17
|
|
|
18
18
|
@override
|
|
19
|
-
async def __call__(self, params: PlusParams) ->
|
|
19
|
+
async def __call__(self, params: PlusParams) -> ToolReturnValue:
|
|
20
20
|
return ToolOk(output=str(params.a + params.b))
|
|
21
21
|
|
|
22
22
|
|
|
@@ -31,7 +31,7 @@ class Compare(CallableTool2[CompareParams]):
|
|
|
31
31
|
params: type[CompareParams] = CompareParams
|
|
32
32
|
|
|
33
33
|
@override
|
|
34
|
-
async def __call__(self, params: CompareParams) ->
|
|
34
|
+
async def __call__(self, params: CompareParams) -> ToolReturnValue:
|
|
35
35
|
if params.a > params.b:
|
|
36
36
|
return ToolOk(output="greater")
|
|
37
37
|
elif params.a < params.b:
|
|
@@ -50,6 +50,6 @@ class Panic(CallableTool2[PanicParams]):
|
|
|
50
50
|
params: type[PanicParams] = PanicParams
|
|
51
51
|
|
|
52
52
|
@override
|
|
53
|
-
async def __call__(self, params: PanicParams) ->
|
|
53
|
+
async def __call__(self, params: PanicParams) -> ToolReturnValue:
|
|
54
54
|
await asyncio.sleep(2)
|
|
55
55
|
raise Exception(f"panicked with a message with {len(params.message)} characters")
|
kimi_cli/tools/think/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import override
|
|
3
3
|
|
|
4
|
-
from kosong.tooling import CallableTool2, ToolOk,
|
|
4
|
+
from kosong.tooling import CallableTool2, ToolOk, ToolReturnValue
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
7
|
from kimi_cli.tools.utils import load_desc
|
|
@@ -17,5 +17,5 @@ class Think(CallableTool2[Params]):
|
|
|
17
17
|
params: type[Params] = Params
|
|
18
18
|
|
|
19
19
|
@override
|
|
20
|
-
async def __call__(self, params: Params) ->
|
|
20
|
+
async def __call__(self, params: Params) -> ToolReturnValue:
|
|
21
21
|
return ToolOk(output="", message="Thought logged")
|
kimi_cli/tools/todo/__init__.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Literal, override
|
|
3
3
|
|
|
4
|
-
from kosong.tooling import CallableTool2,
|
|
4
|
+
from kosong.tooling import CallableTool2, ToolReturnValue
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
|
+
from kimi_cli.tools.display import TodoDisplayBlock, TodoDisplayItem
|
|
8
|
+
from kimi_cli.tools.utils import load_desc
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class Todo(BaseModel):
|
|
9
12
|
title: str = Field(description="The title of the todo", min_length=1)
|
|
10
|
-
status: Literal["
|
|
13
|
+
status: Literal["pending", "in_progress", "done"] = Field(description="The status of the todo")
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class Params(BaseModel):
|
|
@@ -16,12 +19,15 @@ class Params(BaseModel):
|
|
|
16
19
|
|
|
17
20
|
class SetTodoList(CallableTool2[Params]):
|
|
18
21
|
name: str = "SetTodoList"
|
|
19
|
-
description: str = (Path(__file__).parent / "set_todo_list.md")
|
|
22
|
+
description: str = load_desc(Path(__file__).parent / "set_todo_list.md")
|
|
20
23
|
params: type[Params] = Params
|
|
21
24
|
|
|
22
25
|
@override
|
|
23
|
-
async def __call__(self, params: Params) ->
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
async def __call__(self, params: Params) -> ToolReturnValue:
|
|
27
|
+
items = [TodoDisplayItem(title=todo.title, status=todo.status) for todo in params.todos]
|
|
28
|
+
return ToolReturnValue(
|
|
29
|
+
is_error=False,
|
|
30
|
+
output="",
|
|
31
|
+
message="Todo list updated",
|
|
32
|
+
display=[TodoDisplayBlock(items=items)],
|
|
33
|
+
)
|