RFC-0032: ACP Slash Commands Protocol Compliance - #33
Conversation
Align AgentPool ACP slash command advertisement with the official ACP specification by moving command declaration from initialize to session/update (available_commands_update). - Remove slash_commands from AgentCapabilities schema - Rely on existing session/update path after session creation - Update tests and documentation accordingly
There was a problem hiding this comment.
Code Review
This pull request introduces a draft RFC (RFC-0032) proposing to align AgentPool's ACP slash command advertisement with the official Agent Client Protocol (ACP) specification by moving the advertisement from the initialize handshake to the session/update notification. The reviewer's feedback focuses on correcting several outdated line number references to acp_agent.py and session.py throughout the RFC document. Additionally, the reviewer suggests explicitly marking the rejected design options (Option 2 and Option 3) as "Rejected" in their respective section headers to comply with repository guidelines and preserve historical context.
| 1. **`initialize` response** (`acp_agent.py:482-505`): The `initialize()` method builds `InitializeResponse` with `slash_commands=skill_commands`, populating `AgentCapabilities.slash_commands` in the JSON-RPC response. | ||
| 2. **`session/update` notification** (`session.py:562-572`): The `ACPSession.send_available_commands_update()` method sends `AvailableCommandsUpdate` via `ACPNotifications.update_commands()` after session creation. |
There was a problem hiding this comment.
The line numbers referenced for acp_agent.py and session.py are incorrect relative to the current codebase. In the actual implementation:
initialize()is located at lines303-326inacp_agent.py(not482-505).send_available_commands_update()is located at lines537-548insession.py(not562-572).
Please update these references to ensure the RFC is accurate.
| - `InitializeResponse.create(slash_commands=...)` (`agent_responses.py:284-339`) | ||
| - `AvailableCommandsUpdate` (`session_updates.py:354-363`) | ||
|
|
||
| **Current `initialize` response path** (`acp_agent.py:490-505`): |
| ) | ||
| ``` | ||
|
|
||
| **Current `session/update` path** (`acp_agent.py:547-550`, `621`, `732`): |
|
|
||
| - `capabilities.py:273`: `slash_commands: list[AvailableCommand]` defined on `AgentCapabilities` | ||
| - `agent_responses.py:301`: `InitializeResponse.create()` accepts `slash_commands` parameter | ||
| - `acp_agent.py:504`: `slash_commands=skill_commands` passed to `InitializeResponse.create()` |
|
|
||
| --- | ||
|
|
||
| ### Option 2: Deprecation with Fallback |
There was a problem hiding this comment.
According to the repository's general rules, when a design option discussed in an RFC is rejected, it must be explicitly marked as 'Rejected' with a reference to the decision to preserve historical context for future readers.
| ### Option 2: Deprecation with Fallback | |
| ### Option 2: Deprecation with Fallback (Rejected) |
References
- When a design option discussed in an RFC is rejected, explicitly mark it as 'Rejected' with a reference to the decision, rather than removing it, to preserve historical context for future readers.
|
|
||
| --- | ||
|
|
||
| ### Option 3: Keep Both Paths with Client Detection |
There was a problem hiding this comment.
According to the repository's general rules, when a design option discussed in an RFC is rejected, it must be explicitly marked as 'Rejected' with a reference to the decision to preserve historical context for future readers.
| ### Option 3: Keep Both Paths with Client Detection | |
| ### Option 3: Keep Both Paths with Client Detection (Rejected) |
References
- When a design option discussed in an RFC is rejected, explicitly mark it as 'Rejected' with a reference to the decision, rather than removing it, to preserve historical context for future readers.
| slash_commands=skill_commands, | ||
| ``` | ||
|
|
||
| The `get_skill_commands()` method on `AgentPoolACPAgent` is currently used **only** by `initialize()`. After removal, it becomes dead code and should be evaluated for deletion. Note: `ACPSession.send_available_commands_update()` does **not** call `get_skill_commands()` — it calls `self.get_acp_commands()` which operates on the session's `command_store` directly (`session.py:614-628`). |
| - `src/agentpool_server/acp_server/acp_agent.py:482-505` — `AgentPoolACPAgent.initialize()` | ||
| - `src/agentpool_server/acp_server/session.py:562-572` — `ACPSession.send_available_commands_update()` |
There was a problem hiding this comment.
The line numbers in the code references do not match the actual implementation in the codebase. Specifically:
AgentPoolACPAgent.initialize()is located at lines303-326inacp_agent.py(not482-505).ACPSession.send_available_commands_update()is located at lines537-548insession.py(not562-572).
Align AgentPool ACP slash command advertisement with the official ACP specification (RFC-0032): - Remove slash_commands field from AgentCapabilities schema - Remove slash_commands parameter from InitializeResponse.create() - Stop passing slash_commands in AgentPoolACPAgent.initialize() - Remove unused AvailableCommand import from agent_responses.py - Rewrite test_capabilities.py: remove slash_commands tests, add backward-compat deserialization test - Rewrite test_acp_skill_commands.py: verify initialize() does NOT expose slash_commands per ACP spec - Update docs/features/skill-commands.md: document session/update path
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request aligns AgentPool's ACP slash command advertisement with the official ACP specification by removing slash_commands from the AgentCapabilities and InitializeResponse schemas, and removing them from the initialize response in AgentPoolACPAgent. It also updates documentation and tests to reflect this change. However, the review highlights a critical bug where skill commands are now completely lost and never advertised because they are not registered at the session level or bridged into the session's command_store. Additionally, there is a gap in test coverage, and a new test should be added to verify that skill commands are correctly advertised via the session/update notification after session creation.
| logger.info("Client info", request=params.model_dump_json()) | ||
| self._initialized = True | ||
| skill_commands = self.get_skill_commands() | ||
| return InitializeResponse.create( |
There was a problem hiding this comment.
Critical Bug: Skill Commands are Completely Lost and Never Advertised
By removing slash_commands=skill_commands from the initialize response, skill commands are now completely lost and will never be advertised to the client or be executable.
Why this happens:
- No Session-Level Registration:
ACPSession.send_available_commands_update()only sends commands returned byself.get_acp_commands(), which lists commands fromself.command_store. However,self.command_storeis statically initialized withget_all_commands(), which does not include skill commands. - No Execution Path: Because skill commands are not registered in the session's
command_store, any attempt to execute them viaexecute_slash_commandwill fail asself.command_store.get_command(command_name)will returnNone. - Asynchronous Race Condition: In
new_session(),session.init_client_skills()is scheduled as an asynchronous background task. Even if there were a mechanism to register skill commands dynamically,send_available_commands_update()is scheduled concurrently and does not wait for skills to be loaded, nor doesinit_client_skills()trigger a command update notification once it completes.
Recommendation:
To align with the ACP specification while preserving skill command functionality, we must bridge skill commands into the session's command_store and trigger an update when they are loaded:
- Update
ACPSessionto listen to the pool'sskill_commandsregistry changes (similar to howAgentPoolACPAgentdoes via_setup_skill_bridge). - When skill commands are loaded or updated, register them dynamically into the session's
command_storeand callawait self.send_available_commands_update().
References
- Verify the direction and structure of protocol messages (e.g., Agent-to-Client vs. Client-to-Agent) against the official specification (e.g., RFD) rather than relying solely on existing code or initial schema definitions, as these may be semantically incorrect.
| async def test_initialize_does_not_expose_skill_commands( | ||
| mock_acp_agent_with_skills: AgentPoolACPAgent, | ||
| ): |
There was a problem hiding this comment.
Missing Test Coverage for session/update Skill Command Advertisement
The test test_initialize_exposes_skill_commands was renamed to test_initialize_does_not_expose_skill_commands and updated to assert that initialize does not expose skill commands. However, no corresponding test was added to verify that skill commands are indeed advertised via the session/update notification (available_commands_update) after session creation.
This leaves a critical gap in test coverage and hides the fact that skill commands are currently completely unadvertised and broken in the session lifecycle.
Recommendation:
Add a test (e.g., test_session_update_exposes_skill_commands) that creates a session and asserts that the client receives an available_commands_update notification containing the registered skill commands (e.g., "test-skill").
Per code review feedback on PR #33, skill commands were completely lost after removing slash_commands from initialize(). The session/update path (AvailableCommandsUpdate) only sent commands from ACPSession.command_store, which did not include skill commands. - Add _register_skill_commands() to ACPSession.__post_init__ - Subscribe to pool's SkillCommandRegistry changes via on_command_change - Convert SkillCommand -> SlashedCommand using create_skill_command (reusing OpenCode server's existing bridge logic) - Register/unregister skill commands in command_store dynamically - Trigger send_available_commands_update() when skill commands change - Add TDD test: test_session_update_exposes_skill_commands verifies skill commands are sent via session/update after session creation
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements RFC-0032, aligning AgentPool's ACP slash command advertisement with the official Agent Client Protocol (ACP) specification by moving slash command exposure from the global initialize handshake to the per-session session/update notification. It removes the slash_commands field from AgentCapabilities and InitializeResponse, registers skill commands dynamically within ACPSession, and updates documentation and tests accordingly. Feedback highlights a potential memory leak from not unregistering the skill command callback on session closure, redundant notification tasks triggered during initial registration, and the need to use the codebase's standard TaskManager instead of bypassing it with asyncio.get_event_loop().create_task().
- Memory leak: unregister skill command callback in close() to prevent ACPSession from being kept alive by the global registry - Redundant notifications: use _skill_commands_initializing flag to skip scheduling updates during initial on_command_change registration - TaskManager consistency: use self.acp_agent.tasks.create_task() instead of asyncio.get_event_loop().create_task()
rfc_id: RFC-0032
title: "ACP Slash Commands Protocol Compliance: Move from initialize to session/update"
status: DRAFT
author: yuchen.liu
reviewers:
status: completed
status: completed
created: 2026-05-26
last_updated: 2026-05-26
decision_date:
related_rfcs:
RFC-0032: ACP Slash Commands Protocol Compliance
Overview
This RFC proposes aligning AgentPool's ACP slash command advertisement with the official Agent Client Protocol (ACP) specification. Currently, AgentPool declares available slash commands during the
initializehandshake viaAgentCapabilities.slash_commands. The ACP specification mandates that slash commands be advertised after session creation through thesession/updatenotification withavailable_commands_update. This RFC outlines the migration path to removeslash_commandsfromAgentCapabilitiesand rely exclusively on the per-sessionsession/updatemechanism — which AgentPool already partially implements.Table of Contents
Background & Context
Current State
AgentPool's ACP server (
AgentPoolACPAgent) advertises slash commands in two places:initializeresponse (acp_agent.py:482-505): Theinitialize()method buildsInitializeResponsewithslash_commands=skill_commands, populatingAgentCapabilities.slash_commandsin the JSON-RPC response.session/updatenotification (session.py:562-572): TheACPSession.send_available_commands_update()method sendsAvailableCommandsUpdateviaACPNotifications.update_commands()after session creation.The schema layer supports both paths:
AgentCapabilities.slash_commands: list[AvailableCommand](capabilities.py:273)InitializeResponse.create(slash_commands=...)(agent_responses.py:284-339)AvailableCommandsUpdate(session_updates.py:354-363)Current
initializeresponse path (acp_agent.py:490-505):Current
session/updatepath (acp_agent.py:547-550,621,732):The
session/updatepath is already invoked afternew_session(),load_session(), andresume_session(), meaning AgentPool currently sends slash commands twice: once globally at initialization, and once per session.ACP Protocol Specification
The official ACP specification (
agent-client-protocol/docs/protocol/slash-commands.mdx) states:The spec provides this example:
{ "jsonrpc": "2.0", "method": "session/update", "params": { "sessionId": "sess_abc123def456", "update": { "sessionUpdate": "available_commands_update", "availableCommands": [...] } } }Key protocol requirements:
session/update, notinitializeGlossary
AgentCapabilitiesinitializeresponseAvailableCommandsUpdatesession/updateinitializeACPSkillBridgeProblem Statement
The Problem
AgentPool's current implementation violates the ACP protocol specification for slash command advertisement:
initialize(global/static) rather than aftersession/new(per-session/dynamic).AgentCapabilities.slash_commandsis not part of the official ACP spec for theinitializeresponse. While the field exists in AgentPool's schema, it has no equivalent in the protocol'sAgentCapabilitiesdefinition.Evidence
capabilities.py:273:slash_commands: list[AvailableCommand]defined onAgentCapabilitiesagent_responses.py:301:InitializeResponse.create()acceptsslash_commandsparameteracp_agent.py:504:slash_commands=skill_commandspassed toInitializeResponse.create()slash-commands.mdx:10: "After creating a session, the Agent MAY send a list of available commands via theavailable_commands_updatesession notification"slash-commands.mdx:71-73: "The Agent can update the list of available commands at any time during a session by sending anotheravailable_commands_updatenotification"Impact of Inaction
initialize-timeslash_commandsentirely, causing skill commands to be invisible until a session is created — but since some clients rely on the spec-compliantsession/updatepath, they will work.initialize-timeslash_commandswill break when AgentPool eventually aligns with the spec.Goals & Non-Goals
Goals (In Scope)
slash_commandsfromAgentCapabilitiesschema andInitializeResponsesession/update(AvailableCommandsUpdate)initialize-timeslash_commandsNon-Goals (Out of Scope)
ACPSkillBridgeor skill command discovery logicprocess_prompt/execute_slash_command)Success Criteria
initializeresponse no longer containsslash_commandsinagent_capabilitiesAgentCapabilitiesschema no longer has aslash_commandsfieldsession/send_available_commands_update()continues to work after session creationsession/updateas per specEvaluation Criteria
slash-commands.mdxslash_commandsfrominitializeslash_commands-related tests updatedOptions Analysis
Option 1: Complete Removal from
initialize(Recommended)Remove
slash_commandsentirely fromAgentCapabilities,InitializeResponse.create(), andAgentPoolACPAgent.initialize(). Rely solely on the existingsession/update(AvailableCommandsUpdate) path that is already invoked afternew_session,load_session, andresume_session.Advantages:
slash-commands.mdxsession/updatepath already exists and worksDisadvantages:
initialize-timeslash_commandswill no longer see commands until session creationtest_capabilities.pyand integration testsEvaluation Against Criteria:
session/update; most already doinitializereferencesEffort Estimate:
Risk Assessment:
session/update; verify during testingAgentCapabilitiesis internal to AgentPool; external clients parse JSONOption 2: Deprecation with Fallback
Keep
AgentCapabilities.slash_commandsbut set it to an empty list ininitialize(). Continue sending actual commands viasession/update. Add a deprecation comment/note indicating the field will be removed in a future release.Advantages:
Disadvantages:
AgentCapabilitiesat allEvaluation Against Criteria:
Effort Estimate:
Risk Assessment:
Option 3: Keep Both Paths with Client Detection
Retain
initialize-timeslash_commandsand add client capability detection: only sendslash_commandsininitializeif the client advertises that it does not supportsession/updatenotifications. Otherwise, rely onsession/update.Advantages:
Disadvantages:
session/updatesupportinitialize()logicEvaluation Against Criteria:
Effort Estimate:
Options Comparison Summary
Recommendation
Option 1: Complete Removal from
initialize.The
session/updatepath for slash commands is already fully implemented and tested. Removing theinitialize-time path is a net reduction in code and aligns AgentPool with the ACP specification. The risk of client breakage is low because:session/updatenotification is a baseline ACP requirement — all compliant clients must support itsession/updateafter every session creationinitialize-timeslash_commandsfor command discoveryAccepted Trade-offs
slash_commandsfield will be removed in the next minor release. No formal deprecation cycle is needed because the field is not part of the public ACP spec.initializewill need to adapt. This is considered acceptable because such a client would already be non-compliant with the ACP specification.Conditions
session/send_available_commands_update()must be verified to work correctly in integration testsTechnical Design
Architecture Overview
Key Components
1.
AgentCapabilitiesSchema ChangeFile:
src/acp/schema/capabilities.pyRemove:
Update
AgentCapabilities.create(): Removeslash_commandsparameter and its usage in the method body.2.
InitializeResponseSchema ChangeFile:
src/acp/schema/agent_responses.pyUpdate
InitializeResponse.create(): Removeslash_commandsparameter and its forwarding toAgentCapabilities.create().3.
AgentPoolACPAgent.initialize()UpdateFile:
src/agentpool_server/acp_server/acp_agent.pyRemove:
The
get_skill_commands()method onAgentPoolACPAgentis currently used only byinitialize(). After removal, it becomes dead code and should be evaluated for deletion. Note:ACPSession.send_available_commands_update()does not callget_skill_commands()— it callsself.get_acp_commands()which operates on the session'scommand_storedirectly (session.py:614-628).4. Test Updates
File:
tests/acp/schema/test_capabilities.pyTestAgentCapabilitiesSlashCommandstest methodsAgentCapabilitiesdoes not containslash_commandsafter deserialization from old JSON (backward compat)File:
tests/servers/acp_server/test_acp_skill_commands.pytest_initialize_exposes_skill_commands,test_initialize_without_skills_has_empty_commands) are fundamentally testing removed behavior. These must be rewritten or deleted, not merely updated with new assertions.initialize()returnsAgentCapabilitieswithoutslash_commands, and that commands are received viasession/updatenotification after session creation.Files:
tests/server/acp/test_skill_commands.py,tests/integration/test_skill_commands_e2e.pyinitialize-timeslash_commandssession/updateinstead5. Documentation Updates
File:
docs/features/skill-commands.mdinitialize-time command advertisementsession/updateImplementation Plan
Phase 1: Schema and Agent Layer Changes
Scope: Remove
slash_commandsfrom schema andinitialize()Files:
src/acp/schema/capabilities.pyslash_commandsfield fromAgentCapabilities; updatecreate()src/acp/schema/agent_responses.pyslash_commandsparameter fromInitializeResponse.create()src/agentpool_server/acp_server/acp_agent.pyslash_commands=skill_commandsfrominitialize()Duration: 0.5 day
Phase 2: Test Updates
Scope: Rewrite/delete tests that assert on
initialize-timeslash_commands; add tests forsession/updatepathFiles:
tests/acp/schema/test_capabilities.pyTestAgentCapabilitiesSlashCommands; add backward-compat deserialization testtests/servers/acp_server/test_acp_skill_commands.pyinitialize-time tests withsession/updatepath teststests/server/acp/test_skill_commands.pytests/integration/test_skill_commands_e2e.pysession/updatepath end-to-endDuration: 0.5–1 day
Phase 3: Documentation and Validation
Scope: Update docs and run full test suite
Files:
docs/features/skill-commands.mdinitializereferences; clarifysession/updatepathValidation:
pytest tests/acp/schema/pytest tests/server/acp/pytest tests/integration/test_skill_commands_e2e.pypytest tests/servers/acp_server/test_acp_skill_commands.pyDuration: 0.5 day
Rollback Strategy
Revert by restoring:
slash_commandsfield inAgentCapabilitiesslash_commandsparameter inInitializeResponse.create()slash_commands=skill_commandsinAgentPoolACPAgent.initialize()Review Findings
Metis Review (2026-05-26)
Ambiguities and AI Failure Points Identified:
Backward compatibility contradiction (Addressed): The RFC originally claimed backward compatibility as a goal while rejecting the only backward-compatible option (deprecation). The "Accepted Trade-offs" section has been updated to clarify that no formal deprecation cycle is needed because the field is not part of the public ACP spec, but implementers should verify no external consumers depend on it.
get_skill_commands()usage analysis error (Addressed): The original RFC incorrectly stated thatget_skill_commands()is used bysend_available_commands_update(). Code inspection showssend_available_commands_update()callsself.get_acp_commands()(session-level) instead.get_skill_commands()is only used byinitialize()and becomes dead code after removal. The Technical Design section has been corrected.Test scope underestimated (Addressed): The original RFC described test changes as "assertion updates." In reality,
tests/servers/acp_server/test_acp_skill_commands.pycontains tests whose entire premise isinitialize-time command exposure — these must be rewritten or deleted, not patched. The Implementation Plan now explicitly calls out test rewriting.Race condition:
session/updatetiming (Documented):send_available_commands_update()is scheduled as a background task (self.tasks.create_task()) after thesession/newresponse is returned. There is no ordering guarantee between the response and the notification. A fast client could query for commands before the async task runs. The current behavior is accepted as-is because:session/updateMissing edge cases (Added to Open Questions):
initialize: client gets zero commands (acceptable per spec)send_available_commands_update()sendsavailableCommands: []— this is spec-compliant_register_mcp_prompts_as_commands()and_register_prompt_hub_commands()Oracle Review (2026-05-26)
Technical Assessment:
Recommended approach is correct (Confirmed): The
session/updatepath is already fully implemented and robust. Removing theinitialize-time path is a net code reduction with zero new infrastructure needed.Schema safety verified (Confirmed):
AgentCapabilitiesinherits fromAnnotatedObject→ PydanticBaseModel. Pydantic v2 default isextra='ignore', so old JSON withslash_commandswill deserialize safely. However, implementers should add an explicit backward-compat test.Client impact: Low (Confirmed):
src/acp/client/has zero references toslash_commands. Major ACP clients (Zed, Toad) strictly follow the spec and already handlesession/update. The risk of breaking real clients is low.Missing: Deprecation warning phase (Recommendation): Oracle recommends a hybrid approach:
slash_commands=[]ininitialize(), keep field in schema, emitDeprecationWarningThis costs ~1 line (
warnings.warn(...)) and provides measurable safety. The RFC author has considered this and decided on hard removal due to the field being non-spec, but acknowledges the risk.InitializeResponse.create()docstring bug (Drive-by): The docstring incorrectly says "Create an instance of AgentCapabilities" — it creates anInitializeResponse. This pre-existing bug should be fixed as a drive-by.Dynamic command updates: Verified (Resolved):
session.py:582callssend_available_commands_update()after_register_mcp_prompts_as_commands().session.py:268-272handles nested ACP agent command updates. This is already complete.Criteria weighting: Appropriate (Confirmed): Protocol Compliance (Critical), Backward Compatibility (High), Minimality (High), Test Coverage (High), Documentation (Medium) are correctly weighted.
Oracle + Metis Consensus
get_skill_commands()should be evaluated for deletion after removalextra='ignore'onAnnotatedObjectbefore mergeOpen Questions
External client dependency on
initialize-timeslash_commandsinitialize?AgentCapabilitiesbackward compatibilityslash_commandskey must still deserialize without errors after removing the field.AnnotatedObjectinherits from PydanticBaseModel; Pydantic v2 default isextra='ignore', so unknown fields are dropped safely. This should still be verified with an explicit test.extra='ignore'onAnnotatedObjectDynamic command updates during session
session.py:582callssend_available_commands_update()after_register_mcp_prompts_as_commands(), andsession.py:268-272handles nested ACP agent command updates.Client verification
initialize-timeslash_commands? The ACP baseline spec (capabilities.py:207-208) states all agents MUST supportsession/update, so compliant clients are expected to handle it.get_skill_commands()dead codeslash_commandsfrominitialize(),AgentPoolACPAgent.get_skill_commands()has no remaining callers. It should be evaluated for deletion or retained if future features need it.Decision Record
Decision
Status: PENDING REVIEW
Date:
Approvers:
Decision Summary
[To be filled after review]
Key Discussion Points
Conditions of Approval
[To be filled after review]
References
Related Documents
Code References
src/acp/schema/capabilities.py:273—AgentCapabilities.slash_commandssrc/acp/schema/agent_responses.py:284-339—InitializeResponse.create()src/agentpool_server/acp_server/acp_agent.py:482-505—AgentPoolACPAgent.initialize()src/agentpool_server/acp_server/session.py:562-572—ACPSession.send_available_commands_update()src/acp/agent/notifications.py:336-339—ACPNotifications.update_commands()src/acp/schema/session_updates.py:354-363—AvailableCommandsUpdateExternal Resources