Agent Lifecycle Cleanup
Deterministic teardown of MCP tools, circuit breakers, and clients when an agent closes
agent.close() and await agent.aclose() shut down every resource the agent owns โ LLM clients, memory, MCP tools, runtime MCPs, and per-agent circuit breakers โ in one call.
from praisonaiagents import Agent, MCP
agent = Agent(name="TimeAgent", tools=[MCP("uvx mcp-server-time")])
try:
agent.start("What time is it in UTC?")
finally:
agent.close() # subprocesses, streams, and breakers shut down deterministicallyClosing an agent fans out to each owned resource so nothing leaks between runs.
Quick Start
- Simple
Pass an MCP at construction time;
close()shuts down its subprocess and stream.python from praisonaiagents import Agent, MCP agent = Agent( name="TimeAgent", instructions="Report the current time.", tools=[MCP("uvx mcp-server-time")], ) try: agent.start("What time is it in UTC?") finally: agent.close() - Async
Use the async context manager or call
aclose()explicitly โ it prefers each tool’saclose().python import asyncio from praisonaiagents import Agent, MCP async def main(): async with Agent( name="TimeAgent", instructions="Report the current time.", tools=[MCP("uvx mcp-server-time")], ) as agent: await agent.achat("What time is it in UTC?") # aclose() runs on exit โ MCP tools, breakers, and clients shut down asyncio.run(main())
How It Works
Closing an agent walks each owned resource and shuts it down best-effort.
| Resource | Sync path | Async path | Notes |
|---|---|---|---|
llm_instance client | close() / bridged aclose() | aclose() (preferred, awaited) then sync close() fallback โ only if _llm_instance is already materialised. aclose() deliberately does not touch the llm_instance property (that would lazily create a client just to tear it down). Also closes _Agent__openai_client.close() and self.llm._client.close() when present. | Falls back to the private OpenAI client and self.llm._client.close() for legacy patterns. Async path fixed in PR #5083. Sync OpenAIClient.close() no longer calls AsyncOpenAI.close() un-awaited โ it drops the async reference; use await agent.aclose() for async teardown (PR #5859). |
Memory (_memory_instance / memory) | close_connections() | await memory.aclose() else close_connections() | |
MCP tools passed via tools=[MCP(...)] | t.shutdown() for each tool in self.tools | prefer await t.aclose(), else t.shutdown() | Mirrors remove_mcp_server()‘s best-effort shutdown. Since PR #5078, the stdio subprocess is joined and terminated as part of MCP.shutdown(), not just signalled. |
| Runtime-attached MCPs | _shutdown_runtime_mcp_servers() | same | See Runtime MCP. |
| Circuit breakers | _cleanup_circuit_breakers() | same | Removes every tool_{id(agent)}_* entry from the global registry. |
| Background tasks | task.cancel() | task.cancel() + await task | |
| Thread-pool executor | shutdown(wait=False, cancel_futures=True) | same via run_in_executor | cancel_futures only on Python 3.9+. |
| Approval-registry grants | _release_approval_scope() | same | Evicts only this agent’s skill allowed-tools auto-approvals, “this session” human approvals, and per-context approval-cache keys โ all keyed by this agent’s _approval_scope_id. See Permissions โ Approval-scope cleanup. |
Garbage-Collection Safety
An agent that is dropped without close() is cleaned up by a finalizer that never blocks.
- No deadlock.
Agent.__del__reads the approval registry directly, without taking its lock. GC firing on a thread that holds the lock no longer hangs. - Silent no-op without a registry. If no approval-gated tool ever ran, there is nothing to release.
- No leaked approval keys. Agents created and dropped in a long-lived worker thread, event loop, or notebook no longer pile up cached approvals. The next approval write prunes dead agents’ keys.
See Approval cache bounds and cleanup and MCP Lifecycle โ Garbage-collection safety.
Best Practices
Prefer a team-level context manager
Wrap a multi-agent run in with PraisonAIAgents(...) as workflow: โ the team-level context manager fans out to each agent’s close(). See Resource Lifecycle.
Use try/finally for a single agent
For a lone agent, wrap the run in try/finally and call agent.close() (or await agent.aclose()), so cleanup runs even when the run raises.
Use aclose() for async teardown of OpenAI clients
agent.close() releases the sync HTTP client and drops the cached async client reference; it cannot close the async client (its close() is a coroutine). For deterministic async teardown โ a FastAPI lifespan, a worker shutdown hook, or tests run with -W error::ResourceWarning โ call await agent.aclose().
Don't rely on __del__ alone
Garbage-collection cleanup is best-effort and never blocks โ the async client cannot be awaited from a finalizer. Call close() / aclose() (or use with) explicitly for deterministic teardown.
Cleanup never masks your error
Each step is guarded individually; failures are logged as warnings and never raise, so a bad shutdown never hides the original exception.
Long-running invoke servers close clones for you
The HTTP invoke API (POST /api/v1/agents/{id}/invoke) and invoke_agent_standalone() both wrap the invocation in try/finally and close the per-request clone after every request. You do not need to add extra cleanup around individual invokes; the wrapper handles per-request clones. See n8n API โ Per-request cleanup.
The invoke endpoint also accepts an Idempotency-Key header so client retries never start a second run โ see n8n API โ Safe Retries.
Long-running servers: approval grants are reclaimed
If you create one Agent(...) per request or session and never call close(), the process-global approval registry no longer grows without bound โ __del__ also releases the agent’s scope when it is garbage-collected. close() / aclose() remain the recommended deterministic path, since finalizer timing is not guaranteed. Only the closing agent’s own grants are dropped; every other live agent keeps its approvals. See Permissions โ Approval-scope cleanup.
Related
Attach and detach MCP servers at runtime
MCP LifecyclePer-MCP context manager and manual shutdown
Resource LifecycleTeam-level cleanup with PraisonAIAgents
Tool Circuit BreakerHow per-agent breakers work and auto-prune
PermissionsApproval scopes and the cleanup that close() performs
n8n APIPer-request clone cleanup in the HTTP invoke endpoint
Agent Invoke RegistryTemplates vs. per-request clones and their teardown