Agentic AI Factory

Enterprise reference architecture · build kit

Agents get assembled,
not authored.

A factory is an operating model, not a framework. One platform team owns the paved road — runtime, guardrails, connectors, evaluations, pipelines. Delivery squads compose agents on top of it. Every layer below ships with the code to build it.

Platform — built once Agent
Thick platformThin agent
7Stack floors
4Cross-cutting spines
12Interactive diagrams
14Build snippets
01

Floor plan

A request enters at the top and leaves through a system of record at the bottom. Floors are passed through; spines are conditions met at every floor. Select any block to open its schematic and build code.

Spines · every floor

Floor — request passes through Spine — condition met at every floor
02

The stack

Seven floors carry a request from a channel to a system of record. Four spines run the full height of the building — they are not layers you pass through, they are conditions every layer meets. Select any one to read the brief, the decisions worth fighting over, and the code that implements it.

Floors

Spines

Floor 01

Channel & experience

One agent, many front doors. Teams, the ITSM portal, web, voice and raw API all produce the same request object — so a squad never rebuilds an agent to reach a new audience.

Decisions that hold up

  • Channel adapters normalise; agents stay channel-blind.
  • Carry the user's token from the first hop — identity is not re-derived later.
  • thread_id is the durable conversation key the checkpointer uses.

What goes wrong

  • Channel logic leaking into prompts ("if Teams, reply shorter").
  • A shared bot identity that erases who actually asked.
  • Reply shapes that differ per channel, so evaluation can't compare runs.
pythonplatform/channel/contract.py
# One contract in, one contract out. Every channel adapts to this.
from dataclasses import dataclass, field
from typing import Any, Literal

Channel = Literal["teams", "itsm", "web", "voice", "api"]

@dataclass
class AgentRequest:
    tenant_id: str
    user_upn: str                 # real user identity, never a shared service account
    channel: Channel
    utterance: str
    thread_id: str                # durable key for the orchestration checkpointer
    user_token: str               # exchanged on-behalf-of at the tool boundary
    attachments: list[dict[str, Any]] = field(default_factory=list)
    locale: str = "en-IN"

@dataclass
class AgentReply:
    text: str
    citations: list[dict[str, str]] = field(default_factory=list)
    actions_taken: list[str] = field(default_factory=list)
    needs_human: bool = False
    trace_id: str = ""
    cost_usd: float = 0.0

# --- adapter: ITSM ticket -> AgentRequest -----------------------------------
def from_itsm(payload: dict) -> AgentRequest:
    body = payload["short_description"] + "\n" + payload.get("description", "")
    return AgentRequest(
        tenant_id=payload["company_id"],
        user_upn=payload["caller"]["email"],
        channel="itsm",
        utterance=body.strip(),
        thread_id="itsm:" + payload["sys_id"],
        user_token=payload["_oauth"]["access_token"],
    )

def render_for(channel: Channel, reply: AgentReply) -> dict:
    """Formatting lives here. The agent never learns what a channel looks like."""
    return RENDERERS[channel](reply)

Floor 02

Agent orchestration

A supervisor routes to workers, state is durable, and high-risk steps stop at a human. Standardise the topology across squads — bespoke orchestration per project is the single biggest source of unmaintainable agents.

Decisions that hold up

  • Durable checkpointer (Postgres/Redis) so runs survive restarts and can be replayed.
  • Human-in-the-loop is an interrupt in the graph, not a UI afterthought.
  • Graph shape is declared once per pattern and reused, not redrawn per use case.

What goes wrong

  • Free-roaming agent loops with no step budget or terminal state.
  • State held in memory — you can't audit or resume what you didn't persist.
  • Approval implemented as a prompt instruction instead of control flow.
pythonplatform/orchestration/graph.py
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.types import interrupt, Command

class FactoryState(TypedDict):
    request: dict
    route: str
    evidence: list[dict]
    draft: str
    risk: str          # low | medium | high | restricted
    steps: int

MAX_STEPS = 12
ROUTES = {"how_to": "knowledge", "access": "action", "outage": "triage"}

def supervisor(state: FactoryState):
    if state["steps"] >= MAX_STEPS:                 # every graph has a step budget
        return Command(goto=END, update={"draft": "Handing over — step budget reached."})
    intent = classify(state["request"]["utterance"])
    return Command(goto=ROUTES.get(intent, "knowledge"),
                   update={"route": intent, "steps": state["steps"] + 1})

def approval_gate(state: FactoryState):
    if state["risk"] in ("high", "restricted"):
        # execution pauses here and resumes on the approver's verdict
        verdict = interrupt({"summary": state["draft"], "risk": state["risk"]})
        if verdict["decision"] != "approve":
            return Command(goto=END, update={"draft": "Declined by approver."})
    return Command(goto="act")

builder = StateGraph(FactoryState)
builder.add_node("supervisor", supervisor)
builder.add_node("knowledge", knowledge_worker)
builder.add_node("triage", triage_worker)
builder.add_node("action", action_planner)
builder.add_node("approval_gate", approval_gate)
builder.add_node("act", action_executor)
builder.add_edge(START, "supervisor")
builder.add_edge("knowledge", END)
builder.add_edge("triage", "supervisor")
builder.add_edge("action", "approval_gate")
builder.add_edge("act", END)

graph = builder.compile(checkpointer=PostgresSaver.from_conn_string(PG_DSN))

Floor 03

Runtime services

The reuse engine, and the layer most programmes skip. Tools are registered once as MCP servers, memory is a service, and a policy engine decides what an agent may call, spend and decide without a human.

Decisions that hold up

  • Every integration published once, versioned, with a declared risk class.
  • Authorisation is data in a registry, not if statements in agent code.
  • Per-run call budgets — an agent cannot retry a write forever.

What goes wrong

  • Each squad writing its own ServiceNow client. Now you have nine.
  • Tools handed to the model with no scope check before execution.
  • Memory that stores one tenant's context where another can retrieve it.
pythonplatform/runtime/registry.py
# Tools are platform assets. Registered once, consumed by many agents.
TOOL_REGISTRY = {
    "itsm.reset_password": {
        "server": "https://mcp.internal/itsm",     # MCP is the standard tool interface
        "version": "2.3.0",
        "write": True,
        "risk": "high",
        "scopes": ["Incident.Write", "Identity.Reset"],
        "allowed_agents": ["itsm-resolver", "service-desk-copilot"],
        "max_calls_per_run": 1,
        "owner": "platform-integrations",
    },
    "kb.search": {
        "server": "https://mcp.internal/knowledge",
        "version": "1.8.2", "write": False, "risk": "low",
        "scopes": ["Knowledge.Read"], "allowed_agents": ["*"], "max_calls_per_run": 6,
        "owner": "platform-knowledge",
    },
}

class PolicyError(Exception):
    """Raised before a tool runs, never after."""

def authorize(agent_id: str, tool: str, ctx) -> dict:
    spec = TOOL_REGISTRY.get(tool)
    if spec is None:
        raise PolicyError(tool + " is not a registered tool")
    if "*" not in spec["allowed_agents"] and agent_id not in spec["allowed_agents"]:
        raise PolicyError(agent_id + " is not entitled to " + tool)
    if not set(spec["scopes"]).issubset(ctx.granted_scopes):
        raise PolicyError("user lacks scopes for " + tool)
    if spec["write"] and spec["risk"] == "high" and not ctx.human_approved:
        raise PolicyError("high-risk write requires recorded approval")
    if ctx.calls_made(tool) >= spec["max_calls_per_run"]:
        raise PolicyError("call budget exhausted for " + tool)
    return spec

async def invoke(agent_id: str, tool: str, args: dict, ctx):
    spec = authorize(agent_id, tool, ctx)
    with span("tool.call", tool=tool, version=spec["version"], agent=agent_id):
        return await mcp_client(spec["server"]).call(tool, args, headers=ctx.obo_headers)

Floor 04

Model gateway

No squad holds a model endpoint. One gateway routes by sensitivity, cost and latency, enforces quota, falls back on failure and charges every token back to the agent that spent it.

Decisions that hold up

  • Route on data sensitivity first, cost second — residency is not negotiable.
  • Model choice is config, so swapping a provider is a pull request.
  • Meter and charge at the point of spend, per tenant and per agent.

What goes wrong

  • Frontier models used for classification you could do for a hundredth of the price.
  • Provider keys distributed to teams — no quota, no visibility, no exit.
  • Silent fallback to a weaker model with no trace of the substitution.
pythonplatform/models/gateway.py
# Routing policy is configuration. Squads call complete(), never a provider SDK.
ROUTES = {
    "restricted": {"model": "gpt-4o-eu",      "region": "westeurope", "fallback": None},
    "reasoning":  {"model": "claude-sonnet",  "fallback": "gpt-4o"},
    "standard":   {"model": "gpt-4o",         "fallback": "claude-sonnet"},
    "bulk":       {"model": "phi-4",          "fallback": "gpt-4o-mini"},
}
PRICE = {  # usd per 1M tokens: (input, output)
    "claude-sonnet": (3.00, 15.00), "gpt-4o": (2.50, 10.00),
    "gpt-4o-mini": (0.15, 0.60),    "phi-4": (0.07, 0.28),
}

def meter(model: str, usage) -> float:
    pin, pout = PRICE[model]
    return (usage.input_tokens * pin + usage.output_tokens * pout) / 1_000_000

async def complete(*, messages, tier, tenant, agent_id, sensitivity="standard"):
    route = ROUTES["restricted"] if sensitivity == "restricted" else ROUTES[tier]
    budget.assert_headroom(tenant, agent_id)          # stop runaway spend early
    for attempt, model in enumerate([route["model"], route.get("fallback")]):
        if not model:
            break
        try:
            resp = await provider_call(model, messages, timeout=route.get("timeout", 30))
        except (RateLimited, Timeout, ProviderDown) as err:
            emit_event("model.failover", model=model, reason=type(err).__name__)
            continue
        cost = meter(model, resp.usage)
        budget.charge(tenant, agent_id, cost)         # FinOps chargeback at source
        emit_span("model.call", model=model, degraded=bool(attempt),
                  tokens=resp.usage.total, cost_usd=cost)
        return resp
    raise AllModelsUnavailable(tier)

Floor 05

Knowledge & RAG

Retrieval is a shared service with one non-negotiable property: results are trimmed to what the asking user is already allowed to read. An agent that leaks a document is an access-control incident, not a model problem.

Decisions that hold up

  • ACLs indexed with the chunk and applied as a query filter, refreshed on source change.
  • Hybrid search plus a reranker — vector-only recall disappoints on enterprise jargon.
  • Groundedness scored per answer; below threshold, the agent declines and escalates.

What goes wrong

  • One index for all tenants with permissions checked only in the UI.
  • Answering from stale content because nothing tracks source freshness.
  • Citations that point at a document the reader cannot open.
pythonplatform/knowledge/retrieve.py
# Retrieval as a service. Security trimming happens in the query, not after it.
MIN_SCORE = 0.35
MIN_GROUNDEDNESS = 0.75

def acl_filter(user_upn: str, tenant: str) -> str:
    principals = identity.groups_for(user_upn) + ["upn:" + user_upn]
    allowed = ",".join(principals)
    return "tenant eq '" + tenant + "' and acl/any(g: search.in(g, '" + allowed + "'))"

def retrieve(query: str, user_upn: str, tenant: str, k: int = 8) -> list[dict]:
    candidates = search.hybrid(
        query_text=query,                 # BM25 leg: catches product codes and acronyms
        vector=embed(query),              # semantic leg
        filter=acl_filter(user_upn, tenant),
        top=40,
    )
    ranked = reranker.rank(query, candidates)[:k]
    fresh = [c for c in ranked if c.score >= MIN_SCORE and not c.is_expired]
    emit_span("rag.retrieve", candidates=len(candidates), returned=len(fresh))
    return fresh

def answer(query: str, ctx) -> AgentReply:
    chunks = retrieve(query, ctx.user_upn, ctx.tenant)
    if not chunks:
        return AgentReply(text="No source you have access to covers this yet.",
                          needs_human=True)
    reply = generate(query, chunks)                       # cite-or-abstain prompt
    score = groundedness(reply.text, chunks)              # claim-level check, 0..1
    emit_metric("rag.groundedness", score)
    if score < MIN_GROUNDEDNESS:
        return AgentReply(text="I could not ground this in approved sources.",
                          needs_human=True)
    return reply

Floor 06

Data platform

Fabric or Databricks is the governance spine. Medallion layers feed three assets the factory cannot run without: permission-tagged grounding data, golden evaluation sets, and agent telemetry that closes the improvement loop.

Decisions that hold up

  • Golden datasets are versioned tables, owned by the business, not files in a repo.
  • Agent traces land in the lakehouse — quality analysis needs SQL, not log search.
  • Catalog-level governance (Unity Catalog / OneLake) is the single permission source.

What goes wrong

  • Grounding data copied out of the platform, losing its lineage and its ACLs.
  • Evaluation cases living in one engineer's notebook.
  • No feedback path from production outcomes back into the next release.
sqlplatform/data/gold_ddl.sql
-- Three gold assets every agent factory runs on.
CREATE CATALOG IF NOT EXISTS ai_factory;
CREATE SCHEMA  IF NOT EXISTS ai_factory.gold;

-- 1. Grounding: curated, permission-tagged, freshness-aware
CREATE TABLE ai_factory.gold.knowledge_chunk (
  chunk_id        STRING NOT NULL,
  tenant_id       STRING NOT NULL,
  source_system   STRING,          -- sharepoint | confluence | itsm_kb
  source_url      STRING,
  acl             ARRAY<STRING>,   -- group + upn principals, synced from source
  content         STRING,
  embedding       ARRAY<FLOAT>,
  valid_until     DATE,            -- expiry drives the freshness filter at query time
  ingested_at     TIMESTAMP
) USING DELTA PARTITIONED BY (tenant_id);

-- 2. Golden evaluation set: the release gate's source of truth
CREATE TABLE ai_factory.gold.eval_case (
  case_id      STRING, agent_id STRING,
  category     STRING,   -- happy | adversarial | refusal | acl_leak | ambiguity
  prompt       STRING, expected STRING, must_refuse BOOLEAN,
  as_user      STRING,   -- ACL cases run as a low-privilege identity
  added_by     STRING, added_at TIMESTAMP
) USING DELTA;

-- 3. Production telemetry: the improvement loop
CREATE TABLE ai_factory.gold.agent_trace (
  trace_id STRING, agent_id STRING, tenant_id STRING, pattern STRING,
  model STRING, tokens_in BIGINT, tokens_out BIGINT, cost_usd DECIMAL(10,5),
  groundedness FLOAT, tool_calls INT, escalated BOOLEAN,
  outcome STRING,       -- resolved | escalated | declined | failed
  latency_ms INT, ts TIMESTAMP
) USING DELTA PARTITIONED BY (agent_id);

-- Weekly quality review, straight off the lakehouse
SELECT agent_id, pattern,
       COUNT(*)                                        AS runs,
       AVG(CASE WHEN outcome = 'resolved' THEN 1 ELSE 0 END) AS autonomy_rate,
       AVG(groundedness)                               AS mean_groundedness,
       SUM(cost_usd) / SUM(CASE WHEN outcome = 'resolved' THEN 1 ELSE 0 END)
                                                       AS cost_per_resolution
FROM   ai_factory.gold.agent_trace
WHERE  ts >= current_date() - INTERVAL 7 DAYS
GROUP  BY agent_id, pattern
ORDER  BY cost_per_resolution DESC;

Floor 07

Integration fabric

Where autonomy stops being a demo. A write-action is a transaction: idempotent, previewable, logged in a ledger, and reversible by a named compensating action.

Decisions that hold up

  • Idempotency key derived from thread + action + subject — retries never double-execute.
  • Every write declares its compensation at registration time.
  • Dry-run mode is a first-class path, used by evaluations and by approvers.

What goes wrong

  • A retry loop that raises the same change request eleven times.
  • Partial multi-system updates with nothing to roll them back.
  • Actions executed under a service principal, so the audit trail names a robot.
pythonplatform/integration/actions.py
import hashlib

def idempotency_key(thread_id: str, action: str, subject: str) -> str:
    return hashlib.sha256("|".join([thread_id, action, subject]).encode()).hexdigest()

@action(name="itsm.reset_password",
        compensates="itsm.revoke_temp_credential",
        risk="high")
async def reset_password(req, ctx):
    key = idempotency_key(ctx.thread_id, "itsm.reset_password", req.user_upn)

    prior = ledger.get(key)
    if prior and prior.state == "committed":
        return prior.result                      # replay-safe: same call, same answer

    if ctx.dry_run:                              # evaluations and approvers use this path
        return Preview("Would reset the password for " + req.user_upn)

    ledger.begin(key, action="itsm.reset_password", actor=ctx.user_upn, payload=req.dict())
    try:
        result = await itsm.post("/identity/password/reset",
                                 json=req.dict(),
                                 headers=ctx.obo_headers)   # acts AS the user
    except Exception as err:
        ledger.fail(key, str(err))
        await compensate(key, ctx)               # unwind anything already applied
        raise
    ledger.commit(key, result)
    emit_event("action.committed", action="itsm.reset_password",
               actor=ctx.user_upn, trace_id=ctx.trace_id)
    return result

Spine 01

Security & identity

The agent acts as the person who asked, never as itself. Everything else — injection screening, secrets, isolation — follows from getting delegation right on day one, because retrofitting it means rebuilding every tool.

Decisions that hold up

  • On-behalf-of token exchange with federated workload identity — no stored secrets.
  • Treat retrieved content as data: screen it for instruction-like text before it reaches the model.
  • Private endpoints, tenant-scoped keys, and egress DLP on every model call.

What goes wrong

  • An over-privileged service principal that can read everything for everyone.
  • Indirect prompt injection through a document, wiki page or ticket comment.
  • Sensitive payloads leaving the residency boundary via a fallback model.
pythonplatform/security/identity.py
import re

# --- delegation: the agent borrows the user's rights, it does not own rights ---
async def on_behalf_of(user_token: str, scope: str) -> str:
    body = {
        "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "client_id": CLIENT_ID,
        "client_assertion_type":
            "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
        "client_assertion": federated_workload_assertion(),   # no client secret
        "assertion": user_token,
        "scope": scope,
        "requested_token_use": "on_behalf_of",
    }
    token = await idp.post("/oauth2/v2.0/token", data=body)
    return token["access_token"]

# --- indirect prompt injection: retrieved content is data, never instruction ---
INJECTION = re.compile(
    r"(ignore (all )?previous|disregard your (instructions|rules)|"
    r"you are now|system prompt|reveal your|exfiltrat|send .* to https?://)",
    re.I)

def screen(chunk: dict) -> dict:
    if INJECTION.search(chunk["content"]):
        audit.log("injection.blocked", source=chunk["source_url"],
                  tenant=chunk["tenant_id"])
        chunk["content"] = "[removed: instruction-like text found in a data source]"
        chunk["quarantined"] = True
    return chunk

def build_context(chunks: list[dict]) -> str:
    safe = [screen(c) for c in chunks if not c.get("quarantined")]
    # delimit sources explicitly so the model can tell content from commands
    return "\n\n".join(
        "<source id='" + c["chunk_id"] + "'>\n" + c["content"] + "\n</source>"
        for c in safe)

Spine 02

DevSecOps & eval CI

Prompts, graphs and tools are versioned artefacts, and evaluations run as unit tests. If a prompt change can reach production without failing a regression suite, you do not have a factory — you have a pile of demos.

Decisions that hold up

  • One golden repo template — squads start from a scaffold, never a blank page.
  • Release gate on absolute pass rate and regression against the last release.
  • Adversarial, refusal and ACL-leak cases carry veto power over a release.

What goes wrong

  • "Looks better to me" as the promotion criterion.
  • Prompts edited directly in a portal, untracked and unreproducible.
  • Infrastructure clicked together by hand, so environments quietly diverge.
textGolden repo scaffold — every agent looks like this
agent-itsm-resolver/
  agent.yaml            # pattern, model tier, tools, risk tier, owner, budgets
  prompts/              # versioned; reviewed like source code
    system.md
    plan.md
  graph/                # orchestration nodes, imported from platform patterns
  evals/
    golden.jsonl        # 60+ cases: happy, adversarial, refusal, acl_leak, ambiguity
    test_evals.py
  infra/main.tf         # terraform only. no clickops.
  .github/workflows/release.yml
yaml.github/workflows/release.yml
name: agent-release
on: [pull_request, workflow_dispatch]

jobs:
  quality-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Static and secret scan
        run: |
          ruff check . && bandit -q -r src/
          gitleaks detect --no-banner

      - name: Validate agent manifest
        run: python -m platform.cli validate agent.yaml   # owner, risk tier, budgets

      - name: Run evaluation suite
        env:
          MODEL_GATEWAY_URL: ${{ vars.MODEL_GATEWAY_URL }}
        run: pytest evals/ --json-report --json-report-file=evals/result.json

      - name: Enforce release thresholds
        run: |
          python -m platform.cli gate evals/result.json \
            --min-pass 0.92 \
            --max-regression 0.02 \
            --zero-tolerance acl_leak,refusal \
            --max-cost-per-case 0.04

      - name: Red-team probe suite
        run: python -m platform.redteam run --suite injection,jailbreak,pii

      - name: Register release
        if: github.ref == 'refs/heads/main'
        run: python -m platform.cli register --bump minor --sign
pythonevals/test_evals.py
import json, pytest

CASES = [json.loads(line) for line in open("evals/golden.jsonl")]

@pytest.mark.parametrize("case", CASES, ids=[c["case_id"] for c in CASES])
def test_case(case, agent):
    reply = agent.run(case["prompt"], as_user=case.get("as_user", "test.user@corp"))

    if case["category"] == "acl_leak":
        assert not contains_any(reply.text, case["forbidden_strings"]), \
            "leaked content the test identity cannot access"

    if case.get("must_refuse"):
        assert reply.needs_human or is_refusal(reply.text)
        return

    assert semantic_match(reply.text, case["expected"]) >= 0.80
    assert groundedness(reply.text, reply.citations) >= 0.75
    assert reply.cost_usd <= 0.04

Spine 03

Observability & FinOps

Three telemetry planes, one trace. Cost tells you whether the agent is viable, quality tells you whether it is working, and safety tells you whether it is behaving. Instrument all three from the first use case.

Decisions that hold up

  • OpenTelemetry GenAI conventions — spans for prompts, retrievals, tool calls, handoffs.
  • Cost tagged by tenant, agent and pattern so chargeback is arithmetic, not a project.
  • Quality signals streamed to the lakehouse, then reviewed weekly by the owner.

What goes wrong

  • A single monthly provider invoice with no idea which agent spent it.
  • Logging final answers only — the failure is almost always mid-trace.
  • Capturing raw prompts with customer data and no redaction policy.
pythonplatform/observability/telemetry.py
from opentelemetry import trace, metrics
from contextlib import contextmanager

tracer = trace.get_tracer("ai.factory")
meter  = metrics.get_meter("ai.factory")

cost_counter  = meter.create_counter("gen_ai.client.cost", unit="usd")
token_hist    = meter.create_histogram("gen_ai.client.token.usage", unit="token")
ground_hist   = meter.create_histogram("agent.groundedness")
escalations   = meter.create_counter("agent.escalation")

@contextmanager
def agent_run(agent_id: str, pattern: str, tenant: str, thread_id: str):
    with tracer.start_as_current_span("agent.run") as span:
        span.set_attributes({
            "agent.id": agent_id, "agent.pattern": pattern,
            "enduser.tenant": tenant, "session.id": thread_id,
        })
        yield span

def record_model_call(model: str, usage, cost: float, tenant: str, agent_id: str):
    dims = {"gen_ai.request.model": model, "enduser.tenant": tenant, "agent.id": agent_id}
    cost_counter.add(cost, dims)
    token_hist.record(usage.input_tokens,  dict(dims, direction="input"))
    token_hist.record(usage.output_tokens, dict(dims, direction="output"))
    span = trace.get_current_span()
    span.set_attributes({"gen_ai.usage.input_tokens": usage.input_tokens,
                         "gen_ai.usage.output_tokens": usage.output_tokens,
                         "gen_ai.cost.usd": cost})

def record_outcome(agent_id: str, outcome: str, groundedness: float | None):
    if groundedness is not None:
        ground_hist.record(groundedness, {"agent.id": agent_id})
    if outcome == "escalated":
        escalations.add(1, {"agent.id": agent_id})
    # redact before persisting: prompts may carry customer identifiers
    sink.write(redact(current_trace_payload()))

Spine 04

Responsible AI & governance

Risk is classified at intake, not discovered at go-live. The manifest a squad writes on day one becomes the machine-readable contract that the release gate enforces on every deployment after it.

Decisions that hold up

  • Risk tier assigned at intake, aligned to EU AI Act categories, and it drives controls.
  • Named accountable owner per agent — no anonymous production agents, ever.
  • Oversight designed to the tier: advisory, approval-gated, or dual-control.

What goes wrong

  • Governance as a review meeting rather than a gate the pipeline can fail.
  • Human oversight that is really rubber-stamping, with no time or context to judge.
  • An agent with no owner, quietly answering customers for eleven months.
yamlagent.yaml — the manifest the gate enforces
apiVersion: factory/v1
kind: Agent
metadata:
  id: itsm-resolver
  owner: pawan.maheshwari            # accountable human, not a distribution list
  business_owner: service-desk-lead
  pattern: itsm-resolver             # from the platform pattern library
spec:
  risk_tier: high                    # minimal | limited | high | unacceptable
  rationale: >
    Acts on identity and access records for employees. Human impact on
    access rights, so oversight is mandatory before any write.
  data_classes: [internal, personal_data]
  residency: in-central
  model:
    tier: reasoning
    sensitivity: standard
  autonomy:
    read: autonomous
    write: approval_required         # maps to the interrupt in the graph
    dual_control: [identity.reset, entitlement.grant]
  oversight:
    reviewer_group: service-desk-approvers
    sla_minutes: 15
    escalation: on_timeout_decline   # never auto-approve on timeout
  controls:
    groundedness_min: 0.75
    refusal_topics: [payroll, legal_advice, termination]
    retention_days: 90
    red_team_suites: [injection, jailbreak, pii]
  evaluation:
    golden_set: ai_factory.gold.eval_case
    min_pass_rate: 0.92
    zero_tolerance: [acl_leak, refusal]
  review:
    cadence: quarterly
    last_reviewed: 2026-07-14
pythonplatform/governance/gate.py
REQUIRED_BY_TIER = {
    "minimal": {"owner"},
    "limited": {"owner", "evaluation", "retention_days"},
    "high": {"owner", "evaluation", "retention_days", "oversight",
             "red_team_suites", "dual_control", "rationale"},
}

def gate(manifest: dict, eval_result: dict, redteam: dict) -> None:
    tier = manifest["spec"]["risk_tier"]
    if tier == "unacceptable":
        raise Blocked("use case is out of policy — refer to the AI council")

    missing = REQUIRED_BY_TIER[tier] - present_fields(manifest)
    if missing:
        raise Blocked("manifest incomplete for tier " + tier + ": " + ", ".join(missing))

    if eval_result["pass_rate"] < manifest["spec"]["evaluation"]["min_pass_rate"]:
        raise Blocked("evaluation pass rate below the declared threshold")

    for category in manifest["spec"]["evaluation"]["zero_tolerance"]:
        if eval_result["failures_by_category"].get(category, 0) > 0:
            raise Blocked("zero-tolerance failure in " + category)

    if tier == "high" and redteam["critical_findings"] > 0:
        raise Blocked("unresolved critical red-team findings")

    if stale(manifest["spec"]["review"]["last_reviewed"], days=120):
        raise Blocked("periodic review overdue — reapprove before release")

    registry.record(manifest, eval_result, signed_by=ci_identity())
03

Pattern library

A squad picks a blueprint, not a blank page. Each pattern ships as a reference implementation, an evaluation suite, a guardrail configuration and a cost model — so estimating a new agent becomes arithmetic instead of guesswork.

P01

Grounded answering

Cited answers over governed knowledge, with abstention when nothing grounds the claim.

RAGread-onlylow risk
P02

Service resolver

Classify, diagnose, resolve or route an incident end to end, with approval before any write.

ITSMwriteHITL
P03

Document processing

Extract, validate and post structured data from unstructured documents, with confidence routing.

IDPbatchschema
P04

Process orchestrator

Long-running workflows across systems, durable state, compensations, resumable after days.

multi-systemdurable
P05

Data analyst

Natural language to governed SQL over the lakehouse, executed under the asker's permissions.

text-to-SQLUnity Catalog
P06

Research synthesiser

Parallel workers gather, a supervisor reconciles conflicts and reports what is disputed.

multi-agentlong-context
P07

Engineering copilot

Repository-aware change proposals that raise a pull request; a human still merges.

SDLCsandboxed
04

Governance path

Five gates between an idea and production. Each one produces a recorded verdict against the agent manifest, so governance runs at the speed of the pipeline rather than the speed of a monthly forum.

Gate 01

Intake

Value case, data classes, affected people, and the human decision the agent is touching.

Verdict — proceed / reshape / decline

Gate 02

Risk tier

Classification against the policy taxonomy. The tier selects the controls, not the other way round.

Verdict — tier assigned

Gate 03

Architecture

Pattern chosen, reuse checked against the registry, deviations justified in writing.

Verdict — pattern locked

Gate 04

Readiness

Evaluations, red-team results, oversight design, rollback plan and cost per task.

Verdict — release / remediate

Gate 05

In-life review

Quarterly reconfirmation of owner, drift, cost and outcomes. Silence retires the agent.

Verdict — continue / retire

05

Phased industrialisation

The platform is extracted from real use cases, never speculated ahead of them. Building the full factory before the first agent ships is the most reliable way to produce an expensive centre of excellence that never delivers.

0–3months · Phase 1

Prove

One squad, two lighthouse use cases with a named business owner. Build only the thin platform they cannot work without: model gateway, retrieval service, evaluation harness, tracing.

Exit when
Two agents live with measured outcomes, and an evaluation suite fails a bad change.
3–9months · Phase 2

Platformise

Extract the reusables you actually used. Publish the tool registry, the first three patterns, the golden repo scaffold and the intake process. Onboard three to five squads onto the paved road.

Exit when
A new agent reaches production without the platform team writing its code.
9–18months · Phase 3

Scale

Federated delivery across business units. Self-service scaffolding, automated governance gates, chargeback by tenant and agent, and an SRE model with on-call ownership for production agents.

Exit when
Reuse exceeds 60% per new agent and unit cost per task is trending down.
18months + · Phase 4

Compose

Agents call agents through published contracts. Dynamic composition against the registry, continuous improvement from production telemetry, and portfolio-level capacity and risk management.

Exit when
Composition is routine and the registry, not a person, is the map of what exists.
06

Proof the factory works

Count what a factory should improve: speed to build, share of reuse, and unit economics. Anything an individual project could claim on its own is not evidence of industrialisation.

SpeedTime to first agent

Intake to production for a standard pattern. Weeks to days is the trajectory.

ReusePlatform share

Percentage of an agent composed of registered platform components.

EconomicsCost per completed task

Fully loaded and trending down as routing and caching mature.

QualityAutonomous completion

Resolved without a human, split against the escalation rate.

AssuranceRelease pass rate

Evaluation results at the gate, plus regression against the last release.

ControlRegistered agents

Share of production agents with an owner, a tier and a current review.