Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"vitest": "^4.0.10"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.22.1",
"@agentclientprotocol/sdk": "^0.25.1",
"@openai/codex": "^0.139.0",
"diff": "^8.0.3",
"open": "^11.0.0",
Expand Down
63 changes: 60 additions & 3 deletions src/AcpExtensions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,68 @@
export type ExtMethodRequest = AuthenticationStatusRequest | AuthenticationLogoutRequest
import type {
ClientSideConnection,
LoadSessionResponse,
NewSessionResponse,
ResumeSessionResponse,
SessionId,
} from "@agentclientprotocol/sdk";

export const LEGACY_SET_SESSION_MODEL_METHOD = "session/set_model";

export type LegacySessionModel = {
modelId: string;
name: string;
description?: string | null;
}

export type LegacySessionModelState = {
availableModels: Array<LegacySessionModel>;
currentModelId: string;
}

export type LegacySetSessionModelRequest = {
sessionId: SessionId;
modelId: string;
}

export type LegacySetSessionModelResponse = {}

export type LegacyNewSessionResponse = NewSessionResponse & {
models?: LegacySessionModelState | null;
}

export type LegacyLoadSessionResponse = LoadSessionResponse & {
models?: LegacySessionModelState | null;
}

export type LegacyResumeSessionResponse = ResumeSessionResponse & {
models?: LegacySessionModelState | null;
}

export type ExtMethodRequest =
AuthenticationStatusRequest
| AuthenticationLogoutRequest
| LegacySetSessionModelExtRequest

export function isExtMethodRequest(request: { method: string, params: Record<string, unknown> }): request is ExtMethodRequest {
return request.method === "authentication/status" || request.method === "authentication/logout";
return request.method === "authentication/status"
|| request.method === "authentication/logout"
|| request.method === LEGACY_SET_SESSION_MODEL_METHOD;
}

export type AuthenticationStatusRequest = { method: "authentication/status", params: {} }
export type AuthenticationStatusResponse = { type: "api-key" } | { type: "chat-gpt", email: string } | { type: "gateway", name: string } | { type: "unauthenticated" }

export type AuthenticationLogoutRequest = { method: "authentication/logout", params: {} }
export type AuthenticationLogoutResponse = {}
export type AuthenticationLogoutResponse = {}

export type LegacySetSessionModelExtRequest = {
method: typeof LEGACY_SET_SESSION_MODEL_METHOD;
params: LegacySetSessionModelRequest;
}

export async function legacySetSessionModel(
connection: Pick<ClientSideConnection, "extMethod">,
params: LegacySetSessionModelRequest,
): Promise<LegacySetSessionModelResponse> {
return await connection.extMethod(LEGACY_SET_SESSION_MODEL_METHOD, params) as LegacySetSessionModelResponse;
}
53 changes: 38 additions & 15 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as acp from "@agentclientprotocol/sdk";
import {RequestError, type SessionId, type SessionModelState, type SessionModeState} from "@agentclientprotocol/sdk";
import {RequestError, type SessionId, type SessionModeState} from "@agentclientprotocol/sdk";
import {CodexEventHandler} from "./CodexEventHandler";
import {CodexApprovalHandler} from "./CodexApprovalHandler";
import {CodexElicitationHandler} from "./CodexElicitationHandler";
Expand Down Expand Up @@ -33,7 +33,16 @@ import {CodexCommands} from "./CodexCommands";
import type {QuotaMeta} from "./QuotaMeta";
import {logger} from "./Logger";
import {sanitizeMcpServerName} from "./McpServerName";
import {isExtMethodRequest} from "./AcpExtensions";
import {
type LegacyLoadSessionResponse,
type LegacyNewSessionResponse,
type LegacyResumeSessionResponse,
type LegacySessionModelState,
type LegacySetSessionModelRequest,
type LegacySetSessionModelResponse,
isExtMethodRequest,
LEGACY_SET_SESSION_MODEL_METHOD,
} from "./AcpExtensions";
import {
createCommandExecutionUpdate,
createDynamicToolCallUpdate,
Expand Down Expand Up @@ -189,9 +198,11 @@ export class CodexAcpServer implements acp.Agent {
case "authentication/status":
return await this.runWithProcessCheck(() => this.codexAcpClient.getAuthenticationStatus());
case "authentication/logout": {
await this.unstable_logout({});
await this.logout({});
return {};
}
case LEGACY_SET_SESSION_MODEL_METHOD:
return await this.unstable_setSessionModel(this.parseLegacySetSessionModelParams(methodRequest.params));
}
}

Expand All @@ -212,7 +223,7 @@ export class CodexAcpServer implements acp.Agent {
}
}

async getOrCreateSession(request: acp.NewSessionRequest | acp.ResumeSessionRequest): Promise<[SessionId, SessionModelState, SessionModeState]> {
async getOrCreateSession(request: acp.NewSessionRequest | acp.ResumeSessionRequest): Promise<[SessionId, LegacySessionModelState, SessionModeState]> {
try {
return await this.tryCreateSession(request);
} catch (e) {
Expand Down Expand Up @@ -292,7 +303,7 @@ export class CodexAcpServer implements acp.Agent {
return generation;
}

async tryCreateSession(request: acp.NewSessionRequest | acp.ResumeSessionRequest): Promise<[SessionId, SessionModelState, SessionModeState]> {
async tryCreateSession(request: acp.NewSessionRequest | acp.ResumeSessionRequest): Promise<[SessionId, LegacySessionModelState, SessionModeState]> {
const requestedSessionGeneration = "sessionId" in request
? this.beginSessionOpen(request.sessionId)
: null;
Expand Down Expand Up @@ -372,7 +383,7 @@ export class CodexAcpServer implements acp.Agent {
}

this.publishAvailableCommandsAsync(sessionId);
const sessionModelState: SessionModelState = this.createModelState(models, currentModelId);
const sessionModelState: LegacySessionModelState = this.createModelState(models, currentModelId);
const sessionModeState: SessionModeState = sessionState.agentMode.toSessionModeState();

return [sessionId, sessionModelState, sessionModeState];
Expand All @@ -386,7 +397,7 @@ export class CodexAcpServer implements acp.Agent {
return accountResponse.account;
}

async loadSession(params: acp.LoadSessionRequest): Promise<acp.LoadSessionResponse> {
async loadSession(params: acp.LoadSessionRequest): Promise<LegacyLoadSessionResponse> {
logger.log("Loading session...", {sessionId: params.sessionId});
const {
sessionId,
Expand All @@ -409,7 +420,7 @@ export class CodexAcpServer implements acp.Agent {
};
}

async resumeSession(params: acp.ResumeSessionRequest): Promise<acp.ResumeSessionResponse> {
async resumeSession(params: acp.ResumeSessionRequest): Promise<LegacyResumeSessionResponse> {
logger.log("Resuming session...", {sessionId: params.sessionId});
const [sessionId, modelState, modeState] = await this.getOrCreateSession(params);

Expand Down Expand Up @@ -465,7 +476,7 @@ export class CodexAcpServer implements acp.Agent {
return {};
}

async unstable_deleteSession(params: acp.DeleteSessionRequest): Promise<acp.DeleteSessionResponse> {
async deleteSession(params: acp.DeleteSessionRequest): Promise<acp.DeleteSessionResponse> {
logger.log("Deleting session...", {sessionId: params.sessionId});
const sessionId = params.sessionId;
const shouldCloseLocalSession = this.hasLocalSession(sessionId);
Expand Down Expand Up @@ -502,7 +513,7 @@ export class CodexAcpServer implements acp.Agent {

async newSession(
params: acp.NewSessionRequest,
): Promise<acp.NewSessionResponse> {
): Promise<LegacyNewSessionResponse> {
logger.log("Starting new session...");
const [sessionId, modelState, modeState] = await this.getOrCreateSession(params);

Expand Down Expand Up @@ -533,7 +544,7 @@ export class CodexAcpServer implements acp.Agent {
return { };
}

async unstable_logout(_params: acp.LogoutRequest): Promise<void> {
async logout(_params: acp.LogoutRequest): Promise<void> {
logger.log("Logout request received");
await this.runWithProcessCheck(() => this.codexAcpClient.logout());
logger.log("Logout request completed");
Expand Down Expand Up @@ -630,7 +641,7 @@ export class CodexAcpServer implements acp.Agent {
sessionState.currentModelSupportsFast = modelSupportsFast(model);
}

async unstable_setSessionModel(params: acp.SetSessionModelRequest): Promise<acp.SetSessionModelResponse | void> {
async unstable_setSessionModel(params: LegacySetSessionModelRequest): Promise<LegacySetSessionModelResponse> {
logger.log("Set session model requested", {
sessionId: params.sessionId,
modelId: params.modelId
Expand Down Expand Up @@ -661,6 +672,18 @@ export class CodexAcpServer implements acp.Agent {
return {};
}

private parseLegacySetSessionModelParams(params: Record<string, unknown>): LegacySetSessionModelRequest {
const sessionId = params["sessionId"];
const modelId = params["modelId"];
if (typeof sessionId !== "string" || typeof modelId !== "string") {
throw RequestError.invalidParams();
}
return {
sessionId: sessionId,
modelId: modelId,
};
}

private createSessionConfigOptions(sessionState: SessionState): Array<acp.SessionConfigOption> {
const currentModelId = ModelId.fromString(sessionState.currentModelId);
return [
Expand Down Expand Up @@ -703,7 +726,7 @@ export class CodexAcpServer implements acp.Agent {
.join("-");
}

private createModelState(availableModels: Model[], selectedModelId: string): SessionModelState {
private createModelState(availableModels: Model[], selectedModelId: string): LegacySessionModelState {
const allowedModels = availableModels
.flatMap((model) =>
model.supportedReasoningEfforts.map((effort) => ({
Expand All @@ -722,7 +745,7 @@ export class CodexAcpServer implements acp.Agent {
request: acp.LoadSessionRequest
): Promise<{
sessionId: SessionId;
modelState: SessionModelState;
modelState: LegacySessionModelState;
modeState: SessionModeState;
thread: Thread;
}> {
Expand Down Expand Up @@ -797,7 +820,7 @@ export class CodexAcpServer implements acp.Agent {
}

await this.availableCommands.publish(sessionId);
const sessionModelState: SessionModelState = this.createModelState(models, currentModelId);
const sessionModelState: LegacySessionModelState = this.createModelState(models, currentModelId);
const sessionModeState: SessionModeState = sessionState.agentMode.toSessionModeState();

return {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/CodexACPAgent/CodexAcpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('ACP server test', { timeout: 40_000 }, () => {
const authenticatedResponse = await keyFixture.getCodexAcpAgent().extMethod("authentication/status", {});
expect(authenticatedResponse).toEqual({type: "api-key"});

await keyFixture.getCodexAcpAgent().unstable_logout({});
await keyFixture.getCodexAcpAgent().logout({});
const logoutResponse = await keyFixture.getCodexAcpAgent().extMethod("authentication/status", {});
expect(logoutResponse).toEqual({type: "unauthenticated"});
});
Expand Down Expand Up @@ -210,7 +210,7 @@ describe('ACP server test', { timeout: 40_000 }, () => {
const mockFixture = createCodexMockTestFixture();
const codexAcpAgent = mockFixture.getCodexAcpAgent();

const logoutSpy = vi.spyOn(codexAcpAgent, "unstable_logout").mockResolvedValue();
const logoutSpy = vi.spyOn(codexAcpAgent, "logout").mockResolvedValue();

await expect(codexAcpAgent.extMethod("authentication/logout", {})).resolves.toEqual({});
expect(logoutSpy).toHaveBeenCalledWith({});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {afterEach, expect, it} from "vitest";
import {legacySetSessionModel, type LegacyLoadSessionResponse} from "../../../AcpExtensions";
import {
createAuthenticatedFixture,
describeE2E,
Expand All @@ -22,7 +23,7 @@ describeE2E("E2E session persistence tests", () => {
beforeRestartFixture = await createAuthenticatedFixture();
const sessionId = (await beforeRestartFixture.createSession()).sessionId;

await beforeRestartFixture.connection.unstable_setSessionModel({sessionId, modelId: OTHER_TEST_MODEL_ID.toString()});
await legacySetSessionModel(beforeRestartFixture.connection, {sessionId, modelId: OTHER_TEST_MODEL_ID.toString()});
const memorizedToken = "token-for-tests-123";
await beforeRestartFixture.expectPromptText(
sessionId,
Expand All @@ -36,7 +37,7 @@ describeE2E("E2E session persistence tests", () => {
sessionId,
cwd: afterRestartFixture.workspaceDir,
mcpServers: [],
});
}) as LegacyLoadSessionResponse;
expect(loadSessionResponse.models?.currentModelId).toBe(OTHER_TEST_MODEL_ID.toString());

await afterRestartFixture.expectPromptText(
Expand Down
3 changes: 2 additions & 1 deletion src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from "node:path";
import {afterEach, expect, it} from "vitest";
import {AgentMode} from "../../../AgentMode";
import {legacySetSessionModel} from "../../../AcpExtensions";
import {
createAuthenticatedFixture,
createGatewayFixture,
Expand Down Expand Up @@ -49,7 +50,7 @@ describeE2E("E2E tests", () => {
expect(models.availableModels.length).toBeGreaterThan(0);
expect(models.currentModelId).toBe(DEFAULT_TEST_MODEL_ID.toString());

await fixture.connection.unstable_setSessionModel({
await legacySetSessionModel(fixture.connection, {
sessionId: session.sessionId,
modelId: OTHER_TEST_MODEL_ID.toString(),
});
Expand Down
7 changes: 4 additions & 3 deletions src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {expect, vi} from "vitest";
import {ModelId} from "../../../ModelId";
import {removeDirectoryWithRetry, writeCodexHomeConfig} from "../../acp-test-utils";
import type {PermissionResponder} from "./permission-responders";
import type {LegacyNewSessionResponse} from "../../../AcpExtensions";

export const DEFAULT_TEST_MODEL_ID = ModelId.create("gpt-5.2", "none");
export const OTHER_TEST_MODEL_ID = ModelId.create("gpt-5.3-codex", "low");
Expand All @@ -20,7 +21,7 @@ export interface TestSkill {
export interface SpawnedAgentFixture {
readonly connection: acp.ClientSideConnection;
readonly workspaceDir: string;
createSession(mcpServers?: acp.McpServer[]): Promise<acp.NewSessionResponse>;
createSession(mcpServers?: acp.McpServer[]): Promise<LegacyNewSessionResponse>;
restart(): Promise<SpawnedAgentFixture>;
writeSkill(skill: TestSkill, rootDir?: string): void;
setPermissionResponder(responder: PermissionResponder): void;
Expand Down Expand Up @@ -166,11 +167,11 @@ class SpawnedAgentFixtureImpl implements SpawnedAgentFixture {
return this.paths.workspaceDir;
}

async createSession(mcpServers: acp.McpServer[] = []): Promise<acp.NewSessionResponse> {
async createSession(mcpServers: acp.McpServer[] = []): Promise<LegacyNewSessionResponse> {
return await this.connection.newSession({
cwd: this.workspaceDir,
mcpServers,
});
}) as LegacyNewSessionResponse;
}

async restart(): Promise<SpawnedAgentFixture> {
Expand Down
17 changes: 16 additions & 1 deletion src/__tests__/CodexACPAgent/session-config-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
REASONING_EFFORT_CONFIG_ID,
} from "../../ModelConfigOption";
import type {Model, ReasoningEffortOption} from "../../app-server/v2";
import {LEGACY_SET_SESSION_MODEL_METHOD} from "../../AcpExtensions";

const lowEffort: ReasoningEffortOption = {reasoningEffort: "low", description: "Fast"};
const mediumEffort: ReasoningEffortOption = {reasoningEffort: "medium", description: "Balanced"};
Expand Down Expand Up @@ -43,7 +44,7 @@ async function createSession(currentModelId: string, availableModels: Array<Mode
});

const response = await codexAcpAgent.newSession({cwd: "/test/cwd", mcpServers: []});
return {codexAcpAgent, response};
return {codexAcpAgent, codexAcpClient, response};
}

describe("Session config options", () => {
Expand Down Expand Up @@ -189,6 +190,20 @@ describe("Session config options", () => {
expect(sessionState.availableModels.map(m => m.id)).toEqual(["fast-model", "extra-model"]);
});

it("changes the model through the legacy session/set_model extMethod", async () => {
const {fast, slow} = buildModels();
const {codexAcpAgent, codexAcpClient} = await createSession("fast-model[medium]", [fast]);
vi.spyOn(codexAcpClient, "fetchAvailableModels").mockResolvedValue([fast, slow]);

const response = await codexAcpAgent.extMethod(LEGACY_SET_SESSION_MODEL_METHOD, {
sessionId: "session-id",
modelId: "slow-model[medium]",
});

expect(response).toEqual({});
expect(codexAcpAgent.getSessionState("session-id").currentModelId).toBe("slow-model[medium]");
});

it("rejects unknown model, effort, and mode values", async () => {
const {fast} = buildModels();
const {codexAcpAgent} = await createSession("fast-model[medium]", [fast]);
Expand Down
Loading
Loading