Serving Extensions¶
An extension is a small HTTP service that Agent Studio calls. It runs in its
own pod, next to the agents. latent.extension writes one in Python, and
latent serve --extension runs it. Studio's Node SDK
(@agent-studio/extension-server in agent-studio) speaks the same contract.
An extension can serve:
- gates — Studio calls a gate in one lifecycle phase
(
before_conversation,before_agent,after_agent,after_conversation), and the gate decidescontinue,veto,escalateorinject; - processors — Studio sends the conversation; a
terminalprocessor scores it after it ends, aliveprocessor classifies it while it runs; - routes — Studio serves each route at
/x/<extension>/<path>, to signed-in Studio users (studio-user) or to anyone (public).
The extension only declares what it serves. A Studio admin decides which gates
run where (bindings) and which processors run (processor configs). The Studio
side is documented in agent-studio's docs/extensions.md.
Write an extension¶
import hashlib
from latent.contracts import ConversationData, GateRequest
from latent.extension import (
Extension, ExtensionRequest, GateDecision, ProcessorResult,
cont, processed, veto,
)
ext = Extension("demo-rules")
@ext.gate(phase="before_conversation", name="g8_probe", timeout_ms=800)
async def g8_probe(req: GateRequest) -> GateDecision:
"""Veto customer keys that start with deny:."""
if (req.customer_key or "").startswith("deny:"):
return veto("denied_by_rule")
return cont()
@ext.processor(name="reply_length", when="terminal", processor_type="scale",
timeout_ms=5000, display_name_key="processors.replyLength")
async def reply_length(conv: ConversationData) -> ProcessorResult:
"""Score the total reply length (1.0 at 1000 characters)."""
chars = sum(len(m.content) for m in conv.messages if m.role == "assistant")
return processed({"chars": chars}, score=min(chars / 1000, 1.0))
@ext.route("whoami", methods=["GET"], auth="studio-user", roles=["admin", "sme"])
async def whoami(request: ExtensionRequest) -> dict[str, str | None]:
"""The signed-in Studio user."""
return {"id": request.studio_user.id, "role": request.studio_user.role}
@ext.route("public/echo", methods=["POST"], auth="public")
async def echo(request: ExtensionRequest) -> dict[str, str]:
"""The sha256 of the raw request body."""
return {"sha256": hashlib.sha256(request.raw_body).hexdigest()}
Gates¶
@ext.gate(phase=..., name=..., timeout_ms=...) declares an
async (GateRequest) -> GateDecision handler. Build the decision with cont(),
veto(reason), escalate(reason) or inject(**fields).
timeout_msmust be within 100–5000 ms. Studio gives the gate its full timeout or does not call it.- The gates of one phase share a budget: 3000 ms for
before_conversation, 12000 ms forbefore_agent, 20000 ms for eachafter_*phase.Extensionrefuses a declaration that would exceed it. needs_ticket=Truemeans Studio calls the gate only when the channel has ticket facts (the Glassix claim path). The request then carriesticket,handoffsand the top-levelhandoffs_lookback_hoursthose hand-offs were read for.
Processors¶
@ext.processor(...) declares an async (ConversationData) -> ProcessorResult
handler. Return processed(result, score=..., tags=[...]) or
processor_failed(error).
processor_typeisthumbsorscale.- A
terminalprocessor may take up to 60 s; aliveone up to 10 s. - A
liveprocessor's first tag becomes the conversation's classification tag.
Routes¶
@ext.route(path, methods=[...], auth=...) declares a route. The handler
receives an ExtensionRequest (method, path, params, query, headers,
raw_body, studio_user) and returns JSON-ready data or a Starlette
Response.
- A
studio-userroute receives the user inrequest.studio_user. Studio refuses users whose role is not inroles. - A
publicroute must start withpublic/. It takes noroles. - A path segment is a literal or one
{param}, for exampleitems/{itemId}. max_body_kb(default 256) andtimeout_ms(default 10000) bound a call.verify_headers(public routes only, up to 4 lowercase names) lists the headers a third party signs its callback with, for example["x-hub-signature-256"]or["stripe-signature"]. Studio forwards exactly those, so the handler can verify the sender overraw_body; it never forwards cookies, credentials or headers it reserves. A manifest that usesverify_headersdeclares contracts revision 2 and needs a Studio that reads it.
Serve it¶
Run from the directory that holds the module:
# Print the manifest (canonical JSON) and exit.
latent serve --extension ext:ext --manifest
# Serve locally on :8080, unregistered. K is LATENT_SERVE_SECRET or random.
STUDIO_ORIGIN=http://localhost:3000 latent serve --extension ext:ext
A local server keeps its own nonce store and never calls Studio. STUDIO_ORIGIN
is required only when the extension has studio-user routes: the user
assertions Studio sends name it.
Deployed mode¶
When LATENT_EXTENSION_VERSION is set, the pod registers itself with Studio
(POST /api/extensions/register) and re-registers at half the lease. It needs:
| Variable | Meaning |
|---|---|
LATENT_EXTENSION_VERSION |
The version, ^[a-z0-9]{1,16}$. The serving-operator sets it. |
LATENT_SERVE_SECRET |
The HMAC key K, shared by every replica of the version. |
LATENT_SERVE_PUBLIC_URL |
The version's Service URL, http://x-<name>-<version>-backend.<namespace>.svc.cluster.local. |
AGENT_STUDIO_URL |
Studio's base URL. |
AGENT_STUDIO_API_KEY |
An lsk_ project key scoped to this extension. |
STUDIO_ORIGIN |
The Studio origin user assertions name. |
infra/Dockerfile.extension builds the image. Its entrypoint
(infra/latent-extension-entrypoint.sh) runs
latent serve --extension "$LATENT_EXTENSION_ENTRY".
Registration failures do not crash the pod:
- 401 or 403 — the key is wrong or scoped to something else. The process exits non-zero.
- Anything else —
/readyzstays 503 and the pod retries with backoff (1–60 s, full jitter). - 409
HmacKeyConflict— the version is live in Studio with a different K. This means its Secret changed under a live version. The pod logs the conflict at error on every retry and stays unready; the other replicas keep serving. Restore the Secret, or ship the next version with its own Secret (x-<name>-<next version>-secrets). Never edit the Secret of a live version.
The pod never deregisters. Its registration lapses with the lease, so one replica's shutdown never takes the version from the others.
Check it: conformance¶
latent conformance runs the Studio contract checks against a running pod:
LATENT_SERVE_SECRET=<K> latent conformance --url http://extension:8080 \
--manifest manifest.json --report conformance.json \
--studio-origin http://localhost:3000
It checks health and readiness, that unsigned, wrongly signed and replayed
calls get 401, that the signed GET /v1/manifest equals the extracted
manifest, 404 for undeclared names, 422 for invalid bodies, the phase budgets,
and every 200 body against its schema. The example calls are reported but do not
block, since a CI network cannot reach customer systems. The command exits 0
only when every blocking check passed. --studio-origin must equal the pod's
STUDIO_ORIGIN when the extension has studio-user routes. Pass K through the
environment rather than --secret, so it stays out of process listings.
Register and promote¶
CI registers a built version with its manifest and passing report:
latent studio agents register-version --kind extension \
--name demo-rules --version v1 --image <registry>/<repo>@sha256:<digest> \
--manifest manifest.json --conformance conformance.json
agent-studio's reusable workflow build-extension.yml runs build, manifest,
conformance on an internal Docker network, and this registration.
Registering serves nothing. Promote, inspect and roll back with:
latent studio extensions versions demo-rules
latent studio extensions promote demo-rules v1
latent studio extensions rollout demo-rules
latent studio extensions rollback demo-rules
latent studio extensions clear demo-rules
An extension has one active version and a last known good one; there are no
weights. clear leaves no active version, so every required gate falls back to
Studio's outage rule.
See also¶
- Pod events — events an agent handles inside its serving pod.
- Serving Agents — the agent pod, HMAC signing and registration.