Skip to main content

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

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.
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:
ScopeResulting 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:
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

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

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

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

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

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