AgentOS Chat Session Isolation

How AgentOS /chat clones a per-request agent so concurrent callers never share chat_history

AgentOS clones a fresh agent for each POST /api/chat, so concurrent callers never share chat_history and the agent you passed into AgentOS(agents=[…]) is never mutated by request traffic.

Diagram

Yes

No: mock / plain callable

🖥️ Client

🌐 POST /api/chat

🔍 Supports
session isolation?

⚙️ clone template

🔑 bind session_id

🤖 agent.achat

🤖 shared template

✅ response echoes session_id

Quick Start

  1. Serve an Agent
    python
    from praisonai import AgentOS
    from praisonaiagents import Agent
    
    app = AgentOS(agents=[
        Agent(name="assistant", instructions="Be helpful"),
    ])
    app.serve(port=8000)
  2. Chat With a Session

    Supply a session_id to continue the same conversation across calls. Omit it to make each call a fresh session.

    bash
    # First turn — mint a session by supplying an id
    curl -s -X POST http://127.0.0.1:8000/api/chat \
      -H "Content-Type: application/json" \
      -d '{"message": "Hi, my name is Alice.", "session_id": "alice", "agent_name": "assistant"}'
    
    # Follow-up on the same session — the agent remembers "Alice" for this session id
    curl -s -X POST http://127.0.0.1:8000/api/chat \
      -H "Content-Type: application/json" \
      -d '{"message": "What is my name?", "session_id": "alice", "agent_name": "assistant"}'

Two concurrent clients using different session_ids never see each other’s transcripts.


How It Works

Each request resolves a per-request agent, resets its transcript, binds the session, and runs one turn.

Diagram
Cloned Agentbind_sessionAgentOS /chatClientCloned Agentbind_sessionAgentOS /chatClientalt[Isolate][Fall back]POST {message, session_id, agent_name?}resolve template (name lookup or agents[0])_supports_session_isolation(template)?bind_session(clone, session_id)chat_history=[] (must not raise)_session_id, _history_session_idbound cloneachat(message)replytemplate.achat(message)reply{response, agent_name, session_id}

The /chat handler and the /invoke router share one session-isolation contract: both call _supports_session_isolation → _clone_agent → bind_session(agent, session_id) from praisonai.api.agent_invoke. Any behaviour change to session binding lands in bind_session() and both surfaces pick it up together.


Session Contract

RequestBehaviour
session_id suppliedBound to agent._session_id (and _history_session_id when the attribute exists). Follow-up calls with the same id continue that conversation.
session_id omitted / falsyBoth cleared to None on the clone — each such call is a fresh session, not a sticky default.
Clone reset raisesHTTP 500 — the request is refused rather than delivering a clone that still holds the template’s transcript.
Any requestThe response echoes the supplied session_id back verbatim.

The response model is unchanged: {response, agent_name, session_id}. Only the internal binding of session_id and the concurrency semantics changed.


When Cloning Applies

Only real Agent instances that expose the per-session machinery are cloned. Plain mocks and lightweight callables fall back to the shared template.

Diagram

Agent with no handoffs

Agent with handoffs

Mock / plain callable

What did you pass to AgentOS?

✅ Per-request clone
session_id bound

↩️ Shared template
backward compatible


Best Practices

Send a session_id to continue a conversation

Supplying the same session_id on each turn is how a client continues one conversation. Omitting it starts a fresh session on a fresh clone every time.

Handoff graphs survive the per-request clone

clone_for_channel() rebinds every handoffs=[...] tool closure to the clone, so chat_history stays isolated per request and the clone can still delegate to its handoff peers.

Retry on a 500 isolation error

A 500 — Failed to isolate agent for session means the clone step raised, usually from a non-copyable object on the agent. It is safe to retry after removing the offending object.

Isolation is in-process only

This clone model isolates concurrent in-process requests. It does not persist sessions across process restarts — use a session store backend for durable history.

Don't swap in a chat_history whose setter can raise

bind_session() treats a raised setter as a fatal cross-tenant hazard and refuses the request. If you attach a custom chat_history descriptor (e.g. one that locks a backing store on assignment), make its setter always succeed — either accept [] unconditionally or replace it with a plain attribute.


Related