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.

Files changed (137) hide show
  1. kimi_cli/CHANGELOG.md +349 -40
  2. kimi_cli/__init__.py +6 -0
  3. kimi_cli/acp/AGENTS.md +91 -0
  4. kimi_cli/acp/__init__.py +13 -0
  5. kimi_cli/acp/convert.py +111 -0
  6. kimi_cli/acp/kaos.py +270 -0
  7. kimi_cli/acp/mcp.py +46 -0
  8. kimi_cli/acp/server.py +335 -0
  9. kimi_cli/acp/session.py +445 -0
  10. kimi_cli/acp/tools.py +158 -0
  11. kimi_cli/acp/types.py +13 -0
  12. kimi_cli/agents/default/agent.yaml +4 -4
  13. kimi_cli/agents/default/sub.yaml +2 -1
  14. kimi_cli/agents/default/system.md +79 -21
  15. kimi_cli/agents/okabe/agent.yaml +17 -0
  16. kimi_cli/agentspec.py +53 -25
  17. kimi_cli/app.py +180 -52
  18. kimi_cli/cli/__init__.py +595 -0
  19. kimi_cli/cli/__main__.py +8 -0
  20. kimi_cli/cli/info.py +63 -0
  21. kimi_cli/cli/mcp.py +349 -0
  22. kimi_cli/config.py +153 -17
  23. kimi_cli/constant.py +3 -0
  24. kimi_cli/exception.py +23 -2
  25. kimi_cli/flow/__init__.py +117 -0
  26. kimi_cli/flow/d2.py +376 -0
  27. kimi_cli/flow/mermaid.py +218 -0
  28. kimi_cli/llm.py +129 -23
  29. kimi_cli/metadata.py +32 -7
  30. kimi_cli/platforms.py +262 -0
  31. kimi_cli/prompts/__init__.py +2 -0
  32. kimi_cli/prompts/compact.md +4 -5
  33. kimi_cli/session.py +223 -31
  34. kimi_cli/share.py +2 -0
  35. kimi_cli/skill.py +145 -0
  36. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  37. kimi_cli/skills/skill-creator/SKILL.md +351 -0
  38. kimi_cli/soul/__init__.py +51 -20
  39. kimi_cli/soul/agent.py +213 -85
  40. kimi_cli/soul/approval.py +86 -17
  41. kimi_cli/soul/compaction.py +64 -53
  42. kimi_cli/soul/context.py +38 -5
  43. kimi_cli/soul/denwarenji.py +2 -0
  44. kimi_cli/soul/kimisoul.py +442 -60
  45. kimi_cli/soul/message.py +54 -54
  46. kimi_cli/soul/slash.py +72 -0
  47. kimi_cli/soul/toolset.py +387 -6
  48. kimi_cli/toad.py +74 -0
  49. kimi_cli/tools/AGENTS.md +5 -0
  50. kimi_cli/tools/__init__.py +42 -34
  51. kimi_cli/tools/display.py +25 -0
  52. kimi_cli/tools/dmail/__init__.py +10 -10
  53. kimi_cli/tools/dmail/dmail.md +11 -9
  54. kimi_cli/tools/file/__init__.py +1 -3
  55. kimi_cli/tools/file/glob.py +20 -23
  56. kimi_cli/tools/file/grep.md +1 -1
  57. kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
  58. kimi_cli/tools/file/read.md +24 -6
  59. kimi_cli/tools/file/read.py +134 -50
  60. kimi_cli/tools/file/replace.md +1 -1
  61. kimi_cli/tools/file/replace.py +36 -29
  62. kimi_cli/tools/file/utils.py +282 -0
  63. kimi_cli/tools/file/write.py +43 -22
  64. kimi_cli/tools/multiagent/__init__.py +7 -0
  65. kimi_cli/tools/multiagent/create.md +11 -0
  66. kimi_cli/tools/multiagent/create.py +50 -0
  67. kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
  68. kimi_cli/tools/shell/__init__.py +120 -0
  69. kimi_cli/tools/{bash → shell}/bash.md +1 -2
  70. kimi_cli/tools/shell/powershell.md +25 -0
  71. kimi_cli/tools/test.py +4 -4
  72. kimi_cli/tools/think/__init__.py +2 -2
  73. kimi_cli/tools/todo/__init__.py +14 -8
  74. kimi_cli/tools/utils.py +64 -24
  75. kimi_cli/tools/web/fetch.py +68 -13
  76. kimi_cli/tools/web/search.py +10 -12
  77. kimi_cli/ui/acp/__init__.py +65 -412
  78. kimi_cli/ui/print/__init__.py +37 -49
  79. kimi_cli/ui/print/visualize.py +179 -0
  80. kimi_cli/ui/shell/__init__.py +141 -84
  81. kimi_cli/ui/shell/console.py +2 -0
  82. kimi_cli/ui/shell/debug.py +28 -23
  83. kimi_cli/ui/shell/keyboard.py +5 -1
  84. kimi_cli/ui/shell/prompt.py +220 -194
  85. kimi_cli/ui/shell/replay.py +111 -46
  86. kimi_cli/ui/shell/setup.py +89 -82
  87. kimi_cli/ui/shell/slash.py +422 -0
  88. kimi_cli/ui/shell/update.py +4 -2
  89. kimi_cli/ui/shell/usage.py +271 -0
  90. kimi_cli/ui/shell/visualize.py +574 -72
  91. kimi_cli/ui/wire/__init__.py +267 -0
  92. kimi_cli/ui/wire/jsonrpc.py +142 -0
  93. kimi_cli/ui/wire/protocol.py +1 -0
  94. kimi_cli/utils/__init__.py +0 -0
  95. kimi_cli/utils/aiohttp.py +2 -0
  96. kimi_cli/utils/aioqueue.py +72 -0
  97. kimi_cli/utils/broadcast.py +37 -0
  98. kimi_cli/utils/changelog.py +12 -7
  99. kimi_cli/utils/clipboard.py +12 -0
  100. kimi_cli/utils/datetime.py +37 -0
  101. kimi_cli/utils/environment.py +58 -0
  102. kimi_cli/utils/envvar.py +12 -0
  103. kimi_cli/utils/frontmatter.py +44 -0
  104. kimi_cli/utils/logging.py +7 -6
  105. kimi_cli/utils/message.py +9 -14
  106. kimi_cli/utils/path.py +99 -9
  107. kimi_cli/utils/pyinstaller.py +6 -0
  108. kimi_cli/utils/rich/__init__.py +33 -0
  109. kimi_cli/utils/rich/columns.py +99 -0
  110. kimi_cli/utils/rich/markdown.py +961 -0
  111. kimi_cli/utils/rich/markdown_sample.md +108 -0
  112. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  113. kimi_cli/utils/signals.py +2 -0
  114. kimi_cli/utils/slashcmd.py +124 -0
  115. kimi_cli/utils/string.py +2 -0
  116. kimi_cli/utils/term.py +168 -0
  117. kimi_cli/utils/typing.py +20 -0
  118. kimi_cli/wire/__init__.py +98 -29
  119. kimi_cli/wire/serde.py +45 -0
  120. kimi_cli/wire/types.py +299 -0
  121. kimi_cli-0.78.dist-info/METADATA +200 -0
  122. kimi_cli-0.78.dist-info/RECORD +135 -0
  123. kimi_cli-0.78.dist-info/entry_points.txt +4 -0
  124. kimi_cli/cli.py +0 -250
  125. kimi_cli/soul/runtime.py +0 -96
  126. kimi_cli/tools/bash/__init__.py +0 -99
  127. kimi_cli/tools/file/patch.md +0 -8
  128. kimi_cli/tools/file/patch.py +0 -143
  129. kimi_cli/tools/mcp.py +0 -85
  130. kimi_cli/ui/shell/liveview.py +0 -386
  131. kimi_cli/ui/shell/metacmd.py +0 -262
  132. kimi_cli/wire/message.py +0 -91
  133. kimi_cli-0.44.dist-info/METADATA +0 -188
  134. kimi_cli-0.44.dist-info/RECORD +0 -89
  135. kimi_cli-0.44.dist-info/entry_points.txt +0 -3
  136. /kimi_cli/tools/{task → multiagent}/task.md +0 -0
  137. {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
@@ -1,14 +1,17 @@
1
1
  from pathlib import Path
2
2
  from typing import Literal, override
3
3
 
4
- import aiofiles
5
- from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
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.soul.runtime import BuiltinSystemPromptArgs
10
+ from kimi_cli.tools.display import DisplayBlock
10
11
  from kimi_cli.tools.file import FileActions
11
- from kimi_cli.tools.utils import ToolRejectedError
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").read_text(encoding="utf-8")
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, **kwargs):
33
- super().__init__(**kwargs)
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: Path) -> ToolError | None:
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.resolve()
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 str(resolved_path).startswith(str(resolved_work_dir)):
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) -> ToolReturnType:
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 = Path(params.path)
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
- async with aiofiles.open(p, mode=file_mode, encoding="utf-8") as f:
105
- await f.write(params.content)
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 ToolOk(
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,7 @@
1
+ from .create import CreateSubagent
2
+ from .task import Task
3
+
4
+ __all__ = [
5
+ "Task",
6
+ "CreateSubagent",
7
+ ]
@@ -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, ToolReturnType
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, load_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.runtime import Runtime
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 WireUISide
18
- from kimi_cli.wire.message import ApprovalRequest, WireMessage
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, agent_spec: ResolvedAgentSpec, runtime: Runtime, **kwargs):
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}`: {spec.description}"
59
- for name, spec in agent_spec.subagents.items()
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
- try:
71
- loop = asyncio.get_running_loop()
72
- self._load_task = loop.create_task(self._load_subagents(agent_spec.subagents))
73
- except RuntimeError:
74
- # In case there's no running event loop, e.g., during synchronous tests
75
- self._load_task = None
76
- asyncio.run(self._load_subagents(agent_spec.subagents))
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 sub_history_file is not None
93
- return sub_history_file
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) -> ToolReturnType:
97
- if self._load_task is not None:
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 self._subagents:
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 = self._subagents[params.subagent_name]
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) -> ToolReturnType:
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
- # TODO: visualize subagent behavior by sending other messages in some way
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: WireUISide) -> None:
120
+ async def _ui_loop_fn(wire: Wire) -> None:
121
+ wire_ui = wire.ui_side(merge=True)
127
122
  while True:
128
- msg = await wire.receive()
123
+ msg = await wire_ui.receive()
129
124
  _super_wire_send(msg)
130
125
 
131
- subagent_history_file = await self._get_subagent_history_file()
132
- context = Context(file_backend=subagent_history_file)
133
- soul = KimiSoul(agent, runtime=self._runtime, context=context)
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 = message_extract_text(context.history[-1])
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 = message_extract_text(context.history[-1])
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 shell command. Use this tool to explore the filesystem, edit files, run scripts, get system information, etc.
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, ToolReturnType
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) -> ToolReturnType:
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) -> ToolReturnType:
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) -> ToolReturnType:
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")
@@ -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, ToolReturnType
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) -> ToolReturnType:
20
+ async def __call__(self, params: Params) -> ToolReturnValue:
21
21
  return ToolOk(output="", message="Thought logged")
@@ -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, ToolOk, ToolReturnType
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["Pending", "In Progress", "Done"] = Field(description="The status of the todo")
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").read_text(encoding="utf-8")
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) -> ToolReturnType:
24
- rendered = ""
25
- for todo in params.todos:
26
- rendered += f"- {todo.title} [{todo.status}]\n"
27
- return ToolOk(output=rendered)
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
+ )