Authoring a custom runtime
A custom runtime lets you replace Omnia’s built-in PromptKit runtime container with your own image while keeping the rest of the platform — the facade sidecar, the policy broker, sessions, exposure, and identity-aware policy enforcement — unchanged. Use it when you need an orchestration framework Omnia doesn’t ship (a LangChain/LangGraph app, a bespoke agent loop, a house inference stack) but still want Omnia’s protocol translation, tool execution surface, and policy enforcement in front of it.
Your container is the runtime: it serves the
omnia.runtime.v1.RuntimeService gRPC service on the pod-internal runtime port,
and the facade sidecar dials it for every turn. Unlike a
custom facade — which is
Enterprise-licensed — a custom runtime is not license-gated; any AgentRuntime
may declare one.
This guide is the end-to-end contract your container must honour. For the underlying message surface and identity metadata, see Facade ↔ runtime protocol.
Go SDK: pkg/runtime
Section titled “Go SDK: pkg/runtime”Go authors do not need to hand-write the omnia.runtime.v1 wire protocol.
Implement the small Handler interface and runtime.Serve gives you a fully
conformant runtime — hello-first, client-tool round-trips, ServerMessage
marshalling, health, and capability advertisement are all handled for you.
import ( "context" "net"
rt "github.com/altairalabs/omnia/pkg/runtime" "github.com/altairalabs/omnia/pkg/runtime/contract")
type myRuntime struct{}
func (myRuntime) Capabilities() []string { return []string{contract.CapabilityClientTools} }
func (myRuntime) Converse(ctx context.Context, turn rt.Turn, emit rt.Emitter) error { // ... call your model / framework, stream output ... if err := emit.Chunk("hello"); err != nil { return err } return emit.Done(rt.Done{Final: "hello"})}
func main() { lis, _ := net.Listen("tcp", ":9090") _ = rt.Serve(lis, myRuntime{})}Capabilities() must be honest: implement the optional Invoker interface to
advertise invoke, and the optional ConversationProber interface to answer
resume probes. Do not advertise duplex_audio unless you handle duplex
sessions. The Wave-4 conformance suite verifies advertisement matches behaviour.
PromptKit-based runtimes should instead import
github.com/altairalabs/omnia/pkg/runtime/promptkit, which exposes Omnia’s
first-party PromptKit runtime directly.
Prerequisites
Section titled “Prerequisites”- A container image you control, published to a registry the cluster can pull.
- A gRPC server implementation of
omnia.runtime.v1.RuntimeService(any language). Generate your stubs fromapi/proto/runtime/v1/runtime.protoat a pinned git ref — do not hand-copy the file.
Step 1 — Declare the custom runtime
Section titled “Step 1 — Declare the custom runtime”Set spec.framework.type: custom and point image at your container. Only
promptkit has a built-in image; custom (and langchain) must supply one
explicitly, or the AgentRuntime blocks with FrameworkImageUnavailable rather
than silently running PromptKit:
apiVersion: omnia.altairalabs.ai/v1alpha1kind: AgentRuntimemetadata: name: my-agentspec: promptPackRef: name: my-pack framework: type: custom image: ghcr.io/acme/my-runtime:v1.0.0Alternatively an operator can register a default image for a type cluster-wide
with a repeatable --framework-image=custom=ghcr.io/acme/my-runtime:v1.0.0
flag, so individual AgentRuntimes need not repeat it.
Step 2 — Environment and ports
Section titled “Step 2 — Environment and ports”The operator injects the runtime container’s wiring; your container reads these rather than the AgentRuntime CRD. The core set:
| Variable | Value | Notes |
|---|---|---|
OMNIA_AGENT_NAME |
AgentRuntime name | From the Downward API |
OMNIA_NAMESPACE |
Pod namespace | From the Downward API |
OMNIA_GRPC_PORT |
gRPC listen port | Defaults to 9000 — serve RuntimeService here |
OMNIA_HEALTH_PORT |
Health/metrics port | Defaults to 9001 |
OMNIA_PROMPTPACK_PATH |
Path to the compiled PromptPack | The operator mounts the resolved pack here |
The facade dials your gRPC server at localhost:9000 (OMNIA_GRPC_PORT). The
platform-input contract (PromptPack, ToolRegistry, skills, providers) and a
first-class config-source port are still being finalised in later waves of the
custom-runtime epic; the reference example shows the offline devroot approach in
the meantime.
Step 3 — Serve the omnia.runtime.v1 gRPC contract
Section titled “Step 3 — Serve the omnia.runtime.v1 gRPC contract”Implement the RuntimeService methods:
Health(HealthRequest) → HealthResponse— reporthealthy, thecontract_versionyou built against, and yourcapabilities(Step 4).Converse(stream ClientMessage) → stream ServerMessage— the bidirectional turn stream. Handle everyClientMessagefield you may receive (it is not aoneof— several may be set at once); never drop a message part silently. Your firstServerMessageon the stream must be aRuntimeHello(Step 4).Invoke(InvocationRequest) → InvocationResponse— one-shot function mode (spec.mode: function). If you only servespec.mode: agent, leave itUnimplementedand do not advertise theinvokecapability.HasConversation(HasConversationRequest) → HasConversationResponse— report whether a named session’s working context can still be resumed (RESUMABLE/NOT_FOUND/UNAVAILABLE).
Read caller identity from the flat x-omnia-* gRPC metadata (see the
protocol reference);
the raw bearer token is deliberately withheld. Never forward the caller’s
credentials to third-party tool upstreams.
Step 4 — Advertise capabilities and negotiate
Section titled “Step 4 — Advertise capabilities and negotiate”Return the optional surfaces you actually implement in
HealthResponse.capabilities — and send a RuntimeHello as your first
ServerMessage carrying the same set. The known names are listed in the
Capabilities reference;
the set is open, so advertise names outside it freely if you implement new
behaviour.
Advertisement must be honest: the conformance suite fails a runtime that
advertises invoke or duplex_audio but then returns Unimplemented, and the
operator will not schedule a runtime that claims less than the AgentRuntime
requires.
For a duplex session, your RuntimeHello may carry a MediaNegotiation
counter-offer (the audio format you require); the facade relays it to the client
as a session_config message or fails the session closed if it cannot be met.
See
Per-session negotiation.
Step 5 — Verify with the conformance suite
Section titled “Step 5 — Verify with the conformance suite”Omnia ships a protocol conformance suite. Build the CLI and point it at your running runtime:
go build -o runtime-conformance ./cmd/runtime-conformance./runtime-conformance --addr localhost:9000It checks, protocol-only and language-agnostically:
Healthis healthy andcontract_versionis semver;- the first
Converseframe is aRuntimeHellowhose capabilities matchHealth; - a text turn ends with
done, with nodonebefore the hello; - an empty/malformed
ClientMessageis answered on-protocol, never a crash; - capability honesty — an advertised
invoke/duplex_audioworks; an unadvertised one returnsUnimplemented.
A non-zero exit means non-conformant, with a per-check table naming what failed. This is exactly how to measure any runtime’s gap against the contract — including an unsupported LangChain container’s — before you rely on it.
See also
Section titled “See also”- Facade ↔ runtime protocol — the message surface, capability table, and identity metadata.
examples/custom-runtime/av-preprocessor/— a minimal conformant runtime with an A/V-preprocessing seam.- Authoring a custom facade — the mirror image (BYO facade, Enterprise-gated).