minimax-code-sdk 0.1.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.0b1"
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
+ ]
minimax_code/_args.py ADDED
@@ -0,0 +1,90 @@
1
+ """Builds ``mcode exec`` argv. Mirrors ``packages/code-sdk/src/args.ts``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from .errors import MCodeInvocationError
8
+ from .types import RunOptions, ThreadOptions
9
+
10
+ _PERMISSIONS = {"smart", "full", "off"}
11
+ _MODES = {"standard", "lightweight"}
12
+ _PROMPT_MODES = {"tui", "coding", "work"}
13
+ MAX_TIMEOUT_MS = 2**31 - 1
14
+
15
+
16
+ def validate_thread_options(options: ThreadOptions, resuming: bool) -> None:
17
+ if options.permission is not None and options.permission not in _PERMISSIONS:
18
+ raise MCodeInvocationError(
19
+ f"permission must be smart, full or off; received {options.permission!r}."
20
+ )
21
+ if options.mode is not None and options.mode not in _MODES:
22
+ raise MCodeInvocationError(
23
+ f"mode must be standard or lightweight; received {options.mode!r}."
24
+ )
25
+ if resuming and options.mode == "lightweight":
26
+ raise MCodeInvocationError(
27
+ 'mode "lightweight" requires a new Thread; it cannot be resumed.'
28
+ )
29
+ if options.prompt_mode is not None and options.prompt_mode not in _PROMPT_MODES:
30
+ raise MCodeInvocationError(
31
+ f"prompt_mode must be tui, coding or work; received {options.prompt_mode!r}."
32
+ )
33
+
34
+
35
+ def _positive_int(value: object) -> bool:
36
+ return isinstance(value, int) and not isinstance(value, bool) and value > 0
37
+
38
+
39
+ def validate_run_options(options: RunOptions) -> None:
40
+ if options.timeout_ms is not None and (
41
+ not _positive_int(options.timeout_ms) or options.timeout_ms > MAX_TIMEOUT_MS
42
+ ):
43
+ raise MCodeInvocationError(
44
+ f"timeout_ms must be a positive integer no greater than {MAX_TIMEOUT_MS}."
45
+ )
46
+ if options.max_steps is not None and not _positive_int(options.max_steps):
47
+ raise MCodeInvocationError("max_steps must be a positive integer.")
48
+
49
+
50
+ def build_exec_args(
51
+ *,
52
+ thread: ThreadOptions,
53
+ session_id: str | None,
54
+ run: RunOptions,
55
+ config_path: str | None = None,
56
+ system_prompt_file: str | None = None,
57
+ append_system_prompt_file: str | None = None,
58
+ ) -> list[str]:
59
+ """The prompt travels over stdin and system prompts through temp files; values use the
60
+ ``--flag=value`` form so a value starting with ``-`` is never parsed as a flag."""
61
+ args = [
62
+ "exec",
63
+ "--output-format=stream-json",
64
+ "--input=-",
65
+ "--input-format=json",
66
+ f"--cwd={os.path.abspath(thread.cwd or os.getcwd())}",
67
+ ]
68
+
69
+ def push(flag: str, value: str | None) -> None:
70
+ if value is not None:
71
+ args.append(f"{flag}={value}")
72
+
73
+ push("--model", thread.model)
74
+ push("--effort", thread.effort)
75
+ push("--permission", thread.permission)
76
+ push("--prompt-mode", thread.prompt_mode)
77
+ push("--config", config_path)
78
+ if session_id is None:
79
+ push("--mode", thread.mode)
80
+ else:
81
+ push("--session", session_id)
82
+ push("--system-prompt-file", system_prompt_file)
83
+ push("--append-system-prompt-file", append_system_prompt_file)
84
+ for file in run.files:
85
+ push("--file", file)
86
+ if run.timeout_ms is not None:
87
+ push("--timeout", f"{run.timeout_ms}ms")
88
+ if run.max_steps is not None:
89
+ push("--max-steps", str(run.max_steps))
90
+ return args
minimax_code/_cli.py ADDED
@@ -0,0 +1,87 @@
1
+ """Locates and version-checks the ``mcode`` CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import re
8
+ import shutil
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+
12
+ from .errors import MCODE_INSTALL_HINT, MCodeNotFoundError, MCodeVersionError
13
+
14
+ MIN_CLI_VERSION = (0, 6, 3)
15
+ _JS_SUFFIXES = (".js", ".mjs", ".cjs")
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ResolvedCli:
20
+ path: str
21
+
22
+ def command(self, args: list[str]) -> list[str]:
23
+ if self.path.endswith(_JS_SUFFIXES):
24
+ node = shutil.which("node") or "node"
25
+ return [node, self.path, *args]
26
+ return [self.path, *args]
27
+
28
+
29
+ def resolve_cli(bin: str | None, env: Mapping[str, str]) -> ResolvedCli:
30
+ """Explicit ``bin`` → ``$MCODE_BIN`` → ``mcode`` on ``PATH``."""
31
+ explicit = bin or env.get("MCODE_BIN") or None
32
+ if explicit:
33
+ path = os.path.abspath(os.path.expanduser(explicit))
34
+ if not os.path.isfile(path):
35
+ source = "bin" if bin else "MCODE_BIN"
36
+ raise MCodeNotFoundError(f"mcode CLI from {source} does not exist: {path}")
37
+ return ResolvedCli(path)
38
+ found = shutil.which("mcode", path=env.get("PATH", os.defpath))
39
+ if not found:
40
+ raise MCodeNotFoundError(
41
+ "Could not find the mcode CLI on PATH. Install it with "
42
+ f"`{MCODE_INSTALL_HINT}`, set MCODE_BIN, or pass MCodeOptions(bin=...)."
43
+ )
44
+ return ResolvedCli(found)
45
+
46
+
47
+ def parse_version(text: str) -> tuple[int, int, int] | None:
48
+ match = re.search(r"(\d+)\.(\d+)\.(\d+)", text)
49
+ if not match:
50
+ return None
51
+ major, minor, patch = (int(part) for part in match.groups())
52
+ return (major, minor, patch)
53
+
54
+
55
+ async def check_cli_version(cli: ResolvedCli, env: Mapping[str, str]) -> str:
56
+ try:
57
+ proc = await asyncio.create_subprocess_exec(
58
+ *cli.command(["--version"]),
59
+ stdin=asyncio.subprocess.DEVNULL,
60
+ stdout=asyncio.subprocess.PIPE,
61
+ stderr=asyncio.subprocess.PIPE,
62
+ env=dict(env),
63
+ )
64
+ out, err = await asyncio.wait_for(proc.communicate(), timeout=30)
65
+ except (OSError, asyncio.TimeoutError) as error:
66
+ raise MCodeNotFoundError(f"Could not run `{cli.path} --version`: {error}") from error
67
+ if proc.returncode != 0:
68
+ raise MCodeVersionError(
69
+ f"`mcode --version` failed ({proc.returncode}); cannot verify CLI compatibility.",
70
+ exit_code=proc.returncode,
71
+ stderr=err.decode("utf-8", "replace"),
72
+ )
73
+ text = out.decode("utf-8", "replace")
74
+ version = parse_version(text)
75
+ if version is None:
76
+ raise MCodeVersionError(f"Could not read the mcode version from: {text.strip()!r}.")
77
+ if version < MIN_CLI_VERSION:
78
+ minimum = ".".join(map(str, MIN_CLI_VERSION))
79
+ found = ".".join(map(str, version))
80
+ raise MCodeVersionError(
81
+ f"mcode {found} is older than the minimum {minimum} required by minimax-code-sdk. "
82
+ f"Upgrade with `{MCODE_INSTALL_HINT}`."
83
+ )
84
+ return ".".join(map(str, version))
85
+
86
+
87
+ __all__ = ["ResolvedCli", "check_cli_version", "resolve_cli"]
minimax_code/_items.py ADDED
@@ -0,0 +1,71 @@
1
+ """Folds ``item.*`` events into SDK items. Mirrors ``packages/code-sdk/src/items.ts``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from .types import AgentMessageItem, ReasoningItem, ThreadEvent, ThreadItem, ToolCallItem
10
+
11
+
12
+ @dataclass
13
+ class _State:
14
+ type: str
15
+ text: str = ""
16
+ tool_call: dict[str, Any] | None = None
17
+ completed: bool = False
18
+ snapshot: str = ""
19
+
20
+
21
+ class ItemAccumulator:
22
+ def __init__(self) -> None:
23
+ self._states: dict[str, _State] = {}
24
+
25
+ def apply(self, event: ThreadEvent) -> bool:
26
+ """Applies one event. Returns False for a duplicate snapshot ``item.updated``."""
27
+ if event["type"] not in ("item.started", "item.updated", "item.completed"):
28
+ return True
29
+ item = event.get("item")
30
+ if item is None:
31
+ return True
32
+ delta = item.get("contentDelta")
33
+ snapshot = json.dumps(item, sort_keys=True) if delta is None else ""
34
+ state = self._states.get(item["id"])
35
+ if (
36
+ state is not None
37
+ and event["type"] == "item.updated"
38
+ and snapshot
39
+ and state.snapshot == snapshot
40
+ and not state.completed
41
+ ):
42
+ return False
43
+ if state is None:
44
+ state = _State(type=item["type"])
45
+ self._states[item["id"]] = state
46
+ state.snapshot = snapshot
47
+ if delta is not None:
48
+ state.text += delta
49
+ if "content" in item:
50
+ state.text = item["content"]
51
+ if "toolCall" in item:
52
+ state.tool_call = item["toolCall"]
53
+ if event["type"] == "item.completed":
54
+ state.completed = True
55
+ return True
56
+
57
+ def items(self) -> list[ThreadItem]:
58
+ out: list[ThreadItem] = []
59
+ for item_id, s in self._states.items():
60
+ incomplete = not s.completed
61
+ if s.type == "agent_message":
62
+ out.append(AgentMessageItem(id=item_id, text=s.text, incomplete=incomplete))
63
+ elif s.type == "reasoning":
64
+ out.append(ReasoningItem(id=item_id, text=s.text, incomplete=incomplete))
65
+ elif s.type == "tool_call":
66
+ out.append(
67
+ ToolCallItem(
68
+ id=item_id, tool_call=s.tool_call or {"id": item_id}, incomplete=incomplete
69
+ )
70
+ )
71
+ return out
@@ -0,0 +1,56 @@
1
+ """Parses stdout lines. Mirrors ``packages/code-sdk/src/protocol.ts``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, cast
7
+
8
+ from .errors import MCodeProtocolError, MCodeVersionError
9
+ from .types import ThreadEvent
10
+
11
+ SUPPORTED_SCHEMA_VERSION = 1
12
+
13
+
14
+ def _truncate(text: str) -> str:
15
+ return text if len(text) <= 200 else text[:200] + "…"
16
+
17
+
18
+ def parse_event_line(line: str) -> ThreadEvent | None:
19
+ text = line.strip()
20
+ if not text:
21
+ return None
22
+ try:
23
+ value: Any = json.loads(text)
24
+ except json.JSONDecodeError as error:
25
+ raise MCodeProtocolError(f"mcode stdout is not JSON: {_truncate(text)}") from error
26
+ if not isinstance(value, dict):
27
+ raise MCodeProtocolError(f"mcode stdout line is not a JSON object: {_truncate(text)}")
28
+ if value.get("schemaVersion") != SUPPORTED_SCHEMA_VERSION:
29
+ raise MCodeVersionError(
30
+ f"Unsupported mcode event schemaVersion {value.get('schemaVersion')!r}; "
31
+ f"this SDK supports {SUPPORTED_SCHEMA_VERSION}."
32
+ )
33
+ if not isinstance(value.get("type"), str) or not isinstance(value.get("sessionId"), str):
34
+ raise MCodeProtocolError(f"mcode event is missing type or sessionId: {_truncate(text)}")
35
+ kind: str = value["type"]
36
+ if kind.startswith("item."):
37
+ item = value.get("item")
38
+ if not (
39
+ isinstance(item, dict)
40
+ and isinstance(item.get("id"), str)
41
+ and isinstance(item.get("type"), str)
42
+ ):
43
+ raise MCodeProtocolError(f"mcode {kind} event has an invalid item: {_truncate(text)}")
44
+ if kind == "exec.completed":
45
+ result = value.get("result")
46
+ if not (
47
+ isinstance(result, dict)
48
+ and result.get("schemaVersion") == SUPPORTED_SCHEMA_VERSION
49
+ and isinstance(result.get("status"), str)
50
+ and isinstance(result.get("sessionId"), str)
51
+ and isinstance(result.get("turnId"), str)
52
+ ):
53
+ raise MCodeProtocolError(
54
+ f"mcode exec.completed has an invalid result: {_truncate(text)}"
55
+ )
56
+ return cast(ThreadEvent, value)
minimax_code/client.py ADDED
@@ -0,0 +1,61 @@
1
+ """Entry point. Mirrors ``packages/code-sdk/src/mcode.ts``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+
8
+ from ._args import validate_thread_options
9
+ from ._cli import ResolvedCli, check_cli_version, resolve_cli
10
+ from .errors import MCodeInvocationError
11
+ from .thread import Thread, ThreadHost
12
+ from .types import MCodeOptions, ThreadOptions
13
+
14
+
15
+ class MCode:
16
+ """One client resolves and version-checks the CLI once, then starts Threads."""
17
+
18
+ def __init__(self, options: MCodeOptions | None = None) -> None:
19
+ self._options = options or MCodeOptions()
20
+ self._cli: ResolvedCli | None = None
21
+ self._lock: asyncio.Lock | None = None
22
+ config = self._options.config_path
23
+ self._host = ThreadHost(
24
+ config_path=os.path.abspath(config) if config else None,
25
+ child_env=self._child_env,
26
+ prepare_cli=self._prepare_cli,
27
+ )
28
+
29
+ def start_thread(self, options: ThreadOptions | None = None) -> Thread:
30
+ """Starts a new Thread. The Session is created by its first Turn."""
31
+ options = options or ThreadOptions()
32
+ validate_thread_options(options, resuming=False)
33
+ return Thread(self._host, options, None)
34
+
35
+ def resume_thread(self, thread_id: str, options: ThreadOptions | None = None) -> Thread:
36
+ """Continues an existing Session, for example one persisted from ``thread.id``."""
37
+ if not isinstance(thread_id, str) or not thread_id.strip():
38
+ raise MCodeInvocationError("resume_thread requires a non-empty Session id.")
39
+ options = options or ThreadOptions()
40
+ validate_thread_options(options, resuming=True)
41
+ return Thread(self._host, options, thread_id.strip())
42
+
43
+ def _child_env(self) -> dict[str, str]:
44
+ env = dict(self._options.env if self._options.env is not None else os.environ)
45
+ if self._options.data_dir is not None:
46
+ env["MINIMAX_DATA_DIR"] = os.path.abspath(self._options.data_dir)
47
+ return env
48
+
49
+ async def _prepare_cli(self) -> ResolvedCli:
50
+ if self._cli is not None:
51
+ return self._cli
52
+ if self._lock is None:
53
+ self._lock = asyncio.Lock()
54
+ async with self._lock:
55
+ if self._cli is None:
56
+ env = self._child_env()
57
+ cli = resolve_cli(self._options.bin, env)
58
+ await check_cli_version(cli, env)
59
+ # Only cached on success, so a later run works after installing the CLI.
60
+ self._cli = cli
61
+ return self._cli
minimax_code/errors.py ADDED
@@ -0,0 +1,139 @@
1
+ """SDK errors. Mirrors ``packages/code-sdk/src/errors.ts``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ from .types import ExecResult, ThreadItem
9
+
10
+ #: Exit codes of ``mcode exec`` (``MINIMAX_CODE_EXEC_EXIT_CODES`` in the CLI).
11
+ MCODE_EXEC_EXIT_CODES: dict[str, int] = {
12
+ "success": 0,
13
+ "invocation": 2,
14
+ "config": 3,
15
+ "runtime": 4,
16
+ "timeout": 6,
17
+ "limit": 7,
18
+ "internal": 70,
19
+ "cancelled": 130,
20
+ "broken_pipe": 141,
21
+ }
22
+
23
+ MCODE_INSTALL_HINT = "npm install -g @minimax-ai/code@latest"
24
+
25
+
26
+ class MCodeError(Exception):
27
+ """Base class for every error raised by the SDK."""
28
+
29
+ def __init__(
30
+ self,
31
+ message: str,
32
+ *,
33
+ exit_code: int | None = None,
34
+ stderr: str = "",
35
+ session_id: str | None = None,
36
+ result: ExecResult | None = None,
37
+ items: Sequence[ThreadItem] = (),
38
+ ) -> None:
39
+ super().__init__(message)
40
+ self.exit_code = exit_code
41
+ #: Tail of the CLI stderr, for diagnostics only.
42
+ self.stderr = stderr
43
+ self.session_id = session_id
44
+ self.result = result
45
+ self.items = list(items)
46
+
47
+
48
+ class MCodeInvocationError(MCodeError):
49
+ """Invalid SDK options, or the CLI rejected the invocation (exit code 2)."""
50
+
51
+
52
+ class MCodeRunError(MCodeError):
53
+ """The Turn ended with ``failed``, ``timeout`` or ``limit_exceeded``."""
54
+
55
+ def __init__(self, message: str, *, status: str, category: str | None = None, **kw: Any):
56
+ super().__init__(message, **kw)
57
+ self.status = status
58
+ self.category = category
59
+
60
+
61
+ class MCodeAbortError(MCodeError):
62
+ """The Turn was cancelled."""
63
+
64
+
65
+ class MCodeProtocolError(MCodeError):
66
+ """The CLI produced output the SDK cannot understand."""
67
+
68
+
69
+ class MCodeNotFoundError(MCodeError):
70
+ """The ``mcode`` CLI could not be found."""
71
+
72
+
73
+ class MCodeVersionError(MCodeError):
74
+ """The CLI is too old or speaks an unsupported schema version."""
75
+
76
+
77
+ class ThreadBusyError(MCodeError):
78
+ """A Thread already has a Turn in progress."""
79
+
80
+ def __init__(self, thread_id: str | None) -> None:
81
+ super().__init__(f"Thread {thread_id or '(new)'} already has a Turn in progress.")
82
+
83
+
84
+ _STATUS_BY_EXIT_CODE: dict[int, tuple[str, str | None]] = {
85
+ 3: ("failed", "config"),
86
+ 4: ("failed", "runtime"),
87
+ 6: ("timeout", None),
88
+ 7: ("limit_exceeded", None),
89
+ 70: ("failed", "internal"),
90
+ }
91
+
92
+
93
+ def error_for_result(
94
+ result: ExecResult, items: Sequence[ThreadItem], *, exit_code: int | None, stderr: str
95
+ ) -> MCodeError:
96
+ common: dict[str, Any] = {
97
+ "exit_code": exit_code,
98
+ "stderr": stderr,
99
+ "session_id": result["sessionId"],
100
+ "result": result,
101
+ "items": items,
102
+ }
103
+ if result["status"] == "cancelled":
104
+ return MCodeAbortError("The MCode run was cancelled.", **common)
105
+ status = "failed" if result["status"] == "succeeded" else result["status"]
106
+ error = result.get("error")
107
+ reason = error["message"] if error else f"status {result['status']}"
108
+ category = error["category"] if error else None
109
+ return MCodeRunError(
110
+ f"The MCode run ended with {status}: {reason}", status=status, category=category, **common
111
+ )
112
+
113
+
114
+ def error_for_exit(
115
+ *, exit_code: int | None, stderr: str, items: Sequence[ThreadItem], session_id: str | None
116
+ ) -> MCodeError:
117
+ lines = [line.strip() for line in stderr.splitlines() if line.strip()]
118
+ reason = lines[-1] if lines else f"exit code {exit_code}"
119
+ common: dict[str, Any] = {
120
+ "exit_code": exit_code,
121
+ "stderr": stderr,
122
+ "items": items,
123
+ "session_id": session_id,
124
+ }
125
+ if exit_code == MCODE_EXEC_EXIT_CODES["invocation"]:
126
+ return MCodeInvocationError(f"mcode rejected the invocation: {reason}", **common)
127
+ if exit_code == MCODE_EXEC_EXIT_CODES["cancelled"] or (exit_code is not None and exit_code < 0):
128
+ return MCodeAbortError("The MCode run was cancelled.", **common)
129
+ if exit_code == MCODE_EXEC_EXIT_CODES["broken_pipe"]:
130
+ return MCodeProtocolError(f"mcode reported a broken output pipe: {reason}", **common)
131
+ if exit_code == 0:
132
+ return MCodeProtocolError("mcode exited without emitting exec.completed.", **common)
133
+ status, category = _STATUS_BY_EXIT_CODE.get(exit_code or -1, ("failed", "internal"))
134
+ return MCodeRunError(
135
+ f"The MCode run ended without a result: {reason}",
136
+ status=status,
137
+ category=category,
138
+ **common,
139
+ )
minimax_code/py.typed ADDED
File without changes
minimax_code/sync.py ADDED
@@ -0,0 +1,64 @@
1
+ """Blocking wrappers around the async API, for scripts and notebooks without an event loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Iterator
7
+ from typing import Any
8
+
9
+ from .client import MCode
10
+ from .thread import Thread
11
+ from .types import MCodeOptions, RunOptions, ThreadEvent, ThreadOptions, Turn
12
+
13
+
14
+ class SyncThread:
15
+ def __init__(self, loop: asyncio.AbstractEventLoop, thread: Thread) -> None:
16
+ self._loop = loop
17
+ self._thread = thread
18
+
19
+ @property
20
+ def id(self) -> str | None:
21
+ return self._thread.id
22
+
23
+ def run(self, input: str, options: RunOptions | None = None) -> Turn:
24
+ return self._loop.run_until_complete(self._thread.run(input, options))
25
+
26
+ def run_streamed(self, input: str, options: RunOptions | None = None) -> Iterator[ThreadEvent]:
27
+ """Yields events; closing the iterator early cancels the Turn."""
28
+ streamed = self._loop.run_until_complete(self._thread.run_streamed(input, options))
29
+ events = streamed.events
30
+ try:
31
+ while True:
32
+ try:
33
+ yield self._loop.run_until_complete(events.__anext__())
34
+ except StopAsyncIteration:
35
+ return
36
+ finally:
37
+ self._loop.run_until_complete(streamed.aclose())
38
+
39
+
40
+ class SyncMCode:
41
+ """Blocking client. Owns a private event loop; call ``close()`` (or use ``with``) when done."""
42
+
43
+ def __init__(self, options: MCodeOptions | None = None) -> None:
44
+ self._loop = asyncio.new_event_loop()
45
+ self._client = MCode(options)
46
+
47
+ def start_thread(self, options: ThreadOptions | None = None) -> SyncThread:
48
+ return SyncThread(self._loop, self._client.start_thread(options))
49
+
50
+ def resume_thread(self, thread_id: str, options: ThreadOptions | None = None) -> SyncThread:
51
+ return SyncThread(self._loop, self._client.resume_thread(thread_id, options))
52
+
53
+ def close(self) -> None:
54
+ if not self._loop.is_closed():
55
+ # Let subprocess transports finish closing before the loop goes away.
56
+ self._loop.run_until_complete(asyncio.sleep(0.05))
57
+ self._loop.run_until_complete(self._loop.shutdown_asyncgens())
58
+ self._loop.close()
59
+
60
+ def __enter__(self) -> SyncMCode:
61
+ return self
62
+
63
+ def __exit__(self, *exc: Any) -> None:
64
+ self.close()
minimax_code/thread.py ADDED
@@ -0,0 +1,331 @@
1
+ """Threads and Turns. Mirrors ``packages/code-sdk/src/thread.ts`` and ``exec-process.ts``.
2
+
3
+ The SDK holds only the Session id: every Turn starts a new ``mcode exec`` process and continues
4
+ the Session with ``--session``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import json
12
+ import os
13
+ import shutil
14
+ import signal
15
+ import sys
16
+ import tempfile
17
+ from collections.abc import AsyncIterator, Awaitable, Callable
18
+ from types import TracebackType
19
+ from typing import Any
20
+
21
+ from ._args import build_exec_args, validate_run_options
22
+ from ._cli import ResolvedCli
23
+ from ._items import ItemAccumulator
24
+ from ._protocol import parse_event_line
25
+ from .errors import (
26
+ MCodeError,
27
+ MCodeNotFoundError,
28
+ MCodeProtocolError,
29
+ ThreadBusyError,
30
+ error_for_exit,
31
+ error_for_result,
32
+ )
33
+ from .types import ExecResult, RunOptions, ThreadEvent, ThreadOptions, Turn
34
+
35
+ STDERR_TAIL_BYTES = 64 * 1024
36
+ CANCEL_GRACE_S = 10.0
37
+ BACKSTOP_EXTRA_S = 30.0
38
+ _STDOUT_LINE_LIMIT = 64 * 1024 * 1024
39
+
40
+
41
+ class ThreadHost:
42
+ """What a Thread needs from its owning client."""
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ config_path: str | None,
48
+ child_env: Callable[[], dict[str, str]],
49
+ prepare_cli: Callable[[], Awaitable[ResolvedCli]],
50
+ ) -> None:
51
+ self.config_path = config_path
52
+ self.child_env = child_env
53
+ self.prepare_cli = prepare_cli
54
+
55
+
56
+ class StreamedTurn:
57
+ """Events of one running Turn. Iterate with ``async for``; leaving the loop early (``break``,
58
+ an exception, or task cancellation) cancels the Turn and waits for the CLI to exit."""
59
+
60
+ def __init__(
61
+ self,
62
+ events: AsyncIterator[ThreadEvent],
63
+ cancel: Callable[[], None],
64
+ finalize: Callable[[], Awaitable[None]],
65
+ ) -> None:
66
+ self.events = events
67
+ self._cancel = cancel
68
+ self._finalize = finalize
69
+
70
+ def __aiter__(self) -> AsyncIterator[ThreadEvent]:
71
+ return self.events
72
+
73
+ def cancel(self) -> None:
74
+ """Asks the CLI to stop (SIGINT). The stream then ends with the CLI's final events."""
75
+ self._cancel()
76
+
77
+ async def aclose(self) -> None:
78
+ """Stops the Turn if it is still running and releases the Thread."""
79
+ aclose = getattr(self.events, "aclose", None)
80
+ if aclose is not None:
81
+ await aclose()
82
+ # Covers a stream that was never iterated, whose generator body never ran.
83
+ await self._finalize()
84
+
85
+ async def __aenter__(self) -> StreamedTurn:
86
+ return self
87
+
88
+ async def __aexit__(
89
+ self,
90
+ exc_type: type[BaseException] | None,
91
+ exc: BaseException | None,
92
+ tb: TracebackType | None,
93
+ ) -> None:
94
+ await self.aclose()
95
+
96
+
97
+ class _Exec:
98
+ """One ``mcode exec`` child process."""
99
+
100
+ def __init__(self, proc: asyncio.subprocess.Process) -> None:
101
+ self.proc = proc
102
+ self.stderr_tail = ""
103
+ self._stderr_task = asyncio.ensure_future(self._drain_stderr())
104
+ self._interrupted = False
105
+
106
+ async def _drain_stderr(self) -> None:
107
+ assert self.proc.stderr is not None
108
+ while chunk := await self.proc.stderr.read(65536):
109
+ text = self.stderr_tail + chunk.decode("utf-8", "replace")
110
+ self.stderr_tail = text[-STDERR_TAIL_BYTES:]
111
+
112
+ def interrupt(self) -> None:
113
+ if self.proc.returncode is not None or self._interrupted:
114
+ return
115
+ self._interrupted = True
116
+ with contextlib.suppress(ProcessLookupError):
117
+ if sys.platform == "win32":
118
+ self.proc.terminate()
119
+ else:
120
+ self.proc.send_signal(signal.SIGINT)
121
+
122
+ async def shutdown(self, grace: float = CANCEL_GRACE_S) -> None:
123
+ """Interrupts a live CLI, then kills it after ``grace`` seconds."""
124
+ if self.proc.returncode is None:
125
+ self.interrupt()
126
+ try:
127
+ await asyncio.wait_for(asyncio.shield(self.proc.wait()), timeout=grace)
128
+ except asyncio.TimeoutError:
129
+ with contextlib.suppress(ProcessLookupError):
130
+ self.proc.kill()
131
+ await self.proc.wait()
132
+ with contextlib.suppress(asyncio.TimeoutError):
133
+ await asyncio.wait_for(asyncio.shield(self._stderr_task), timeout=2)
134
+ # Close the pipes now, while the loop is alive; asyncio has no public API for this.
135
+ transport = getattr(self.proc, "_transport", None)
136
+ if transport is not None:
137
+ transport.close()
138
+
139
+
140
+ class Thread:
141
+ """A conversation with the Agent."""
142
+
143
+ def __init__(self, host: ThreadHost, options: ThreadOptions, thread_id: str | None) -> None:
144
+ self._host = host
145
+ self._options = options
146
+ self._session_id = thread_id
147
+ self._busy = False
148
+
149
+ @property
150
+ def id(self) -> str | None:
151
+ """Session id. Available after the first event of the first Turn, even if it fails."""
152
+ return self._session_id
153
+
154
+ async def run(self, input: str, options: RunOptions | None = None) -> Turn:
155
+ """Runs one Turn. Returns only when it succeeds; otherwise raises an ``MCodeError``."""
156
+ state: dict[str, Any] = {}
157
+ result: ExecResult | None = None
158
+ streamed = await self._start(input, options or RunOptions(), state)
159
+ async with streamed:
160
+ async for event in streamed:
161
+ if event["type"] == "exec.completed" and "result" in event:
162
+ result = event["result"]
163
+ if result is None:
164
+ raise MCodeProtocolError("mcode finished without exec.completed.")
165
+ acc: ItemAccumulator = state["items"]
166
+ items = acc.items()
167
+ if result["status"] != "succeeded":
168
+ raise error_for_result(
169
+ result, items, exit_code=state.get("exit_code"), stderr=state.get("stderr", "")
170
+ )
171
+ output = result.get("output")
172
+ final = output if isinstance(output, str) else "" if output is None else json.dumps(output)
173
+ return Turn(
174
+ final_response=final,
175
+ items=items,
176
+ session_id=result["sessionId"],
177
+ turn_id=result["turnId"],
178
+ duration_ms=result["durationMs"],
179
+ result=result,
180
+ usage=result.get("usage"),
181
+ model=result.get("model"),
182
+ )
183
+
184
+ async def run_streamed(self, input: str, options: RunOptions | None = None) -> StreamedTurn:
185
+ """Runs one Turn and yields CLI events as they arrive. ``turn.failed`` is an ordinary
186
+ event; iteration raises only for process or protocol failures."""
187
+ return await self._start(input, options or RunOptions(), {})
188
+
189
+ async def _start(self, input: str, options: RunOptions, state: dict[str, Any]) -> StreamedTurn:
190
+ if self._busy:
191
+ raise ThreadBusyError(self._session_id)
192
+ if not isinstance(input, str):
193
+ raise TypeError("input must be a string.")
194
+ validate_run_options(options)
195
+ self._busy = True
196
+ temp_dir: str | None = None
197
+ try:
198
+ cli = await self._host.prepare_cli()
199
+ prompt_files: dict[str, str] = {}
200
+ if self._options.system_prompt is not None or (
201
+ self._options.append_system_prompt is not None
202
+ ):
203
+ temp_dir = tempfile.mkdtemp(prefix="mcode-sdk-")
204
+ for key, text in (
205
+ ("system_prompt_file", self._options.system_prompt),
206
+ ("append_system_prompt_file", self._options.append_system_prompt),
207
+ ):
208
+ if text is not None:
209
+ path = os.path.join(temp_dir, key.replace("_file", "") + ".txt")
210
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
211
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
212
+ fh.write(text)
213
+ prompt_files[key] = path
214
+ args = build_exec_args(
215
+ thread=self._options,
216
+ session_id=self._session_id,
217
+ run=options,
218
+ config_path=self._host.config_path,
219
+ **prompt_files,
220
+ )
221
+ try:
222
+ proc = await asyncio.create_subprocess_exec(
223
+ *cli.command(args),
224
+ stdin=asyncio.subprocess.PIPE,
225
+ stdout=asyncio.subprocess.PIPE,
226
+ stderr=asyncio.subprocess.PIPE,
227
+ env=self._host.child_env(),
228
+ limit=_STDOUT_LINE_LIMIT,
229
+ )
230
+ except OSError as error:
231
+ raise MCodeNotFoundError(f"Could not start mcode at {cli.path}: {error}") from error
232
+ except BaseException:
233
+ self._release(temp_dir)
234
+ raise
235
+ child = _Exec(proc)
236
+ released = False
237
+
238
+ def release() -> None:
239
+ nonlocal released
240
+ if not released:
241
+ released = True
242
+ self._release(temp_dir)
243
+
244
+ events = self._events(child, input, options, state, release)
245
+
246
+ async def finalize() -> None:
247
+ await _quiet_shutdown(child)
248
+ release()
249
+
250
+ return StreamedTurn(events, child.interrupt, finalize)
251
+
252
+ async def _events(
253
+ self,
254
+ child: _Exec,
255
+ prompt: str,
256
+ options: RunOptions,
257
+ state: dict[str, Any],
258
+ release: Callable[[], None],
259
+ ) -> AsyncIterator[ThreadEvent]:
260
+ items = ItemAccumulator()
261
+ state["items"] = items
262
+ completed = False
263
+ backstop: asyncio.TimerHandle | None = None
264
+ if options.timeout_ms is not None:
265
+ delay = options.timeout_ms / 1000 + BACKSTOP_EXTRA_S
266
+ backstop = asyncio.get_running_loop().call_later(delay, _kill, child.proc)
267
+ try:
268
+ assert child.proc.stdin is not None and child.proc.stdout is not None
269
+ with contextlib.suppress(BrokenPipeError, ConnectionResetError):
270
+ child.proc.stdin.write(json.dumps({"prompt": prompt}).encode("utf-8"))
271
+ await child.proc.stdin.drain()
272
+ child.proc.stdin.close()
273
+ line_no = 0
274
+ while True:
275
+ try:
276
+ raw = await child.proc.stdout.readline()
277
+ except ValueError as error:
278
+ raise MCodeProtocolError("mcode stdout line exceeds the size limit.") from error
279
+ if not raw:
280
+ break
281
+ line_no += 1
282
+ try:
283
+ event = parse_event_line(raw.decode("utf-8", "replace"))
284
+ except MCodeError as error:
285
+ error.stderr = child.stderr_tail
286
+ raise
287
+ if event is None:
288
+ continue
289
+ if self._session_id is None:
290
+ self._session_id = event["sessionId"]
291
+ if event["type"] == "exec.completed":
292
+ completed = True
293
+ if items.apply(event):
294
+ yield event
295
+ await child.proc.wait()
296
+ await child.shutdown()
297
+ state["exit_code"] = child.proc.returncode
298
+ state["stderr"] = child.stderr_tail
299
+ if not completed:
300
+ raise error_for_exit(
301
+ exit_code=child.proc.returncode,
302
+ stderr=child.stderr_tail,
303
+ items=items.items(),
304
+ session_id=self._session_id,
305
+ )
306
+ except BaseException:
307
+ await _quiet_shutdown(child)
308
+ raise
309
+ finally:
310
+ if backstop is not None:
311
+ backstop.cancel()
312
+ release()
313
+
314
+ def _release(self, temp_dir: str | None) -> None:
315
+ self._busy = False
316
+ if temp_dir:
317
+ shutil.rmtree(temp_dir, ignore_errors=True)
318
+
319
+
320
+ def _kill(proc: asyncio.subprocess.Process) -> None:
321
+ if proc.returncode is None:
322
+ with contextlib.suppress(ProcessLookupError):
323
+ proc.kill()
324
+
325
+
326
+ async def _quiet_shutdown(child: _Exec) -> None:
327
+ with contextlib.suppress(Exception):
328
+ await asyncio.shield(child.shutdown())
329
+
330
+
331
+ __all__ = ["StreamedTurn", "Thread", "ThreadHost"]
minimax_code/types.py ADDED
@@ -0,0 +1,184 @@
1
+ """Public types for the ``mcode exec --output-format stream-json`` contract (schemaVersion 1).
2
+
3
+ Mirrors ``packages/code-sdk/src/types.ts``. Events are passed through as plain dicts typed with
4
+ ``TypedDict`` so new fields or event types added by newer CLIs never break parsing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Literal, TypedDict
11
+
12
+ from typing_extensions import NotRequired
13
+
14
+ ExecResultStatus = Literal["succeeded", "failed", "timeout", "cancelled", "limit_exceeded"]
15
+ ExecErrorCategory = Literal["config", "runtime", "internal"]
16
+ UsageSource = Literal["completed_responses", "analytics_fallback", "unavailable"]
17
+ ExecItemType = Literal["agent_message", "reasoning", "tool_call"]
18
+ PermissionPolicy = Literal["smart", "full", "off"]
19
+ ContextMode = Literal["standard", "lightweight"]
20
+ PromptMode = Literal["tui", "coding", "work"]
21
+
22
+
23
+ class ExecError(TypedDict):
24
+ category: ExecErrorCategory
25
+ message: str
26
+ code: NotRequired[str]
27
+ retryable: NotRequired[bool]
28
+
29
+
30
+ class ModelIdentity(TypedDict):
31
+ providerId: str
32
+ modelId: str
33
+ variant: NotRequired[str]
34
+ providerSource: NotRequired[str]
35
+ providerKind: NotRequired[str]
36
+ protocol: NotRequired[str]
37
+
38
+
39
+ class Usage(TypedDict, total=False):
40
+ """Token usage. ``totalTokens = inputTokens + outputTokens``; cache buckets are separate."""
41
+
42
+ totalTokens: int
43
+ inputTokens: int
44
+ outputTokens: int
45
+ reasoningTokens: int
46
+ cacheReadTokens: int
47
+ cacheWriteTokens: int
48
+
49
+
50
+ class ExecResult(TypedDict):
51
+ """Final result of one ``mcode exec`` run (``exec.completed.result``)."""
52
+
53
+ schemaVersion: Literal[1]
54
+ type: Literal["exec.result"]
55
+ runId: str
56
+ sessionId: str
57
+ turnId: str
58
+ status: ExecResultStatus
59
+ durationMs: int
60
+ output: NotRequired[Any]
61
+ error: NotRequired[ExecError]
62
+ model: NotRequired[ModelIdentity]
63
+ usage: NotRequired[Usage]
64
+ usageSource: NotRequired[UsageSource]
65
+ usageIncomplete: NotRequired[bool]
66
+
67
+
68
+ RawToolCall = dict[str, Any]
69
+ """Experimental raw tool call snapshot (``id``, ``name``, ``status``, ``input``, ``output``...)."""
70
+
71
+
72
+ class ExecItem(TypedDict):
73
+ id: str
74
+ type: ExecItemType
75
+ content: NotRequired[str]
76
+ contentDelta: NotRequired[str]
77
+ toolCall: NotRequired[RawToolCall]
78
+
79
+
80
+ class ThreadEvent(TypedDict):
81
+ """One CLI stream event. Check ``type`` and read the fields specific to it.
82
+
83
+ Known types: ``exec.started``, ``session.started``, ``session.resumed``, ``turn.started``,
84
+ ``item.started``, ``item.updated``, ``item.completed`` (with ``item``), ``turn.completed``
85
+ (``model``/``usage``/``durationMs``), ``turn.failed`` (``status``/``error``/``durationMs``),
86
+ ``exec.completed`` (``result``). Unknown types are passed through; keep a default branch.
87
+ """
88
+
89
+ schemaVersion: Literal[1]
90
+ type: str
91
+ sequence: int
92
+ timestampMs: int
93
+ runId: str
94
+ sessionId: str
95
+ turnId: str
96
+ item: NotRequired[ExecItem]
97
+ result: NotRequired[ExecResult]
98
+ model: NotRequired[ModelIdentity]
99
+ usage: NotRequired[Usage]
100
+ usageSource: NotRequired[UsageSource]
101
+ usageIncomplete: NotRequired[bool]
102
+ status: NotRequired[str]
103
+ error: NotRequired[ExecError]
104
+ durationMs: NotRequired[int]
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class AgentMessageItem:
109
+ id: str
110
+ text: str
111
+ incomplete: bool = False
112
+ type: Literal["agent_message"] = "agent_message"
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class ReasoningItem:
117
+ id: str
118
+ text: str
119
+ incomplete: bool = False
120
+ type: Literal["reasoning"] = "reasoning"
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class ToolCallItem:
125
+ id: str
126
+ tool_call: RawToolCall
127
+ incomplete: bool = False
128
+ type: Literal["tool_call"] = "tool_call"
129
+
130
+
131
+ ThreadItem = AgentMessageItem | ReasoningItem | ToolCallItem
132
+
133
+
134
+ @dataclass(frozen=True)
135
+ class Turn:
136
+ """Result of a successful ``thread.run()``."""
137
+
138
+ final_response: str
139
+ items: list[ThreadItem]
140
+ session_id: str
141
+ turn_id: str
142
+ duration_ms: int
143
+ result: ExecResult
144
+ usage: Usage | None = None
145
+ model: ModelIdentity | None = None
146
+ status: Literal["succeeded"] = "succeeded"
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class MCodeOptions:
151
+ """Client options.
152
+
153
+ ``bin``: explicit ``mcode`` executable (or JS entry). Otherwise ``$MCODE_BIN``, then ``PATH``.
154
+ ``data_dir``: passed to the CLI as ``MINIMAX_DATA_DIR``.
155
+ ``env``: child environment; replaces ``os.environ`` when provided.
156
+ ``config_path``: runtime config file, passed as ``--config``.
157
+ """
158
+
159
+ bin: str | None = None
160
+ data_dir: str | None = None
161
+ env: dict[str, str] | None = None
162
+ config_path: str | None = None
163
+
164
+
165
+ @dataclass(frozen=True)
166
+ class ThreadOptions:
167
+ cwd: str | None = None
168
+ model: str | None = None
169
+ """Model as ``provider/model``, for example ``minimax/MiniMax-M3``."""
170
+ effort: str | None = None
171
+ permission: PermissionPolicy | None = None
172
+ mode: ContextMode | None = None
173
+ """Context mode for a new Thread. ``lightweight`` cannot be resumed."""
174
+ prompt_mode: PromptMode | None = None
175
+ system_prompt: str | None = None
176
+ append_system_prompt: str | None = None
177
+
178
+
179
+ @dataclass(frozen=True)
180
+ class RunOptions:
181
+ files: list[str] = field(default_factory=list)
182
+ """Files attached to this Turn (``--file``). Relative paths resolve against ``cwd``."""
183
+ timeout_ms: int | None = None
184
+ max_steps: int | None = None
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.5
2
+ Name: minimax-code-sdk
3
+ Version: 0.1.0b1
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: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: typing-extensions>=4.7
17
+ Description-Content-Type: text/markdown
18
+
19
+ # minimax-code-sdk
20
+
21
+ Python SDK for driving the local [MiniMax Code](https://agent.minimax.io) agent (`mcode`).
22
+
23
+ It mirrors the TypeScript SDK
24
+ [`@minimax-ai/code-sdk`](https://github.com/MiniMax-AI/minimax-code/tree/main/packages/code-sdk): every Turn runs
25
+ `mcode exec --output-format=stream-json` and continues the Session with `--session`.
26
+
27
+ ## Requirements
28
+
29
+ - Python 3.10+
30
+ - The `mcode` CLI (0.6.3 or newer): `npm install -g @minimax-ai/code`
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install --pre minimax-code-sdk # or: uv add --prerelease=allow minimax-code-sdk
36
+ ```
37
+
38
+ The SDK is in beta, so installers need `--pre` (or an exact version such as
39
+ `minimax-code-sdk==0.1.0b1`) until `0.1.0` is released. Import it as `minimax_code`.
40
+
41
+ The CLI is found from `MCodeOptions(bin=...)`, then the `MCODE_BIN` environment variable,
42
+ then `PATH`.
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ import asyncio
48
+ from minimax_code import MCode, ThreadOptions
49
+
50
+
51
+ async def main() -> None:
52
+ client = MCode()
53
+ thread = client.start_thread(ThreadOptions(cwd=".", permission="smart"))
54
+
55
+ turn = await thread.run("Summarize this repository in one sentence.")
56
+ print(turn.final_response, turn.usage)
57
+
58
+ # Same Session, next Turn.
59
+ turn = await thread.run("Now list its top-level directories.")
60
+
61
+ # Persist thread.id and continue later.
62
+ later = client.resume_thread(thread.id)
63
+ print((await later.run("What did I ask first?")).final_response)
64
+
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ ### Streaming
70
+
71
+ ```python
72
+ streamed = await thread.run_streamed("Fix the failing test.")
73
+ async for event in streamed:
74
+ if event["type"] == "item.updated" and event["item"]["type"] == "agent_message":
75
+ print(event["item"].get("contentDelta", ""), end="")
76
+ elif event["type"] == "turn.failed":
77
+ print("failed:", event.get("error"))
78
+ ```
79
+
80
+ Events are the CLI's stream-json events, passed through unchanged (unknown types included).
81
+ `turn.failed` is an ordinary event; iteration raises only for process or protocol failures.
82
+
83
+ ### Cancellation
84
+
85
+ - `streamed.cancel()` sends SIGINT; the stream ends with the CLI's final events.
86
+ - Leaving the `async for` early, or cancelling the task running `thread.run()`, stops the CLI
87
+ (SIGINT, then SIGKILL after a grace period).
88
+ - `RunOptions(timeout_ms=...)` is enforced by the CLI.
89
+
90
+ ### Blocking API
91
+
92
+ ```python
93
+ from minimax_code import SyncMCode
94
+
95
+ with SyncMCode() as client:
96
+ thread = client.start_thread()
97
+ print(thread.run("hello").final_response)
98
+ for event in thread.run_streamed("again"):
99
+ print(event["type"])
100
+ ```
101
+
102
+ ## Options
103
+
104
+ | Object | Fields |
105
+ | --- | --- |
106
+ | `MCodeOptions` | `bin`, `data_dir` (`MINIMAX_DATA_DIR`), `env` (replaces `os.environ`), `config_path` |
107
+ | `ThreadOptions` | `cwd`, `model` (`provider/model`), `effort`, `permission` (`smart`/`full`/`off`), `mode`, `prompt_mode`, `system_prompt`, `append_system_prompt` |
108
+ | `RunOptions` | `files`, `timeout_ms`, `max_steps` |
109
+
110
+ ## Errors
111
+
112
+ All errors subclass `MCodeError` (with `exit_code`, `stderr`, `session_id`, `result`, `items`):
113
+ `MCodeNotFoundError`, `MCodeVersionError`, `MCodeInvocationError`, `MCodeRunError`
114
+ (`status`, `category`), `MCodeAbortError`, `MCodeProtocolError`, `ThreadBusyError`.
115
+
116
+ ## Limitations
117
+
118
+ Each Turn is a separate process, so interactive permission prompts (`permission="ask"`) and
119
+ custom in-process tools are not supported yet. A long-lived ACP transport is planned.
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ uv sync
125
+ uv run pytest
126
+ uv run ruff check . && uv run ruff format --check . && uv run mypy
127
+ ```
128
+
129
+ End-to-end tests against a real CLI and model are opt-in:
130
+ `MCODE_SDK_E2E=1 MCODE_BIN=/path/to/mcode uv run pytest -m e2e`.
131
+
132
+ Release notes are in
133
+ [CHANGELOG.md](https://github.com/MiniMax-AI/minimax-code/blob/main/packages/code-sdk-python/CHANGELOG.md);
134
+ maintainers publish with
135
+ [RELEASING.md](https://github.com/MiniMax-AI/minimax-code/blob/main/packages/code-sdk-python/RELEASING.md).
136
+
137
+ ## License
138
+
139
+ MIT
@@ -0,0 +1,15 @@
1
+ minimax_code/__init__.py,sha256=Ur2NagxwfLigJUBOPRlJfsyjmxrlanCFtYJFkUJ9H_Q,1275
2
+ minimax_code/_args.py,sha256=23WFCz0y3SRqisEChPPsCWvAtl_U3Jzc1kGCcTQh1SE,3251
3
+ minimax_code/_cli.py,sha256=nfqda8v_3EHQ2hov3zfBRCE9JHtmV0uTjz04Xxeik9o,3143
4
+ minimax_code/_items.py,sha256=rFtcUV2ZstKYiY1x6R0x5dpqflZZDqngMBRdr7UHLgE,2423
5
+ minimax_code/_protocol.py,sha256=rhHZdoTTPDI3QFme5Jg1PoWa15GhI8pvC6QANj7DFP8,2138
6
+ minimax_code/client.py,sha256=2O8Tnah8xBqNe1agi1Xt_3t3CcoIDpH1Kif-pjwUOkQ,2539
7
+ minimax_code/errors.py,sha256=V8M3Y-WL_3z6mT-EEt3CsQ9uE1KehVI0hUGtw89NFO0,4484
8
+ minimax_code/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ minimax_code/sync.py,sha256=xupP3Wqy8TNIsRbZVKl-11nzc6TpEzVz6ry68D7_8K0,2366
10
+ minimax_code/thread.py,sha256=53TbJdbyRmjzUnOvoyZ2G6FTw2dOUuJEPzrIOO_Aq5s,12162
11
+ minimax_code/types.py,sha256=LXjJGyT-_W_HC2St34-vPY8d2CRZV9FBOtPj1_fB4PI,5363
12
+ minimax_code_sdk-0.1.0b1.dist-info/METADATA,sha256=CtVpGo46KC5tfgzF_c2yNc8-7ppgpW036k5OPZtDMwg,4584
13
+ minimax_code_sdk-0.1.0b1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
14
+ minimax_code_sdk-0.1.0b1.dist-info/licenses/LICENSE,sha256=KLtcKUh0L5-NJ-2EiChE0J3_B8td9k8gcph16FgqGOo,1069
15
+ minimax_code_sdk-0.1.0b1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.