pleach
Use cases / Education

Per-district tenant + cost-cap

Composing educationAgent per-district with hard cost ceilings enforced at the gateway boundary via @pleach/gateway/swarm's CostCeilingMiddleware, plus audit ledger scoping by districtTenantId via @pleach/compliance/audit.

K-12 buyers expect three controls before they sign:

  1. Strong tenancy — district A's transcripts and spend cannot touch district B's, even if the runtime is shared across districts to keep per-seat pricing reasonable.
  2. A hard per-session spend ceiling, enforced before the model call, so a runaway loop in a classroom can't burn the district's monthly quota.
  3. An audit trail scoped to one district so end-of-year review pulls cleanly without leaking other districts' data.

This page shows how to compose those three controls today.

The recipe-layer split

educationAgent.ask() does NOT enforce the cost ceiling internally. The factory registers a CostCapPolicy on the runtime via the EDUCATION_RUNTIME_TAG symbol; the consumer's gateway / accounting layer is the enforcement point.

This split is intentional. The recipe sees user-message text and the runtime; it does not see per-call USD cost (that lives in the routing pipeline, behind the model-family matrix). The gateway sees cost on every routing call and is the right layer to read-and-veto. Wiring the gateway-side enforcer is one middleware factory and one aggregator.recordCost() call.

Per-district runtime construction

Construct one educationAgent per district. The factory is cheap to call — there is no shared global state, so it's safe to build on every request, on a startup loop, or per-tenant in a long-lived pool.

import { educationAgent } from "@pleach/recipes/education";

function buildDistrictAgent(districtTenantId: string) {
  return educationAgent({
    districtId: districtTenantId,
    complianceProfile: "ferpa+coppa",
    contentSafety: ["self-harm", "profanity"],
    costCapUsdPerSession: 0.25,
    perDistrictMonthlyBudgetUsd: 1500,
  });
}

// In your request handler:
const bot = buildDistrictAgent(req.tenantId);
const reply = await bot.ask(req.body.message);

The districtId is stamped on the runtime tag and is the value your downstream code (audit ledger, gateway aggregator, billing pipeline) should read for per-district scoping.

Cost-cap enforcement at the gateway boundary

@pleach/gateway/swarm ships CostCeilingMiddleware — a pre-call guard that wraps any routing call and throws CostCeilingExceededError before transport if the per-root-turn ceiling is already breached.

import {
  createCostCeilingMiddleware,
  createRootTurnCostAggregator,
} from "@pleach/gateway/swarm";

const aggregator = createRootTurnCostAggregator();

const middleware = createCostCeilingMiddleware({
  aggregator,
  maxCostUsdPerRootTurn: 0.25, // matches educationAgent's cap
  warnCostUsdPerRootTurn: 0.15,
  onBreach: async (event) => {
    await auditLedger.recordBreach({
      tenantId: event.tenantId,
      rootTurnId: event.rootTurnId,
      severity: event.severity, // "warn" | "breach"
      spentUsd: event.accumulatedUsd,
      ceilingUsd: event.ceilingUsd,
    });
  },
});

// Wrap every routing call:
const result = await middleware.guard({
  rootTurnId: req.rootTurnId,
  invoke: () => gatewayClient.route(routingInput),
});

The middleware enforces the ceiling at the routing-call boundary; the RootTurnCostAggregator accumulates spend across every descendant sub-agent under the same rootTurnId. A swarm fan-out (multiple sub-agents under one user turn) cannot escape the ceiling by spawning more children — the aggregator key is the root turn, not the child.

For the per-root-turn-cap primitive itself (independent of the gateway middleware), see createRootTurnCostCapPolicy at packages/core/src/spawn/rootTurnCostCap.ts:

import { createRootTurnCostCapPolicy } from "@pleach/core/spawn";

const policy = createRootTurnCostCapPolicy({
  ceilingUsd: 25,
  onBreach: async ({ rootTurnId, spentUsd }) => {
    await notifyOperator({ rootTurnId, spentUsd, action: "swarm-paused" });
  },
});

if (!policy.canSpend(rootTurnId, projectedCostUsd)) {
  throw new Error("root-turn cost-cap reached");
}
await policy.recordSpend(rootTurnId, actualCostUsd);

This is the substrate the gateway middleware composes. Use it directly when you're enforcing the cap outside the gateway (e.g. in a custom routing pipeline that doesn't go through @pleach/gateway).

Per-session vs per-root-turn: which to use

ScopePrimitiveSourceWhen to use
Per chat sessioncreateCostCapPolicy@pleach/compliance/educationOne student, one tutoring session, no sub-agent fan-out.
Per root user turncreateRootTurnCostCapPolicy@pleach/core/spawnSub-agent swarms; one user turn can fan out into many child calls.
Per gateway callcreateCostCeilingMiddleware@pleach/gateway/swarmProduction — the pre-call guard that actually rejects before transport.

educationAgent registers the per-session policy by default (costCapUsdPerSession, default $0.50). Layer the per-root-turn cap on top when one user turn fans out into sub-agents.

Driving the per-session cap from your turn gate

The per-session CostCapPolicy is registered on the runtime tag — read it via the EDUCATION_RUNTIME_TAG symbol and drive it from your per-turn gate:

import { EDUCATION_RUNTIME_TAG } from "@pleach/recipes/education";

const bot = educationAgent({
  districtId: "district-1234",
  costCapUsdPerSession: 0.25,
});

const tag = (bot.runtime as unknown as Record<symbol, any>)[
  EDUCATION_RUNTIME_TAG
];

// In your per-turn handler:
const projectedCostUsd = estimateTurnCost(message); // your projector
if (!tag.costCap.canSpend(sessionId, projectedCostUsd)) {
  throw new Error("classroom session cost-cap reached");
}

const reply = await bot.ask(message);

// After the model returns, record the actual cost:
await tag.costCap.recordSpend(sessionId, actualCostUsd);

The policy is in-memory and single-process — lossy across restarts. Production deployments back this with a tenant DB (Postgres / DynamoDB / Redis) by implementing the same CostCapPolicy interface directly against the same store the billing pipeline reads. The shape is intentionally narrow (canSpend / recordSpend / spentFor / reset) so a custom implementation drops in.

Per-district monthly budget

perDistrictMonthlyBudgetUsd is recorded only. The substrate does NOT enforce a monthly kill-switch today. Two workable paths:

  1. Roll it up in your billing pipeline — query the audit ledger by tenantId = districtId at month-roll and disable the district's API token at your application layer.
  2. Compose a second CostCeilingMiddleware with a RootTurnCostAggregator scoped to month-bucket keys (rootTurnId: ${districtId}-${yyyymm}). Hackier but stays inside the gateway layer.

A first-class per-district monthly budget primitive is tracked under an open scoping decision.

Audit ledger scoped by districtTenantId

@pleach/compliance/audit exposes a vendor-neutral queryAudit() projector that pulls a filtered slice of audit rows from any storage backend that satisfies AuditStoreLike:

import { queryAudit, exportAuditPdf } from "@pleach/compliance/audit";

const result = await queryAudit({
  store, // your AuditStoreLike (Postgres / Supabase / SQLite / in-memory)
  tenantId: "district-1234", // scope to one district
  from: "2026-01-01T00:00:00Z",
  to: "2026-02-01T00:00:00Z",
  limit: 1000,
});

if (!result.chainValid) {
  throw new Error("Audit log tampered with — chain broken.");
}

// Render to a single-file PDF for distribution to district counsel:
const pdfBuffer = await exportAuditPdf({
  rows: result.rows,
  metadata: {
    tenantId: "district-1234",
    framework: "FERPA+COPPA (mapped to GDPR bundle)",
    title: "District 1234 — January 2026 AI Tutor audit",
    from: "2026-01-01T00:00:00Z",
    to: "2026-02-01T00:00:00Z",
    chainValid: result.chainValid,
    notes: "Substitution: requested ferpa+coppa, applied gdpr bundle.",
  },
  format: "letter",
});

The PDF carries:

  • Header — tenant, framework, time range, generated-at.
  • Chain-validity badge — OK / TAMPERED at the top.
  • Per-row table — sequence, recorded-at, event-type, chat, hash prefix.
  • Footer — @pleach/compliance version + page numbers.

pdfkit is an OPTIONAL PEER. If you haven't installed it, exportAuditPdf throws MissingOptionalPeerError("pdfkit") so the caller can either install the peer or fall back to JSON / Markdown via produceAuditReport().

The hash-chain verification (result.chainValid) is the load-bearing piece for a district auditor: it certifies that between recordedAt time and now, no row in the projection was mutated or removed.

Putting it together — production-shape sketch

import { educationAgent, EDUCATION_RUNTIME_TAG } from "@pleach/recipes/education";
import {
  createCostCeilingMiddleware,
  createRootTurnCostAggregator,
} from "@pleach/gateway/swarm";
import { queryAudit, exportAuditPdf } from "@pleach/compliance/audit";

const aggregator = createRootTurnCostAggregator();

const middleware = createCostCeilingMiddleware({
  aggregator,
  maxCostUsdPerRootTurn: 0.25,
  warnCostUsdPerRootTurn: 0.15,
  onBreach: (event) => auditStore.recordBreach(event),
});

async function handleTutorTurn(req: Request) {
  const bot = educationAgent({
    districtId: req.districtId,
    complianceProfile: "ferpa+coppa",
    costCapUsdPerSession: 0.25,
  });
  const tag = (bot.runtime as any)[EDUCATION_RUNTIME_TAG];
  if (!tag.costCap.canSpend(req.sessionId, estimateTurnCost(req.message))) {
    return new Response("session cost-cap reached", { status: 429 });
  }
  return middleware.guard({
    rootTurnId: req.rootTurnId,
    invoke: async () => {
      const reply = await bot.ask(req.message);
      const actual = await readActualCostFromGateway();
      await tag.costCap.recordSpend(req.sessionId, actual);
      return {
        result: new Response(reply),
        costEvent: { tenantId: req.districtId, usd: actual, rootTurnId: req.rootTurnId },
      };
    },
  });
}

async function monthlyDistrictAudit(districtId: string, month: string) {
  const result = await queryAudit({
    store: auditStore,
    tenantId: districtId,
    from: `${month}-01T00:00:00Z`,
    to: `${month}-31T23:59:59Z`,
  });
  return exportAuditPdf({
    rows: result.rows,
    metadata: {
      tenantId: districtId,
      framework: "FERPA+COPPA (mapped to GDPR bundle)",
      chainValid: result.chainValid,
    },
  });
}

Source

  • packages/recipes/src/education.ts
  • packages/core/src/spawn/rootTurnCostCap.ts
  • packages/gateway/src/swarm/CostCeilingMiddleware.ts
  • packages/compliance/src/audit/{queryAudit,exportPdf}.ts

See also

On this page