Plan Agent targets before launching Genies

Teach the Agent runtime and UI to resolve target packages from installed apps and start one child Genie session per selected package, with an optional package override for debugging.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Iliyan Malchev
2026-03-19 11:22:33 -07:00
parent fa0c22441c
commit 3a5ab674f0
10 changed files with 415 additions and 137 deletions

View File

@@ -0,0 +1,136 @@
package com.openai.codexd
import java.io.IOException
import org.json.JSONArray
import org.json.JSONObject
object CodexResponsesClient {
fun requestText(
context: android.content.Context,
model: String,
instructions: String,
prompt: String,
): String {
val requestBody = buildRequest(
model = model,
instructions = instructions,
prompt = prompt,
)
val response = CodexdLocalClient.waitForResponse(
context = context,
method = "POST",
path = "/v1/responses",
body = requestBody.toString(),
)
if (response.statusCode != 200) {
throw IOException("HTTP ${response.statusCode}: ${response.body}")
}
return parseResponsesOutputText(response.body)
}
fun buildRequest(
model: String,
instructions: String,
prompt: String,
): JSONObject {
return JSONObject()
.put("model", model)
.put("store", false)
.put("stream", true)
.put("instructions", instructions)
.put(
"input",
JSONArray().put(
JSONObject()
.put("role", "user")
.put(
"content",
JSONArray().put(
JSONObject()
.put("type", "input_text")
.put("text", prompt),
),
),
),
)
}
fun parseResponsesOutputText(body: String): String {
val trimmedBody = body.trim()
if (trimmedBody.startsWith("event:") || trimmedBody.startsWith("data:")) {
return parseResponsesStreamOutputText(trimmedBody)
}
return parseResponsesJsonOutputText(JSONObject(trimmedBody))
}
private fun parseResponsesJsonOutputText(data: JSONObject): String {
val directOutput = data.optString("output_text")
if (directOutput.isNotBlank()) {
return directOutput
}
val output = data.optJSONArray("output")
?: throw IOException("Responses payload missing output")
val combined = buildString {
for (outputIndex in 0 until output.length()) {
val item = output.optJSONObject(outputIndex) ?: continue
val content = item.optJSONArray("content") ?: continue
for (contentIndex in 0 until content.length()) {
val part = content.optJSONObject(contentIndex) ?: continue
if (part.optString("type") == "output_text") {
append(part.optString("text"))
}
}
}
}
if (combined.isBlank()) {
throw IOException("Responses payload missing output_text content")
}
return combined
}
private fun parseResponsesStreamOutputText(body: String): String {
val deltaText = StringBuilder()
val completedItems = mutableListOf<String>()
body.split("\n\n").forEach { rawEvent ->
val lines = rawEvent.lineSequence().map(String::trimEnd).toList()
if (lines.isEmpty()) {
return@forEach
}
val dataPayload = lines
.filter { it.startsWith("data:") }
.joinToString("\n") { it.removePrefix("data:").trimStart() }
.trim()
if (dataPayload.isEmpty() || dataPayload == "[DONE]") {
return@forEach
}
val event = JSONObject(dataPayload)
when (event.optString("type")) {
"response.output_text.delta" -> deltaText.append(event.optString("delta"))
"response.output_item.done" -> {
val item = event.optJSONObject("item") ?: return@forEach
val content = item.optJSONArray("content") ?: return@forEach
val text = buildString {
for (index in 0 until content.length()) {
val part = content.optJSONObject(index) ?: continue
if (part.optString("type") == "output_text") {
append(part.optString("text"))
}
}
}
if (text.isNotBlank()) {
completedItems += text
}
}
"response.failed" -> throw IOException(event.toString())
}
}
if (deltaText.isNotBlank()) {
return deltaText.toString()
}
val completedText = completedItems.joinToString("")
if (completedText.isNotBlank()) {
return completedText
}
throw IOException("Responses stream missing output_text content")
}
}