Skip to content

Commit fffea7a

Browse files
authored
feat(sdk): add Python SDK (minimax-code-sdk) 0.1 (#571)
## Summary Adds `packages/code-sdk-python`, the Python counterpart of `@minimax-ai/code-sdk`. The PyPI name is `minimax-code-sdk`, the import is `minimax_code`, it is MIT licensed and needs Python 3.10+. Its only runtime dependency is `typing_extensions`. The SDK has the same shape as the TS SDK. Each Turn runs `mcode exec --output-format=stream-json --input=- --input-format=json` and later Turns continue the Session with `--session`. Events are typed from `packages/code-sdk/src/types.ts` (schemaVersion 1) and are passed through unchanged. ## API sketch ```python from minimax_code import MCode, MCodeOptions, ThreadOptions, RunOptions client = MCode(MCodeOptions(bin=None, data_dir=None, env=None, config_path=None)) thread = client.start_thread(ThreadOptions(cwd=".", model="minimax/MiniMax-M3", permission="smart")) turn = await thread.run("Fix the failing test", RunOptions(timeout_ms=600_000)) turn.final_response, turn.items, turn.usage, turn.session_id streamed = await thread.run_streamed("Explain the diff") async for event in streamed: # raw CLI events; turn.failed is an ordinary event ... streamed.cancel() # SIGINT; breaking out of the loop or cancelling the task also stops the CLI later = client.resume_thread(thread.id) from minimax_code import SyncMCode # blocking wrapper ``` - The CLI is found from `bin`, then `$MCODE_BIN`, then `PATH`. JS entries run through `node`. The SDK checks `mcode --version` against a minimum of 0.6.3, the same as the TS SDK. - Errors follow the TS SDK: `MCodeNotFoundError`, `MCodeVersionError`, `MCodeInvocationError`, `MCodeRunError` (with `status` and `category`), `MCodeAbortError`, `MCodeProtocolError` and `ThreadBusyError`. They carry `exit_code`, the `stderr` tail, `session_id`, `result` and `items`. Exit codes map the same way as `MCODE_EXEC_EXIT_CODES`. - `item.*` events are merged by id, and duplicate snapshot updates are dropped, the same as `ItemAccumulator`. System prompts are passed through priv
1 parent c6c7f28 commit fffea7a

25 files changed

Lines changed: 2334 additions & 0 deletions

‎.github/workflows/python-sdk.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Python SDK
2+
on:
3+
push:
4+
branches: [main]
5+
paths:
6+
- "packages/code-sdk-python/**"
7+
- ".github/workflows/python-sdk.yml"
8+
pull_request:
9+
paths:
10+
- "packages/code-sdk-python/**"
11+
- ".github/workflows/python-sdk.yml"
12+
workflow_dispatch:
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
permissions:
17+
contents: read
18+
jobs:
19+
test:
20+
name: python-sdk (${{ matrix.os }}, py${{ matrix.python }})
21+
timeout-minutes: 10
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
os: [ubuntu-latest, macos-latest]
26+
python: ["3.10", "3.13"]
27+
runs-on: ${{ matrix.os }}
28+
defaults:
29+
run:
30+
working-directory: packages/code-sdk-python
31+
steps:
32+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
33+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
34+
with:
35+
python-version: ${{ matrix.python }}
36+
- run: uv sync --locked
37+
- run: uv run ruff check .
38+
- run: uv run ruff format --check .
39+
- run: uv run mypy
40+
- run: uv run pytest -W error
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.venv/
2+
__pycache__/
3+
.pytest_cache/
4+
.mypy_cache/
5+
.ruff_cache/
6+
dist/

‎packages/code-sdk-python/LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 MiniMax Code
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎packages/code-sdk-python/README.md‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# minimax-code-sdk
2+
3+
Python SDK for driving the local [MiniMax Code](https://agent.minimax.io) agent (`mcode`).
4+
5+
It mirrors the TypeScript SDK [`@minimax-ai/code-sdk`](../code-sdk): every Turn runs
6+
`mcode exec --output-format=stream-json` and continues the Session with `--session`.
7+
8+
## Requirements
9+
10+
- Python 3.10+
11+
- The `mcode` CLI (0.6.3 or newer): `npm install -g @minimax-ai/code`
12+
13+
The CLI is found from `MCodeOptions(bin=...)`, then the `MCODE_BIN` environment variable,
14+
then `PATH`.
15+
16+
## Quick start
17+
18+
```python
19+
import asyncio
20+
from minimax_code import MCode, ThreadOptions
21+
22+
23+
async def main() -> None:
24+
client = MCode()
25+
thread = client.start_thread(ThreadOptions(cwd=".", permission="smart"))
26+
27+
turn = await thread.run("Summarize this repository in one sentence.")
28+
print(turn.final_response, turn.usage)
29+
30+
# Same Session, next Turn.
31+
turn = await thread.run("Now list its top-level directories.")
32+
33+
# Persist thread.id and continue later.
34+
later = client.resume_thread(thread.id)
35+
print((await later.run("What did I ask first?")).final_response)
36+
37+
38+
asyncio.run(main())
39+
```
40+
41+
### Streaming
42+
43+
```python
44+
streamed = await thread.run_streamed("Fix the failing test.")
45+
async for event in streamed:
46+
if event["type"] == "item.updated" and event["item"]["type"] == "agent_message":
47+
print(event["item"].get("contentDelta", ""), end="")
48+
elif event["type"] == "turn.failed":
49+
print("failed:", event.get("error"))
50+
```
51+
52+
Events are the CLI's stream-json events, passed through unchanged (unknown types included).
53+
`turn.failed` is an ordinary event; iteration raises only for process or protocol failures.
54+
55+
### Cancellation
56+
57+
- `streamed.cancel()` sends SIGINT; the stream ends with the CLI's final events.
58+
- Leaving the `async for` early, or cancelling the task running `thread.run()`, stops the CLI
59+
(SIGINT, then SIGKILL after a grace period).
60+
- `RunOptions(timeout_ms=...)` is enforced by the CLI.
61+
62+
### Blocking API
63+
64+
```python
65+
from minimax_code import SyncMCode
66+
67+
with SyncMCode() as client:
68+
thread = client.start_thread()
69+
print(thread.run("hello").final_response)
70+
for event in thread.run_streamed("again"):
71+
print(event["type"])
72+
```
73+
74+
## Options
75+
76+
| Object | Fields |
77+
| --- | --- |
78+
| `MCodeOptions` | `bin`, `data_dir` (`MINIMAX_DATA_DIR`), `env` (replaces `os.environ`), `config_path` |
79+
| `ThreadOptions` | `cwd`, `model` (`provider/model`), `effort`, `permission` (`smart`/`full`/`off`), `mode`, `prompt_mode`, `system_prompt`, `append_system_prompt` |
80+
| `RunOptions` | `files`, `timeout_ms`, `max_steps` |
81+
82+
## Errors
83+
84+
All errors subclass `MCodeError` (with `exit_code`, `stderr`, `session_id`, `result`, `items`):
85+
`MCodeNotFoundError`, `MCodeVersionError`, `MCodeInvocationError`, `MCodeRunError`
86+
(`status`, `category`), `MCodeAbortError`, `MCodeProtocolError`, `ThreadBusyError`.
87+
88+
## Limitations
89+
90+
Each Turn is a separate process, so interactive permission prompts (`permission="ask"`) and
91+
custom in-process tools are not supported yet. A long-lived ACP transport is planned.
92+
93+
## Development
94+
95+
```bash
96+
uv sync
97+
uv run pytest
98+
uv run ruff check . && uv run ruff format --check . && uv run mypy
99+
```
100+
101+
End-to-end tests against a real CLI and model are opt-in:
102+
`MCODE_SDK_E2E=1 MCODE_BIN=/path/to/mcode uv run pytest -m e2e`.
103+
104+
## License
105+
106+
MIT
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Run one Turn, a follow-up Turn, and resume the Session from its id."""
2+
3+
import asyncio
4+
import sys
5+
6+
from minimax_code import MCode, ThreadOptions
7+
8+
9+
async def main() -> None:
10+
client = MCode()
11+
thread = client.start_thread(ThreadOptions(cwd=".", permission="smart"))
12+
turn = await thread.run(sys.argv[1] if len(sys.argv) > 1 else "Say hello in one sentence.")
13+
print(turn.final_response)
14+
print("session:", thread.id, "usage:", turn.usage)
15+
16+
assert thread.id is not None
17+
resumed = client.resume_thread(thread.id)
18+
print((await resumed.run("Repeat your previous answer in five words.")).final_response)
19+
20+
21+
if __name__ == "__main__":
22+
asyncio.run(main())
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Print the agent's answer as it streams, and tool calls as they finish."""
2+
3+
import asyncio
4+
import sys
5+
6+
from minimax_code import MCode
7+
8+
9+
async def main() -> None:
10+
thread = MCode().start_thread()
11+
streamed = await thread.run_streamed(sys.argv[1] if len(sys.argv) > 1 else "List files here.")
12+
async for event in streamed:
13+
item = event.get("item")
14+
if item and item["type"] == "agent_message":
15+
print(item.get("contentDelta", ""), end="", flush=True)
16+
elif item and item["type"] == "tool_call" and event["type"] == "item.completed":
17+
print(f"\n[tool] {item.get('toolCall', {}).get('name')}")
18+
elif event["type"] == "turn.failed":
19+
print("\nturn failed:", event.get("error"))
20+
print()
21+
22+
23+
if __name__ == "__main__":
24+
asyncio.run(main())
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[build-system]
2+
requires = ["hatchling>=1.24"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "minimax-code-sdk"
7+
version = "0.1.0"
8+
description = "Python SDK for driving the local MiniMax Code (mcode) agent."
9+
readme = "README.md"
10+
license = "MIT"
11+
license-files = ["LICENSE"]
12+
requires-python = ">=3.10"
13+
dependencies = ["typing_extensions>=4.7"]
14+
keywords = ["minimax", "minimax-code", "mcode", "agent", "coding-agent", "sdk"]
15+
classifiers = [
16+
"Programming Language :: Python :: 3",
17+
"Operating System :: OS Independent",
18+
"Typing :: Typed",
19+
]
20+
21+
[project.urls]
22+
Homepage = "https://agent.minimax.io"
23+
Repository = "https://github.com/MiniMax-AI/minimax-code"
24+
Issues = "https://github.com/MiniMax-AI/minimax-code/issues"
25+
26+
[dependency-groups]
27+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "ruff>=0.6", "mypy>=1.11"]
28+
29+
[tool.hatch.build.targets.wheel]
30+
packages = ["src/minimax_code"]
31+
32+
[tool.pytest.ini_options]
33+
asyncio_mode = "auto"
34+
asyncio_default_fixture_loop_scope = "function"
35+
testpaths = ["tests"]
36+
markers = ["e2e: runs a real mcode CLI against a real model (opt-in via MCODE_SDK_E2E=1)"]
37+
38+
[tool.ruff]
39+
line-length = 100
40+
target-version = "py310"
41+
42+
[tool.ruff.lint]
43+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
44+
45+
[tool.mypy]
46+
strict = true
47+
python_version = "3.10"
48+
files = ["src", "tests"]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Python SDK for driving the local MiniMax Code (``mcode``) agent."""
2+
3+
from .client import MCode
4+
from .errors import (
5+
MCODE_EXEC_EXIT_CODES,
6+
MCodeAbortError,
7+
MCodeError,
8+
MCodeInvocationError,
9+
MCodeNotFoundError,
10+
MCodeProtocolError,
11+
MCodeRunError,
12+
MCodeVersionError,
13+
ThreadBusyError,
14+
)
15+
from .sync import SyncMCode, SyncThread
16+
from .thread import StreamedTurn, Thread
17+
from .types import (
18+
AgentMessageItem,
19+
ExecError,
20+
ExecItem,
21+
ExecResult,
22+
MCodeOptions,
23+
ModelIdentity,
24+
ReasoningItem,
25+
RunOptions,
26+
ThreadEvent,
27+
ThreadItem,
28+
ThreadOptions,
29+
ToolCallItem,
30+
Turn,
31+
Usage,
32+
)
33+
34+
__version__ = "0.1.0"
35+
36+
__all__ = [
37+
"MCODE_EXEC_EXIT_CODES",
38+
"AgentMessageItem",
39+
"ExecError",
40+
"ExecItem",
41+
"ExecResult",
42+
"MCode",
43+
"MCodeAbortError",
44+
"MCodeError",
45+
"MCodeInvocationError",
46+
"MCodeNotFoundError",
47+
"MCodeOptions",
48+
"MCodeProtocolError",
49+
"MCodeRunError",
50+
"MCodeVersionError",
51+
"ModelIdentity",
52+
"ReasoningItem",
53+
"RunOptions",
54+
"StreamedTurn",
55+
"SyncMCode",
56+
"SyncThread",
57+
"Thread",
58+
"ThreadBusyError",
59+
"ThreadEvent",
60+
"ThreadItem",
61+
"ThreadOptions",
62+
"ToolCallItem",
63+
"Turn",
64+
"Usage",
65+
"__version__",
66+
]

0 commit comments

Comments
 (0)