diff --git a/readme-dev.md b/readme-dev.md index bc147807..2de6d510 100644 --- a/readme-dev.md +++ b/readme-dev.md @@ -6,7 +6,8 @@ Set `CODEX_PATH` to run a different Codex binary; versions other than the one sp - `CODEX_API_KEY` - API key used when the API-key auth method is selected. Takes precedence over `OPENAI_API_KEY`. - `OPENAI_API_KEY` - fallback API key used when the API-key auth method is selected. - `CODEX_PATH` - run a specific Codex executable instead of the bundled package dependency. -- `CODEX_CONFIG` - JSON object merged into the Codex session config. +- `CODEX_CONFIG` - JSON object merged into the Codex session config. Supported keys include: + - `model_reasoning_summary` - controls reasoning summary verbosity per turn. Valid values: `"auto"`, `"concise"`, `"detailed"`, `"none"`. Defaults to `"auto"` when unset. Example: `CODEX_CONFIG='{"model_reasoning_summary":"detailed"}'`. - `MODEL_PROVIDER` - model provider to pass to Codex for new sessions. - `DEFAULT_AUTH_REQUEST` - ACP auth request JSON used when Codex requires authentication. - `INITIAL_AGENT_MODE` - initial mode id: `read-only`, `agent`, or `agent-full-access`. diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 2ea875b5..3109340c 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -13,6 +13,7 @@ import type {Disposable} from "vscode-jsonrpc"; import type { ClientInfo, ReasoningEffort, + ReasoningSummary, ServiceTier, ServerNotification } from "./app-server"; @@ -672,7 +673,7 @@ export class CodexAcpClient { input: input, approvalPolicy: agentMode.approvalPolicy, sandboxPolicy: addAdditionalDirectoriesToSandboxPolicy(agentMode.sandboxPolicy, additionalDirectories), - summary: disableSummary ? "none" : "auto", + summary: disableSummary ? "none" : (resolveConfiguredSummary(this.config) ?? "auto"), effort: effort, model: modelId.model, serviceTier: serviceTier, @@ -854,6 +855,16 @@ export type SessionMetadataWithThread = SessionMetadata & { thread: Thread, } +const validReasoningSummaries: ReadonlySet = new Set(["auto", "concise", "detailed", "none"]); + +function resolveConfiguredSummary(config: JsonObject): ReasoningSummary | undefined { + const value = config["model_reasoning_summary"]; + if (typeof value === "string" && validReasoningSummaries.has(value)) { + return value as ReasoningSummary; + } + return undefined; +} + function buildPromptItems(prompt: acp.ContentBlock[]): UserInput[] { return prompt.map((block): UserInput | null => { switch (block.type) { diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 255d4ab2..802282ea 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -11,6 +11,7 @@ import { type TestFixture } from "../acp-test-utils"; import type {ServerNotification} from "../../app-server"; +import type {JsonObject} from "../../CodexAcpClient"; import type {SessionState} from "../../CodexAcpServer"; import {AgentMode} from "../../AgentMode"; import type {Model, ReviewStartResponse, ThreadGoal, TurnCompletedNotification, TurnStartParams} from "../../app-server/v2"; @@ -2968,8 +2969,8 @@ describe('ACP server test', { timeout: 40_000 }, () => { * Sets up a mock fixture with turnStart/awaitTurnCompleted spied on, * and a given session state. Returns the fixture and turnStart spy. */ - function setupPromptFixture(sessionOverrides?: Partial) { - const mockFixture = createCodexMockTestFixture(); + function setupPromptFixture(sessionOverrides?: Partial, options?: { codexConfig?: JsonObject }) { + const mockFixture = createCodexMockTestFixture(options); const sessionState = createTestSessionState(sessionOverrides); const turnStartSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "turnStart").mockResolvedValue({ turn: { @@ -3092,6 +3093,36 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ summary: "auto" })); }); + it ('should honor configured model_reasoning_summary from CODEX_CONFIG', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture({ + account: { type: "chatgpt", email: "test@example.com", planType: "pro" }, + }, { codexConfig: { model_reasoning_summary: "detailed" } }); + + await mockFixture.getCodexAcpAgent().prompt({ sessionId: "id", prompt: [{ type: "text", text: "test" }] }); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ summary: "detailed" })); + }); + + it ('should fall back to auto for invalid model_reasoning_summary value', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture({ + account: { type: "chatgpt", email: "test@example.com", planType: "pro" }, + }, { codexConfig: { model_reasoning_summary: "verbose" } }); + + await mockFixture.getCodexAcpAgent().prompt({ sessionId: "id", prompt: [{ type: "text", text: "test" }] }); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ summary: "auto" })); + }); + + it ('should force none when disableSummary is true regardless of configured summary', async () => { + const { mockFixture, turnStartSpy } = setupPromptFixture({ + account: { type: "apiKey" }, + }, { codexConfig: { model_reasoning_summary: "detailed" } }); + + await mockFixture.getCodexAcpAgent().prompt({ sessionId: "id", prompt: [{ type: "text", text: "test" }] }); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ summary: "none" })); + }); + it ('should reject prompt with images when model does not support image input', async () => { const { mockFixture } = setupPromptFixture({ supportedInputModalities: ["text"], diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index 13505155..9c6a3399 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -1,6 +1,6 @@ import * as acp from "@agentclientprotocol/sdk"; import type {CreateElicitationResponse, McpServerStdio, RequestPermissionResponse} from "@agentclientprotocol/sdk"; -import {CodexAcpClient} from '../CodexAcpClient'; +import {CodexAcpClient, type JsonObject} from '../CodexAcpClient'; import {CodexAppServerClient, type CodexConnectionEvent} from '../CodexAppServerClient'; import {startCodexConnection} from "../CodexJsonRpcConnection"; import {CodexAcpServer, type SessionState} from "../CodexAcpServer"; @@ -84,6 +84,7 @@ export interface ConnectionConfig { connection: MessageConnection; getExitCode: () => number | null; acpConnection?: AcpConnectionConfig; + codexConfig?: JsonObject; } export function createBaseTestFixture(config: ConnectionConfig): TestFixture { @@ -96,7 +97,7 @@ export function createBaseTestFixture(config: ConnectionConfig): TestFixture { }); const codexAppServerClient = new CodexAppServerClient(config.connection); - const codexAcpClient = new CodexAcpClient(codexAppServerClient); + const codexAcpClient = new CodexAcpClient(codexAppServerClient, config.codexConfig); const codexAcpAgent = new CodexAcpServer(acpConnection, codexAcpClient, undefined, config.getExitCode); const transportEvents: CodexConnectionEvent[] = []; @@ -254,7 +255,7 @@ export interface CodexMockTestFixture extends TestFixture { * Provides `sendServerRequest()` to simulate server-initiated requests (e.g., approval requests). * Provides `setPermissionResponse()` to control ACP permission dialog responses. */ -export function createCodexMockTestFixture(): CodexMockTestFixture { +export function createCodexMockTestFixture(options?: { codexConfig?: JsonObject }): CodexMockTestFixture { let unhandledNotificationHandler: ((notification: any) => void) | null = null; const requestHandlers = new Map Promise>(); @@ -306,7 +307,8 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { connection: acpConnection, events: acpConnectionEvents, eventHandlers: acpEventHandlers, - } + }, + ...(options?.codexConfig != null ? { codexConfig: options.codexConfig } : {}), }); return {