Multi-Agent Context Safety
Context-isolated runtime and resolver state for safe concurrent agent operations
Context-isolated runtime and tool resolution prevents agents from interfering with each other when running concurrently in the same process.
import asyncio
from praisonaiagents import Agent
async def run_agents():
agent1 = Agent(name="Agent1", instructions="Help with coding")
agent2 = Agent(name="Agent2", instructions="Help with writing")
await asyncio.gather(agent1.start("Fix this bug"), agent2.start("Draft release notes"))
asyncio.run(run_agents())The user runs agents concurrently; each agent keeps isolated runtime and tool state.
Quick Start
- Default Behavior (Safe)
Each agent automatically gets its own context-isolated runtime and tool resolver:
python import asyncio from praisonaiagents import Agent async def run_agents(): # Each agent gets its own context automatically agent1 = Agent(name="Agent1", instructions="Help with coding") agent2 = Agent(name="Agent2", instructions="Help with writing") # These run safely in parallel - no state bleed tasks = [ asyncio.create_task(agent1.astart("Write a Python function")), asyncio.create_task(agent2.astart("Write a blog post")) ] results = await asyncio.gather(*tasks) print("Both agents completed safely") asyncio.run(run_agents()) - Concurrent handoffs to a shared target
Overlapping handoffs to the same target agent — a natural pattern when
parallel_handoffsfans several prompts to one specialist — run one at a time on that agent. Its seeded conversation context stays isolated per handoff and cannot interleave:python import asyncio from praisonaiagents import Agent, parallel_handoffs specialist = Agent(name="Summariser", instructions="Summarise the chunk.") router = Agent(name="Router", instructions="Route work to specialists.") async def run(): results = await parallel_handoffs( router, targets=[ (specialist, "Summarise chunk 1"), (specialist, "Summarise chunk 2"), ], max_concurrent=2, ) print([r.success for r in results]) asyncio.run(run())Each chunk sees only its own seeded context — no leak from the other. See Agent Handoffs and Thread Safety.
- Multi-Project Tool Resolution
Agents in different working directories get their own tool resolver anchored to their CWD:
python import os from praisonaiagents import Agent from praisonai.tool_resolver import reset_default_resolver # Project A with its own tools.py os.chdir("/project_a") agent_a = Agent(name="ProjectA", instructions="Use project A tools") agent_a.start("Use my_custom_tool") # Uses /project_a/tools.py # Switch to project B os.chdir("/project_b") reset_default_resolver() # Force re-anchor for new CWD agent_b = Agent(name="ProjectB", instructions="Use project B tools") agent_b.start("Use my_custom_tool") # Uses /project_b/tools.py
How It Works
Context variables (contextvars.ContextVar) automatically isolate state per execution context. Each agent/task/request gets its own:
- Interactive runtime with separate LSP/ACP configurations
- Tool resolver anchored to its own working directory
- Memory and cache state that won’t leak between agents
- Handoff chain (
_handoff_chain_var) rebound per task, soparallel_handoffssiblings keep isolated cycle/depth tracking — see Handoff chain isolation - User-tool module registration — each
load_user_modulecall registers under a per-load-uniquesys.moduleskey, so concurrent tool loads never share a slot and a failed load cannot pop another agent’s live module — see Concurrent loads stay isolated
Configuration Options
The context safety is automatic – no configuration required. However, there are some control points:
| Component | Behavior | Control Function |
|---|---|---|
| Tool Resolver | Per-context CWD anchoring | reset_default_resolver() |
| Interactive Runtime | Per-context config isolation | cleanup_runtime() (context-local) |
| Memory State | Automatic isolation | Built-in with async-safe locks |
Common Patterns
Pattern 1: Daemon with Project Switching
import os
from praisonai.tool_resolver import reset_default_resolver
from praisonaiagents import Agent
class MultiProjectDaemon:
def switch_project(self, project_path: str):
"""Switch to a different project directory."""
os.chdir(project_path)
reset_default_resolver() # Re-anchor tool resolver
print(f"Switched to project: {project_path}")
def process_request(self, project: str, task: str):
"""Process a request for a specific project."""
self.switch_project(f"/workspace/{project}")
agent = Agent(
name=f"Agent-{project}",
instructions=f"Work on {project} tasks"
)
return agent.start(task)
# Usage
daemon = MultiProjectDaemon()
result1 = daemon.process_request("frontend", "Fix the CSS bug")
result2 = daemon.process_request("backend", "Optimize the database query")Pattern 2: Concurrent Multi-Agent Workflow
import asyncio
from praisonaiagents import Agent
async def parallel_analysis():
"""Run multiple agents concurrently without interference."""
# Each agent gets isolated context automatically
agents = [
Agent(name="DataAnalyst", instructions="Analyze data patterns"),
Agent(name="SecurityReviewer", instructions="Check for security issues"),
Agent(name="CodeReviewer", instructions="Review code quality")
]
tasks = [
"Analyze the sales data for trends",
"Audit the authentication system",
"Review the new API endpoints"
]
# All run safely in parallel
results = await asyncio.gather(*[
agent.astart(task) for agent, task in zip(agents, tasks)
])
return results
# Each agent uses its own runtime/resolver context
results = asyncio.run(parallel_analysis())Pattern 3: IDE Plugin Integration
import asyncio
from praisonaiagents import Agent
from praisonai.tool_resolver import reset_default_resolver
class IDEPlugin:
def __init__(self):
self.current_workspace = None
async def handle_workspace_change(self, workspace_path: str):
"""Handle IDE workspace changes."""
if self.current_workspace != workspace_path:
self.current_workspace = workspace_path
reset_default_resolver() # Re-anchor to new workspace
print(f"Workspace changed to: {workspace_path}")
async def execute_command(self, command: str):
"""Execute command in current workspace context."""
agent = Agent(
name="IDEAssistant",
instructions="Help with IDE tasks"
)
# Agent automatically uses workspace-specific context
return await agent.astart(command)
# Usage in IDE
plugin = IDEPlugin()
await plugin.handle_workspace_change("/user/projects/myapp")
result = await plugin.execute_command("Refactor this function")Best Practices
Always use reset_default_resolver() after chdir
When switching working directories in long-lived processes, explicitly reset the tool resolver to pick up the new directory’s tools.py.
import os
from praisonai.tool_resolver import reset_default_resolver
# Good - explicit reset after chdir
os.chdir("/new/project")
reset_default_resolver()
agent = Agent(name="Agent", instructions="Use local tools")
# Bad - may use cached resolver from old directory
os.chdir("/new/project")
agent = Agent(name="Agent", instructions="Use local tools")Trust automatic context isolation
Don’t manually manage context variables – the framework handles isolation automatically. Each async task or thread gets its own context.
# Good - automatic isolation
async def worker(agent_name: str):
agent = Agent(name=agent_name, instructions="Work independently")
return await agent.astart("Do the task")
# Run multiple workers - each gets isolated context
tasks = [worker(f"Agent{i}") for i in range(5)]
results = await asyncio.gather(*tasks)
# Bad - manual context management (unnecessary)
import contextvars
ctx_var = contextvars.ContextVar('agent_state')
# Don't do this - framework handles itUse context-local cleanup functions
When cleaning up resources, use the context-local versions that only affect the current agent’s context.
from praisonai.cli.features.interactive_tools import cleanup_runtime
async def agent_task():
agent = Agent(name="TempAgent", instructions="Temporary task")
try:
result = await agent.astart("Do something")
return result
finally:
cleanup_runtime() # Only cleans up this context's runtimeLeverage CWD-per-context for multi-project tools
Each context anchors its tool resolver to the working directory when first called. Use this for clean project separation.
# Project structure:
# /project_a/tools.py - has custom_tool_a()
# /project_b/tools.py - has custom_tool_b()
import os
from praisonaiagents import Agent
# Agent A in project A context
os.chdir("/project_a")
agent_a = Agent(name="AgentA", instructions="Use project A tools")
# Automatically resolves to /project_a/tools.py
# Agent B in project B context
os.chdir("/project_b")
agent_b = Agent(name="AgentB", instructions="Use project B tools")
# Automatically resolves to /project_b/tools.py
# Each agent uses its own project's tools