> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentium.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-Tenant Primitives

> AgentFactory, TeamFactory, WorkflowFactory, and ScopedStorage for SaaS deployments

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

```typescript theme={null}
import { ScopedStorage, SqliteStorage } from "@agentium/core";

const raw = new SqliteStorage("./data.db");

const acmeUserA = new ScopedStorage(raw, { tenantId: "acme", userId: "u-alice" });
const acmeUserB = new ScopedStorage(raw, { tenantId: "acme", userId: "u-bob" });

await acmeUserA.set("notes", "favorite_color", "blue");
await acmeUserB.set("notes", "favorite_color", "green");

await acmeUserA.get("notes", "favorite_color"); // "blue"
await acmeUserB.get("notes", "favorite_color"); // "green"
```

### Namespace transformation

```
inner.set("sessions", "session-123", value)
  with scope { tenantId: "acme", userId: "u-alice" }
  →  inner.set("tenant:acme:user:u-alice:sessions", "session-123", value)
```

Rules:

| Scope                                     | Resulting namespace                         |
| ----------------------------------------- | ------------------------------------------- |
| `{ tenantId: "acme", userId: "u-alice" }` | `tenant:acme:user:u-alice:<ns>`             |
| `{ tenantId: "acme" }` (no user)          | `tenant:acme:<ns>`                          |
| `{ userId: "alice" }` (no tenant)         | `user:alice:<ns>`                           |
| `{}` (empty)                              | `<ns>` (no prefix; identical to raw driver) |

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`:

```typescript theme={null}
const agent = new Agent({
  name: "assistant",
  model: openai("gpt-4o"),
  memory: {
    storage: new ScopedStorage(rawStorage, { tenantId, userId }),
  },
});
```

## `AgentFactory`

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

### Construction

```typescript theme={null}
import { AgentFactory, InMemoryStorage, openai } from "@agentium/core";

const factory = new AgentFactory({
  name: "assistant",
  model: openai("gpt-4o"),
  instructions: "You are a helpful assistant.",
  memory: {
    storage: new InMemoryStorage(), // shared underlying storage
  },
});
```

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

```typescript theme={null}
import express from "express";

const app = express();
app.use(express.json());

app.post("/chat", async (req, res) => {
  const tenantId = req.user.tenant;
  const userId = req.user.id;

  const agent = factory.create({ tenantId, userId });
  const result = await agent.run(req.body.input, { sessionId: `${tenantId}:${userId}` });
  res.json({ text: result.text });
});
```

`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

```typescript theme={null}
class AgentFactory {
  constructor(base: AgentConfig);
  create(scope?: FactoryContext): Agent;
}

interface FactoryContext {
  tenantId?: string;
  userId?: string;
}
```

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

### What gets scoped automatically

| AgentConfig field                           | Wrapped in ScopedStorage?                |
| ------------------------------------------- | ---------------------------------------- |
| `memory.storage`                            | ✅                                        |
| `checkpointing` (when set as `{ storage }`) | ✅                                        |
| `culture.storage`                           | ✅                                        |
| `versioning.storage`                        | ✅                                        |
| `costTracker`                               | ❌ (you tag costs per scope yourself)     |
| `semanticCache`                             | ❌ (you decide whether to share or scope) |
| Custom `tools` that hold their own storage  | ❌ (your responsibility)                  |

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`:

```typescript theme={null}
import { TeamFactory, openai, TeamMode } from "@agentium/core";

const teamFactory = new TeamFactory({
  name: "research-crew",
  mode: TeamMode.Coordinate,
  model: openai("gpt-4o"),
  members: [researchAgent, writerAgent, reviewerAgent],
  memory: { storage: sharedStorage },
});

app.post("/team", async (req, res) => {
  const team = teamFactory.create({ tenantId: req.user.tenant });
  const result = await team.run(req.body.input);
  res.json(result);
});
```

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`

```typescript theme={null}
import { WorkflowFactory } from "@agentium/core";

const wfFactory = new WorkflowFactory({
  name: "etl",
  initialState: { rows: [] },
  steps: [...],
});

const wf = wfFactory.create({ tenantId: "acme" });
await wf.run();
```

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

```typescript theme={null}
import {
  AgentFactory,
  InMemoryStorage,
  openai,
  SemanticToolSelector,
  OpenAIEmbedding,
} from "@agentium/core";
import express from "express";

// One-time setup
const storage = new InMemoryStorage();
const embedder = new OpenAIEmbedding();

const factory = new AgentFactory({
  name: "assistant",
  model: openai("gpt-4o"),
  memory: { storage },
});

const selector = new SemanticToolSelector({ embedder, topK: 5 });
await selector.indexTools(allRegisteredTools);

const app = express();
app.use(express.json());

app.post("/chat", async (req, res) => {
  const { tenant, id: userId } = req.user;
  const shortlist = await selector.select(req.body.input);

  const agent = factory.create({ tenantId: tenant, userId });
  agent.setTools([...alwaysOnTools, ...shortlist]);

  const result = await agent.run(req.body.input, {
    sessionId: `${tenant}:${userId}`,
  });

  res.json({ text: result.text, usage: result.usage });
});

app.listen(3000);
```

## 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

* [Sessions and Memory](/agents/sessions) — how sessions interact with scoped storage
* [Incremental Session Manager](/memory/incremental-sessions) — pair with `ScopedStorage` for cheap multi-tenant sessions
* [Cost Tracking](/cost/overview) — per-tenant budgets
