Skip to content

docs: update for MCP transport cleanup + agent-scoped circuit breaker (PraisonAI #3130, #3131) #2116

Description

@MervinPraison

Context

Two recent SDK fixes on MervinPraison/PraisonAI change user-visible behaviour of agent.close() / agent.aclose(), the tool circuit breaker, MCP transport shutdown, and the LLM-instance resolution used by guardrails and token tracking. The existing docs either miss the new behaviour or actively contradict it (circuit-breaker retrieval by name no longer returns the runtime breaker).

Source commits on MervinPraison/PraisonAI:

  • 48d51f1fix: MCP client leaks, cross-agent circuit-breaker bleed, dead llm getattr fallback (fixes #3130)
  • 021d193fix: async MCP transport cleanup + circuit-breaker registry leak

Related upstream issue: MervinPraison/PraisonAI#3130.

Important

Per AGENTS.md §1.8, all NEW pages and updates MUST go in docs/features/. Do not modify anything under docs/concepts/, docs/js/, or docs/rust/. Concept-page suggestions listed in section C below are for human review only.


Summary of behaviour changes

# Change User impact SDK file (praisonai-package/)
1 MCP.shutdown() now falls back to close() for HTTPStreamMCPClient / WebSocketMCPClient, which only expose close() / aclose() (not shutdown()). Also fixes Agent.aclose() fallback. HTTP-stream and WebSocket MCP transports no longer leak subprocesses/threads on shutdown; context-manager exit and mcp.shutdown() are now reliable across every transport. src/praisonai-agents/praisonaiagents/mcp/mcp.py
2 Agent.close() and Agent.aclose() now walk self.tools for MCP-like objects (anything with shutdown() / aclose()) instead of the always-empty self._mcp_clients dict. MCP clients passed at construction time via Agent(tools=[MCP(...)]) are now deterministically shut down when the agent is closed. Previously they leaked. src/praisonai-agents/praisonaiagents/agent/agent.py
3 Circuit-breaker registry key changed from f"tool_{function_name}" to f"tool_{id(self)}_{function_name}" — scoped to the Agent instance. One agent's failing tool no longer trips the breaker for another agent's same-named tool. But: any existing code that calls get_circuit_breaker("tool_my_tool") will now retrieve a different breaker from the one actually protecting runtime calls. src/praisonai-agents/praisonaiagents/agent/tool_execution.py
4 Agent.close() / Agent.aclose() now call _cleanup_circuit_breakers() to remove that agent's instance-scoped breakers from the process-global registry. The registry stays bounded across create/close cycles, and a reused CPython object id cannot inherit a stale OPEN breaker from a previously-collected agent. src/praisonai-agents/praisonaiagents/agent/agent.py
5 Guardrail and token-tracking paths now prefer getattr(self, 'llm_instance', None) over getattr(self, 'llm', None). The llm attribute is always a truthy model-name string, so the old fallback order silently discarded configured api_key / base_url / client overrides and disabled set_current_agent / last_token_metrics wiring. LLM-based string guardrails and PraisonAIAgents token metrics now use the configured LLM instance (with its overrides), not the bare model-name string. set_current_agent and last_token_metrics are re-connected on the multi-agent path. src/praisonai-agents/praisonaiagents/agent/agent.py, src/praisonai-agents/praisonaiagents/agents/agents.py

Documentation work needed

A. NEW page — place in docs/features/

A1. docs/features/agent-lifecycle-cleanup.mdx (NEW)

Covers items #2, #4 — the agent-level cleanup story for MCP tools and circuit breakers. Currently the only doc that even mentions agent.close() / agent.aclose() is docs/features/runtime-mcp.mdx, and it only covers the runtime-attached case.

Required structure (per AGENTS.md template):

  • Frontmatter: icon: "broom", title "Agent Lifecycle Cleanup", sidebar "Agent Cleanup".

  • Hero Mermaid diagram: agent → close() → { LLM client, memory, MCP tools, runtime MCPs, circuit breakers, background tasks, executors } → clean exit. Use the AGENTS.md colour scheme (#8B0000 agent, #189AB4 process, #10B981 success).

  • Quick Start <Steps>:

    • Step 1 — Simple: agent-centric example with Agent(tools=[MCP("uvx mcp-server-time")]) followed by agent.close(); note that subprocesses/streams shut down deterministically.
    • Step 2 — Async: same example using async with on a helper, or explicit await agent.aclose().
  • How It Works sequenceDiagram: user → agent → (memory close, LLM close, MCP tool shutdown(), runtime-MCP _shutdown_runtime_mcp_servers(), circuit-breaker registry prune) → clean exit.

  • What gets cleaned up (table) — mirror exactly what Agent.close() / Agent.aclose() do (source: praisonaiagents/agent/agent.py:6121-6215 and :6217-6281):

    Resource Sync path Async path Notes
    llm_instance client close() / bridged aclose() aclose() if awaitable Falls back to __openai_client.close() and self.llm._client.close() for legacy patterns.
    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() New in #3131. Mirrors remove_mcp_server().
    Runtime-attached MCPs _shutdown_runtime_mcp_servers() same Existing behaviour, now cross-linked.
    Circuit breakers _cleanup_circuit_breakers() same New in #3131. Removes every tool_{id(self)}_* 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
  • Idempotency note: close() / aclose() set _closed = True and become no-ops on subsequent calls.

  • Best practices <AccordionGroup>:

    • Prefer with PraisonAIAgents(...) as workflow: — team-level context manager fans out to each agent's close() (link to docs/features/resource-lifecycle.mdx).
    • For a single agent, wrap in try/finally or write a small helper that calls agent.close().
    • Do not rely on __del__ alone — cleanup is best-effort under GC and the async client cannot be awaited.
    • Note that cleanup failures are logged as warnings and never raise, so they never mask the original error.
  • Related <CardGroup>:

    • docs/features/runtime-mcp.mdx — attaching / detaching MCPs at runtime.
    • docs/features/mcp-lifecycle.mdx — per-MCP context manager & manual shutdown().
    • docs/features/resource-lifecycle.mdx — team-level with PraisonAIAgents(...).
    • docs/features/tool-circuit-breaker.mdx — how per-agent breakers work.

B. Updates to existing FEATURE pages (AI agents may edit)

B1. docs/features/tool-circuit-breaker.mdxbreaking-change fix required

The page's current get_circuit_breaker("tool_my_tool") examples will silently return a fresh, disconnected breaker after the fix, because the runtime key is now f"tool_{id(agent)}_{function_name}". Update as follows:

  • Add a callout near the top (<Warning>): breaker keys are now scoped to the Agent instance (tool_{id(agent)}_{function_name}). Retrieving a breaker by tool name alone no longer returns the runtime instance. Point readers at the new observability pattern below and at the new agent-lifecycle-cleanup.mdx for the auto-prune behaviour.
  • Line 108 (get_circuit_breaker("tool_my_tool", ...)), line 232 (get_circuit_breaker("tool_my_tool")), lines 254 & 261 (get_circuit_breaker("tool_external_api", ...) / get_circuit_breaker("tool_internal_db", ...)), and line 276 (get_circuit_breaker("tool_my_tool")):
    • Replace with the new key format: get_circuit_breaker(f"tool_{id(agent)}_my_tool").
    • In the "Custom Config Per Tool" tab, restructure the example so the config is registered before the agent runs the tool for the first time (the breaker is lazily created on first call using the config passed to get_circuit_breaker — see agent/tool_execution.py:1859-1880 for exact semantics). If pre-registration is not viable with the new keying, replace the tab with guidance to fork the tool with a wrapper that owns its own breaker.
  • Update the Observability tab (line 225 onward) — show how to enumerate this agent's breakers:
    from praisonaiagents.tools.circuit_breaker import _get_global_registry
    
    registry = _get_global_registry()
    prefix = f"tool_{id(agent)}_"
    for name in registry.list_services():
        if name.startswith(prefix):
            breaker = registry.get(name)
            print(name, breaker.state, breaker.stats.failure_count)
  • Add a new subsection "Lifecycle" explaining that agent.close() / agent.aclose() removes every tool_{id(agent)}_* entry from the registry, keeping it bounded and preventing a reused id from inheriting a stale OPEN breaker. Cross-link to docs/features/agent-lifecycle-cleanup.mdx.
  • Update the Best Practices accordion "Reset between test runs" to note that per-agent breakers are also auto-pruned by agent.close(), so reset_all_circuit_breakers() remains the sledgehammer for global tests but per-request isolation is now free when the agent is closed.

Source lines to verify against: praisonaiagents/agent/tool_execution.py:1859-1880 and praisonaiagents/agent/agent.py:4705-4725, 6121-6281.

B2. docs/features/mcp-lifecycle.mdx

  • Update the "Connection Types" section (line 211) so the four-transport list (stdio / SSE / HTTP Stream / WebSocket) explicitly states that HTTP-stream and WebSocket now close via the close() fallback inside MCP.shutdown() — previously these two transports leaked on shutdown.

  • Extend "Lifecycle Methods → shutdown()" (line 193) with a short table:

    Transport Client method actually invoked
    Stdio subprocess terminate + stream close
    SSE HTTP stream close
    HTTP Stream close() (fallback added in #3131)
    WebSocket close() (fallback added in #3131)
  • Add a Related card pointing to the new docs/features/agent-lifecycle-cleanup.mdx.

Source lines: praisonaiagents/mcp/mcp.py:1085-1112 (specifically the new hasattr(..., 'close') fallbacks for http_stream_client and websocket_client).

B3. docs/features/runtime-mcp.mdx

  • Update the "Cleanup" section (line 135) to note that agent.close() / agent.aclose() now also shut down MCP objects passed at construction time via Agent(tools=[MCP(...)]) — not just runtime-attached servers. Add one sentence:

    agent.close() and await agent.aclose() also walk self.tools and call shutdown() / aclose() on any construction-time MCP objects, so you get the same clean-shutdown guarantee whether you attach an MCP at build time or add one at runtime.

  • Add a Related card pointing to the new docs/features/agent-lifecycle-cleanup.mdx.

Source lines: praisonaiagents/agent/agent.py:6168-6180 (sync) and :6228-6240 (async).

B4. docs/features/mcp-client-protocol.mdx

  • Add a paragraph in "Protocol Methods" (around line 117) explaining the current dispatch order used by MCP.shutdown(): it prefers shutdown(), falls back to close(), then to aclose() if awaitable. Custom MCPClientProtocol implementations that expose only close() (as many httpx-style clients do) are now fully supported at teardown.
  • Cross-link to docs/features/mcp-lifecycle.mdx#shutdown and to the new agent-lifecycle-cleanup.mdx.

Source lines: praisonaiagents/mcp/mcp.py:1080-1112.

B5. docs/features/guardrails.mdx (feature page)

  • Add a short "LLM-based guardrails" callout (<Note>): when guardrail is a string, PraisonAI now resolves the underlying LLM by preferring agent.llm_instance over agent.llm. This ensures the guardrail LLM inherits api_key, base_url, and any custom client you passed to the agent — previously the string form silently discarded these overrides.
  • Do not touch docs/concepts/guardrails.mdx (concept-page edits need human approval — flag in section C).

Source lines: praisonaiagents/agent/agent.py:5507-5514 (the new llm_instance-first fallback in _setup_llm_guardrail).

B6. docs/features/token-tracking.mdx (if the page exists — otherwise skip; do not create a new page in this issue)

  • Add a note that in multi-agent teams the executor's LLM instance (executor_agent.llm_instance) is now the one wired into set_current_agent() / last_token_metrics, so per-agent token accounting is accurate again for agents constructed with a real LLM object rather than a bare model-name string.

Source lines: praisonaiagents/agents/agents.py:357-368 (the reordered getattr(..., 'llm_instance', None) or getattr(..., 'llm', None)).


C. Concept-page suggestions (HUMAN APPROVAL ONLY — do NOT auto-edit)

Flag these for maintainer review; the AI agent must not modify these files under docs/concepts/:

  • docs/concepts/mcp.mdx (293 lines) — the "Connection Types" table (line 84) should note that HTTP Stream / WebSocket cleanup is now uniform with stdio and SSE.
  • docs/concepts/guardrails.mdx (586 lines) — clarify that string guardrails now inherit the agent's configured llm_instance (not the bare llm model-name string), and cross-reference the on_error hook if applicable.
  • docs/concepts/agents.mdx — add a brief "Lifecycle" subsection linking to the new docs/features/agent-lifecycle-cleanup.mdx.

Reference: SDK source files to read

The downstream agent MUST read these files before writing/updating each page (per AGENTS.md §1.2 SDK-First Cycle). Paths are relative to MervinPraison/PraisonAI:

  • src/praisonai-agents/praisonaiagents/agent/agent.py
    • _cleanup_circuit_breakers() — lines 4705-4725
    • _setup_llm_guardrail() LLM resolution — lines 5507-5514
    • close() — lines 6121-6215 (memory, LLM, MCP tools, runtime MCPs, circuit breakers, executor)
    • aclose() — lines 6217-6281
  • src/praisonai-agents/praisonaiagents/agent/tool_execution.py
    • Circuit-breaker key construction — lines 1855-1880 (f"tool_{id(self)}_{function_name}")
  • src/praisonai-agents/praisonaiagents/agents/agents.py
    • _build_execution_context() — lines 355-368 (llm_instance-first resolution for token tracking)
  • src/praisonai-agents/praisonaiagents/mcp/mcp.py
    • MCP.shutdown() HTTP-stream / WebSocket fallbacks — lines 1080-1112
  • src/praisonai-agents/praisonaiagents/tools/circuit_breaker.py
    • _get_global_registry(), list_services(), remove() — for the new observability example in B1.

Per AGENTS.md §1.4, the SDK paths in the docs repo are under repo root praisonaiagents/ (synced daily by update_repos.sh). Verify each cited line against the mirrored copy in PraisonAIDocs/praisonaiagents/ before writing.

Multi-SDK parity: The circuit-breaker key change and the MCP-tool cleanup are Python-only. TypeScript (src/praisonai-ts/) and Rust (src/praisonai-rust/) implementations should be checked; if they lack an equivalent, do not add TS/Rust pages — the auto-generated parity tables (docs/js/DOCS_PARITY.md, docs/rust/DOCS_PARITY.md) will surface the gap.


Acceptance checklist for the doc PR

  • New page docs/features/agent-lifecycle-cleanup.mdx created with full Mintlify frontmatter, hero Mermaid diagram (AGENTS.md colour scheme), <Steps> Quick Start, <AccordionGroup> best practices, <CardGroup> related links.
  • Quick Start is agent-centric and copy-paste runnable: minimal imports (from praisonaiagents import Agent, MCP), no placeholder values.
  • docs/features/tool-circuit-breaker.mdx updated so every get_circuit_breaker("tool_...") example uses the new f"tool_{id(agent)}_..." key format (or is replaced with the new observability pattern), plus the new <Warning> callout and Lifecycle subsection.
  • docs/features/mcp-lifecycle.mdx updates B2 applied (Connection Types note + shutdown() transport table).
  • docs/features/runtime-mcp.mdx update B3 applied (Cleanup section extended, Related card added).
  • docs/features/mcp-client-protocol.mdx update B4 applied (dispatch-order paragraph + cross-links).
  • docs/features/guardrails.mdx update B5 applied.
  • docs/features/token-tracking.mdx update B6 applied only if the page already exists — do not create a new page for this in this pass.
  • docs.json updated to register docs/features/agent-lifecycle-cleanup.mdx under the Features group (never under Concepts), and JSON validity re-verified.
  • No edits inside docs/concepts/, docs/js/, or docs/rust/.
  • All code examples verified against the SDK source lines listed in the "Reference" section above.
  • Every code block runs without modification (imports included, no "your-key-here" placeholders).
  • Mermaid diagrams use the standard colour scheme and white text.
  • If parity trackers are affected, run python3 src/praisonai/scripts/generate_docs_parity.py --copy-docs before pushing.

Branch instructions for the downstream agent

Per the task instructions for this routine:

  • Develop on branch claude/admiring-euler-jku6yc in MervinPraison/PraisonAIDocs.
  • Push with git push -u origin claude/admiring-euler-jku6yc and open a draft PR referencing this issue.

cc @MervinPraison

Metadata

Metadata

Assignees

No one assigned

    Labels

    claudeTrigger Claude Code analysisdocumentationImprovements or additions to documentation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions