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.

python
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 deterministically

Closing an agent fans out to each owned resource so nothing leaks between runs.

Diagram

๐Ÿค– Agent

close / aclose

LLM client

Memory

MCP tools

Runtime MCPs

Circuit breakers

Background tasks

Executor

Approval grants

โœ… Clean exit

Quick Start

  1. 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()
  2. Async

    Use the async context manager or call aclose() explicitly โ€” it prefers each tool’s aclose().

    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.

Diagram
ResourcesAgentUserResourcesAgentUserclose() / await aclose()memory.close_connections()llm_instance.close() / aclose()MCP tool shutdown() / aclose()_shutdown_runtime_mcp_servers()_cleanup_circuit_breakers()cancel background tasksexecutor.shutdown()release approval grants (release_scope)_closed = True
ResourceSync pathAsync pathNotes
llm_instance clientclose() / 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.toolsprefer 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()sameSee Runtime MCP.
Circuit breakers_cleanup_circuit_breakers()sameRemoves every tool_{id(agent)}_* entry from the global registry.
Background taskstask.cancel()task.cancel() + await task
Thread-pool executorshutdown(wait=False, cancel_futures=True)same via run_in_executorcancel_futures only on Python 3.9+.
Approval-registry grants_release_approval_scope()sameEvicts 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.

Diagram

No

Yes

๐Ÿ—‘๏ธ GC

โš™๏ธ Agent.del

๐Ÿ“š Registry
exists?

โœ… No-op

๐Ÿ“ฅ Queue scope id
no lock

โœ๏ธ Next approval write

๐Ÿงน Prune dead keys

  • 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