Browse Source

docs: add ACP agents guide

alvinreal 1 month ago
parent
commit
76fadced51
7 changed files with 133 additions and 10 deletions
  1. 1 0
      README.ja-JP.md
  2. 1 0
      README.ko-KR.md
  3. 1 0
      README.md
  4. 1 0
      README.zh-CN.md
  5. 104 0
      docs/acp-agents.md
  6. 3 0
      docs/configuration.md
  7. 22 10
      src/tools/acp-run.ts

+ 1 - 0
README.ja-JP.md

@@ -595,6 +595,7 @@ Worktrees は、Git worktree を `.slim/worktrees/<slug>/` 配下の安全で隔
 |-----|----------------|
 | **[Council](docs/council.md)** | 複数のモデルを並列実行し、`@council` で 1 つの回答に統合します |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | カスタムプロンプト、モデル、MCP アクセス、Orchestrator の委譲ルールを備えた独自の専門エージェントを定義します |
+| **[ACP Agents](docs/acp-agents.md)** | Claude Code ACP や Gemini ACP などの外部 ACP 互換エージェントを委譲可能なサブエージェントとして接続します |
 | **[Multiplexer Integration](docs/multiplexer-integration.md)** | エージェントの動作を Tmux や Zellij のペインでライブ表示します |
 | **[Codemap](docs/codemap.md)** | 階層的なコードマップを生成し、大規模コードベースを迅速に理解します |
 | **[Clonedeps](docs/clonedeps.md)** | 選択した依存関係のソースを ignore 済みのローカルワークスペースにクローンし、調査できるようにします |

+ 1 - 0
README.ko-KR.md

@@ -593,6 +593,7 @@ Worktrees는 Git worktree를 `.slim/worktrees/<slug>/` 아래의 안전하고 
 |-----|------|
 | **[Council](docs/council.md)** | `@council`로 여러 모델을 병렬 실행하고 하나의 답변으로 종합 |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | 커스텀 프롬프트, 모델, MCP 접근, Orchestrator 위임 규칙으로 커스텀 전문 에이전트 정의 |
+| **[ACP Agents](docs/acp-agents.md)** | Claude Code ACP 또는 Gemini ACP 같은 외부 ACP 호환 에이전트를 위임 가능한 서브에이전트로 연결 |
 | **[Multiplexer Integration](docs/multiplexer-integration.md)** | Tmux 또는 Zellij 페인에서 에이전트 작업을 실시간으로 확인 |
 | **[Codemap](docs/codemap.md)** | 계층형 코드맵을 생성하여 대규모 코드베이스를 빠르게 파악 |
 | **[Clonedeps](docs/clonedeps.md)** | 선택한 의존성 소스를 무시된 로컬 워크스페이스에 복제하여 검사 |

+ 1 - 0
README.md

@@ -619,6 +619,7 @@ Use this section as a map: start with installation, then jump to features, confi
 |-----|----------------|
 | **[Council](docs/council.md)** | Run multiple models in parallel and synthesize a single answer with `@council` |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | Define your own specialists with custom prompts, models, MCP access, and Orchestrator delegation rules |
+| **[ACP Agents](docs/acp-agents.md)** | Connect external ACP-compatible agents such as Claude Code ACP or Gemini ACP as delegatable subagents |
 | **[Multiplexer Integration](docs/multiplexer-integration.md)** | Watch agents work live in Tmux or Zellij panes |
 | **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
 | **[Clonedeps](docs/clonedeps.md)** | Clone selected dependency source into an ignored local workspace for inspection |

+ 1 - 0
README.zh-CN.md

@@ -590,6 +590,7 @@ Worktrees 将 Git worktree 作为安全、隔离的编码通道管理,默认
 |-----|----------------|
 | **[Council](docs/council.md)** | 使用 `@council` 并行运行多个模型并合成单一答案 |
 | **[自定义智能体](docs/configuration.md#custom-agents)** | 使用自定义提示词、模型、MCP 访问和 Orchestrator 委派规则定义自己的专家 |
+| **[ACP Agents](docs/acp-agents.md)** | 将 Claude Code ACP 或 Gemini ACP 等外部 ACP 兼容智能体连接为可委派子智能体 |
 | **[多路复用器集成](docs/multiplexer-integration.md)** | 在 Tmux 或 Zellij 窗格中实时观看智能体工作 |
 | **[Codemap](docs/codemap.md)** | 生成层级代码地图,更快理解大型代码库 |
 | **[Clonedeps](docs/clonedeps.md)** | 将选定的依赖源码克隆到被忽略的本地工作区中以供检查 |

+ 104 - 0
docs/acp-agents.md

@@ -0,0 +1,104 @@
+# ACP Agents
+
+Expose external [Agent Client Protocol](https://agentclientprotocol.com/) agents
+as optional OpenCode subagents.
+
+Use this when you want the orchestrator to delegate to software-connected tools
+such as Claude Code ACP, Gemini ACP, or another ACP-compatible coding agent.
+
+## How it works
+
+Each `acpAgents` entry creates a lightweight wrapper subagent. The wrapper can
+only call `acp_run`, which:
+
+1. Starts the configured ACP subprocess over stdio.
+2. Sends `initialize`.
+3. Creates a session with `session/new`.
+4. Sends the task with `session/prompt`.
+5. Collects `session/update` `agent_message_chunk` text.
+6. Returns the external agent's final output to OpenCode.
+
+The wrapper is sandboxed from normal local tools such as `bash`, `edit`,
+`task`, `webfetch`, `grep`, and `glob`.
+
+## Configuration
+
+Add `acpAgents` to `~/.config/opencode/oh-my-opencode-slim.jsonc` or a
+project-local `.opencode/oh-my-opencode-slim.jsonc` file:
+
+```jsonc
+{
+  "acpAgents": {
+    "claude-research": {
+      "command": "claude-code-acp",
+      "args": [],
+      "description": "Claude Code subscription agent for deep research",
+      "wrapperModel": "openai/gpt-5.4-mini",
+      "permissionMode": "ask",
+      "timeoutMs": 300000
+    },
+    "gemini-acp": {
+      "command": "gemini",
+      "args": ["--experimental-acp"],
+      "description": "Gemini CLI through ACP"
+    }
+  }
+}
+```
+
+Restart OpenCode after changing config. Then call the generated agent directly:
+
+```text
+@claude-research investigate this bug and summarize the likely cause
+```
+
+Or let the orchestrator delegate to it when its routing prompt matches the task.
+
+## Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `command` | string | — | ACP executable. Put flags in `args`, not here. |
+| `args` | string[] | `[]` | Arguments for the ACP command. |
+| `env` | object | `{}` | Extra environment variables for the subprocess. |
+| `cwd` | string | current session directory | Working directory override. ACP paths should be absolute. |
+| `description` | string | generated | Role text shown to OpenCode and the orchestrator. |
+| `prompt` | string | generated | Full prompt for the wrapper subagent. Usually unnecessary. |
+| `orchestratorPrompt` | string | generated | Exact routing block injected into the orchestrator prompt. |
+| `wrapperModel` | string | fixer default | Cheap OpenCode model used by the wrapper. |
+| `permissionMode` | `ask` \| `allow` \| `reject` | `ask` | How ACP permission requests are answered. |
+| `timeoutMs` | integer | `300000` | Timeout for one ACP run. |
+
+## Authentication
+
+ACP agents may advertise `authMethods` during initialization and may require
+authentication before `session/new`. The bridge attempts the first advertised
+auth method if the agent reports an auth-required error.
+
+Some agents still require manual setup first. For example, run the external
+agent's login command in your terminal before using the wrapper:
+
+```bash
+claude /login
+```
+
+Use the command required by your ACP server.
+
+## Safety notes
+
+- The plugin asks before launching the configured subprocess.
+- The wrapper agent can only call `acp_run`.
+- `acp_run` can only be called by the matching wrapper agent.
+- External ACP agents may still run their own tools depending on their own
+  implementation and permission flow.
+- Keep secrets in environment variables and pass only the minimum needed via
+  `env`.
+
+## Troubleshooting
+
+- **Agent not available:** restart OpenCode after editing config.
+- **Unknown ACP agent:** check that the `acpAgents` key name matches your
+  `@agent` name.
+- **Auth required:** run the ACP agent's login/auth setup command directly.
+- **No output:** verify the command works as an ACP server in a terminal or ACP
+  client.

+ 3 - 0
docs/configuration.md

@@ -171,6 +171,9 @@ entry. The wrapper calls the built-in `acp_run` tool, which starts the ACP
 process, creates a session, sends the task, and returns the streamed result.
 `command` is only the executable; put flags and subcommands in `args`.
 
+See **[ACP Agents](acp-agents.md)** for the dedicated setup guide, auth notes,
+and troubleshooting.
+
 ```jsonc
 {
   "acpAgents": {

+ 22 - 10
src/tools/acp-run.ts

@@ -58,16 +58,15 @@ class AcpClient {
       this.errors.push(String(chunk));
     });
     this.child.on('error', (error) => {
-      for (const item of this.pending.values()) item.reject(error);
-      this.pending.clear();
+      this.rejectPending(error);
     });
     this.child.on('exit', (code, signal) => {
       if (this.pending.size === 0) return;
-      const error = new Error(
-        `ACP agent '${name}' exited before replying (code ${code ?? 'null'}, signal ${signal ?? 'null'})`,
+      this.rejectPending(
+        new Error(
+          `ACP agent '${name}' exited before replying (code ${code ?? 'null'}, signal ${signal ?? 'null'})`,
+        ),
       );
-      for (const item of this.pending.values()) item.reject(error);
-      this.pending.clear();
     });
 
     createInterface({ input: this.child.stdout }).on('line', (line) => {
@@ -155,10 +154,18 @@ class AcpClient {
 
   private async receive(line: string): Promise<void> {
     if (!line.trim()) return;
-    const message = JSON.parse(line) as
-      | RpcResponse
-      | RpcRequest
-      | RpcNotification;
+    let message: RpcResponse | RpcRequest | RpcNotification;
+    try {
+      message = JSON.parse(line) as RpcResponse | RpcRequest | RpcNotification;
+    } catch {
+      const error = new Error(
+        `ACP agent '${this.name}' wrote non-JSON stdout: ${line.slice(0, 200)}`,
+      );
+      this.errors.push(error.message);
+      this.rejectPending(error);
+      this.close();
+      return;
+    }
     if ('id' in message && ('result' in message || 'error' in message)) {
       const pending = this.pending.get(message.id);
       if (!pending) return;
@@ -177,6 +184,11 @@ class AcpClient {
     if ('method' in message) this.handleNotification(message);
   }
 
+  private rejectPending(error: Error): void {
+    for (const item of this.pending.values()) item.reject(error);
+    this.pending.clear();
+  }
+
   private async handleRequest(message: RpcRequest): Promise<void> {
     if (message.method === 'session/request_permission') {
       const title = readPermissionTitle(message.params);