Skip to content

Authoring a custom runtime

Run your own container as an agent’s runtime by serving omnia.runtime.v1.RuntimeService and declaring the image on the AgentRuntime. The facade sidecar dials your container for every turn, and the policy broker, sessions and exposure stay as they are. A custom runtime needs no licence entitlement. A custom facade gets an admission warning when the Enterprise webhook is on and the licence lacks the custom-facade entitlement, and is admitted.

A minimal conformant runtime lives at examples/custom-runtime/av-preprocessor/ in the Omnia repository. examples/custom-runtime/ also shows how to run a runtime offline against a directory of manifests.

  • A container image in a registry the cluster can pull from.
  • A gRPC server for omnia.runtime.v1.RuntimeService, in any language. Generate stubs from api/proto/runtime/v1/runtime.proto at a pinned git ref and do not hand-copy the file.
  • A checkout of the Omnia repository, to build the conformance CLI in step 5.

Set spec.framework.type: custom and spec.framework.image. Only promptkit has a built-in image. A custom or langchain AgentRuntime with no image blocks with FrameworkImageUnavailable.

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: AgentRuntime
metadata:
name: my-agent
spec:
promptPackRef:
name: my-pack
framework:
type: custom
image: ghcr.io/acme/my-runtime:v1.0.0

To set the image for every AgentRuntime of a type, pass the repeatable operator flag --framework-image=custom=ghcr.io/acme/my-runtime:v1.0.0. spec.framework.image takes precedence over the flag.

Step 2: Read the environment and open the ports

Section titled “Step 2: Read the environment and open the ports”

The operator injects the runtime container’s wiring. Read these variables instead of the AgentRuntime CRD.

VariableValue
OMNIA_AGENT_NAMEAgentRuntime name (from a pod label)
OMNIA_NAMESPACEPod namespace
OMNIA_GRPC_PORT9000. Serve RuntimeService here.
OMNIA_HEALTH_PORT9001. Serve HTTP GET /healthz here.
OMNIA_PROMPTPACK_PATHThe PromptPack file, /etc/omnia/pack/pack.json. Unset for a live pack.

The readiness and liveness probes issue GET /healthz against the health port. A runtime that does not serve it never becomes Ready.

The facade dials localhost:9000. To run a runtime outside a cluster, set OMNIA_CONFIG_DIR to a directory of YAML manifests. Omnia’s Kubernetes client then serves the CRDs from those files instead of an API server. examples/custom-runtime/devroot/ is a working example.

Implement the four RuntimeService methods below. Two more are optional, and the facade tolerates Unimplemented from either:

  • EndConversation: the facade calls it when a WebSocket conversation ends. Implement it only if you hold per-conversation resources worth releasing.
  • WatchInvocation (contract 1.8.0): a stream of one function invocation’s tool calls and tool results, which the facade relays to a caller that asked for an event stream. Without it, that caller gets the outcome with no progress. If you implement it, send ready first, end the stream once the invocation has finished, and serve it to loopback callers only.
MethodBehaviour
HealthReturn healthy, the contract_version you built against (semver), and your capabilities.
ConverseA bidirectional stream of ClientMessage and ServerMessage. ClientMessage is not a oneof, so several fields can be set at once: handle each one you receive and do not drop a part silently. Your first ServerMessage is a RuntimeHello.
InvokeOne-shot function mode (spec.mode: function). If you only serve spec.mode: agent, return Unimplemented and do not advertise invoke.
HasConversationReturn RESUMABLE, NOT_FOUND or UNAVAILABLE for a session’s working context.

Read caller identity from the flat x-omnia-* gRPC metadata. The raw bearer token is withheld. Do not forward the caller’s credentials to third-party tool upstreams. See identity and claims metadata for the keys.

Implement Handler from github.com/altairalabs/omnia/pkg/runtime and call runtime.Serve. The SDK sends the RuntimeHello first, answers Health, runs client-tool round-trips and registers the standard gRPC health service. If Converse returns without calling Done, the SDK sends an empty one.

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 or framework and stream output.
if err := emit.Chunk("hello"); err != nil {
return err
}
return emit.Done(rt.Done{Final: "hello"})
}
func main() {
lis, _ := net.Listen("tcp", ":9000")
_ = rt.Serve(lis, myRuntime{})
}

To serve Invoke, implement the optional Invoker interface and list contract.CapabilityInvoke in Capabilities(). To answer resume probes, implement ConversationProber. A handler without it reports every session as UNAVAILABLE. The SDK refuses duplex sessions, so do not advertise duplex_audio from it.

To wrap Omnia’s own PromptKit runtime, import github.com/altairalabs/omnia/pkg/runtime/promptkit.

List the optional surfaces you implement in HealthResponse.capabilities, and send the same list in the RuntimeHello. The capabilities table lists the known names. The set is open, so you can advertise a name outside it.

Advertise only what works. The conformance suite fails a runtime that advertises invoke or duplex_audio and then returns Unimplemented. The operator also requires duplex_audio and interruption for an AgentRuntime with duplex enabled, and invoke for one with a rest or mcp facade. If the runtime advertises less, the operator scales the agent to zero.

For a duplex session, the RuntimeHello can carry a MediaNegotiation counter-offer. The facade relays an audio counter-offer to the client as a session_config message and fails the session closed on a video one. See per-session negotiation.

Build the CLI from the repository root and point it at your running runtime.

Terminal window
go build -o runtime-conformance ./cmd/runtime-conformance
./runtime-conformance --addr localhost:9000

The suite checks, over gRPC only:

  • Health is healthy and contract_version is semver.
  • The first Converse frame is a RuntimeHello whose capabilities match Health.
  • A text turn ends with done, and no done arrives before the hello.
  • An empty ClientMessage gets an on-protocol answer, not an internal stream error.
  • Capability honesty: with invoke advertised, Invoke works. Without it, Invoke returns Unimplemented. With duplex_audio advertised, a duplex session emits a frame. Without it, the duplex check is skipped.

The CLI prints a per-check table. It exits 0 when conformant, 1 when any check fails and 2 when it cannot dial the address.

After you apply the AgentRuntime, confirm the operator accepted the runtime’s capabilities.

Terminal window
kubectl get agentruntime my-agent -o jsonpath='{.status.runtimeCapabilities}'
kubectl get agentruntime my-agent \
-o jsonpath='{.status.conditions[?(@.type=="CapabilitiesSatisfied")].status}'

The first command prints the capability list your runtime reported. The second prints True.

The AgentRuntime reports FrameworkImageUnavailable

Section titled “The AgentRuntime reports FrameworkImageUnavailable”

The custom type has no built-in image. Set spec.framework.image, or start the operator with --framework-image=custom=<image>.

The probes call GET /healthz on OMNIA_HEALTH_PORT. Serve that route, in addition to the gRPC port.

CapabilitiesSatisfied is False with reason CapabilitiesMissing

Section titled “CapabilitiesSatisfied is False with reason CapabilitiesMissing”

The runtime advertises less than the spec requires. Add the missing capability to your runtime or change the spec. The condition message names the missing and advertised capabilities.