Keep Genie transport local-only

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-19 15:22:06 -07:00
parent ac85481fee
commit 5206fac8af
16 changed files with 745 additions and 258 deletions

View File

@@ -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 -> {

View File

@@ -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()

View File

@@ -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?,
)
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -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}")