Use framework-owned Android session transport

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-23 13:36:17 -07:00
parent 5953e415f7
commit 2a6b4021f6
7 changed files with 409 additions and 38 deletions

View File

@@ -1,7 +1,9 @@
package com.openai.codex.agent
import android.content.Context
import android.os.Bundle
import android.util.Log
import com.openai.codex.bridge.FrameworkSessionTransportCompat
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
@@ -18,6 +20,17 @@ object AgentResponsesProxy {
private const val DEFAULT_CHATGPT_BASE_URL = "https://chatgpt.com/backend-api/codex"
private const val DEFAULT_ORIGINATOR = "codex_cli_rs"
private const val DEFAULT_USER_AGENT = "codex_cli_rs/android_agent_bridge"
private const val HEADER_AUTHORIZATION = "Authorization"
private const val HEADER_CONTENT_TYPE = "Content-Type"
private const val HEADER_ACCEPT = "Accept"
private const val HEADER_ACCEPT_ENCODING = "Accept-Encoding"
private const val HEADER_CHATGPT_ACCOUNT_ID = "ChatGPT-Account-ID"
private const val HEADER_ORIGINATOR = "originator"
private const val HEADER_USER_AGENT = "User-Agent"
private const val HEADER_VALUE_BEARER_PREFIX = "Bearer "
private const val HEADER_VALUE_APPLICATION_JSON = "application/json"
private const val HEADER_VALUE_TEXT_EVENT_STREAM = "text/event-stream"
private const val HEADER_VALUE_IDENTITY = "identity"
internal data class AuthSnapshot(
val authMode: String,
@@ -35,10 +48,7 @@ object AgentResponsesProxy {
requestBody: String,
): HttpResponse {
val authSnapshot = loadAuthSnapshot(File(context.filesDir, "codex-home/auth.json"))
val upstreamUrl = buildResponsesUrl(
upstreamBaseUrl = "provider-default",
authSnapshot.authMode,
)
val upstreamUrl = buildResponsesUrl(upstreamBaseUrl = "provider-default", authMode = authSnapshot.authMode)
val requestBodyBytes = requestBody.toByteArray(StandardCharsets.UTF_8)
Log.i(
TAG,
@@ -47,11 +57,24 @@ object AgentResponsesProxy {
return executeRequest(upstreamUrl, requestBodyBytes, authSnapshot)
}
internal fun buildResponsesUrl(
internal fun buildFrameworkSessionNetworkConfig(
context: Context,
upstreamBaseUrl: String,
): FrameworkSessionTransportCompat.SessionNetworkConfig {
val authSnapshot = loadAuthSnapshot(File(context.filesDir, "codex-home/auth.json"))
return FrameworkSessionTransportCompat.SessionNetworkConfig(
baseUrl = buildResponsesBaseUrl(upstreamBaseUrl, authSnapshot.authMode),
defaultHeaders = buildDefaultHeaders(authSnapshot),
connectTimeoutMillis = CONNECT_TIMEOUT_MS,
readTimeoutMillis = READ_TIMEOUT_MS,
)
}
internal fun buildResponsesBaseUrl(
upstreamBaseUrl: String,
authMode: String,
): String {
val normalizedBaseUrl = when {
return when {
upstreamBaseUrl.isBlank() || upstreamBaseUrl == "provider-default" -> {
if (authMode == "chatgpt") {
DEFAULT_CHATGPT_BASE_URL
@@ -60,8 +83,14 @@ object AgentResponsesProxy {
}
}
else -> upstreamBaseUrl
}
return "${normalizedBaseUrl.trimEnd('/')}/responses"
}.trimEnd('/')
}
internal fun buildResponsesUrl(
upstreamBaseUrl: String,
authMode: String,
): String {
return "${buildResponsesBaseUrl(upstreamBaseUrl, authMode)}/responses"
}
internal fun loadAuthSnapshot(authFile: File): AuthSnapshot {
@@ -148,14 +177,17 @@ object AgentResponsesProxy {
doInput = true
doOutput = true
instanceFollowRedirects = true
setRequestProperty("Authorization", "Bearer ${authSnapshot.bearerToken}")
setRequestProperty("Content-Type", "application/json")
setRequestProperty("Accept", "text/event-stream")
setRequestProperty("Accept-Encoding", "identity")
setRequestProperty("originator", DEFAULT_ORIGINATOR)
setRequestProperty("User-Agent", DEFAULT_USER_AGENT)
if (authSnapshot.authMode == "chatgpt" && !authSnapshot.accountId.isNullOrBlank()) {
setRequestProperty("ChatGPT-Account-ID", authSnapshot.accountId)
val defaultHeaders = buildDefaultHeaders(authSnapshot)
defaultHeaders.keySet().forEach { key ->
defaultHeaders.getString(key)?.let { value ->
setRequestProperty(key, value)
}
}
val requestHeaders = buildResponsesRequestHeaders()
requestHeaders.keySet().forEach { key ->
requestHeaders.getString(key)?.let { value ->
setRequestProperty(key, value)
}
}
}
} catch (err: IOException) {
@@ -163,6 +195,25 @@ object AgentResponsesProxy {
}
}
internal fun buildDefaultHeaders(authSnapshot: AuthSnapshot): Bundle {
return Bundle().apply {
putString(HEADER_AUTHORIZATION, "$HEADER_VALUE_BEARER_PREFIX${authSnapshot.bearerToken}")
putString(HEADER_ORIGINATOR, DEFAULT_ORIGINATOR)
putString(HEADER_USER_AGENT, DEFAULT_USER_AGENT)
if (authSnapshot.authMode == "chatgpt" && !authSnapshot.accountId.isNullOrBlank()) {
putString(HEADER_CHATGPT_ACCOUNT_ID, authSnapshot.accountId)
}
}
}
internal fun buildResponsesRequestHeaders(): Bundle {
return Bundle().apply {
putString(HEADER_CONTENT_TYPE, HEADER_VALUE_APPLICATION_JSON)
putString(HEADER_ACCEPT, HEADER_VALUE_TEXT_EVENT_STREAM)
putString(HEADER_ACCEPT_ENCODING, HEADER_VALUE_IDENTITY)
}
}
internal fun describeRequestFailure(
phase: String,
upstreamUrl: String,

View File

@@ -7,6 +7,7 @@ import android.content.Context
import android.os.Binder
import android.os.Process
import android.util.Log
import com.openai.codex.bridge.FrameworkSessionTransportCompat
import com.openai.codex.bridge.SessionExecutionSettings
import java.util.concurrent.Executor
@@ -157,6 +158,7 @@ class AgentSessionController(context: Context) {
childSessionIds += childSession.sessionId
presentationPolicyStore.savePolicy(childSession.sessionId, target.finalPresentationPolicy)
executionSettingsStore.saveSettings(childSession.sessionId, executionSettings)
provisionSessionNetworkConfig(childSession.sessionId)
manager.publishTrace(
parentSession.sessionId,
"Created child session ${childSession.sessionId} for ${target.packageName} with required final presentation ${target.finalPresentationPolicy.wireValue}.",
@@ -204,6 +206,7 @@ class AgentSessionController(context: Context) {
presentationPolicyStore.savePolicy(session.sessionId, finalPresentationPolicy)
executionSettingsStore.saveSettings(session.sessionId, executionSettings)
try {
provisionSessionNetworkConfig(session.sessionId)
manager.publishTrace(
session.sessionId,
"Starting Codex app-scoped session for $targetPackage with required final presentation ${finalPresentationPolicy.wireValue}.",
@@ -252,6 +255,7 @@ class AgentSessionController(context: Context) {
presentationPolicyStore.savePolicy(sessionId, finalPresentationPolicy)
executionSettingsStore.saveSettings(sessionId, executionSettings)
try {
provisionSessionNetworkConfig(sessionId)
manager.publishTrace(
sessionId,
"Starting Codex app-scoped session for $targetPackage with required final presentation ${finalPresentationPolicy.wireValue}.",
@@ -303,6 +307,7 @@ class AgentSessionController(context: Context) {
AgentSessionBridgeServer.ensureStarted(appContext, manager, childSession.sessionId)
presentationPolicyStore.savePolicy(childSession.sessionId, target.finalPresentationPolicy)
executionSettingsStore.saveSettings(childSession.sessionId, executionSettings)
provisionSessionNetworkConfig(childSession.sessionId)
manager.startGenieSession(
childSession.sessionId,
geniePackage,
@@ -386,6 +391,38 @@ class AgentSessionController(context: Context) {
return checkNotNull(agentManager) { "AgentManager unavailable" }
}
private fun provisionSessionNetworkConfig(sessionId: String) {
val manager = requireAgentManager()
val configured = FrameworkSessionTransportCompat.setSessionNetworkConfig(
agentManager = manager,
sessionId = sessionId,
config = AgentResponsesProxy.buildFrameworkSessionNetworkConfig(
context = appContext,
upstreamBaseUrl = resolveUpstreamBaseUrl(),
),
)
if (configured) {
Log.i(TAG, "Configured framework-owned /responses transport for $sessionId")
} else {
Log.i(
TAG,
"Framework-owned /responses transport unavailable for $sessionId; keeping Agent-owned fallback",
)
}
}
private fun resolveUpstreamBaseUrl(): String {
val cachedStatus = AgentCodexAppServerClient.currentRuntimeStatus()
if (cachedStatus?.upstreamBaseUrl?.isNotBlank() == true) {
return cachedStatus.upstreamBaseUrl
}
return runCatching {
AgentCodexAppServerClient.readRuntimeStatus(appContext).upstreamBaseUrl
}.getOrElse {
"provider-default"
}
}
private fun shouldRetryAnswerQuestion(
sessionId: String,
err: Throwable,

View File

@@ -29,6 +29,17 @@ class AgentResponsesProxyTest {
)
}
@Test
fun buildResponsesBaseUrlKeepsConfiguredBaseWithoutTrailingSlash() {
assertEquals(
"https://example.invalid/custom",
AgentResponsesProxy.buildResponsesBaseUrl(
upstreamBaseUrl = "https://example.invalid/custom/",
authMode = "chatgpt",
),
)
}
@Test
fun loadAuthSnapshotReadsChatgptTokens() {
val authFile = writeTempAuthJson(

View File

@@ -1,4 +1,5 @@
import org.gradle.api.GradleException
import org.gradle.api.tasks.Sync
plugins {
id("com.android.library")
@@ -15,6 +16,12 @@ if (hostJavaMajorVersion < minAndroidJavaVersion) {
}
val androidJavaTargetVersion = hostJavaMajorVersion.coerceAtMost(maxAndroidJavaVersion)
val androidJavaVersion = JavaVersion.toVersion(androidJavaTargetVersion)
val agentPlatformStubSdkZip = providers
.gradleProperty("agentPlatformStubSdkZip")
.orElse(providers.environmentVariable("ANDROID_AGENT_PLATFORM_STUB_SDK_ZIP"))
val extractedAgentPlatformJar = layout.buildDirectory.file(
"generated/agent-platform/android-agent-platform-stub-sdk.jar"
)
android {
namespace = "com.openai.codex.bridge"
@@ -30,6 +37,25 @@ android {
}
}
val extractAgentPlatformStubSdk = tasks.register<Sync>("extractAgentPlatformStubSdk") {
val sdkZip = agentPlatformStubSdkZip.orNull
?: throw GradleException(
"Set ANDROID_AGENT_PLATFORM_STUB_SDK_ZIP or -PagentPlatformStubSdkZip to the Android Agent Platform stub SDK zip."
)
val outputDir = extractedAgentPlatformJar.get().asFile.parentFile
from(zipTree(sdkZip)) {
include("payloads/compile_only/android-agent-platform-stub-sdk.jar")
eachFile { path = name }
includeEmptyDirs = false
}
into(outputDir)
}
tasks.named("preBuild").configure {
dependsOn(extractAgentPlatformStubSdk)
}
dependencies {
compileOnly(files(extractedAgentPlatformJar))
testImplementation("junit:junit:4.13.2")
}

View File

@@ -0,0 +1,204 @@
package com.openai.codex.bridge
import android.app.agent.AgentManager
import android.app.agent.GenieService
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.util.Log
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.nio.charset.StandardCharsets
object FrameworkSessionTransportCompat {
private const val TAG = "FrameworkSessionCompat"
private const val NETWORK_CONFIG_CLASS_NAME = "android.app.agent.AgentSessionNetworkConfig"
private const val HTTP_BRIDGE_CLASS_NAME = "android.app.agent.FrameworkSessionHttpBridge"
private const val HTTP_REQUEST_CLASS_NAME = "android.app.agent.FrameworkSessionHttpBridge\$HttpRequest"
private const val HTTP_RESPONSE_CLASS_NAME = "android.app.agent.FrameworkSessionHttpBridge\$HttpResponse"
private const val OPEN_FRAMEWORK_SESSION_BRIDGE_METHOD = "openFrameworkSessionBridge"
private const val SET_SESSION_NETWORK_CONFIG_METHOD = "setSessionNetworkConfig"
private const val EXECUTE_REQUEST_AND_READ_FULLY_METHOD = "executeRequestAndReadFully"
data class SessionNetworkConfig(
val baseUrl: String,
val defaultHeaders: Bundle,
val connectTimeoutMillis: Int,
val readTimeoutMillis: Int,
)
data class HttpRequest(
val method: String,
val path: String,
val headers: Bundle,
val body: ByteArray,
)
data class HttpResponse(
val statusCode: Int,
val headers: Bundle,
val body: ByteArray,
val bodyString: String,
)
private data class AvailableRuntimeApi(
val setSessionNetworkConfigMethod: Method,
val networkConfigConstructor: java.lang.reflect.Constructor<*>,
val executeRequestAndReadFullyMethod: Method,
val httpRequestConstructor: java.lang.reflect.Constructor<*>,
val httpResponseGetStatusCodeMethod: Method,
val httpResponseGetHeadersMethod: Method,
val httpResponseGetBodyMethod: Method,
val httpResponseGetBodyAsStringMethod: Method,
)
private sealed interface RuntimeApiAvailability {
data object Missing : RuntimeApiAvailability
data class Available(
val api: AvailableRuntimeApi,
) : RuntimeApiAvailability
}
private val runtimeApiAvailability: RuntimeApiAvailability by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
resolveRuntimeApiAvailability()
}
fun setSessionNetworkConfig(
agentManager: AgentManager,
sessionId: String,
config: SessionNetworkConfig,
): Boolean {
val api = (runtimeApiAvailability as? RuntimeApiAvailability.Available)?.api ?: return false
val platformConfig = invokeChecked {
api.networkConfigConstructor.newInstance(
config.baseUrl,
Bundle(config.defaultHeaders),
config.connectTimeoutMillis,
config.readTimeoutMillis,
)
}
invokeChecked {
api.setSessionNetworkConfigMethod.invoke(agentManager, sessionId, platformConfig)
}
return true
}
fun openFrameworkSessionBridge(
callback: GenieService.Callback,
sessionId: String,
): ParcelFileDescriptor? {
val api = runtimeApiAvailability
if (api !is RuntimeApiAvailability.Available) {
return null
}
val method = runCatching {
callback.javaClass.getMethod(
OPEN_FRAMEWORK_SESSION_BRIDGE_METHOD,
String::class.java,
)
}.getOrElse { err ->
if (err is NoSuchMethodException) {
Log.i(
TAG,
"Framework session HTTP bridge callback is unavailable; falling back to Agent-owned transport",
)
return null
}
throw err
}
return invokeChecked {
method.invoke(callback, sessionId) as ParcelFileDescriptor
}
}
fun executeRequestAndReadFully(
bridge: ParcelFileDescriptor,
request: HttpRequest,
): HttpResponse {
val api = (runtimeApiAvailability as? RuntimeApiAvailability.Available)?.api
?: throw IllegalStateException("Framework session HTTP bridge is unavailable")
val requestObject = invokeChecked {
api.httpRequestConstructor.newInstance(
request.method,
request.path,
Bundle(request.headers),
request.body,
)
}
val responseObject = invokeChecked {
api.executeRequestAndReadFullyMethod.invoke(null, bridge, requestObject)
}
val statusCode = invokeChecked {
api.httpResponseGetStatusCodeMethod.invoke(responseObject) as Int
}
val headers = invokeChecked {
api.httpResponseGetHeadersMethod.invoke(responseObject) as? Bundle
} ?: Bundle.EMPTY
val body = invokeChecked {
api.httpResponseGetBodyMethod.invoke(responseObject) as? ByteArray
} ?: ByteArray(0)
val bodyString = invokeChecked {
api.httpResponseGetBodyAsStringMethod.invoke(responseObject) as? String
} ?: body.toString(StandardCharsets.UTF_8)
return HttpResponse(
statusCode = statusCode,
headers = Bundle(headers),
body = body,
bodyString = bodyString,
)
}
private fun resolveRuntimeApiAvailability(): RuntimeApiAvailability {
return try {
val networkConfigClass = Class.forName(NETWORK_CONFIG_CLASS_NAME)
val httpBridgeClass = Class.forName(HTTP_BRIDGE_CLASS_NAME)
val httpRequestClass = Class.forName(HTTP_REQUEST_CLASS_NAME)
val httpResponseClass = Class.forName(HTTP_RESPONSE_CLASS_NAME)
RuntimeApiAvailability.Available(
AvailableRuntimeApi(
setSessionNetworkConfigMethod = AgentManager::class.java.getMethod(
SET_SESSION_NETWORK_CONFIG_METHOD,
String::class.java,
networkConfigClass,
),
networkConfigConstructor = networkConfigClass.getConstructor(
String::class.java,
Bundle::class.java,
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
),
executeRequestAndReadFullyMethod = httpBridgeClass.getMethod(
EXECUTE_REQUEST_AND_READ_FULLY_METHOD,
ParcelFileDescriptor::class.java,
httpRequestClass,
),
httpRequestConstructor = httpRequestClass.getConstructor(
String::class.java,
String::class.java,
Bundle::class.java,
ByteArray::class.java,
),
httpResponseGetStatusCodeMethod = httpResponseClass.getMethod("getStatusCode"),
httpResponseGetHeadersMethod = httpResponseClass.getMethod("getHeaders"),
httpResponseGetBodyMethod = httpResponseClass.getMethod("getBody"),
httpResponseGetBodyAsStringMethod = httpResponseClass.getMethod("getBodyAsString"),
),
)
} catch (err: ReflectiveOperationException) {
Log.i(
TAG,
"Framework-owned HTTP session transport APIs are unavailable; using Agent-owned fallback",
err,
)
RuntimeApiAvailability.Missing
}
}
private fun <T> invokeChecked(block: () -> T): T {
try {
return block()
} catch (err: InvocationTargetException) {
throw err.targetException ?: err
}
}
}

View File

@@ -1,8 +1,10 @@
package com.openai.codex.genie
import android.app.agent.GenieService
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.util.Log
import com.openai.codex.bridge.FrameworkSessionTransportCompat
import com.openai.codex.bridge.SessionExecutionSettings
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
@@ -27,15 +29,30 @@ class AgentBridgeClient(
private const val OP_READ_INSTALLED_AGENTS_FILE = "readInstalledAgentsFile"
private const val OP_READ_SESSION_EXECUTION_SETTINGS = "readSessionExecutionSettings"
private const val WRITE_CHUNK_BYTES = 4096
private const val RESPONSES_METHOD = "POST"
private const val RESPONSES_PATH = "/responses"
private const val HEADER_CONTENT_TYPE = "Content-Type"
private const val HEADER_ACCEPT = "Accept"
private const val HEADER_ACCEPT_ENCODING = "Accept-Encoding"
private const val HEADER_VALUE_APPLICATION_JSON = "application/json"
private const val HEADER_VALUE_TEXT_EVENT_STREAM = "text/event-stream"
private const val HEADER_VALUE_IDENTITY = "identity"
}
private val bridgeFd: ParcelFileDescriptor = callback.openSessionBridge(sessionId)
private val frameworkHttpBridgeFd: ParcelFileDescriptor? =
FrameworkSessionTransportCompat.openFrameworkSessionBridge(callback, sessionId)
private val input = DataInputStream(BufferedInputStream(FileInputStream(bridgeFd.fileDescriptor)))
private val output = DataOutputStream(BufferedOutputStream(FileOutputStream(bridgeFd.fileDescriptor)))
private val ioLock = Any()
init {
Log.i(TAG, "Using framework session bridge transport for $sessionId")
if (frameworkHttpBridgeFd != null) {
Log.i(TAG, "Using framework-owned HTTP bridge for $sessionId")
} else {
Log.i(TAG, "Framework-owned HTTP bridge unavailable for $sessionId; using Agent-owned fallback")
}
}
fun getRuntimeStatus(): CodexAgentBridge.RuntimeStatus {
@@ -70,6 +87,26 @@ class AgentBridgeClient(
}
fun sendResponsesRequest(body: String): AgentResponsesHttpResponse {
val frameworkBridge = frameworkHttpBridgeFd
if (frameworkBridge != null) {
val response = FrameworkSessionTransportCompat.executeRequestAndReadFully(
bridge = frameworkBridge,
request = FrameworkSessionTransportCompat.HttpRequest(
method = RESPONSES_METHOD,
path = RESPONSES_PATH,
headers = Bundle().apply {
putString(HEADER_CONTENT_TYPE, HEADER_VALUE_APPLICATION_JSON)
putString(HEADER_ACCEPT, HEADER_VALUE_TEXT_EVENT_STREAM)
putString(HEADER_ACCEPT_ENCODING, HEADER_VALUE_IDENTITY)
},
body = body.toByteArray(StandardCharsets.UTF_8),
),
)
return AgentResponsesHttpResponse(
statusCode = response.statusCode,
body = response.bodyString,
)
}
val response = request(
JSONObject()
.put("method", OP_SEND_RESPONSES_REQUEST)
@@ -86,6 +123,7 @@ class AgentBridgeClient(
runCatching { input.close() }
runCatching { output.close() }
runCatching { bridgeFd.close() }
runCatching { frameworkHttpBridgeFd?.close() }
}
}

View File

@@ -43,13 +43,12 @@ The current repo now contains these implementation slices:
package inspection, activity launch, input injection, and UI dumping instead
of host-side Kotlin wrappers for those operations.
- The hosted `codex app-server` process now routes `/v1/responses` traffic over
the existing app-server JSON-RPC channel to the Android host, and the Android
host forwards that traffic to the Agent over the framework session bridge.
This keeps network/auth Agent-owned without depending on target-sandbox local
sockets or direct cross-app IPC.
- The session bridge now exposes a **narrow Responses transport** owned by the
Agent app itself, so Genie model traffic no longer depends on any separate
sidecar socket service.
the existing app-server JSON-RPC channel to the Android host, and the Genie
host now executes those requests through the framework-owned HTTP session
bridge instead of bouncing them through the Agent app UID.
- The Agent now provisions per-session network config with auth/base-url
inputs before each Genie start, while the framework owns active session
`/responses` execution and streaming.
- The Genie runtime now keeps host dynamic tools limited to framework-only
detached-target controls and frame capture, while standard Android shell and
device commands stay in the normal Codex tool path.
@@ -76,8 +75,9 @@ The current repo now contains these implementation slices:
correct, for example `cmd activity start-activity --user 0 ...` or
`am start --user 0 ...`.
The Android app now owns auth, runtime status, and Genie Responses forwarding
directly through the hosted Agent runtime. The older standalone
The Android app now owns auth origination, runtime status, and per-session
transport configuration handoff. Active Genie model traffic is framework-owned.
The older standalone
service/client split has been removed from the repo and is no longer part of
the Android Agent/Genie flow.
@@ -98,17 +98,17 @@ the Android Agent/Genie flow.
- The Agent decides which target package(s) should receive child Genie sessions.
- Each child Genie decides its own local tool usage inside the paired sandbox.
- The Agent is the only runtime that owns:
- auth
- outbound network access
- upstream provider selection
- sign-in UX and auth-material origination
- non-session outbound network access
- upstream provider selection and session-scoped network config handoff
- orchestration of parent + child sessions
- Internal Agent<->Genie coordination now splits into:
- framework per-session bridges for fixed-form control/data RPC
- AgentSDK session events for free-form product dialogue
- hosted `codex app-server` inside Genie for the actual Codex execution loop
- Genie-local transport termination between the hosted `codex` child process
and the framework session bridge
- Agent-owned Responses transport termination between the framework session bridge
and the framework session bridges
- framework-owned Responses transport termination between the live Genie session
and the upstream model backend
## Runtime Model
@@ -127,7 +127,8 @@ the Android Agent/Genie flow.
- starting Genie sessions
- answering Genie questions
- aggregating child progress/results into a parent task
- acting as the eventual network/auth proxy for Genie traffic
- provisioning per-session auth/base-url transport inputs for framework-owned
Genie traffic
### Genie
@@ -143,7 +144,8 @@ the Android Agent/Genie flow.
- Android dynamic tool execution
- Agent escalation via `request_user_input`
- runtime bootstrap from the framework session bridge
- forwarding hosted `codex` `/v1/responses` traffic onto the framework session bridge
- forwarding hosted `codex` `/v1/responses` traffic onto the framework-owned
HTTP bridge
## First Milestone Scope
@@ -167,9 +169,9 @@ the Android Agent/Genie flow.
- Agent-hosted runtime metadata for Genie bootstrap
- Shell-first Genie execution for package inspection, activity launch, input injection, and UI dumping
- Hosted `codex app-server` inside Genie, with model traffic routed through the
app-server request/response channel and then over the Agent framework session bridge
- Agent-owned `/v1/responses` proxying in
`android/app/src/main/java/com/openai/codex/agent/AgentResponsesProxy.kt`
app-server request/response channel and then through the framework-owned HTTP bridge
- Per-session framework transport provisioning in
`android/bridge/src/main/java/com/openai/codex/bridge/FrameworkSessionTransportCompat.kt`
- Framework-only Android dynamic tools registered on the Genie Codex thread with:
- detached target show/hide/attach/close
- detached frame capture
@@ -210,13 +212,15 @@ the Android Agent/Genie flow.
- Genie lifecycle host for the embedded `codex app-server`
- `android/genie/src/main/java/com/openai/codex/genie/CodexAppServerHost.kt`
- stdio JSON-RPC host for `codex app-server`, framework-only dynamic tools,
`request_user_input` bridging, and `/v1/responses` forwarding
`request_user_input` bridging, and `/v1/responses` forwarding onto the
framework-owned HTTP bridge
- `android/app/src/main/java/com/openai/codex/agent/AgentSessionBridgeServer.kt`
- Agent-side server for the framework-managed per-session bridge
- `android/app/src/main/java/com/openai/codex/agent/AgentResponsesProxy.kt`
- Agent-owned Responses transport used by Genie model traffic
- Agent-owned Responses transport used by the hosted Agent runtime itself
- `android/genie/src/main/java/com/openai/codex/genie/AgentBridgeClient.kt`
- Genie-side client for the framework-managed session bridge
- Genie-side client for the framework-managed control bridge plus the
framework-owned HTTP bridge
- `android/app/src/main/java/com/openai/codex/agent/AgentCodexAppServerClient.kt`
- hosted Agent `codex app-server` client for planning, orchestration, auto-answering, runtime metadata, and narrow Agent tool calls