PydanTask is an alpha harness for building deep, multi-step agents on top of Pydantic AI.
If you want agents that can plan, execute, self-critique, and ship a final artifact (not just chat), this gives you the backbone.
What you get:
- Dynamic task DAGs: a supervisor creates/patches a task graph at runtime
- Parallel execution: run dependency-satisfied tasks concurrently
- Critic QA + retries: failed tasks become
RERUNuntilmax_attempts, thenFAILED - Observability: optional tracing (Langfuse, Logfire, LangSmith)
- Recovery/auditability: optional event-sourced checkpointing (
events.jsonl+ summaries + large-result sidecars) - Extensibility: register your own capabilities via
CapabilityDescription
- Install:
pip install pydantask- Set env vars:
export OPENAI_API_KEY="..."
# Optional: enables Tavily web search; otherwise DuckDuckGo-based search is used
export TAVILY_API_KEY="..."- Run a minimal agent:
import asyncio
from pydantask.agents import DeepAgent
async def main() -> None:
agent = DeepAgent(
objective="Compare 3 open-source LLMs for local inference and recommend one.",
model="openai:gpt-4.1-mini", # or "anthropic:..." or pass a Model instance
trace=False,
checkpoint=False,
max_steps=10,
)
result = await agent.run()
print(result.final_result.detailed_output if result.final_result else result.errors)
if __name__ == "__main__":
asyncio.run(main())Alpha note: the core loop is working and tested, but the API and prompts are still evolving. If you hit rough edges, please open an issue with a minimal repro.
For deeper docs and API reference, see: pydantask.readthedocs.io
The core orchestrator is DeepAgent:
from pydantask.agents.agent import DeepAgentDeepAgent coordinates several built‑in agents:
- Supervisor – plans and chooses which tasks to run next, based on statuses and dependencies.
- Researcher – performs web/external research for tasks that need new information.
- Producer – synthesizes intermediate results into a final answer or artifact.
- Critic – evaluates task outputs and drives deterministic retry/fail transitions.
They all operate over a shared RuntimeState:
from pydantask.models import RuntimeState, TaskItem, TaskResult, PlanKey concepts:
- Plan (
Plan):reasoning_steps: planner’s internal notestasks: list ofTaskIteminstances
- TaskItem: one sub‑task in the plan, with:
task_id,overall_objective,sub_task_objectivecapability(which sub‑agent to use, e.g."research_agent")sub_task_dependencies(other task IDs that must complete first)status(TaskStatus:PENDING,READY,RUNNING,NEEDS_REVIEW,COMPLETED,FAILED,ERRORED,RERUN)result(TaskResult) andtask_feedback(TaskQAResult)
- RuntimeState:
plan: Dict[int, TaskItem]objective: strcapability_registry: Dict[str, CapabilityDescription](excluded from serialization)document_store,knowledge_store,runtime_steps, etc.
The control loop in DeepAgent.run():
- Supervisor incrementally builds a task DAG for the objective (via tools like
add_task). RuntimeStateis initialized with the capability registry.- In each cycle:
- Supervisor decides which tasks to execute next based on plan progress, task dependencies, and self reflection.
- Ready tasks, so long as dependencies are satisfied, are executed by the appropriate capability (sub‑agent).
- Critic reviews each result and produces a "QA" report for the supervisor to review if the task failed.
- Loop stops when:
- the Supervisor sets
all_tasks_completed = Trueand the run’s completion invariants are met (exactly one task is markedis_final=True, and that task isCOMPLETEDwith aTaskResult), or max_stepsis reached, or- the harness stops after several no-progress cycles (safety guardrail).
- the Supervisor sets
For more detail, see docs/agents.md.
PydanTask assumes you already have Pydantic AI and an OpenAI‑compatible model configured. A Tavily API key is optional for the built‑in research agent (it falls back to DuckDuckGo search if omitted).
From your project root:
pip install pydantask(or however you manage your environment; if you use Poetry, adjust accordingly.)
Set the following environment variables (e.g. in your shell or a .env file):
OPENAI_API_KEY– for the underlying OpenAIChatModel (or whatever your Pydantic AI provider expects).TAVILY_API_KEY– (optional) used by theresearch_agent(viatavily_search_tool). If this key is not set, it defaults to DuckDuckGo search.
Minimal example that creates a DeepAgent and runs it on a single objective:
import asyncio
from pydantask.agents.agent import DeepAgent
async def main() -> None:
agent = DeepAgent(
objective="Write an overview of ghost lights folklore and summarize scientific explanations.",
model="gpt-4.1-mini", # or any compatible OpenAIChatModel name
max_steps=10,
)
run_result = await agent.run()
runtime_state = run_result.runtime_state
# Inspect the final plan and results
for task_id, task in sorted(runtime_state.plan.items()):
print(f"Task {task_id} [{task.status}]: {task.sub_task_objective}")
if task.result is not None:
print(" Summary:", task.result.summary)
# The main long-form output for the task is stored in-memory:
print(" Detailed output:", (task.result.detailed_output or "<empty>"))
print()
if __name__ == "__main__":
asyncio.run(main())What this does:
- Constructs a
DeepAgentwith default Supervisor, Researcher, Producer, and Critic. - Supervisor (dynamic DAG architect) breaks down the objective into
TaskItems using built-in capabilities. - Supervisor picks tasks to run in each loop iteration.
- Researcher and Producer execute those tasks and return structured
TaskResults. - Critic evaluates each task result and marks tasks as:
COMPLETEDwhen QA passesRERUNwhen QA fails but retries remain (critic feedback is appended to the task objective)FAILEDwhen QA fails andmax_attemptsis exceeded
- When done, you get a
RuntimeStatewith the full plan and results.
Note: by default, this harness treats task artifacts as in-memory outputs (e.g.
TaskResult.detailed_output).However, it does support optional event-sourced checkpointing (
checkpoint=True) which persists an append-onlyevents.jsonllog (plus summaries and, when needed, sidecar JSON files for large results) under_checkpoint/.Filesystem tools exist in
pydantask.tools.default_tools, but they are not enabled by default in the built-in agents.
You can add custom sub‑agents or tools via CapabilityDescription and the sub_agents argument.
from pydantic_ai import Agent
from pydantask.agents.agent import DeepAgent
from pydantask.models import CapabilityDescription, TaskResult, TaskRunDeps
my_special_agent = Agent(
model=..., # e.g. the same OpenAIChatModel
name="_my_special_agent",
system_prompt="You are a specialized agent for security analysis.",
deps_type=TaskRunDeps, # gives tools access to deps.runtime_state + deps.task
output_type=TaskResult,
tools=[...], # tools should typically accept RunContext[TaskRunDeps]
)
custom_capability = CapabilityDescription(
name="security_agent", # used in TaskItem.capability
description="Performs security-focused analysis and risk assessment.",
tool_func=my_special_agent,
)
agent = DeepAgent(
objective="Assess the security posture of this web application.",
sub_agents=[custom_capability],
)
# Now the Planner can choose `security_agent` as a capability in the plan.DeepAgent expects a capability to be runnable (i.e. something with a .run(prompt, deps, usage_limits=...) method). For plain functions, wrap them with as_runner(...).
from pydantask.agents.agent import DeepAgent
from pydantask.capabilities.runner import as_runner
from pydantask.models import CapabilityDescription, TaskResult, TaskRunDeps
async def my_utility_capability(prompt: str, deps: TaskRunDeps) -> TaskResult:
# prompt is the task prompt; deps.runtime_state + deps.task give you context
return TaskResult(task_id=deps.task.task_id, summary="processed", detailed_output=prompt)
utility_capability = CapabilityDescription(
name="my_utility_tool",
description="Utility capability that processes a prompt and returns a TaskResult.",
tool_func=as_runner(my_utility_capability),
)
agent = DeepAgent(objective="Some goal...", sub_agents=[utility_capability])For more customization details, see:
docs/customization.mddocs/tools.mddocs/agents.md
Tests live under the test/ directory and are written to be compatible with both pytest and the standard library unittest.
From the repository root:
pip install pytest
pytestIf you prefer unittest, you can still run the suite with:
python -m unittest discover -s test -p "test_*.py"Make sure required environment variables (e.g. TAVILY_API_KEY, OPENAI_API_KEY) are set, or that tests patch them appropriately (as in test/test_agent.py).
