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.
- minimax_code/__init__.py +66 -0
- minimax_code/_args.py +90 -0
- minimax_code/_cli.py +87 -0
- minimax_code/_items.py +71 -0
- minimax_code/_protocol.py +56 -0
- minimax_code/client.py +61 -0
- minimax_code/errors.py +139 -0
- minimax_code/py.typed +0 -0
- minimax_code/sync.py +64 -0
- minimax_code/thread.py +331 -0
- minimax_code/types.py +184 -0
- minimax_code_sdk-0.1.0b1.dist-info/METADATA +139 -0
- minimax_code_sdk-0.1.0b1.dist-info/RECORD +15 -0
- minimax_code_sdk-0.1.0b1.dist-info/WHEEL +4 -0
- minimax_code_sdk-0.1.0b1.dist-info/licenses/LICENSE +21 -0
minimax_code/__init__.py
ADDED
|
@@ -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,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.
|