Conversation
Closes #221. Runs selected tools as fire-and-forget asyncio tasks and delivers their results via the pending message queue (pydantic-ai #4980): the model gets an immediate ack and continues working; the real result is delivered as a follow-up message when the task completes. Default selector matches metadata['background']=True so users can opt tools in piecemeal, mark a whole MCP server with SetToolMetadata, or override entirely via the tools= parameter (matching CodeMode's pattern). Points pydantic-ai-slim at the 'background-tools' branch where the queue primitive lives until that PR merges to main.
| handler: Any, | ||
| ) -> Any: |
There was a problem hiding this comment.
🟡 handler: Any in wrap_run violates "no Any types" rule
The AGENTS.md coding standard explicitly requires "pyright strict mode -- no Any types, full type annotations". In wrap_run, the handler parameter and return type both use Any (lines 157-158), whereas the analogous wrap_tool_execute method correctly imports and uses the specific WrapToolExecuteHandler type from pydantic_ai.capabilities.abstract (_capability.py:21). There should be a corresponding WrapRunHandler type (or equivalent) imported and used here instead of Any.
Prompt for agents
In pydantic_ai_harness/background_tools/_capability.py, the wrap_run method uses `handler: Any` and `-> Any` on lines 157-158. The AGENTS.md rule mandates no Any types. The wrap_tool_execute method already correctly imports WrapToolExecuteHandler from pydantic_ai.capabilities.abstract. Check pydantic_ai.capabilities.abstract for a WrapRunHandler type (or similar) and import it in the TYPE_CHECKING block at line 21 to replace the Any usages. The return type should also be specified concretely (likely the agent run result type from the abstract base class signature).
Was this helpful? React with 👍 or 👎 to provide feedback.
| ToolCallPart, | ||
| ToolReturnPart, | ||
| ) | ||
| from pydantic_ai.models.function import AgentInfo, FunctionModel |
There was a problem hiding this comment.
🟡 Tests use FunctionModel instead of TestModel violating testing pattern rule
The AGENTS.md testing patterns section explicitly states: "Use pydantic_ai.models.TestModel for all tests (no real API calls)". All 6 test methods in test_background_tools.py use FunctionModel exclusively, while the existing code_mode tests primarily use TestModel and only use FunctionModel for 2 specific integration tests. While FunctionModel doesn't make real API calls, it directly violates the stated rule. TestModel should be the primary model used, with FunctionModel reserved for tests that absolutely need hand-driven model responses.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
|
||
| [tool.uv.sources] | ||
| pydantic-ai-slim = { git = 'https://github.com/pydantic/pydantic-ai.git', branch = 'main', subdirectory = 'pydantic_ai_slim' } | ||
| pydantic-ai-slim = { git = 'https://github.com/pydantic/pydantic-ai.git', branch = 'background-tools', subdirectory = 'pydantic_ai_slim' } |
There was a problem hiding this comment.
🚩 pydantic-ai-slim pinned to feature branch background-tools instead of main
The pyproject.toml source for pydantic-ai-slim was changed from branch = 'main' to branch = 'background-tools'. This is clearly intentional for development of this feature (the capability depends on APIs in that branch), but this PR should not be merged until the background-tools branch is merged into main in the pydantic-ai repo, otherwise the published package would depend on a feature branch that could be deleted or force-pushed.
Was this helpful? React with 👍 or 👎 to provide feedback.
Closes #221.
Summary
Adds the
BackgroundToolscapability that runs selected tools as fire-and-forget asyncio tasks. The agent receives an immediate acknowledgment string and keeps working; when the task completes, its result is delivered as a follow-up message via Pydantic AI's pending message queue (in-flight in pydantic/pydantic-ai#4980).Selector
Mirrors CodeMode's
tools=API — accepts the standardToolSelector:dict(default{'background': True}) — by metadataSequence[str]— by nameCallable[(ctx, td), bool]— predicate'all'— every toolComposes with
SetToolMetadataandFunctionToolset.with_metadata(...)to mark a whole MCP server / toolset as background.Implementation notes
for_runso concurrent runs don't share taskswrap_tool_executespawns the task and returns the ackafter_node_runwaits on at least one task before lettingEndpropagate, so the corePendingMessageDrainCapability(which runs after us in reverse order viawrapped_by=[PendingMessageDrainCapability]) finds a follow-up to drainwrap_runfinally cancels remaining tasks;asyncio.CancelledErrordoes not produce a spurious failure follow-upDependency note
[tool.uv.sources]pointspydantic-ai-slimat thebackground-toolsbranch (where the pending message queue lives) until pydantic/pydantic-ai#4980 merges. We'll switch back tomainonce that lands.Test plan
make lint && make typecheck && make testclean