diff --git a/docs/best-practices/agent-retry-strategies.mdx b/docs/best-practices/agent-retry-strategies.mdx index 21230bc9..27e79dfb 100644 --- a/docs/best-practices/agent-retry-strategies.mdx +++ b/docs/best-practices/agent-retry-strategies.mdx @@ -898,6 +898,40 @@ async def test_hedged_requests(): assert call_count == 2 # Both requests started ``` +## Built-in Task Guardrail Retries + +PraisonAI provides built-in retry functionality specifically for guardrail validation failures, distinct from the generic `ExponentialBackoffRetry` patterns above. + + +Guardrail retries are handled automatically by the executor when `Task(guardrail=..., max_retries=...)` is configured. This is separate from manual retry implementations. + + +```python +from praisonaiagents import Agent, Task, PraisonAIAgents + +def validate_content(output): + """Built-in guardrail with retry support""" + if len(output.raw.split()) < 50: + return False, "Content too short - needs at least 50 words" + return True, output + +task = Task( + description="Write a detailed explanation", + agent=agent, + guardrail=validate_content, + max_retries=3, # Built-in executor-level retry + retry_with_feedback=True +) + +# The executor automatically handles: +# - Guardrail validation +# - Retry logic on failure +# - Feedback to agent on retry +# - Final failure after max_retries +``` + +This differs from manual retry strategies as it's integrated into the task execution workflow and handles the retry loop at the executor level. + ## Conclusion Implementing robust retry strategies is essential for building resilient multi-agent systems. By choosing the appropriate retry pattern and configuring it correctly, you can handle transient failures gracefully while avoiding issues like retry storms and cascading failures. \ No newline at end of file diff --git a/docs/best-practices/memory-cleanup.mdx b/docs/best-practices/memory-cleanup.mdx index d6919ff0..530893d2 100644 --- a/docs/best-practices/memory-cleanup.mdx +++ b/docs/best-practices/memory-cleanup.mdx @@ -89,6 +89,19 @@ class MemoryEfficientConversationManager: ### 2. Agent Memory Management +Memory construction is now thread-safe and async-safe. Concurrent `Task`s sharing a `memory_config` will coordinate through locks rather than each creating duplicate stores. + +```python +from praisonaiagents import Agent, Task, PraisonAIAgents + +memory_config = {"storage": {"path": "./shared.db"}, "provider": "file"} + +agents = [Agent(name=f"A{i}", instructions="Summarize one line.") for i in range(4)] +tasks = [Task(description=f"Summarize doc {i}.", agent=agents[i], config={"memory_config": memory_config}) for i in range(4)] + +PraisonAIAgents(agents=agents, tasks=tasks).start() +``` + Implement memory limits and cleanup for agents: ```python @@ -371,6 +384,24 @@ class AutomaticMemoryManager: schedule.every(5).minutes.do(conditional_cleanup) ``` +### 3. Agent Garbage Collection Safety Net + +Since PR #1514, `Agent.__del__` runs a best-effort `close_connections()` during garbage collection as a safety net. However, this may be skipped by the Python interpreter and **must not be relied upon**. Always use explicit cleanup: + +```python +# Preferred: Explicit cleanup +agent = Agent(name="Analyst", instructions="Analyze data.") +try: + result = agent.start("Analyze quarterly numbers.") +finally: + agent.close() # Guaranteed cleanup + +# Better: Context manager (recommended) +with Agent(name="Analyst", instructions="Analyze data.") as agent: + result = agent.start("Analyze quarterly numbers.") +# Cleanup happens automatically here +``` + ## Best Practices ### 1. Use Context Managers diff --git a/docs/features/guardrails.mdx b/docs/features/guardrails.mdx index bceef5c7..606e31cc 100644 --- a/docs/features/guardrails.mdx +++ b/docs/features/guardrails.mdx @@ -254,7 +254,7 @@ guardrail = LLMGuardrail( ### Retry Configuration -Configure retry behaviour for failed validations: +Configure retry behaviour for failed validations (works in both sync and async execution paths): ```python task = Task( @@ -267,6 +267,35 @@ task = Task( ) ``` +**Execution Order**: Guardrail validation → retry (if failed) or pass → memory/user callbacks → task marked `completed`. + +### How Retries Work + +When a guardrail validation fails: +1. **Before PR #1514**: Async execution bypassed retry logic +2. **After PR #1514**: Both sync and async execution paths properly retry failed validations + +The executor increments `task.retry_count`, sets `task.status = "in progress"`, logs the retry, and continues the execution loop. On final failure after `max_retries`, it raises an exception. When a guardrail returns a modified `TaskOutput` or string, the downstream `task.result` and any memory callbacks receive the **modified** value, not the original. + +```python +from praisonaiagents import Agent, Task, PraisonAIAgents + +def must_mention_price(output): + ok = "$" in output.raw + return (ok, output if ok else "Rewrite and include a price in USD.") + +agent = Agent(name="Writer", instructions="Write a one-line product blurb.") + +task = Task( + description="Write a blurb for a coffee mug.", + agent=agent, + guardrail=must_mention_price, + max_retries=3, +) + +PraisonAIAgents(agents=[agent], tasks=[task]).start() +``` + ### Composite Guardrails Combine multiple validation criteria: diff --git a/docs/features/resource-lifecycle.mdx b/docs/features/resource-lifecycle.mdx index 21b0dac6..97173f96 100644 --- a/docs/features/resource-lifecycle.mdx +++ b/docs/features/resource-lifecycle.mdx @@ -83,6 +83,7 @@ sequenceDiagram User->>Team: (exit with block) Team->>Agents: close() each Team->>Memory: close() + Note right of Memory: Closes SQLite, MongoDB, etc. Team-->>User: cleanup complete ``` @@ -208,6 +209,21 @@ async with PraisonAIAgents(agents=[agent]) as workflow: ``` + +Since PR #1514, `Memory.close_connections()` also closes MongoDB clients when present. Multiple calls to `close_connections()` are safe (idempotent). Agent `__del__` provides a safety net but should not be relied upon: + +```python +# Explicit cleanup (preferred) +with Agent(name="Analyst", instructions="Analyze quarterly numbers.") as agent: + agent.start("Summarize Q1 revenue.") +# MongoDB / SQLite / registered connections closed here. + +# Async form +async with Agent(name="Analyst", instructions="...") as agent: + await agent.astart("...") +``` + + Once you exit a `with` block, consider the AgentTeam closed. Create a new one for additional work. diff --git a/docs/features/thread-safety.mdx b/docs/features/thread-safety.mdx index 72a89f0a..e278da61 100644 --- a/docs/features/thread-safety.mdx +++ b/docs/features/thread-safety.mdx @@ -53,6 +53,18 @@ Prior to PR #1488, chat_history mutations bypassed thread-safety locks at 31+ ca - `chat_history` setter now acquires the `AsyncSafeState` lock for assignments +#### What changed in PR #1514 + + +PR #1514 enhanced thread-safety in three key areas: + +**1. Locked Memory Initialization**: `Task.initialize_memory()` now uses `threading.Lock` with double-checked locking pattern. A new async variant `initialize_memory_async()` uses `asyncio.Lock` and offloads construction with `asyncio.to_thread()` to prevent event loop blocking. + +**2. Async-Locked Workflow State**: New `_set_workflow_finished(value)` method uses async locks to safely update workflow completion status across concurrent tasks. + +**3. Non-Mutating Task Context**: Task execution no longer mutates `task.description` during runs. Per-execution context is stored in `_execution_context` field, keeping the user-facing `task.description` stable across multiple executions. + + #### Safe operations ```python