> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    A[🤖 Agent] --> C[close / aclose]
    C --> L[LLM client]
    C --> M[Memory]
    C --> T[MCP tools]
    C --> R[Runtime MCPs]
    C --> B[Circuit breakers]
    C --> K[Background tasks]
    C --> E[Executor]
    C --> P[Approval grants]
    L --> X[✅ Clean exit]
    M --> X
    T --> X
    R --> X
    B --> X
    K --> X
    E --> X
    P --> X

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class A agent
    class C,L,M,T,R,B,K,E,P process
    class X result
```

## Quick Start

<Steps>
  <Step title="Simple">
    Pass an MCP at construction time; `close()` shuts down its subprocess and stream.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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()
    ```
  </Step>

  <Step title="Async">
    Use the async context manager or call `aclose()` explicitly — it prefers each tool's `aclose()`.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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())
    ```
  </Step>
</Steps>

***

## How It Works

Closing an agent walks each owned resource and shuts it down best-effort.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Resources

    User->>Agent: close() / await aclose()
    Agent->>Resources: memory.close_connections()
    Agent->>Resources: llm_instance.close() / aclose()
    Agent->>Resources: MCP tool shutdown() / aclose()
    Agent->>Resources: _shutdown_runtime_mcp_servers()
    Agent->>Resources: _cleanup_circuit_breakers()
    Agent->>Resources: cancel background tasks
    Agent->>Resources: executor.shutdown()
    Agent->>Resources: release approval grants (release_scope)
    Agent-->>User: _closed = True
```

| 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](https://github.com/MervinPraison/PraisonAI/pull/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](https://github.com/MervinPraison/PraisonAI/pull/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](https://github.com/MervinPraison/PraisonAI/pull/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](/docs/features/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](/docs/features/permissions#approval-scope-cleanup). |

<Note>
  `close()` / `aclose()` set `_closed = True` and become no-ops on subsequent calls, so calling them twice is safe. Check `agent.is_closed` to confirm.
</Note>

<Note>
  **Before PR #5083** the async path (`aclose()`) skipped LLM-client teardown entirely, so a per-request clone in the HTTP invoke API leaked its client pool even when the caller (correctly) awaited `aclose()`. If you build long-running services that rely on `await agent.aclose()`, upgrade to that release.
</Note>

***

## Garbage-Collection Safety

An agent that is dropped without `close()` is cleaned up by a finalizer that never blocks.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    GC[🗑️ GC] --> Del[⚙️ Agent.__del__]
    Del --> Reg{📚 Registry<br/>exists?}
    Reg -->|No| Noop[✅ No-op]
    Reg -->|Yes| Q[📥 Queue scope id<br/>no lock]
    Q --> Next[✍️ Next approval write]
    Next --> Prune[🧹 Prune dead keys]

    classDef start fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef step fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class GC,Del start
    class Reg,Q,Next step
    class Noop,Prune ok
```

* **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.

<Note>
  `close()` / `aclose()` and `with` blocks stay the authoritative teardown. They are deterministic and release resources immediately; GC only runs when Python decides to.
</Note>

See [Approval cache bounds and cleanup](/docs/features/approval#approval-cache-bounds-and-cleanup) and [MCP Lifecycle → Garbage-collection safety](/docs/features/mcp-lifecycle#garbage-collection-safety).

***

## Best Practices

<AccordionGroup>
  <Accordion title="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](/docs/features/resource-lifecycle).
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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()`.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](/docs/features/n8n-api).
  </Accordion>

  <Accordion title="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](/docs/features/permissions#approval-scope-cleanup).
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Runtime MCP" icon="plug" href="/docs/features/runtime-mcp">
    Attach and detach MCP servers at runtime
  </Card>

  <Card title="MCP Lifecycle" icon="arrows-rotate" href="/docs/features/mcp-lifecycle">
    Per-MCP context manager and manual shutdown
  </Card>

  <Card title="Resource Lifecycle" icon="recycle" href="/docs/features/resource-lifecycle">
    Team-level cleanup with PraisonAIAgents
  </Card>

  <Card title="Tool Circuit Breaker" icon="shield-halved" href="/docs/features/tool-circuit-breaker">
    How per-agent breakers work and auto-prune
  </Card>

  <Card title="Permissions" icon="shield-check" href="/docs/features/permissions">
    Approval scopes and the cleanup that close() performs
  </Card>

  <Card title="n8n API" icon="code" href="/docs/features/n8n-api">
    Per-request clone cleanup in the HTTP invoke endpoint
  </Card>

  <Card title="Agent Invoke Registry" icon="user" href="/docs/features/agent-invoke-registry">
    Templates vs. per-request clones and their teardown
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.