kimi-agent-sdk-x 0.0.5__tar.gz
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_agent_sdk_x-0.0.5/PKG-INFO +32 -0
- kimi_agent_sdk_x-0.0.5/README.md +5 -0
- kimi_agent_sdk_x-0.0.5/pyproject.toml +80 -0
- kimi_agent_sdk_x-0.0.5/src/kimi_agent_sdk/__init__.py +195 -0
- kimi_agent_sdk_x-0.0.5/src/kimi_agent_sdk/_aggregator.py +113 -0
- kimi_agent_sdk_x-0.0.5/src/kimi_agent_sdk/_approval.py +26 -0
- kimi_agent_sdk_x-0.0.5/src/kimi_agent_sdk/_exception.py +15 -0
- kimi_agent_sdk_x-0.0.5/src/kimi_agent_sdk/_prompt.py +120 -0
- kimi_agent_sdk_x-0.0.5/src/kimi_agent_sdk/_session.py +303 -0
- kimi_agent_sdk_x-0.0.5/src/kimi_agent_sdk/py.typed +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: kimi-agent-sdk-x
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: A Python SDK for Kimi Agent (Kimi CLI).
|
|
5
|
+
Keywords: kimi,agent,sdk,coding-agents,llm,automation
|
|
6
|
+
Author: Moonshot AI
|
|
7
|
+
Author-email: Moonshot AI <team@moonshot.ai>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Classifier: Typing :: Typed
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
|
+
Requires-Dist: kimi-cli-x>=1.12.0,<1.13.0
|
|
19
|
+
Requires-Dist: kosong>=0.42.0,<0.43.0
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Project-URL: Changelog, https://github.com/MoonshotAI/kimi-agent-sdk/blob/main/python/CHANGELOG.md
|
|
22
|
+
Project-URL: Documentation, https://github.com/MoonshotAI/kimi-agent-sdk/tree/main/README.md
|
|
23
|
+
Project-URL: Homepage, https://github.com/MoonshotAI/kimi-agent-sdk
|
|
24
|
+
Project-URL: Issues, https://github.com/MoonshotAI/kimi-agent-sdk/issues
|
|
25
|
+
Project-URL: Repository, https://github.com/MoonshotAI/kimi-agent-sdk
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Kimi Agent SDK for Python
|
|
29
|
+
|
|
30
|
+
Python SDK for programmatically controlling the Kimi CLI (Kimi Code) agent runtime. Use it to run sessions, stream responses, and handle approvals from Python applications.
|
|
31
|
+
|
|
32
|
+
Quick start: [guides/python/quickstart.md](../guides/python/quickstart.md)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Kimi Agent SDK for Python
|
|
2
|
+
|
|
3
|
+
Python SDK for programmatically controlling the Kimi CLI (Kimi Code) agent runtime. Use it to run sessions, stream responses, and handle approvals from Python applications.
|
|
4
|
+
|
|
5
|
+
Quick start: [guides/python/quickstart.md](../guides/python/quickstart.md)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kimi-agent-sdk-x"
|
|
3
|
+
version = "0.0.5"
|
|
4
|
+
description = "A Python SDK for Kimi Agent (Kimi CLI)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
keywords = ["kimi", "agent", "sdk", "coding-agents", "llm", "automation"]
|
|
9
|
+
authors = [{ name = "Moonshot AI", email = "team@moonshot.ai" }]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Typing :: Typed",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"kimi-cli-x>=1.12.0,<1.13.0",
|
|
23
|
+
"kosong>=0.42.0,<0.43.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/MoonshotAI/kimi-agent-sdk"
|
|
28
|
+
Repository = "https://github.com/MoonshotAI/kimi-agent-sdk"
|
|
29
|
+
Documentation = "https://github.com/MoonshotAI/kimi-agent-sdk/tree/main/README.md"
|
|
30
|
+
Changelog = "https://github.com/MoonshotAI/kimi-agent-sdk/blob/main/python/CHANGELOG.md"
|
|
31
|
+
Issues = "https://github.com/MoonshotAI/kimi-agent-sdk/issues"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"inline-snapshot[black]>=0.31.1",
|
|
36
|
+
"pdoc>=16.0.0",
|
|
37
|
+
"pyright>=1.1.407",
|
|
38
|
+
"ty>=0.0.7",
|
|
39
|
+
"pytest>=9.0.2",
|
|
40
|
+
"pytest-asyncio>=1.3.0",
|
|
41
|
+
"ruff>=0.14.10",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["uv_build>=0.8.5,<0.10.0"]
|
|
46
|
+
build-backend = "uv_build"
|
|
47
|
+
|
|
48
|
+
[tool.uv.build-backend]
|
|
49
|
+
module-name = ["kimi_agent_sdk"]
|
|
50
|
+
source-exclude = ["tests/**/*"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = [
|
|
57
|
+
"E", # pycodestyle
|
|
58
|
+
"F", # Pyflakes
|
|
59
|
+
"UP", # pyupgrade
|
|
60
|
+
"B", # flake8-bugbear
|
|
61
|
+
"SIM", # flake8-simplify
|
|
62
|
+
"I", # isort
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.pyright]
|
|
66
|
+
typeCheckingMode = "strict"
|
|
67
|
+
pythonVersion = "3.14"
|
|
68
|
+
include = [
|
|
69
|
+
"src/**/*.py",
|
|
70
|
+
"tests/**/*.py",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.ty.environment]
|
|
74
|
+
python-version = "3.14"
|
|
75
|
+
|
|
76
|
+
[tool.ty.src]
|
|
77
|
+
include = [
|
|
78
|
+
"src/**/*.py",
|
|
79
|
+
"tests/**/*.py",
|
|
80
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kimi Agent SDK is a Python SDK for building AI agents powered by Kimi.
|
|
3
|
+
It provides both high-level and low-level APIs for seamless agent integration, stateful sessions,
|
|
4
|
+
and tool orchestration in modern AI applications.
|
|
5
|
+
|
|
6
|
+
Key features:
|
|
7
|
+
|
|
8
|
+
- `kimi_agent_sdk.prompt` provides a high-level async generator API that sends prompts to Kimi
|
|
9
|
+
and yields aggregated Message objects, handling approval requests automatically or via custom
|
|
10
|
+
handlers.
|
|
11
|
+
- `kimi_agent_sdk.Session` offers low-level control with Wire message access, manual approval
|
|
12
|
+
handling, session persistence, and context management for long-running agent interactions.
|
|
13
|
+
- Message structures, approval types, and exceptions are re-exported from kosong and kimi_cli
|
|
14
|
+
for convenient access.
|
|
15
|
+
|
|
16
|
+
Example (high-level API):
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
|
|
21
|
+
from kimi_agent_sdk import prompt
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def main() -> None:
|
|
25
|
+
async for message in prompt(
|
|
26
|
+
"What is the capital of France?",
|
|
27
|
+
model="kimi",
|
|
28
|
+
yolo=True,
|
|
29
|
+
):
|
|
30
|
+
print(message.extract_text())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
asyncio.run(main())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Example (low-level API with Session):
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
|
|
41
|
+
from kaos.path import KaosPath
|
|
42
|
+
|
|
43
|
+
from kimi_agent_sdk import ApprovalRequest, Session
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def main() -> None:
|
|
47
|
+
async with await Session.create(
|
|
48
|
+
work_dir=KaosPath.cwd(),
|
|
49
|
+
model="kimi",
|
|
50
|
+
yolo=False,
|
|
51
|
+
) as session:
|
|
52
|
+
async for wire_msg in session.prompt("What is the capital of France?"):
|
|
53
|
+
if isinstance(wire_msg, ApprovalRequest):
|
|
54
|
+
wire_msg.resolve("approve")
|
|
55
|
+
else:
|
|
56
|
+
print(wire_msg)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
from __future__ import annotations
|
|
64
|
+
|
|
65
|
+
from fastmcp.mcp_config import MCPConfig
|
|
66
|
+
from kimi_cli.config import Config
|
|
67
|
+
from kimi_cli.exception import (
|
|
68
|
+
AgentSpecError,
|
|
69
|
+
ConfigError,
|
|
70
|
+
InvalidToolError,
|
|
71
|
+
KimiCLIException,
|
|
72
|
+
MCPConfigError,
|
|
73
|
+
MCPRuntimeError,
|
|
74
|
+
SystemPromptTemplateError,
|
|
75
|
+
)
|
|
76
|
+
from kimi_cli.soul import LLMNotSet, LLMNotSupported, MaxStepsReached, RunCancelled
|
|
77
|
+
from kimi_cli.wire.types import (
|
|
78
|
+
ApprovalRequest,
|
|
79
|
+
ApprovalResponse,
|
|
80
|
+
BriefDisplayBlock,
|
|
81
|
+
CompactionBegin,
|
|
82
|
+
CompactionEnd,
|
|
83
|
+
DiffDisplayBlock,
|
|
84
|
+
DisplayBlock,
|
|
85
|
+
Event,
|
|
86
|
+
Request,
|
|
87
|
+
ShellDisplayBlock,
|
|
88
|
+
StatusUpdate,
|
|
89
|
+
StepBegin,
|
|
90
|
+
StepInterrupted,
|
|
91
|
+
SubagentEvent,
|
|
92
|
+
TodoDisplayBlock,
|
|
93
|
+
TodoDisplayItem,
|
|
94
|
+
TokenUsage,
|
|
95
|
+
ToolCallPart,
|
|
96
|
+
ToolResult,
|
|
97
|
+
TurnBegin,
|
|
98
|
+
TurnEnd,
|
|
99
|
+
WireMessage,
|
|
100
|
+
is_event,
|
|
101
|
+
is_request,
|
|
102
|
+
)
|
|
103
|
+
from kosong.chat_provider import (
|
|
104
|
+
APIConnectionError,
|
|
105
|
+
APIEmptyResponseError,
|
|
106
|
+
APIStatusError,
|
|
107
|
+
APITimeoutError,
|
|
108
|
+
ChatProviderError,
|
|
109
|
+
)
|
|
110
|
+
from kosong.message import (
|
|
111
|
+
AudioURLPart,
|
|
112
|
+
ContentPart,
|
|
113
|
+
ImageURLPart,
|
|
114
|
+
Message,
|
|
115
|
+
TextPart,
|
|
116
|
+
ThinkPart,
|
|
117
|
+
ToolCall,
|
|
118
|
+
VideoURLPart,
|
|
119
|
+
)
|
|
120
|
+
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnValue
|
|
121
|
+
|
|
122
|
+
from kimi_agent_sdk._approval import ApprovalHandlerFn
|
|
123
|
+
from kimi_agent_sdk._exception import PromptValidationError, SessionStateError
|
|
124
|
+
from kimi_agent_sdk._prompt import prompt
|
|
125
|
+
from kimi_agent_sdk._session import Session
|
|
126
|
+
|
|
127
|
+
__all__ = [
|
|
128
|
+
# Core API
|
|
129
|
+
"prompt",
|
|
130
|
+
"Session",
|
|
131
|
+
# Approval
|
|
132
|
+
"ApprovalHandlerFn",
|
|
133
|
+
"ApprovalRequest",
|
|
134
|
+
# High-level types
|
|
135
|
+
"Message",
|
|
136
|
+
"ContentPart",
|
|
137
|
+
"TextPart",
|
|
138
|
+
"ThinkPart",
|
|
139
|
+
"ImageURLPart",
|
|
140
|
+
"AudioURLPart",
|
|
141
|
+
"VideoURLPart",
|
|
142
|
+
"ToolCall",
|
|
143
|
+
# Low-level types (Wire)
|
|
144
|
+
"WireMessage",
|
|
145
|
+
"Event",
|
|
146
|
+
"Request",
|
|
147
|
+
"TurnBegin",
|
|
148
|
+
"TurnEnd",
|
|
149
|
+
"StepBegin",
|
|
150
|
+
"StepInterrupted",
|
|
151
|
+
"CompactionBegin",
|
|
152
|
+
"CompactionEnd",
|
|
153
|
+
"StatusUpdate",
|
|
154
|
+
"ToolCallPart",
|
|
155
|
+
"ToolResult",
|
|
156
|
+
"ToolReturnValue",
|
|
157
|
+
"ApprovalResponse",
|
|
158
|
+
"SubagentEvent",
|
|
159
|
+
"DisplayBlock",
|
|
160
|
+
"BriefDisplayBlock",
|
|
161
|
+
"DiffDisplayBlock",
|
|
162
|
+
"ShellDisplayBlock",
|
|
163
|
+
"TodoDisplayBlock",
|
|
164
|
+
"TodoDisplayItem",
|
|
165
|
+
"TokenUsage",
|
|
166
|
+
"is_event",
|
|
167
|
+
"is_request",
|
|
168
|
+
"CallableTool2",
|
|
169
|
+
"ToolOk",
|
|
170
|
+
"ToolError",
|
|
171
|
+
# Exceptions
|
|
172
|
+
"KimiAgentException",
|
|
173
|
+
"ConfigError",
|
|
174
|
+
"AgentSpecError",
|
|
175
|
+
"InvalidToolError",
|
|
176
|
+
"MCPConfigError",
|
|
177
|
+
"MCPRuntimeError",
|
|
178
|
+
"SystemPromptTemplateError",
|
|
179
|
+
"LLMNotSet",
|
|
180
|
+
"LLMNotSupported",
|
|
181
|
+
"ChatProviderError",
|
|
182
|
+
"APIConnectionError",
|
|
183
|
+
"APITimeoutError",
|
|
184
|
+
"APIStatusError",
|
|
185
|
+
"APIEmptyResponseError",
|
|
186
|
+
"MaxStepsReached",
|
|
187
|
+
"RunCancelled",
|
|
188
|
+
"PromptValidationError",
|
|
189
|
+
"SessionStateError",
|
|
190
|
+
# Others
|
|
191
|
+
"Config",
|
|
192
|
+
"MCPConfig",
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
type KimiAgentException = KimiCLIException
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from kimi_cli.soul.message import tool_result_to_message
|
|
6
|
+
from kimi_cli.wire.types import StepBegin, StepInterrupted, ToolCallPart, ToolResult, WireMessage
|
|
7
|
+
from kosong.message import ContentPart, Message, ToolCall
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _merge_content(buffer: list[ContentPart], part: ContentPart) -> None:
|
|
11
|
+
if not buffer or not buffer[-1].merge_in_place(part):
|
|
12
|
+
buffer.append(part)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MessageAggregator:
|
|
16
|
+
"""
|
|
17
|
+
Aggregate WireMessage stream into Message stream.
|
|
18
|
+
|
|
19
|
+
- final_message_only=False: like JsonPrinter, outputs per step and tool results
|
|
20
|
+
- final_message_only=True: like FinalOnlyJsonPrinter, outputs only the last step text
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class _ToolCallState:
|
|
25
|
+
tool_call: ToolCall
|
|
26
|
+
tool_result: ToolResult | None
|
|
27
|
+
|
|
28
|
+
def __init__(self, final_message_only: bool = False) -> None:
|
|
29
|
+
self._final_message_only = final_message_only
|
|
30
|
+
self._content_buffer: list[ContentPart] = []
|
|
31
|
+
self._tool_call_buffer: dict[str, MessageAggregator._ToolCallState] = {}
|
|
32
|
+
self._last_tool_call: ToolCall | None = None
|
|
33
|
+
|
|
34
|
+
def feed(self, msg: WireMessage) -> list[Message]:
|
|
35
|
+
match msg:
|
|
36
|
+
case StepBegin() | StepInterrupted():
|
|
37
|
+
if self._final_message_only:
|
|
38
|
+
self._reset_buffers()
|
|
39
|
+
return []
|
|
40
|
+
return self._flush()
|
|
41
|
+
case ContentPart() as part:
|
|
42
|
+
_merge_content(self._content_buffer, part)
|
|
43
|
+
case ToolCall() as call:
|
|
44
|
+
if self._final_message_only:
|
|
45
|
+
return []
|
|
46
|
+
self._tool_call_buffer[call.id] = MessageAggregator._ToolCallState(
|
|
47
|
+
tool_call=call, tool_result=None
|
|
48
|
+
)
|
|
49
|
+
self._last_tool_call = call
|
|
50
|
+
case ToolCallPart() as part:
|
|
51
|
+
if self._final_message_only:
|
|
52
|
+
return []
|
|
53
|
+
if self._last_tool_call is None:
|
|
54
|
+
return []
|
|
55
|
+
self._last_tool_call.merge_in_place(part)
|
|
56
|
+
case ToolResult() as result:
|
|
57
|
+
if self._final_message_only:
|
|
58
|
+
return []
|
|
59
|
+
state = self._tool_call_buffer.get(result.tool_call_id)
|
|
60
|
+
if state is None:
|
|
61
|
+
return []
|
|
62
|
+
state.tool_result = result
|
|
63
|
+
case _:
|
|
64
|
+
pass
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
def flush(self) -> list[Message]:
|
|
68
|
+
return self._flush()
|
|
69
|
+
|
|
70
|
+
def _flush(self) -> list[Message]:
|
|
71
|
+
if self._final_message_only:
|
|
72
|
+
return self._flush_final_only()
|
|
73
|
+
return self._flush_full()
|
|
74
|
+
|
|
75
|
+
def _flush_final_only(self) -> list[Message]:
|
|
76
|
+
if not self._content_buffer:
|
|
77
|
+
return []
|
|
78
|
+
message = Message(role="assistant", content=self._content_buffer)
|
|
79
|
+
text = message.extract_text()
|
|
80
|
+
self._reset_buffers()
|
|
81
|
+
if not text:
|
|
82
|
+
return []
|
|
83
|
+
return [Message(role="assistant", content=text)]
|
|
84
|
+
|
|
85
|
+
def _flush_full(self) -> list[Message]:
|
|
86
|
+
if not self._content_buffer and not self._tool_call_buffer:
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
tool_calls: list[ToolCall] = []
|
|
90
|
+
tool_results: list[ToolResult] = []
|
|
91
|
+
for state in self._tool_call_buffer.values():
|
|
92
|
+
if state.tool_result is None:
|
|
93
|
+
continue
|
|
94
|
+
tool_calls.append(state.tool_call)
|
|
95
|
+
tool_results.append(state.tool_result)
|
|
96
|
+
|
|
97
|
+
messages: list[Message] = [
|
|
98
|
+
Message(
|
|
99
|
+
role="assistant",
|
|
100
|
+
content=self._content_buffer,
|
|
101
|
+
tool_calls=tool_calls or None,
|
|
102
|
+
)
|
|
103
|
+
]
|
|
104
|
+
for result in tool_results:
|
|
105
|
+
messages.append(tool_result_to_message(result))
|
|
106
|
+
|
|
107
|
+
self._reset_buffers()
|
|
108
|
+
return messages
|
|
109
|
+
|
|
110
|
+
def _reset_buffers(self) -> None:
|
|
111
|
+
self._content_buffer.clear()
|
|
112
|
+
self._tool_call_buffer.clear()
|
|
113
|
+
self._last_tool_call = None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
from kimi_cli.wire.types import ApprovalRequest
|
|
6
|
+
|
|
7
|
+
type ApprovalHandlerFn = (
|
|
8
|
+
Callable[[ApprovalRequest], None] | Callable[[ApprovalRequest], Awaitable[None]]
|
|
9
|
+
)
|
|
10
|
+
"""
|
|
11
|
+
Approval handler callback function type.
|
|
12
|
+
|
|
13
|
+
The callback receives an ApprovalRequest with the following attributes and is responsible
|
|
14
|
+
for calling request.resolve(...):
|
|
15
|
+
- id: Unique request identifier
|
|
16
|
+
- tool_call_id: Associated tool call ID
|
|
17
|
+
- sender: Name of the tool that initiated the request
|
|
18
|
+
- action: Action type
|
|
19
|
+
- description: Detailed description
|
|
20
|
+
- display: List of visualization info
|
|
21
|
+
|
|
22
|
+
Resolve with:
|
|
23
|
+
- "approve": Approve this request
|
|
24
|
+
- "approve_for_session": Approve and auto-approve subsequent similar requests
|
|
25
|
+
- "reject": Reject the request
|
|
26
|
+
"""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from kimi_cli.exception import KimiCLIException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PromptValidationError(KimiCLIException, ValueError):
|
|
7
|
+
"""Invalid prompt configuration."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SessionStateError(KimiCLIException, RuntimeError):
|
|
13
|
+
"""Invalid session state for prompt execution."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from kaos.path import KaosPath
|
|
9
|
+
from kimi_cli.config import Config
|
|
10
|
+
from kimi_cli.wire.types import ApprovalRequest
|
|
11
|
+
from kosong.message import ContentPart, Message
|
|
12
|
+
|
|
13
|
+
from kimi_agent_sdk._aggregator import MessageAggregator
|
|
14
|
+
from kimi_agent_sdk._approval import ApprovalHandlerFn
|
|
15
|
+
from kimi_agent_sdk._exception import PromptValidationError
|
|
16
|
+
from kimi_agent_sdk._session import Session
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from fastmcp.mcp_config import MCPConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def prompt(
|
|
23
|
+
user_input: str | list[ContentPart],
|
|
24
|
+
*,
|
|
25
|
+
# Basic configuration
|
|
26
|
+
work_dir: KaosPath | None = None,
|
|
27
|
+
config: Config | Path | None = None,
|
|
28
|
+
model: str | None = None,
|
|
29
|
+
thinking: bool = False,
|
|
30
|
+
# Run mode
|
|
31
|
+
yolo: bool = False,
|
|
32
|
+
approval_handler_fn: ApprovalHandlerFn | None = None,
|
|
33
|
+
# Extensions
|
|
34
|
+
agent_file: Path | None = None,
|
|
35
|
+
mcp_configs: list[MCPConfig] | list[dict[str, Any]] | None = None,
|
|
36
|
+
skills_dir: KaosPath | None = None,
|
|
37
|
+
# Loop control
|
|
38
|
+
max_steps_per_turn: int | None = None,
|
|
39
|
+
max_retries_per_step: int | None = None,
|
|
40
|
+
max_ralph_iterations: int | None = None,
|
|
41
|
+
# Output control
|
|
42
|
+
final_message_only: bool = False,
|
|
43
|
+
) -> AsyncGenerator[Message, None]:
|
|
44
|
+
"""
|
|
45
|
+
Send a prompt to the Kimi Agent and get streaming responses.
|
|
46
|
+
|
|
47
|
+
This is the highest-level API that aggregates Wire messages into Message objects,
|
|
48
|
+
similar to `kimi --print --output stream-json`.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
user_input: User input, can be plain text or a list of content parts.
|
|
52
|
+
work_dir: Working directory (KaosPath). Defaults to current directory.
|
|
53
|
+
config: Configuration object or path to a config file.
|
|
54
|
+
model: Model name, e.g. "kimi".
|
|
55
|
+
thinking: Whether to enable thinking mode (requires model support).
|
|
56
|
+
yolo: Automatically approve all approval requests.
|
|
57
|
+
approval_handler_fn: Custom approval handler callback (sync or async).
|
|
58
|
+
agent_file: Agent specification file path.
|
|
59
|
+
mcp_configs: MCP server configurations.
|
|
60
|
+
skills_dir: Skills directory (KaosPath).
|
|
61
|
+
max_steps_per_turn: Maximum number of steps in one turn.
|
|
62
|
+
max_retries_per_step: Maximum number of retries per step.
|
|
63
|
+
max_ralph_iterations: Extra iterations in Ralph mode (-1 for unlimited).
|
|
64
|
+
final_message_only: Only return the final Message of the last step.
|
|
65
|
+
|
|
66
|
+
Yields:
|
|
67
|
+
Message: Aggregated assistant/tool messages.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
LLMNotSet: When the LLM is not set.
|
|
71
|
+
LLMNotSupported: When the LLM does not have required capabilities.
|
|
72
|
+
ChatProviderError: When the LLM provider returns an error.
|
|
73
|
+
MaxStepsReached: When the maximum number of steps is reached.
|
|
74
|
+
RunCancelled: When the run is cancelled by the cancel event.
|
|
75
|
+
PromptValidationError: When neither of yolo/approval_handler_fn are provided.
|
|
76
|
+
|
|
77
|
+
Note:
|
|
78
|
+
If yolo=True and approval_handler_fn is provided, the handler is ignored.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if not yolo and approval_handler_fn is None:
|
|
82
|
+
raise PromptValidationError("Either yolo=True or approval_handler_fn must be provided")
|
|
83
|
+
|
|
84
|
+
def _auto_approve(request: ApprovalRequest) -> None:
|
|
85
|
+
request.resolve("approve")
|
|
86
|
+
|
|
87
|
+
if yolo:
|
|
88
|
+
approval_handler: ApprovalHandlerFn = _auto_approve
|
|
89
|
+
else:
|
|
90
|
+
assert approval_handler_fn is not None
|
|
91
|
+
approval_handler = approval_handler_fn
|
|
92
|
+
|
|
93
|
+
async with await Session.create(
|
|
94
|
+
work_dir=work_dir,
|
|
95
|
+
config=config,
|
|
96
|
+
model=model,
|
|
97
|
+
thinking=thinking,
|
|
98
|
+
yolo=yolo,
|
|
99
|
+
agent_file=agent_file,
|
|
100
|
+
mcp_configs=mcp_configs,
|
|
101
|
+
skills_dir=skills_dir,
|
|
102
|
+
max_steps_per_turn=max_steps_per_turn,
|
|
103
|
+
max_retries_per_step=max_retries_per_step,
|
|
104
|
+
max_ralph_iterations=max_ralph_iterations,
|
|
105
|
+
) as session:
|
|
106
|
+
aggregator = MessageAggregator(final_message_only=final_message_only)
|
|
107
|
+
async for wire_msg in session.prompt(user_input, merge_wire_messages=True):
|
|
108
|
+
if isinstance(wire_msg, ApprovalRequest):
|
|
109
|
+
result = approval_handler(wire_msg)
|
|
110
|
+
if inspect.isawaitable(result):
|
|
111
|
+
await result
|
|
112
|
+
if not wire_msg.resolved:
|
|
113
|
+
wire_msg.resolve("reject")
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
for message in aggregator.feed(wire_msg):
|
|
117
|
+
yield message
|
|
118
|
+
|
|
119
|
+
for message in aggregator.flush():
|
|
120
|
+
yield message
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
8
|
+
|
|
9
|
+
from kaos.path import KaosPath
|
|
10
|
+
from kimi_cli.app import KimiCLI
|
|
11
|
+
from kimi_cli.config import Config
|
|
12
|
+
from kimi_cli.session import Session as CliSession
|
|
13
|
+
from kimi_cli.soul import StatusSnapshot
|
|
14
|
+
from kimi_cli.wire.types import ContentPart, WireMessage
|
|
15
|
+
from kimi_cli.soul.agent import BuiltinSystemPromptArgs
|
|
16
|
+
|
|
17
|
+
from kimi_agent_sdk._exception import SessionStateError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from kimi_agent_sdk import MCPConfig
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure_type(name: str, value: object, expected: type) -> None:
|
|
24
|
+
if not isinstance(value, expected):
|
|
25
|
+
raise TypeError(f"{name} must be {expected.__name__}, got {type(value).__name__}")
|
|
26
|
+
|
|
27
|
+
def _ensure_skill_dirs(skill_dirs: object) -> list[KaosPath]:
|
|
28
|
+
from collections.abc import Iterable
|
|
29
|
+
if skill_dirs is None:
|
|
30
|
+
return []
|
|
31
|
+
if type(skill_dirs) == list:
|
|
32
|
+
return skill_dirs
|
|
33
|
+
if isinstance(skill_dirs, Iterable) and not isinstance(skill_dirs, (str, bytes)):
|
|
34
|
+
return [i for i in skill_dirs]
|
|
35
|
+
return [skill_dirs]
|
|
36
|
+
|
|
37
|
+
class Session:
|
|
38
|
+
"""
|
|
39
|
+
Kimi Agent session with low-level control.
|
|
40
|
+
|
|
41
|
+
Use this class when you need full access to Wire messages, manual approval
|
|
42
|
+
handling, or session persistence across prompts.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, cli: KimiCLI) -> None:
|
|
46
|
+
self._cli = cli
|
|
47
|
+
self._cancel_event: asyncio.Event | None = None
|
|
48
|
+
self._closed = False
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
async def create(
|
|
52
|
+
work_dir: KaosPath | None = None,
|
|
53
|
+
*,
|
|
54
|
+
# Basic configuration
|
|
55
|
+
session_id: str | None = None,
|
|
56
|
+
config: Config | Path | None = None,
|
|
57
|
+
model: str | None = None,
|
|
58
|
+
thinking: bool = False,
|
|
59
|
+
# Run mode
|
|
60
|
+
yolo: bool = False,
|
|
61
|
+
plan_mode: bool = False,
|
|
62
|
+
# Extensions
|
|
63
|
+
agent_file: Path | None = None,
|
|
64
|
+
mcp_configs: list[MCPConfig] | list[dict[str, Any]] | None = None,
|
|
65
|
+
skills_dirs: list[KaosPath] | KaosPath | None = None,
|
|
66
|
+
# Loop control
|
|
67
|
+
max_steps_per_turn: int | None = None,
|
|
68
|
+
max_retries_per_step: int | None = None,
|
|
69
|
+
max_ralph_iterations: int | None = None,
|
|
70
|
+
tool_call_failed_list: list[tuple[str, str, str, str]] | None = None, # Add by maxwell
|
|
71
|
+
custom_system_prompt : Callable[[BuiltinSystemPromptArgs], str] | None = None, # Add by maxwell
|
|
72
|
+
) -> Session:
|
|
73
|
+
"""
|
|
74
|
+
Create a new Session instance.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
work_dir: Working directory (KaosPath). Defaults to current directory.
|
|
78
|
+
session_id: Custom session ID (optional).
|
|
79
|
+
config: Configuration object or path to a config file.
|
|
80
|
+
model: Model name, e.g. "kimi".
|
|
81
|
+
thinking: Whether to enable thinking mode (requires model support).
|
|
82
|
+
yolo: Automatically approve all approval requests.
|
|
83
|
+
agent_file: Agent specification file path.
|
|
84
|
+
mcp_configs: MCP server configurations.
|
|
85
|
+
skills_dirs: Skills directories (KaosPath or list of KaosPath).
|
|
86
|
+
max_steps_per_turn: Maximum number of steps in one turn.
|
|
87
|
+
max_retries_per_step: Maximum number of retries per step.
|
|
88
|
+
max_ralph_iterations: Extra iterations in Ralph mode (-1 for unlimited).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Session: A new Session instance.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
FileNotFoundError: When the agent file is not found.
|
|
95
|
+
ConfigError(KimiCLIException, ValueError): When the configuration is invalid.
|
|
96
|
+
AgentSpecError(KimiCLIException, ValueError): When the agent specification is invalid.
|
|
97
|
+
InvalidToolError(KimiCLIException, ValueError): When any tool cannot be loaded.
|
|
98
|
+
MCPConfigError(KimiCLIException, ValueError): When any MCP configuration is invalid.
|
|
99
|
+
MCPRuntimeError(KimiCLIException, RuntimeError): When any MCP server cannot be
|
|
100
|
+
connected.
|
|
101
|
+
"""
|
|
102
|
+
if work_dir is None:
|
|
103
|
+
work_dir_path = KaosPath.cwd()
|
|
104
|
+
else:
|
|
105
|
+
_ensure_type("work_dir", work_dir, KaosPath)
|
|
106
|
+
work_dir_path = work_dir
|
|
107
|
+
skill_dirs_list = _ensure_skill_dirs(skills_dirs)
|
|
108
|
+
for i, sd in enumerate(skill_dirs_list):
|
|
109
|
+
_ensure_type(f"skills_dirs[{i}]", sd, KaosPath)
|
|
110
|
+
cli_session = await CliSession.create(work_dir_path, session_id)
|
|
111
|
+
cli = await KimiCLI.create(
|
|
112
|
+
cli_session,
|
|
113
|
+
config=config,
|
|
114
|
+
model_name=model,
|
|
115
|
+
thinking=thinking,
|
|
116
|
+
yolo=yolo,
|
|
117
|
+
plan_mode=plan_mode,
|
|
118
|
+
agent_file=agent_file,
|
|
119
|
+
mcp_configs=mcp_configs,
|
|
120
|
+
skills_dirs=skill_dirs_list,
|
|
121
|
+
max_steps_per_turn=max_steps_per_turn,
|
|
122
|
+
max_retries_per_step=max_retries_per_step,
|
|
123
|
+
max_ralph_iterations=max_ralph_iterations,
|
|
124
|
+
tool_call_failed_list=tool_call_failed_list,
|
|
125
|
+
custom_system_prompt=custom_system_prompt,
|
|
126
|
+
)
|
|
127
|
+
return Session(cli)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
async def resume(
|
|
131
|
+
work_dir: KaosPath,
|
|
132
|
+
session_id: str | None = None,
|
|
133
|
+
*,
|
|
134
|
+
# Basic configuration
|
|
135
|
+
config: Config | Path | None = None,
|
|
136
|
+
model: str | None = None,
|
|
137
|
+
thinking: bool = False,
|
|
138
|
+
# Run mode
|
|
139
|
+
yolo: bool = False,
|
|
140
|
+
plan_mode: bool = False,
|
|
141
|
+
# Extensions
|
|
142
|
+
agent_file: Path | None = None,
|
|
143
|
+
mcp_configs: list[MCPConfig] | list[dict[str, Any]] | None = None,
|
|
144
|
+
skills_dirs: list[KaosPath] | KaosPath | None = None,
|
|
145
|
+
# Loop control
|
|
146
|
+
max_steps_per_turn: int | None = None,
|
|
147
|
+
max_retries_per_step: int | None = None,
|
|
148
|
+
max_ralph_iterations: int | None = None,
|
|
149
|
+
tool_call_failed_list: list[tuple[str, str, str, str]] | None = None, # Add by maxwell
|
|
150
|
+
custom_system_prompt : Callable[[BuiltinSystemPromptArgs], str] | None = None, # Add by maxwell
|
|
151
|
+
) -> Session | None:
|
|
152
|
+
"""
|
|
153
|
+
Resume an existing session.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
work_dir: Working directory to resume from (KaosPath).
|
|
157
|
+
session_id: Session ID to resume. If None, resumes the most recent session.
|
|
158
|
+
config: Configuration object or path to a config file.
|
|
159
|
+
model: Model name, e.g. "kimi".
|
|
160
|
+
thinking: Whether to enable thinking mode (requires model support).
|
|
161
|
+
yolo: Automatically approve all approval requests.
|
|
162
|
+
agent_file: Agent specification file path.
|
|
163
|
+
mcp_configs: MCP server configurations.
|
|
164
|
+
skills_dirs: Skills directories (KaosPath or list of KaosPath).
|
|
165
|
+
max_steps_per_turn: Maximum number of steps in one turn.
|
|
166
|
+
max_retries_per_step: Maximum number of retries per step.
|
|
167
|
+
max_ralph_iterations: Extra iterations in Ralph mode (-1 for unlimited).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Session | None: The resumed session, or None if not found.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
FileNotFoundError: When the agent file is not found.
|
|
174
|
+
ConfigError(KimiCLIException, ValueError): When the configuration is invalid.
|
|
175
|
+
AgentSpecError(KimiCLIException, ValueError): When the agent specification is invalid.
|
|
176
|
+
InvalidToolError(KimiCLIException, ValueError): When any tool cannot be loaded.
|
|
177
|
+
MCPConfigError(KimiCLIException, ValueError): When any MCP configuration is invalid.
|
|
178
|
+
MCPRuntimeError(KimiCLIException, RuntimeError): When any MCP server cannot be
|
|
179
|
+
connected.
|
|
180
|
+
"""
|
|
181
|
+
_ensure_type("work_dir", work_dir, KaosPath)
|
|
182
|
+
skill_dirs_list = _ensure_skill_dirs(skills_dirs)
|
|
183
|
+
for i, sd in enumerate(skill_dirs_list):
|
|
184
|
+
_ensure_type(f"skills_dirs[{i}]", sd, KaosPath)
|
|
185
|
+
if session_id is None:
|
|
186
|
+
cli_session = await CliSession.continue_(work_dir)
|
|
187
|
+
else:
|
|
188
|
+
cli_session = await CliSession.find(work_dir, session_id)
|
|
189
|
+
if cli_session is None:
|
|
190
|
+
return None
|
|
191
|
+
cli = await KimiCLI.create(
|
|
192
|
+
cli_session,
|
|
193
|
+
config=config,
|
|
194
|
+
model_name=model,
|
|
195
|
+
thinking=thinking,
|
|
196
|
+
yolo=yolo,
|
|
197
|
+
plan_mode=plan_mode,
|
|
198
|
+
agent_file=agent_file,
|
|
199
|
+
mcp_configs=mcp_configs,
|
|
200
|
+
skills_dirs=skill_dirs_list,
|
|
201
|
+
max_steps_per_turn=max_steps_per_turn,
|
|
202
|
+
max_retries_per_step=max_retries_per_step,
|
|
203
|
+
max_ralph_iterations=max_ralph_iterations,
|
|
204
|
+
tool_call_failed_list=tool_call_failed_list,
|
|
205
|
+
custom_system_prompt=custom_system_prompt,
|
|
206
|
+
)
|
|
207
|
+
return Session(cli)
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def id(self) -> str:
|
|
211
|
+
"""Session ID."""
|
|
212
|
+
return self._cli.session.id
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def model_name(self) -> str:
|
|
216
|
+
"""Name of the current model."""
|
|
217
|
+
return self._cli.soul.model_name
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def status(self) -> StatusSnapshot:
|
|
221
|
+
"""Current status snapshot (context usage, yolo state, etc.)."""
|
|
222
|
+
return self._cli.soul.status
|
|
223
|
+
|
|
224
|
+
async def prompt(
|
|
225
|
+
self,
|
|
226
|
+
user_input: str | list[ContentPart],
|
|
227
|
+
*,
|
|
228
|
+
merge_wire_messages: bool = False,
|
|
229
|
+
) -> AsyncGenerator[WireMessage, None]:
|
|
230
|
+
"""
|
|
231
|
+
Send a prompt and get a WireMessage stream.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
user_input: User input, can be plain text or a list of content parts.
|
|
235
|
+
merge_wire_messages: Whether to merge consecutive Wire messages.
|
|
236
|
+
|
|
237
|
+
Yields:
|
|
238
|
+
WireMessage: Wire messages, including ApprovalRequest.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
LLMNotSet: When the LLM is not set.
|
|
242
|
+
LLMNotSupported: When the LLM does not have required capabilities.
|
|
243
|
+
ChatProviderError: When the LLM provider returns an error.
|
|
244
|
+
MaxStepsReached: When the maximum number of steps is reached.
|
|
245
|
+
RunCancelled: When the run is cancelled by the cancel event.
|
|
246
|
+
SessionStateError: When the session is closed or already running.
|
|
247
|
+
|
|
248
|
+
Note:
|
|
249
|
+
Callers must handle ApprovalRequest manually unless yolo=True.
|
|
250
|
+
"""
|
|
251
|
+
if self._closed:
|
|
252
|
+
raise SessionStateError("Session is closed")
|
|
253
|
+
if self._cancel_event is not None:
|
|
254
|
+
raise SessionStateError("Session is already running")
|
|
255
|
+
cancel_event = asyncio.Event()
|
|
256
|
+
self._cancel_event = cancel_event
|
|
257
|
+
try:
|
|
258
|
+
async for msg in self._cli.run(
|
|
259
|
+
user_input,
|
|
260
|
+
cancel_event,
|
|
261
|
+
merge_wire_messages=merge_wire_messages,
|
|
262
|
+
):
|
|
263
|
+
yield msg
|
|
264
|
+
finally:
|
|
265
|
+
if self._cancel_event is cancel_event:
|
|
266
|
+
self._cancel_event = None
|
|
267
|
+
|
|
268
|
+
def cancel(self) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Cancel the current prompt operation.
|
|
271
|
+
|
|
272
|
+
This sets the cancel event used by the underlying KimiCLI.run call and
|
|
273
|
+
results in RunCancelled being raised from the active prompt coroutine.
|
|
274
|
+
"""
|
|
275
|
+
if self._cancel_event is not None:
|
|
276
|
+
self._cancel_event.set()
|
|
277
|
+
|
|
278
|
+
async def close(self) -> None:
|
|
279
|
+
"""
|
|
280
|
+
Close the Session and release resources.
|
|
281
|
+
|
|
282
|
+
This cancels any ongoing prompt and cleans up tool resources.
|
|
283
|
+
"""
|
|
284
|
+
if self._closed:
|
|
285
|
+
return
|
|
286
|
+
self._closed = True
|
|
287
|
+
if self._cancel_event is not None:
|
|
288
|
+
self._cancel_event.set()
|
|
289
|
+
toolset = getattr(self._cli.soul.agent, "toolset", None)
|
|
290
|
+
cleanup = getattr(toolset, "cleanup", None)
|
|
291
|
+
if cleanup is None:
|
|
292
|
+
return
|
|
293
|
+
result = cleanup()
|
|
294
|
+
if inspect.isawaitable(result):
|
|
295
|
+
await result
|
|
296
|
+
|
|
297
|
+
async def __aenter__(self) -> Session:
|
|
298
|
+
"""Async context manager entry."""
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
302
|
+
"""Async context manager exit."""
|
|
303
|
+
await self.close()
|
|
File without changes
|