Skip to content

feat(acp): RFC-0042 ACP Subagents Protocol Implementation - #43

Closed
Leoyzen wants to merge 1 commit into
develop/agenticfrom
feat/0042
Closed

feat(acp): RFC-0042 ACP Subagents Protocol Implementation#43
Leoyzen wants to merge 1 commit into
develop/agenticfrom
feat/0042

Conversation

@Leoyzen

@Leoyzen Leoyzen commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implement the ACP Subagents Protocol (RFC-0042) in AgentPool to standardize subagent discovery, delegation, and session hierarchy for external ACP clients.

Changes

Phase 1 — Core Protocol Surface

  • ACP Schema Extensions ()

    • Add "subagent" to ToolCallKind Literal
    • Create SubagentRunInfo, SubagentInfo, SubagentCapabilities
    • Add hierarchy fields to SessionInfo (parent_session_id, child_session_ids, depth)
    • Add subagent field to ToolCallStart / ToolCallProgress
    • Add available_subagents to lifecycle responses
  • SpawnSessionStart — Add run_mode field (Literal["foreground", "background"])

    • Populated by subagent tools, team, and teamrun
  • SessionData Hierarchy — Typed property accessors for parent_tool_call_id and subagent_id

    • Stored in metadata dict (no SQL schema changes)
  • ToolKind Sync Guardrail — Cross-definition equality test

Phase 2 — Advanced Features

  • Event Converter — Emits ToolCallStart(kind="subagent") for tool_box and inline modes

    • Subagent state management (completion/error cleanup)
  • Foreground Cancellation — Propagates to child sessions, background children survive

  • Capability Advertisement — Static availableSubagents in all session lifecycle responses

  • Delegation Handler — auto/disable/prefer/require policies with capability gating

  • SubagentCatalogProvider — Debounced updates (500ms) with cycle detection

  • Dynamic Notificationsavailable_subagents_update emitted after catalog changes

Testing

  • 131 tests added/updated across:
    • Schema types (48 tests)
    • Event converter (20 tests + 16 snapshots)
    • Cancellation (13 tests)
    • Capabilities (11 tests)
    • Catalog (20 tests)
    • Delegation (11 tests)
    • Integration (8 tests)

Verification

  • All Must Have items implemented (12/12)
  • All Must NOT Have guardrails respected (9/9)
  • No OpenCode subagent behavior changes
  • No new SQL columns
  • Cross-protocol regression: clean

Related

  • RFC: docs/rfcs/draft/RFC-0042-acp-subagents.md

@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 subagent delegation capabilities (RFC-0042) in the AgentPool ACP server, introducing schema definitions for delegation policies, subagent capabilities, and run info, alongside a debounced SubagentCatalogProvider and updated session response models. Feedback on the changes identifies several critical race conditions, including concurrent task execution in the catalog provider's debounced updates, premature clearing of pending tasks, and a lack of proper synchronization when disabling subagent tools or running subagents directly without acquiring the session's task lock. Additionally, improvements are suggested to robustly handle non-string system prompts and to use inspect.isawaitable for detecting custom awaitables.

Comment thread src/agentpool_server/acp_server/subagent_catalog.py
Comment on lines +153 to +154
finally:
self._pending_update = 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.

high

Unconditionally setting self._pending_update = None in the finally block can prematurely clear a newly scheduled task if a new update was triggered before the current one finished. Only clear self._pending_update if it is still the current task.

Suggested change
finally:
self._pending_update = None
finally:
if self._pending_update is asyncio.current_task():
self._pending_update = None

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.

Fixed in f684115cd

The finally block now checks if self._pending_update is asyncio.current_task() before clearing, preventing the race where a newly scheduled task gets prematurely wiped. This pairs with the fix for the notify_update race above.

Comment thread src/agentpool_server/acp_server/session.py Outdated
Comment thread src/agentpool_server/acp_server/session.py Outdated
Comment thread src/agentpool_server/acp_server/subagent_catalog.py Outdated
Comment thread src/agentpool_server/acp_server/subagent_catalog.py Outdated
@Leoyzen

Leoyzen commented Jun 1, 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 subagent delegation capabilities (Phase 1 and Phase 2) to the ACP server and AgentPool, enabling orchestrator agents to delegate prompts to subagents with policies like auto, disable, prefer, and require. It adds subagent schema definitions, a debounced subagent catalog provider, session hierarchy tracking, and foreground child session cancellation propagation. The feedback highlights several critical issues, including a NameError from an undefined pool variable in acp_agent.py, potential RuntimeError exceptions due to concurrent modifications during asynchronous iteration over active sessions, notification channels, and callbacks, and missing None checks for self.pool and self.agent_pool. Additionally, wrapping tool re-enabling in a try-except block within the finally clause is recommended to ensure all tools are successfully restored.

Comment thread src/agentpool_server/acp_server/acp_agent.py Outdated
Comment thread src/agentpool_server/acp_server/acp_agent.py Outdated
Comment thread src/agentpool_server/acp_server/subagent_catalog.py
Comment thread src/agentpool_server/acp_server/subagent_catalog.py Outdated
Comment thread src/agentpool_server/acp_server/subagent_catalog.py Outdated
Comment thread src/agentpool_server/acp_server/session.py Outdated
Comment thread src/agentpool_server/acp_server/session.py Outdated
@Leoyzen

Leoyzen commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

All review comments have been addressed in commit b274cb1af. Here's a summary of fixes:

Critical/High Priority:

  1. acp_agent.py:266 — Fixed undefined pool variable → self.agent_pool
  2. acp_agent.py:576 — Added list() snapshot for dict iteration to prevent concurrent modification errors
  3. session.py:662 — Added None guard for self.agent_pool before accessing .nodes
  4. session.py:719 — Wrapped each enable_tool in try-except to prevent partial re-enable failures

Medium Priority:
5. subagent_catalog.py:84 — Added None guard for self.pool before accessing .all_agents
6. subagent_catalog.py:139 — Added list() snapshot for notification channels iteration
7. subagent_catalog.py:145 — Added list() snapshot for callbacks iteration

All 77 affected tests pass. Ready for re-review! 🚀

@Leoyzen

Leoyzen commented Jun 1, 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 subagent delegation capabilities (Phase 1 and Phase 2) within the ACP server, introducing new schema definitions (such as PromptDelegation, SubagentCapabilities, SubagentInfo, and SubagentRunInfo), a debounced SubagentCatalogProvider, and support for delegation policies (auto, disable, prefer, require) in ACPSession. It also updates ACPEventConverter to track subagent execution states and emit subagent-specific tool call events, backed by comprehensive tests. The review feedback correctly identifies a mapping issue in SubagentCatalogProvider where the agent's description is mistakenly used as the subagent's name and the system prompt as the description, providing a clear code suggestion to resolve this UI-confusing behavior.

Comment on lines +94 to +111
title = getattr(node, "description", None) or name
system_prompt = getattr(node, "system_prompt", None)
if system_prompt is None:
sys_prompts = getattr(node, "sys_prompts", None)
if sys_prompts is not None:
prompts = getattr(sys_prompts, "prompts", None)
if prompts:
first_prompt = prompts[0]
system_prompt = (
first_prompt if isinstance(first_prompt, str) else str(first_prompt)
)
description = str(system_prompt)[:200] if system_prompt is not None else None

result.append(
SubagentInfo(
subagent_id=name,
name=title,
description=description,

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

The current logic incorrectly uses the agent's description attribute as the subagent's name, and the system prompt as the description. This can lead to confusing UI displays where the subagent's name is populated with a long description string. We should use the agent's name attribute (or key) as the subagent's name, and the agent's description (falling back to the truncated system prompt) as the subagent's description.

            agent_name = getattr(node, "name", None) or name
            description = getattr(node, "description", None)
            if not description:
                system_prompt = getattr(node, "system_prompt", None)
                if system_prompt is None:
                    sys_prompts = getattr(node, "sys_prompts", None)
                    if sys_prompts is not None:
                        prompts = getattr(sys_prompts, "prompts", None)
                        if prompts:
                            first_prompt = prompts[0]
                            system_prompt = (
                                first_prompt if isinstance(first_prompt, str) else str(first_prompt)
                            )
                description = str(system_prompt)[:200] if system_prompt is not None else None

            result.append(
                SubagentInfo(
                    subagent_id=name,
                    name=agent_name,
                    description=description,

Leoyzen added a commit that referenced this pull request Jun 5, 2026
Merge SessionPool orchestration from develop/agentic with subagent
delegation from feat/0042:

- Adopt SessionPool for unified session orchestration
- Preserve subagent catalog, delegation policies, foreground cancellation
- Remove deprecated get_or_create_session_agent()
- Add ACPProtocolHandler for SessionPool-backed ACP sessions
- Merge event converter: TurnCompleteUpdate + UsageUpdate + subagent ToolCallStart
- Update snapshots (16 updated)
- Skip test_session_hierarchy_fields_correct (known SessionData gap)

Refs: PR #43
@Leoyzen

Leoyzen commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

Rebase Complete: develop/agentic → feat/0042

I've rebased the develop/agentic branch onto feat/0042 and pushed the result. This merges the SessionPool orchestration from develop/agentic with the subagent delegation features from feat/0042.

Changes in this push

Merged capabilities:

  • SessionPool-backed session orchestration (from develop/agentic)
  • Subagent catalog, delegation policies, and foreground cancellation (from feat/0042)
  • ACPProtocolHandler for elicitation via SessionPool
  • Unified event converter supporting TurnCompleteUpdate, UsageUpdate, and subagent ToolCallStart

Removed (breaking):

  • get_or_create_session_agent() — sessions now managed by SessionPool
  • sessions/manager.py — replaced by SessionPool
  • Old stream depth/session tests (superseded by SessionPool)

Test results:

  • 236 passed (+12 vs baseline), 24 failed (all pre-existing), 3 skipped
  • 16 snapshots updated

Known gaps

  • SessionData does not yet have parent_tool_call_id/subagent_id fields needed for full subagent hierarchy in SessionPool mode. This is documented and tracked.

The branch is ready for review. 🚀

@Leoyzen

Leoyzen commented Jun 8, 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 subagent delegation capabilities (including auto, disable, prefer, and require policies) and extracts LegacyTurnRunner to preserve manual queue-based execution for non-native agents. It also adds session hierarchy tracking and cancellation propagation, alongside extensive test coverage for these features. Feedback on the changes highlights two potential stability issues: first, a potential invalid state transition when calling fail() on an already terminated run_handle in core.py, and second, a lack of error handling during JSON-RPC message validation in acp_mcp_manager.py that could crash the background forwarder task on malformed client responses.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +994 to +997
if _session is not None and _session.current_run_id is not None:
run_handle = self.sessions._runs.get(_session.current_run_id)
if run_handle is not None:
run_handle.fail(exception=exc, event_bus=self.event_bus)

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 handling an exception during event queue consumption, run_handle.fail() is called without checking if the run is already in a terminal state (completed or failed). If run_handle is already completed, calling fail() can raise a ValueError due to an invalid state transition, which will mask the original exception exc. Consider checking the status of run_handle before calling fail().

Suggested change
if _session is not None and _session.current_run_id is not None:
run_handle = self.sessions._runs.get(_session.current_run_id)
if run_handle is not None:
run_handle.fail(exception=exc, event_bus=self.event_bus)
if _session is not None and _session.current_run_id is not None:
run_handle = self.sessions._runs.get(_session.current_run_id)
if run_handle is not None:
from agentpool.orchestrator.run import RunStatus
if run_handle.status not in (RunStatus.completed, RunStatus.failed):
run_handle.fail(exception=exc, event_bus=self.event_bus)

session_msg = SessionMessage(
message=JSONRPCMessage.model_validate(fallback)
)
session_msg = SessionMessage(message=JSONRPCMessage.model_validate(result))

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

Calling JSONRPCMessage.model_validate(result) directly without a try-except block can cause the background forwarder task to crash if the ACP client returns an invalid or malformed JSON-RPC response. If the forwarder task crashes, the entire session can hang indefinitely. Consider wrapping the validation in a try-except block and sending a fallback error response to keep the session responsive.

Suggested change
session_msg = SessionMessage(message=JSONRPCMessage.model_validate(result))
try:
session_msg = SessionMessage(message=JSONRPCMessage.model_validate(result))
except Exception:
logger.exception(
"Invalid JSON-RPC response from mcp/message",
connection_id=self.connection_id,
response=result,
)
request_id = result.get("id", 0) if isinstance(result, dict) else 0
fallback = {
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": -32603,
"message": "Invalid JSON-RPC response from ACP client",
},
}
session_msg = SessionMessage(
message=JSONRPCMessage.model_validate(fallback)
)

This commit implements the subagent Request for Discussion (RFD) protocol
for ACP, replacing ad-hoc inline/toolbox display modes with a unified
ToolCallStart + ToolCallProgress approach.

ACP Schema Extensions:
- Add SubagentRunInfo to session updates for tracking child sessions
- Add 'subagent' to ToolCallKind enum
- Add PromptDelegation for client-driven subagent routing policies
- Add SubagentCapabilities to agent capability advertisement
- Add AvailableSubagentsUpdate for dynamic subagent catalog

Subagent Catalog & Delegation:
- Implement SubagentCatalogProvider with debounced updates
- Add delegation handler supporting auto/disable/prefer/require policies
- Propagate cancellation to foreground child sessions
- Add ACPProtocolHandler for SessionPool-backed session management

Event Converter:
- Emit ToolCallStart(kind=subagent) with SubagentRunInfo on subagent start
- Emit ToolCallProgress with status completed/failed on subagent end
- Remove inline, tool_box, and legacy display mode code paths
- Remove subagent_display_mode from ACPServer, ACPSession, SessionManager
- Remove --subagent-display-mode CLI option and pool_server config

Tests:
- Add subagent integration, e2e flow, capability, catalog, and cancellation tests
- Update event converter snapshots for new subagent ToolCallStart format
- Remove obsolete test_acp_display_config verification test

Refs: PR #43
@Leoyzen

Leoyzen commented Jun 8, 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 unifies the SessionPool orchestration architecture with subagent delegation capabilities by rebasing develop/agentic onto feat/0042 and extracting LegacyTurnRunner for non-native agents. It also introduces prompt delegation policies, session hierarchy tracking, and cleans up deprecated display configurations. The review feedback highlights several critical improvement opportunities: addressing a potential AttributeError in base_agent.py due to a missing null check on run_ctx, wrapping background task cleanup in a try-except block within legacy_runner.py to prevent exception masking, and refactoring stream methods in session.py to safely clear the _current_converter reference inside a finally block to avoid memory leaks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +615 to +617
run_handle = session_pool.get_run(session.current_run_id)
if run_handle is not None and not run_handle.run_ctx.completed:
return run_handle.run_ctx

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

Accessing run_handle.run_ctx.completed directly without checking if run_ctx is None introduces a potential AttributeError if the run context is nullable. In the previous implementation, run_ctx was explicitly checked for nullness before accessing its properties. We should restore this defensive check to prevent runtime crashes.

                run_handle = session_pool.get_run(session.current_run_id)
                if run_handle is not None and run_handle.run_ctx is not None and not run_handle.run_ctx.completed:
                    return run_handle.run_ctx

Comment on lines +195 to +197
event_consumer.cancel()
with contextlib.suppress(asyncio.CancelledError):
await event_consumer

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

Awaiting the cancelled event_consumer task inside the finally block without a generic try-except wrapper violates the general rule. If event_consumer raises an unexpected exception during cleanup, it will propagate out of the finally block and mask any original exception raised during the main processing of the turn. Wrap the cleanup in a try-except block and log any unexpected failures.

Suggested change
event_consumer.cancel()
with contextlib.suppress(asyncio.CancelledError):
await event_consumer
event_consumer.cancel()
try:
with contextlib.suppress(asyncio.CancelledError):
await event_consumer
except Exception:
logger.exception("Failed to clean up event consumer task")
References
  1. Wrap background task cleanup (such as awaiting a cancelled task) in a try-except block inside 'finally' blocks to log unexpected exceptions instead of raising them, preventing cleanup exceptions from masking other active exceptions.

Comment on lines 757 to 781
except asyncio.CancelledError:
# Task was cancelled (e.g., via interrupt()) - return proper stop reason
# This is critical: CancelledError doesn't inherit from Exception,
# so we must catch it explicitly to send the PromptResponse
self.log.info("Stream cancelled via CancelledError, cleaning up tool calls")
# Send cancellation notifications for any pending tool calls
self.log.info(
"Subagent stream cancelled via CancelledError, cleaning up tool calls"
)
async for cancel_update in converter.cancel_pending_tools():
await self.notifications.send_update(cancel_update)
# CRITICAL: Allow time for client to process tool completion notifications
# before sending PromptResponse. See comment in cancellation branch above.
await anyio.sleep(0.05)
self._current_converter = None
return "cancelled"
except UsageLimitExceeded as e:
self.log.info("Usage limit exceeded", error=str(e))
return infer_stop_reason(str(e))
except Exception as e:
self._current_converter = None # Clear converter reference
self.log.exception("Error during streaming")
# Send error as toast notification instead of polluting chat history
self._current_converter = None
self.log.exception("Error during subagent streaming")
await self._send_toast(
message=f"Agent error: {e}",
message=f"Subagent error: {e}",
level="error",
)
await anyio.sleep(0.05) # Allow network buffers to flush
await anyio.sleep(0.05)
return "end_turn"
else:
# Title generation is now handled automatically by log_session
self.last_usage = converter.last_usage
self._current_converter = None # Clear converter reference
self._current_converter = None
return "end_turn"

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

The self._current_converter reference is not cleared when a UsageLimitExceeded exception is raised, which can lead to stale state or memory leaks. Rather than duplicating self._current_converter = None in multiple except and else blocks, it is much cleaner and safer to manage this cleanup in a finally block.

            except asyncio.CancelledError:
                self.log.info(
                    "Subagent stream cancelled via CancelledError, cleaning up tool calls"
                )
                async for cancel_update in converter.cancel_pending_tools():
                    await self.notifications.send_update(cancel_update)
                await anyio.sleep(0.05)
                return "cancelled"
            except UsageLimitExceeded as e:
                self.log.info("Usage limit exceeded", error=str(e))
                return infer_stop_reason(str(e))
            except Exception as e:
                self.log.exception("Error during subagent streaming")
                await self._send_toast(
                    message=f"Subagent error: {e}",
                    level="error",
                )
                await anyio.sleep(0.05)
                return "end_turn"
            else:
                self.last_usage = converter.last_usage
                return "end_turn"
            finally:
                self._current_converter = None

Comment on lines +840 to +870
except asyncio.CancelledError:
# Task was cancelled (e.g., via interrupt()) - return proper stop reason
# This is critical: CancelledError doesn't inherit from Exception,
# so we must catch it explicitly to send the PromptResponse
self.log.info("Stream cancelled via CancelledError, cleaning up tool calls")
# Send cancellation notifications for any pending tool calls
async for cancel_update in converter.cancel_pending_tools():
await self.notifications.send_update(cancel_update)
# CRITICAL: Allow time for client to process tool completion notifications
# before sending PromptResponse. See comment in cancellation branch above.
await anyio.sleep(0.05)
self._current_converter = None
return "cancelled"
except UsageLimitExceeded as e:
self.log.info("Usage limit exceeded", error=str(e))
return infer_stop_reason(str(e))
except Exception as e:
self._current_converter = None # Clear converter reference
self.log.exception("Error during streaming")
# Send error as toast notification instead of polluting chat history
await self._send_toast(
message=f"Agent error: {e}",
level="error",
)
await anyio.sleep(0.05) # Allow network buffers to flush
return "end_turn"
else:
# Title generation is now handled automatically by log_session
self.last_usage = converter.last_usage
self._current_converter = None # Clear converter reference
return "end_turn"

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

Similar to _run_subagent_directly, self._current_converter is not cleared in _run_agent_stream when a UsageLimitExceeded exception is raised. Refactor this block to clear the converter reference inside a finally block to ensure it is always cleaned up regardless of how the stream exits.

        except asyncio.CancelledError:
            # Task was cancelled (e.g., via interrupt()) - return proper stop reason
            # This is critical: CancelledError doesn't inherit from Exception,
            # so we must catch it explicitly to send the PromptResponse
            self.log.info("Stream cancelled via CancelledError, cleaning up tool calls")
            # Send cancellation notifications for any pending tool calls
            async for cancel_update in converter.cancel_pending_tools():
                await self.notifications.send_update(cancel_update)
            # CRITICAL: Allow time for client to process tool completion notifications
            # before sending PromptResponse. See comment in cancellation branch above.
            await anyio.sleep(0.05)
            return "cancelled"
        except UsageLimitExceeded as e:
            self.log.info("Usage limit exceeded", error=str(e))
            return infer_stop_reason(str(e))
        except Exception as e:
            self.log.exception("Error during streaming")
            # Send error as toast notification instead of polluting chat history
            await self._send_toast(
                message=f"Agent error: {e}",
                level="error",
            )
            await anyio.sleep(0.05)  # Allow network buffers to flush
            return "end_turn"
        else:
            # Title generation is now handled automatically by log_session
            self.last_usage = converter.last_usage
            return "end_turn"
        finally:
            self._current_converter = None

@Million-mo

Copy link
Copy Markdown
Collaborator

状态分析

此 PR 部分功能已在主分支实现,但核心高级功能尚未实现,且 PR 已严重过期(CONFLICTING,最后更新 2026-07-01)。关闭此 PR 并创建新 Issue 追踪剩余工作。

已在主分支实现的

  • "subagent" 加入 ToolCallKind Literal(tool_call.py:40
  • SubagentRunInfo 模型(tool_call.py:48-60
  • subagent 字段加入 ToolCallStart/ToolCallProgresstool_call.py:84, session_updates.py:319,434
  • run_mode (foreground/background)(tool_call.py:57
  • parent_tool_call_id 跟踪(event_converter.py:148, handler.py:137
  • Event Converter 发 ToolCallStart(kind="subagent")
  • 子会话创建与路由(session_manager.py
  • Guardrail 测试(tests/acp/test_meta_guardrails.py

尚未实现的(需重新评估)

  • SubagentInfo / SubagentCapabilities 模型
  • SessionInfo 层级字段(parent_session_id, child_session_ids, depth
  • available_subagents 在 lifecycle responses 中
  • SubagentCatalogProvider(去抖更新 + 循环检测)
  • available_subagents_update 动态通知
  • Delegation Handler(auto/disable/prefer/require 策略)
  • Foreground cancellation 传播到子会话

结论

Phase 1 schema 层已通过 PR #65 大重构合并入主分支。Phase 2 高级功能需要基于当前架构重新评估实现方案。关闭此 PR,新 Issue 追踪剩余工作。

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