feat(ai): preserve Codex end_turn for debugging (#7766)

This commit is contained in:
David Brailovsky 2026-08-07 18:03:12 +02:00 • committed by GitHub
parent 541ed488d8
commit c3e7bc60a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 24 additions and 6 deletions

View file

@ -662,7 +662,7 @@ async function processStream(
grammarToolInputProperties: ReadonlyMap<string, string>,
options?: OpenAICodexResponsesOptions,
): Promise<void> {
await processResponsesStream(mapCodexEvents(parseSSE(response, options?.signal)), output, stream, model, {
await processResponsesStream(mapCodexEvents(parseSSE(response, options?.signal), output), output, stream, model, {
serviceTier: options?.serviceTier,
grammarToolInputProperties,
resolveServiceTier: resolveCodexServiceTier,
@ -719,7 +719,10 @@ function extractCodexEventError(event: Record<string, unknown>): { code?: string
};
}
async function* mapCodexEvents(events: AsyncIterable<Record<string, unknown>>): AsyncGenerator<ResponseStreamEvent> {
async function* mapCodexEvents(
events: AsyncIterable<Record<string, unknown>>,
output: AssistantMessage,
): AsyncGenerator<ResponseStreamEvent> {
for await (const event of events) {
const type = typeof event.type === "string" ? event.type : undefined;
if (!type) continue;
@ -740,7 +743,10 @@ async function* mapCodexEvents(events: AsyncIterable<Record<string, unknown>>):
}
if (type === "response.done" || type === "response.completed" || type === "response.incomplete") {
const response = (event as { response?: { status?: unknown } }).response;
const response = (event as { response?: { status?: unknown; end_turn?: unknown } }).response;
if (typeof response?.end_turn === "boolean") {
output.endTurn = response.end_turn;
}
const normalizedResponse = response
? { ...response, status: normalizeCodexStatus(response.status) }
: response;
@ -1504,7 +1510,7 @@ async function processWebSocketStream(
socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
await processResponsesStream(
startWebSocketOutputOnFirstEvent(
mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)),
mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs), output),
onStart,
),
output,

View file

@ -424,6 +424,11 @@ export interface AssistantMessage {
deferred?: DeferredHandle;
errorMessage?: string;
rawStopReason?: string;
/**
* Provider indication of whether the model explicitly ended its turn.
* Preserved for debugging and does not currently affect agent control flow.
*/
endTurn?: boolean;
timestamp: number; // Unix timestamp in milliseconds
}

View file

@ -49,9 +49,11 @@ function decodeCodexRequestBody(body: RequestInit["body"] | undefined): Record<s
function buildSSEPayload({
status,
includeDone = false,
endTurn,
}: {
status: "completed" | "incomplete";
includeDone?: boolean;
endTurn?: boolean;
}): string {
const terminalType = status === "incomplete" ? "response.incomplete" : "response.completed";
const events = [
@ -75,6 +77,7 @@ function buildSSEPayload({
type: terminalType,
response: {
status,
end_turn: endTurn,
incomplete_details: status === "incomplete" ? { reason: "max_output_tokens" } : null,
usage: {
input_tokens: 5,
@ -210,7 +213,7 @@ describe("openai-codex streaming", () => {
process.env.PI_CODING_AGENT_DIR = tempDir;
const token = mockToken();
const encoder = new TextEncoder();
const sse = buildSSEPayload({ status: "completed", includeDone: true });
const sse = buildSSEPayload({ status: "completed", includeDone: true, endTurn: false });
const stream = new ReadableStream<Uint8Array>({
start(controller) {
@ -263,6 +266,7 @@ describe("openai-codex streaming", () => {
expect(result.content.find((c) => c.type === "text")?.text).toBe("Hello");
expect(result.stopReason).toBe("stop");
expect(result.endTurn).toBe(false);
});
it("maps response.incomplete to stopReason length even when the SSE body stays open", async () => {
@ -1273,6 +1277,7 @@ describe("openai-codex streaming", () => {
type: "response.completed",
response: {
status: "completed",
end_turn: false,
usage: {
input_tokens: 5,
output_tokens: 3,
@ -1317,12 +1322,13 @@ describe("openai-codex streaming", () => {
messages: [{ role: "user", content: "Say hello", timestamp: 1 }],
};
await streamSimpleOpenAICodexResponses(model, context, {
const result = await streamSimpleOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "session-auto",
transport: "auto",
}).result();
expect(result.endTurn).toBe(false);
expect(sentBodies).toHaveLength(1);
expect(capturedWebSocketHeaders?.["session-id"]).toBe("session-auto");
expect(capturedWebSocketHeaders?.session_id).toBeUndefined();

View file

@ -94,6 +94,7 @@ type _AiAssistantMessageFieldsAccountedFor = Assert<
| "deferred"
| "errorMessage"
| "rawStopReason"
| "endTurn"
| "timestamp"
>
>;