Skip to content

Commit 9396eb8

Browse files
Merge pull request #230 from MervinPraison/claude/issue-228-20260422-1641
docs: update guardrails, thread-safety, and resource-lifecycle pages for PR #1514 architectural fixes
2 parents 7c1d4bc + 3444238 commit 9396eb8

5 files changed

Lines changed: 123 additions & 1 deletion

File tree

docs/best-practices/agent-retry-strategies.mdx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,40 @@ async def test_hedged_requests():
898898
assert call_count == 2 # Both requests started
899899
```
900900

901+
## Built-in Task Guardrail Retries
902+
903+
PraisonAI provides built-in retry functionality specifically for guardrail validation failures, distinct from the generic `ExponentialBackoffRetry` patterns above.
904+
905+
<Note>
906+
Guardrail retries are handled automatically by the executor when `Task(guardrail=..., max_retries=...)` is configured. This is separate from manual retry implementations.
907+
</Note>
908+
909+
```python
910+
from praisonaiagents import Agent, Task, PraisonAIAgents
911+
912+
def validate_content(output):
913+
"""Built-in guardrail with retry support"""
914+
if len(output.raw.split()) < 50:
915+
return False, "Content too short - needs at least 50 words"
916+
return True, output
917+
918+
task = Task(
919+
description="Write a detailed explanation",
920+
agent=agent,
921+
guardrail=validate_content,
922+
max_retries=3, # Built-in executor-level retry
923+
retry_with_feedback=True
924+
)
925+
926+
# The executor automatically handles:
927+
# - Guardrail validation
928+
# - Retry logic on failure
929+
# - Feedback to agent on retry
930+
# - Final failure after max_retries
931+
```
932+
933+
This differs from manual retry strategies as it's integrated into the task execution workflow and handles the retry loop at the executor level.
934+
901935
## Conclusion
902936

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

docs/best-practices/memory-cleanup.mdx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ class MemoryEfficientConversationManager:
8989

9090
### 2. Agent Memory Management
9191

92+
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.
93+
94+
```python
95+
from praisonaiagents import Agent, Task, PraisonAIAgents
96+
97+
memory_config = {"storage": {"path": "./shared.db"}, "provider": "file"}
98+
99+
agents = [Agent(name=f"A{i}", instructions="Summarize one line.") for i in range(4)]
100+
tasks = [Task(description=f"Summarize doc {i}.", agent=agents[i], config={"memory_config": memory_config}) for i in range(4)]
101+
102+
PraisonAIAgents(agents=agents, tasks=tasks).start()
103+
```
104+
92105
Implement memory limits and cleanup for agents:
93106

94107
```python
@@ -371,6 +384,24 @@ class AutomaticMemoryManager:
371384
schedule.every(5).minutes.do(conditional_cleanup)
372385
```
373386

387+
### 3. Agent Garbage Collection Safety Net
388+
389+
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:
390+
391+
```python
392+
# Preferred: Explicit cleanup
393+
agent = Agent(name="Analyst", instructions="Analyze data.")
394+
try:
395+
result = agent.start("Analyze quarterly numbers.")
396+
finally:
397+
agent.close() # Guaranteed cleanup
398+
399+
# Better: Context manager (recommended)
400+
with Agent(name="Analyst", instructions="Analyze data.") as agent:
401+
result = agent.start("Analyze quarterly numbers.")
402+
# Cleanup happens automatically here
403+
```
404+
374405
## Best Practices
375406

376407
### 1. Use Context Managers

docs/features/guardrails.mdx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ guardrail = LLMGuardrail(
254254

255255
### Retry Configuration
256256

257-
Configure retry behaviour for failed validations:
257+
Configure retry behaviour for failed validations (works in both sync and async execution paths):
258258

259259
```python
260260
task = Task(
@@ -267,6 +267,35 @@ task = Task(
267267
)
268268
```
269269

270+
**Execution Order**: Guardrail validation → retry (if failed) or pass → memory/user callbacks → task marked `completed`.
271+
272+
### How Retries Work
273+
274+
When a guardrail validation fails:
275+
1. **Before PR #1514**: Async execution bypassed retry logic
276+
2. **After PR #1514**: Both sync and async execution paths properly retry failed validations
277+
278+
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.
279+
280+
```python
281+
from praisonaiagents import Agent, Task, PraisonAIAgents
282+
283+
def must_mention_price(output):
284+
ok = "$" in output.raw
285+
return (ok, output if ok else "Rewrite and include a price in USD.")
286+
287+
agent = Agent(name="Writer", instructions="Write a one-line product blurb.")
288+
289+
task = Task(
290+
description="Write a blurb for a coffee mug.",
291+
agent=agent,
292+
guardrail=must_mention_price,
293+
max_retries=3,
294+
)
295+
296+
PraisonAIAgents(agents=[agent], tasks=[task]).start()
297+
```
298+
270299
### Composite Guardrails
271300

272301
Combine multiple validation criteria:

docs/features/resource-lifecycle.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ sequenceDiagram
8383
User->>Team: (exit with block)
8484
Team->>Agents: close() each
8585
Team->>Memory: close()
86+
Note right of Memory: Closes SQLite, MongoDB, etc.
8687
Team-->>User: cleanup complete
8788
```
8889

@@ -208,6 +209,21 @@ async with PraisonAIAgents(agents=[agent]) as workflow:
208209
```
209210
</Accordion>
210211

212+
<Accordion title="MongoDB connections are now included in cleanup">
213+
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:
214+
215+
```python
216+
# Explicit cleanup (preferred)
217+
with Agent(name="Analyst", instructions="Analyze quarterly numbers.") as agent:
218+
agent.start("Summarize Q1 revenue.")
219+
# MongoDB / SQLite / registered connections closed here.
220+
221+
# Async form
222+
async with Agent(name="Analyst", instructions="...") as agent:
223+
await agent.astart("...")
224+
```
225+
</Accordion>
226+
211227
<Accordion title="Don't reuse a team after exiting its with block">
212228
Once you exit a `with` block, consider the AgentTeam closed. Create a new one for additional work.
213229

docs/features/thread-safety.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ Prior to PR #1488, chat_history mutations bypassed thread-safety locks at 31+ ca
5353
- `chat_history` setter now acquires the `AsyncSafeState` lock for assignments
5454
</Note>
5555

56+
#### What changed in PR #1514
57+
58+
<Note>
59+
PR #1514 enhanced thread-safety in three key areas:
60+
61+
**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.
62+
63+
**2. Async-Locked Workflow State**: New `_set_workflow_finished(value)` method uses async locks to safely update workflow completion status across concurrent tasks.
64+
65+
**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.
66+
</Note>
67+
5668
#### Safe operations
5769

5870
```python

0 commit comments

Comments
 (0)