sycommon-python-lib 0.2.7a0__py3-none-any.whl → 0.2.7a1__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.
- sycli/acp/__init__.py +7 -30
- sycli/acp/registry.py +182 -114
- sycli/acp/server.py +15 -51
- sycli/cli.py +1 -3
- sycli/commands/acp_cmd.py +28 -32
- sycli/simple_chat_agent.py +181 -0
- sycommon/agent/acp/__init__.py +64 -0
- sycommon/agent/acp/client.py +679 -0
- sycommon/agent/acp/fetch.py +190 -0
- sycommon/agent/acp/ssh_init.py +228 -0
- sycommon/agent/acp/tool.py +270 -0
- sycommon/agent/deep_agent.py +3 -11
- sycommon/agent/sandbox/file_ops.py +166 -37
- sycommon/agent/sandbox/http_sandbox_backend.py +43 -17
- sycommon/config/ACPAgentConfig.py +77 -0
- sycommon/config/Config.py +4 -1
- sycommon/logging/kafka_log.py +18 -2
- sycommon/middleware/sandbox.py +7 -7
- {sycommon_python_lib-0.2.7a0.dist-info → sycommon_python_lib-0.2.7a1.dist-info}/METADATA +12 -12
- {sycommon_python_lib-0.2.7a0.dist-info → sycommon_python_lib-0.2.7a1.dist-info}/RECORD +23 -16
- {sycommon_python_lib-0.2.7a0.dist-info → sycommon_python_lib-0.2.7a1.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.2.7a0.dist-info → sycommon_python_lib-0.2.7a1.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.2.7a0.dist-info → sycommon_python_lib-0.2.7a1.dist-info}/top_level.txt +0 -0
sycli/acp/__init__.py
CHANGED
|
@@ -1,38 +1,16 @@
|
|
|
1
|
-
"""ACP (Agent
|
|
1
|
+
"""ACP (Agent Client Protocol — Zed) support for sycli.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# Patch uvicorn config type aliases for compatibility with uvicorn>=0.34
|
|
7
|
-
# acp-sdk uses uvicorn.config.LoopSetupType etc. which were removed in uvicorn 0.48+
|
|
8
|
-
import uvicorn.config as _uvicorn_config
|
|
9
|
-
|
|
10
|
-
_TYPE_ALIASES = (
|
|
11
|
-
"LoopSetupType",
|
|
12
|
-
"HTTPProtocolType",
|
|
13
|
-
"WSProtocolType",
|
|
14
|
-
"LifespanType",
|
|
15
|
-
"InterfaceType",
|
|
16
|
-
)
|
|
17
|
-
for _alias in _TYPE_ALIASES:
|
|
18
|
-
if not hasattr(_uvicorn_config, _alias):
|
|
19
|
-
setattr(_uvicorn_config, _alias, str)
|
|
3
|
+
把本地 CLI 工具暴露成 Zed ACP agent(stdio JSON-RPC),可被任何 ACP client
|
|
4
|
+
(Zed、或我们 sycommon 的 ACP client)连接调用。
|
|
20
5
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"disable_existing_loggers": False,
|
|
25
|
-
"formatters": {},
|
|
26
|
-
"handlers": {},
|
|
27
|
-
"loggers": {},
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
del _uvicorn_config, _TYPE_ALIASES, _alias
|
|
6
|
+
使用官方 `agent-client-protocol`(import 名 `acp`)的 stdio transport,
|
|
7
|
+
照 examples/echo_agent.py 模板实现 Agent 协议。
|
|
8
|
+
"""
|
|
31
9
|
|
|
32
10
|
from sycli.acp.config import ACPConfig, ToolArgument, ToolDef
|
|
33
11
|
from sycli.acp.executor import build_cli_command, execute_command, stream_command
|
|
34
12
|
from sycli.acp.registry import CLIToolAgent, build_agents
|
|
35
|
-
from sycli.acp.server import
|
|
13
|
+
from sycli.acp.server import run_acp_server
|
|
36
14
|
|
|
37
15
|
__all__ = [
|
|
38
16
|
"ACPConfig",
|
|
@@ -41,7 +19,6 @@ __all__ = [
|
|
|
41
19
|
"ToolDef",
|
|
42
20
|
"build_agents",
|
|
43
21
|
"build_cli_command",
|
|
44
|
-
"create_acp_server",
|
|
45
22
|
"execute_command",
|
|
46
23
|
"run_acp_server",
|
|
47
24
|
"stream_command",
|
sycli/acp/registry.py
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Zed ACP agent registry.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
把 .sycli/acp.yaml 里声明的 CLI 工具暴露成一个 Zed ACP Agent(stdio)。
|
|
4
|
+
照官方 examples/echo_agent.py 模板实现 Agent 协议。
|
|
5
|
+
|
|
6
|
+
设计:一个 agent 进程承载多个 CLI 工具。prompt 到来时:
|
|
7
|
+
1. 从输入文本解析 `tool=<name>`(或第一段当作工具名),定位 ToolDef
|
|
8
|
+
2. build_cli_command 拼命令 → stream_command/execute_command 执行
|
|
9
|
+
3. 通过 session_update 把输出(逐行流式或一次性)推给 client
|
|
10
|
+
4. 返回 PromptResponse(stop_reason="end_turn")
|
|
6
11
|
"""
|
|
7
12
|
|
|
8
13
|
from __future__ import annotations
|
|
9
14
|
|
|
10
15
|
import os
|
|
11
16
|
from collections.abc import AsyncGenerator
|
|
12
|
-
from typing import TYPE_CHECKING
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
from acp import (
|
|
21
|
+
Agent,
|
|
22
|
+
InitializeResponse,
|
|
23
|
+
NewSessionResponse,
|
|
24
|
+
PromptResponse,
|
|
25
|
+
text_block,
|
|
26
|
+
update_agent_message,
|
|
27
|
+
update_plan,
|
|
28
|
+
)
|
|
29
|
+
from acp.interfaces import Client
|
|
30
|
+
from acp.schema import (
|
|
31
|
+
AudioContentBlock,
|
|
32
|
+
ClientCapabilities,
|
|
33
|
+
EmbeddedResourceContentBlock,
|
|
34
|
+
HttpMcpServer,
|
|
35
|
+
ImageContentBlock,
|
|
36
|
+
Implementation,
|
|
37
|
+
McpServerStdio,
|
|
38
|
+
PlanEntry,
|
|
39
|
+
ResourceContentBlock,
|
|
40
|
+
SseMcpServer,
|
|
41
|
+
TextContentBlock,
|
|
42
|
+
)
|
|
18
43
|
|
|
19
44
|
from sycli.acp.executor import build_cli_command, execute_command, stream_command
|
|
20
45
|
|
|
@@ -22,134 +47,177 @@ if TYPE_CHECKING:
|
|
|
22
47
|
from sycli.acp.config import ACPConfig, ToolDef
|
|
23
48
|
|
|
24
49
|
|
|
25
|
-
class CLIToolAgent(
|
|
26
|
-
"""
|
|
50
|
+
class CLIToolAgent(Agent):
|
|
51
|
+
"""一个承载多个 CLI 工具的 Zed ACP agent。
|
|
27
52
|
|
|
28
|
-
|
|
29
|
-
is an async generator, enabling both streaming and batch modes
|
|
30
|
-
via the ACP SDK executor.
|
|
53
|
+
每次 prompt:解析工具名 → 执行对应 CLI → session_update 推输出。
|
|
31
54
|
"""
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
self._tool_def = tool_def
|
|
35
|
-
self._project_root = project_root
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def name(self) -> str:
|
|
39
|
-
return self._tool_def.name
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def description(self) -> str:
|
|
43
|
-
return self._tool_def.description or f"CLI tool: {self._tool_def.command}"
|
|
56
|
+
_conn: Client | None = None
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
def __init__(self, config: "ACPConfig", project_root: str) -> None:
|
|
59
|
+
self._config = config
|
|
60
|
+
self._project_root = project_root
|
|
61
|
+
# 工具名 → ToolDef 索引,便于路由
|
|
62
|
+
self._tools_by_name: dict[str, "ToolDef"] = {t.name: t for t in config.tools}
|
|
63
|
+
|
|
64
|
+
# ---- Agent 协议回调 ----
|
|
65
|
+
def on_connect(self, conn: Client) -> None:
|
|
66
|
+
self._conn = conn
|
|
67
|
+
|
|
68
|
+
async def initialize(
|
|
69
|
+
self,
|
|
70
|
+
protocol_version: int,
|
|
71
|
+
client_capabilities: ClientCapabilities | None = None,
|
|
72
|
+
client_info: Implementation | None = None,
|
|
73
|
+
**kwargs: Any,
|
|
74
|
+
) -> InitializeResponse:
|
|
75
|
+
return InitializeResponse(protocol_version=protocol_version)
|
|
76
|
+
|
|
77
|
+
async def new_session(
|
|
78
|
+
self,
|
|
79
|
+
cwd: str,
|
|
80
|
+
additional_directories: list[str] | None = None,
|
|
81
|
+
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
|
|
82
|
+
**kwargs: Any,
|
|
83
|
+
) -> NewSessionResponse:
|
|
84
|
+
return NewSessionResponse(session_id=uuid4().hex)
|
|
85
|
+
|
|
86
|
+
async def prompt(
|
|
87
|
+
self,
|
|
88
|
+
prompt: list[
|
|
89
|
+
TextContentBlock
|
|
90
|
+
| ImageContentBlock
|
|
91
|
+
| AudioContentBlock
|
|
92
|
+
| ResourceContentBlock
|
|
93
|
+
| EmbeddedResourceContentBlock
|
|
94
|
+
],
|
|
95
|
+
session_id: str,
|
|
96
|
+
**kwargs: Any,
|
|
97
|
+
) -> PromptResponse:
|
|
98
|
+
assert self._conn is not None, "Agent 未连接"
|
|
99
|
+
|
|
100
|
+
# 1. 提取输入文本
|
|
101
|
+
input_text = _extract_input_text_from_blocks(prompt)
|
|
102
|
+
|
|
103
|
+
# 2. 路由到具体 CLI 工具
|
|
104
|
+
tool_def, remaining_input = _resolve_tool(input_text, self._tools_by_name)
|
|
105
|
+
|
|
106
|
+
if tool_def is None:
|
|
107
|
+
# 找不到工具,列出可用工具
|
|
108
|
+
available = ", ".join(self._tools_by_name.keys()) or "(无)"
|
|
109
|
+
await self._conn.session_update(
|
|
110
|
+
session_id=session_id,
|
|
111
|
+
update=update_agent_message(text_block(
|
|
112
|
+
f"❌ 未识别的 CLI 工具。可用工具: {available}\n"
|
|
113
|
+
f"用法: 第一个词为工具名,或用 tool=<name> 指定。"
|
|
114
|
+
)),
|
|
115
|
+
)
|
|
116
|
+
return PromptResponse(stop_reason="end_turn")
|
|
117
|
+
|
|
118
|
+
# 3. 上报计划(让 client 看到要执行什么)
|
|
119
|
+
await self._conn.session_update(
|
|
120
|
+
session_id=session_id,
|
|
121
|
+
update=update_plan([PlanEntry(
|
|
122
|
+
content=f"执行 CLI 工具: {tool_def.name} ({tool_def.command})",
|
|
123
|
+
priority="medium",
|
|
124
|
+
status="in_progress",
|
|
125
|
+
)]),
|
|
57
126
|
)
|
|
58
127
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- Sync/async mode: yields complete Message at the end
|
|
67
|
-
|
|
68
|
-
Flow:
|
|
69
|
-
1. Extract text from input Messages (concatenate text/plain parts)
|
|
70
|
-
2. Build CLI command via build_cli_command()
|
|
71
|
-
3. If stream_output=True: stream_command(), yield MessagePart per line
|
|
72
|
-
4. Otherwise: execute_command(), yield single Message
|
|
73
|
-
"""
|
|
74
|
-
# Step 1: Extract input text
|
|
75
|
-
input_text = _extract_input_text(input)
|
|
76
|
-
|
|
77
|
-
# Step 2: Build command
|
|
78
|
-
command = build_cli_command(self._tool_def, input_text)
|
|
79
|
-
|
|
80
|
-
# Step 3: Determine working directory
|
|
81
|
-
cwd = self._tool_def.working_dir or self._project_root
|
|
82
|
-
|
|
83
|
-
# Step 4: Build environment
|
|
84
|
-
env = None
|
|
85
|
-
if self._tool_def.env:
|
|
86
|
-
env = {**os.environ, **self._tool_def.env}
|
|
87
|
-
|
|
88
|
-
if self._tool_def.stream_output:
|
|
89
|
-
# Streaming: yield MessagePart per stdout line
|
|
128
|
+
# 4. 拼命令 + 执行环境
|
|
129
|
+
command = build_cli_command(tool_def, remaining_input)
|
|
130
|
+
cwd = tool_def.working_dir or self._project_root
|
|
131
|
+
env = {**os.environ, **tool_def.env} if tool_def.env else None
|
|
132
|
+
|
|
133
|
+
# 5. 执行并推送输出
|
|
134
|
+
if tool_def.stream_output:
|
|
90
135
|
async for line in stream_command(
|
|
91
|
-
command,
|
|
92
|
-
cwd=cwd,
|
|
93
|
-
timeout=self._tool_def.timeout,
|
|
94
|
-
env=env,
|
|
136
|
+
command, cwd=cwd, timeout=tool_def.timeout, env=env,
|
|
95
137
|
):
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
138
|
+
await self._conn.session_update(
|
|
139
|
+
session_id=session_id,
|
|
140
|
+
update=update_agent_message(text_block(line)),
|
|
99
141
|
)
|
|
100
142
|
else:
|
|
101
|
-
# Batch: execute and yield complete output
|
|
102
143
|
result = await execute_command(
|
|
103
144
|
command,
|
|
104
145
|
cwd=cwd,
|
|
105
|
-
timeout=
|
|
146
|
+
timeout=tool_def.timeout,
|
|
106
147
|
env=env,
|
|
107
|
-
max_output_bytes=
|
|
108
|
-
stdin_input=
|
|
148
|
+
max_output_bytes=self._config.max_output_bytes,
|
|
149
|
+
stdin_input=remaining_input if tool_def.input_mode == "stdin" else None,
|
|
109
150
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
parts = []
|
|
115
|
-
if result.stdout:
|
|
116
|
-
parts.append(result.stdout)
|
|
117
|
-
if result.stderr:
|
|
118
|
-
parts.append(f"[stderr]\n{result.stderr}")
|
|
119
|
-
parts.append(f"Exit code: {result.exit_code}")
|
|
120
|
-
output = "\n".join(parts)
|
|
121
|
-
else:
|
|
122
|
-
output = result.stdout or "(no output)"
|
|
123
|
-
|
|
124
|
-
yield Message(
|
|
125
|
-
role="agent",
|
|
126
|
-
parts=[MessagePart(content=output, content_type="text/plain")],
|
|
151
|
+
output = _format_execution_result(result, tool_def)
|
|
152
|
+
await self._conn.session_update(
|
|
153
|
+
session_id=session_id,
|
|
154
|
+
update=update_agent_message(text_block(output)),
|
|
127
155
|
)
|
|
128
156
|
|
|
157
|
+
return PromptResponse(stop_reason="end_turn")
|
|
158
|
+
|
|
129
159
|
|
|
130
|
-
def
|
|
131
|
-
"""
|
|
160
|
+
def _extract_input_text_from_blocks(prompt: list[Any]) -> str:
|
|
161
|
+
"""从 prompt 内容块列表里拼接所有 text/plain 文本。"""
|
|
132
162
|
parts: list[str] = []
|
|
133
|
-
for
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if content_type in ("text/plain", None) and content:
|
|
138
|
-
parts.append(content)
|
|
163
|
+
for block in prompt:
|
|
164
|
+
text = block.get("text", "") if isinstance(block, dict) else getattr(block, "text", "")
|
|
165
|
+
if text:
|
|
166
|
+
parts.append(text)
|
|
139
167
|
return "\n".join(parts)
|
|
140
168
|
|
|
141
169
|
|
|
142
|
-
def
|
|
143
|
-
|
|
170
|
+
def _resolve_tool(
|
|
171
|
+
input_text: str, tools_by_name: dict[str, "ToolDef"]
|
|
172
|
+
) -> tuple["ToolDef | None", str]:
|
|
173
|
+
"""从输入文本解析工具名,返回 (ToolDef, 剩余输入)。
|
|
174
|
+
|
|
175
|
+
支持两种写法:
|
|
176
|
+
- `tool=<name> <args...>`(显式指定)
|
|
177
|
+
- `<name> <args...>`(第一段当工具名)
|
|
178
|
+
"""
|
|
179
|
+
stripped = input_text.strip()
|
|
180
|
+
if not stripped:
|
|
181
|
+
return None, ""
|
|
182
|
+
|
|
183
|
+
# tool=xxx 形式
|
|
184
|
+
if stripped.startswith("tool="):
|
|
185
|
+
head, _, rest = stripped.partition(" ")
|
|
186
|
+
name = head[len("tool="):].strip()
|
|
187
|
+
if name in tools_by_name:
|
|
188
|
+
return tools_by_name[name], rest.strip()
|
|
189
|
+
return None, stripped
|
|
190
|
+
|
|
191
|
+
# 第一段当工具名
|
|
192
|
+
first, _, rest = stripped.partition(" ")
|
|
193
|
+
if first in tools_by_name:
|
|
194
|
+
return tools_by_name[first], rest.strip()
|
|
195
|
+
return None, stripped
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _format_execution_result(result: Any, tool_def: "ToolDef") -> str:
|
|
199
|
+
"""把 executor.ExecutionResult 格式化成给 client 看的文本。"""
|
|
200
|
+
if result.timed_out:
|
|
201
|
+
return f"Error: Command timed out after {tool_def.timeout}s\n{result.stdout}"
|
|
202
|
+
if result.exit_code not in tool_def.allowed_exit_codes:
|
|
203
|
+
parts = []
|
|
204
|
+
if result.stdout:
|
|
205
|
+
parts.append(result.stdout)
|
|
206
|
+
if result.stderr:
|
|
207
|
+
parts.append(f"[stderr]\n{result.stderr}")
|
|
208
|
+
parts.append(f"Exit code: {result.exit_code}")
|
|
209
|
+
return "\n".join(parts)
|
|
210
|
+
return result.stdout or "(no output)"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def build_agents(config: "ACPConfig", project_root: str) -> CLIToolAgent:
|
|
214
|
+
"""创建承载所有 CLI 工具的 Zed ACP agent 实例。
|
|
144
215
|
|
|
145
216
|
Args:
|
|
146
|
-
config: ACP
|
|
147
|
-
project_root:
|
|
217
|
+
config: ACP 配置(含 tools 列表)。
|
|
218
|
+
project_root: CLI 命令执行根目录。
|
|
148
219
|
|
|
149
220
|
Returns:
|
|
150
|
-
|
|
221
|
+
CLIToolAgent 实例,ready for acp.run_agent()。
|
|
151
222
|
"""
|
|
152
|
-
return
|
|
153
|
-
CLIToolAgent(tool_def=tool_def, project_root=project_root)
|
|
154
|
-
for tool_def in config.tools
|
|
155
|
-
]
|
|
223
|
+
return CLIToolAgent(config=config, project_root=project_root)
|
sycli/acp/server.py
CHANGED
|
@@ -1,69 +1,33 @@
|
|
|
1
|
-
"""ACP server bootstrap for sycli.
|
|
1
|
+
"""Zed ACP server bootstrap for sycli.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
启动一个 Zed ACP agent(stdio JSON-RPC),把 .sycli/acp.yaml 里声明的 CLI 工具
|
|
4
|
+
作为该 agent 的能力暴露。照官方 examples/echo_agent.py 的 run_agent 模式。
|
|
5
|
+
|
|
6
|
+
注意:Zed ACP 是 stdio 协议,不像旧 acp-sdk 走 HTTP/FastAPI/uvicorn。
|
|
7
|
+
每个 CLI 工具不是独立的 agent,而是同一个 agent 用「会话内识别指令 → 路由到
|
|
8
|
+
对应工具」的方式承载(一个 agent 进程,多工具)。
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
9
12
|
|
|
10
13
|
from typing import TYPE_CHECKING
|
|
11
14
|
|
|
12
|
-
from acp_sdk.server.app import create_app
|
|
13
|
-
import uvicorn
|
|
14
|
-
|
|
15
15
|
from sycli.acp.registry import build_agents
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from acp_sdk.server import Server
|
|
19
18
|
from sycli.acp.config import ACPConfig
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
def
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
config: ACP configuration (loaded from .sycli/acp.yaml).
|
|
27
|
-
project_root: Project root directory for command execution.
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
FastAPI application instance with ACP routes.
|
|
31
|
-
"""
|
|
32
|
-
agents = build_agents(config, project_root)
|
|
33
|
-
app = create_app(*agents)
|
|
34
|
-
return app
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def create_acp_server(config: ACPConfig, project_root: str) -> Server:
|
|
38
|
-
"""Create and configure an ACP Server with all registered tool agents.
|
|
21
|
+
def run_acp_server(config: ACPConfig, project_root: str) -> None:
|
|
22
|
+
"""启动 Zed ACP agent(阻塞,直到 client 断开或收到关闭信号)。
|
|
39
23
|
|
|
40
24
|
Args:
|
|
41
|
-
config: ACP
|
|
42
|
-
project_root:
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
Configured Server instance (not yet started).
|
|
25
|
+
config: ACP 配置(来自 .sycli/acp.yaml),含 tools 列表。
|
|
26
|
+
project_root: CLI 命令执行的根目录。
|
|
46
27
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
server = Server()
|
|
50
|
-
agents = build_agents(config, project_root)
|
|
51
|
-
server.register(*agents)
|
|
52
|
-
return server
|
|
53
|
-
|
|
28
|
+
import asyncio
|
|
54
29
|
|
|
55
|
-
|
|
56
|
-
"""Create and start the ACP server (blocking until shutdown).
|
|
57
|
-
|
|
58
|
-
This is the main entry point called from acp_cmd.py.
|
|
59
|
-
Uses uvicorn directly to avoid acp-sdk / uvicorn version mismatches.
|
|
60
|
-
"""
|
|
61
|
-
app = create_acp_app(config, project_root)
|
|
30
|
+
from acp import run_agent
|
|
62
31
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
host=config.host,
|
|
66
|
-
port=config.port,
|
|
67
|
-
log_level="info",
|
|
68
|
-
headers=[("server", "acp")],
|
|
69
|
-
)
|
|
32
|
+
agent = build_agents(config, project_root)
|
|
33
|
+
asyncio.run(run_agent(agent))
|
sycli/cli.py
CHANGED
|
@@ -169,10 +169,8 @@ def _create_parser() -> argparse.ArgumentParser:
|
|
|
169
169
|
cdp_sub.add_parser("interactive", help="Start interactive CDP REPL")
|
|
170
170
|
|
|
171
171
|
# ── acp ──
|
|
172
|
-
p_acp = sub.add_parser("acp", help="Start ACP
|
|
172
|
+
p_acp = sub.add_parser("acp", help="Start Zed ACP agent exposing CLI tools over stdio")
|
|
173
173
|
p_acp.add_argument("--config", "-c", help="Path to ACP config file (default: .sycli/acp.yaml)")
|
|
174
|
-
p_acp.add_argument("--host", help="Override server host")
|
|
175
|
-
p_acp.add_argument("--port", "-p", type=int, help="Override server port")
|
|
176
174
|
p_acp.add_argument("--project-root", "-r", default=".", help="Project root directory")
|
|
177
175
|
|
|
178
176
|
# ── version ──
|
sycli/commands/acp_cmd.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
"""sycli acp command — start ACP
|
|
1
|
+
"""sycli acp command — start a Zed ACP agent exposing CLI tools over stdio."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import signal
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
@@ -17,10 +18,10 @@ def handle_acp(args) -> int:
|
|
|
17
18
|
2. Load .sycli/acp.yaml (or --config path)
|
|
18
19
|
3. Validate tool definitions
|
|
19
20
|
4. Print registered agents summary
|
|
20
|
-
5. Start ACP
|
|
21
|
+
5. Start Zed ACP agent over stdio (blocking)
|
|
21
22
|
|
|
22
23
|
Args:
|
|
23
|
-
args: Parsed argparse namespace with config,
|
|
24
|
+
args: Parsed argparse namespace with config, project_root.
|
|
24
25
|
|
|
25
26
|
Returns:
|
|
26
27
|
Exit code (0 for success, 1 for error).
|
|
@@ -28,22 +29,23 @@ def handle_acp(args) -> int:
|
|
|
28
29
|
project_root = Path(args.project_root or ".").resolve()
|
|
29
30
|
sycli_dir = project_root / ".sycli"
|
|
30
31
|
|
|
31
|
-
if not sycli_dir.exists():
|
|
32
|
-
error(".sycli/ not found. Run `sycli init` first.")
|
|
33
|
-
return 1
|
|
34
|
-
|
|
35
32
|
# Determine config path
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
# 显式 --config 优先(允许指向任意位置,不强制 .sycli/);
|
|
34
|
+
# 否则回退到 <project_root>/.sycli/acp.{yaml,json}
|
|
35
|
+
if args.config:
|
|
36
|
+
config_path = Path(args.config)
|
|
37
|
+
else:
|
|
38
|
+
config_path = sycli_dir / "acp.yaml"
|
|
39
|
+
if not config_path.exists():
|
|
40
|
+
config_path = sycli_dir / "acp.json"
|
|
39
41
|
|
|
40
42
|
if not config_path.exists():
|
|
41
|
-
|
|
43
|
+
if not args.config and not sycli_dir.exists():
|
|
44
|
+
error(".sycli/ not found. Run `sycli init` first, or 用 --config 指定外部配置。")
|
|
45
|
+
error(f"ACP config not found at {config_path}")
|
|
42
46
|
error("Create one with your tool definitions. Example:")
|
|
43
47
|
error("""
|
|
44
|
-
#
|
|
45
|
-
host: "127.0.0.1"
|
|
46
|
-
port: 8100
|
|
48
|
+
# acp.yaml
|
|
47
49
|
tools:
|
|
48
50
|
- name: "git-status"
|
|
49
51
|
command: "git"
|
|
@@ -62,33 +64,28 @@ def handle_acp(args) -> int:
|
|
|
62
64
|
error(f"Failed to load ACP config: {e}")
|
|
63
65
|
return 1
|
|
64
66
|
|
|
65
|
-
# Override port/host from CLI args
|
|
66
|
-
if args.host:
|
|
67
|
-
config.host = args.host
|
|
68
|
-
if args.port:
|
|
69
|
-
config.port = args.port
|
|
70
|
-
|
|
71
67
|
if not config.tools:
|
|
72
68
|
error("No tools defined in ACP config. Add tools to acp.yaml.")
|
|
73
69
|
return 1
|
|
74
70
|
|
|
75
|
-
# Print summary
|
|
76
|
-
header("ACP
|
|
77
|
-
info(f"Config:
|
|
78
|
-
info(f"
|
|
79
|
-
info(f"
|
|
80
|
-
info(f"Agents: {len(config.tools)} registered")
|
|
71
|
+
# Print summary to stderr(stdout 留给 stdio JSON-RPC)
|
|
72
|
+
header("Zed ACP Agent Configuration")
|
|
73
|
+
info(f"Config: {config_path}")
|
|
74
|
+
info(f"Transport: stdio (JSON-RPC)")
|
|
75
|
+
info(f"Agents: {len(config.tools)} CLI tools exposed")
|
|
81
76
|
print()
|
|
82
77
|
for tool in config.tools:
|
|
83
78
|
desc = tool.description or "(no description)"
|
|
84
79
|
info(f" {tool.name}: {tool.command} {' '.join(tool.args) if tool.args else ''}")
|
|
85
80
|
info(f" {desc}")
|
|
86
81
|
print()
|
|
82
|
+
print("-" * 60, file=sys.stderr)
|
|
83
|
+
info("Agent ready — waiting for ACP client on stdin/stdout...")
|
|
84
|
+
print("-" * 60, file=sys.stderr)
|
|
87
85
|
|
|
88
|
-
# Start
|
|
86
|
+
# Start agent (stdio, blocking)
|
|
89
87
|
from sycli.acp.server import run_acp_server
|
|
90
88
|
|
|
91
|
-
# Graceful shutdown on SIGINT
|
|
92
89
|
_shutdown = False
|
|
93
90
|
|
|
94
91
|
def _signal_handler(sig, frame):
|
|
@@ -96,18 +93,17 @@ def handle_acp(args) -> int:
|
|
|
96
93
|
if _shutdown:
|
|
97
94
|
sys.exit(1)
|
|
98
95
|
_shutdown = True
|
|
99
|
-
warning("\nShutting down ACP
|
|
96
|
+
warning("\nShutting down ACP agent...")
|
|
100
97
|
|
|
101
98
|
signal.signal(signal.SIGINT, _signal_handler)
|
|
102
99
|
|
|
103
100
|
try:
|
|
104
|
-
success(f"Starting ACP server on http://{config.host}:{config.port}")
|
|
105
101
|
run_acp_server(config, str(project_root))
|
|
106
102
|
except KeyboardInterrupt:
|
|
107
|
-
warning("ACP
|
|
103
|
+
warning("ACP agent stopped.")
|
|
108
104
|
return 0
|
|
109
105
|
except Exception as e:
|
|
110
|
-
error(f"ACP
|
|
106
|
+
error(f"ACP agent failed: {e}")
|
|
111
107
|
import traceback
|
|
112
108
|
|
|
113
109
|
traceback.print_exc()
|