mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Use framework-owned Android session transport
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user