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.
@@ -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