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.
Files changed (88) hide show
  1. kimi_cli/CHANGELOG.md +165 -0
  2. kimi_cli/__init__.py +0 -374
  3. kimi_cli/agents/{koder → default}/agent.yaml +1 -1
  4. kimi_cli/agents/{koder → default}/system.md +1 -1
  5. kimi_cli/agentspec.py +115 -0
  6. kimi_cli/app.py +208 -0
  7. kimi_cli/cli.py +321 -0
  8. kimi_cli/config.py +33 -16
  9. kimi_cli/constant.py +4 -0
  10. kimi_cli/exception.py +16 -0
  11. kimi_cli/llm.py +144 -3
  12. kimi_cli/metadata.py +6 -69
  13. kimi_cli/prompts/__init__.py +4 -0
  14. kimi_cli/session.py +103 -0
  15. kimi_cli/soul/__init__.py +130 -9
  16. kimi_cli/soul/agent.py +159 -0
  17. kimi_cli/soul/approval.py +5 -6
  18. kimi_cli/soul/compaction.py +106 -0
  19. kimi_cli/soul/context.py +1 -1
  20. kimi_cli/soul/kimisoul.py +180 -80
  21. kimi_cli/soul/message.py +6 -6
  22. kimi_cli/soul/runtime.py +96 -0
  23. kimi_cli/soul/toolset.py +3 -2
  24. kimi_cli/tools/__init__.py +35 -31
  25. kimi_cli/tools/bash/__init__.py +25 -9
  26. kimi_cli/tools/bash/cmd.md +31 -0
  27. kimi_cli/tools/dmail/__init__.py +5 -4
  28. kimi_cli/tools/file/__init__.py +8 -0
  29. kimi_cli/tools/file/glob.md +1 -1
  30. kimi_cli/tools/file/glob.py +4 -4
  31. kimi_cli/tools/file/grep.py +36 -19
  32. kimi_cli/tools/file/patch.py +52 -10
  33. kimi_cli/tools/file/read.py +6 -5
  34. kimi_cli/tools/file/replace.py +16 -4
  35. kimi_cli/tools/file/write.py +16 -4
  36. kimi_cli/tools/mcp.py +7 -4
  37. kimi_cli/tools/task/__init__.py +60 -41
  38. kimi_cli/tools/task/task.md +1 -1
  39. kimi_cli/tools/todo/__init__.py +4 -2
  40. kimi_cli/tools/utils.py +1 -1
  41. kimi_cli/tools/web/fetch.py +2 -1
  42. kimi_cli/tools/web/search.py +13 -12
  43. kimi_cli/ui/__init__.py +0 -68
  44. kimi_cli/ui/acp/__init__.py +67 -38
  45. kimi_cli/ui/print/__init__.py +46 -69
  46. kimi_cli/ui/shell/__init__.py +145 -154
  47. kimi_cli/ui/shell/console.py +27 -1
  48. kimi_cli/ui/shell/debug.py +187 -0
  49. kimi_cli/ui/shell/keyboard.py +183 -0
  50. kimi_cli/ui/shell/metacmd.py +34 -81
  51. kimi_cli/ui/shell/prompt.py +245 -28
  52. kimi_cli/ui/shell/replay.py +104 -0
  53. kimi_cli/ui/shell/setup.py +19 -19
  54. kimi_cli/ui/shell/update.py +11 -5
  55. kimi_cli/ui/shell/visualize.py +576 -0
  56. kimi_cli/ui/wire/README.md +109 -0
  57. kimi_cli/ui/wire/__init__.py +340 -0
  58. kimi_cli/ui/wire/jsonrpc.py +48 -0
  59. kimi_cli/utils/__init__.py +0 -0
  60. kimi_cli/utils/aiohttp.py +10 -0
  61. kimi_cli/utils/changelog.py +6 -2
  62. kimi_cli/utils/clipboard.py +10 -0
  63. kimi_cli/utils/message.py +15 -1
  64. kimi_cli/utils/rich/__init__.py +33 -0
  65. kimi_cli/utils/rich/markdown.py +959 -0
  66. kimi_cli/utils/rich/markdown_sample.md +108 -0
  67. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  68. kimi_cli/utils/signals.py +41 -0
  69. kimi_cli/utils/string.py +8 -0
  70. kimi_cli/utils/term.py +114 -0
  71. kimi_cli/wire/__init__.py +73 -0
  72. kimi_cli/wire/message.py +191 -0
  73. kimi_cli-0.52.dist-info/METADATA +186 -0
  74. kimi_cli-0.52.dist-info/RECORD +99 -0
  75. kimi_cli-0.52.dist-info/entry_points.txt +3 -0
  76. kimi_cli/agent.py +0 -261
  77. kimi_cli/agents/koder/README.md +0 -3
  78. kimi_cli/prompts/metacmds/__init__.py +0 -4
  79. kimi_cli/soul/wire.py +0 -101
  80. kimi_cli/ui/shell/liveview.py +0 -158
  81. kimi_cli/utils/provider.py +0 -64
  82. kimi_cli-0.35.dist-info/METADATA +0 -24
  83. kimi_cli-0.35.dist-info/RECORD +0 -76
  84. kimi_cli-0.35.dist-info/entry_points.txt +0 -3
  85. /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
  86. /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
  87. /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
  88. {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
@@ -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.agent import BuiltinSystemPromptArgs
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
  ):
@@ -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.agent import BuiltinSystemPromptArgs
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").read_text()
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()
@@ -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.agent import BuiltinSystemPromptArgs
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").read_text()
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 kosong.base.message import AudioURLPart, ContentPart, ImageURLPart, TextPart
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)
@@ -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.agent import Agent, AgentGlobals, AgentSpec, load_agent
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.wire import ApprovalRequest, Wire, WireMessage, get_wire_or_none
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: 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
-
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(descs),
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._agent_globals = agent_globals
69
- self._session = agent_globals.session
70
- self._subagents = 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.run(prompt, sub_wire)
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.run(CONTINUE_PROMPT, sub_wire)
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
@@ -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 scenerios you may want this tool for context isolation:
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.
@@ -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").read_text()
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
@@ -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
- aiohttp.ClientSession() as session,
28
+ new_client_session() as session,
28
29
  session.get(
29
30
  params.url,
30
31
  headers={
@@ -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 not None:
45
- self._base_url = config.services.moonshot_search.base_url
46
- self._api_key = config.services.moonshot_search.api_key.get_secret_value()
47
- else:
48
- self._base_url = ""
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
- aiohttp.ClientSession() as session,
65
+ new_client_session() as session,
66
66
  session.post(
67
67
  self._base_url,
68
68
  headers={
69
- "User-Agent": kimi_cli.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")