Skip to content

feat(agent-server): realtime voice broker for Insider controller conversations - #5180

Draft
enyst wants to merge 21 commits into
mainfrom
feat/insider-voice-realtime
Draft

enyst wants to merge 21 commits into
mainfrom
feat/insider-voice-realtime

Conversation

@enyst

@enyst enyst commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

HUMAN:
Experimental branches. Testing live voice with an Insider conversation that knows your other conversations and helps you keep on track.
This is a bit of a combination between the way of working with a personal agent, and the way of working with a visual web interface for conversations: one of the conversations helps you more keep track of the others.


AGENT:

Why

Realtime voice should reach the selected saved Insider Cat, execute work there, and speak its persisted result. An intermediate relay turn could previously omit the forwarding step.

Summary

 VoiceClient → authenticated, conversation-bound Voice API
   default OpenAI → WebRTC setup; delegation owned by client
   opt-in Codex   → dedicated sign-in home; server relay
                      handoff ID reserved before dispatch
                        ↓ one accepted message/run
                      selected saved Cat + tools + stop hooks
                        ↓ persisted answer
                      speak result

 end call → release voice transport; accepted Cat work continues
 condense → invoke condenser only; never execute pending tools

Add availability, call setup, status and teardown APIs plus the TypeScript VoiceClient and ConversationManager.voice. Codex handoffs go directly to the saved conversation, with replay protection and fixed error categories distinguishing unsent work, uncertain results and transport failure. Explicit condensation returns 409 without a compatible condenser and does not advance pending approvals.

Source: voice_router.py · codex_voice.py · voice-client.ts · local_conversation.py.

REST API contract changes

Compared with base OpenAPI 60192ed54db7 for public /api/** paths.

--- base public OpenAPI
+++ head public OpenAPI
@@ -6,0 +7 @@
+operation DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} operationId=end_realtime_call_api_conversations__conversation_id__voice_realtime__call_id__delete
@@ -37,0 +39,2 @@
+operation GET /api/conversations/{conversation_id}/voice operationId=voice_availability_api_conversations__conversation_id__voice_get
+operation GET /api/conversations/{conversation_id}/voice/realtime/{call_id} operationId=realtime_call_status_api_conversations__conversation_id__voice_realtime__call_id__get
@@ -132,0 +136 @@
+operation POST /api/conversations/{conversation_id}/voice/realtime operationId=create_realtime_call_api_conversations__conversation_id__voice_realtime_post
@@ -171,0 +176,2 @@
+parameter DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} path:call_id required=true schema=type="string" pattern="^[A-Za-z0-9_-]{1,128}$"
+parameter DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} path:conversation_id required=true schema=type="string" format="uuid"
@@ -227,0 +234,3 @@
+parameter GET /api/conversations/{conversation_id}/voice path:conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{conversation_id}/voice/realtime/{call_id} path:call_id required=true schema=type="string" pattern="^[A-Za-z0-9_-]{1,128}$"
+parameter GET /api/conversations/{conversation_id}/voice/realtime/{call_id} path:conversation_id required=true schema=type="string" format="uuid"
@@ -346,0 +356 @@
+parameter POST /api/conversations/{conversation_id}/voice/realtime path:conversation_id required=true schema=type="string" format="uuid"
@@ -400,0 +411 @@
+requestBody POST /api/conversations/{conversation_id}/voice/realtime application/json required=true schema=RealtimeOffer
@@ -438,0 +450,2 @@
+response DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} 200 application/json schema=Success
+response DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} 422 application/json schema=HTTPValidationError
@@ -509,0 +523,4 @@
+response GET /api/conversations/{conversation_id}/voice 200 application/json schema=VoiceAvailability
+response GET /api/conversations/{conversation_id}/voice 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{conversation_id}/voice/realtime/{call_id} 200 application/json schema=CodexVoiceStatus
+response GET /api/conversations/{conversation_id}/voice/realtime/{call_id} 422 application/json schema=HTTPValidationError
@@ -731,0 +749,2 @@
+response POST /api/conversations/{conversation_id}/voice/realtime 200 application/json schema=RealtimeAnswer
+response POST /api/conversations/{conversation_id}/voice/realtime 422 application/json schema=HTTPValidationError
@@ -1466,0 +1486,7 @@
+schema CodexVoiceErrorCode type="string" enum=["request_not_sent","relay_failed","connection_failed"]
+schema CodexVoiceStatus property error optional schema=anyOf=[type="string",type="null"]
+schema CodexVoiceStatus property error_code optional schema=anyOf=[CodexVoiceErrorCode,type="null"]
+schema CodexVoiceStatus property provider optional schema=type="string" const="codex" default="codex"
+schema CodexVoiceStatus property status required schema=type="string" enum=["listening","thinking","speaking","closed","error"]
+schema CodexVoiceStatus property transcripts required schema=type="array" items=VoiceTranscript
+schema CodexVoiceStatus type="object"
@@ -2640,0 +2667,8 @@
+schema RealtimeAnswer property call_id optional schema=anyOf=[type="string",type="null"]
+schema RealtimeAnswer property delegation optional schema=type="string" enum=["client","server"] default="client"
+schema RealtimeAnswer property model optional schema=type="string" default="gpt-realtime-2.1"
+schema RealtimeAnswer property provider optional schema=type="string" enum=["openai","codex"] default="openai"
+schema RealtimeAnswer property sdp required schema=type="string"
+schema RealtimeAnswer type="object"
+schema RealtimeOffer property sdp required schema=type="string" minLength=1 maxLength=200000
+schema RealtimeOffer type="object" additionalProperties=false
@@ -3237,0 +3272,12 @@
+schema VoiceAvailability property available required schema=type="boolean"
+schema VoiceAvailability property delegation optional schema=type="string" enum=["client","server"] default="client"
+schema VoiceAvailability property execution_status required schema=ConversationExecutionStatus
+schema VoiceAvailability property model optional schema=type="string" default="gpt-realtime-2.1"
+schema VoiceAvailability property provider optional schema=type="string" enum=["openai","codex"] default="openai"
+schema VoiceAvailability property reason optional schema=anyOf=[type="string" enum=["missing_openai_api_key","codex_not_installed","codex_not_signed_in","codex_unavailable"],type="null"]
+schema VoiceAvailability property run_active required schema=type="boolean"
+schema VoiceAvailability type="object"
+schema VoiceTranscript property id required schema=type="string"
+schema VoiceTranscript property role required schema=type="string" enum=["user","assistant"]
+schema VoiceTranscript property text required schema=type="string"
+schema VoiceTranscript type="object"

Issue Number

Refs #5515, which contains the shared acceptance criteria and validation status.

Related PRs

Related implementation: Canvas #17558 and Insider #2. Canvas issue #17557 tracks host behavior; notebook #24 records the current architecture and maintenance evidence.

How to Test

Integrated verification — 26 October 2026

The final integrated ledger has 42 passing and four unrun latest entry checks, retaining earlier failures and revision provenance; these are entry checks, not full feature-family coverage. See the dated verification record. verify-agent-server now documents F35 Voice and strengthens F10's no-extra-turn compaction assertions. The full map validates 35 families / 951 IDs; nine credential-free F35 API/client recipes passed on a fresh server. Browser/protocol requirements were explicitly blocked in that API-only replay rather than counted as passes.

The paired UI run used a normal OpenHands Cat with actual tools and skills through the authorized Luna profile. A spoken backend-count request reached that Cat, performed the query and returned its saved result through real Codex Voice. A new call recalled saved context after /condense; independent API checks found only the condensation request, condensation result and state update, with all 44 earlier event hashes unchanged. Generated microphone input and media statistics establish the tested provider/transport path, not physical-device audio. A final uninterrupted call after both fixes saved the spoken request, calculated 2+3 in the terminal, saved 5 and returned 5 through Voice; real response audio and clean End were measured.

Pending-answer fencing in 29a83a65 / 27080d11, paired with App PR #4, still failed the first live spoken-correction retest. The additional open-transcript fence in 2795340a passed 63 focused tests, a live-HTTP smoke check and a bounded real-provider replay: the old result remained saved but was not spoken after the correction, including more than 60 seconds after completion. The replay exposed a misleading acknowledgment of an unsent correction. SDK b09624dd passed four protocol/busy-handoff tests and a subsequent live replay: Voice truthfully said the new request had not been sent and should be repeated after the saved task finished; the old saved result was not spoken. No complete raw-audio barge-in or pending-request steering claim is made.

Main refresh — 23 October 2026

Head c99daf37f8221f741b3364817e8678378931166e merges main at 60192ed54db73ac658ac1a07462916126c32d3a0 (release 1.58.1). The single conflict was the condensation error response: the merged branch uses upstream's typed CondensationUnavailable handling for HTTP 409 and retains the Insider change that invokes only the condenser, without executing pending tools. The four Voice endpoints and the App's profile-launch, appended context, confirmation, and active-profile contracts remain compatible.

  • 380 focused Python tests passed, covering Voice routing, Codex relay/protocol, API/init lifecycle, explicit condensation, profile launch additions, conversation routes/config, and the real-HTTP missing-key/condensation regression.
  • 397 TypeScript tests passed with coverage; lint (zero errors, nine existing warnings), build, formatting, and public-type-budget checks passed.
  • Required pre-commit checks passed across the retained Insider Python changes, including Pyright, import boundaries, dynamic-attribute and tool-registration gates.
  • The generated client contract matches the pinned Agent Server 1.58.1 release artifact. The merged server's OpenAPI quality and endpoint audits pass, with no client-only routes.

This is source and regression-test evidence. Existing services were not restarted; no fresh real-provider speech or physical iPad microphone/speaker check was performed. This PR remains draft, and current-head CI is running after the push.

Previously recorded verification

Run the focused Python suite from the repository root:

uv run pytest tests/agent_server/test_voice_router.py tests/agent_server/test_codex_voice.py tests/agent_server/test_codex_voice_protocol.py tests/agent_server/test_api.py tests/agent_server/test_init_router.py tests/sdk/conversation/test_condense.py tests/cross/test_remote_conversation_live_server.py::test_insider_voice_missing_key_and_condense_are_safe_over_http

From clients/typescript, run the client tests, build, lint, type-budget and API checks described in its README. The Voice tests cover authenticated requests, encoded identifiers, SDP payloads, fixed failures without retries, and cancellable polling.

Merge update — 19 October 2026

Head 8a95ad604619465855cc608ed61cbb560778d8c4 merges main at 6ae64079cfd30cfe2f01a060c59168a4d650d883. The single conflict was adjacent imports in the real-HTTP regression file; the resolution keeps Voice coverage and upstream configuration-isolation checks. Voice endpoints, profile launch additions, confirmation settings, and active-profile response fields remain compatible with the Insider App. Upstream lifecycle, session-key export, and run-admission fixes are retained.

  • 170 focused Python tests passed: Voice router, Codex relay and protocol, API/init lifecycle, explicit condensation, and the real HTTP regression.
  • 397 TypeScript unit tests passed with coverage; lint (zero errors, nine existing warnings), build, formatting, and public-type-budget checks passed.
  • Required pre-commit hooks passed on all PR Python changes, including Pyright, import boundaries, and tool registration.
  • Generated client schema matches the pinned Agent Server 1.55.1 release. Merged-server OpenAPI quality and endpoint audits passed, with no client-only routes.

Current-head CI will run after this push. No live provider call, physical iPad audio check, or existing service restart was performed in this maintenance pass; historical speech results below remain historical. The PR stays draft.

Merge update — 9 October 2026

The 9 October head b30376433d9c8a7b6bc9bd59ea062308fddf7d15 merges main at 1cfba2117e92730b0fe1c32520a39aad8611bb8c. The sole text conflict was a lifecycle test; its resolution keeps the Voice manager teardown checks and the newer short tmux path fixture. All 139 focused Python cases passed across local runs, along with the TypeScript build, lint and format checks, and 122 selected client tests. The first CI attempt for this head hit Docker Hub HTTP 429 while fetching python:3.13-bookworm in Agent Server image builds and integration tests, so a complete green CI run is still pending. The provider and device limits described below still apply.

Validation — 5 October 2026 (earlier head)

Earlier source tested at 2b602507af962badfe82027f2a14911dfc9e90e5, including a clean merge of then-current main (54daf056bd863bb46f922a2fe9324dd736b37ff6).

  • 135 Python tests passed, covering Voice, protocol handling, API lifecycle, safe condensation, and a real HTTP regression.
  • 358 TypeScript tests passed; client lint, build, formatting, and public-type budget passed.
  • Server OpenAPI validation, the schema check against the canonical Agent Server 1.53.0 release artifact, and the endpoint audit passed. The four prerelease Voice endpoints are documented explicitly in the audit configuration because the generated client schema stays pinned to the released server. Against the current server OpenAPI, all four methods matched real routes without that allowance.
  • The built TypeScript client also reached the actual isolated Agent Server over HTTP: authenticated availability returned the missing-key reason, call creation returned 409, unknown status/teardown returned 404, and missing authentication returned 401. All four routes were present in that server's OpenAPI.
  • A separate subagent reviewed paths, request bodies, authentication, cancellation, exports, response types, and ownership without finding a defect.
  • CI for that earlier head: Agent Server builds, TypeScript, Python, and integration tests. All executed checks passed at this revision, with no failures or pending jobs; conditional skips are recorded in the workflows. The full repository description validator also passed locally, including the linked issue requirements. This includes the Linux, macOS, and Windows binaries, Python and TypeScript suites, integration checks, and Docker builds.

The original macOS CI failure occurred during a PyPI download of nodejs-wheel-binaries==22.19.0, before the build or tests started. The failed job passed on rerun; no production workaround was added for that network timeout.

A later schema check ran while the Agent Server 1.53.0 release artifact was still being published. Once the canonical artifact was public, the targeted rerun passed without changing the schema or weakening the check.

Main refresh — 15 October 2026 (earlier head)

The original description also recorded integration head 339d850809c06bfe781c362c01375c8e081eeaaf, merging main f5f849f5a: 165 focused Python cases and 386 TypeScript tests passed, alongside lint/build/formatting/type-budget/OpenAPI/endpoint checks and commit hooks. On macOS, TMPDIR=/tmp avoided tmux Unix-socket path-length failures. The separately installed Insider App had 129 passing tests and a reproducible committed browser bundle. No fresh provider-audio or physical-device check was performed.

Video/Screenshots

The paired Canvas/App browser check used real Agent Server persistence and terminal execution with scripted model responses. The evidence notes identify the exact source revisions and verify saved Cat identity, New Cat/resume, compaction, and missing-key guidance; this does not claim live-provider speech.

The saved Cat in regular Canvas chat

Design Doc

Before/after design and validation page, with source links and API/lifecycle diagrams. This is a temporary .pr/ review artifact; the pinned link survives cleanup.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

This remains an experimental draft. OpenAI is the default provider with client-side delegation; Codex requires explicit opt-in and a dedicated sign-in, with no automatic fallback. Status polling is for Codex calls; OpenAI state comes from WebRTC. Ending a call does not cancel accepted work. September 19–20 speech trials remain historical. The October 26 generated-input run supplies fresh real-provider and browser-media evidence, including a bounded stale-answer replay that passed after a further transcript fence. The subsequent acknowledgment-honesty repair also passed a live retest. Physical iPad microphone/speaker behavior remains unverified. The generated TypeScript schema stays pinned to Agent Server 1.58.1; the four Voice endpoints require this unreleased server change. The linked design page supplies the larger lifecycle diagrams, so the PR can keep the ownership sketch inline.

Visual explanation by Codex using HumanLayer show-me.

REST API contract changes

Compared with base OpenAPI 6ae64079cfd3 for public /api/** paths.

--- base public OpenAPI
+++ head public OpenAPI
@@ -6,0 +7 @@
+operation DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} operationId=end_realtime_call_api_conversations__conversation_id__voice_realtime__call_id__delete
@@ -37,0 +39,2 @@
+operation GET /api/conversations/{conversation_id}/voice operationId=voice_availability_api_conversations__conversation_id__voice_get
+operation GET /api/conversations/{conversation_id}/voice/realtime/{call_id} operationId=realtime_call_status_api_conversations__conversation_id__voice_realtime__call_id__get
@@ -131,0 +135 @@
+operation POST /api/conversations/{conversation_id}/voice/realtime operationId=create_realtime_call_api_conversations__conversation_id__voice_realtime_post
@@ -170,0 +175,2 @@
+parameter DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} path:call_id required=true schema=type="string" pattern="^[A-Za-z0-9_-]{1,128}$"
+parameter DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} path:conversation_id required=true schema=type="string" format="uuid"
@@ -226,0 +233,3 @@
+parameter GET /api/conversations/{conversation_id}/voice path:conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{conversation_id}/voice/realtime/{call_id} path:call_id required=true schema=type="string" pattern="^[A-Za-z0-9_-]{1,128}$"
+parameter GET /api/conversations/{conversation_id}/voice/realtime/{call_id} path:conversation_id required=true schema=type="string" format="uuid"
@@ -345,0 +355 @@
+parameter POST /api/conversations/{conversation_id}/voice/realtime path:conversation_id required=true schema=type="string" format="uuid"
@@ -399,0 +410 @@
+requestBody POST /api/conversations/{conversation_id}/voice/realtime application/json required=true schema=RealtimeOffer
@@ -437,0 +449,2 @@
+response DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} 200 application/json schema=Success
+response DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} 422 application/json schema=HTTPValidationError
@@ -508,0 +522,4 @@
+response GET /api/conversations/{conversation_id}/voice 200 application/json schema=VoiceAvailability
+response GET /api/conversations/{conversation_id}/voice 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{conversation_id}/voice/realtime/{call_id} 200 application/json schema=CodexVoiceStatus
+response GET /api/conversations/{conversation_id}/voice/realtime/{call_id} 422 application/json schema=HTTPValidationError
@@ -662,0 +680 @@
+response POST /api/conversations/{conversation_id}/condense 409 no-content
@@ -728,0 +747,2 @@
+response POST /api/conversations/{conversation_id}/voice/realtime 200 application/json schema=RealtimeAnswer
+response POST /api/conversations/{conversation_id}/voice/realtime 422 application/json schema=HTTPValidationError
@@ -1461,0 +1482,7 @@
+schema CodexVoiceErrorCode type="string" enum=["request_not_sent","relay_failed","connection_failed"]
+schema CodexVoiceStatus property error optional schema=anyOf=[type="string",type="null"]
+schema CodexVoiceStatus property error_code optional schema=anyOf=[CodexVoiceErrorCode,type="null"]
+schema CodexVoiceStatus property provider optional schema=type="string" const="codex" default="codex"
+schema CodexVoiceStatus property status required schema=type="string" enum=["listening","thinking","speaking","closed","error"]
+schema CodexVoiceStatus property transcripts required schema=type="array" items=VoiceTranscript
+schema CodexVoiceStatus type="object"
@@ -2626,0 +2654,8 @@
+schema RealtimeAnswer property call_id optional schema=anyOf=[type="string",type="null"]
+schema RealtimeAnswer property delegation optional schema=type="string" enum=["client","server"] default="client"
+schema RealtimeAnswer property model optional schema=type="string" default="gpt-realtime-2.1"
+schema RealtimeAnswer property provider optional schema=type="string" enum=["openai","codex"] default="openai"
+schema RealtimeAnswer property sdp required schema=type="string"
+schema RealtimeAnswer type="object"
+schema RealtimeOffer property sdp required schema=type="string" minLength=1 maxLength=200000
+schema RealtimeOffer type="object" additionalProperties=false
@@ -3207,0 +3243,12 @@
+schema VoiceAvailability property available required schema=type="boolean"
+schema VoiceAvailability property delegation optional schema=type="string" enum=["client","server"] default="client"
+schema VoiceAvailability property execution_status required schema=ConversationExecutionStatus
+schema VoiceAvailability property model optional schema=type="string" default="gpt-realtime-2.1"
+schema VoiceAvailability property provider optional schema=type="string" enum=["openai","codex"] default="openai"
+schema VoiceAvailability property reason optional schema=anyOf=[type="string" enum=["missing_openai_api_key","codex_not_installed","codex_not_signed_in","codex_unavailable"],type="null"]
+schema VoiceAvailability property run_active required schema=type="boolean"
+schema VoiceAvailability type="object"
+schema VoiceTranscript property id required schema=type="string"
+schema VoiceTranscript property role required schema=type="string" enum=["user","assistant"]
+schema VoiceTranscript property text required schema=type="string"
+schema VoiceTranscript type="object"

REST API contract changes

Compared with base OpenAPI 54daf056bd86 for public /api/** paths.

--- base public OpenAPI
+++ head public OpenAPI
@@ -6,0 +7 @@
+operation DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} operationId=end_realtime_call_api_conversations__conversation_id__voice_realtime__call_id__delete
@@ -37,0 +39,2 @@
+operation GET /api/conversations/{conversation_id}/voice operationId=voice_availability_api_conversations__conversation_id__voice_get
+operation GET /api/conversations/{conversation_id}/voice/realtime/{call_id} operationId=realtime_call_status_api_conversations__conversation_id__voice_realtime__call_id__get
@@ -129,0 +133 @@
+operation POST /api/conversations/{conversation_id}/voice/realtime operationId=create_realtime_call_api_conversations__conversation_id__voice_realtime_post
@@ -167,0 +172,2 @@
+parameter DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} path:call_id required=true schema=type="string" pattern="^[A-Za-z0-9_-]{1,128}$"
+parameter DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} path:conversation_id required=true schema=type="string" format="uuid"
@@ -223,0 +230,3 @@
+parameter GET /api/conversations/{conversation_id}/voice path:conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{conversation_id}/voice/realtime/{call_id} path:call_id required=true schema=type="string" pattern="^[A-Za-z0-9_-]{1,128}$"
+parameter GET /api/conversations/{conversation_id}/voice/realtime/{call_id} path:conversation_id required=true schema=type="string" format="uuid"
@@ -341,0 +351 @@
+parameter POST /api/conversations/{conversation_id}/voice/realtime path:conversation_id required=true schema=type="string" format="uuid"
@@ -393,0 +404 @@
+requestBody POST /api/conversations/{conversation_id}/voice/realtime application/json required=true schema=RealtimeOffer
@@ -430,0 +442,2 @@
+response DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} 200 application/json schema=Success
+response DELETE /api/conversations/{conversation_id}/voice/realtime/{call_id} 422 application/json schema=HTTPValidationError
@@ -501,0 +515,4 @@
+response GET /api/conversations/{conversation_id}/voice 200 application/json schema=VoiceAvailability
+response GET /api/conversations/{conversation_id}/voice 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{conversation_id}/voice/realtime/{call_id} 200 application/json schema=CodexVoiceStatus
+response GET /api/conversations/{conversation_id}/voice/realtime/{call_id} 422 application/json schema=HTTPValidationError
@@ -649,0 +667 @@
+response POST /api/conversations/{conversation_id}/condense 409 no-content
@@ -715,0 +734,2 @@
+response POST /api/conversations/{conversation_id}/voice/realtime 200 application/json schema=RealtimeAnswer
+response POST /api/conversations/{conversation_id}/voice/realtime 422 application/json schema=HTTPValidationError
@@ -1410,0 +1431,7 @@
+schema CodexVoiceErrorCode type="string" enum=["request_not_sent","relay_failed","connection_failed"]
+schema CodexVoiceStatus property error optional schema=anyOf=[type="string",type="null"]
+schema CodexVoiceStatus property error_code optional schema=anyOf=[CodexVoiceErrorCode,type="null"]
+schema CodexVoiceStatus property provider optional schema=type="string" const="codex" default="codex"
+schema CodexVoiceStatus property status required schema=type="string" enum=["listening","thinking","speaking","closed","error"]
+schema CodexVoiceStatus property transcripts required schema=type="array" items=VoiceTranscript
+schema CodexVoiceStatus type="object"
@@ -2554,0 +2582,8 @@
+schema RealtimeAnswer property call_id optional schema=anyOf=[type="string",type="null"]
+schema RealtimeAnswer property delegation optional schema=type="string" enum=["client","server"] default="client"
+schema RealtimeAnswer property model optional schema=type="string" default="gpt-realtime-2.1"
+schema RealtimeAnswer property provider optional schema=type="string" enum=["openai","codex"] default="openai"
+schema RealtimeAnswer property sdp required schema=type="string"
+schema RealtimeAnswer type="object"
+schema RealtimeOffer property sdp required schema=type="string" minLength=1 maxLength=200000
+schema RealtimeOffer type="object" additionalProperties=false
@@ -3089,0 +3125,12 @@
+schema VoiceAvailability property available required schema=type="boolean"
+schema VoiceAvailability property delegation optional schema=type="string" enum=["client","server"] default="client"
+schema VoiceAvailability property execution_status required schema=ConversationExecutionStatus
+schema VoiceAvailability property model optional schema=type="string" default="gpt-realtime-2.1"
+schema VoiceAvailability property provider optional schema=type="string" enum=["openai","codex"] default="openai"
+schema VoiceAvailability property reason optional schema=anyOf=[type="string" enum=["missing_openai_api_key","codex_not_installed","codex_not_signed_in","codex_unavailable"],type="null"]
+schema VoiceAvailability property run_active required schema=type="boolean"
+schema VoiceAvailability type="object"
+schema VoiceTranscript property id required schema=type="string"
+schema VoiceTranscript property role required schema=type="string" enum=["user","assistant"]
+schema VoiceTranscript property text required schema=type="string"
+schema VoiceTranscript type="object"

🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python-slim amd64, arm64 python-node-runtime Link
python-minimal amd64, arm64 python-node-runtime Link
python amd64, arm64 python-node-runtime Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:e6b52e9-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-e6b52e9-python \
  ghcr.io/openhands/agent-server:e6b52e9-python

All tags pushed for this build

ghcr.io/openhands/agent-server:e6b52e9-golang-amd64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-golang-amd64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-golang-amd64
ghcr.io/openhands/agent-server:e6b52e9-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:e6b52e9-golang-arm64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-golang-arm64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-golang-arm64
ghcr.io/openhands/agent-server:e6b52e9-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:e6b52e9-java-amd64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-java-amd64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-java-amd64
ghcr.io/openhands/agent-server:e6b52e9-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:e6b52e9-java-arm64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-java-arm64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-java-arm64
ghcr.io/openhands/agent-server:e6b52e9-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:e6b52e9-python-amd64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python-amd64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python-amd64
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime-amd64
ghcr.io/openhands/agent-server:e6b52e9-python-arm64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python-arm64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python-arm64
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime-arm64
ghcr.io/openhands/agent-server:e6b52e9-python-minimal-amd64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python-minimal-amd64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python-minimal-amd64
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime-minimal-amd64
ghcr.io/openhands/agent-server:e6b52e9-python-minimal-arm64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python-minimal-arm64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python-minimal-arm64
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime-minimal-arm64
ghcr.io/openhands/agent-server:e6b52e9-python-slim-amd64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python-slim-amd64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python-slim-amd64
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime-slim-amd64
ghcr.io/openhands/agent-server:e6b52e9-python-slim-arm64
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python-slim-arm64
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python-slim-arm64
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime-slim-arm64
ghcr.io/openhands/agent-server:e6b52e9-golang
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-golang
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-golang
ghcr.io/openhands/agent-server:e6b52e9-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:e6b52e9-java
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-java
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-java
ghcr.io/openhands/agent-server:e6b52e9-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:e6b52e9-python-minimal
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python-minimal
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python-minimal
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime-minimal
ghcr.io/openhands/agent-server:e6b52e9-python-slim
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python-slim
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python-slim
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime-slim
ghcr.io/openhands/agent-server:e6b52e9-python
ghcr.io/openhands/agent-server:e6b52e905a9c19e2fedc1ec3409e5f871007f903-python
ghcr.io/openhands/agent-server:feat-insider-voice-realtime-python
ghcr.io/openhands/agent-server:e6b52e9-python-node-runtime

About Multi-Architecture Support

  • Each variant tag (e.g., e6b52e9-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., e6b52e9-python-amd64) are also available if needed

enyst and others added 2 commits September 19, 2026 00:32
LocalConversation.condense() used to process the condensation request by
calling agent.step(), which also executes pending actions, including tool
calls still waiting for user confirmation. Invoke the condenser directly
instead so an explicit compaction only compacts history.

The condense endpoint now maps the ValueError raised when no compatible
condenser exists to HTTP 409 instead of a 500.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ersations

Add a conversation-scoped voice API under /api/conversations/{id}/voice,
restricted to conversations tagged as Insider controllers:

- GET reports availability, provider, model, and whether a run is active.
- POST /realtime brokers a WebRTC offer. On the default OpenAI provider the
  server builds the Realtime session (instructions, bounded saved context,
  send_to_insider and end_voice_call tools) and exchanges the SDP through
  OpenAI's calls endpoint with the server-held API key.
- GET and DELETE /realtime/{call_id} poll and end a call, with call IDs
  bound to their conversation.

Add an opt-in Codex Voice relay (voice_provider = "codex") that delegates
the call to a Codex app-server sign-in kept in a dedicated home directory,
configured through codex_voice_home.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

Build lifecycle test state with the Codex Voice manager created by create_app and verify it is awaited during shutdown.

Co-authored-by: openhands <openhands@all-hands.dev>
@smolpaws

Copy link
Copy Markdown
Contributor

Traceability and validation update (2026-09-19): the original tested SDK worktree is now preserved at enyst/agent-sdk@7c806a919, on gpt/insider-ui-voice. Its 11 feature-file changes match this draft's original head 134f57d06: all newly added files are byte-identical, and the existing files have identical feature additions/removals. That snapshot retains its older base; this draft remains the upstream review target.

The six failing lifecycle cases were missing the Voice manager in their fake application state. The test-only follow-up adds that manager and verifies shutdown awaits it. Production behavior is unchanged. The corresponding fix is also preserved on the original tested branch at d5dc75ee6.

Validation:

  • Original tested snapshot: 148 focused tests passed (44 Voice, 13 condensation, 91 conversation-router), plus the real HTTP regression test_insider_voice_missing_key_and_condense_are_safe_over_http.
  • This draft's newer base with the fixture fix: all six previously failing lifecycle cases passed with LITELLM_LOCAL_MODEL_COST_MAP=True OPENHANDS_SUPPRESS_BANNER=1 .venv/bin/python -m pytest tests/agent_server/test_api.py::TestServiceParallelization tests/agent_server/test_init_router.py::test_lifespan_teardown_releases_conversation_service_after_init -q.
  • Required pre-commit checks passed on both checkouts, including Pyright; no model or provider calls were needed for these checks.

The HTTP regression verifies authentication, missing-provider availability, and safe rejection of condensation without a condenser. Pending-approval preservation is covered by the separate condensation unit test. Full CI still needs to complete on the new head; this update does not claim that the earlier sdk-tests failure is resolved.

@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   api.py4163392%143, 197–198, 207–208, 210, 213–214, 221–222, 236–237, 254, 260, 263–264, 338, 344, 407, 412, 425–427, 453, 482, 488, 492, 513–514, 719, 801, 804, 810
   codex_voice.py3985786%120–130, 135–137, 154–157, 217, 248, 275, 283, 298, 319, 332, 367, 376–379, 387, 394–396, 403–405, 407–408, 422–423, 489–490, 539, 569, 583, 600, 605, 641–644, 648, 668, 672–673, 687
   config.py173199%610
   voice_router.py158994%129, 154, 193, 204–205, 321, 330–331, 333
openhands-sdk/openhands/sdk/conversation/impl
   local_conversation.py11397494%169, 373, 439, 524, 711–712, 754, 796, 1156–1157, 1162, 1177, 1179, 1250, 1347, 1373, 1462, 1467–1472, 1525, 1545–1547, 1596, 1931–1932, 2195, 2198–2199, 2224, 2255, 2261, 2400, 2403, 2406, 2410–2411, 2415–2416, 2419, 2426, 2446, 2450, 2453, 2472, 2524, 2527, 2559, 2566–2567, 2575, 2579–2581, 2588, 2701, 2760, 2765, 2876–2877, 2885–2886, 2904–2905, 2938, 3149, 3153, 3223, 3230–3231
TOTAL45411477189% 

Validate and deduplicate realtime handoffs before dispatch, speak only saved results, and expose safe error codes for connection and request failures.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. The directory will be automatically removed after this PR merges to main through a cleanup PR.

enyst and others added 3 commits October 5, 2026 15:03
Expose typed voice setup, status and hangup through ConversationManager and the secondary clients entrypoint. Keep the generated schema pinned to its release and track these prerelease routes in the endpoint audit.

Co-authored-by: openhands <openhands@all-hands.dev>
Document the additive API, saved-controller handoff path, safe compaction, typed client and fresh test evidence with pinned source links.

Co-authored-by: openhands <openhands@all-hands.dev>
Preserve pinned-file links when HTMLPreview rewrites fragments, and contain figures inside the narrow document column.

Co-authored-by: openhands <openhands@all-hands.dev>
enyst and others added 2 commits October 5, 2026 15:32
Keep the generated contract aligned with the canonical 1.53.0 release while preserving the tracked prerelease voice endpoints.

Co-authored-by: openhands <openhands@all-hands.dev>
Keep the Voice manager lifecycle checks alongside main's short tmux
temporary-directory fixture.

Co-authored-by: openhands <openhands@all-hands.dev>
@smolpaws

smolpaws commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Progress update (9 Oct): I merged current main into this branch at b303764 and resolved the test-fixture conflict. The branch now merges cleanly. Locally, 139 focused Python tests, 122 selected TypeScript client tests, the TypeScript build and lint, and Ruff/pycodestyle passed.

The new CI run reports nine failed image/integration jobs. The inspected jobs hit Docker Hub HTTP 429 while pulling python:3.13-bookworm; rerun the failed jobs after the rate limit clears. The older “all checks passed” text in the PR description refers to head 2b60250, before this merge.

This draft still needs real provider speech, device audio, and call cleanup tests with Canvas #17558 and Insider app #2.

enyst and others added 2 commits October 15, 2026 22:14
Keep the current server lifecycle and client release contract. Give the
new tmux lifespan test fixtures the existing Voice manager dependency.

Co-authored-by: openhands <openhands@all-hands.dev>
Preserve both Voice HTTP coverage and upstream configuration-isolation checks. Validated 170 focused Python tests, 397 TypeScript tests, hooks, type checks, and API schema compatibility.

Co-authored-by: openhands <openhands@all-hands.dev>
enyst and others added 6 commits October 23, 2026 07:25
Retain upstream typed condensation-unavailable handling while preserving condenser-only execution and the Insider Voice API.

Co-authored-by: openhands <openhands@all-hands.dev>
Document conversation-bound Voice, delegation, cleanup and API failure recipes. Assert explicit condensation does not advance the agent.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Codex V3 may keep a user transcript open across a delegated request and a
spoken correction. Count each nonempty input delta so the correction
suppresses the obsolete spoken answer while its saved task still finishes.

Cover both a new transcript and a correction within the already open
transcript through the real child-process protocol fixture.
Do not describe a spoken correction as forwarded, queued, or applied before
the host confirms acceptance. Explain that a running saved task is not
steered by later speech and an unsent request must be repeated afterward.
@enyst enyst added the integration-test Runs the integration tests and comments the results label Oct 26, 2026
enyst and others added 2 commits October 26, 2026 08:44
Co-authored-by: openhands <openhands@all-hands.dev>
Preserve bounded provider, user-flow and media evidence, the historical
failed replays, and explicit remaining device and workflow limits.

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

Copy link
Copy Markdown
Contributor

Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Integration Tests Results

Overall Success Rate: 95.8%
Total Cost: $2.12
Models Tested: 5
Timestamp: 2026-10-26 07:54:33 UTC

📁 Detailed Logs & Artifacts

Click the links below to access detailed agent/LLM logs showing the complete reasoning process for each model. On the GitHub Actions page, scroll down to the 'Artifacts' section to download the logs.

📊 Summary

Model Overall Tests Passed Skipped Total Cost Tokens
litellm_proxy_openai_gpt_5.5 100.0% 10/10 0 10 $0.89 303,611
litellm_proxy_deepseek_deepseek_v4.1_flash 88.9% 8/9 1 10 $0.02 382,778
litellm_proxy_gemini_3.1_pro_preview 100.0% 10/10 0 10 $0.63 352,770
litellm_proxy_anthropic_claude_sonnet_4_6 90.0% 9/10 0 10 $0.58 390,065
litellm_proxy_minimax_MiniMax_M2.7 100.0% 9/9 1 10 $0.00 333,609

📋 Detailed Results

litellm_proxy_openai_gpt_5.5

  • Success Rate: 100.0% (10/10)
  • Total Cost: $0.89
  • Token Usage: prompt: 298,708, completion: 4,903, cache_read: 165,888, reasoning: 1,460
  • Run Suffix: litellm_proxy_openai_gpt_5.5_b09624d_gpt_5_5_run_N10_20261026_075255

litellm_proxy_deepseek_deepseek_v4.1_flash

  • Success Rate: 88.9% (8/9)
  • Total Cost: $0.02
  • Token Usage: prompt: 375,993, completion: 6,785, cache_read: 267,676, reasoning: 2,074
  • Run Suffix: litellm_proxy_deepseek_deepseek_v4.1_flash_b09624d_deepseek_v4_1_flash_run_N10_20261026_075258
  • Skipped Tests: 1

Skipped Tests:

  • t08_image_file_viewing: This test requires a vision-capable LLM model. Please use a model that supports image input.

Failed Tests:

  • t02_add_bash_hello: Shell script 'shell/hello.sh' not found (Cost: $8.7e-04)

litellm_proxy_gemini_3.1_pro_preview

  • Success Rate: 100.0% (10/10)
  • Total Cost: $0.63
  • Token Usage: prompt: 348,290, completion: 4,480, cache_read: 68,515, reasoning: 2,547
  • Run Suffix: litellm_proxy_gemini_3.1_pro_preview_b09624d_gemini_3_1_pro_run_N10_20261026_075251

litellm_proxy_anthropic_claude_sonnet_4_6

  • Success Rate: 90.0% (9/10)
  • Total Cost: $0.58
  • Token Usage: prompt: 385,007, completion: 5,058, cache_read: 272,502, cache_write: 112,447, reasoning: 438
  • Run Suffix: litellm_proxy_anthropic_claude_sonnet_4_6_b09624d_claude_sonnet_4_6_run_N10_20261026_075245

Failed Tests:

  • t02_add_bash_hello: Shell script is not executable (Cost: $0.05)

litellm_proxy_minimax_MiniMax_M2.7

  • Success Rate: 100.0% (9/9)
  • Total Cost: $0.00
  • Token Usage: prompt: 329,110, completion: 4,499, cache_read: 169,171
  • Run Suffix: litellm_proxy_minimax_MiniMax_M2.7_b09624d_minimax_m2_7_run_N10_20261026_075235
  • Skipped Tests: 1

Skipped Tests:

  • t08_image_file_viewing: This test requires a vision-capable LLM model. Please use a model that supports image input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-test Runs the integration tests and comments the results

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants