minimax-code-sdk 0.1.0__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.
- minimax_code_sdk-0.1.0/.gitignore +6 -0
- minimax_code_sdk-0.1.0/CHANGELOG.md +76 -0
- minimax_code_sdk-0.1.0/LICENSE +21 -0
- minimax_code_sdk-0.1.0/PKG-INFO +174 -0
- minimax_code_sdk-0.1.0/README.md +151 -0
- minimax_code_sdk-0.1.0/RELEASING.md +135 -0
- minimax_code_sdk-0.1.0/examples/basic.py +22 -0
- minimax_code_sdk-0.1.0/examples/streaming.py +24 -0
- minimax_code_sdk-0.1.0/pyproject.toml +54 -0
- minimax_code_sdk-0.1.0/scripts/release_check.py +212 -0
- minimax_code_sdk-0.1.0/src/minimax_code/__init__.py +66 -0
- minimax_code_sdk-0.1.0/src/minimax_code/_args.py +90 -0
- minimax_code_sdk-0.1.0/src/minimax_code/_cli.py +205 -0
- minimax_code_sdk-0.1.0/src/minimax_code/_items.py +71 -0
- minimax_code_sdk-0.1.0/src/minimax_code/_protocol.py +56 -0
- minimax_code_sdk-0.1.0/src/minimax_code/client.py +76 -0
- minimax_code_sdk-0.1.0/src/minimax_code/errors.py +167 -0
- minimax_code_sdk-0.1.0/src/minimax_code/py.typed +0 -0
- minimax_code_sdk-0.1.0/src/minimax_code/sync.py +71 -0
- minimax_code_sdk-0.1.0/src/minimax_code/thread.py +480 -0
- minimax_code_sdk-0.1.0/src/minimax_code/types.py +192 -0
- minimax_code_sdk-0.1.0/tests/conftest.py +19 -0
- minimax_code_sdk-0.1.0/tests/fake_mcode.py +161 -0
- minimax_code_sdk-0.1.0/tests/test_cli.py +115 -0
- minimax_code_sdk-0.1.0/tests/test_e2e.py +48 -0
- minimax_code_sdk-0.1.0/tests/test_release_check.py +136 -0
- minimax_code_sdk-0.1.0/tests/test_sdk.py +365 -0
- minimax_code_sdk-0.1.0/uv.lock +517 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# `minimax-code-sdk` Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `minimax-code-sdk` Python package are documented here. The SDK is
|
|
4
|
+
versioned independently of the MiniMax Code CLI and of the TypeScript SDK. The first
|
|
5
|
+
`## X.Y.Z · YYYY-MM-DD` heading is the version being released; see [RELEASING.md](RELEASING.md).
|
|
6
|
+
|
|
7
|
+
## 0.1.0 · 2026-11-04
|
|
8
|
+
|
|
9
|
+
First stable release. `pip install minimax-code-sdk` installs it without `--pre`. The public
|
|
10
|
+
API is the same as `0.1.0b1`; the changes below fix process handling, errors and Windows support.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Leaving `async for event in streamed` early with `break` or an exception in the loop body now
|
|
15
|
+
stops the CLI and releases the Thread, as documented. Previously the CLI kept running and every
|
|
16
|
+
later Turn on the Thread raised `ThreadBusyError` until `streamed.aclose()` was called. The
|
|
17
|
+
next Turn waits for the previous CLI to shut down.
|
|
18
|
+
- When the `timeout_ms` backstop kills a CLI that did not stop on its own, the Turn raises
|
|
19
|
+
`MCodeRunError` with `status="timeout"` instead of `MCodeAbortError` ("cancelled").
|
|
20
|
+
- An `exec.completed` result without `durationMs` now raises `MCodeProtocolError`, and a failed
|
|
21
|
+
result whose `error` lacks `message` or `category` raises `MCodeRunError`, instead of a bare
|
|
22
|
+
`KeyError`.
|
|
23
|
+
- SDK errors survive pickling: `MCodeRunError` can be unpickled, and `ThreadBusyError` keeps its
|
|
24
|
+
message instead of repeating it.
|
|
25
|
+
- `repr(MCodeOptions(...))` no longer prints `env` values, which could expose secrets in logs.
|
|
26
|
+
- Cancellation sends SIGTERM instead of SIGINT. After Ctrl+C in a terminal had already delivered
|
|
27
|
+
SIGINT to the CLI, the SDK's second SIGINT hit the CLI's default action and killed it before it
|
|
28
|
+
could persist the Turn and write `exec.completed`.
|
|
29
|
+
- Cancellation waits for the CLI's 60 second shutdown budget before SIGKILL (was 10 seconds), and
|
|
30
|
+
the `timeout_ms` backstop fires 90 seconds after `timeout_ms` (was 30), matching the
|
|
31
|
+
TypeScript SDK. The shorter values could kill a CLI that was still shutting down cleanly.
|
|
32
|
+
- `exec.completed` ends the Turn: later output is ignored and a CLI that does not exit within
|
|
33
|
+
5 seconds is killed. A background process that inherited the CLI's stdout or stderr no longer
|
|
34
|
+
keeps `thread.run()` waiting until it exits; after the CLI exits, remaining output is read for
|
|
35
|
+
at most 2 seconds.
|
|
36
|
+
- A Thread pins `ThreadOptions.cwd` (default: the current directory) when it is created. A later
|
|
37
|
+
`os.chdir()` used to move the next Turn to another workspace, which the CLI rejects for an
|
|
38
|
+
existing Session.
|
|
39
|
+
- Windows: `mcode.cmd` launchers from the official installer and npm / pnpm are no longer run
|
|
40
|
+
through cmd.exe. The SDK runs the JS entry and Node runtime behind them, so arguments are not
|
|
41
|
+
reinterpreted by cmd.exe and cancelling stops the CLI itself rather than only cmd.exe.
|
|
42
|
+
`MCODE_BIN` and `PATH` are looked up case-insensitively on Windows.
|
|
43
|
+
|
|
44
|
+
### Documentation
|
|
45
|
+
|
|
46
|
+
- The README states that identical repeated `item.updated` snapshots are skipped, and that the
|
|
47
|
+
blocking API cannot run where an event loop is already running (for example Jupyter).
|
|
48
|
+
|
|
49
|
+
## 0.1.0b1 · 2026-11-03
|
|
50
|
+
|
|
51
|
+
First public beta. Install it with `pip install --pre minimax-code-sdk`; the API may still
|
|
52
|
+
change before `0.1.0`.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- `MCode`, `start_thread()`, `resume_thread()`, `thread.run()` and `thread.run_streamed()` drive
|
|
57
|
+
`mcode exec --output-format=stream-json`, one CLI process per Turn, continuing the Session with
|
|
58
|
+
`--session`.
|
|
59
|
+
- `SyncMCode` and `SyncThread`, a blocking API for code without a running event loop. It does
|
|
60
|
+
not work where a loop is already running, such as Jupyter notebooks; use the async API there.
|
|
61
|
+
- CLI resolution: explicit `MCodeOptions(bin=...)`, then `$MCODE_BIN`, then `mcode` on `PATH`.
|
|
62
|
+
Requires MiniMax Code CLI `0.6.3` or later and Python `3.10` or later.
|
|
63
|
+
- Stream events are passed through unchanged as typed dicts (event `schemaVersion` 1), including
|
|
64
|
+
unknown event types, except that identical repeated `item.updated` snapshots are skipped;
|
|
65
|
+
`thread.run()` folds them into `Turn.items`.
|
|
66
|
+
- Typed errors for invocation, run, cancellation, protocol, missing CLI, version and busy Thread
|
|
67
|
+
failures, mapped from the CLI exit codes.
|
|
68
|
+
- Cancellation through `StreamedTurn.cancel()`, early loop exit or task cancellation (`SIGINT`,
|
|
69
|
+
then `SIGKILL` after 10 seconds) and a `timeout_ms` backstop of `timeout_ms + 30s`.
|
|
70
|
+
- `system_prompt` and `append_system_prompt` are passed through private temporary files.
|
|
71
|
+
- Ships `py.typed`; the only runtime dependency is `typing_extensions`.
|
|
72
|
+
|
|
73
|
+
### Not yet available
|
|
74
|
+
|
|
75
|
+
- Interactive permission prompts (`permission="ask"`), custom in-process tools and a long-lived
|
|
76
|
+
ACP transport.
|
|
@@ -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.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: minimax-code-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for driving the local MiniMax Code (mcode) agent.
|
|
5
|
+
Project-URL: Homepage, https://agent.minimax.io
|
|
6
|
+
Project-URL: Repository, https://github.com/MiniMax-AI/minimax-code
|
|
7
|
+
Project-URL: Issues, https://github.com/MiniMax-AI/minimax-code/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,coding-agent,mcode,minimax,minimax-code,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: typing-extensions>=4.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# minimax-code-sdk
|
|
25
|
+
|
|
26
|
+
Python SDK for driving the local [MiniMax Code](https://agent.minimax.io) agent (`mcode`).
|
|
27
|
+
|
|
28
|
+
It mirrors the TypeScript SDK
|
|
29
|
+
[`@minimax-ai/code-sdk`](https://github.com/MiniMax-AI/minimax-code/tree/main/packages/code-sdk): every Turn runs
|
|
30
|
+
`mcode exec --output-format=stream-json` and continues the Session with `--session`.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python 3.10+
|
|
35
|
+
- The `mcode` CLI (0.6.3 or newer): `npm install -g @minimax-ai/code`
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install minimax-code-sdk # or: uv add minimax-code-sdk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Import it as `minimax_code`.
|
|
44
|
+
|
|
45
|
+
The CLI is found from `MCodeOptions(bin=...)`, then the `MCODE_BIN` environment variable,
|
|
46
|
+
then `PATH`. The SDK never starts it through a shell. On Windows, the `mcode.cmd` launcher
|
|
47
|
+
written by the official installer or by npm / pnpm is followed to the JS entry and Node runtime
|
|
48
|
+
it runs (the installer's release pointer is read again before every Turn, so a CLI self-update
|
|
49
|
+
is picked up); an unrecognized `.cmd` file is rejected, and you can pass the JS entry as `bin`.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import asyncio
|
|
55
|
+
from minimax_code import MCode, ThreadOptions
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def main() -> None:
|
|
59
|
+
client = MCode()
|
|
60
|
+
thread = client.start_thread(ThreadOptions(cwd=".", permission="smart"))
|
|
61
|
+
|
|
62
|
+
turn = await thread.run("Summarize this repository in one sentence.")
|
|
63
|
+
print(turn.final_response, turn.usage)
|
|
64
|
+
|
|
65
|
+
# Same Session, next Turn.
|
|
66
|
+
turn = await thread.run("Now list its top-level directories.")
|
|
67
|
+
|
|
68
|
+
# Persist thread.id and continue later, in the same workspace.
|
|
69
|
+
later = client.resume_thread(thread.id, ThreadOptions(cwd="."))
|
|
70
|
+
print((await later.run("What did I ask first?")).final_response)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Resuming a Session
|
|
77
|
+
|
|
78
|
+
A Session belongs to the workspace it was created in. Pass the same `cwd` to `resume_thread()`;
|
|
79
|
+
when `cwd` is omitted, the current working directory at that call is used. A Thread pins its
|
|
80
|
+
`cwd` when it is created, so a later `os.chdir()` does not affect it. A different workspace fails with
|
|
81
|
+
`MCodeRunError` ("Session ... belongs to workspace ..."), and retrying does not help.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
thread = client.start_thread(ThreadOptions(cwd="/path/to/project"))
|
|
85
|
+
await thread.run("...")
|
|
86
|
+
later = client.resume_thread(thread.id, ThreadOptions(cwd="/path/to/project"))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Streaming
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
streamed = await thread.run_streamed("Fix the failing test.")
|
|
93
|
+
async for event in streamed:
|
|
94
|
+
if event["type"] == "item.updated" and event["item"]["type"] == "agent_message":
|
|
95
|
+
print(event["item"].get("contentDelta", ""), end="")
|
|
96
|
+
elif event["type"] == "turn.failed":
|
|
97
|
+
print("failed:", event.get("error"))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Events are the CLI's stream-json events, passed through unchanged (unknown types included),
|
|
101
|
+
with one exception: an `item.updated` snapshot (no `contentDelta`) identical to the previous
|
|
102
|
+
snapshot of the same item is skipped. Text deltas are never skipped. `turn.failed` is an ordinary
|
|
103
|
+
event; iteration raises only for process or protocol failures.
|
|
104
|
+
|
|
105
|
+
### Cancellation
|
|
106
|
+
|
|
107
|
+
- `streamed.cancel()` sends SIGTERM, the CLI's cancel signal; the stream ends with the CLI's
|
|
108
|
+
final events. SIGTERM is used instead of SIGINT because Ctrl+C in a terminal already sends
|
|
109
|
+
SIGINT to the CLI, and a second SIGINT would skip its graceful shutdown.
|
|
110
|
+
- Leaving the `async for` early (`break` or an exception in the loop body), or cancelling the
|
|
111
|
+
task running `thread.run()`, stops the CLI: SIGTERM, then SIGKILL if it is still running after
|
|
112
|
+
the CLI's 60 second shutdown budget. The next Turn on the same Thread waits for that shutdown
|
|
113
|
+
instead of raising `ThreadBusyError`; `async with streamed:` or `await streamed.aclose()`
|
|
114
|
+
stops it deterministically. On Windows, cancellation stops the CLI immediately.
|
|
115
|
+
- `RunOptions(timeout_ms=...)` is enforced by the CLI. If the CLI is still running 90 seconds
|
|
116
|
+
after `timeout_ms` (its shutdown budget plus startup time), the SDK kills it and raises
|
|
117
|
+
`MCodeRunError` with `status="timeout"`.
|
|
118
|
+
- `exec.completed` ends the Turn. A CLI that keeps running afterwards is stopped after 5 seconds,
|
|
119
|
+
and a background process that inherited the CLI's output does not keep the Turn open.
|
|
120
|
+
|
|
121
|
+
### Blocking API
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from minimax_code import SyncMCode
|
|
125
|
+
|
|
126
|
+
with SyncMCode() as client:
|
|
127
|
+
thread = client.start_thread()
|
|
128
|
+
print(thread.run("hello").final_response)
|
|
129
|
+
for event in thread.run_streamed("again"):
|
|
130
|
+
print(event["type"])
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The blocking API runs its own event loop, so it cannot be used where a loop is already running,
|
|
134
|
+
such as a Jupyter notebook or an `async def` function. Use the async API with `await` there.
|
|
135
|
+
|
|
136
|
+
## Options
|
|
137
|
+
|
|
138
|
+
| Object | Fields |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `MCodeOptions` | `bin`, `data_dir` (`MINIMAX_DATA_DIR`), `env` (replaces `os.environ`; values hidden in `repr()`), `config_path` |
|
|
141
|
+
| `ThreadOptions` | `cwd`, `model` (`provider/model`), `effort`, `permission` (`smart`/`full`/`off`), `mode`, `prompt_mode`, `system_prompt`, `append_system_prompt` |
|
|
142
|
+
| `RunOptions` | `files`, `timeout_ms`, `max_steps` |
|
|
143
|
+
|
|
144
|
+
## Errors
|
|
145
|
+
|
|
146
|
+
All errors subclass `MCodeError` (with `exit_code`, `stderr`, `session_id`, `result`, `items`):
|
|
147
|
+
`MCodeNotFoundError`, `MCodeVersionError`, `MCodeInvocationError`, `MCodeRunError`
|
|
148
|
+
(`status`, `category`), `MCodeAbortError`, `MCodeProtocolError`, `ThreadBusyError`.
|
|
149
|
+
Errors can be pickled, for example to return them from a `ProcessPoolExecutor` worker.
|
|
150
|
+
|
|
151
|
+
## Limitations
|
|
152
|
+
|
|
153
|
+
Each Turn is a separate process, so interactive permission prompts (`permission="ask"`) and
|
|
154
|
+
custom in-process tools are not supported yet. A long-lived ACP transport is planned.
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uv sync
|
|
160
|
+
uv run pytest
|
|
161
|
+
uv run ruff check . && uv run ruff format --check . && uv run mypy
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
End-to-end tests against a real CLI and model are opt-in:
|
|
165
|
+
`MCODE_SDK_E2E=1 MCODE_BIN=/path/to/mcode uv run pytest -m e2e`.
|
|
166
|
+
|
|
167
|
+
Release notes are in
|
|
168
|
+
[CHANGELOG.md](https://github.com/MiniMax-AI/minimax-code/blob/main/packages/code-sdk-python/CHANGELOG.md);
|
|
169
|
+
maintainers publish with
|
|
170
|
+
[RELEASING.md](https://github.com/MiniMax-AI/minimax-code/blob/main/packages/code-sdk-python/RELEASING.md).
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
|
6
|
+
[`@minimax-ai/code-sdk`](https://github.com/MiniMax-AI/minimax-code/tree/main/packages/code-sdk): every Turn runs
|
|
7
|
+
`mcode exec --output-format=stream-json` and continues the Session with `--session`.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Python 3.10+
|
|
12
|
+
- The `mcode` CLI (0.6.3 or newer): `npm install -g @minimax-ai/code`
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install minimax-code-sdk # or: uv add minimax-code-sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Import it as `minimax_code`.
|
|
21
|
+
|
|
22
|
+
The CLI is found from `MCodeOptions(bin=...)`, then the `MCODE_BIN` environment variable,
|
|
23
|
+
then `PATH`. The SDK never starts it through a shell. On Windows, the `mcode.cmd` launcher
|
|
24
|
+
written by the official installer or by npm / pnpm is followed to the JS entry and Node runtime
|
|
25
|
+
it runs (the installer's release pointer is read again before every Turn, so a CLI self-update
|
|
26
|
+
is picked up); an unrecognized `.cmd` file is rejected, and you can pass the JS entry as `bin`.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import asyncio
|
|
32
|
+
from minimax_code import MCode, ThreadOptions
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def main() -> None:
|
|
36
|
+
client = MCode()
|
|
37
|
+
thread = client.start_thread(ThreadOptions(cwd=".", permission="smart"))
|
|
38
|
+
|
|
39
|
+
turn = await thread.run("Summarize this repository in one sentence.")
|
|
40
|
+
print(turn.final_response, turn.usage)
|
|
41
|
+
|
|
42
|
+
# Same Session, next Turn.
|
|
43
|
+
turn = await thread.run("Now list its top-level directories.")
|
|
44
|
+
|
|
45
|
+
# Persist thread.id and continue later, in the same workspace.
|
|
46
|
+
later = client.resume_thread(thread.id, ThreadOptions(cwd="."))
|
|
47
|
+
print((await later.run("What did I ask first?")).final_response)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
asyncio.run(main())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Resuming a Session
|
|
54
|
+
|
|
55
|
+
A Session belongs to the workspace it was created in. Pass the same `cwd` to `resume_thread()`;
|
|
56
|
+
when `cwd` is omitted, the current working directory at that call is used. A Thread pins its
|
|
57
|
+
`cwd` when it is created, so a later `os.chdir()` does not affect it. A different workspace fails with
|
|
58
|
+
`MCodeRunError` ("Session ... belongs to workspace ..."), and retrying does not help.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
thread = client.start_thread(ThreadOptions(cwd="/path/to/project"))
|
|
62
|
+
await thread.run("...")
|
|
63
|
+
later = client.resume_thread(thread.id, ThreadOptions(cwd="/path/to/project"))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Streaming
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
streamed = await thread.run_streamed("Fix the failing test.")
|
|
70
|
+
async for event in streamed:
|
|
71
|
+
if event["type"] == "item.updated" and event["item"]["type"] == "agent_message":
|
|
72
|
+
print(event["item"].get("contentDelta", ""), end="")
|
|
73
|
+
elif event["type"] == "turn.failed":
|
|
74
|
+
print("failed:", event.get("error"))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Events are the CLI's stream-json events, passed through unchanged (unknown types included),
|
|
78
|
+
with one exception: an `item.updated` snapshot (no `contentDelta`) identical to the previous
|
|
79
|
+
snapshot of the same item is skipped. Text deltas are never skipped. `turn.failed` is an ordinary
|
|
80
|
+
event; iteration raises only for process or protocol failures.
|
|
81
|
+
|
|
82
|
+
### Cancellation
|
|
83
|
+
|
|
84
|
+
- `streamed.cancel()` sends SIGTERM, the CLI's cancel signal; the stream ends with the CLI's
|
|
85
|
+
final events. SIGTERM is used instead of SIGINT because Ctrl+C in a terminal already sends
|
|
86
|
+
SIGINT to the CLI, and a second SIGINT would skip its graceful shutdown.
|
|
87
|
+
- Leaving the `async for` early (`break` or an exception in the loop body), or cancelling the
|
|
88
|
+
task running `thread.run()`, stops the CLI: SIGTERM, then SIGKILL if it is still running after
|
|
89
|
+
the CLI's 60 second shutdown budget. The next Turn on the same Thread waits for that shutdown
|
|
90
|
+
instead of raising `ThreadBusyError`; `async with streamed:` or `await streamed.aclose()`
|
|
91
|
+
stops it deterministically. On Windows, cancellation stops the CLI immediately.
|
|
92
|
+
- `RunOptions(timeout_ms=...)` is enforced by the CLI. If the CLI is still running 90 seconds
|
|
93
|
+
after `timeout_ms` (its shutdown budget plus startup time), the SDK kills it and raises
|
|
94
|
+
`MCodeRunError` with `status="timeout"`.
|
|
95
|
+
- `exec.completed` ends the Turn. A CLI that keeps running afterwards is stopped after 5 seconds,
|
|
96
|
+
and a background process that inherited the CLI's output does not keep the Turn open.
|
|
97
|
+
|
|
98
|
+
### Blocking API
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from minimax_code import SyncMCode
|
|
102
|
+
|
|
103
|
+
with SyncMCode() as client:
|
|
104
|
+
thread = client.start_thread()
|
|
105
|
+
print(thread.run("hello").final_response)
|
|
106
|
+
for event in thread.run_streamed("again"):
|
|
107
|
+
print(event["type"])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The blocking API runs its own event loop, so it cannot be used where a loop is already running,
|
|
111
|
+
such as a Jupyter notebook or an `async def` function. Use the async API with `await` there.
|
|
112
|
+
|
|
113
|
+
## Options
|
|
114
|
+
|
|
115
|
+
| Object | Fields |
|
|
116
|
+
| --- | --- |
|
|
117
|
+
| `MCodeOptions` | `bin`, `data_dir` (`MINIMAX_DATA_DIR`), `env` (replaces `os.environ`; values hidden in `repr()`), `config_path` |
|
|
118
|
+
| `ThreadOptions` | `cwd`, `model` (`provider/model`), `effort`, `permission` (`smart`/`full`/`off`), `mode`, `prompt_mode`, `system_prompt`, `append_system_prompt` |
|
|
119
|
+
| `RunOptions` | `files`, `timeout_ms`, `max_steps` |
|
|
120
|
+
|
|
121
|
+
## Errors
|
|
122
|
+
|
|
123
|
+
All errors subclass `MCodeError` (with `exit_code`, `stderr`, `session_id`, `result`, `items`):
|
|
124
|
+
`MCodeNotFoundError`, `MCodeVersionError`, `MCodeInvocationError`, `MCodeRunError`
|
|
125
|
+
(`status`, `category`), `MCodeAbortError`, `MCodeProtocolError`, `ThreadBusyError`.
|
|
126
|
+
Errors can be pickled, for example to return them from a `ProcessPoolExecutor` worker.
|
|
127
|
+
|
|
128
|
+
## Limitations
|
|
129
|
+
|
|
130
|
+
Each Turn is a separate process, so interactive permission prompts (`permission="ask"`) and
|
|
131
|
+
custom in-process tools are not supported yet. A long-lived ACP transport is planned.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
uv sync
|
|
137
|
+
uv run pytest
|
|
138
|
+
uv run ruff check . && uv run ruff format --check . && uv run mypy
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
End-to-end tests against a real CLI and model are opt-in:
|
|
142
|
+
`MCODE_SDK_E2E=1 MCODE_BIN=/path/to/mcode uv run pytest -m e2e`.
|
|
143
|
+
|
|
144
|
+
Release notes are in
|
|
145
|
+
[CHANGELOG.md](https://github.com/MiniMax-AI/minimax-code/blob/main/packages/code-sdk-python/CHANGELOG.md);
|
|
146
|
+
maintainers publish with
|
|
147
|
+
[RELEASING.md](https://github.com/MiniMax-AI/minimax-code/blob/main/packages/code-sdk-python/RELEASING.md).
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Releasing `minimax-code-sdk`
|
|
2
|
+
|
|
3
|
+
The Python SDK is published to the public PyPI index as `minimax-code-sdk` by the
|
|
4
|
+
`Python SDK release` workflow (`.github/workflows/python-sdk-release.yml`) using PyPI Trusted
|
|
5
|
+
Publishing. No PyPI token is stored in the repository or in GitHub secrets. It is versioned
|
|
6
|
+
independently of the CLI and of `@minimax-ai/code-sdk`.
|
|
7
|
+
|
|
8
|
+
## Rules
|
|
9
|
+
|
|
10
|
+
- Publish only from a commit on `main` that went through a reviewed pull request. The workflow
|
|
11
|
+
rejects a tag whose commit is not contained in `origin/main`.
|
|
12
|
+
- A published version is immutable. PyPI never accepts the same file name twice, even after a
|
|
13
|
+
release is deleted. Fix problems in a new version; prefer **yanking** a broken release over
|
|
14
|
+
deleting it.
|
|
15
|
+
- Versions are stable `X.Y.Z` or PEP 440 pre-releases `X.Y.ZaN`, `X.Y.ZbN` and `X.Y.ZrcN` (for
|
|
16
|
+
example `0.1.0b1`), written in canonical form. Never use npm-style `0.1.0-beta.1`. See
|
|
17
|
+
[Pre-releases](#pre-releases).
|
|
18
|
+
- Release tags are `code-sdk-python-v<version>`, for example `code-sdk-python-v0.1.0b1`. They are separate from CLI tags (`vX.Y.Z`) and
|
|
19
|
+
TypeScript SDK tags (`code-sdk-vX.Y.Z`). Never move or delete a pushed release tag.
|
|
20
|
+
- Do not upload from a workstation. If the workflow is unavailable, fix the workflow.
|
|
21
|
+
|
|
22
|
+
## One-time setup
|
|
23
|
+
|
|
24
|
+
1. The PyPI project `minimax-code-sdk` belongs to the `minimax-ai` PyPI organization. Every
|
|
25
|
+
owner and maintainer account uses two-factor authentication.
|
|
26
|
+
2. On PyPI, add a Trusted Publisher (a *pending* publisher before the first upload) with:
|
|
27
|
+
|
|
28
|
+
| Field | Value |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| PyPI project name | `minimax-code-sdk` |
|
|
31
|
+
| Owner | `MiniMax-AI` |
|
|
32
|
+
| Repository | `minimax-code` |
|
|
33
|
+
| Workflow | `python-sdk-release.yml` |
|
|
34
|
+
| Environment | `pypi` |
|
|
35
|
+
|
|
36
|
+
3. For rehearsals, add the same publisher on TestPyPI with environment `testpypi`. TestPyPI
|
|
37
|
+
accounts and projects are separate from PyPI.
|
|
38
|
+
4. In the GitHub repository settings, create the environments `pypi` and `testpypi`. Give
|
|
39
|
+
`pypi` required reviewers and restrict its deployment refs to `code-sdk-python-v*` tags.
|
|
40
|
+
|
|
41
|
+
## 1. Prepare the release in a pull request
|
|
42
|
+
|
|
43
|
+
1. Update `version` in `packages/code-sdk-python/pyproject.toml` and `__version__` in
|
|
44
|
+
`src/minimax_code/__init__.py`.
|
|
45
|
+
2. Add a section at the top of `CHANGELOG.md`: `## X.Y.Z · YYYY-MM-DD` with the user-visible
|
|
46
|
+
changes.
|
|
47
|
+
3. If the minimum supported CLI, the Python range or the event `schemaVersion` changes, update the
|
|
48
|
+
README and `MIN_CLI_VERSION` in `src/minimax_code/_cli.py`.
|
|
49
|
+
4. Run the local checks:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd packages/code-sdk-python
|
|
53
|
+
uv sync --locked
|
|
54
|
+
uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest -W error
|
|
55
|
+
uv run python scripts/release_check.py metadata --index pypi
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
5. Merge the pull request into `main` after the usual review and checks.
|
|
59
|
+
|
|
60
|
+
## 2. Optional: rehearse on TestPyPI
|
|
61
|
+
|
|
62
|
+
Run the `Python SDK release` workflow manually with `repository: testpypi`. It runs every
|
|
63
|
+
check below and uploads to TestPyPI. Each version can be uploaded to TestPyPI only once, so
|
|
64
|
+
rehearse only versions you intend to release. A manual run with `repository: none` builds and
|
|
65
|
+
checks without uploading anywhere.
|
|
66
|
+
|
|
67
|
+
## 3. Tag and publish
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git switch main
|
|
71
|
+
git pull --ff-only
|
|
72
|
+
git tag -a code-sdk-python-vX.Y.Z -m "minimax-code-sdk X.Y.Z"
|
|
73
|
+
git push origin code-sdk-python-vX.Y.Z
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The tag push starts the workflow. Its `build` job:
|
|
77
|
+
|
|
78
|
+
1. checks that the tag matches `pyproject.toml`, `__version__` and the first CHANGELOG heading,
|
|
79
|
+
that the tagged commit is on `origin/main`, and that the version is not on PyPI yet;
|
|
80
|
+
2. runs ruff, mypy and the test suite;
|
|
81
|
+
3. builds the wheel and sdist with `uv build` and runs `twine check --strict`;
|
|
82
|
+
4. runs `scripts/release_check.py dist`, which rejects unexpected wheel entries, local or
|
|
83
|
+
generated files and unsafe paths in the sdist, and prints the sha256 of both files;
|
|
84
|
+
5. scans the extracted archives with the repository's public text policy
|
|
85
|
+
(`scripts/lib/public-text-policy.mjs`) and Gitleaks;
|
|
86
|
+
6. installs the wheel into an empty virtual environment and checks `minimax_code.__version__`.
|
|
87
|
+
|
|
88
|
+
The `publish-pypi` job then waits for approval on the `pypi` environment and uploads exactly the
|
|
89
|
+
checked files with `pypa/gh-action-pypi-publish`, which also attaches PyPI attestations.
|
|
90
|
+
|
|
91
|
+
## Pre-releases
|
|
92
|
+
|
|
93
|
+
Pre-releases go to the real PyPI index through the same workflow and checks; only the version
|
|
94
|
+
differs. Installers skip them unless the user passes `--pre` (pip), `--prerelease=allow` (uv) or
|
|
95
|
+
pins the exact version, so `pip install minimax-code-sdk` keeps resolving to the newest stable
|
|
96
|
+
release once one exists. Keep the README install instructions in line with the latest release.
|
|
97
|
+
|
|
98
|
+
- Fix a broken pre-release with the next number (`0.1.0b2`), not by reusing a version.
|
|
99
|
+
- Promote by releasing `0.1.0` with its own CHANGELOG section; earlier pre-releases stay in the
|
|
100
|
+
history and need no cleanup.
|
|
101
|
+
- When the first stable release ships, drop `--pre` from the README install instructions and
|
|
102
|
+
replace `Development Status :: 4 - Beta` in `pyproject.toml` as appropriate.
|
|
103
|
+
|
|
104
|
+
## 4. Read back
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
curl -fsS https://pypi.org/pypi/minimax-code-sdk/X.Y.Z/json \
|
|
108
|
+
| python3 -c 'import json,sys; [print(u["digests"]["sha256"], u["filename"]) for u in json.load(sys.stdin)["urls"]]'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Both digests must equal the `sha256` lines printed by the `Check distribution contents` step. If
|
|
112
|
+
they differ, stop: the index holds different bytes than the checked files.
|
|
113
|
+
|
|
114
|
+
## 5. Smoke test the published package
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
smoke="$(mktemp -d)" && cd "$smoke"
|
|
118
|
+
uv venv -q && uv pip install -q "minimax-code-sdk==X.Y.Z"
|
|
119
|
+
.venv/bin/python -c "import minimax_code; print(minimax_code.__version__, minimax_code.MCode)"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
It must print `X.Y.Z` and the `MCode` class. PyPI's CDN can take a few minutes to serve a new
|
|
123
|
+
version.
|
|
124
|
+
|
|
125
|
+
## Failure handling
|
|
126
|
+
|
|
127
|
+
- **The `build` job fails**: nothing was published. Fix it in a pull request, merge, and tag a new
|
|
128
|
+
commit. If the version number itself was wrong, use a new version; do not reuse the tag.
|
|
129
|
+
- **`publish-pypi` fails before upload** (Trusted Publisher mismatch, network): fix the cause and
|
|
130
|
+
re-run the failed job of the same workflow run. The artifact is the already checked build.
|
|
131
|
+
- **Upload accepted but read-back fails**: wait and repeat the read-back. Do not re-run the upload
|
|
132
|
+
unless PyPI confirms the version does not exist.
|
|
133
|
+
- **Digests differ or the smoke test fails**: the version is published and cannot be replaced.
|
|
134
|
+
Release a fixed version and yank the broken one on PyPI (*Manage project → Releases → Options →
|
|
135
|
+
Yank*) with a reason.
|
|
@@ -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())
|
|
@@ -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())
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
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
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://agent.minimax.io"
|
|
29
|
+
Repository = "https://github.com/MiniMax-AI/minimax-code"
|
|
30
|
+
Issues = "https://github.com/MiniMax-AI/minimax-code/issues"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "ruff>=0.6", "mypy>=1.11"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/minimax_code"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
asyncio_mode = "auto"
|
|
40
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
markers = ["e2e: runs a real mcode CLI against a real model (opt-in via MCODE_SDK_E2E=1)"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 100
|
|
46
|
+
target-version = "py310"
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
strict = true
|
|
53
|
+
python_version = "3.10"
|
|
54
|
+
files = ["src", "tests"]
|