mirror of
https://github.com/openai/codex.git
synced 2026-04-29 17:06:51 +00:00
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:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user