mirror of
https://github.com/openai/codex.git
synced 2026-04-27 18:01:04 +03:00
Require framework HTTP for Android Agent runtime
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -32,11 +32,6 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".AgentRuntimeForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -107,11 +107,7 @@ object AgentCodexAppServerClient {
|
||||
executionSettings: SessionExecutionSettings = SessionExecutionSettings.default,
|
||||
requestTimeoutMs: Long = REQUEST_TIMEOUT_MS,
|
||||
frameworkSessionId: String? = null,
|
||||
keepForeground: Boolean = false,
|
||||
): String = synchronized(lifecycleLock) {
|
||||
if (keepForeground) {
|
||||
AgentRuntimeForegroundService.start(context.applicationContext)
|
||||
}
|
||||
ensureStarted(context.applicationContext)
|
||||
val previousFrameworkSessionId = activeFrameworkSessionId
|
||||
activeFrameworkSessionId = frameworkSessionId?.trim()?.ifEmpty { null }
|
||||
@@ -142,9 +138,6 @@ object AgentCodexAppServerClient {
|
||||
activeRequests.decrementAndGet()
|
||||
updateClientCount()
|
||||
activeFrameworkSessionId = previousFrameworkSessionId
|
||||
if (keepForeground) {
|
||||
AgentRuntimeForegroundService.stop(context.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,28 +289,12 @@ object AgentCodexAppServerClient {
|
||||
}
|
||||
val agentManager = context.getSystemService(AgentManager::class.java)
|
||||
?: throw IOException("AgentManager unavailable for framework session transport")
|
||||
return runCatching {
|
||||
AgentResponsesProxy.sendResponsesRequestThroughFramework(
|
||||
agentManager = agentManager,
|
||||
sessionId = frameworkSessionId,
|
||||
context = context,
|
||||
requestBody = requestBody,
|
||||
)
|
||||
}.getOrElse { err ->
|
||||
if (!shouldFallbackToDirectResponsesProxy(err)) {
|
||||
throw err
|
||||
}
|
||||
Log.i(
|
||||
TAG,
|
||||
"Falling back to direct Agent /responses proxy for $frameworkSessionId: ${err.message}",
|
||||
)
|
||||
AgentResponsesProxy.sendResponsesRequest(context, requestBody)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldFallbackToDirectResponsesProxy(err: Throwable): Boolean {
|
||||
return err is SecurityException &&
|
||||
err.message?.contains("Only the active Genie runtime may open framework HTTP exchanges") == true
|
||||
return AgentResponsesProxy.sendResponsesRequestThroughFramework(
|
||||
agentManager = agentManager,
|
||||
sessionId = frameworkSessionId,
|
||||
context = context,
|
||||
requestBody = requestBody,
|
||||
)
|
||||
}
|
||||
|
||||
private fun initialize() {
|
||||
|
||||
@@ -39,7 +39,6 @@ object AgentPlannerRuntimeManager {
|
||||
check(activePlannerSessions.putIfAbsent(plannerSessionId, true) == null) {
|
||||
"Planner runtime already active for parent session $plannerSessionId"
|
||||
}
|
||||
AgentRuntimeForegroundService.acquire(applicationContext)
|
||||
try {
|
||||
AgentPlannerRuntime(
|
||||
context = applicationContext,
|
||||
@@ -56,7 +55,6 @@ object AgentPlannerRuntimeManager {
|
||||
}
|
||||
} finally {
|
||||
activePlannerSessions.remove(plannerSessionId)
|
||||
AgentRuntimeForegroundService.release(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,33 +321,17 @@ object AgentPlannerRuntimeManager {
|
||||
|
||||
private fun forwardResponsesRequest(requestBody: String): AgentResponsesProxy.HttpResponse {
|
||||
val activeFrameworkSessionId = frameworkSessionId
|
||||
if (activeFrameworkSessionId.isNullOrBlank()) {
|
||||
return AgentResponsesProxy.sendResponsesRequest(context, requestBody)
|
||||
check(!activeFrameworkSessionId.isNullOrBlank()) {
|
||||
"Planner runtime requires a framework session id for /responses transport"
|
||||
}
|
||||
val agentManager = context.getSystemService(AgentManager::class.java)
|
||||
?: throw IOException("AgentManager unavailable for framework session transport")
|
||||
return runCatching {
|
||||
AgentResponsesProxy.sendResponsesRequestThroughFramework(
|
||||
agentManager = agentManager,
|
||||
sessionId = activeFrameworkSessionId,
|
||||
context = context,
|
||||
requestBody = requestBody,
|
||||
)
|
||||
}.getOrElse { err ->
|
||||
if (!shouldFallbackToDirectResponsesProxy(err)) {
|
||||
throw err
|
||||
}
|
||||
Log.i(
|
||||
TAG,
|
||||
"Falling back to direct Agent /responses proxy for $activeFrameworkSessionId: ${err.message}",
|
||||
)
|
||||
AgentResponsesProxy.sendResponsesRequest(context, requestBody)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldFallbackToDirectResponsesProxy(err: Throwable): Boolean {
|
||||
return err is SecurityException &&
|
||||
err.message?.contains("Only the active Genie runtime may open framework HTTP exchanges") == true
|
||||
return AgentResponsesProxy.sendResponsesRequestThroughFramework(
|
||||
agentManager = agentManager,
|
||||
sessionId = activeFrameworkSessionId,
|
||||
context = context,
|
||||
requestBody = requestBody,
|
||||
)
|
||||
}
|
||||
|
||||
private fun request(
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package com.openai.codex.agent
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class AgentRuntimeForegroundService : Service() {
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "codex_agent_runtime"
|
||||
private const val CHANNEL_NAME = "Codex Agent Runtime"
|
||||
private const val NOTIFICATION_ID = 0xC0D3002
|
||||
private const val ACTION_START = "com.openai.codex.agent.action.START_RUNTIME_FOREGROUND"
|
||||
private const val ACTION_STOP = "com.openai.codex.agent.action.STOP_RUNTIME_FOREGROUND"
|
||||
private val activeLeases = AtomicInteger(0)
|
||||
|
||||
fun acquire(context: Context) {
|
||||
val previous = activeLeases.getAndIncrement()
|
||||
if (previous > 0) {
|
||||
return
|
||||
}
|
||||
val intent = Intent(context, AgentRuntimeForegroundService::class.java).apply {
|
||||
action = ACTION_START
|
||||
}
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
|
||||
fun release(context: Context) {
|
||||
while (true) {
|
||||
val current = activeLeases.get()
|
||||
if (current <= 0) {
|
||||
return
|
||||
}
|
||||
if (!activeLeases.compareAndSet(current, current - 1)) {
|
||||
continue
|
||||
}
|
||||
if (current > 1) {
|
||||
return
|
||||
}
|
||||
val intent = Intent(context, AgentRuntimeForegroundService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
context.startService(intent)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fun start(context: Context) {
|
||||
acquire(context)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
release(context)
|
||||
}
|
||||
|
||||
fun activeLeaseCount(): Int {
|
||||
return activeLeases.get()
|
||||
}
|
||||
|
||||
fun resetLeases() {
|
||||
activeLeases.set(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
resetLeases()
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelfResult(startId)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
ensureChannel(manager)
|
||||
startForeground(NOTIFICATION_ID, buildNotification())
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?) = null
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
val openAgentIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
return Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_codex)
|
||||
.setContentTitle("Codex Agent is working")
|
||||
.setContentText("Planning or supervising an active Agent session.")
|
||||
.setContentIntent(openAgentIntent)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ensureChannel(manager: NotificationManager) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || manager.getNotificationChannel(CHANNEL_ID) != null) {
|
||||
return
|
||||
}
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
).apply {
|
||||
description = "Shows when Codex Agent is actively planning or supervising a session."
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
@@ -54,13 +54,11 @@ The current repo now contains these implementation slices:
|
||||
Agent sessions can plan concurrently.
|
||||
- Planner runtime ownership is 1:1 with the top-level parent session id. The
|
||||
app does not allow overlapping planner runtimes for the same parent session.
|
||||
- A single narrow foreground service now acts as a shared keepalive lease for
|
||||
the app UID while any Agent-owned planner runtime is active in the
|
||||
background.
|
||||
- The framework still only permits `openFrameworkHttpExchange(...)` for active
|
||||
Genie runtimes. Each direct parent planner runtime therefore attempts
|
||||
framework transport first and falls back to the Agent-owned proxy only for
|
||||
that planner path.
|
||||
- The framework now permits both active Genie runtimes and active top-level
|
||||
Agent runtimes to open framework HTTP exchanges. Direct parent planner
|
||||
runtimes therefore use the same framework-owned `/responses` transport as
|
||||
Genie child runtimes, and the old Agent-owned planner fallback has been
|
||||
removed.
|
||||
- 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.
|
||||
@@ -96,7 +94,8 @@ The current repo now contains these implementation slices:
|
||||
framework-owned recovery instead of guessed ordinary app launch.
|
||||
|
||||
The Android app now owns auth origination, runtime status, and per-session
|
||||
transport configuration handoff. Active Genie model traffic is framework-owned.
|
||||
transport configuration handoff. Active Genie and top-level Agent planner model
|
||||
traffic are 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.
|
||||
@@ -167,8 +166,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-owned
|
||||
HTTP bridge
|
||||
- forwarding hosted `codex` `/v1/responses` traffic from both Agent and Genie
|
||||
runtimes onto the framework-owned HTTP bridge
|
||||
|
||||
## First Milestone Scope
|
||||
|
||||
@@ -241,12 +240,8 @@ the Android Agent/Genie flow.
|
||||
- `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 the hosted Agent runtime itself,
|
||||
including the direct-parent planner fallback when the framework rejects
|
||||
hidden Agent-side HTTP exchange opens outside an active Genie runtime
|
||||
- `android/app/src/main/java/com/openai/codex/agent/AgentRuntimeForegroundService.kt`
|
||||
- shared foreground-service keepalive for Agent-owned planning while the UI
|
||||
is backgrounded
|
||||
- Agent/Genie bridge helper for framework-owned Responses transport setup and
|
||||
execution
|
||||
- `android/genie/src/main/java/com/openai/codex/genie/AgentBridgeClient.kt`
|
||||
- Genie-side client for the framework-managed control bridge plus the
|
||||
framework-owned streaming HTTP exchange bridge
|
||||
|
||||
Reference in New Issue
Block a user