Skip to content

SIP edge modularization and pluggable voice agent runtimes

Status: Draft | Last reviewed: 2026-07-22

This doc analyzes carving the SIP/media services into a standalone, centrally-maintained "TrueServe SIP Edge" module — deployable inside the main stack (compose/Helm, unchanged) and independently via CDK — and making the AI runtime behind it pluggable. The flagship scenario: an Amazon Connect external voice transfer lands on the SIP edge, which fronts either the real ai-orchestrator or a voice agent running on Amazon Bedrock AgentCore with Strands bidirectional streaming. It also designs an agentcore-sdk export format so TrueServe voice agents can be packaged for AgentCore. Read it before starting any of the phased work in the roadmap.

Decisions already made: CDK is the first IaC target (CloudFormation/Terraform later); the Connect entry path is SIP external transfer via an Amazon Chime SDK Voice Connector. Non-goals: replacing the existing Helm/compose deployment paths, and non-SIP Connect integrations (see Appendix A).

Current state: Connect SIP ingress already works

No new ingress work is needed. The Amazon Connect path is live end to end today:

flowchart LR
  C[Amazon Connect<br/>external voice transfer] --> VC[Chime SDK<br/>Voice Connector]
  VC -->|"SIP INVITE<br/>X-Amzn-SourceArn"| K[Kamailio]
  K -->|NG control| R[rtpengine]
  K -->|SIP :5061| A[Asterisk]
  A -->|"Stasis(ai-bridge)"| ARI[ari-controller-node]
  ARI -->|"POST /session + externalMedia"| MP[media-proxy]
  MP -->|"gRPC VoiceAgent/Stream"| O[ai-orchestrator]
  • Kamailio admits the call via a dedicated X-Amzn-SourceArn matcher (kamailio.cfg:519), after a source-IP ACL against sip_provider_endpoints.ip_cidrs. Connect calls get media profile RTP/AVP (plain RTP; answering SAVP breaks audio).
  • Asterisk's ccaas-inbound-handler extracts the full Connect header set (X-Amzn-SourceArn, X-VoiceConnector-ID, X-Amzn-ConnectContactId, X-Amzn-ConnectInitialContactId, X-Amzn-SourceAccount) into Stasis args (extensions.conf).
  • ccaas-detector.ts maps X-Amzn-SourceArn to an amazon_connect integration; the default-deny ccaas_pipeline_integrations allow-list gates admission (see ari-controller).
  • Hand-back to Connect works: connect-attributes.ts writes session-end parameters via UpdateContactAttributes before the BYE, with the chime-sip-attrs Lambda as a CloudWatch-log fallback.

The SIP set is also already a proven seam from the cluster side: values-profile-no-sip.yaml disables exactly this service set, and FEATURE_SIP=false runs the dev stack without it. The work is therefore packaging and the runtime seam — not new call-path code.

Module boundary: TrueServe SIP Edge

The module is five services: kamailio, rtpengine, asterisk (+asterisk-worker), ari-controller-node, media-proxy. Janus and coturn are deliberately excluded — they serve browser WebRTC, which a SIP-only edge for Connect transfers does not need (the Helm no-sip profile lists all eight; this module narrows to the SIP-transfer path).

The same container images serve all three deployment shapes (dev compose, EKS Helm, standalone CDK); only configuration and a seed dataset differ.

Coupling cuts per service

Service Coupling today Standalone cut
kamailio Postgres via sqlops: sip_provider_endpoints, sip_digest_auth, sip_logs, integrations join Minimal 4-table schema; the integrations join can be denormalized into sip_provider_endpoints
rtpengine None (env only) None needed; keep the dual-interface invariant (below)
asterisk ODBC realtime ps_* tables; func_odbc reads agent_versions for extension routing Static pjsip.conf endpoints; skip func_odbc — header-routed CCaaS calls never use extension lookup
ari-controller-node integrations, agents, agent_versions, ccaas_pipeline_integrations; Redis (required); ai-manager credential API Minimal schema + seed job; small Redis; credential resolution is already fail-soft to the AWS default provider chain (integration-credentials.ts) — standalone mode relies on IAM roles. ari-controller never holds MASTER_ENCRYPTION_KEY (config.ts:106)
media-proxy No Postgres; orchestrator gRPC target; optional Redis allocator None beyond the routing change in Per-agent orchestrator routing

The standalone profile ships a small Postgres (RDS or container, tier-dependent) provisioned by a seed job that extends deploy/edge-templates/init-edge.sql: the minimal table set above, plus the ccaas_pipeline_integrations allow-list rows and sip_provider_endpoints rows (Chime SBC CIDRs) derived from deploy-time parameters.

Warning: rtpengine behind EC2 + Global Accelerator must run dual-interface (pub/${PUBLIC_IP}!${PUBLIC_IP};priv/${LAN_IP}). Single-interface kills inbound RTP silently. This invariant carries into the CDK app unchanged.

Schema ownership

The edge schema subset must not drift from the mothership baseline (deploy/init-scripts/000_full_schema.sql). The doc recommends generating init-edge.sql's table DDL from the baseline (script + CI drift check, mirroring schema-drift.yml) rather than hand-maintaining a fork. Open question 6 tracks this.

The TrueServe Voice Agent Contract

The entire voice runtime interface is one bidirectional gRPC RPC, defined in voice.agent.proto:

service VoiceAgent {
  rpc Stream (stream ClientMessage) returns (stream ServerMessage);
}

This analysis elevates it to a named, versioned contract — TrueServe Voice Agent Contract v1 — that any runtime (the real ai-orchestrator, or an external service) can implement. Normative wire semantics, all already implemented by every client (media-proxy, genesys/five9/webex connectors, conversation-api):

Requirement Detail
First client message SessionConfig — session_id, agent_id, modalities, metadata map (session_variables, ani, dnis, custom SIP headers), proposed audio formats
First server message ServerHello — authoritative input/output audio formats; the server may override what the client proposed
Audio AudioChunk raw bytes (never base64); PCM16 mono 16 kHz operational default; G.711 µ-law/A-law supported
Barge-in Server sends Control TYPE_INTERRUPTION when the user interrupts; the client flushes its playout buffer
Session end Server sends Control TYPE_DISCONNECT; client drains pending audio then tears down. TYPE_BOT_END carries disposition
Mid-call migration Server may send Control TYPE_SESSION_MIGRATING (value = {"window_seconds": N}, default 10 s, client caps at 30 s) — the client suppresses its teardown paths for the bounded window, then TYPE_SESSION_ACTIVE disarms it; window expiry resumes the suppressed teardown so a failed migration never strands a leg. Optional: a client that ignores both types keeps today's behavior
DTMF DtmfEvent bidirectional (caller keypresses in; agent-driven IVR digits out)
Greeting Server-initiated on stream connection; no client trigger message exists
Health gRPC health service registered under the name trueserve.VoiceAgent

Equally important is what an external implementation must not need — these are ai-orchestrator internals, not contract:

  • Postgres agent resolution (resolve_agent_version*) and conversations persistence
  • The ConversationBridge gRPC stream to conversation-api (transcripts, agent-assist)
  • Redis mcp:session:{id} registration (A2UI MCP)
  • The ari-controller HTTP consult API (warm transfer)
  • MinIO recording

The caller supplies only agent_id + metadata; all config resolution is server-side. An external runtime resolves its own agent identity (for the AgentCore bridge, the AgentCore runtime ARN is the agent identity — see below).

Per-agent orchestrator routing

Today no per-agent routing exists: every connector points at one static address (PIPECAT_GRPC in media-proxy, ORCH_ADDR in the Go connectors), and media-proxy dials a single shared connection (allocator.go:100-165). To front different agents with different runtimes, close this gap.

Recommended: option A — ari-controller resolves the target per session. ari-controller already owns integration resolution (ccaas-detector) and already calls media-proxy POST /session with per-call metadata. Add an optional orchestrator_target (host:port + TLS flag) to the CCaaS mapping (an integrations.config key or a column on ccaas_pipeline_integrations), pass it in the /session body, and have media-proxy keep a small per-target connection cache instead of one global conn. This mirrors the existing per-instance precedent, AgentConfig.orchestrator_target in agent_control.proto, and gives per-call granularity with the smallest change surface.

Alternatives considered and rejected for v1:

  • DNIS→target map inside media-proxy — duplicates routing knowledge media-proxy doesn't otherwise have; ari-controller is the natural owner.
  • A dedicated gRPC routing proxy — an extra hop and service for a lookup ari-controller already performs.

Note: an orchestrator_target pointing off-cluster cannot ride today's plaintext :50051. Off-cluster targets require TLS on the gRPC leg or terminate inside the module at the bridge (the recommended pattern below keeps gRPC in-VPC and lets the bridge speak SigV4-authenticated WSS outward).

agentcore-voice-connector: the gRPC ↔ AgentCore bridge

A new, small Go service that implements the VoiceAgent server side of the contract and, per session, dials the AgentCore Runtime bidirectional WebSocket:

wss://bedrock-agentcore.<region>.amazonaws.com/runtimes/<agentRuntimeArn>/ws

AgentCore Runtime natively supports this shape: containers serve a WebSocket on :8080 /ws (plus GET /ping), clients authenticate with SigV4 or OAuth, and AWS ships samples including a Strands BidiAgent voice agent (see runtime bidirectional streaming and the WebSocket samples).

The bridge ships with the SIP Edge module (present in its compose/Helm/CDK profiles) but stays a separate service, never fused into media-proxy. That keeps the gRPC contract the only interface: choosing the real orchestrator versus AgentCore is purely a per-session routing-target decision.

Bridge responsibilities

Concern Design
Agent identity Bridge maps SessionConfig.agent_id (or a bridge-side table) → AgentCore runtime ARN; one bridge instance serves many agents
Session correlation Derive the ≥33-char X-Amzn-Bedrock-AgentCore-Runtime-Session-Id deterministically from session_id (e.g. tsf-<session_id>-<pad>), so traces correlate across both systems
Wire schema over WS The JSON/binary event schema defined once and shared with the export format (below): session_start (SessionConfig-equivalent payload incl. session_variables), binary audio frames in/out, control (interruption, disconnect, bot_end), dtmf
Framing AgentCore caps WS frames at 32 KB; at 16 kHz PCM16 (~32 KB/s) chunk audio at ≤500 ms — 20–100 ms frames recommended for latency anyway
Audio ServerHello declares PCM16 16 kHz in / provider-native out (e.g. 24 kHz for Nova Sonic / OpenAI Realtime); resample in the bridge only if the far side can't match
Interruption Strands BidiAgent VAD interruption events map to Control TYPE_INTERRUPTION toward media-proxy; client-side interruptions forward as a control event
Greeting The contract expects server-initiated greeting on connect. Cold-start latency on the first session to a runtime is a real risk — the bridge should support optional comfort audio/ring-continuation until the first agent audio arrives
Timeouts Nova Sonic sessions cap at 8 minutes; AgentCore idle timeout defaults to 15 minutes (reset by message activity, including ping/pong). Bridge sends WS keepalives and, on provider cap, either ends gracefully with TYPE_BOT_END or re-dials with context replay (open question 2)
Custom context X-Amzn-Bedrock-AgentCore-Runtime-Custom-* headers carry ani/dnis/tenant metadata into the agent container at connect time

Fidelity loss versus the real orchestrator

Capability Status behind the bridge Mitigation
Transcripts / conversation persistence (ConversationBridge) Lost — AgentCore agent doesn't speak it Optional bridge-side tap: forward transcript events from the WS schema to conversation-api; v2
Warm transfer / consult Unsupported (and factory.ts has no Amazon Connect CallControlProvider yet regardless) Declare unsupported in v1; Connect-side hand-back via contact attributes covers "return to Connect"
Recording (MinIO) Lost AgentCore observability + Connect-side recording; bridge-side tap possible later
A2UI / multimodal Lost Out of scope for phone calls
Draft testing, preflight, agent versioning N/A — agent lifecycle lives in AgentCore The export flow (below) is the authoring path

agentcore-sdk export format

The pipelines export route already supports ?format=strands — a generated, runnable Python package with prompt extraction, tool/MCP bindings, secret scrubbing to env placeholders plus secret_report.json, and unsupported.json for non-portable features (lib/agent-code-export/; sample output in agent-3032-strands/). The route reserves the format name agentcore-sdk for exactly this next step (route.ts:123-125).

?format=agentcore-sdk builds on the strands core to emit:

  • A BedrockAgentCoreApp entrypoint with an @app.websocket handler speaking the same WS wire schema the bridge expects — defined once (a small schema file beside voice.agent.proto) and consumed by both the bridge and the export generator, so an exported agent is callable through the SIP edge with zero adapter code.
  • A Strands BidiAgent wiring the pipeline's S2S stage to the matching bidi provider (Nova Sonic / OpenAI Realtime / Gemini Live), tools carried over from the strands export core.
  • A linux/arm64 Dockerfile + AgentCore project config for agentcore create/agentcore deploy.
  • secret_report.json re-binding guidance targeting Secrets Manager / runtime env.

Scope v1: S2S (speech-to-speech) pipelines only. Cascaded STT→LLM→TTS pipelines would need a pipecat-equivalent runtime inside the container — defer; the exporter writes them to unsupported.json as it already does for other non-portable features.

CDK packaging

The CDK app lives at infra/cdk/sip-edge/ (TypeScript); shared artifacts (compose profile, init-edge.sql, config templates) stay in deploy/edge-templates/ so all three deployment shapes consume one source. The direct precedent to port is gcp-voice-agent-poc/iac/ — an existing CDK app that already stands up an EC2 Kamailio+rtpengine SBC (bootstrap script + config templates, config-driven via lib/config.ts), a Chime Voice Connector, and Connect flow wiring.

Construct Choice Why
SBC (kamailio + rtpengine) EC2 (ASG of 1+, EIPs) Kernel-level packet handling, host networking, the dual-interface invariant; proven by the PoC and the EC2 dev flavor
asterisk, ari-controller-node, media-proxy, agentcore-voice-connector ECS Fargate Stateless, no host-network need at this tier
Ingress NLB with EIP allocations (mixed TCP/UDP) Mirrors infra/stacks/aws-kms (Terraform EIPs on the kamailio NLB)
Telephony Chime SDK Voice Connector + Connect contact-flow wiring + Route53 Port from the PoC stack
Data RDS Postgres (small) + ElastiCache Redis; container Postgres option for a dev tier Only the minimal edge schema
Seeding A deploy-time job parameterized with SourceArn → agent → orchestrator_target rows Populates sip_provider_endpoints, integrations, ccaas_pipeline_integrations
Optional AgentCore runtime deployment for exported agents (extends the edge-templates terraform AgentCore skeleton, in CDK) One-command demo path
Attributes hand-back The chime-sip-attrs Lambda + DynamoDB Already exists; port its Terraform to CDK

Images come from the existing CI registry (per-service Dockerfiles; kamailio is an upstream vendor image) — the CDK app pulls, it does not build.

Phased roadmap

  1. Contract + standalone profile. Write the Voice Agent Contract v1 doc section into the handbook; build the standalone compose profile (minimal schema + seed job) and prove a Connect transfer terminates on it against a stock ai-orchestrator.
  2. Routing + bridge. Implement per-session orchestrator_target (option A); build agentcore-voice-connector and validate against the AWS echo sample, then the Strands BidiAgent sample.
  3. CDK app. infra/cdk/sip-edge/ end to end: Connect call → SIP edge → AgentCore voice agent.
  4. Export. ?format=agentcore-sdk; round-trip test — export a TrueServe S2S agent, agentcore deploy, call it through the edge.
  5. Hardening. Transcript tap to conversation-api, observability correlation, CFN/Terraform ports, Connect CallControlProvider for outbound consult if needed.

Risks and open questions

  1. Chime VC media specifics — confirm production codec set and SRTP posture on the Voice Connector leg (today's profile is plain RTP/AVP for Connect); G.711 8 kHz on the carrier leg bounds effective audio quality regardless of 16 kHz internally.
  2. Nova Sonic 8-minute cap — decide rollover (re-dial WS mid-call with context replay) vs accepting the cap for v1; OpenAI Realtime (60 min) and Gemini Live differ.
  3. Keepalive/framing — 15-min idle reset semantics and 32 KB frames need soak testing under real hold music/silence.
  4. Fidelity gaps — is contact-attribute hand-back a sufficient "transfer" story for the target use cases, and is the transcript tap a launch requirement or v2?
  5. Allow-list seeding UX — how operators express SourceArn → agent → orchestrator_target in CDK parameters without hand-editing SQL.
  6. Edge schema ownership — generate from the mothership baseline with a drift check, or hand-maintain init-edge.sql?
  7. rtpengine distribution — EC2 AMI/bootstrap maintenance cost, and confirm the GPL distribution posture for a customer-deployable artifact.
  8. Off-cluster gRPC security — TLS/mTLS for any orchestrator_target outside the VPC (the bridge pattern avoids this by keeping gRPC in-VPC).
  9. Greeting latency — AgentCore session cold start vs the contract's server-initiated greeting; quantify and decide on comfort audio.
  10. Session-id correlation — finalize the deterministic mapping to the ≥33-char AgentCore session id for cross-system observability.

Appendix A: non-SIP alternatives

Path Shape Verdict
connect-kvs-connector Connect media streaming → KVS GetMedia → gRPC One-way only (no TTS injection back to the caller); useful for transcription/assist, not conversational agents
AgentCore WebRTC (KVS TURN) Browser/mobile ↔ AgentCore direct No SIP edge at all — right for web/mobile surfaces, irrelevant to Connect telephony transfers
AudioHook-style connector for Connect Would mirror genesys-connector-go Connect has no equivalent bidirectional media WebSocket for third parties; the SIP transfer path is the supported bidirectional integration

See also