Surface Genie questions through Agent notifications

Keep the Agent/Genie contract free-form and Agent-owned by surfacing non-bridge Genie questions through an Agent notification, while tightening the bridged /v1/responses payload used by the current scaffold.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-19 07:44:35 -07:00
parent 87b9057d63
commit 6acd1f7473
5 changed files with 134 additions and 3 deletions

View File

@@ -0,0 +1,80 @@
package com.openai.codexd
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.agent.AgentManager
import android.content.Context
import android.content.Intent
import android.os.Build
object AgentQuestionNotifier {
private const val CHANNEL_ID = "codex_agent_questions"
private const val CHANNEL_NAME = "Codex Agent Questions"
fun showQuestion(
context: Context,
sessionId: String,
targetPackage: String?,
question: String,
) {
val manager = context.getSystemService(NotificationManager::class.java) ?: return
ensureChannel(manager)
manager.notify(notificationId(sessionId), buildNotification(context, sessionId, targetPackage, question))
}
fun cancel(context: Context, sessionId: String) {
val manager = context.getSystemService(NotificationManager::class.java) ?: return
manager.cancel(notificationId(sessionId))
}
private fun buildNotification(
context: Context,
sessionId: String,
targetPackage: String?,
question: String,
): Notification {
val title = targetPackage?.let { "Question for $it" } ?: "Question for Codex Agent"
val contentIntent = PendingIntent.getActivity(
context,
notificationId(sessionId),
Intent(context, MainActivity::class.java).apply {
putExtra(AgentManager.EXTRA_SESSION_ID, sessionId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText(question)
.setStyle(Notification.BigTextStyle().bigText(question))
.setContentIntent(contentIntent)
.setAutoCancel(false)
.setOngoing(true)
.build()
}
private fun ensureChannel(manager: NotificationManager) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
if (manager.getNotificationChannel(CHANNEL_ID) != null) {
return
}
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = "Questions that need user input for Codex Agent sessions"
setShowBadge(true)
}
manager.createNotificationChannel(channel)
}
private fun notificationId(sessionId: String): Int {
return sessionId.hashCode()
}
}

View File

@@ -27,10 +27,12 @@ class CodexAgentService : AgentService() {
override fun onSessionChanged(session: AgentSessionInfo) {
Log.i(TAG, "onSessionChanged $session")
handleInternalBridgeQuestion(session.sessionId)
updateQuestionNotification(session)
}
override fun onSessionRemoved(sessionId: String) {
Log.i(TAG, "onSessionRemoved sessionId=$sessionId")
AgentQuestionNotifier.cancel(this, sessionId)
}
private fun handleInternalBridgeQuestion(sessionId: String) {
@@ -166,4 +168,28 @@ class CodexAgentService : AgentService() {
return err.message?.contains("not waiting for user input", ignoreCase = true) == true ||
!isSessionWaitingForUser(manager, sessionId)
}
private fun updateQuestionNotification(session: AgentSessionInfo) {
if (session.state != AgentSessionInfo.STATE_WAITING_FOR_USER) {
AgentQuestionNotifier.cancel(this, session.sessionId)
return
}
val manager = agentManager ?: return
val question = manager.getSessionEvents(session.sessionId)
.lastOrNull { event ->
event.type == AgentSessionEvent.TYPE_QUESTION &&
event.message != null &&
!event.message.startsWith(BRIDGE_REQUEST_PREFIX)
}?.message
if (question.isNullOrBlank()) {
AgentQuestionNotifier.cancel(this, session.sessionId)
return
}
AgentQuestionNotifier.showQuestion(
context = this,
sessionId = session.sessionId,
targetPackage = session.targetPackage,
question = question,
)
}
}