Plan Approval Callback
Async plan-approval callbacks that work from inside a running event loop
ApprovalCallback gates a plan before it runs, and its async approve_fn now works even when called from inside a running event loop — such as a FastAPI async route.
Quick Start
- Attach an async approval function
Pass an async
approve_fntoApprovalCallback. It returnsTrueto approve,Falseto reject.python from praisonaiagents.planning import ApprovalCallback async def review_plan(plan) -> bool: # e.g. call a Slack / email / DB check return await my_async_approval_check(plan) approval = ApprovalCallback(approve_fn=review_plan) - Works inside a running loop
The synchronous
__call__can now be reached from inside a running loop without crashing — the asyncapprove_fnruns on the caller’s loop.python import asyncio from praisonaiagents.planning import ApprovalCallback, Plan async def review_plan(plan) -> bool: return True # your async check here approval = ApprovalCallback(approve_fn=review_plan) async def main(): plan = Plan(name="Weekly report") # Reached from inside a running loop — no new-loop crash. approved = approval(plan) print("approved" if approved else "rejected") asyncio.run(main())
How It Works
The sync __call__ detects an async approve_fn and forwards it through the package’s shared safe bridge run_coroutine_from_any_context, which detects a running loop and schedules the coroutine on it instead of spinning up a fresh loop.
| Path | Behaviour |
|---|---|
Sync approve_fn | Called directly and its result used |
Async approve_fn | Routed through the shared safe bridge — runs on the caller’s loop |
No approve_fn, non-interactive | Auto-approves (CI / scripts) |
No approve_fn, interactive TTY | Requires explicit approval |
FastAPI async route
Embed PraisonAI in a FastAPI async endpoint and the plan-approval prompt no longer raises RuntimeError when the plan is presented.
from fastapi import FastAPI
from praisonaiagents import Agent, PlanningConfig
from praisonaiagents.planning import ApprovalCallback
app = FastAPI()
async def review_plan(plan) -> bool:
# e.g. call a Slack / email / DB check
return await my_async_approval_check(plan)
approval = ApprovalCallback(approve_fn=review_plan)
@app.post("/plan")
async def run_plan():
agent = Agent(
name="Planner",
instructions="Solve step by step",
planning=PlanningConfig(auto_approve=False),
)
# Works inside the route's running loop — no new-loop crash.
return await agent.astart("Plan the weekly report")What the user experiences
- A user hits a FastAPI endpoint that runs
await agent.astart("..."). - The planning phase produces a plan;
ApprovalCallbackis invoked. - The user-supplied async
approve_fnruns on the caller’s loop (no new-loop crash). - The plan proceeds if approved, or halts if the callback returns
False.
Common Patterns
Async approval
Route approval through any async check — a database lookup, an HTTP call, a message-queue round-trip.
from praisonaiagents.planning import ApprovalCallback
async def review_plan(plan) -> bool:
return await my_async_approval_check(plan)
approval = ApprovalCallback(approve_fn=review_plan)Sync approval
A plain sync function still works unchanged.
from praisonaiagents.planning import ApprovalCallback
def review_plan(plan) -> bool:
return len(plan.steps) <= 5
approval = ApprovalCallback(approve_fn=review_plan)Auto-approve
Skip prompts entirely for trusted, unattended runs.
from praisonaiagents.planning import ApprovalCallback
approval = ApprovalCallback(auto_approve=True)Best Practices
Prefer an async approve_fn for I/O-bound checks
When your approval decision calls out to Slack, a database, or an HTTP endpoint, use an async approve_fn so it runs on the caller’s loop without blocking — safe from a FastAPI route as of PR #4533.
Return a plain bool from approve_fn
approve_fn must return True to approve or False to reject. Keep the decision logic inside the function and let ApprovalCallback handle the plan’s approve() transition.
Use auto_approve only for unattended runs
ApprovalCallback(auto_approve=True) skips all checks — reserve it for trusted CI or scripted runs where no human is available to review.