feat(acp): add ACP (Agent Client Protocol) external agent support#1544
feat(acp): add ACP (Agent Client Protocol) external agent support#1544YingchaoX wants to merge 4 commits intoagentscope-ai:mainfrom
Conversation
Add ACP feature to enable CoPaw invoking external agents via ACP protocol (e.g., OpenCode, Qwen-code, Gemini CLI). Backend changes: - Add ACP module with config, runtime, transport, service, and permissions - Add sessions_spawn tool for ACP compatibility - Add approvals router for permission handling - Update runner to support external agent execution - Update agent react_agent to register ACP tool - Update command_handler to support /acp command - Update prompt builder with ACP guidance - Add ACP constants for drain loop configuration Frontend changes: - Add ACP configuration page - Add ACP API modules and types - Add approval API for permission handling - Update Chat page with external agent selector - Update session API for external agent metadata - Add ACP navigation and i18n support Follows Conventional Commits specification.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces the Agent Client Protocol (ACP) feature, significantly expanding CoPaw's capabilities by allowing it to seamlessly interact with external coding agents. This integration enables users to leverage specialized tools like OpenCode and Qwen-code directly within their CoPaw conversations, enhancing productivity and providing a more versatile development environment. The changes span both backend and frontend, ensuring robust functionality, configurable settings, and an intuitive user experience for managing and utilizing these external agents. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This is a substantial and well-executed feature addition that significantly expands CoPaw's capabilities by integrating external agents via ACP. The implementation is comprehensive, covering backend services, frontend UI, configuration, permissions, and documentation.
Key strengths of this PR include:
- Robust Architecture: The separation of concerns into transport, runtime, service, and projector layers on the backend is clean and maintainable.
- User Experience: The frontend work is thoughtful, with a dedicated configuration page, a new selector in the chat UI, and support for triggering agents via both commands and natural language.
- Security and Permissions: The integration with the existing approval system for dangerous operations is a critical security feature that has been implemented correctly.
- Backward Compatibility: The
sessions_spawntool shim is a great touch for guiding users from the old way to the new ACP flow.
I have a couple of suggestions for improvement related to maintainability and internationalization, which are detailed in the review comments. Overall, this is an excellent contribution.
| def parse_external_agent_text(raw: str | None) -> ExternalAgentConfig | None: | ||
| """Parse ACP intent from command-style or natural-language text.""" | ||
| text = (raw or "").strip() | ||
| if not text: | ||
| return None | ||
|
|
||
| harness: str | None = None | ||
| working = text | ||
|
|
||
| if re.match(r"^/acp\b", text, re.IGNORECASE): | ||
| working = re.sub(r"^/acp\b", "", text, count=1, flags=re.IGNORECASE).strip() | ||
| harness, working = _pop_leading_harness(working) | ||
| if harness is None: | ||
| harness, working = _pop_option_value( | ||
| working, | ||
| ("--harness", "--agent"), | ||
| ) | ||
| harness = normalize_harness_name(harness) | ||
| if not harness: | ||
| return None | ||
| else: | ||
| slash_match = re.match( | ||
| r"^/(opencode|open(?:\s|-)?code|qwen(?:\s*code|-code)?|qwencode)\b\s*(.*)$", | ||
| text, | ||
| re.IGNORECASE, | ||
| ) | ||
| cli_match = re.match( | ||
| r"^(?:--harness)(?:=|\s+)(opencode|open(?:\s|-)?code|qwen(?:\s*code|-code)?|qwencode)\b\s*(.*)$", | ||
| text, | ||
| re.IGNORECASE, | ||
| ) | ||
| zh_match = re.match( | ||
| r"^(?:用|使用|让|通过|调用)\s+(opencode|open(?:\s|-)?code|qwen(?:\s*code|-code)?|qwencode)\b(?:\s*(?:来|去|帮忙|帮助))?\s*(.*)$", | ||
| text, | ||
| re.IGNORECASE, | ||
| ) | ||
| en_match = re.match( | ||
| r"^(?:use|with|via|call)\s+(opencode|open(?:\s|-)?code|qwen(?:\s*code|-code)?|qwencode)\b(?:\s+to)?\s*(.*)$", | ||
| text, | ||
| re.IGNORECASE, | ||
| ) | ||
| match = slash_match or cli_match or zh_match or en_match | ||
| if match is None: | ||
| return None | ||
| harness = normalize_harness_name(match.group(1).replace(" ", " ")) | ||
| working = match.group(2).strip() | ||
|
|
||
| keep_session = False | ||
| keep_session_specified = False | ||
|
|
||
| keep_flag, working = _pop_flag(working, ("--keep-session", "--session")) | ||
| if keep_flag: | ||
| keep_session = True | ||
| keep_session_specified = True | ||
|
|
||
| session_id, working = _pop_option_value( | ||
| working, | ||
| ("--session-id", "--resume-session", "--load-session"), | ||
| ) | ||
|
|
||
| if session_id is None: | ||
| natural_session = re.search( | ||
| r"(?:继续|复用|加载)\s*(?:session|会话)\s+(\"[^\"]+\"|'[^']+'|\S+)", | ||
| working, | ||
| re.IGNORECASE, | ||
| ) | ||
| if natural_session is not None: | ||
| session_id = _strip_quotes(natural_session.group(1)) | ||
| working = ( | ||
| working[:natural_session.start()] + " " + working[natural_session.end():] | ||
| ).strip() | ||
|
|
||
| cwd, working = _pop_option_value( | ||
| working, | ||
| ("--cwd", "--workdir", "--working-dir", "--work-path"), | ||
| ) | ||
|
|
||
| if cwd is None: | ||
| explicit_cwd = re.search( | ||
| r"(?:工作路径|工作目录|workdir|cwd)\s*(?:是|为|=|:|:)?\s*(\"[^\"]+\"|'[^']+'|\S+)", | ||
| working, | ||
| re.IGNORECASE, | ||
| ) | ||
| if explicit_cwd is not None: | ||
| cwd = _strip_quotes(explicit_cwd.group(1)) | ||
| working = (working[:explicit_cwd.start()] + " " + working[explicit_cwd.end():]).strip() | ||
|
|
||
| if cwd is None: | ||
| natural_cwd = re.search( | ||
| r"在\s+(\"[^\"]+\"|'[^']+'|\S+)\s+(?:下|目录下|工作目录下)", | ||
| working, | ||
| re.IGNORECASE, | ||
| ) | ||
| candidate = _strip_quotes(natural_cwd.group(1)) if natural_cwd is not None else None | ||
| if natural_cwd is not None and _looks_like_path(candidate): | ||
| cwd = candidate | ||
| working = (working[:natural_cwd.start()] + " " + working[natural_cwd.end():]).strip() | ||
|
|
||
| if re.search(r"(?:保持会话|keep session)", working, re.IGNORECASE): | ||
| keep_session = True | ||
| keep_session_specified = True | ||
| working = re.sub(r"(?:保持会话|keep session)", " ", working, flags=re.IGNORECASE) | ||
|
|
||
| if re.search( | ||
| r"(?:之前的|上一个|上次的|刚才的|当前的?|现在的?)\s*(?:acp\s*)?(?:session|会话)|(?:previous|last|current)\s+(?:acp\s+)?session", | ||
| working, | ||
| re.IGNORECASE, | ||
| ): | ||
| keep_session = True | ||
| keep_session_specified = True | ||
| working = re.sub( | ||
| r"(?:请)?\s*(?:使用|复用|继续用|沿用|在)?\s*(?:之前的|上一个|上次的|刚才的|当前的?|现在的?)\s*(?:acp\s*)?(?:session|会话)(?:\s*用)?|(?:use|reuse|continue with)\s+(?:the\s+)?(?:previous|last|current)\s+(?:acp\s+)?session", | ||
| " ", | ||
| working, | ||
| flags=re.IGNORECASE, | ||
| ) | ||
|
|
||
| if session_id: | ||
| keep_session = True | ||
| keep_session_specified = True | ||
|
|
||
| return ExternalAgentConfig( | ||
| enabled=True, | ||
| harness=harness, | ||
| keep_session=keep_session, | ||
| cwd=cwd, | ||
| existing_session_id=session_id, | ||
| prompt=_normalize_prompt(working), | ||
| keep_session_specified=keep_session_specified, | ||
| ) |
There was a problem hiding this comment.
The logic for parsing external agent commands from text in parse_external_agent_text is quite complex and appears to be duplicated in the frontend in console/src/pages/Chat/index.tsx. This creates a maintainability issue, as any changes to the parsing rules (e.g., adding new flags or natural language triggers) must be manually synchronized between the Python and TypeScript implementations.
Consider unifying this logic. One approach could be to have the frontend send the raw text and let the backend always handle the parsing. This would remove the need for the complex parseExternalAgentFromText function in the frontend, making the system easier to maintain and evolve.
src/copaw/acp/permissions.py
Outdated
| def _build_summary( | ||
| self, | ||
| *, | ||
| tool_call: dict[str, Any], | ||
| tool_name: str, | ||
| tool_kind: str, | ||
| ) -> str: | ||
| target = ( | ||
| tool_call.get("path") | ||
| or tool_call.get("target") | ||
| or tool_call.get("command") | ||
| or tool_call.get("description") | ||
| or tool_call.get("input") | ||
| or "" | ||
| ) | ||
| target_text = str(target).strip() | ||
| if len(target_text) > 240: | ||
| target_text = target_text[:240] + "..." | ||
|
|
||
| lines = [ | ||
| f"等待外部 Agent 权限确认 / Waiting for external agent approval", | ||
| "", | ||
| f"- Harness: `{tool_call.get('harness') or 'external-agent'}`", | ||
| f"- Tool: `{tool_name}`", | ||
| f"- Kind: `{tool_kind or 'unknown'}`", | ||
| ] | ||
| if target_text: | ||
| lines.append(f"- Target: `{target_text}`") | ||
| lines.extend( | ||
| [ | ||
| "", | ||
| "可以在聊天里输入 `/approve` 批准,或发送任意消息拒绝。", | ||
| "You can type `/approve` to allow it, or send any other message to deny it.", | ||
| ], | ||
| ) | ||
| return "\n".join(lines) |
There was a problem hiding this comment.
The _build_summary function constructs a user-facing message with hardcoded English and Chinese text. This couples the backend with UI presentation concerns and makes it difficult to add support for new languages.
A better approach would be for the backend to return structured data or message keys (e.g., approval_request_summary with placeholders for harness, tool_name, etc.) and let the frontend be responsible for rendering and translating the text using its i18n framework. This would improve separation of concerns and simplify future internationalization efforts.
- Add /config/acp/parse-text API endpoint to unify external agent text parsing between frontend and backend (Gemini comment agentscope-ai#1) - Remove duplicated parseExternalAgentFromText() from Chat/index.tsx - Make _build_summary() return structured data for frontend i18n rendering instead of hardcoded text (Gemini comment agentscope-ai#2) - Add ACPApprovalSummary dataclass for type-safe approval data - Add i18n translation keys for approval UI (en/zh/ja/ru) - Add renderApprovalSummary() helper with backward compatibility - Fix code formatting with black and prettier - Fix flake8 E501 line too long error
- Add /config/acp/parse-text API endpoint to unify external agent text parsing between frontend and backend (Gemini comment agentscope-ai#1) - Remove duplicated parseExternalAgentFromText() from Chat/index.tsx - Make _build_summary() return structured data for frontend i18n rendering instead of hardcoded text (Gemini comment agentscope-ai#2) - Add ACPApprovalSummary dataclass for type-safe approval data - Add i18n translation keys for approval UI (en/zh/ja/ru) - Add renderApprovalSummary() helper with backward compatibility - Fix code formatting with black and prettier - Fix flake8 E501 line too long error
…into feat/acp-external-agent # Conflicts: # src/copaw/acp/permissions.py # src/copaw/app/routers/config.py
Add ACP feature to enable CoPaw invoking external agents via ACP protocol (e.g., OpenCode, Qwen-code, Gemini CLI).
Backend changes:
Frontend changes:
Follows Conventional Commits specification.
Description
This PR implements ACP (Agent Client Protocol) support, enabling CoPaw to invoke external coding agents (OpenCode, Qwen-code, Gemini CLI) via t
he ACP protocol. Users can now leverage specialized coding agents directly from CoPaw chat interface.
Key Features:
keep_sessionoption/acp opencode ...,/opencode ..., and natural language like "用 opencode 分析代码"Why:
CoPaw users often need specialized coding capabilities that external agents provide. Instead of switching between tools, users can now invoke them
seamlessly within CoPaw conversations while maintaining context.
Related Issue: Fixes #1059
Security Considerations:
require_approvalconfiguration - ACP tasks can require user confirmationType of Change
Component(s) Affected
Checklist
pre-commit run --all-fileslocally and it passespytestor as relevant) and they passTesting
Backend: