feat: PydanticAI thinning refactor - #69
Conversation
…y, tool thinning, event passthrough Replace ~1,500 lines of custom code that duplicates PydanticAI functionality with thin adapters delegating to native PydanticAI capabilities: - HooksCapabilityAdapter: bridges AgentPool hooks to pydantic_ai.capabilities.Hooks with priority combining (deny > ask > allow) - ProcessHistory: delegates to pydantic_ai.capabilities.ProcessHistory directly - Tool thinning: simplified Tool.to_pydantic_ai() conversion, removed ToolKind taxonomy and redundant metadata - Event passthrough: PartStartEvent/PartDeltaEvent use session_id via context instead of subclassing - PromptInjectionManager: inject/consume preserved for tool result augmentation; queue/pop_queued removed for native agents (delegated to PendingMessageDrainCapability) Added: - src/agentpool/agents/native_agent/hooks_capability_adapter.py - 116 regression tests (hook combining, process history, prompt injection, tool conversion, event subclasses) - RFC-0001, migration guide, CHANGELOG - OpenSpec change: pydanticai-thinning-refactor (all tasks complete) No behavioral changes — all existing tests pass at baseline rate. Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
There was a problem hiding this comment.
Code Review
This pull request implements a thinning refactor of AgentPool's integration with PydanticAI, delegating hook lifecycle management to PydanticAI's native capabilities via a new HooksCapabilityAdapter. It also adds comprehensive regression tests covering hook combining, process history, prompt injection, tool conversion, and event subclassing. The review feedback suggests robustifying coroutine detection in _execute_hook to support functools.partial and custom callables, replacing deprecated asyncio.get_event_loop() calls, and using explicit is not None checks instead of implicit truthiness checks on dictionary mappings.
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.
| if inspect.iscoroutinefunction(hook): | ||
| result = await hook(**kwargs) | ||
| else: | ||
| result = await asyncio.get_event_loop().run_in_executor(None, lambda: hook(**kwargs)) |
There was a problem hiding this comment.
Using inspect.iscoroutinefunction directly on the hook callable can fail to detect coroutines if the hook is a functools.partial wrapper or a custom callable object (an instance with a __call__ method). If undetected, calling the async hook synchronously or via run_in_executor will return an unawaited coroutine object, causing the hook to not execute and raising runtime warnings. Additionally, asyncio.get_event_loop() is deprecated in favor of asyncio.get_running_loop() within active async contexts. Avoid using hasattr to check for method or attribute existence; use explicit type checks like isinstance(func, Callable) instead.
| if inspect.iscoroutinefunction(hook): | |
| result = await hook(**kwargs) | |
| else: | |
| result = await asyncio.get_event_loop().run_in_executor(None, lambda: hook(**kwargs)) | |
| import functools | |
| from collections.abc import Callable | |
| func = hook | |
| while isinstance(func, functools.partial): | |
| func = func.func | |
| is_coro = inspect.iscoroutinefunction(func) or ( | |
| isinstance(func, Callable) | |
| and inspect.iscoroutinefunction(func.__call__) | |
| ) | |
| if is_coro: | |
| result = await hook(**kwargs) | |
| else: | |
| loop = asyncio.get_running_loop() | |
| result = await loop.run_in_executor(None, functools.partial(hook, **kwargs)) |
References
- Avoid using
hasattrto check for method or attribute existence if the project enforces strict type-safety rules. Use explicit type checks, protocols, or other type-safe alternatives instead.
| if not pattern.search(tool_name): | ||
| return False | ||
|
|
||
| if input_match: |
There was a problem hiding this comment.
Avoid using implicit truthiness checks on objects implementing __len__ or __bool__ (such as dictionaries/mappings) when checking for existence or nullness. Use an explicit is not None check instead to prevent unexpected behavior when the collection is instantiated but empty.
| if input_match: | |
| if input_match is not None: |
References
- Avoid using implicit truthiness checks (e.g.,
if pool:) to check for the existence or nullness of objects that inherit fromMutableMappingor implement__len__/__bool__. Since empty collections evaluate toFalsein a boolean context, such checks will fail when the object is instantiated but empty. Use explicitis not Nonechecks instead.
Leoyzen
left a comment
There was a problem hiding this comment.
.gitignore要更新下。docs 和 specs 是不是单独和 feature 来提交?
Closing: Superseded by #93This PR's goals have been substantially achieved by subsequent work on
The branch is 22 commits behind Action: Closing as superseded. The remaining thinning work (ToolKind removal, ResourceProvider migration) is tracked in |
Summary
Replace ~1,500 lines of custom code that duplicates PydanticAI functionality with thin adapters delegating to native PydanticAI capabilities. AgentPool becomes "thin" at the agent-engine layer (where PydanticAI is source of truth) and "thick" only at the orchestration layer (sessions, multi-agent composition, protocol servers, skills).
Changes
pydantic_ai.capabilities.Hookswith priority combining (deny > ask > allow), matcher filtering, and return type normalization.from_agent_hooks()factory for transparent migration from existingCallableHook/CommandHook/PromptHook.pydantic_ai.capabilities.ProcessHistorydirectly, removing customProcessHistoryAdapter(~200 lines of caching + signature validation)Tool.to_pydantic_ai()to 1:1 mapping, removedToolKindtaxonomy and redundant metadata fieldsPartStartEvent/PartDeltaEventusesession_idvia context instead of subclassinginject()/consume()preserved for tool result augmentation;queue()/pop_queued()removed for native agents (delegated toPendingMessageDrainCapability)Regression Tests
116 new tests covering:
All 116 tests pass on
develop/agentic.Documentation
docs/migration/pydanticai-thinning.mdpydanticai-thinning-refactor(all tasks complete)Deprecations (no breaking changes)
AgentHooks.as_capability()→ useHooksCapabilityAdapter.from_agent_hooks()HookABC hierarchy → thin adapters, remains for YAML configProcessHistoryAdapter→ usepydantic_ai.capabilities.ProcessHistorydirectlyToolKind→ string-based tool name patternsToolResult.structured_content→ PydanticAI nativeToolReturnPartStartEvent/PartDeltaEventsubclassing → useAgentContext/RunContext.depsTest Results
Co-authored-by: Sisyphus clio-agent@sisyphuslabs.ai