mirror of
https://github.com/openai/codex.git
synced 2026-04-29 17:06:51 +00:00
Keep Genie transport local-only
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package com.openai.codexd
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.openai.codex.bridge.HostedCodexConfig
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -36,6 +37,7 @@ object AgentCodexAppServerClient {
|
||||
private var writer: BufferedWriter? = null
|
||||
private var stdoutThread: Thread? = null
|
||||
private var stderrThread: Thread? = null
|
||||
private var localProxy: AgentLocalCodexProxy? = null
|
||||
private var initialized = false
|
||||
|
||||
fun requestText(
|
||||
@@ -49,6 +51,10 @@ object AgentCodexAppServerClient {
|
||||
ensureStarted(context.applicationContext)
|
||||
activeRequests.incrementAndGet()
|
||||
try {
|
||||
Log.i(
|
||||
TAG,
|
||||
"requestText start tools=${dynamicTools?.length() ?: 0} prompt=${prompt.take(160)}",
|
||||
)
|
||||
notifications.clear()
|
||||
val threadId = startThread(
|
||||
context = context.applicationContext,
|
||||
@@ -56,7 +62,9 @@ object AgentCodexAppServerClient {
|
||||
dynamicTools = dynamicTools,
|
||||
)
|
||||
startTurn(threadId, prompt)
|
||||
waitForTurnCompletion(toolCallHandler, requestUserInputHandler)
|
||||
waitForTurnCompletion(toolCallHandler, requestUserInputHandler).also { response ->
|
||||
Log.i(TAG, "requestText completed response=${response.take(160)}")
|
||||
}
|
||||
} finally {
|
||||
activeRequests.decrementAndGet()
|
||||
}
|
||||
@@ -91,6 +99,12 @@ object AgentCodexAppServerClient {
|
||||
notifications.clear()
|
||||
pendingResponses.clear()
|
||||
val codexHome = File(context.filesDir, "codex-home").apply(File::mkdirs)
|
||||
localProxy = AgentLocalCodexProxy { requestBody ->
|
||||
AgentResponsesProxy.sendResponsesRequest(context, requestBody)
|
||||
}.also(AgentLocalCodexProxy::start)
|
||||
val proxyBaseUrl = localProxy?.baseUrl
|
||||
?: throw IOException("local Agent proxy did not start")
|
||||
HostedCodexConfig.write(codexHome, proxyBaseUrl)
|
||||
val startedProcess = ProcessBuilder(
|
||||
listOf(
|
||||
CodexCliBinaryLocator.resolve(context).absolutePath,
|
||||
@@ -117,6 +131,8 @@ object AgentCodexAppServerClient {
|
||||
stderrThread?.interrupt()
|
||||
runCatching { writer?.close() }
|
||||
writer = null
|
||||
localProxy?.close()
|
||||
localProxy = null
|
||||
process?.destroy()
|
||||
process = null
|
||||
initialized = false
|
||||
@@ -209,8 +225,19 @@ object AgentCodexAppServerClient {
|
||||
.append(params.optString("delta"))
|
||||
}
|
||||
}
|
||||
"item/started" -> {
|
||||
val item = params.optJSONObject("item")
|
||||
Log.i(
|
||||
TAG,
|
||||
"item/started type=${item?.optString("type")} tool=${item?.optString("tool")}",
|
||||
)
|
||||
}
|
||||
"item/completed" -> {
|
||||
val item = params.optJSONObject("item") ?: continue
|
||||
Log.i(
|
||||
TAG,
|
||||
"item/completed type=${item.optString("type")} status=${item.optString("status")} tool=${item.optString("tool")}",
|
||||
)
|
||||
if (item.optString("type") == "agentMessage") {
|
||||
val itemId = item.optString("id")
|
||||
val text = item.optString("text").ifBlank {
|
||||
@@ -223,6 +250,10 @@ object AgentCodexAppServerClient {
|
||||
}
|
||||
"turn/completed" -> {
|
||||
val turn = params.optJSONObject("turn") ?: JSONObject()
|
||||
Log.i(
|
||||
TAG,
|
||||
"turn/completed status=${turn.optString("status")} error=${turn.opt("error")} finalMessage=${finalAgentMessage?.take(160)}",
|
||||
)
|
||||
return when (turn.optString("status")) {
|
||||
"completed" -> finalAgentMessage?.takeIf(String::isNotBlank)
|
||||
?: throw IOException("Agent turn completed without an assistant message")
|
||||
@@ -245,6 +276,7 @@ object AgentCodexAppServerClient {
|
||||
val requestId = message.opt("id") ?: return
|
||||
val method = message.optString("method", "unknown")
|
||||
val params = message.optJSONObject("params") ?: JSONObject()
|
||||
Log.i(TAG, "handleServerRequest method=$method")
|
||||
when (method) {
|
||||
"item/tool/call" -> {
|
||||
if (toolCallHandler == null) {
|
||||
@@ -257,6 +289,7 @@ object AgentCodexAppServerClient {
|
||||
}
|
||||
val toolName = params.optString("tool").trim()
|
||||
val arguments = params.optJSONObject("arguments") ?: JSONObject()
|
||||
Log.i(TAG, "tool/call tool=$toolName arguments=$arguments")
|
||||
val result = runCatching { toolCallHandler(toolName, arguments) }
|
||||
.getOrElse { err ->
|
||||
sendError(
|
||||
@@ -266,6 +299,7 @@ object AgentCodexAppServerClient {
|
||||
)
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "tool/call completed tool=$toolName result=$result")
|
||||
sendResult(requestId, result)
|
||||
}
|
||||
"item/tool/requestUserInput" -> {
|
||||
@@ -278,6 +312,7 @@ object AgentCodexAppServerClient {
|
||||
return
|
||||
}
|
||||
val questions = params.optJSONArray("questions") ?: JSONArray()
|
||||
Log.i(TAG, "requestUserInput questions=$questions")
|
||||
val result = runCatching { requestUserInputHandler(questions) }
|
||||
.getOrElse { err ->
|
||||
sendError(
|
||||
@@ -287,6 +322,7 @@ object AgentCodexAppServerClient {
|
||||
)
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "requestUserInput completed result=$result")
|
||||
sendResult(requestId, result)
|
||||
}
|
||||
else -> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.openai.codexd
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.io.IOException
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
@@ -10,11 +11,12 @@ class AgentFrameworkToolBridge(
|
||||
private val sessionController: AgentSessionController,
|
||||
) {
|
||||
companion object {
|
||||
const val START_DIRECT_SESSION_TOOL = "android.framework.sessions.start_direct"
|
||||
const val LIST_SESSIONS_TOOL = "android.framework.sessions.list"
|
||||
const val ANSWER_QUESTION_TOOL = "android.framework.sessions.answer_question"
|
||||
const val ATTACH_TARGET_TOOL = "android.framework.sessions.attach_target"
|
||||
const val CANCEL_SESSION_TOOL = "android.framework.sessions.cancel"
|
||||
private const val TAG = "AgentFrameworkTool"
|
||||
const val START_DIRECT_SESSION_TOOL = "android_framework_sessions_start_direct"
|
||||
const val LIST_SESSIONS_TOOL = "android_framework_sessions_list"
|
||||
const val ANSWER_QUESTION_TOOL = "android_framework_sessions_answer_question"
|
||||
const val ATTACH_TARGET_TOOL = "android_framework_sessions_attach_target"
|
||||
const val CANCEL_SESSION_TOOL = "android_framework_sessions_cancel"
|
||||
|
||||
internal fun parseStartDirectSessionArguments(
|
||||
arguments: JSONObject,
|
||||
@@ -82,6 +84,7 @@ class AgentFrameworkToolBridge(
|
||||
onSessionStarted: ((SessionStartResult) -> Unit)? = null,
|
||||
focusedSessionId: String? = null,
|
||||
): JSONObject {
|
||||
Log.i(TAG, "handleToolCall tool=$toolName arguments=$arguments")
|
||||
return when (toolName) {
|
||||
START_DIRECT_SESSION_TOOL -> {
|
||||
val request = parseStartDirectSessionArguments(
|
||||
@@ -93,6 +96,10 @@ class AgentFrameworkToolBridge(
|
||||
plan = request.plan,
|
||||
allowDetachedMode = request.allowDetachedMode,
|
||||
)
|
||||
Log.i(
|
||||
TAG,
|
||||
"Started framework sessions parent=${startedSession.parentSessionId} children=${startedSession.childSessionIds}",
|
||||
)
|
||||
onSessionStarted?.invoke(startedSession)
|
||||
successText(
|
||||
JSONObject()
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package com.openai.codexd
|
||||
|
||||
import android.util.Log
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class AgentLocalCodexProxy(
|
||||
private val requestForwarder: (String) -> CodexdLocalClient.HttpResponse,
|
||||
) : Closeable {
|
||||
companion object {
|
||||
private const val TAG = "AgentLocalProxy"
|
||||
}
|
||||
|
||||
private val pathSecret = UUID.randomUUID().toString().replace("-", "")
|
||||
private val loopbackAddress = InetAddress.getByName("127.0.0.1")
|
||||
private val serverSocket = ServerSocket(0, 50, loopbackAddress)
|
||||
private val closed = AtomicBoolean(false)
|
||||
private val clientSockets = Collections.synchronizedSet(mutableSetOf<Socket>())
|
||||
private val acceptThread = Thread(::acceptLoop, "AgentLocalProxy")
|
||||
|
||||
val baseUrl: String = "http://${loopbackAddress.hostAddress}:${serverSocket.localPort}/${pathSecret}/v1"
|
||||
|
||||
fun start() {
|
||||
acceptThread.start()
|
||||
logInfo("Listening on $baseUrl")
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
runCatching { serverSocket.close() }
|
||||
synchronized(clientSockets) {
|
||||
clientSockets.forEach { socket -> runCatching { socket.close() } }
|
||||
clientSockets.clear()
|
||||
}
|
||||
acceptThread.interrupt()
|
||||
}
|
||||
|
||||
private fun acceptLoop() {
|
||||
while (!closed.get()) {
|
||||
val socket = try {
|
||||
serverSocket.accept()
|
||||
} catch (err: IOException) {
|
||||
if (!closed.get()) {
|
||||
logWarn("Failed to accept local proxy connection", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
clientSockets += socket
|
||||
Thread(
|
||||
{ handleClient(socket) },
|
||||
"AgentLocalProxyClient",
|
||||
).start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClient(socket: Socket) {
|
||||
socket.use { client ->
|
||||
try {
|
||||
val request = readRequest(client)
|
||||
logInfo("Forwarding ${request.method} ${request.forwardPath}")
|
||||
val response = forwardResponsesRequest(request)
|
||||
writeResponse(
|
||||
socket = client,
|
||||
statusCode = response.statusCode,
|
||||
body = response.body,
|
||||
path = request.forwardPath,
|
||||
)
|
||||
} catch (err: Exception) {
|
||||
if (!closed.get()) {
|
||||
logWarn("Local proxy request failed", err)
|
||||
runCatching {
|
||||
writeResponse(
|
||||
socket = client,
|
||||
statusCode = 502,
|
||||
body = err.message ?: err::class.java.simpleName,
|
||||
path = "/error",
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
clientSockets -= client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forwardResponsesRequest(request: ParsedRequest): CodexdLocalClient.HttpResponse {
|
||||
if (request.method != "POST") {
|
||||
return CodexdLocalClient.HttpResponse(
|
||||
statusCode = 405,
|
||||
body = "Unsupported local proxy method: ${request.method}",
|
||||
)
|
||||
}
|
||||
if (request.forwardPath != "/v1/responses") {
|
||||
return CodexdLocalClient.HttpResponse(
|
||||
statusCode = 404,
|
||||
body = "Unsupported local proxy path: ${request.forwardPath}",
|
||||
)
|
||||
}
|
||||
return requestForwarder(request.body.orEmpty())
|
||||
}
|
||||
|
||||
private fun readRequest(socket: Socket): ParsedRequest {
|
||||
val input = socket.getInputStream()
|
||||
val headerBuffer = ByteArrayOutputStream()
|
||||
var matched = 0
|
||||
while (matched < 4) {
|
||||
val next = input.read()
|
||||
if (next == -1) {
|
||||
throw EOFException("unexpected EOF while reading local proxy request headers")
|
||||
}
|
||||
headerBuffer.write(next)
|
||||
matched = when {
|
||||
matched == 0 && next == '\r'.code -> 1
|
||||
matched == 1 && next == '\n'.code -> 2
|
||||
matched == 2 && next == '\r'.code -> 3
|
||||
matched == 3 && next == '\n'.code -> 4
|
||||
next == '\r'.code -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
val headerBytes = headerBuffer.toByteArray()
|
||||
val headerText = headerBytes
|
||||
.copyOfRange(0, headerBytes.size - 4)
|
||||
.toString(StandardCharsets.US_ASCII)
|
||||
val lines = headerText.split("\r\n")
|
||||
val requestLine = lines.firstOrNull()
|
||||
?: throw IOException("local proxy request line missing")
|
||||
val requestParts = requestLine.split(" ", limit = 3)
|
||||
if (requestParts.size < 2) {
|
||||
throw IOException("invalid local proxy request line: $requestLine")
|
||||
}
|
||||
|
||||
val headers = mutableMapOf<String, String>()
|
||||
lines.drop(1).forEach { line ->
|
||||
val separatorIndex = line.indexOf(':')
|
||||
if (separatorIndex <= 0) {
|
||||
return@forEach
|
||||
}
|
||||
val name = line.substring(0, separatorIndex).trim().lowercase()
|
||||
val value = line.substring(separatorIndex + 1).trim()
|
||||
headers[name] = value
|
||||
}
|
||||
|
||||
if (headers["transfer-encoding"]?.contains("chunked", ignoreCase = true) == true) {
|
||||
throw IOException("chunked local proxy requests are unsupported")
|
||||
}
|
||||
|
||||
val contentLength = headers["content-length"]?.toIntOrNull() ?: 0
|
||||
val bodyBytes = ByteArray(contentLength)
|
||||
var offset = 0
|
||||
while (offset < bodyBytes.size) {
|
||||
val read = input.read(bodyBytes, offset, bodyBytes.size - offset)
|
||||
if (read == -1) {
|
||||
throw EOFException("unexpected EOF while reading local proxy request body")
|
||||
}
|
||||
offset += read
|
||||
}
|
||||
|
||||
val rawPath = requestParts[1]
|
||||
val forwardPath = normalizeForwardPath(rawPath)
|
||||
return ParsedRequest(
|
||||
method = requestParts[0],
|
||||
forwardPath = forwardPath,
|
||||
body = if (bodyBytes.isEmpty()) null else bodyBytes.toString(StandardCharsets.UTF_8),
|
||||
)
|
||||
}
|
||||
|
||||
private fun normalizeForwardPath(rawPath: String): String {
|
||||
val expectedPrefix = "/$pathSecret"
|
||||
if (!rawPath.startsWith(expectedPrefix)) {
|
||||
throw IOException("unexpected local proxy path: $rawPath")
|
||||
}
|
||||
val strippedPath = rawPath.removePrefix(expectedPrefix)
|
||||
return if (strippedPath.isBlank()) "/" else strippedPath
|
||||
}
|
||||
|
||||
private fun writeResponse(
|
||||
socket: Socket,
|
||||
statusCode: Int,
|
||||
body: String,
|
||||
path: String,
|
||||
) {
|
||||
val bodyBytes = body.toByteArray(StandardCharsets.UTF_8)
|
||||
val contentType = when {
|
||||
path.startsWith("/v1/responses") -> "text/event-stream; charset=utf-8"
|
||||
body.trimStart().startsWith("{") || body.trimStart().startsWith("[") -> {
|
||||
"application/json; charset=utf-8"
|
||||
}
|
||||
else -> "text/plain; charset=utf-8"
|
||||
}
|
||||
val responseHeaders = buildString {
|
||||
append("HTTP/1.1 $statusCode ${reasonPhrase(statusCode)}\r\n")
|
||||
append("Content-Type: $contentType\r\n")
|
||||
append("Content-Length: ${bodyBytes.size}\r\n")
|
||||
append("Connection: close\r\n")
|
||||
append("\r\n")
|
||||
}
|
||||
|
||||
val output = socket.getOutputStream()
|
||||
output.write(responseHeaders.toByteArray(StandardCharsets.US_ASCII))
|
||||
output.write(bodyBytes)
|
||||
output.flush()
|
||||
}
|
||||
|
||||
private fun reasonPhrase(statusCode: Int): String {
|
||||
return when (statusCode) {
|
||||
200 -> "OK"
|
||||
400 -> "Bad Request"
|
||||
401 -> "Unauthorized"
|
||||
403 -> "Forbidden"
|
||||
404 -> "Not Found"
|
||||
500 -> "Internal Server Error"
|
||||
502 -> "Bad Gateway"
|
||||
503 -> "Service Unavailable"
|
||||
else -> "Response"
|
||||
}
|
||||
}
|
||||
|
||||
private fun logInfo(message: String) {
|
||||
runCatching { Log.i(TAG, message) }
|
||||
}
|
||||
|
||||
private fun logWarn(
|
||||
message: String,
|
||||
err: Throwable,
|
||||
) {
|
||||
runCatching { Log.w(TAG, message, err) }
|
||||
}
|
||||
|
||||
private data class ParsedRequest(
|
||||
val method: String,
|
||||
val forwardPath: String,
|
||||
val body: String?,
|
||||
)
|
||||
}
|
||||
@@ -28,18 +28,11 @@ object AgentResponsesProxy {
|
||||
context: Context,
|
||||
requestBody: String,
|
||||
): CodexdLocalClient.HttpResponse {
|
||||
val runtimeStatus = AgentCodexAppServerClient.readRuntimeStatus(
|
||||
context = context,
|
||||
refreshToken = true,
|
||||
)
|
||||
if (!runtimeStatus.authenticated) {
|
||||
return CodexdLocalClient.HttpResponse(
|
||||
statusCode = 401,
|
||||
body = "not authenticated",
|
||||
)
|
||||
}
|
||||
val authSnapshot = loadAuthSnapshot(File(context.filesDir, "codex-home/auth.json"))
|
||||
val upstreamUrl = buildResponsesUrl(runtimeStatus.upstreamBaseUrl, authSnapshot.authMode)
|
||||
val upstreamUrl = buildResponsesUrl(
|
||||
upstreamBaseUrl = "provider-default",
|
||||
authSnapshot.authMode,
|
||||
)
|
||||
Log.i(TAG, "Proxying /v1/responses -> $upstreamUrl (auth_mode=${authSnapshot.authMode})")
|
||||
return executeRequest(upstreamUrl, requestBody, authSnapshot)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.openai.codexd
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.io.IOException
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
@@ -21,6 +22,8 @@ data class AgentDelegationPlan(
|
||||
}
|
||||
|
||||
object AgentTaskPlanner {
|
||||
private const val TAG = "AgentTaskPlanner"
|
||||
|
||||
private val PLANNER_INSTRUCTIONS =
|
||||
"""
|
||||
You are Codex acting as the Android Agent orchestrator.
|
||||
@@ -43,6 +46,7 @@ object AgentTaskPlanner {
|
||||
requestUserInputHandler: ((JSONArray) -> JSONObject)? = null,
|
||||
): SessionStartResult {
|
||||
if (!targetPackageOverride.isNullOrBlank()) {
|
||||
Log.i(TAG, "Using explicit target override $targetPackageOverride")
|
||||
return sessionController.startDirectSession(
|
||||
plan = AgentDelegationPlan(
|
||||
originalObjective = userObjective,
|
||||
@@ -60,6 +64,7 @@ object AgentTaskPlanner {
|
||||
}
|
||||
var sessionStartResult: SessionStartResult? = null
|
||||
val frameworkToolBridge = AgentFrameworkToolBridge(context, sessionController)
|
||||
Log.i(TAG, "Planning Agent session for objective=${userObjective.take(160)}")
|
||||
AgentCodexAppServerClient.requestText(
|
||||
context = context,
|
||||
instructions = PLANNER_INSTRUCTIONS,
|
||||
@@ -74,12 +79,17 @@ object AgentTaskPlanner {
|
||||
if (sessionStartResult != null) {
|
||||
throw IOException("Agent runtime attempted to start multiple Genie batches")
|
||||
}
|
||||
Log.i(
|
||||
TAG,
|
||||
"Framework tool started parent=${startedSession.parentSessionId} children=${startedSession.childSessionIds}",
|
||||
)
|
||||
sessionStartResult = startedSession
|
||||
},
|
||||
)
|
||||
},
|
||||
requestUserInputHandler = requestUserInputHandler,
|
||||
)
|
||||
Log.i(TAG, "Planner sessionStartResult=$sessionStartResult")
|
||||
return sessionStartResult
|
||||
?: throw IOException("Agent runtime did not launch any Genie sessions")
|
||||
}
|
||||
|
||||
@@ -7,10 +7,15 @@ import android.app.agent.AgentSessionInfo
|
||||
import android.util.Log
|
||||
import java.io.IOException
|
||||
import kotlin.concurrent.thread
|
||||
import org.json.JSONObject
|
||||
|
||||
class CodexAgentService : AgentService() {
|
||||
companion object {
|
||||
private const val TAG = "CodexAgentService"
|
||||
private const val BRIDGE_REQUEST_PREFIX = "__codex_bridge__ "
|
||||
private const val BRIDGE_RESPONSE_PREFIX = "__codex_bridge_result__ "
|
||||
private const val BRIDGE_METHOD_GET_RUNTIME_STATUS = "getRuntimeStatus"
|
||||
private const val BRIDGE_METHOD_SEND_RESPONSES_REQUEST = "sendResponsesRequest"
|
||||
private const val AUTO_ANSWER_ESCALATE_PREFIX = "ESCALATE:"
|
||||
private const val AUTO_ANSWER_INSTRUCTIONS =
|
||||
"You are Codex acting as the Android Agent supervising a Genie execution. If you can answer the current Genie question from the available session context, call the framework session tool `android.framework.sessions.answer_question` exactly once with a short free-form answer. You may inspect current framework state with `android.framework.sessions.list`. If user input is required, do not call any framework tool. Instead reply with `ESCALATE: ` followed by the exact question the Agent should ask the user."
|
||||
@@ -49,7 +54,7 @@ class CodexAgentService : AgentService() {
|
||||
}
|
||||
val manager = agentManager ?: return
|
||||
val events = manager.getSessionEvents(session.sessionId)
|
||||
val question = findVisibleQuestion(events) ?: return
|
||||
val question = findLatestQuestion(events) ?: return
|
||||
val questionKey = genieQuestionKey(session.sessionId, question)
|
||||
if (handledGenieQuestions.contains(questionKey) || !pendingGenieQuestions.add(questionKey)) {
|
||||
return
|
||||
@@ -57,26 +62,33 @@ class CodexAgentService : AgentService() {
|
||||
thread(name = "CodexAgentAutoAnswer-${session.sessionId}") {
|
||||
Log.i(TAG, "Attempting Agent auto-answer for ${session.sessionId}")
|
||||
runCatching {
|
||||
when (val result = requestGenieAutoAnswer(session, question, events)) {
|
||||
AutoAnswerResult.Answered -> {
|
||||
handledGenieQuestions.add(questionKey)
|
||||
AgentQuestionNotifier.cancel(this, session.sessionId)
|
||||
Log.i(TAG, "Auto-answered Genie question for ${session.sessionId}")
|
||||
}
|
||||
is AutoAnswerResult.Escalate -> {
|
||||
if (sessionController.isSessionWaitingForUser(session.sessionId)) {
|
||||
AgentQuestionNotifier.showQuestion(
|
||||
context = this,
|
||||
sessionId = session.sessionId,
|
||||
targetPackage = session.targetPackage,
|
||||
question = result.question,
|
||||
)
|
||||
if (isBridgeQuestion(question)) {
|
||||
answerBridgeQuestion(session, question)
|
||||
handledGenieQuestions.add(questionKey)
|
||||
AgentQuestionNotifier.cancel(this, session.sessionId)
|
||||
Log.i(TAG, "Answered bridge question for ${session.sessionId}")
|
||||
} else {
|
||||
when (val result = requestGenieAutoAnswer(session, question, events)) {
|
||||
AutoAnswerResult.Answered -> {
|
||||
handledGenieQuestions.add(questionKey)
|
||||
AgentQuestionNotifier.cancel(this, session.sessionId)
|
||||
Log.i(TAG, "Auto-answered Genie question for ${session.sessionId}")
|
||||
}
|
||||
is AutoAnswerResult.Escalate -> {
|
||||
if (sessionController.isSessionWaitingForUser(session.sessionId)) {
|
||||
AgentQuestionNotifier.showQuestion(
|
||||
context = this,
|
||||
sessionId = session.sessionId,
|
||||
targetPackage = session.targetPackage,
|
||||
question = result.question,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure { err ->
|
||||
Log.i(TAG, "Agent auto-answer unavailable for ${session.sessionId}: ${err.message}")
|
||||
if (sessionController.isSessionWaitingForUser(session.sessionId)) {
|
||||
if (!isBridgeQuestion(question) && sessionController.isSessionWaitingForUser(session.sessionId)) {
|
||||
AgentQuestionNotifier.showQuestion(
|
||||
context = this,
|
||||
sessionId = session.sessionId,
|
||||
@@ -95,11 +107,15 @@ class CodexAgentService : AgentService() {
|
||||
return
|
||||
}
|
||||
val manager = agentManager ?: return
|
||||
val question = findVisibleQuestion(manager.getSessionEvents(session.sessionId))
|
||||
val question = findLatestQuestion(manager.getSessionEvents(session.sessionId))
|
||||
if (question.isNullOrBlank()) {
|
||||
AgentQuestionNotifier.cancel(this, session.sessionId)
|
||||
return
|
||||
}
|
||||
if (isBridgeQuestion(question)) {
|
||||
AgentQuestionNotifier.cancel(this, session.sessionId)
|
||||
return
|
||||
}
|
||||
if (pendingGenieQuestions.contains(genieQuestionKey(session.sessionId, question))) {
|
||||
return
|
||||
}
|
||||
@@ -194,13 +210,75 @@ class CodexAgentService : AgentService() {
|
||||
return context.takeLast(MAX_AUTO_ANSWER_CONTEXT_CHARS)
|
||||
}
|
||||
|
||||
private fun findVisibleQuestion(events: List<AgentSessionEvent>): String? {
|
||||
private fun findLatestQuestion(events: List<AgentSessionEvent>): String? {
|
||||
return events.lastOrNull { event ->
|
||||
event.type == AgentSessionEvent.TYPE_QUESTION &&
|
||||
!event.message.isNullOrBlank()
|
||||
}?.message
|
||||
}
|
||||
|
||||
private fun isBridgeQuestion(question: String): Boolean {
|
||||
return question.startsWith(BRIDGE_REQUEST_PREFIX)
|
||||
}
|
||||
|
||||
private fun answerBridgeQuestion(
|
||||
session: AgentSessionInfo,
|
||||
question: String,
|
||||
) {
|
||||
val request = JSONObject(question.removePrefix(BRIDGE_REQUEST_PREFIX))
|
||||
val requestId = request.optString("requestId")
|
||||
val response: JSONObject = runCatching {
|
||||
when (request.optString("method")) {
|
||||
BRIDGE_METHOD_GET_RUNTIME_STATUS -> {
|
||||
val status = AgentCodexAppServerClient.readRuntimeStatus(this)
|
||||
JSONObject()
|
||||
.put("requestId", requestId)
|
||||
.put("ok", true)
|
||||
.put(
|
||||
"runtimeStatus",
|
||||
JSONObject()
|
||||
.put("authenticated", status.authenticated)
|
||||
.put("accountEmail", status.accountEmail)
|
||||
.put("clientCount", status.clientCount)
|
||||
.put("modelProviderId", status.modelProviderId)
|
||||
.put("configuredModel", status.configuredModel)
|
||||
.put("effectiveModel", status.effectiveModel)
|
||||
.put("upstreamBaseUrl", status.upstreamBaseUrl),
|
||||
)
|
||||
}
|
||||
BRIDGE_METHOD_SEND_RESPONSES_REQUEST -> {
|
||||
val httpResponse = AgentResponsesProxy.sendResponsesRequest(
|
||||
this,
|
||||
request.optString("requestBody"),
|
||||
)
|
||||
JSONObject()
|
||||
.put("requestId", requestId)
|
||||
.put("ok", true)
|
||||
.put(
|
||||
"httpResponse",
|
||||
JSONObject()
|
||||
.put("statusCode", httpResponse.statusCode)
|
||||
.put("body", httpResponse.body),
|
||||
)
|
||||
}
|
||||
else -> JSONObject()
|
||||
.put("requestId", requestId)
|
||||
.put("ok", false)
|
||||
.put("error", "Unsupported bridge method: ${request.optString("method")}")
|
||||
}
|
||||
}.getOrElse { err ->
|
||||
JSONObject()
|
||||
.put("requestId", requestId)
|
||||
.put("ok", false)
|
||||
.put("error", err.message ?: err::class.java.simpleName)
|
||||
}
|
||||
sessionController.answerQuestion(
|
||||
session.sessionId,
|
||||
BRIDGE_RESPONSE_PREFIX + response.toString(),
|
||||
session.parentSessionId,
|
||||
)
|
||||
}
|
||||
|
||||
private fun eventTypeToString(type: Int): String {
|
||||
return when (type) {
|
||||
AgentSessionEvent.TYPE_TRACE -> "Trace"
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
@@ -30,7 +31,12 @@ import kotlin.concurrent.thread
|
||||
|
||||
class MainActivity : Activity() {
|
||||
companion object {
|
||||
private const val TAG = "CodexMainActivity"
|
||||
private const val STATUS_REFRESH_INTERVAL_MS = 2000L
|
||||
private const val ACTION_DEBUG_START_AGENT_SESSION =
|
||||
"com.openai.codexd.action.DEBUG_START_AGENT_SESSION"
|
||||
private const val EXTRA_DEBUG_PROMPT = "prompt"
|
||||
private const val EXTRA_DEBUG_TARGET_PACKAGE = "targetPackage"
|
||||
}
|
||||
|
||||
@Volatile
|
||||
@@ -121,6 +127,24 @@ class MainActivity : Activity() {
|
||||
if (!sessionId.isNullOrEmpty()) {
|
||||
focusedFrameworkSessionId = sessionId
|
||||
}
|
||||
maybeStartSessionFromIntent(intent)
|
||||
}
|
||||
|
||||
private fun maybeStartSessionFromIntent(intent: Intent?) {
|
||||
if (intent?.action != ACTION_DEBUG_START_AGENT_SESSION) {
|
||||
return
|
||||
}
|
||||
val prompt = intent.getStringExtra(EXTRA_DEBUG_PROMPT)?.trim().orEmpty()
|
||||
if (prompt.isEmpty()) {
|
||||
Log.w(TAG, "Ignoring debug start intent without prompt")
|
||||
return
|
||||
}
|
||||
val targetPackageOverride = intent.getStringExtra(EXTRA_DEBUG_TARGET_PACKAGE)?.trim()
|
||||
findViewById<EditText>(R.id.agent_prompt).setText(prompt)
|
||||
findViewById<EditText>(R.id.agent_target_package).setText(targetPackageOverride.orEmpty())
|
||||
Log.i(TAG, "Handling debug start intent override=$targetPackageOverride prompt=${prompt.take(160)}")
|
||||
startDirectAgentSession(findViewById(R.id.agent_start_button))
|
||||
intent.action = null
|
||||
}
|
||||
|
||||
private fun registerSessionListenerIfNeeded() {
|
||||
@@ -180,6 +204,7 @@ class MainActivity : Activity() {
|
||||
showToast("Enter a prompt")
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "startDirectAgentSession override=$targetPackageOverride prompt=${prompt.take(160)}")
|
||||
thread {
|
||||
val result = runCatching {
|
||||
AgentTaskPlanner.startSession(
|
||||
@@ -194,10 +219,15 @@ class MainActivity : Activity() {
|
||||
)
|
||||
}
|
||||
result.onFailure { err ->
|
||||
Log.w(TAG, "Failed to start Agent session", err)
|
||||
showToast("Failed to start Agent session: ${err.message}")
|
||||
refreshAgentSessions()
|
||||
}
|
||||
result.onSuccess { sessionStart ->
|
||||
Log.i(
|
||||
TAG,
|
||||
"Started Agent session parent=${sessionStart.parentSessionId} children=${sessionStart.childSessionIds}",
|
||||
)
|
||||
focusedFrameworkSessionId = sessionStart.childSessionIds.firstOrNull()
|
||||
val targetSummary = sessionStart.plannedTargets.joinToString(", ")
|
||||
showToast("Started ${sessionStart.childSessionIds.size} Genie session(s) for $targetSummary via ${sessionStart.geniePackage}")
|
||||
|
||||
Reference in New Issue
Block a user