The Strands extension provides distributed session management for Strands Agents, persisting sessions, agents, and messages to any Dapr state store with optional TTL and consistency controls.
dapr/ext/strands/
├── __init__.py # Exports: DaprSessionManager
├── dapr_session_manager.py # Main implementation (~550 lines)
└── py.typed
tests/ext/strands/
└── test_session_manager.py # Unit tests with mocked DaprClient
Installed via the strands extra on core dapr: pip install "dapr[strands]".
from dapr.ext.strands import DaprSessionManagerExtends both RepositorySessionManager and SessionRepository from the Strands agents framework.
Constructor:
manager = DaprSessionManager(
session_id='my-session',
state_store_name='statestore',
dapr_client=client, # DaprClient instance
ttl=3600, # Optional: TTL in seconds
consistency='eventual', # 'eventual' (default) or 'strong'
)Factory method:
manager = DaprSessionManager.from_address(
session_id='my-session',
state_store_name='statestore',
dapr_address='localhost:50001', # Auto-creates DaprClient
)Session operations:
create_session(session)→Session— creates new session (raises if exists)read_session(session_id)→Optional[Session]delete_session(session_id)— cascade deletes session + all agents + messages
Agent operations:
create_agent(session_id, session_agent)— creates agent, initializes empty messages, updates manifestread_agent(session_id, agent_id)→Optional[SessionAgent]update_agent(session_id, session_agent)— preserves originalcreated_at
Message operations:
create_message(session_id, agent_id, message)— appends to message listread_message(session_id, agent_id, message_id)→Optional[SessionMessage]update_message(session_id, agent_id, message)— preserves originalcreated_atlist_messages(session_id, agent_id, limit=None, offset=0)→List[SessionMessage]
Lifecycle:
close()— closes DaprClient if owned by this manager
| Key pattern | Contents |
|---|---|
{session_id}:session |
Session metadata (JSON) |
{session_id}:agents:{agent_id} |
Agent metadata (JSON) |
{session_id}:messages:{agent_id} |
Message list: {"messages": [...]} (JSON) |
{session_id}:manifest |
Agent ID registry: {"agents": [...]} (used for cascade deletion) |
dapr(core, same wheel as this extension)strands-agents >= 1.30.0, < 2.0.0python-ulid >= 3.0.0, < 4.0.0msgpack >= 1.0, < 2.0
uv run python -m unittest discover -v ./tests/ext/strands8 test cases using @mock.patch('dapr.ext.strands.dapr_session_manager.DaprClient'):
test_create_and_read_session,test_create_session_raises_if_existstest_create_and_read_agent,test_update_agent_preserves_created_attest_create_and_read_message,test_update_message_preserves_created_attest_delete_session_deletes_agents_and_messages(verifies cascade: 6 delete calls for 2 agents)test_close_only_closes_owned_client
- ID validation: Session IDs and agent IDs are validated via
strands._identifier.validate()— path separators (/,\) are rejected. - Manifest pattern: A manifest key tracks all agent IDs per session, enabling cascade deletion without scanning.
- TTL support: Optional time-to-live via Dapr state metadata (
ttlInSeconds). - Consistency levels: Maps to Dapr's
Consistency.eventual/Consistency.strongviaStateOptions. - Client ownership: The
_owns_clientflag tracks whetherDaprSessionManagercreated its own client (viafrom_address) or received one externally. Only owned clients are closed byclose(). - Timestamp preservation:
update_agentandupdate_messageread the existing record first to preserve the originalcreated_attimestamp. - All errors are
SessionException: All Dapr state operation failures are wrapped in Strands'SessionException.