kimi-cli 0.35__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.

Files changed (76) hide show
  1. kimi_cli/CHANGELOG.md +304 -0
  2. kimi_cli/__init__.py +374 -0
  3. kimi_cli/agent.py +261 -0
  4. kimi_cli/agents/koder/README.md +3 -0
  5. kimi_cli/agents/koder/agent.yaml +24 -0
  6. kimi_cli/agents/koder/sub.yaml +11 -0
  7. kimi_cli/agents/koder/system.md +72 -0
  8. kimi_cli/config.py +138 -0
  9. kimi_cli/llm.py +8 -0
  10. kimi_cli/metadata.py +117 -0
  11. kimi_cli/prompts/metacmds/__init__.py +4 -0
  12. kimi_cli/prompts/metacmds/compact.md +74 -0
  13. kimi_cli/prompts/metacmds/init.md +21 -0
  14. kimi_cli/py.typed +0 -0
  15. kimi_cli/share.py +8 -0
  16. kimi_cli/soul/__init__.py +59 -0
  17. kimi_cli/soul/approval.py +69 -0
  18. kimi_cli/soul/context.py +142 -0
  19. kimi_cli/soul/denwarenji.py +37 -0
  20. kimi_cli/soul/kimisoul.py +248 -0
  21. kimi_cli/soul/message.py +76 -0
  22. kimi_cli/soul/toolset.py +25 -0
  23. kimi_cli/soul/wire.py +101 -0
  24. kimi_cli/tools/__init__.py +85 -0
  25. kimi_cli/tools/bash/__init__.py +97 -0
  26. kimi_cli/tools/bash/bash.md +31 -0
  27. kimi_cli/tools/dmail/__init__.py +38 -0
  28. kimi_cli/tools/dmail/dmail.md +15 -0
  29. kimi_cli/tools/file/__init__.py +21 -0
  30. kimi_cli/tools/file/glob.md +17 -0
  31. kimi_cli/tools/file/glob.py +149 -0
  32. kimi_cli/tools/file/grep.md +5 -0
  33. kimi_cli/tools/file/grep.py +285 -0
  34. kimi_cli/tools/file/patch.md +8 -0
  35. kimi_cli/tools/file/patch.py +131 -0
  36. kimi_cli/tools/file/read.md +14 -0
  37. kimi_cli/tools/file/read.py +139 -0
  38. kimi_cli/tools/file/replace.md +7 -0
  39. kimi_cli/tools/file/replace.py +132 -0
  40. kimi_cli/tools/file/write.md +5 -0
  41. kimi_cli/tools/file/write.py +107 -0
  42. kimi_cli/tools/mcp.py +85 -0
  43. kimi_cli/tools/task/__init__.py +156 -0
  44. kimi_cli/tools/task/task.md +26 -0
  45. kimi_cli/tools/test.py +55 -0
  46. kimi_cli/tools/think/__init__.py +21 -0
  47. kimi_cli/tools/think/think.md +1 -0
  48. kimi_cli/tools/todo/__init__.py +27 -0
  49. kimi_cli/tools/todo/set_todo_list.md +15 -0
  50. kimi_cli/tools/utils.py +150 -0
  51. kimi_cli/tools/web/__init__.py +4 -0
  52. kimi_cli/tools/web/fetch.md +1 -0
  53. kimi_cli/tools/web/fetch.py +94 -0
  54. kimi_cli/tools/web/search.md +1 -0
  55. kimi_cli/tools/web/search.py +126 -0
  56. kimi_cli/ui/__init__.py +68 -0
  57. kimi_cli/ui/acp/__init__.py +441 -0
  58. kimi_cli/ui/print/__init__.py +176 -0
  59. kimi_cli/ui/shell/__init__.py +326 -0
  60. kimi_cli/ui/shell/console.py +3 -0
  61. kimi_cli/ui/shell/liveview.py +158 -0
  62. kimi_cli/ui/shell/metacmd.py +309 -0
  63. kimi_cli/ui/shell/prompt.py +574 -0
  64. kimi_cli/ui/shell/setup.py +192 -0
  65. kimi_cli/ui/shell/update.py +204 -0
  66. kimi_cli/utils/changelog.py +101 -0
  67. kimi_cli/utils/logging.py +18 -0
  68. kimi_cli/utils/message.py +8 -0
  69. kimi_cli/utils/path.py +23 -0
  70. kimi_cli/utils/provider.py +64 -0
  71. kimi_cli/utils/pyinstaller.py +24 -0
  72. kimi_cli/utils/string.py +12 -0
  73. kimi_cli-0.35.dist-info/METADATA +24 -0
  74. kimi_cli-0.35.dist-info/RECORD +76 -0
  75. kimi_cli-0.35.dist-info/WHEEL +4 -0
  76. kimi_cli-0.35.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,132 @@
1
+ from pathlib import Path
2
+ from typing import override
3
+
4
+ import aiofiles
5
+ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
6
+ from pydantic import BaseModel, Field
7
+
8
+ from kimi_cli.agent import BuiltinSystemPromptArgs
9
+
10
+
11
+ class Edit(BaseModel):
12
+ old: str = Field(description="The old string to replace. Can be multi-line.")
13
+ new: str = Field(description="The new string to replace with. Can be multi-line.")
14
+ replace_all: bool = Field(description="Whether to replace all occurrences.", default=False)
15
+
16
+
17
+ class Params(BaseModel):
18
+ path: str = Field(description="The absolute path to the file to edit.")
19
+ edit: Edit | list[Edit] = Field(
20
+ description=(
21
+ "The edit(s) to apply to the file. "
22
+ "You can provide a single edit or a list of edits here."
23
+ )
24
+ )
25
+
26
+
27
+ class StrReplaceFile(CallableTool2[Params]):
28
+ name: str = "StrReplaceFile"
29
+ description: str = (Path(__file__).parent / "replace.md").read_text()
30
+ params: type[Params] = Params
31
+
32
+ def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
33
+ super().__init__(**kwargs)
34
+ self._work_dir = builtin_args.KIMI_WORK_DIR
35
+
36
+ def _validate_path(self, path: Path) -> ToolError | None:
37
+ """Validate that the path is safe to edit."""
38
+ # Check for path traversal attempts
39
+ resolved_path = path.resolve()
40
+ resolved_work_dir = self._work_dir.resolve()
41
+
42
+ # Ensure the path is within work directory
43
+ if not str(resolved_path).startswith(str(resolved_work_dir)):
44
+ return ToolError(
45
+ message=(
46
+ f"`{path}` is outside the working directory. "
47
+ "You can only edit files within the working directory."
48
+ ),
49
+ brief="Path outside working directory",
50
+ )
51
+ return None
52
+
53
+ def _apply_edit(self, content: str, edit: Edit) -> str:
54
+ """Apply a single edit to the content."""
55
+ if edit.replace_all:
56
+ return content.replace(edit.old, edit.new)
57
+ else:
58
+ return content.replace(edit.old, edit.new, 1)
59
+
60
+ @override
61
+ async def __call__(self, params: Params) -> ToolReturnType:
62
+ try:
63
+ p = Path(params.path)
64
+
65
+ if not p.is_absolute():
66
+ return ToolError(
67
+ message=(
68
+ f"`{params.path}` is not an absolute path. "
69
+ "You must provide an absolute path to edit a file."
70
+ ),
71
+ brief="Invalid path",
72
+ )
73
+
74
+ # Validate path safety
75
+ path_error = self._validate_path(p)
76
+ if path_error:
77
+ return path_error
78
+
79
+ if not p.exists():
80
+ return ToolError(
81
+ message=f"`{params.path}` does not exist.",
82
+ brief="File not found",
83
+ )
84
+ if not p.is_file():
85
+ return ToolError(
86
+ message=f"`{params.path}` is not a file.",
87
+ brief="Invalid path",
88
+ )
89
+
90
+ # Read the file content
91
+ async with aiofiles.open(p, encoding="utf-8", errors="replace") as f:
92
+ content = await f.read()
93
+
94
+ original_content = content
95
+ edits = [params.edit] if isinstance(params.edit, Edit) else params.edit
96
+
97
+ # Apply all edits
98
+ for edit in edits:
99
+ content = self._apply_edit(content, edit)
100
+
101
+ # Check if any changes were made
102
+ if content == original_content:
103
+ return ToolError(
104
+ message="No replacements were made. The old string was not found in the file.",
105
+ brief="No replacements made",
106
+ )
107
+
108
+ # Write the modified content back to the file
109
+ async with aiofiles.open(p, mode="w", encoding="utf-8") as f:
110
+ await f.write(content)
111
+
112
+ # Count changes for success message
113
+ total_replacements = 0
114
+ for edit in edits:
115
+ if edit.replace_all:
116
+ total_replacements += original_content.count(edit.old)
117
+ else:
118
+ total_replacements += 1 if edit.old in original_content else 0
119
+
120
+ return ToolOk(
121
+ output="",
122
+ message=(
123
+ f"File successfully edited. "
124
+ f"Applied {len(edits)} edit(s) with {total_replacements} total replacement(s)."
125
+ ),
126
+ )
127
+
128
+ except Exception as e:
129
+ return ToolError(
130
+ message=f"Failed to edit. Error: {e}",
131
+ brief="Failed to edit file",
132
+ )
@@ -0,0 +1,5 @@
1
+ Write content to a file.
2
+
3
+ **Tips:**
4
+ - When `mode` is not specified, it defaults to `overwrite`. Always write with caution.
5
+ - When the content to write is too long (e.g. > 100 lines), use this tool multiple times instead of a single call. Use `overwrite` mode at the first time, then use `append` mode after the first write.
@@ -0,0 +1,107 @@
1
+ from pathlib import Path
2
+ from typing import Literal, override
3
+
4
+ import aiofiles
5
+ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
6
+ from pydantic import BaseModel, Field
7
+
8
+ from kimi_cli.agent import BuiltinSystemPromptArgs
9
+
10
+
11
+ class Params(BaseModel):
12
+ path: str = Field(description="The absolute path to the file to write")
13
+ content: str = Field(description="The content to write to the file")
14
+ mode: Literal["overwrite", "append"] = Field(
15
+ description=(
16
+ "The mode to use to write to the file. "
17
+ "Two modes are supported: `overwrite` for overwriting the whole file and "
18
+ "`append` for appending to the end of an existing file."
19
+ ),
20
+ default="overwrite",
21
+ )
22
+
23
+
24
+ class WriteFile(CallableTool2[Params]):
25
+ name: str = "WriteFile"
26
+ description: str = (Path(__file__).parent / "write.md").read_text()
27
+ params: type[Params] = Params
28
+
29
+ def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
30
+ super().__init__(**kwargs)
31
+ self._work_dir = builtin_args.KIMI_WORK_DIR
32
+
33
+ def _validate_path(self, path: Path) -> ToolError | None:
34
+ """Validate that the path is safe to write."""
35
+ # Check for path traversal attempts
36
+ resolved_path = path.resolve()
37
+ resolved_work_dir = self._work_dir.resolve()
38
+
39
+ # Ensure the path is within work directory
40
+ if not str(resolved_path).startswith(str(resolved_work_dir)):
41
+ return ToolError(
42
+ message=(
43
+ f"`{path}` is outside the working directory. "
44
+ "You can only write files within the working directory."
45
+ ),
46
+ brief="Path outside working directory",
47
+ )
48
+ return None
49
+
50
+ @override
51
+ async def __call__(self, params: Params) -> ToolReturnType:
52
+ # TODO: checks:
53
+ # - check if the path may contain secrets
54
+ # - check if the file format is writable
55
+ try:
56
+ p = Path(params.path)
57
+
58
+ if not p.is_absolute():
59
+ return ToolError(
60
+ message=(
61
+ f"`{params.path}` is not an absolute path. "
62
+ "You must provide an absolute path to write a file."
63
+ ),
64
+ brief="Invalid path",
65
+ )
66
+
67
+ # Validate path safety
68
+ path_error = self._validate_path(p)
69
+ if path_error:
70
+ return path_error
71
+
72
+ if not p.parent.exists():
73
+ return ToolError(
74
+ message=f"`{params.path}` parent directory does not exist.",
75
+ brief="Parent directory not found",
76
+ )
77
+
78
+ # Validate mode parameter
79
+ if params.mode not in ["overwrite", "append"]:
80
+ return ToolError(
81
+ message=(
82
+ f"Invalid write mode: `{params.mode}`. "
83
+ "Mode must be either `overwrite` or `append`."
84
+ ),
85
+ brief="Invalid write mode",
86
+ )
87
+
88
+ # Determine file mode for aiofiles
89
+ file_mode = "w" if params.mode == "overwrite" else "a"
90
+
91
+ # Write content to file
92
+ async with aiofiles.open(p, mode=file_mode, encoding="utf-8") as f:
93
+ await f.write(params.content)
94
+
95
+ # Get file info for success message
96
+ file_size = p.stat().st_size
97
+ action = "overwritten" if params.mode == "overwrite" else "appended to"
98
+ return ToolOk(
99
+ output="",
100
+ message=(f"File successfully {action}. Current size: {file_size} bytes."),
101
+ )
102
+
103
+ except Exception as e:
104
+ return ToolError(
105
+ message=f"Failed to write to {params.path}. Error: {e}",
106
+ brief="Failed to write file",
107
+ )
kimi_cli/tools/mcp.py ADDED
@@ -0,0 +1,85 @@
1
+ import fastmcp
2
+ import mcp
3
+ from fastmcp.client.client import CallToolResult
4
+ from kosong.base.message import AudioURLPart, ContentPart, ImageURLPart, TextPart
5
+ from kosong.tooling import CallableTool, ToolOk, ToolReturnType
6
+
7
+
8
+ class MCPTool(CallableTool):
9
+ def __init__(self, mcp_tool: mcp.Tool, client: fastmcp.Client, **kwargs):
10
+ super().__init__(
11
+ name=mcp_tool.name,
12
+ description=mcp_tool.description or "",
13
+ parameters=mcp_tool.inputSchema,
14
+ **kwargs,
15
+ )
16
+ self._mcp_tool = mcp_tool
17
+ self._client = client
18
+
19
+ async def __call__(self, *args, **kwargs) -> ToolReturnType:
20
+ async with self._client as client:
21
+ result = await client.call_tool(self._mcp_tool.name, kwargs, timeout=20)
22
+ return convert_tool_result(result)
23
+
24
+
25
+ def convert_tool_result(result: CallToolResult) -> ToolReturnType:
26
+ content: list[ContentPart] = []
27
+ for part in result.content:
28
+ match part:
29
+ case mcp.types.TextContent(text=text):
30
+ content.append(TextPart(text=text))
31
+ case mcp.types.ImageContent(data=data, mimeType=mimeType):
32
+ content.append(
33
+ ImageURLPart(
34
+ image_url=ImageURLPart.ImageURL(url=f"data:{mimeType};base64,{data}")
35
+ )
36
+ )
37
+ case mcp.types.AudioContent(data=data, mimeType=mimeType):
38
+ content.append(
39
+ AudioURLPart(
40
+ audio_url=AudioURLPart.AudioURL(url=f"data:{mimeType};base64,{data}")
41
+ )
42
+ )
43
+ case mcp.types.EmbeddedResource(
44
+ resource=mcp.types.BlobResourceContents(uri=_uri, mimeType=mimeType, blob=blob)
45
+ ):
46
+ mimeType = mimeType or "application/octet-stream"
47
+ if mimeType.startswith("image/"):
48
+ content.append(
49
+ ImageURLPart(
50
+ type="image_url",
51
+ image_url=ImageURLPart.ImageURL(
52
+ url=f"data:{mimeType};base64,{blob}",
53
+ ),
54
+ )
55
+ )
56
+ elif mimeType.startswith("audio/"):
57
+ content.append(
58
+ AudioURLPart(
59
+ type="audio_url",
60
+ audio_url=AudioURLPart.AudioURL(url=f"data:{mimeType};base64,{blob}"),
61
+ )
62
+ )
63
+ else:
64
+ raise ValueError(f"Unsupported mime type: {mimeType}")
65
+ case mcp.types.ResourceLink(uri=uri, mimeType=mimeType, description=_description):
66
+ mimeType = mimeType or "application/octet-stream"
67
+ if mimeType.startswith("image/"):
68
+ content.append(
69
+ ImageURLPart(
70
+ type="image_url",
71
+ image_url=ImageURLPart.ImageURL(url=str(uri)),
72
+ )
73
+ )
74
+ elif mimeType.startswith("audio/"):
75
+ content.append(
76
+ AudioURLPart(
77
+ type="audio_url",
78
+ audio_url=AudioURLPart.AudioURL(url=str(uri)),
79
+ )
80
+ )
81
+ else:
82
+ raise ValueError(f"Unsupported mime type: {mimeType}")
83
+ case _:
84
+ raise ValueError(f"Unsupported MCP tool result part: {part}")
85
+ return ToolOk(output=content)
@@ -0,0 +1,156 @@
1
+ from pathlib import Path
2
+ from typing import override
3
+
4
+ from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
5
+ from pydantic import BaseModel, Field
6
+
7
+ from kimi_cli.agent import Agent, AgentGlobals, AgentSpec, load_agent
8
+ from kimi_cli.soul import MaxStepsReached
9
+ from kimi_cli.soul.context import Context
10
+ from kimi_cli.soul.kimisoul import KimiSoul
11
+ from kimi_cli.soul.wire import ApprovalRequest, Wire, WireMessage, get_wire_or_none
12
+ from kimi_cli.tools.utils import load_desc
13
+ from kimi_cli.utils.message import message_extract_text
14
+ from kimi_cli.utils.path import next_available_rotation
15
+
16
+ # Maximum continuation attempts for task summary
17
+ MAX_CONTINUE_ATTEMPTS = 1
18
+
19
+
20
+ CONTINUE_PROMPT = """
21
+ Your previous response was too brief. Please provide a more comprehensive summary that includes:
22
+
23
+ 1. Specific technical details and implementations
24
+ 2. Complete code examples if relevant
25
+ 3. Detailed findings and analysis
26
+ 4. All important information that should be aware of by the caller
27
+ """.strip()
28
+
29
+
30
+ class Params(BaseModel):
31
+ description: str = Field(description="A short (3-5 word) description of the task")
32
+ subagent_name: str = Field(
33
+ description="The name of the specialized subagent to use for this task"
34
+ )
35
+ prompt: str = Field(
36
+ description=(
37
+ "The task for the subagent to perform. "
38
+ "You must provide a detailed prompt with all necessary background information "
39
+ "because the subagent cannot see anything in your context."
40
+ )
41
+ )
42
+
43
+
44
+ class Task(CallableTool2[Params]):
45
+ name: str = "Task"
46
+ params: type[Params] = Params
47
+
48
+ def __init__(self, agent_spec: AgentSpec, agent_globals: AgentGlobals, **kwargs):
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
+
58
+ super().__init__(
59
+ description=load_desc(
60
+ Path(__file__).parent / "task.md",
61
+ {
62
+ "SUBAGENTS_MD": "\n".join(descs),
63
+ },
64
+ ),
65
+ **kwargs,
66
+ )
67
+
68
+ self._agent_globals = agent_globals
69
+ self._session = agent_globals.session
70
+ self._subagents = subagents
71
+
72
+ async def _get_subagent_history_file(self) -> Path:
73
+ """Generate a unique history file path for subagent."""
74
+ main_history_file = self._session.history_file
75
+ subagent_base_name = f"{main_history_file.stem}_sub"
76
+ main_history_file.parent.mkdir(parents=True, exist_ok=True) # just in case
77
+ sub_history_file = await next_available_rotation(
78
+ main_history_file.parent / f"{subagent_base_name}{main_history_file.suffix}"
79
+ )
80
+ assert sub_history_file is not None
81
+ return sub_history_file
82
+
83
+ @override
84
+ async def __call__(self, params: Params) -> ToolReturnType:
85
+ if params.subagent_name not in self._subagents:
86
+ return ToolError(
87
+ message=f"Subagent not found: {params.subagent_name}",
88
+ brief="Subagent not found",
89
+ )
90
+ agent = self._subagents[params.subagent_name]
91
+ try:
92
+ result = await self._run_subagent(agent, params.prompt)
93
+ return result
94
+ except Exception as e:
95
+ return ToolError(
96
+ message=f"Failed to run subagent: {e}",
97
+ brief="Failed to run subagent",
98
+ )
99
+
100
+ async def _run_subagent(self, agent: Agent, prompt: str) -> ToolReturnType:
101
+ """Run subagent with optional continuation for task summary."""
102
+ subagent_history_file = await self._get_subagent_history_file()
103
+ 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)
113
+
114
+ try:
115
+ await soul.run(prompt, sub_wire)
116
+ except MaxStepsReached as e:
117
+ return ToolError(
118
+ message=(
119
+ f"Max steps {e.n_steps} reached when running subagent. "
120
+ "Please try splitting the task into smaller subtasks."
121
+ ),
122
+ brief="Max steps reached",
123
+ )
124
+
125
+ _error_msg = (
126
+ "The subagent seemed not to run properly. Maybe you have to do the task yourself."
127
+ )
128
+
129
+ # Check if the subagent context is valid
130
+ if len(context.history) == 0 or context.history[-1].role != "assistant":
131
+ return ToolError(message=_error_msg, brief="Failed to run subagent")
132
+
133
+ final_response = message_extract_text(context.history[-1])
134
+
135
+ # Check if response is too brief, if so, run again with continuation prompt
136
+ n_attempts_remaining = MAX_CONTINUE_ATTEMPTS
137
+ if len(final_response) < 200 and n_attempts_remaining > 0:
138
+ await soul.run(CONTINUE_PROMPT, sub_wire)
139
+
140
+ if len(context.history) == 0 or context.history[-1].role != "assistant":
141
+ return ToolError(message=_error_msg, brief="Failed to run subagent")
142
+ final_response = message_extract_text(context.history[-1])
143
+
144
+ 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
@@ -0,0 +1,26 @@
1
+ Spawn a subagent to perform a specific task. Subagent will be spawned with a fresh context without any history of yours.
2
+
3
+ **Context Isolation**
4
+
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
+
7
+ Here are some scenerios you may want this tool for context isolation:
8
+
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
+ - 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.
11
+
12
+ DO NOT directly forward the user prompt to Task tool. DO NOT simply spawn Task tool for each todo item. This will cause the user confused because the user cannot see what the subagent do. Only you can see the response from the subagent. So, only spawn subagents for very specific and narrow tasks like fixing a compilation error, or searching for a specific solution.
13
+
14
+ **Parallel Multi-Tasking**
15
+
16
+ Parallel multi-tasking is another key benefit of this tool. When the user request involves multiple subtasks that are independent of each other, you can use Task tool multiple times in a single response to let subagents work in parallel for you.
17
+
18
+ Examples:
19
+
20
+ - User requests to code, refactor or fix multiple modules/files in a project, and they can be tested independently. In this case you can spawn multiple subagents each working on a different module/file.
21
+ - When you need to analyze a huge codebase (> hundreds of thousands of lines), you can spawn multiple subagents each exploring on a different part of the codebase and gather the summarized results.
22
+ - When you need to search the web for multiple queries, you can spawn multiple subagents for better efficiency.
23
+
24
+ **Available Subagents:**
25
+
26
+ ${SUBAGENTS_MD}
kimi_cli/tools/test.py ADDED
@@ -0,0 +1,55 @@
1
+ import asyncio
2
+ from typing import override
3
+
4
+ from kosong.tooling import CallableTool2, ToolOk, ToolReturnType
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class PlusParams(BaseModel):
9
+ a: float
10
+ b: float
11
+
12
+
13
+ class Plus(CallableTool2[PlusParams]):
14
+ name: str = "plus"
15
+ description: str = "Add two numbers"
16
+ params: type[PlusParams] = PlusParams
17
+
18
+ @override
19
+ async def __call__(self, params: PlusParams) -> ToolReturnType:
20
+ return ToolOk(output=str(params.a + params.b))
21
+
22
+
23
+ class CompareParams(BaseModel):
24
+ a: float
25
+ b: float
26
+
27
+
28
+ class Compare(CallableTool2[CompareParams]):
29
+ name: str = "compare"
30
+ description: str = "Compare two numbers"
31
+ params: type[CompareParams] = CompareParams
32
+
33
+ @override
34
+ async def __call__(self, params: CompareParams) -> ToolReturnType:
35
+ if params.a > params.b:
36
+ return ToolOk(output="greater")
37
+ elif params.a < params.b:
38
+ return ToolOk(output="less")
39
+ else:
40
+ return ToolOk(output="equal")
41
+
42
+
43
+ class PanicParams(BaseModel):
44
+ message: str
45
+
46
+
47
+ class Panic(CallableTool2[PanicParams]):
48
+ name: str = "panic"
49
+ description: str = "Raise an exception to cause the tool call to fail."
50
+ params: type[PanicParams] = PanicParams
51
+
52
+ @override
53
+ async def __call__(self, params: PanicParams) -> ToolReturnType:
54
+ await asyncio.sleep(2)
55
+ raise Exception(f"panicked with a message with {len(params.message)} characters")
@@ -0,0 +1,21 @@
1
+ from pathlib import Path
2
+ from typing import override
3
+
4
+ from kosong.tooling import CallableTool2, ToolOk, ToolReturnType
5
+ from pydantic import BaseModel, Field
6
+
7
+ from kimi_cli.tools.utils import load_desc
8
+
9
+
10
+ class Params(BaseModel):
11
+ thought: str = Field(description=("A thought to think about."))
12
+
13
+
14
+ class Think(CallableTool2[Params]):
15
+ name: str = "Think"
16
+ description: str = load_desc(Path(__file__).parent / "think.md", {})
17
+ params: type[Params] = Params
18
+
19
+ @override
20
+ async def __call__(self, params: Params) -> ToolReturnType:
21
+ return ToolOk(output="", message="Thought logged")
@@ -0,0 +1 @@
1
+ Use the tool to think about something. It will not obtain new information or change the database, but just append the thought to the log. Use it when complex reasoning or some cache memory is needed.
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+ from typing import Literal, override
3
+
4
+ from kosong.tooling import CallableTool2, ToolOk, ToolReturnType
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Todo(BaseModel):
9
+ title: str = Field(description="The title of the todo", min_length=1)
10
+ status: Literal["Pending", "In Progress", "Done"] = Field(description="The status of the todo")
11
+
12
+
13
+ class Params(BaseModel):
14
+ todos: list[Todo] = Field(description="The updated todo list")
15
+
16
+
17
+ class SetTodoList(CallableTool2[Params]):
18
+ name: str = "SetTodoList"
19
+ description: str = (Path(__file__).parent / "set_todo_list.md").read_text()
20
+ params: type[Params] = Params
21
+
22
+ @override
23
+ async def __call__(self, params: Params) -> ToolReturnType:
24
+ rendered = ""
25
+ for todo in params.todos:
26
+ rendered += f"- {todo.title} [{todo.status}]\n"
27
+ return ToolOk(output=rendered)
@@ -0,0 +1,15 @@
1
+ Update the whole todo list.
2
+
3
+ Todo list is a simple yet powerful tool to help you get things done. You typically want to use this tool when the given task involves multiple subtasks/milestones, or, multiple tasks are given in a single request. This tool can help you to break down the task and track the progress.
4
+
5
+ This is the only todo list tool available to you. That said, each time you want to operate on the todo list, you need to update the whole. Make sure to maintain the todo items and their statuses properly.
6
+
7
+ Once you finished a subtask/milestone, remember to update the todo list to reflect the progress. Also, you can give yourself a self-encouragement to keep you motivated.
8
+
9
+ Abusing this tool to track too small steps will just waste your time and make your context messy. For example, here are some cases you should not use this tool:
10
+
11
+ - When the user just simply ask you a question. E.g. "What language and framework is used in the project?", "What is the best practice for x?"
12
+ - When it only takes a few steps/tool calls to complete the task. E.g. "Fix the unit test function 'test_xxx'", "Refactor the function 'xxx' to make it more solid."
13
+ - When the user prompt is very specific and the only thing you need to do is brainlessly following the instructions. E.g. "Replace xxx to yyy in the file zzz", "Create a file xxx with content yyy."
14
+
15
+ However, do not get stuck in a rut. Be flexible. Sometimes, you may try to use todo list at first, then realize the task is too simple and you can simply stop using it; or, sometimes, you may realize the task is complex after a few steps and then you can start using todo list to break it down.