kimi-cli 0.35__py3-none-any.whl → 0.52__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kimi_cli/CHANGELOG.md +165 -0
- kimi_cli/__init__.py +0 -374
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/app.py +208 -0
- kimi_cli/cli.py +321 -0
- kimi_cli/config.py +33 -16
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +144 -3
- kimi_cli/metadata.py +6 -69
- kimi_cli/prompts/__init__.py +4 -0
- kimi_cli/session.py +103 -0
- kimi_cli/soul/__init__.py +130 -9
- kimi_cli/soul/agent.py +159 -0
- kimi_cli/soul/approval.py +5 -6
- kimi_cli/soul/compaction.py +106 -0
- kimi_cli/soul/context.py +1 -1
- kimi_cli/soul/kimisoul.py +180 -80
- kimi_cli/soul/message.py +6 -6
- kimi_cli/soul/runtime.py +96 -0
- kimi_cli/soul/toolset.py +3 -2
- kimi_cli/tools/__init__.py +35 -31
- kimi_cli/tools/bash/__init__.py +25 -9
- kimi_cli/tools/bash/cmd.md +31 -0
- kimi_cli/tools/dmail/__init__.py +5 -4
- kimi_cli/tools/file/__init__.py +8 -0
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +4 -4
- kimi_cli/tools/file/grep.py +36 -19
- kimi_cli/tools/file/patch.py +52 -10
- kimi_cli/tools/file/read.py +6 -5
- kimi_cli/tools/file/replace.py +16 -4
- kimi_cli/tools/file/write.py +16 -4
- kimi_cli/tools/mcp.py +7 -4
- kimi_cli/tools/task/__init__.py +60 -41
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +4 -2
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +13 -12
- kimi_cli/ui/__init__.py +0 -68
- kimi_cli/ui/acp/__init__.py +67 -38
- kimi_cli/ui/print/__init__.py +46 -69
- kimi_cli/ui/shell/__init__.py +145 -154
- kimi_cli/ui/shell/console.py +27 -1
- kimi_cli/ui/shell/debug.py +187 -0
- kimi_cli/ui/shell/keyboard.py +183 -0
- kimi_cli/ui/shell/metacmd.py +34 -81
- kimi_cli/ui/shell/prompt.py +245 -28
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/setup.py +19 -19
- kimi_cli/ui/shell/update.py +11 -5
- kimi_cli/ui/shell/visualize.py +576 -0
- kimi_cli/ui/wire/README.md +109 -0
- kimi_cli/ui/wire/__init__.py +340 -0
- kimi_cli/ui/wire/jsonrpc.py +48 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +6 -2
- kimi_cli/utils/clipboard.py +10 -0
- kimi_cli/utils/message.py +15 -1
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/markdown.py +959 -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 +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/utils/term.py +114 -0
- kimi_cli/wire/__init__.py +73 -0
- kimi_cli/wire/message.py +191 -0
- kimi_cli-0.52.dist-info/METADATA +186 -0
- kimi_cli-0.52.dist-info/RECORD +99 -0
- kimi_cli-0.52.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- kimi_cli/prompts/metacmds/__init__.py +0 -4
- kimi_cli/soul/wire.py +0 -101
- kimi_cli/ui/shell/liveview.py +0 -158
- kimi_cli/utils/provider.py +0 -64
- kimi_cli-0.35.dist-info/METADATA +0 -24
- kimi_cli-0.35.dist-info/RECORD +0 -76
- kimi_cli-0.35.dist-info/entry_points.txt +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
- /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
kimi_cli/tools/file/read.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import override
|
|
2
|
+
from typing import Any, override
|
|
3
3
|
|
|
4
4
|
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.
|
|
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
|
|
@@ -47,8 +47,9 @@ class ReadFile(CallableTool2[Params]):
|
|
|
47
47
|
)
|
|
48
48
|
params: type[Params] = Params
|
|
49
49
|
|
|
50
|
-
def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
|
|
50
|
+
def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs: Any) -> None:
|
|
51
51
|
super().__init__(**kwargs)
|
|
52
|
+
|
|
52
53
|
self._work_dir = builtin_args.KIMI_WORK_DIR
|
|
53
54
|
|
|
54
55
|
@override
|
|
@@ -84,7 +85,7 @@ class ReadFile(CallableTool2[Params]):
|
|
|
84
85
|
|
|
85
86
|
lines: list[str] = []
|
|
86
87
|
n_bytes = 0
|
|
87
|
-
truncated_line_numbers = []
|
|
88
|
+
truncated_line_numbers: list[int] = []
|
|
88
89
|
max_lines_reached = False
|
|
89
90
|
max_bytes_reached = False
|
|
90
91
|
async with aiofiles.open(p, encoding="utf-8", errors="replace") as f:
|
|
@@ -108,7 +109,7 @@ class ReadFile(CallableTool2[Params]):
|
|
|
108
109
|
break
|
|
109
110
|
|
|
110
111
|
# Format output with line numbers like `cat -n`
|
|
111
|
-
lines_with_no = []
|
|
112
|
+
lines_with_no: list[str] = []
|
|
112
113
|
for line_num, line in zip(
|
|
113
114
|
range(params.line_offset, params.line_offset + len(lines)), lines, strict=True
|
|
114
115
|
):
|
kimi_cli/tools/file/replace.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import override
|
|
2
|
+
from typing import Any, override
|
|
3
3
|
|
|
4
4
|
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.
|
|
8
|
+
from kimi_cli.soul.approval import Approval
|
|
9
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs
|
|
10
|
+
from kimi_cli.tools.file import FileActions
|
|
11
|
+
from kimi_cli.tools.utils import ToolRejectedError, load_desc
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Edit(BaseModel):
|
|
@@ -26,12 +29,13 @@ class Params(BaseModel):
|
|
|
26
29
|
|
|
27
30
|
class StrReplaceFile(CallableTool2[Params]):
|
|
28
31
|
name: str = "StrReplaceFile"
|
|
29
|
-
description: str = (Path(__file__).parent / "replace.md")
|
|
32
|
+
description: str = load_desc(Path(__file__).parent / "replace.md")
|
|
30
33
|
params: type[Params] = Params
|
|
31
34
|
|
|
32
|
-
def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
|
|
35
|
+
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs: Any):
|
|
33
36
|
super().__init__(**kwargs)
|
|
34
37
|
self._work_dir = builtin_args.KIMI_WORK_DIR
|
|
38
|
+
self._approval = approval
|
|
35
39
|
|
|
36
40
|
def _validate_path(self, path: Path) -> ToolError | None:
|
|
37
41
|
"""Validate that the path is safe to edit."""
|
|
@@ -87,6 +91,14 @@ class StrReplaceFile(CallableTool2[Params]):
|
|
|
87
91
|
brief="Invalid path",
|
|
88
92
|
)
|
|
89
93
|
|
|
94
|
+
# Request approval
|
|
95
|
+
if not await self._approval.request(
|
|
96
|
+
self.name,
|
|
97
|
+
FileActions.EDIT,
|
|
98
|
+
f"Edit file `{params.path}`",
|
|
99
|
+
):
|
|
100
|
+
return ToolRejectedError()
|
|
101
|
+
|
|
90
102
|
# Read the file content
|
|
91
103
|
async with aiofiles.open(p, encoding="utf-8", errors="replace") as f:
|
|
92
104
|
content = await f.read()
|
kimi_cli/tools/file/write.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Literal, override
|
|
2
|
+
from typing import Any, Literal, override
|
|
3
3
|
|
|
4
4
|
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.
|
|
8
|
+
from kimi_cli.soul.approval import Approval
|
|
9
|
+
from kimi_cli.soul.runtime import BuiltinSystemPromptArgs
|
|
10
|
+
from kimi_cli.tools.file import FileActions
|
|
11
|
+
from kimi_cli.tools.utils import ToolRejectedError, load_desc
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Params(BaseModel):
|
|
@@ -23,12 +26,13 @@ class Params(BaseModel):
|
|
|
23
26
|
|
|
24
27
|
class WriteFile(CallableTool2[Params]):
|
|
25
28
|
name: str = "WriteFile"
|
|
26
|
-
description: str = (Path(__file__).parent / "write.md")
|
|
29
|
+
description: str = load_desc(Path(__file__).parent / "write.md")
|
|
27
30
|
params: type[Params] = Params
|
|
28
31
|
|
|
29
|
-
def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
|
|
32
|
+
def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs: Any):
|
|
30
33
|
super().__init__(**kwargs)
|
|
31
34
|
self._work_dir = builtin_args.KIMI_WORK_DIR
|
|
35
|
+
self._approval = approval
|
|
32
36
|
|
|
33
37
|
def _validate_path(self, path: Path) -> ToolError | None:
|
|
34
38
|
"""Validate that the path is safe to write."""
|
|
@@ -85,6 +89,14 @@ class WriteFile(CallableTool2[Params]):
|
|
|
85
89
|
brief="Invalid write mode",
|
|
86
90
|
)
|
|
87
91
|
|
|
92
|
+
# Request approval
|
|
93
|
+
if not await self._approval.request(
|
|
94
|
+
self.name,
|
|
95
|
+
FileActions.EDIT,
|
|
96
|
+
f"Write file `{params.path}`",
|
|
97
|
+
):
|
|
98
|
+
return ToolRejectedError()
|
|
99
|
+
|
|
88
100
|
# Determine file mode for aiofiles
|
|
89
101
|
file_mode = "w" if params.mode == "overwrite" else "a"
|
|
90
102
|
|
kimi_cli/tools/mcp.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
1
3
|
import fastmcp
|
|
2
4
|
import mcp
|
|
3
5
|
from fastmcp.client.client import CallToolResult
|
|
4
|
-
from
|
|
6
|
+
from fastmcp.client.transports import ClientTransport
|
|
7
|
+
from kosong.message import AudioURLPart, ContentPart, ImageURLPart, TextPart
|
|
5
8
|
from kosong.tooling import CallableTool, ToolOk, ToolReturnType
|
|
6
9
|
|
|
7
10
|
|
|
8
|
-
class MCPTool(CallableTool):
|
|
9
|
-
def __init__(self, mcp_tool: mcp.Tool, client: fastmcp.Client, **kwargs):
|
|
11
|
+
class MCPTool[T: ClientTransport](CallableTool):
|
|
12
|
+
def __init__(self, mcp_tool: mcp.Tool, client: fastmcp.Client[T], **kwargs: Any):
|
|
10
13
|
super().__init__(
|
|
11
14
|
name=mcp_tool.name,
|
|
12
15
|
description=mcp_tool.description or "",
|
|
@@ -16,7 +19,7 @@ class MCPTool(CallableTool):
|
|
|
16
19
|
self._mcp_tool = mcp_tool
|
|
17
20
|
self._client = client
|
|
18
21
|
|
|
19
|
-
async def __call__(self, *args, **kwargs) -> ToolReturnType:
|
|
22
|
+
async def __call__(self, *args: Any, **kwargs: Any) -> ToolReturnType:
|
|
20
23
|
async with self._client as client:
|
|
21
24
|
result = await client.call_tool(self._mcp_tool.name, kwargs, timeout=20)
|
|
22
25
|
return convert_tool_result(result)
|
kimi_cli/tools/task/__init__.py
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from pathlib import Path
|
|
2
|
-
from typing import override
|
|
3
|
+
from typing import Any, override
|
|
3
4
|
|
|
4
5
|
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
7
|
-
from kimi_cli.
|
|
8
|
-
from kimi_cli.soul import MaxStepsReached
|
|
8
|
+
from kimi_cli.agentspec import ResolvedAgentSpec, SubagentSpec
|
|
9
|
+
from kimi_cli.soul import MaxStepsReached, get_wire_or_none, run_soul
|
|
10
|
+
from kimi_cli.soul.agent import Agent, load_agent
|
|
9
11
|
from kimi_cli.soul.context import Context
|
|
10
12
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
11
|
-
from kimi_cli.soul.
|
|
13
|
+
from kimi_cli.soul.runtime import Runtime
|
|
14
|
+
from kimi_cli.soul.toolset import get_current_tool_call_or_none
|
|
12
15
|
from kimi_cli.tools.utils import load_desc
|
|
13
16
|
from kimi_cli.utils.message import message_extract_text
|
|
14
17
|
from kimi_cli.utils.path import next_available_rotation
|
|
18
|
+
from kimi_cli.wire import WireUISide
|
|
19
|
+
from kimi_cli.wire.message import ApprovalRequest, SubagentEvent, WireMessage
|
|
15
20
|
|
|
16
21
|
# Maximum continuation attempts for task summary
|
|
17
22
|
MAX_CONTINUE_ATTEMPTS = 1
|
|
@@ -45,29 +50,37 @@ class Task(CallableTool2[Params]):
|
|
|
45
50
|
name: str = "Task"
|
|
46
51
|
params: type[Params] = Params
|
|
47
52
|
|
|
48
|
-
def __init__(self, agent_spec:
|
|
49
|
-
subagents: dict[str, Agent] = {}
|
|
50
|
-
descs = []
|
|
51
|
-
|
|
52
|
-
# load all subagents
|
|
53
|
-
assert agent_spec.subagents is not None, "Task tool expects subagents"
|
|
54
|
-
for name, spec in agent_spec.subagents.items():
|
|
55
|
-
subagents[name] = load_agent(spec.path, agent_globals)
|
|
56
|
-
descs.append(f"- `{name}`: {spec.description}")
|
|
57
|
-
|
|
53
|
+
def __init__(self, agent_spec: ResolvedAgentSpec, runtime: Runtime, **kwargs: Any):
|
|
58
54
|
super().__init__(
|
|
59
55
|
description=load_desc(
|
|
60
56
|
Path(__file__).parent / "task.md",
|
|
61
57
|
{
|
|
62
|
-
"SUBAGENTS_MD": "\n".join(
|
|
58
|
+
"SUBAGENTS_MD": "\n".join(
|
|
59
|
+
f"- `{name}`: {spec.description}"
|
|
60
|
+
for name, spec in agent_spec.subagents.items()
|
|
61
|
+
),
|
|
63
62
|
},
|
|
64
63
|
),
|
|
65
64
|
**kwargs,
|
|
66
65
|
)
|
|
67
66
|
|
|
68
|
-
self.
|
|
69
|
-
self._session =
|
|
70
|
-
self._subagents =
|
|
67
|
+
self._runtime = runtime
|
|
68
|
+
self._session = runtime.session
|
|
69
|
+
self._subagents: dict[str, Agent] = {}
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
loop = asyncio.get_running_loop()
|
|
73
|
+
self._load_task = loop.create_task(self._load_subagents(agent_spec.subagents))
|
|
74
|
+
except RuntimeError:
|
|
75
|
+
# In case there's no running event loop, e.g., during synchronous tests
|
|
76
|
+
self._load_task = None
|
|
77
|
+
asyncio.run(self._load_subagents(agent_spec.subagents))
|
|
78
|
+
|
|
79
|
+
async def _load_subagents(self, subagent_specs: dict[str, SubagentSpec]) -> None:
|
|
80
|
+
"""Load all subagents specified in the agent spec."""
|
|
81
|
+
for name, spec in subagent_specs.items():
|
|
82
|
+
agent = await load_agent(spec.path, self._runtime, mcp_configs=[])
|
|
83
|
+
self._subagents[name] = agent
|
|
71
84
|
|
|
72
85
|
async def _get_subagent_history_file(self) -> Path:
|
|
73
86
|
"""Generate a unique history file path for subagent."""
|
|
@@ -82,6 +95,10 @@ class Task(CallableTool2[Params]):
|
|
|
82
95
|
|
|
83
96
|
@override
|
|
84
97
|
async def __call__(self, params: Params) -> ToolReturnType:
|
|
98
|
+
if self._load_task is not None:
|
|
99
|
+
await self._load_task
|
|
100
|
+
self._load_task = None
|
|
101
|
+
|
|
85
102
|
if params.subagent_name not in self._subagents:
|
|
86
103
|
return ToolError(
|
|
87
104
|
message=f"Subagent not found: {params.subagent_name}",
|
|
@@ -99,20 +116,34 @@ class Task(CallableTool2[Params]):
|
|
|
99
116
|
|
|
100
117
|
async def _run_subagent(self, agent: Agent, prompt: str) -> ToolReturnType:
|
|
101
118
|
"""Run subagent with optional continuation for task summary."""
|
|
119
|
+
super_wire = get_wire_or_none()
|
|
120
|
+
assert super_wire is not None
|
|
121
|
+
current_tool_call = get_current_tool_call_or_none()
|
|
122
|
+
assert current_tool_call is not None
|
|
123
|
+
current_tool_call_id = current_tool_call.id
|
|
124
|
+
|
|
125
|
+
def _super_wire_send(msg: WireMessage) -> None:
|
|
126
|
+
if isinstance(msg, ApprovalRequest):
|
|
127
|
+
super_wire.soul_side.send(msg)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
event = SubagentEvent(
|
|
131
|
+
task_tool_call_id=current_tool_call_id,
|
|
132
|
+
event=msg,
|
|
133
|
+
)
|
|
134
|
+
super_wire.soul_side.send(event)
|
|
135
|
+
|
|
136
|
+
async def _ui_loop_fn(wire: WireUISide) -> None:
|
|
137
|
+
while True:
|
|
138
|
+
msg = await wire.receive()
|
|
139
|
+
_super_wire_send(msg)
|
|
140
|
+
|
|
102
141
|
subagent_history_file = await self._get_subagent_history_file()
|
|
103
142
|
context = Context(file_backend=subagent_history_file)
|
|
104
|
-
soul = KimiSoul(
|
|
105
|
-
agent,
|
|
106
|
-
agent_globals=self._agent_globals,
|
|
107
|
-
context=context,
|
|
108
|
-
loop_control=self._agent_globals.config.loop_control,
|
|
109
|
-
)
|
|
110
|
-
wire = get_wire_or_none()
|
|
111
|
-
assert wire is not None, "Wire is expected to be set"
|
|
112
|
-
sub_wire = _SubWire(wire)
|
|
143
|
+
soul = KimiSoul(agent, runtime=self._runtime, context=context)
|
|
113
144
|
|
|
114
145
|
try:
|
|
115
|
-
await soul
|
|
146
|
+
await run_soul(soul, prompt, _ui_loop_fn, asyncio.Event())
|
|
116
147
|
except MaxStepsReached as e:
|
|
117
148
|
return ToolError(
|
|
118
149
|
message=(
|
|
@@ -135,22 +166,10 @@ class Task(CallableTool2[Params]):
|
|
|
135
166
|
# Check if response is too brief, if so, run again with continuation prompt
|
|
136
167
|
n_attempts_remaining = MAX_CONTINUE_ATTEMPTS
|
|
137
168
|
if len(final_response) < 200 and n_attempts_remaining > 0:
|
|
138
|
-
await soul
|
|
169
|
+
await run_soul(soul, CONTINUE_PROMPT, _ui_loop_fn, asyncio.Event())
|
|
139
170
|
|
|
140
171
|
if len(context.history) == 0 or context.history[-1].role != "assistant":
|
|
141
172
|
return ToolError(message=_error_msg, brief="Failed to run subagent")
|
|
142
173
|
final_response = message_extract_text(context.history[-1])
|
|
143
174
|
|
|
144
175
|
return ToolOk(output=final_response)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class _SubWire(Wire):
|
|
148
|
-
def __init__(self, super_wire: Wire):
|
|
149
|
-
super().__init__()
|
|
150
|
-
self._super_wire = super_wire
|
|
151
|
-
|
|
152
|
-
@override
|
|
153
|
-
def send(self, msg: WireMessage):
|
|
154
|
-
if isinstance(msg, ApprovalRequest):
|
|
155
|
-
self._super_wire.send(msg)
|
|
156
|
-
# TODO: visualize subagent behavior by sending other messages in some way
|
kimi_cli/tools/task/task.md
CHANGED
|
@@ -4,7 +4,7 @@ Spawn a subagent to perform a specific task. Subagent will be spawned with a fre
|
|
|
4
4
|
|
|
5
5
|
Context isolation is one of the key benefits of using subagents. By delegating tasks to subagents, you can keep your main context clean and focused on the main goal requested by the user.
|
|
6
6
|
|
|
7
|
-
Here are some
|
|
7
|
+
Here are some scenarios you may want this tool for context isolation:
|
|
8
8
|
|
|
9
9
|
- You wrote some code and it did not work as expected. In this case you can spawn a subagent to fix the code, asking the subagent to return how it is fixed. This can potentially benefit because the detailed process of fixing the code may not be relevant to your main goal, and may clutter your context.
|
|
10
10
|
- When you need some latest knowledge of a specific library, framework or technology to proceed with your task, you can spawn a subagent to search on the internet for the needed information and return to you the gathered relevant information, for example code examples, API references, etc. This can avoid ton of irrelevant search results in your own context.
|
kimi_cli/tools/todo/__init__.py
CHANGED
|
@@ -4,6 +4,8 @@ from typing import Literal, override
|
|
|
4
4
|
from kosong.tooling import CallableTool2, ToolOk, ToolReturnType
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
|
+
from kimi_cli.tools.utils import load_desc
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
class Todo(BaseModel):
|
|
9
11
|
title: str = Field(description="The title of the todo", min_length=1)
|
|
@@ -16,7 +18,7 @@ class Params(BaseModel):
|
|
|
16
18
|
|
|
17
19
|
class SetTodoList(CallableTool2[Params]):
|
|
18
20
|
name: str = "SetTodoList"
|
|
19
|
-
description: str = (Path(__file__).parent / "set_todo_list.md")
|
|
21
|
+
description: str = load_desc(Path(__file__).parent / "set_todo_list.md")
|
|
20
22
|
params: type[Params] = Params
|
|
21
23
|
|
|
22
24
|
@override
|
|
@@ -24,4 +26,4 @@ class SetTodoList(CallableTool2[Params]):
|
|
|
24
26
|
rendered = ""
|
|
25
27
|
for todo in params.todos:
|
|
26
28
|
rendered += f"- {todo.title} [{todo.status}]\n"
|
|
27
|
-
return ToolOk(output=rendered)
|
|
29
|
+
return ToolOk(output="", message="Todo list updated", brief=rendered)
|
kimi_cli/tools/utils.py
CHANGED
|
@@ -7,7 +7,7 @@ from kosong.tooling import ToolError, ToolOk
|
|
|
7
7
|
|
|
8
8
|
def load_desc(path: Path, substitutions: dict[str, str] | None = None) -> str:
|
|
9
9
|
"""Load a tool description from a file, with optional substitutions."""
|
|
10
|
-
description = path.read_text()
|
|
10
|
+
description = path.read_text(encoding="utf-8")
|
|
11
11
|
if substitutions:
|
|
12
12
|
description = string.Template(description).substitute(substitutions)
|
|
13
13
|
return description
|
kimi_cli/tools/web/fetch.py
CHANGED
|
@@ -7,6 +7,7 @@ from kosong.tooling import CallableTool2, ToolReturnType
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
from kimi_cli.tools.utils import ToolResultBuilder, load_desc
|
|
10
|
+
from kimi_cli.utils.aiohttp import new_client_session
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class Params(BaseModel):
|
|
@@ -24,7 +25,7 @@ class FetchURL(CallableTool2[Params]):
|
|
|
24
25
|
|
|
25
26
|
try:
|
|
26
27
|
async with (
|
|
27
|
-
|
|
28
|
+
new_client_session() as session,
|
|
28
29
|
session.get(
|
|
29
30
|
params.url,
|
|
30
31
|
headers={
|
kimi_cli/tools/web/search.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import override
|
|
2
|
+
from typing import Any, override
|
|
3
3
|
|
|
4
|
-
import aiohttp
|
|
5
4
|
from kosong.tooling import CallableTool2, ToolReturnType
|
|
6
5
|
from pydantic import BaseModel, Field, ValidationError
|
|
7
6
|
|
|
8
|
-
import kimi_cli
|
|
9
7
|
from kimi_cli.config import Config
|
|
8
|
+
from kimi_cli.constant import USER_AGENT
|
|
10
9
|
from kimi_cli.soul.toolset import get_current_tool_call_or_none
|
|
10
|
+
from kimi_cli.tools import SkipThisTool
|
|
11
11
|
from kimi_cli.tools.utils import ToolResultBuilder, load_desc
|
|
12
|
+
from kimi_cli.utils.aiohttp import new_client_session
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class Params(BaseModel):
|
|
@@ -39,14 +40,13 @@ class SearchWeb(CallableTool2[Params]):
|
|
|
39
40
|
description: str = load_desc(Path(__file__).parent / "search.md", {})
|
|
40
41
|
params: type[Params] = Params
|
|
41
42
|
|
|
42
|
-
def __init__(self, config: Config, **kwargs):
|
|
43
|
+
def __init__(self, config: Config, **kwargs: Any):
|
|
43
44
|
super().__init__(**kwargs)
|
|
44
|
-
if config.services.moonshot_search is
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
self._api_key = ""
|
|
45
|
+
if config.services.moonshot_search is None:
|
|
46
|
+
raise SkipThisTool()
|
|
47
|
+
self._base_url = config.services.moonshot_search.base_url
|
|
48
|
+
self._api_key = config.services.moonshot_search.api_key.get_secret_value()
|
|
49
|
+
self._custom_headers = config.services.moonshot_search.custom_headers or {}
|
|
50
50
|
|
|
51
51
|
@override
|
|
52
52
|
async def __call__(self, params: Params) -> ToolReturnType:
|
|
@@ -62,13 +62,14 @@ class SearchWeb(CallableTool2[Params]):
|
|
|
62
62
|
assert tool_call is not None, "Tool call is expected to be set"
|
|
63
63
|
|
|
64
64
|
async with (
|
|
65
|
-
|
|
65
|
+
new_client_session() as session,
|
|
66
66
|
session.post(
|
|
67
67
|
self._base_url,
|
|
68
68
|
headers={
|
|
69
|
-
"User-Agent":
|
|
69
|
+
"User-Agent": USER_AGENT,
|
|
70
70
|
"Authorization": f"Bearer {self._api_key}",
|
|
71
71
|
"X-Msh-Tool-Call-Id": tool_call.id,
|
|
72
|
+
**self._custom_headers,
|
|
72
73
|
},
|
|
73
74
|
json={
|
|
74
75
|
"text_query": params.query,
|
kimi_cli/ui/__init__.py
CHANGED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import contextlib
|
|
3
|
-
from collections.abc import Callable, Coroutine
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from kimi_cli.soul import Soul
|
|
7
|
-
from kimi_cli.soul.wire import Wire
|
|
8
|
-
from kimi_cli.utils.logging import logger
|
|
9
|
-
|
|
10
|
-
type UILoopFn = Callable[[Wire], Coroutine[Any, Any, None]]
|
|
11
|
-
"""A long-running async function to visualize the agent behavior."""
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class RunCancelled(Exception):
|
|
15
|
-
"""The run was cancelled by the cancel event."""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
async def run_soul(
|
|
19
|
-
soul: Soul,
|
|
20
|
-
user_input: str,
|
|
21
|
-
ui_loop_fn: UILoopFn,
|
|
22
|
-
cancel_event: asyncio.Event,
|
|
23
|
-
):
|
|
24
|
-
"""
|
|
25
|
-
Run the soul with the given user input.
|
|
26
|
-
|
|
27
|
-
`cancel_event` is a outside handle that can be used to cancel the run. When the event is set,
|
|
28
|
-
the run will be gracefully stopped and a `RunCancelled` will be raised.
|
|
29
|
-
|
|
30
|
-
Raises:
|
|
31
|
-
ChatProviderError: When the LLM provider returns an error.
|
|
32
|
-
MaxStepsReached: When the maximum number of steps is reached.
|
|
33
|
-
RunCancelled: When the run is cancelled by the cancel event.
|
|
34
|
-
"""
|
|
35
|
-
wire = Wire()
|
|
36
|
-
logger.debug("Starting UI loop with function: {ui_loop_fn}", ui_loop_fn=ui_loop_fn)
|
|
37
|
-
|
|
38
|
-
ui_task = asyncio.create_task(ui_loop_fn(wire))
|
|
39
|
-
soul_task = asyncio.create_task(soul.run(user_input, wire))
|
|
40
|
-
|
|
41
|
-
cancel_event_task = asyncio.create_task(cancel_event.wait())
|
|
42
|
-
await asyncio.wait(
|
|
43
|
-
[soul_task, cancel_event_task],
|
|
44
|
-
return_when=asyncio.FIRST_COMPLETED,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
if cancel_event.is_set():
|
|
49
|
-
logger.debug("Cancelling the run task")
|
|
50
|
-
soul_task.cancel()
|
|
51
|
-
try:
|
|
52
|
-
await soul_task
|
|
53
|
-
except asyncio.CancelledError:
|
|
54
|
-
raise RunCancelled from None
|
|
55
|
-
else:
|
|
56
|
-
assert soul_task.done() # either stop event is set or the run task is done
|
|
57
|
-
cancel_event_task.cancel()
|
|
58
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
59
|
-
await cancel_event_task
|
|
60
|
-
soul_task.result() # this will raise if any exception was raised in the run task
|
|
61
|
-
finally:
|
|
62
|
-
logger.debug("Shutting down the visualization loop")
|
|
63
|
-
# shutting down the event queue should break the visualization loop
|
|
64
|
-
wire.shutdown()
|
|
65
|
-
try:
|
|
66
|
-
await asyncio.wait_for(ui_task, timeout=0.5)
|
|
67
|
-
except TimeoutError:
|
|
68
|
-
logger.warning("Visualization loop timed out")
|