mindcode 0.3.2__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mindcode-0.3.2 → mindcode-0.5.2}/PKG-INFO +8 -12
- {mindcode-0.3.2 → mindcode-0.5.2}/README.md +5 -10
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/_version.py +1 -1
- mindcode-0.5.2/mindcode/acp_agent/__init__.py +5 -0
- mindcode-0.5.2/mindcode/acp_agent/__main__.py +70 -0
- mindcode-0.5.2/mindcode/acp_agent/agent.py +469 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/approval.py +4 -4
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/runner.py +6 -4
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/_shared.py +4 -6
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/commands/bench.py +17 -8
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/commands/chat.py +13 -7
- mindcode-0.5.2/mindcode/cli/commands/config_cmd.py +127 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/commands/shell.py +19 -13
- mindcode-0.5.2/mindcode/cli/commands/status.py +52 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/commands/task.py +26 -12
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/shell/completion.py +3 -1
- mindcode-0.5.2/mindcode/cli/shell/input_pipeline.py +124 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/shell/repl.py +451 -157
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/shell/slash.py +66 -38
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/shell/startup.py +1 -1
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/shell/tui.py +156 -23
- mindcode-0.5.2/mindcode/config.py +482 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/policy.py +18 -25
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/render.py +67 -14
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/runtime.py +19 -19
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode.egg-info/PKG-INFO +8 -12
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode.egg-info/SOURCES.txt +6 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode.egg-info/requires.txt +2 -1
- {mindcode-0.3.2 → mindcode-0.5.2}/pyproject.toml +3 -2
- mindcode-0.5.2/tests/test_acp_agent.py +283 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_approval.py +2 -1
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_cli_commands.py +381 -60
- mindcode-0.5.2/tests/test_config.py +311 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_external_file_approval.py +6 -2
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_policy_choice.py +4 -5
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_policy_timeout.py +5 -2
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_provider_retry_defaults.py +4 -1
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_reasoner_compat.py +29 -8
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_render_exec.py +62 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_shell_channels.py +492 -103
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_shell_completion.py +10 -13
- mindcode-0.5.2/tests/test_shell_interrupt.py +860 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_shell_session.py +6 -6
- mindcode-0.5.2/tests/test_shell_slash_priority.py +174 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_shell_tui.py +463 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_skills_mcp.py +11 -5
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_slash_resume.py +29 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_subagents.py +5 -3
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_task_resume.py +11 -11
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_tasking.py +2 -2
- mindcode-0.3.2/mindcode/cli/commands/config_cmd.py +0 -197
- mindcode-0.3.2/mindcode/cli/commands/status.py +0 -37
- mindcode-0.3.2/mindcode/config.py +0 -173
- mindcode-0.3.2/tests/test_config.py +0 -117
- mindcode-0.3.2/tests/test_shell_interrupt.py +0 -167
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/__init__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/__main__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/__init__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/adapters/__init__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/adapters/base.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/adapters/mini_bfcl.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/adapters/mini_gaia.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/adapters/mini_terminal.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/adapters/swe_bench.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/adapters/terminal_bench.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/fake_tools.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/jsonutil.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/paths.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/predictions.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/report.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/schema.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/scorers/base.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/scorers/composite.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/scorers/exact.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/scorers/json_call.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/scorers/swe.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/scorers/terminal.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/scorers/terminal_bench.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/trace.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/bench/workspace.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/__init__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/commands/__init__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/commands/remote.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/commands/terminal.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/shell/__init__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/cli/shell/menu.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/mcp.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/remote.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/skills.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/subagents.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/tasking.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/terminal/__init__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/terminal/__main__.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode/terminal_core.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode.egg-info/dependency_links.txt +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode.egg-info/entry_points.txt +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/mindcode.egg-info/top_level.txt +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/setup.cfg +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_bench.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_diff_capture.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_exec_command_guard.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_remote.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_system_prompt.py +0 -0
- {mindcode-0.3.2 → mindcode-0.5.2}/tests/test_terminal.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mindcode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: codex-style interactive coding agent on top of mindagent
|
|
5
5
|
Author: mindcode
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
|
-
Requires-Dist: mindagent>=0.5.
|
|
9
|
+
Requires-Dist: mindagent>=0.5.3
|
|
10
|
+
Requires-Dist: agent-client-protocol>=0.12
|
|
10
11
|
Requires-Dist: rich>=13
|
|
11
12
|
Requires-Dist: openai>=1.0
|
|
12
13
|
Requires-Dist: python-dotenv>=1.0
|
|
@@ -55,22 +56,17 @@ npm test # vitest 单测
|
|
|
55
56
|
|
|
56
57
|
## 安装
|
|
57
58
|
|
|
58
|
-
要求 Python >= 3.10
|
|
59
|
+
要求 Python >= 3.10。在 mindcode 目录执行安装:
|
|
59
60
|
|
|
60
61
|
```bash
|
|
61
|
-
cd /path/to/mindagent
|
|
62
|
-
pip install -e ".[openai]"
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
然后安装 mindcode:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
cd app/mindcode
|
|
62
|
+
cd /path/to/mindagent/app/mindcode
|
|
69
63
|
pip install -e .
|
|
70
64
|
|
|
71
|
-
mindcode --version # 0.
|
|
65
|
+
mindcode --version # 0.5.0
|
|
72
66
|
```
|
|
73
67
|
|
|
68
|
+
`pip install -e .` 会根据项目依赖自动从 PyPI 安装 `mindagent>=0.5.3`。
|
|
69
|
+
|
|
74
70
|
## 文档
|
|
75
71
|
|
|
76
72
|
- `docs/command.md` — TUI slash 命令规范(状态合同:Implemented / Removed / Proposed / Blocked)
|
|
@@ -40,22 +40,17 @@ npm test # vitest 单测
|
|
|
40
40
|
|
|
41
41
|
## 安装
|
|
42
42
|
|
|
43
|
-
要求 Python >= 3.10
|
|
43
|
+
要求 Python >= 3.10。在 mindcode 目录执行安装:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
cd /path/to/mindagent
|
|
47
|
-
pip install -e ".[openai]"
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
然后安装 mindcode:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
cd app/mindcode
|
|
46
|
+
cd /path/to/mindagent/app/mindcode
|
|
54
47
|
pip install -e .
|
|
55
48
|
|
|
56
|
-
mindcode --version # 0.
|
|
49
|
+
mindcode --version # 0.5.0
|
|
57
50
|
```
|
|
58
51
|
|
|
52
|
+
`pip install -e .` 会根据项目依赖自动从 PyPI 安装 `mindagent>=0.5.3`。
|
|
53
|
+
|
|
59
54
|
## 文档
|
|
60
55
|
|
|
61
56
|
- `docs/command.md` — TUI slash 命令规范(状态合同:Implemented / Removed / Proposed / Blocked)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""ACP agent entry point: ``python -m mindcode.acp_agent``.
|
|
2
|
+
|
|
3
|
+
stdio transport only. stdout carries ACP JSON-RPC frames exclusively; all
|
|
4
|
+
logging is routed to stderr (see docs/acp-layering.md §5 rule 1).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from acp import run_agent
|
|
16
|
+
|
|
17
|
+
from .agent import _AgentHarness
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _setup_logging(verbose: bool) -> None:
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
stream=sys.stderr,
|
|
23
|
+
level=logging.DEBUG if verbose else logging.INFO,
|
|
24
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
25
|
+
)
|
|
26
|
+
# 第三方库的 INFO 噪音也压到 stderr,绝不污染 stdout
|
|
27
|
+
logging.getLogger().setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main(argv: list[str] | None = None) -> int:
|
|
31
|
+
parser = argparse.ArgumentParser(
|
|
32
|
+
prog="python -m mindcode.acp_agent",
|
|
33
|
+
description="Run the mindcode ACP agent over stdio (ACP v1).",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--cwd",
|
|
37
|
+
default=None,
|
|
38
|
+
help="Default workspace used when a client connects without a cwd",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument("--verbose", action="store_true", help="stderr debug logs")
|
|
41
|
+
args = parser.parse_args(argv)
|
|
42
|
+
|
|
43
|
+
_setup_logging(args.verbose)
|
|
44
|
+
|
|
45
|
+
async def _serve() -> int:
|
|
46
|
+
harness_holder: list[_AgentHarness] = []
|
|
47
|
+
|
|
48
|
+
def factory(conn: object) -> _AgentHarness:
|
|
49
|
+
harness = _AgentHarness(
|
|
50
|
+
conn, # type: ignore[arg-type]
|
|
51
|
+
cwd=Path(args.cwd).expanduser() if args.cwd else None,
|
|
52
|
+
)
|
|
53
|
+
harness_holder.append(harness)
|
|
54
|
+
return harness
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
await run_agent(factory)
|
|
58
|
+
finally:
|
|
59
|
+
if harness_holder:
|
|
60
|
+
await harness_holder[0].close()
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
return asyncio.run(_serve())
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
return 130
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""ACP agent adapter — exposes mindcode's native session API over ACP v1.
|
|
2
|
+
|
|
3
|
+
Layering (see docs/acp-layering.md):
|
|
4
|
+
|
|
5
|
+
engine (mindagent-core) → native session API (this file's only bus)
|
|
6
|
+
→ ACP stdio JSON-RPC (acp SDK, ``python -m mindcode.acp_agent``)
|
|
7
|
+
→ TUI Shell (untouched, peer frontend)
|
|
8
|
+
|
|
9
|
+
Only the five native ports are used: create session / run turn / cancel /
|
|
10
|
+
approval callback / event stream. Hard rules honoured here:
|
|
11
|
+
|
|
12
|
+
- stdout carries protocol frames only; every log line goes to stderr.
|
|
13
|
+
- one run per session at a time; a second ``session/prompt`` is rejected.
|
|
14
|
+
- provider/model/approval configuration always comes from ``ConfigStore``
|
|
15
|
+
(fresh ``read()`` per session); clients cannot widen the SessionPolicy.
|
|
16
|
+
- unimplemented stable methods (``session/load``, ``session/set_mode``)
|
|
17
|
+
answer with a JSON-RPC error instead of pretending support.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import logging
|
|
24
|
+
import time
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from acp import AgentSideConnection, helpers, RequestError
|
|
30
|
+
from acp.schema import (
|
|
31
|
+
AgentCapabilities,
|
|
32
|
+
InitializeResponse,
|
|
33
|
+
NewSessionResponse,
|
|
34
|
+
PermissionOption,
|
|
35
|
+
PromptCapabilities,
|
|
36
|
+
PromptResponse,
|
|
37
|
+
RequestPermissionResponse,
|
|
38
|
+
TextContentBlock,
|
|
39
|
+
ToolCallStart,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from mindagent.core import (
|
|
43
|
+
AgentSession,
|
|
44
|
+
AgentRuntime,
|
|
45
|
+
EventType,
|
|
46
|
+
LocalConversationStore,
|
|
47
|
+
RunOutcome,
|
|
48
|
+
SessionEnvironment,
|
|
49
|
+
SessionEventType,
|
|
50
|
+
SessionPolicy,
|
|
51
|
+
)
|
|
52
|
+
from ..approval import describe_action
|
|
53
|
+
from ..config import ConfigError, ConfigStore, ProviderConfig
|
|
54
|
+
from ..policy import ApprovalMode
|
|
55
|
+
from ..runtime import build_runtime
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger("mindcode.acp_agent")
|
|
58
|
+
|
|
59
|
+
PROTOCOL_VERSION = 1 # stable ACP v1; see docs/acp-layering.md §5
|
|
60
|
+
|
|
61
|
+
_PERMISSION_TIMEOUT_S = 120.0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _stop_reason(result: Any) -> str:
|
|
65
|
+
"""Map a native run result to an ACP v1 stop reason (layering §3)."""
|
|
66
|
+
outcome = getattr(result, "outcome", None)
|
|
67
|
+
if outcome is RunOutcome.CANCELLED:
|
|
68
|
+
return "cancelled"
|
|
69
|
+
if outcome in (RunOutcome.ERROR, RunOutcome.PARTIAL):
|
|
70
|
+
return "refusal"
|
|
71
|
+
return "end_turn"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _tool_kind(action_name: str) -> str:
|
|
75
|
+
if action_name.startswith("file_read"):
|
|
76
|
+
return "read"
|
|
77
|
+
if action_name in ("file_write", "file_edit"):
|
|
78
|
+
return "edit"
|
|
79
|
+
if action_name.startswith("exec_") or "command" in action_name:
|
|
80
|
+
return "execute"
|
|
81
|
+
if "search" in action_name or "glob" in action_name or "grep" in action_name:
|
|
82
|
+
return "search"
|
|
83
|
+
return "other"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _extract_text(prompt: list[Any]) -> str:
|
|
87
|
+
parts: list[str] = []
|
|
88
|
+
for block in prompt:
|
|
89
|
+
if isinstance(block, TextContentBlock):
|
|
90
|
+
parts.append(block.text)
|
|
91
|
+
else:
|
|
92
|
+
raise RequestError.invalid_params(
|
|
93
|
+
"only text content blocks are supported"
|
|
94
|
+
)
|
|
95
|
+
return "".join(parts)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class _ToolCallTracker:
|
|
100
|
+
"""ACTION_STARTED → OBSERVATION_CREATED bookkeeping per session."""
|
|
101
|
+
|
|
102
|
+
by_action: dict[str, ToolCallStart] = field(default_factory=dict)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _ManagedSession:
|
|
106
|
+
"""Native session plus the per-session serialisation state."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, session_id: str, session: AgentSession, runtime: AgentRuntime):
|
|
109
|
+
self.session_id = session_id
|
|
110
|
+
self.session = session
|
|
111
|
+
self.runtime = runtime
|
|
112
|
+
self.turn_lock = asyncio.Lock()
|
|
113
|
+
self.tool_calls = _ToolCallTracker()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _AgentHarness:
|
|
117
|
+
"""Owns ACP sessions; each maps to one native ``AgentSession``."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
conn: AgentSideConnection,
|
|
122
|
+
*,
|
|
123
|
+
config_store: ConfigStore | None = None,
|
|
124
|
+
cwd: Path | None = None,
|
|
125
|
+
subagents: bool = False,
|
|
126
|
+
permission_timeout_s: float = _PERMISSION_TIMEOUT_S,
|
|
127
|
+
) -> None:
|
|
128
|
+
self._conn = conn
|
|
129
|
+
self._config_store = config_store or ConfigStore()
|
|
130
|
+
self._cwd = cwd or Path.cwd()
|
|
131
|
+
self._subagents = subagents
|
|
132
|
+
self._permission_timeout_s = permission_timeout_s
|
|
133
|
+
self._sessions: dict[str, _ManagedSession] = {}
|
|
134
|
+
self._new_session_lock = asyncio.Lock()
|
|
135
|
+
|
|
136
|
+
# ----- ACP agent methods (stable surface only) -----
|
|
137
|
+
|
|
138
|
+
async def initialize(
|
|
139
|
+
self,
|
|
140
|
+
protocol_version: int,
|
|
141
|
+
client_capabilities: Any | None = None,
|
|
142
|
+
client_info: Any | None = None,
|
|
143
|
+
**kwargs: Any,
|
|
144
|
+
) -> InitializeResponse:
|
|
145
|
+
logger.info(
|
|
146
|
+
"initialize: client=%s protocol=%s",
|
|
147
|
+
getattr(client_info, "name", "unknown"),
|
|
148
|
+
protocol_version,
|
|
149
|
+
)
|
|
150
|
+
return InitializeResponse(
|
|
151
|
+
protocol_version=PROTOCOL_VERSION,
|
|
152
|
+
agent_capabilities=AgentCapabilities(
|
|
153
|
+
load_session=False,
|
|
154
|
+
prompt_capabilities=PromptCapabilities(),
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
async def new_session(
|
|
159
|
+
self,
|
|
160
|
+
cwd: str,
|
|
161
|
+
mcp_servers: list[Any] | None = None,
|
|
162
|
+
**kwargs: Any,
|
|
163
|
+
) -> NewSessionResponse:
|
|
164
|
+
if mcp_servers:
|
|
165
|
+
raise RequestError.invalid_request(
|
|
166
|
+
"mcpServers are not supported by mindcode yet"
|
|
167
|
+
)
|
|
168
|
+
async with self._new_session_lock:
|
|
169
|
+
workspace = Path(cwd).expanduser() if cwd else self._cwd
|
|
170
|
+
try:
|
|
171
|
+
resolved = self._config_store.resolve()
|
|
172
|
+
except ConfigError as exc:
|
|
173
|
+
raise RequestError.internal_error(str(exc)) from exc
|
|
174
|
+
store = LocalConversationStore(
|
|
175
|
+
self._config_store.root / "sessions"
|
|
176
|
+
)
|
|
177
|
+
runtime = build_runtime(
|
|
178
|
+
workspace,
|
|
179
|
+
provider_config=resolved,
|
|
180
|
+
approve_mode=ApprovalMode(resolved.approval_mode),
|
|
181
|
+
stream=True,
|
|
182
|
+
event_handler=None,
|
|
183
|
+
approval_callback=self._make_approval(resolved),
|
|
184
|
+
)
|
|
185
|
+
try:
|
|
186
|
+
session = runtime.create_session(
|
|
187
|
+
environment=SessionEnvironment(
|
|
188
|
+
workspace_root=workspace,
|
|
189
|
+
working_directory=workspace,
|
|
190
|
+
),
|
|
191
|
+
policy=SessionPolicy(
|
|
192
|
+
allow_write=True,
|
|
193
|
+
allow_dangerous=False,
|
|
194
|
+
max_parallel_actions=1,
|
|
195
|
+
),
|
|
196
|
+
conversation_store=store,
|
|
197
|
+
)
|
|
198
|
+
except Exception:
|
|
199
|
+
await runtime.close(close_dependencies=True)
|
|
200
|
+
raise
|
|
201
|
+
session_id = session.session_id
|
|
202
|
+
self._sessions[session_id] = _ManagedSession(
|
|
203
|
+
session_id, session, runtime
|
|
204
|
+
)
|
|
205
|
+
logger.info(
|
|
206
|
+
"session %s created (workspace=%s provider=%s model=%s approval=%s)",
|
|
207
|
+
session_id,
|
|
208
|
+
workspace,
|
|
209
|
+
resolved.name,
|
|
210
|
+
resolved.model,
|
|
211
|
+
resolved.approval_mode,
|
|
212
|
+
)
|
|
213
|
+
return NewSessionResponse(session_id=session_id)
|
|
214
|
+
|
|
215
|
+
async def prompt(
|
|
216
|
+
self,
|
|
217
|
+
session_id: str,
|
|
218
|
+
prompt: list[Any],
|
|
219
|
+
**kwargs: Any,
|
|
220
|
+
) -> PromptResponse:
|
|
221
|
+
managed = self._managed(session_id)
|
|
222
|
+
text = _extract_text(prompt)
|
|
223
|
+
if not text.strip():
|
|
224
|
+
raise RequestError.invalid_params("prompt must contain text")
|
|
225
|
+
if managed.turn_lock.locked():
|
|
226
|
+
# 铁律 2:每 session 串行,第二轮直接拒收(参照 COMMAND_REJECTED 语义)。
|
|
227
|
+
raise RequestError.invalid_request(
|
|
228
|
+
f"session {session_id} already has an active run"
|
|
229
|
+
)
|
|
230
|
+
async with managed.turn_lock:
|
|
231
|
+
return await self._run_turn(managed, text)
|
|
232
|
+
|
|
233
|
+
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
|
234
|
+
managed = self._managed(session_id)
|
|
235
|
+
run_id = managed.session.active_run_id
|
|
236
|
+
if run_id:
|
|
237
|
+
logger.info("cancel session=%s run=%s", session_id, run_id)
|
|
238
|
+
await managed.session.cancel_run(run_id)
|
|
239
|
+
|
|
240
|
+
async def set_session_mode(
|
|
241
|
+
self, session_id: str, mode_id: str, **kwargs: Any
|
|
242
|
+
) -> None:
|
|
243
|
+
raise RequestError.invalid_request(
|
|
244
|
+
"session/set_mode is not supported yet (planned P2)"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
async def load_session(self, *args: Any, **kwargs: Any) -> None:
|
|
248
|
+
raise RequestError.invalid_request(
|
|
249
|
+
"session/load is not supported yet (planned P2)"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# ----- internals -----
|
|
253
|
+
|
|
254
|
+
def _managed(self, session_id: str) -> _ManagedSession:
|
|
255
|
+
managed = self._sessions.get(session_id)
|
|
256
|
+
if managed is None:
|
|
257
|
+
raise RequestError.resource_not_found(session_id)
|
|
258
|
+
return managed
|
|
259
|
+
|
|
260
|
+
async def close(self) -> None:
|
|
261
|
+
for managed in list(self._sessions.values()):
|
|
262
|
+
try:
|
|
263
|
+
await managed.runtime.close(close_dependencies=True)
|
|
264
|
+
except Exception:
|
|
265
|
+
logger.exception("failed closing session %s", managed.session_id)
|
|
266
|
+
self._sessions.clear()
|
|
267
|
+
|
|
268
|
+
# ----- approval bridge (native port 4 → session/request_permission) -----
|
|
269
|
+
|
|
270
|
+
def _make_approval(self, provider_config: ProviderConfig) -> Any:
|
|
271
|
+
async def approve(context: Any, action: Any) -> bool:
|
|
272
|
+
managed = self._managed(context.session_id)
|
|
273
|
+
summary = describe_action(action)
|
|
274
|
+
started = helpers.start_tool_call(
|
|
275
|
+
tool_call_id=f"perm-{int(time.time() * 1000)}",
|
|
276
|
+
title=summary,
|
|
277
|
+
kind=_tool_kind(getattr(action, "name", "")),
|
|
278
|
+
status="pending",
|
|
279
|
+
)
|
|
280
|
+
await self._notify_update(managed, started)
|
|
281
|
+
options = [
|
|
282
|
+
PermissionOption(
|
|
283
|
+
option_id="allow", name="Allow", kind="allow_once"
|
|
284
|
+
),
|
|
285
|
+
PermissionOption(
|
|
286
|
+
option_id="reject", name="Reject", kind="reject_once"
|
|
287
|
+
),
|
|
288
|
+
]
|
|
289
|
+
try:
|
|
290
|
+
response = await asyncio.wait_for(
|
|
291
|
+
self._conn.request_permission(
|
|
292
|
+
session_id=managed.session_id,
|
|
293
|
+
tool_call=started,
|
|
294
|
+
options=options,
|
|
295
|
+
),
|
|
296
|
+
timeout=self._permission_timeout_s,
|
|
297
|
+
)
|
|
298
|
+
except asyncio.TimeoutError:
|
|
299
|
+
logger.warning(
|
|
300
|
+
"permission timed out after %.0fs → deny (unattended)",
|
|
301
|
+
self._permission_timeout_s,
|
|
302
|
+
)
|
|
303
|
+
return False
|
|
304
|
+
outcome = response.outcome
|
|
305
|
+
option_id = getattr(outcome, "option_id", None)
|
|
306
|
+
if getattr(outcome, "outcome", None) == "cancelled":
|
|
307
|
+
return False
|
|
308
|
+
return option_id == "allow"
|
|
309
|
+
|
|
310
|
+
return approve
|
|
311
|
+
|
|
312
|
+
# ----- event-stream translation (native port 5 → session/update) -----
|
|
313
|
+
|
|
314
|
+
async def _notify_update(self, managed: _ManagedSession, update: Any) -> None:
|
|
315
|
+
await self._conn.session_update(
|
|
316
|
+
session_id=managed.session_id,
|
|
317
|
+
update=update,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
async def _run_turn(self, managed: _ManagedSession, text: str) -> PromptResponse:
|
|
321
|
+
session = managed.session
|
|
322
|
+
checkpoint = session.pending_checkpoint
|
|
323
|
+
if checkpoint is None:
|
|
324
|
+
command_id = await session.submit_run(text)
|
|
325
|
+
else:
|
|
326
|
+
command_id = await session.continue_run(
|
|
327
|
+
checkpoint.checkpoint_id, user_input=text
|
|
328
|
+
)
|
|
329
|
+
while True:
|
|
330
|
+
event = await session.next_event()
|
|
331
|
+
if event.event_type == SessionEventType.RUN_ACCEPTED:
|
|
332
|
+
continue
|
|
333
|
+
if event.event_type == SessionEventType.AGENT_EVENT:
|
|
334
|
+
agent_event = event.agent_event
|
|
335
|
+
if agent_event is None:
|
|
336
|
+
continue
|
|
337
|
+
await self._on_agent_event(managed, agent_event)
|
|
338
|
+
continue
|
|
339
|
+
if event.event_type in (
|
|
340
|
+
SessionEventType.APPROVAL_DECIDED,
|
|
341
|
+
SessionEventType.USER_MESSAGE_APPENDED,
|
|
342
|
+
):
|
|
343
|
+
continue
|
|
344
|
+
if event.event_type == SessionEventType.RUN_INTERRUPTED:
|
|
345
|
+
return PromptResponse(stop_reason="cancelled")
|
|
346
|
+
if event.event_type == SessionEventType.RUN_COMPLETED:
|
|
347
|
+
if event.correlation_id and event.correlation_id != command_id:
|
|
348
|
+
# 中断跟随:旧一轮收尾后新一轮接管,继续等新一轮。
|
|
349
|
+
command_id = event.correlation_id
|
|
350
|
+
continue
|
|
351
|
+
result = event.payload.get("result")
|
|
352
|
+
if result is None:
|
|
353
|
+
logger.error(
|
|
354
|
+
"run %s failed: %s",
|
|
355
|
+
event.run_id,
|
|
356
|
+
event.payload.get("error"),
|
|
357
|
+
)
|
|
358
|
+
return PromptResponse(stop_reason="refusal")
|
|
359
|
+
return PromptResponse(stop_reason=_stop_reason(result))
|
|
360
|
+
if event.event_type == SessionEventType.COMMAND_REJECTED:
|
|
361
|
+
logger.error(
|
|
362
|
+
"command rejected: %s",
|
|
363
|
+
event.payload.get("reason", "unknown"),
|
|
364
|
+
)
|
|
365
|
+
return PromptResponse(stop_reason="refusal")
|
|
366
|
+
if event.event_type == SessionEventType.USER_INPUT_REQUIRED:
|
|
367
|
+
logger.warning(
|
|
368
|
+
"USER_INPUT_REQUIRED without approval bridge; cancelling run"
|
|
369
|
+
)
|
|
370
|
+
run_id = event.run_id or session.active_run_id
|
|
371
|
+
if run_id:
|
|
372
|
+
await session.cancel_run(run_id)
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
async def _on_agent_event(self, managed: _ManagedSession, agent_event: Any) -> None:
|
|
376
|
+
event_type = agent_event.event_type
|
|
377
|
+
payload = agent_event.payload or {}
|
|
378
|
+
if event_type == EventType.TEXT_DELTA:
|
|
379
|
+
delta = payload.get("delta", "")
|
|
380
|
+
if delta:
|
|
381
|
+
await self._notify_update(
|
|
382
|
+
managed, helpers.update_agent_message_text(delta)
|
|
383
|
+
)
|
|
384
|
+
elif event_type == EventType.ACTION_STARTED:
|
|
385
|
+
action_name = payload.get("action_name", "tool")
|
|
386
|
+
arguments = payload.get("arguments", {}) or {}
|
|
387
|
+
title = _describe_started(action_name, arguments)
|
|
388
|
+
started = helpers.start_tool_call(
|
|
389
|
+
tool_call_id=_tool_call_key(agent_event),
|
|
390
|
+
title=title,
|
|
391
|
+
kind=_tool_kind(action_name),
|
|
392
|
+
status="in_progress",
|
|
393
|
+
raw_input=arguments,
|
|
394
|
+
)
|
|
395
|
+
managed.tool_calls.by_action[_tool_call_key(agent_event)] = started
|
|
396
|
+
await self._notify_update(managed, started)
|
|
397
|
+
elif event_type == EventType.OBSERVATION_CREATED:
|
|
398
|
+
key = _tool_call_key(agent_event)
|
|
399
|
+
started = managed.tool_calls.by_action.pop(key, None)
|
|
400
|
+
ok = bool(payload.get("ok", False))
|
|
401
|
+
output = _observation_text(payload)
|
|
402
|
+
if started is not None:
|
|
403
|
+
progress = helpers.update_tool_call(
|
|
404
|
+
started.tool_call_id,
|
|
405
|
+
status="completed" if ok else "failed",
|
|
406
|
+
content=[helpers.tool_content(helpers.text_block(output))],
|
|
407
|
+
raw_output=payload.get("result"),
|
|
408
|
+
)
|
|
409
|
+
await self._notify_update(managed, progress)
|
|
410
|
+
elif event_type == EventType.ERROR_CREATED:
|
|
411
|
+
message = str(payload.get("error", "unknown error"))
|
|
412
|
+
logger.error("agent error: %s", message)
|
|
413
|
+
elif event_type == EventType.DECISION_CREATED:
|
|
414
|
+
# reasoning summary: forward as thought chunk when present
|
|
415
|
+
summary = payload.get("reasoning_summary")
|
|
416
|
+
if summary:
|
|
417
|
+
await self._notify_update(
|
|
418
|
+
managed, helpers.update_agent_thought_text(str(summary))
|
|
419
|
+
)
|
|
420
|
+
# 其他事件类型(COMPLETION_REJECTED 等)记日志即可
|
|
421
|
+
else:
|
|
422
|
+
logger.debug("untranslated agent event: %s", event_type)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _tool_call_key(agent_event: Any) -> str:
|
|
426
|
+
action_id = getattr(agent_event, "action_id", None) or getattr(
|
|
427
|
+
agent_event, "action_batch_id", None
|
|
428
|
+
)
|
|
429
|
+
if action_id:
|
|
430
|
+
return str(action_id)
|
|
431
|
+
payload = agent_event.payload or {}
|
|
432
|
+
return str(payload.get("action_id") or payload.get("batch_id") or "tool")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _describe_started(action_name: str, arguments: dict[str, Any]) -> str:
|
|
436
|
+
target = arguments.get("path") or arguments.get("command") or ""
|
|
437
|
+
title = f"{action_name} {target}".strip()
|
|
438
|
+
return title if title else action_name
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _observation_text(payload: dict[str, Any]) -> str:
|
|
442
|
+
if payload.get("ok", False):
|
|
443
|
+
result = payload.get("result")
|
|
444
|
+
if isinstance(result, dict):
|
|
445
|
+
diff = result.get("mindcode_diff")
|
|
446
|
+
if diff:
|
|
447
|
+
return str(diff)
|
|
448
|
+
output = result.get("output") or result.get("content")
|
|
449
|
+
if output:
|
|
450
|
+
return str(output)
|
|
451
|
+
return "ok"
|
|
452
|
+
error = payload.get("error") or "failed"
|
|
453
|
+
return str(error)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def build_harness(
|
|
457
|
+
conn: AgentSideConnection,
|
|
458
|
+
*,
|
|
459
|
+
config_store: ConfigStore | None = None,
|
|
460
|
+
cwd: Path | None = None,
|
|
461
|
+
subagents: bool = False,
|
|
462
|
+
) -> _AgentHarness:
|
|
463
|
+
"""Factory used by ``__main__`` and tests."""
|
|
464
|
+
return _AgentHarness(
|
|
465
|
+
conn,
|
|
466
|
+
config_store=config_store,
|
|
467
|
+
cwd=cwd,
|
|
468
|
+
subagents=subagents,
|
|
469
|
+
)
|
|
@@ -90,14 +90,14 @@ def make_shell_approval(
|
|
|
90
90
|
result = await chooser(
|
|
91
91
|
f"允许执行? {summary}",
|
|
92
92
|
[("allow", "允许"), ("deny", "拒绝")],
|
|
93
|
-
"
|
|
93
|
+
"allow",
|
|
94
94
|
)
|
|
95
95
|
return result == "allow"
|
|
96
96
|
# Lazy import:非交互进程构造 policy 时不引入 prompt_toolkit。
|
|
97
97
|
from .cli.shell.menu import prompt_yes_no
|
|
98
98
|
|
|
99
99
|
return await prompt_yes_no(
|
|
100
|
-
f"允许执行 {summary}?", default_yes=
|
|
100
|
+
f"允许执行 {summary}?", default_yes=True
|
|
101
101
|
)
|
|
102
102
|
|
|
103
103
|
return _approve
|
|
@@ -119,12 +119,12 @@ def make_shell_file_access_approval(
|
|
|
119
119
|
chooser = get_chooser()
|
|
120
120
|
if chooser is not None:
|
|
121
121
|
result = await chooser(
|
|
122
|
-
title, [("allow", "允许此次只读访问"), ("deny", "拒绝")], "
|
|
122
|
+
title, [("allow", "允许此次只读访问"), ("deny", "拒绝")], "allow",
|
|
123
123
|
)
|
|
124
124
|
approved = result == "allow"
|
|
125
125
|
else:
|
|
126
126
|
from .cli.shell.menu import prompt_yes_no
|
|
127
|
-
approved = await prompt_yes_no(title, default_yes=
|
|
127
|
+
approved = await prompt_yes_no(title, default_yes=True)
|
|
128
128
|
return ExternalFileReadDecision(request_id=request.request_id, approved=approved)
|
|
129
129
|
|
|
130
130
|
return approve
|
|
@@ -12,7 +12,7 @@ from mindagent.core.artifact import (
|
|
|
12
12
|
write_artifact_text,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
from ..config import
|
|
15
|
+
from ..config import ConfigStore
|
|
16
16
|
from ..policy import ApprovalMode
|
|
17
17
|
from ..runtime import build_runtime
|
|
18
18
|
from .adapters import built_in_adapters
|
|
@@ -115,13 +115,15 @@ class MindcodeBenchmarkRunner:
|
|
|
115
115
|
runtime = None
|
|
116
116
|
|
|
117
117
|
try:
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
provider_config = ConfigStore().resolve(
|
|
119
|
+
provider_override=config.provider,
|
|
120
|
+
model_override=config.model,
|
|
121
|
+
approval_override=config.approve_mode,
|
|
122
|
+
)
|
|
120
123
|
runtime = build_runtime(
|
|
121
124
|
workspace,
|
|
122
125
|
provider_config=provider_config,
|
|
123
126
|
approve_mode=ApprovalMode(config.approve_mode),
|
|
124
|
-
model=config.model,
|
|
125
127
|
stream=False,
|
|
126
128
|
event_handler=recorder,
|
|
127
129
|
max_steps=config.max_steps,
|