Skip to content

feat(rfc-0027): ACP subagent Zed compatibility - #42

Closed
Leoyzen wants to merge 10 commits into
develop/agenticfrom
feat/rfc-0027-acp-subagent-zed
Closed

feat(rfc-0027): ACP subagent Zed compatibility#42
Leoyzen wants to merge 10 commits into
develop/agenticfrom
feat/rfc-0027-acp-subagent-zed

Conversation

@Leoyzen

@Leoyzen Leoyzen commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

RFC-0027 implementation: ACP subagent Zed compatibility support.

Changes

  1. Bug fixes in event_converter.py

    • Fixed duplicate _current_message_id field declaration
    • Fixed duplicated reset() body
    • Fixed double reset() call on StreamCompleteEvent
  2. Zed mode type propagation

    • Added "zed" to subagent_display_mode across 7 files (server, CLI, config, session, agent)
  3. SubagentSessionInfo model and helpers

    • Added SubagentSessionInfo Pydantic model with session_id, message_start_index, message_end_index
    • Added _build_subagent_field_meta() helper for constructing _meta payload
  4. _meta filling in zed mode

    • SpawnSessionStart handler emits ToolCallStart with _meta.subagent_session_info and tool_name="task"
    • SubAgentEvent handler routes inner events and emits ToolCallProgress with _meta
    • Guardrails: _meta never leaks in non-zed modes (legacy/inline/tool_box)
  5. Child session creation and routing

    • SpawnSessionStart creates independent ACP subsession via session_manager.create_child_session()
    • SubAgentEvent inner events routed to child session's event loop
    • StreamCompleteEvent closes child session and emits parent completion
  6. Message index tracking

    • message_start_index=0 on SpawnSessionStart
    • message_end_index=count-1 on StreamCompleteEvent
  7. Independent tool_call_id for zed subagent

    • SpawnSessionStart generates a NEW tool_call_id instead of reusing PydanticAI's
    • Zed sees a distinct tool call bearing _meta.subagent_session_info
  8. Tests and snapshots

    • 29 event_converter tests (bug fixes, _meta, guardrails, index tracking, subsessions)
    • 4 zed snapshot tests with fixtures

Verification

  • pytest tests/test_event_converter.py - 29/29 pass
  • pytest tests/test_acp_event_converter_snapshots.py::TestZedModeSnapshots - 4/4 pass
  • Guardrail tests confirm no _meta leakage in non-zed modes

Related

Zed-side change required for full functionality: dynamic subagent loading via EntryUpdated event handling (separate PR).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new 'zed' subagent display mode for Zed editor compatibility, implementing child ACP subsession creation, event routing, and message index tracking. Feedback on the changes highlights several critical issues: a missing return statement in LocalResourceProvider.get_skill, potential premature garbage collection of asyncio tasks stored in local variables, incomplete initialization of ACPEventConverter display modes, database connection leaks in zed_provider when exceptions occur, potential session ID mismatches during child session creation, and semantically incorrect usage of TypeError instead of ValueError for syntax validation.

Comment on lines 125 to 127
try:
skill = self._registry.get(name)
self._cache[name] = skill

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The return skill statement was accidentally removed from the try block of get_skill. This causes the method to implicitly return None on success, violating the -> Skill return type annotation and causing runtime failures for any callers expecting a Skill object.

Suggested change
try:
skill = self._registry.get(name)
self._cache[name] = skill
try:
skill = self._registry.get(name)
self._cache[name] = skill
return skill

Comment thread src/agentpool/resource_providers/local.py Outdated
Comment thread src/agentpool/resource_providers/local.py Outdated
Comment on lines +864 to +866
child_converter = ACPEventConverter(
subagent_display_mode=self._display_mode
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Initializing child_converter with subagent_display_mode=self._display_mode is insufficient because _display_mode is a separate field that defaults to the environment variable value and is not automatically synchronized with subagent_display_mode during dataclass initialization. If the environment variable is not set to 'zed', the child converter will fall back to 'legacy' mode, breaking the Zed-compatible event routing. Pass _display_mode=self._display_mode explicitly to ensure the child converter runs in the correct mode.

                            child_converter = ACPEventConverter(
                                _display_mode=self._display_mode,
                                subagent_display_mode=self._display_mode,
                            )

Comment on lines +573 to +578
converter = ACPEventConverter(
subagent_display_mode=self.subagent_display_mode,
_session_manager=self.manager,
_parent_session_id=self.session_id,
_client=self.client,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When creating the main event converter, passing subagent_display_mode=self.subagent_display_mode does not automatically update the internal _display_mode field of ACPEventConverter if the environment variable ACP_SUBAGENT_DISPLAY_MODE is not set. To ensure the converter runs in the expected mode (especially 'zed'), pass _display_mode=self.subagent_display_mode explicitly.

Suggested change
converter = ACPEventConverter(
subagent_display_mode=self.subagent_display_mode,
_session_manager=self.manager,
_parent_session_id=self.session_id,
_client=self.client,
)
converter = ACPEventConverter(
_display_mode=self.subagent_display_mode,
subagent_display_mode=self.subagent_display_mode,
_session_manager=self.manager,
_parent_session_id=self.session_id,
_client=self.client,
)

Comment thread src/agentpool_storage/zed_provider/provider.py Outdated
Comment on lines +264 to +269
async def create_child_session(
self,
parent_session_id: str,
agent_name: str,
agent_type: str = "acp",
) -> str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If self._pool.sessions.create_child_session generates a new random session ID, it will mismatch with the child_session_id generated by the client/subagent and used in memory for event routing. Consider updating create_child_session to accept an optional child_session_id parameter so that the persistent session is created with the correct, matching ID.

    async def create_child_session(
        self,
        parent_session_id: str,
        agent_name: str,
        agent_type: str = "acp",
        child_session_id: str | None = None,
    ) -> str:

Comment thread src/agentpool/tools/base.py Outdated
@Leoyzen

Leoyzen commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an experimental 'zed' display mode for subagents, allowing compatibility with the Zed editor by tracking child ACP subsessions and routing subagent events accordingly. Feedback on the changes highlights a potential mismatch issue where the returned child_session_id is ignored, and a garbage collection risk in local.py where background tasks are assigned to local variables that immediately go out of scope. Additionally, suggestions were provided to catch broader exceptions for safer graceful degradation during child session creation and to clean up completed child session entries from tracking maps to prevent memory accumulation.

Comment thread src/agentpool_server/acp_server/event_converter.py Outdated
Comment thread src/agentpool/resource_providers/local.py Outdated
Comment thread src/agentpool/resource_providers/local.py Outdated
Comment thread src/agentpool_server/acp_server/event_converter.py Outdated
status="completed",
field_meta=meta,
)
self._child_sessions.pop(child_session_id, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When a child session completes, we should also pop its entries from self._subagent_message_counts and self._subagent_tool_map to prevent memory accumulation of completed subagent states during a single long-running stream.

Suggested change
self._child_sessions.pop(child_session_id, None)
self._child_sessions.pop(child_session_id, None)
self._subagent_message_counts.pop(child_session_id, None)
self._subagent_tool_map.pop(child_session_id, None)

@Leoyzen Leoyzen left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Comment Fixes

All review comments from the Gemini Code Assist review have been addressed in commit b79718e.

High Priority Fixes

  1. local.py task_ref garbage collection

    • Added self._background_tasks set to LocalResourceProvider
    • Both on_added and on_removed callbacks now store tasks and use add_done_callback
  2. zed_provider.py database connection leak

    • Replaced manual conn.close() with try/finally blocks
    • Connection guaranteed to close even if exceptions occur
  3. event_converter.py _display_mode propagation

    • child_converter and main converter now receive _display_mode explicitly
  4. event_converter.py child_session_id mismatch

    • create_child_session() return value captured as actual_child_session_id
    • Actual ID used in ACPNotifications and _ChildSessionState

Medium Priority Fixes

  1. event_converter.py exception handling

    • Changed to catch Exception broadly for safer graceful degradation
  2. event_converter.py cleanup mappings

    • Child session completion now cleans up _subagent_message_counts and _subagent_tool_map

Additional Changes

  • RFC Status Update: Moved RFC-0027 to implemented/ and updated status to IMPLEMENTED

Not Addressed

  • base.py TypeError: Already reverted to ValueError in previous commit
  • session_manager.py child_session_id parameter: Evaluated but not implemented - current design intentionally uses subagent-generated ID as mapping key while actual persisted ID is used for notifications

All 29 event converter tests pass.

@Leoyzen

Leoyzen commented May 31, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements RFC-0027 Wave 3, introducing compatibility with the Zed editor by adding a "zed" subagent display mode. This mode supports creating independent ACP child subsessions, routing subagent events to these child sessions, and tracking message indices to communicate session boundaries to the client. Additionally, the PR cleans up SQLite connection handling in provider.py by ensuring connections are closed in finally blocks, and prevents garbage collection of background tasks in local.py.

Feedback on the PR highlights two main areas for improvement:

  1. In session_manager.py, if initialize_mcp_servers() fails during session creation, the session's async environment remains entered, which can cause resource leaks. It is recommended to call session.close() in the except block.
  2. In event_converter.py, the fallback path for StreamCompleteEvent (when no child session exists) does not clean up child_session_id from _subagent_tool_map and _subagent_message_counts, potentially leading to memory accumulation. These should be popped upon completion.

Comment on lines 180 to +189
await session.initialize_mcp_servers()
self._active[session_id] = session
logger.info("Created ACP session", session_id=session_id, agent=session_agent.name)
return session_id
except Exception:
# Session creation failed - clean up the orphaned agent
try:
with contextlib.suppress(TypeError, AttributeError):
await acp_agent.remove_session_agent(session_id)
except (TypeError, AttributeError):
pass # Mock agent, no cleanup needed
raise
else:
return session_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If session.initialize_mcp_servers() fails after session.initialize() has succeeded, the session's async environment (self.acp_env) is left entered, which can cause resource leaks. Ensure that session.close() is called on failure if the session object was successfully instantiated.

Suggested change
await session.initialize_mcp_servers()
self._active[session_id] = session
logger.info("Created ACP session", session_id=session_id, agent=session_agent.name)
return session_id
except Exception:
# Session creation failed - clean up the orphaned agent
try:
with contextlib.suppress(TypeError, AttributeError):
await acp_agent.remove_session_agent(session_id)
except (TypeError, AttributeError):
pass # Mock agent, no cleanup needed
raise
else:
return session_id
await session.initialize_mcp_servers()
self._active[session_id] = session
logger.info("Created ACP session", session_id=session_id, agent=session_agent.name)
except Exception:
# Session creation failed - clean up the orphaned agent
if "session" in locals() and session is not None:
with contextlib.suppress(Exception):
await session.close()
with contextlib.suppress(TypeError, AttributeError):
await acp_agent.remove_session_agent(session_id)
raise
else:
return session_id

Comment on lines +958 to +974
case StreamCompleteEvent():
count = self._subagent_message_counts.get(
child_session_id, 0
)
message_end_index = (
count - 1 if count > 0 else None
)
meta = self._build_subagent_field_meta(
child_session_id,
tool_call_id,
message_end_index=message_end_index,
)
yield ToolCallProgress(
tool_call_id=tool_call_id,
status="completed",
field_meta=meta,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the fallback path (when no child session exists), once StreamCompleteEvent is processed, the subagent run is complete. However, the child_session_id is not popped from self._subagent_tool_map and self._subagent_message_counts. This can lead to memory accumulation and potential state desync. Pop them from both dictionaries to ensure consistency with the child session path.

                                case StreamCompleteEvent():
                                    count = self._subagent_message_counts.get(
                                        child_session_id, 0
                                    )
                                    message_end_index = (
                                        count - 1 if count > 0 else None
                                    )
                                    meta = self._build_subagent_field_meta(
                                        child_session_id,
                                        tool_call_id,
                                        message_end_index=message_end_index,
                                    )
                                    self._subagent_message_counts.pop(child_session_id, None)
                                    self._subagent_tool_map.pop(child_session_id, None)
                                    yield ToolCallProgress(
                                        tool_call_id=tool_call_id,
                                        status="completed",
                                        field_meta=meta,
                                    )

Leoyzen added 9 commits June 1, 2026 11:04
- SpawnSessionStart in zed mode now generates a NEW tool_call_id
  instead of reusing the PydanticAI-native tool_call_id
- This creates a distinct tool call bearing _meta.subagent_session_info
- Zed can recognize this as a subagent and load the child session
- Original PydanticAI tool call remains untouched

Related Zed-side change: dynamic subagent loading via EntryUpdated
…e providers

- Reverted accidental deletion of get_skills() from ResourceProvider base class
  which broke 18 callers across agent_routes, command_registry, aggregating, etc.
- Restored missing return skill in LocalResourceProvider.get_skill()

Fixes: serve-acp config loading error "Failed to import class"
…fications)

Reverted 50 files that contained:
- ruff auto-fix (import sorting, line length, TYPE_CHECKING moves)
- Exception type changes in tools/base.py (ValueError -> TypeError)
- Import removals in provider_router.py
- Various formatting and style changes

Kept only RFC-0027 core files:
- acp_server/ event_converter, session, session_manager, server, acp_agent
- pool_server config, serve_acp CLI
- Tests and snapshots
- resource_providers base.py and local.py (get_skills fix)
- Fix local.py task_ref garbage collection with _background_tasks set
- Fix zed_provider.py database connection leaks with finally blocks
- Fix event_converter.py child_session_id mismatch using returned ID
- Fix event_converter.py exception handling to catch Exception broadly
- Fix event_converter.py cleanup _subagent_message_counts and _subagent_tool_map on completion
- Fix session.py and event_converter.py _display_mode propagation
- Move RFC-0027 to implemented status
@Leoyzen
Leoyzen force-pushed the feat/rfc-0027-acp-subagent-zed branch from b79718e to e612edf Compare June 1, 2026 03:05
- fix(session_manager): add session.close() on initialization failure to prevent resource leaks
- feat(session_manager): allow passing explicit child_session_id to create_child_session for ID consistency
- fix(event_converter): clean up subagent state on StreamCompleteEvent fallback path to prevent memory accumulation
- fix(sessions/manager): accept optional child_session_id in create_child_session
@Leoyzen

Leoyzen commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review Comments 修复总结

已针对未解决的 review comments 提交修复(dab5d5bb3):

1. event_converter.py — child_converter _display_mode 显式传递 ✅

代码已在 rebase 后的版本中同时传递 _display_mode=self._display_modesubagent_display_mode=self._display_mode,确保子 converter 正确继承父 converter 的显示模式。

2. session.py — 主 converter _display_mode 显式传递 ✅

同上,已同时传递 _display_mode=self.subagent_display_modesubagent_display_mode=self.subagent_display_mode

3. session_manager.py:269create_child_session 接受可选 child_session_id

  • ACPSessionManager.create_child_session() 新增 child_session_id: str | None = None 参数
  • 透传给底层 SessionManager.create_child_session(),确保持久化 session ID 与客户端/事件路由使用的 ID 一致
  • event_converter.py 调用时传入 child_session_id=child_session_id

4. event_converter.py:935 — child session 完成时清理状态 ✅

代码已在 rebase 后版本中清理 _child_sessions_subagent_message_counts_subagent_tool_map 三个字典。

5. session_manager.py:189initialize_mcp_servers 失败时 session.close()

except Exception 块中新增:

if "session" in locals() and session is not None:
    with contextlib.suppress(Exception):
        await session.close()

确保 async environment 在初始化失败时被正确清理。

6. event_converter.py:971 — StreamCompleteEvent fallback 路径清理状态 ✅

在 no-child-session 的 StreamCompleteEvent fallback 分支中,yield ToolCallProgress 后新增:

self._subagent_message_counts.pop(child_session_id, None)
self._subagent_tool_map.pop(child_session_id, None)

防止长时间 stream 中已完成 subagent 的状态累积。


所有修改已通过单元测试验证(209 passed)。

已解决的 comments(如 get_skill return、_background_tasks GC、TypeErrorValueError 等)在 rebase 后的代码中已包含之前的修复。

@Million-mo

Copy link
Copy Markdown
Collaborator

状态分析

此 PR 的功能已全部在主分支上实现,PR 已过时。

具体情况

PR #42 实现 RFC-0027 ACP subagent Zed 兼容性,共 8 项核心变更。当前主分支已包含所有功能:

PR 声明的变更 主分支现状
SubagentSessionInfo 模型 event_converter.py:122
_build_subagent_field_meta() helper event_converter.py:235
subagent_display_mode 包含 "zed" ✅ 覆盖 6 个文件,类型 Literal["legacy", "zed", "qwen"],还额外扩展了 "qwen" 模式
SpawnSessionStart 在 zed 模式发 _meta.subagent_session_info event_converter.py 中实现
SubAgentEvent 内部事件路由 handler.py:167 处理 zed 模式子会话
独立 tool_call_id 生成 ✅ event converter 中实现
子会话创建与路由 session_manager.pycreate_child_session
消息索引跟踪 ✅ event converter 中跟踪

原因

2026-06-30 合并的 PR #65("refactor: eliminate pool-level agents, introduce Run/Turn separation, and overhaul EventBus + ACP session lifecycle")在大规模重构中直接将 Zed 兼容性功能整合进了新架构,并扩展了 "qwen" 模式。PR #43(RFC-0042)也进一步扩展了 subagent 协议。

结论

功能已被主分支完全覆盖,关闭此 PR。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants