Skip to main content

Multi-Tenant Primitives

The problem

A single Agentium process serving a SaaS product needs to:
  1. Isolate tenants. Customer A’s memory must never bleed into customer B’s session.
  2. Isolate users within a tenant. Alice’s userMemory.preferences must not be visible to Bob.
  3. Share infrastructure. One model client, one StorageDriver, one process — but logically partitioned data.
Agentium v2.0 ships two building blocks:
  • AgentFactory / TeamFactory / WorkflowFactory — construct scope-aware agents per request.
  • ScopedStorage — namespace every read/write on any StorageDriver by tenant + user.

ScopedStorage

The lowest-level primitive. Wraps any StorageDriver and rewrites every namespace by prefixing with tenant + user identifiers.

Namespace transformation

Rules: Two tenants on the same underlying driver therefore see disjoint key spaces.

Methods

ScopedStorage implements the full StorageDriver interface — get, set, delete, list, initialize, close. Every call goes through the namespace rewrite. initialize() and close() delegate to the inner driver (so calling close() on the scoped wrapper closes the SHARED driver — be careful in production).

Use directly without a factory

If you don’t need AgentFactory’s sugar, just construct a fresh agent per request with a ScopedStorage:

AgentFactory

Sugar over ScopedStorage. Define the agent template once, call factory.create(scope) per request to materialize a scoped Agent.

Construction

The AgentConfig you pass is the template. Don’t include userId or any per-user state in the template — that’s what the factory injects.

Per-request agent creation

factory.create({ ... }) returns a new Agent(...) whose:
  • userId is set to the scoped user (used by userMemory and event tracing).
  • memory.storage is wrapped in ScopedStorage.
  • checkpointing.storage, culture.storage, versioning.storage are also wrapped if present.
  • register: false is forced so the agent doesn’t pollute the global registry.
The factory itself is cheap (no model client allocation); allocate one per agent template at boot, then create() per request.

Method signature

create() with no args returns an unscoped Agent (useful for admin / cron jobs).

What gets scoped automatically

If you have custom tools that persist anything, wrap their storage in ScopedStorage manually before adding them to the template.

TeamFactory

Identical pattern for Team:
Scopes memory.storage the same way. The team’s members are passed by reference; if they have their own scoped storage, that’s preserved.

WorkflowFactory

Workflows currently don’t directly hold storage in their top-level config, so WorkflowFactory mostly disables global registration. The per-step agents (referenced by the workflow) carry whatever scope they were constructed with.

Putting it together — full SaaS pattern

Operational notes

  • One storage driver per process. Multiple ScopedStorage instances share one underlying driver — keep the driver alive for the process lifetime.
  • Don’t close the inner driver via the scoped wrapper. Adding a check to prevent accidental close-via-wrap is on the roadmap; for now, just call rawStorage.close() directly at shutdown.
  • For metrics / cost tracking, set tenantId on RunContext.metadata so observability picks it up. The factory already does this when you use userId — extend to tenantId in your hooks.
  • Test isolation explicitly. A unit test that writes under tenant A and reads under tenant B (expecting null) catches regressions early.

See also