Handle Codex request_user_input app-server questions

Fixes #195
This commit is contained in:
Mohamed Boudra
2026-04-05 09:21:45 +07:00
parent c32bc847bf
commit 702e2e7db9
3 changed files with 834 additions and 38 deletions
@@ -2,11 +2,14 @@ import { describe, expect, test } from "vitest";
import { execFileSync } from "node:child_process";
import os from "node:os";
import path from "node:path";
import { mkdtempSync } from "node:fs";
import { createServer } from "node:http";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { once } from "node:events";
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
import { createTestLogger } from "../../../test-utils/test-logger.js";
import { agentConfigs } from "../../daemon-e2e/agent-configs.js";
import type { AgentStreamEvent } from "../agent-sdk-types.js";
const CODEX_TEST_MODEL = agentConfigs.codex.model;
const CODEX_TEST_THINKING_OPTION_ID = agentConfigs.codex.thinkingOptionId;
@@ -20,6 +23,192 @@ function isCodexInstalled(): boolean {
}
}
function sse(events: unknown[]): string {
return events
.map((event) => {
const type =
typeof event === "object" &&
event !== null &&
"type" in event &&
typeof (event as { type?: unknown }).type === "string"
? (event as { type: string }).type
: "message";
return `event: ${type}\ndata: ${JSON.stringify(event)}\n\n`;
})
.join("");
}
function responseCreated(id: string): Record<string, unknown> {
return {
type: "response.created",
response: { id },
};
}
function responseCompleted(id: string): Record<string, unknown> {
return {
type: "response.completed",
response: {
id,
usage: {
input_tokens: 0,
input_tokens_details: null,
output_tokens: 0,
output_tokens_details: null,
total_tokens: 0,
},
},
};
}
function functionCallEvent(callId: string, name: string, argumentsJson: string): Record<string, unknown> {
return {
type: "response.output_item.done",
item: {
type: "function_call",
call_id: callId,
name,
arguments: argumentsJson,
},
};
}
function assistantMessageEvent(id: string, text: string): Record<string, unknown> {
return {
type: "response.output_item.done",
item: {
type: "message",
role: "assistant",
id,
content: [{ type: "output_text", text }],
},
};
}
function requestUserInputSse(callId: string): string {
return sse([
responseCreated("resp-1"),
functionCallEvent(
callId,
"request_user_input",
JSON.stringify({
questions: [
{
id: "confirm_path",
header: "Confirm",
question: "Proceed with the plan?",
options: [
{
label: "Yes (Recommended)",
description: "Continue the current plan.",
},
{
label: "No",
description: "Stop and revisit the approach.",
},
],
},
],
}),
),
responseCompleted("resp-1"),
]);
}
function assistantMessageSse(text: string): string {
return sse([responseCreated("resp-2"), assistantMessageEvent("msg-1", text), responseCompleted("resp-2")]);
}
async function startMockResponsesServer(sequence: string[]): Promise<{
url: string;
close: () => Promise<void>;
requestBodies: string[];
}> {
const requestBodies: string[] = [];
let index = 0;
const server = createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
req.on("end", () => {
requestBodies.push(Buffer.concat(chunks).toString("utf8"));
if (req.method !== "POST" || req.url !== "/v1/responses") {
res.statusCode = 404;
res.end("not found");
return;
}
const body = sequence[index] ?? assistantMessageSse("done");
index += 1;
res.statusCode = 200;
res.setHeader("content-type", "text/event-stream");
res.setHeader("cache-control", "no-cache");
res.end(body);
});
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Expected TCP address for mock responses server");
}
return {
url: `http://127.0.0.1:${address.port}`,
requestBodies,
close: () =>
new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
}),
};
}
function writeMockCodexConfig(codexHome: string, serverUrl: string): void {
writeFileSync(
path.join(codexHome, "config.toml"),
`
model = "mock-model"
approval_policy = "untrusted"
sandbox_mode = "read-only"
model_provider = "mock_provider"
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "${serverUrl}/v1"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
`,
);
}
function waitForEvent<TEvent extends AgentStreamEvent>(params: {
session: {
subscribe(callback: (event: AgentStreamEvent) => void): () => void;
};
predicate: (event: AgentStreamEvent) => event is TEvent;
timeoutMs: number;
label: string;
}): Promise<TEvent> {
return new Promise<TEvent>((resolve, reject) => {
const timeout = setTimeout(() => {
unsubscribe();
reject(new Error(`Timed out waiting for ${params.label}`));
}, params.timeoutMs);
const unsubscribe = params.session.subscribe((event) => {
if (!params.predicate(event)) {
return;
}
clearTimeout(timeout);
unsubscribe();
resolve(event);
});
});
}
describe("Codex app-server provider (e2e)", () => {
test.runIf(isCodexInstalled())(
"lists models and runs a simple prompt",
@@ -43,4 +232,171 @@ describe("Codex app-server provider (e2e)", () => {
},
30000,
);
test.runIf(isCodexInstalled())(
"surfaces request_user_input from the app-server as question permissions and timeline tool calls",
async () => {
const cwd = mkdtempSync(path.join(os.tmpdir(), "codex-app-server-question-cwd-"));
const codexHome = mkdtempSync(path.join(os.tmpdir(), "codex-app-server-question-home-"));
const mockServer = await startMockResponsesServer([
requestUserInputSse("call1"),
assistantMessageSse("done"),
]);
try {
writeMockCodexConfig(codexHome, mockServer.url);
const client = new CodexAppServerAgentClient(createTestLogger());
const session = await client.createSession(
{
provider: "codex",
cwd,
modeId: "auto",
model: "mock-model",
thinkingOptionId: "medium",
},
{
env: {
CODEX_HOME: codexHome,
},
},
);
try {
await session.setFeature?.("plan_mode", true);
const events: AgentStreamEvent[] = [];
session.subscribe((event) => {
events.push(event);
});
const questionRequested = waitForEvent({
session,
timeoutMs: 15_000,
label: "question permission request",
predicate: (
event,
): event is Extract<
AgentStreamEvent,
{ type: "permission_requested" }
> =>
event.type === "permission_requested" &&
event.request.provider === "codex" &&
event.request.kind === "question" &&
event.request.name === "request_user_input",
});
const turnFinished = waitForEvent({
session,
timeoutMs: 15_000,
label: "turn completion",
predicate: (
event,
): event is Extract<
AgentStreamEvent,
{ type: "turn_completed" | "turn_failed" | "turn_canceled" }
> =>
event.type === "turn_completed" ||
event.type === "turn_failed" ||
event.type === "turn_canceled",
});
await session.startTurn("ask something");
const permissionEvent = await questionRequested;
expect(permissionEvent.request.input).toEqual({
questions: [
{
id: "confirm_path",
header: "Confirm",
question: "Proceed with the plan?",
isOther: true,
options: [
{
label: "Yes (Recommended)",
description: "Continue the current plan.",
},
{
label: "No",
description: "Stop and revisit the approach.",
},
],
},
],
});
const runningQuestionCall = events.find(
(event) =>
event.type === "timeline" &&
event.provider === "codex" &&
event.item.type === "tool_call" &&
event.item.name === "request_user_input" &&
event.item.status === "running",
);
expect(runningQuestionCall).toBeDefined();
await session.respondToPermission(permissionEvent.request.id, {
behavior: "allow",
updatedInput: {
answers: {
Confirm: "Yes (Recommended)",
},
},
});
const terminalEvent = await turnFinished;
expect(terminalEvent.type).toBe("turn_completed");
const completedQuestionCall = events.find(
(event) =>
event.type === "timeline" &&
event.provider === "codex" &&
event.item.type === "tool_call" &&
event.item.name === "request_user_input" &&
event.item.status === "completed",
);
expect(completedQuestionCall).toBeDefined();
if (
!completedQuestionCall ||
completedQuestionCall.type !== "timeline" ||
completedQuestionCall.item.type !== "tool_call"
) {
throw new Error("Expected completed request_user_input tool call");
}
expect(completedQuestionCall.item.metadata).toMatchObject({
answers: {
confirm_path: ["Yes (Recommended)"],
},
});
expect(
mockServer.requestBodies.some(
(body) =>
body.includes('"type":"function_call_output"') &&
body.includes('"call_id":"call1"'),
),
).toBe(true);
const finalAssistantMessage = [...events]
.reverse()
.find((event) => event.type === "timeline" && event.item.type === "assistant_message");
expect(finalAssistantMessage).toBeDefined();
if (
!finalAssistantMessage ||
finalAssistantMessage.type !== "timeline" ||
finalAssistantMessage.item.type !== "assistant_message"
) {
throw new Error("Expected final assistant message");
}
expect(finalAssistantMessage.item.text.trim()).toBe("done");
} finally {
await session.close();
}
} finally {
await mockServer.close();
rmSync(cwd, { recursive: true, force: true });
rmSync(codexHome, { recursive: true, force: true });
}
},
30_000,
);
});
@@ -1,7 +1,7 @@
import { describe, expect, test } from "vitest";
import { existsSync, rmSync } from "node:fs";
import type { AgentLaunchContext } from "../agent-sdk-types.js";
import type { AgentLaunchContext, AgentSession, AgentSessionConfig, AgentStreamEvent } from "../agent-sdk-types.js";
import {
__codexAppServerInternals,
codexAppServerTurnInputFromPrompt,
@@ -10,6 +10,32 @@ import { createTestLogger } from "../../../test-utils/test-logger.js";
const ONE_BY_ONE_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X1r0AAAAASUVORK5CYII=";
const CODEX_PROVIDER = "codex";
function createConfig(overrides: Partial<AgentSessionConfig> = {}): AgentSessionConfig {
return {
provider: CODEX_PROVIDER,
cwd: "/tmp/codex-question-test",
modeId: "auto",
model: "gpt-5.4",
...overrides,
};
}
function createSession(configOverrides: Partial<AgentSessionConfig> = {}) {
const session = new __codexAppServerInternals.CodexAppServerAgentSession(
createConfig(configOverrides),
null,
createTestLogger(),
() => {
throw new Error("Test session cannot spawn Codex app-server");
},
) as unknown as AgentSession & { [key: string]: unknown };
session.connected = true;
session.currentThreadId = "test-thread";
session.activeForegroundTurnId = "test-turn";
return session;
}
describe("Codex app-server provider", () => {
const logger = createTestLogger();
@@ -138,4 +164,185 @@ describe("Codex app-server provider", () => {
expect(env.PASEO_AGENT_ID).toBe(launchContext.env?.PASEO_AGENT_ID);
expect(env.PASEO_TEST_FLAG).toBe(launchContext.env?.PASEO_TEST_FLAG);
});
test("projects request_user_input into a question permission and running timeline tool call", () => {
const session = createSession();
const events: AgentStreamEvent[] = [];
session.subscribe((event) => events.push(event));
void (session as any).handleToolApprovalRequest({
itemId: "call-question-1",
threadId: "thread-1",
turnId: "turn-1",
questions: [
{
id: "favorite_drink",
header: "Drink",
question: "Which drink do you want?",
options: [
{ label: "Coffee", description: "Default" },
{ label: "Tea" },
],
},
],
});
expect(events).toEqual([
{
type: "timeline",
provider: "codex",
turnId: "test-turn",
item: {
type: "tool_call",
callId: "call-question-1",
name: "request_user_input",
status: "running",
error: null,
detail: {
type: "plain_text",
text: "Drink: Which drink do you want?\nOptions: Coffee, Tea",
icon: "brain",
},
metadata: {
questions: [
{
id: "favorite_drink",
header: "Drink",
question: "Which drink do you want?",
options: [
{ label: "Coffee", description: "Default" },
{ label: "Tea" },
],
},
],
},
},
},
{
type: "permission_requested",
provider: "codex",
turnId: "test-turn",
request: {
id: "permission-call-question-1",
provider: "codex",
name: "request_user_input",
kind: "question",
title: "Question",
detail: {
type: "plain_text",
text: "Drink: Which drink do you want?\nOptions: Coffee, Tea",
icon: "brain",
},
input: {
questions: [
{
id: "favorite_drink",
header: "Drink",
question: "Which drink do you want?",
options: [
{ label: "Coffee", description: "Default" },
{ label: "Tea" },
],
},
],
},
metadata: {
itemId: "call-question-1",
threadId: "thread-1",
turnId: "turn-1",
questions: [
{
id: "favorite_drink",
header: "Drink",
question: "Which drink do you want?",
options: [
{ label: "Coffee", description: "Default" },
{ label: "Tea" },
],
},
],
},
},
},
]);
});
test("maps question responses from headers back to question ids and completes the tool call", async () => {
const session = createSession();
const events: AgentStreamEvent[] = [];
session.subscribe((event) => events.push(event));
const pendingResponse = (session as any).handleToolApprovalRequest({
itemId: "call-question-2",
threadId: "thread-1",
turnId: "turn-1",
questions: [
{
id: "favorite_drink",
header: "Drink",
question: "Which drink do you want?",
options: [{ label: "Coffee" }, { label: "Tea" }],
},
],
});
await session.respondToPermission("permission-call-question-2", {
behavior: "allow",
updatedInput: {
answers: {
Drink: "Tea",
},
},
});
await expect(pendingResponse).resolves.toEqual({
answers: {
favorite_drink: { answers: ["Tea"] },
},
});
expect(events.at(-2)).toEqual({
type: "permission_resolved",
provider: "codex",
turnId: "test-turn",
requestId: "permission-call-question-2",
resolution: {
behavior: "allow",
updatedInput: {
answers: {
Drink: "Tea",
},
},
},
});
expect(events.at(-1)).toEqual({
type: "timeline",
provider: "codex",
turnId: "test-turn",
item: {
type: "tool_call",
callId: "call-question-2",
name: "request_user_input",
status: "completed",
error: null,
detail: {
type: "plain_text",
text: "Drink: Which drink do you want?\nOptions: Coffee, Tea\n\nAnswers:\n\nfavorite_drink: Tea",
icon: "brain",
},
metadata: {
questions: [
{
id: "favorite_drink",
header: "Drink",
question: "Which drink do you want?",
options: [{ label: "Coffee" }, { label: "Tea" }],
},
],
answers: {
favorite_drink: ["Tea"],
},
},
},
});
});
});
@@ -573,6 +573,17 @@ class CodexAppServerClient {
this.child.stdin.write(`${JSON.stringify(payload)}\n`);
}
private writeJsonRpcResponse(response: JsonRpcResponse): void {
if (this.disposed || this.child.stdin.destroyed || !this.child.stdin.writable) {
return;
}
try {
this.child.stdin.write(`${JSON.stringify(response)}\n`);
} catch (error) {
this.logger.debug({ error }, "Failed to write Codex app-server JSON-RPC response");
}
}
async dispose(): Promise<void> {
if (this.disposed) return;
this.disposed = true;
@@ -617,14 +628,12 @@ class CodexAppServerClient {
const handler = this.requestHandlers.get(request.method);
try {
const result = handler ? await handler(request.params) : {};
const response: JsonRpcResponse = { id: request.id, result };
this.child.stdin.write(`${JSON.stringify(response)}\n`);
this.writeJsonRpcResponse({ id: request.id, result });
} catch (error) {
const response: JsonRpcResponse = {
this.writeJsonRpcResponse({
id: request.id,
error: { message: error instanceof Error ? error.message : String(error) },
};
this.child.stdin.write(`${JSON.stringify(response)}\n`);
});
}
return;
}
@@ -723,6 +732,187 @@ function mapCodexPlanToToolCall(params: { callId: string; text: string }): ToolC
};
}
type CodexQuestionOption = {
label: string;
description?: string;
};
type CodexQuestionPrompt = {
id: string;
header: string;
question: string;
options: CodexQuestionOption[];
multiSelect?: boolean;
isOther?: boolean;
isSecret?: boolean;
};
function normalizeCodexQuestionPrompts(raw: unknown): CodexQuestionPrompt[] {
if (!Array.isArray(raw)) {
return [];
}
const questions: CodexQuestionPrompt[] = [];
for (const item of raw) {
if (!item || typeof item !== "object") {
continue;
}
const record = item as Record<string, unknown>;
const id = nonEmptyString(record.id);
const header = nonEmptyString(record.header);
const question = nonEmptyString(record.question);
if (!id || !header || !question) {
continue;
}
const options = Array.isArray(record.options)
? record.options.flatMap((option): CodexQuestionOption[] => {
if (!option || typeof option !== "object") {
return [];
}
const optionRecord = option as Record<string, unknown>;
const label = nonEmptyString(optionRecord.label);
if (!label) {
return [];
}
return [
{
label,
...(typeof optionRecord.description === "string" &&
optionRecord.description.trim().length > 0
? { description: optionRecord.description }
: {}),
},
];
})
: [];
questions.push({
id,
header,
question,
options,
...(record.multiSelect === true ? { multiSelect: true } : {}),
...(record.isOther === true ? { isOther: true } : {}),
...(record.isSecret === true ? { isSecret: true } : {}),
});
}
return questions;
}
function formatCodexQuestionPrompts(questions: CodexQuestionPrompt[]): string {
return questions
.map((question) => {
const lines = [`${question.header}: ${question.question}`];
if (question.options.length > 0) {
lines.push(`Options: ${question.options.map((option) => option.label).join(", ")}`);
}
return lines.join("\n");
})
.join("\n\n")
.trim();
}
function mapCodexQuestionRequestToToolCall(params: {
callId: string;
questions: CodexQuestionPrompt[];
status: ToolCallTimelineItem["status"];
answers?: Record<string, string[]>;
error?: unknown;
}): ToolCallTimelineItem {
const formattedQuestions = formatCodexQuestionPrompts(params.questions);
const formattedAnswers =
params.answers && Object.keys(params.answers).length > 0
? Object.entries(params.answers)
.map(([id, values]) => `${id}: ${values.join(", ")}`)
.join("\n")
: null;
const detailText =
params.status === "completed" && formattedAnswers
? [formattedQuestions, "Answers:", formattedAnswers].filter(Boolean).join("\n\n")
: formattedQuestions;
const base = {
type: "tool_call" as const,
callId: params.callId,
name: "request_user_input",
detail: {
type: "plain_text" as const,
text: detailText,
icon: "brain" as const,
},
metadata: {
questions: params.questions,
...(params.answers ? { answers: params.answers } : {}),
},
};
if (params.status === "failed") {
return {
...base,
status: "failed",
error: params.error ?? { message: "Question dismissed" },
};
}
if (params.status === "canceled") {
return {
...base,
status: "canceled",
error: null,
};
}
if (params.status === "running") {
return {
...base,
status: "running",
error: null,
};
}
return {
...base,
status: "completed",
error: null,
};
}
function mapCodexQuestionResponseByHeader(params: {
questions: CodexQuestionPrompt[];
response: AgentPermissionResponse;
}): Record<string, { answers: string[] }> | null {
if (params.response.behavior !== "allow") {
return null;
}
const answersRecord =
params.response.updatedInput && typeof params.response.updatedInput === "object"
? ((params.response.updatedInput as Record<string, unknown>).answers as
| Record<string, unknown>
| undefined)
: undefined;
if (!answersRecord || typeof answersRecord !== "object") {
return null;
}
const answers: Record<string, { answers: string[] }> = {};
for (const question of params.questions) {
const rawAnswer = answersRecord[question.header];
if (typeof rawAnswer !== "string") {
continue;
}
const normalizedAnswer = rawAnswer.trim();
if (!normalizedAnswer) {
continue;
}
const values = question.multiSelect
? normalizedAnswer
.split(",")
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0)
: [normalizedAnswer];
if (values.length > 0) {
answers[question.id] = { answers: values };
}
}
return Object.keys(answers).length > 0 ? answers : null;
}
type CodexPatchFileChange = {
path: string;
kind?: string;
@@ -2072,8 +2262,8 @@ class CodexAppServerAgentSession implements AgentSession {
string,
{
resolve: (value: unknown) => void;
kind: "command" | "file" | "tool";
questions?: Array<{ id: string; options?: Array<{ label?: string; value?: string }> }>;
kind: "command" | "file" | "question";
questions?: CodexQuestionPrompt[];
}
>();
private resolvedPermissionRequests = new Set<string>();
@@ -2284,6 +2474,10 @@ class CodexAppServerAgentSession implements AgentSession {
this.client.setRequestHandler("item/fileChange/requestApproval", (params) =>
this.handleFileChangeApprovalRequest(params),
);
this.client.setRequestHandler("item/tool/requestUserInput", (params) =>
this.handleToolApprovalRequest(params),
);
// Keep the legacy method name for older Codex builds.
this.client.setRequestHandler("tool/requestUserInput", (params) =>
this.handleToolApprovalRequest(params),
);
@@ -2745,26 +2939,53 @@ class CodexAppServerAgentSession implements AgentSession {
return;
}
// tool/requestUserInput
const answers: Record<string, { answers: string[] }> = {};
const questions = pending.questions ?? [];
const decision =
response.behavior === "allow" ? "accept" : response.interrupt ? "cancel" : "decline";
for (const question of questions) {
let picked = decision;
const options = question.options ?? [];
if (options.length > 0) {
const byLabel = options.find((opt) => (opt.label ?? "").toLowerCase().includes(decision));
const byValue = options.find((opt) => (opt.value ?? "").toLowerCase().includes(decision));
const option = byLabel ?? byValue ?? options[0]!;
picked = option.value ?? option.label ?? decision;
}
answers[question.id] = { answers: [picked] };
const itemId =
typeof pendingRequest?.metadata?.itemId === "string" ? pendingRequest.metadata.itemId : requestId;
if (response.behavior === "allow") {
const mappedAnswers = mapCodexQuestionResponseByHeader({
questions,
response,
});
const answers =
mappedAnswers ??
Object.fromEntries(
questions
.map((question) => {
const fallback = question.options[0]?.label?.trim();
return fallback
? [question.id, { answers: [fallback] }]
: null;
})
.filter((entry): entry is [string, { answers: string[] }] => entry !== null),
);
this.emitEvent({
type: "timeline",
provider: CODEX_PROVIDER,
item: mapCodexQuestionRequestToToolCall({
callId: itemId,
questions,
status: "completed",
answers: Object.fromEntries(
Object.entries(answers).map(([id, value]) => [id, value.answers]),
),
}),
});
pending.resolve({ answers });
return;
}
if (questions.length === 0) {
answers["default"] = { answers: [decision] };
}
pending.resolve({ answers });
this.emitEvent({
type: "timeline",
provider: CODEX_PROVIDER,
item: mapCodexQuestionRequestToToolCall({
callId: itemId,
questions,
status: response.interrupt ? "canceled" : "failed",
error: { message: response.message ?? "Question dismissed" },
}),
});
pending.resolve({ answers: {} });
}
describePersistence(): {
@@ -3463,34 +3684,43 @@ class CodexAppServerAgentSession implements AgentSession {
private handleToolApprovalRequest(params: unknown): Promise<unknown> {
const parsed = params as { itemId: string; threadId: string; turnId: string; questions: any[] };
const requestId = `permission-${parsed.itemId}`;
const questions = normalizeCodexQuestionPrompts(parsed.questions);
const request: AgentPermissionRequest = {
id: requestId,
provider: CODEX_PROVIDER,
name: "CodexTool",
kind: "tool",
title: "Tool action requires approval",
name: "request_user_input",
kind: "question",
title: "Question",
description: undefined,
detail: {
type: "unknown",
input: {
questions: Array.isArray(parsed.questions) ? parsed.questions : [],
},
output: null,
type: "plain_text",
text: formatCodexQuestionPrompts(questions),
icon: "brain",
},
input: { questions },
metadata: {
itemId: parsed.itemId,
threadId: parsed.threadId,
turnId: parsed.turnId,
questions: parsed.questions,
questions,
},
};
this.pendingPermissions.set(requestId, request);
this.emitEvent({
type: "timeline",
provider: CODEX_PROVIDER,
item: mapCodexQuestionRequestToToolCall({
callId: parsed.itemId,
questions,
status: "running",
}),
});
this.emitEvent({ type: "permission_requested", provider: CODEX_PROVIDER, request });
return new Promise((resolve) => {
this.pendingPermissionHandlers.set(requestId, {
resolve,
kind: "tool",
questions: Array.isArray(parsed.questions) ? parsed.questions : [],
kind: "question",
questions,
});
});
}
@@ -3723,8 +3953,11 @@ export const __codexAppServerInternals = {
buildCodexAppServerEnv,
codexModelSupportsFastMode,
CodexAppServerAgentSession,
formatCodexQuestionPrompts,
mapCodexQuestionRequestToToolCall,
mapCodexPatchNotificationToToolCall,
planStepsToMarkdown,
mapCodexPlanToToolCall,
normalizeCodexQuestionPrompts,
threadItemToTimeline,
};