Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,14 +505,14 @@ AgentPool maintains two queue systems because native and non-native agents use d

Native agents drive execution through `RunExecutor`, which calls `agent_run.next(node)` in a loop. The bare `async for node in agent_run:` pattern does not fire `after_node_run` hooks, so `"when_idle"` messages would never drain. `RunExecutor` avoids this by using explicit `next()` calls.

**Non-native agents** (ACP, ClaudeCode, AGUI) do not use PydanticAI's agent loop. They communicate through subprocess JSON-RPC, Claude SDK, or HTTP/SSE. These agents use `TurnRunner`, which preserves the manual queue system:
**Non-native agents** (ACP, ClaudeCode, AGUI) do not use PydanticAI's agent loop. They communicate through subprocess JSON-RPC, Claude SDK, or HTTP/SSE. These agents continue using `LegacyTurnRunner`, which preserves the manual queue system:

- `_post_turn_injections` for immediate injections.
- `_post_turn_prompts` for follow-up prompts.
- `_process_queued_work()` and `_trigger_auto_resume()` for the auto-resume loop.
- `SessionState.turn_lock` for turn serialization.

`TurnRunner` creates `RunHandle` instances and registers them in `SessionController._runs` just like native runs. This gives the pool a unified view of all active execution.
`LegacyTurnRunner` creates `RunHandle` instances and registers them in `SessionController._runs` just like native runs. This gives the pool a unified view of all active execution.

#### RunHandle Lifecycle

Expand Down Expand Up @@ -560,7 +560,7 @@ The `RunExecutor` runs PydanticAI iteration in a background task and pushes even

**For all agents**, `inject()` and `consume()` handle tool result augmentation. When a tool finishes, `after_tool_execute` hooks call `consume()` to inject additional context into the conversation. If no tool runs, `flush_pending_to_queue()` moves unconsumed injections into the queued prompts.

**For non-native agents**, `queue()` and `pop_queued()` also handle follow-up prompts after a turn ends. `TurnRunner` drains these queues through `_process_queued_work()`.
**For non-native agents**, `queue()` and `pop_queued()` also handle follow-up prompts after a turn ends. `LegacyTurnRunner` drains these queues through `_process_queued_work()`.

**For native agents**, the follow-up prompt queue (`queue()` / `pop_queued()`) is replaced by PydanticAI's `PendingMessageDrainCapability`. `inject()` / `consume()` remain in use for tool augmentation.

Expand Down
16 changes: 9 additions & 7 deletions docs/rfcs/RFC-0001-unified-run-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ AgentPool's session orchestration lives in `src/agentpool/orchestrator/core.py`,

**Description**:
- Phase 1: Build `RunHandle` managed by `SessionPool._runs` for all agent types, keeping existing manual queues
- Phase 2: Migrate native agents to PydanticAI `enqueue()`, unify non-native agents into `TurnRunner`
- Phase 2: Migrate native agents to PydanticAI `enqueue()`, extract `LegacyTurnRunner` for non-native agents

**Advantages**:
- **Lower risk**: Phase 1 validates run tracking without changing execution semantics
Expand All @@ -104,7 +104,7 @@ AgentPool's session orchestration lives in `src/agentpool/orchestrator/core.py`,
**Disadvantages**:
- **Two-phase complexity**: Requires maintaining both old and new paths during transition
- **Timeline**: Takes longer than a single-phase approach
- **Legacy code**: Non-native agent queue logic persists in `TurnRunner` (necessary; non-native agents cannot use PydanticAI)
- **Legacy code**: `LegacyTurnRunner` persists indefinitely for non-native agents

**Evaluation Against Criteria**:

Expand Down Expand Up @@ -217,7 +217,7 @@ AgentPool's session orchestration lives in `src/agentpool/orchestrator/core.py`,
- Sets foundation for future pool-level orchestration features (load balancing, circuit breakers, monitoring)

**Acknowledged trade-offs**:
- Non-native agent queue logic persists in `TurnRunner` (necessary; non-native agents cannot use PydanticAI)
- `LegacyTurnRunner` persists indefinitely for non-native agents (necessary; non-native agents cannot use PydanticAI)
- Two queue systems create some cognitive overhead (mitigated by clear documentation and agent-type-aware routing)
- Phase 2 requires a successful event mapping prototype before proceeding

Expand All @@ -243,7 +243,8 @@ AgentPool's session orchestration lives in `src/agentpool/orchestrator/core.py`,
│ │ │ │ │ │
│ │ │ receive_request(session_id, content, priority) │ │ │
│ │ │ ├─ Native agent? ──► _create_run() or enqueue() │ │ │
│ │ │ └─ Non-native? ───► TurnRunner.inject_prompt() │ │ │
│ │ │ └─ Non-native? ───► TurnRunner.inject_prompt() │ │ │
│ │ │ (LegacyTurnRunner in P2) │ │ │
│ │ │ │ │ │
│ │ │ _create_run() → RunHandle → add to _runs │ │ │
│ │ │ _cleanup_run() → remove from _runs │ │ │
Expand Down Expand Up @@ -443,9 +444,10 @@ run task finally block:
- [ ] Map PydanticAI events to AgentPool EventBus
- [ ] Preserve isolated `agent_iteration_task` pattern

2. **TurnRunner Unification**
- [x] Non-native queue logic already in `TurnRunner`; verified integration with `SessionPool._runs`
- [x] `turn_lock` preserved for turn serialization
2. **LegacyTurnRunner Extraction**
- [ ] Extract non-native queue logic into `LegacyTurnRunner`
- [ ] Ensure `LegacyTurnRunner` integrates with `SessionPool._runs`
- [ ] Keep `turn_lock` for turn serialization

3. **Native Agent Queue Migration**
- [ ] Remove manual follow-up prompt queue for native agents
Expand Down
40 changes: 0 additions & 40 deletions openspec/changes/archive/2026-06-05-acp-elicitation/tasks.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
## 1. Schema Changes

- [x] 1.1 Add `turn_complete: bool | None = False` field to `ClientCapabilities` in `src/acp/schema/capabilities.py`
- [x] 1.2 Update `ClientCapabilities.create()` factory method to accept `turn_complete` parameter
- [x] 1.3 Verify `ClientCapabilities` serialization/deserialization handles the new field correctly
- [ ] 1.1 Add `turn_complete: bool | None = False` field to `ClientCapabilities` in `src/acp/schema/capabilities.py`
- [ ] 1.2 Update `ClientCapabilities.create()` factory method to accept `turn_complete` parameter
- [ ] 1.3 Verify `ClientCapabilities` serialization/deserialization handles the new field correctly

## 2. Capability Negotiation

- [x] 2.1 Update `AgentPoolACPAgent.initialize()` to only advertise `turn_complete=True` when `client_capabilities.turn_complete` is truthy
- [x] 2.2 Ensure `AgentPoolACPAgent.initialize()` stores `client_capabilities` for later use (if not already stored)
- [ ] 2.1 Update `AgentPoolACPAgent.initialize()` to only advertise `turn_complete=True` when `client_capabilities.turn_complete` is truthy
- [ ] 2.2 Ensure `AgentPoolACPAgent.initialize()` stores `client_capabilities` for later use (if not already stored)

## 3. ACPEventConverter Changes

- [x] 3.1 Add `client_supports_turn_complete: bool = False` parameter to `ACPEventConverter.__init__` in `src/agentpool_server/acp_server/event_converter.py`
- [x] 3.2 Update `StreamCompleteEvent` branch in `convert()` to only yield `TurnCompleteUpdate` when `self.client_supports_turn_complete` is True
- [x] 3.3 Update `reset()` to preserve the `client_supports_turn_complete` flag across resets
- [ ] 3.1 Add `client_supports_turn_complete: bool = False` parameter to `ACPEventConverter.__init__` in `src/agentpool_server/acp_server/event_converter.py`
- [ ] 3.2 Update `StreamCompleteEvent` branch in `convert()` to only yield `TurnCompleteUpdate` when `self.client_supports_turn_complete` is True
- [ ] 3.3 Update `reset()` to preserve the `client_supports_turn_complete` flag across resets

## 4. Legacy Session Path Updates

- [x] 4.1 Update `ACPSession.process_prompt()` in `src/agentpool_server/acp_server/session.py` to pass `client_supports_turn_complete` when creating `ACPEventConverter`
- [x] 4.2 Derive the flag from `self.client_capabilities.turn_complete`
- [ ] 4.1 Update `ACPSession.process_prompt()` in `src/agentpool_server/acp_server/session.py` to pass `client_supports_turn_complete` when creating `ACPEventConverter`
- [ ] 4.2 Derive the flag from `self.client_capabilities.turn_complete`

## 5. SessionPool Path Updates

- [x] 5.1 Update `ACPProtocolHandler.__init__` to store `client_capabilities` (or derive a boolean flag)
- [x] 5.2 Update `ACPProtocolHandler.handle_prompt()` to block `PromptResponse` until run completion when client does NOT support `turn_complete`
- [x] 5.3 Use `asyncio.wait_for()` with a timeout (e.g., 60s) when awaiting run completion to prevent deadlocks
- [x] 5.4 Update `ACPProtocolHandler._event_consumer_loop()` to pass `client_supports_turn_complete` when creating per-session `ACPEventConverter`
- [ ] 5.1 Update `ACPProtocolHandler.__init__` to store `client_capabilities` (or derive a boolean flag)
- [ ] 5.2 Update `ACPProtocolHandler.handle_prompt()` to block `PromptResponse` until run completion when client does NOT support `turn_complete`
- [ ] 5.3 Use `asyncio.wait_for()` with a timeout (e.g., 60s) when awaiting run completion to prevent deadlocks
- [ ] 5.4 Update `ACPProtocolHandler._event_consumer_loop()` to pass `client_supports_turn_complete` when creating per-session `ACPEventConverter`

## 6. Testing

- [x] 6.1 Add test for `ClientCapabilities` with `turn_complete=True` and `turn_complete=False`
- [x] 6.2 Add test for `AgentPoolACPAgent.initialize()` advertising `turn_complete` only when client supports it
- [x] 6.3 Add test for `ACPEventConverter` emitting `TurnCompleteUpdate` only when flag is True
- [x] 6.4 Add test for `ACPProtocolHandler.handle_prompt()` blocking behavior for legacy clients
- [x] 6.5 Add test for `ACPProtocolHandler.handle_prompt()` non-blocking behavior for modern clients
- [x] 6.6 Run existing ACP server tests to ensure no regressions
- [ ] 6.1 Add test for `ClientCapabilities` with `turn_complete=True` and `turn_complete=False`
- [ ] 6.2 Add test for `AgentPoolACPAgent.initialize()` advertising `turn_complete` only when client supports it
- [ ] 6.3 Add test for `ACPEventConverter` emitting `TurnCompleteUpdate` only when flag is True
- [ ] 6.4 Add test for `ACPProtocolHandler.handle_prompt()` blocking behavior for legacy clients
- [ ] 6.5 Add test for `ACPProtocolHandler.handle_prompt()` non-blocking behavior for modern clients
- [ ] 6.6 Run existing ACP server tests to ensure no regressions

## 7. Documentation & Cleanup

- [x] 7.1 Update any inline comments or docstrings referencing unconditional `turn_complete` behavior
- [x] 7.2 Verify all modified files pass `ruff check` and type checking
- [ ] 7.1 Update any inline comments or docstrings referencing unconditional `turn_complete` behavior
- [ ] 7.2 Verify all modified files pass `ruff check` and type checking
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-05
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Context

`develop/agentic` (2 commits ahead of base) and `feat/0042` (11 commits ahead of base) diverged from `bcd8ac876`. Both modify the ACP server core files but for orthogonal reasons:

- **develop/agentic** replaces per-session agent creation with `SessionPool` orchestration, adds `ACPProtocolHandler` as an event consumer bridge, and gates everything behind a `use_session_pool` canary flag.
- **feat/0042** adds subagent delegation (`PromptDelegation` policies), subagent catalog advertisement, foreground child session cancellation, and `ToolCallStart(kind="subagent")` event conversion.

The rebase must produce a single branch where SessionPool mode and subagent delegation coexist. The key insight is that `ACPProtocolHandler.handle_prompt()` (SessionPool path) currently bypasses `ACPSession.process_prompt()` entirely, so subagent delegation logic must be extracted and made available to both paths.

## Goals / Non-Goals

**Goals:**
- Rebase `develop/agentic` onto `feat/0042` with clean history (2 commits replayed on top of 11)
- Resolve all merge conflicts in ACP server files without losing functionality from either branch
- Preserve backward compatibility: legacy path (`use_session_pool=false`) continues to work exactly as in feat/0042
- Ensure all existing tests pass after rebase; fix or delete tests referencing removed APIs

**Non-Goals:**
- Port subagent delegation into `ACPProtocolHandler` in this change (tracked separately by `subagent-delegation-session-pool-compat` spec)
- Migrate opencode_server or other protocols to SessionPool
- Add new subagent features beyond what already exists in feat/0042
- Preserve the `_session_agents` registry — it is intentionally removed by develop/agentic

## Decisions

### Decision 1: `acp_agent.py` — keep both `_protocol_handler` and `_catalog_provider`
- **Rationale**: `_protocol_handler` (SessionPool bridge) and `_catalog_provider` (subagent catalog) are orthogonal responsibilities. The class is a dataclass with no `__slots__`, so adding both fields is safe.
- **Alternative considered**: Merge them into a single provider. **Rejected** — they have different lifecycles (protocol handler is optional/config-gated; catalog is always present).

### Decision 2: Accept develop/agentic's removal of per-session agent APIs
- **Rationale**: `SessionPool` replaces the per-session agent lifecycle. Keeping both systems would create duplicate state and confusion.
- **Migration**: Any code calling `get_or_create_session_agent()` must use `pool.all_agents[agent_name]` directly, or rely on `SessionPool.create_session()`.

### Decision 3: `event_converter.py` — merge both event handling paths
- **Rationale**: The two branches touch different event types (`StreamCompleteEvent` vs `SpawnSessionStart`) but both modify the converter's state management (`reset()`). We need both `TurnCompleteUpdate` emission AND subagent `ToolCallStart` conversion.
- **Conflict resolution order**: Apply feat/0042's subagent changes first, then layer develop/agentic's `TurnCompleteUpdate` and `UsageUpdate` always-yield logic on top.

### Decision 4: `session_manager.py` — adopt develop's SessionPool child session path, keep feat/0042's `cancel_session()`
- **Rationale**: `cancel_session()` is a simple delegation method used by foreground child cancellation in `ACPSession`. It does not conflict with SessionPool's child session creation logic.

### Decision 5: Delete `test_acp_per_session_agent_red_flags.py`
- **Rationale**: The entire file tests `get_or_create_session_agent()`, which is removed. No replacement tests are needed — SessionPool has its own test coverage.

## Risks / Trade-offs

- **[Risk]** `ACPSession.process_prompt()` subagent delegation does not work when `use_session_pool=true` because `ACPProtocolHandler.handle_prompt()` bypasses `ACPSession` entirely.
- **Mitigation**: Documented as a known gap. The spec `subagent-delegation-session-pool-compat` tracks the follow-up work.
- **[Risk]** Test coverage for subagent catalog + SessionPool interaction is missing.
- **Mitigation**: Add integration tests in `tests/servers/acp_server/` that verify catalog updates are still sent when SessionPool is active.
- **[Risk]** The rebase may introduce subtle bugs in event ordering (converter state is now mutated by both branches' logic).
- **Mitigation**: Run the snapshot test suite (`test_acp_event_converter_snapshots.py`) before and after; any diff indicates an ordering regression.

## Migration Plan

1. **Pre-rebase**: Ensure `feat/0042` branch is clean (all tests pass on current branch).
2. **Rebase**: `git rebase develop/agentic` onto `feat/0042` — resolve 4-file conflicts.
3. **Post-rebase fixes**: Delete `test_acp_per_session_agent_red_flags.py`, fix `test_acp_session_manager_child_session.py` for SessionPool, run full test suite.
4. **Validation**: Run `pytest tests/servers/acp_server/ tests/acp_server/ -v` and confirm green.

## Open Questions

- Should `ACPProtocolHandler.handle_prompt()` be extended to support `PromptDelegation` in this change, or deferred?
- Do any opencode_server tests break due to `_session_agents` removal, and if so, should they be fixed here or in a separate change?
Loading
Loading