diff --git a/package-lock.json b/package-lock.json index 3298dda2..0c9e514e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.46", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "^0.22.1", + "@agentclientprotocol/sdk": "^0.25.1", "@openai/codex": "^0.139.0", "diff": "^8.0.3", "open": "^11.0.0", @@ -28,9 +28,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.22.1.tgz", - "integrity": "sha512-DfqXtl/8gO9NImq094MTaCXEU2vkhh6v7q/kT+9UjZxUqj8hYaya2OjLVIqn16MzNHcXEpShTR2RIauLSYeDQQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.25.1.tgz", + "integrity": "sha512-jx2rF3bdpGwZ75Q/meyEDLLbYmbtxk82Uh9hDCdxDvcEedBnNSF5hZAnL/kJR5VNz56JqwOmqnAqasC84MwwkQ==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" diff --git a/package.json b/package.json index 57d80385..73b2ed67 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/AcpExtensions.ts b/src/AcpExtensions.ts index 13780134..5d411d2f 100644 --- a/src/AcpExtensions.ts +++ b/src/AcpExtensions.ts @@ -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; + 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 }): 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 = {} \ No newline at end of file +export type AuthenticationLogoutResponse = {} + +export type LegacySetSessionModelExtRequest = { + method: typeof LEGACY_SET_SESSION_MODEL_METHOD; + params: LegacySetSessionModelRequest; +} + +export async function legacySetSessionModel( + connection: Pick, + params: LegacySetSessionModelRequest, +): Promise { + return await connection.extMethod(LEGACY_SET_SESSION_MODEL_METHOD, params) as LegacySetSessionModelResponse; +} diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 89d25842..95b9cc0d 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -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"; @@ -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, @@ -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)); } } @@ -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) { @@ -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; @@ -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]; @@ -386,7 +397,7 @@ export class CodexAcpServer implements acp.Agent { return accountResponse.account; } - async loadSession(params: acp.LoadSessionRequest): Promise { + async loadSession(params: acp.LoadSessionRequest): Promise { logger.log("Loading session...", {sessionId: params.sessionId}); const { sessionId, @@ -409,7 +420,7 @@ export class CodexAcpServer implements acp.Agent { }; } - async resumeSession(params: acp.ResumeSessionRequest): Promise { + async resumeSession(params: acp.ResumeSessionRequest): Promise { logger.log("Resuming session...", {sessionId: params.sessionId}); const [sessionId, modelState, modeState] = await this.getOrCreateSession(params); @@ -465,7 +476,7 @@ export class CodexAcpServer implements acp.Agent { return {}; } - async unstable_deleteSession(params: acp.DeleteSessionRequest): Promise { + async deleteSession(params: acp.DeleteSessionRequest): Promise { logger.log("Deleting session...", {sessionId: params.sessionId}); const sessionId = params.sessionId; const shouldCloseLocalSession = this.hasLocalSession(sessionId); @@ -502,7 +513,7 @@ export class CodexAcpServer implements acp.Agent { async newSession( params: acp.NewSessionRequest, - ): Promise { + ): Promise { logger.log("Starting new session..."); const [sessionId, modelState, modeState] = await this.getOrCreateSession(params); @@ -533,7 +544,7 @@ export class CodexAcpServer implements acp.Agent { return { }; } - async unstable_logout(_params: acp.LogoutRequest): Promise { + async logout(_params: acp.LogoutRequest): Promise { logger.log("Logout request received"); await this.runWithProcessCheck(() => this.codexAcpClient.logout()); logger.log("Logout request completed"); @@ -630,7 +641,7 @@ export class CodexAcpServer implements acp.Agent { sessionState.currentModelSupportsFast = modelSupportsFast(model); } - async unstable_setSessionModel(params: acp.SetSessionModelRequest): Promise { + async unstable_setSessionModel(params: LegacySetSessionModelRequest): Promise { logger.log("Set session model requested", { sessionId: params.sessionId, modelId: params.modelId @@ -661,6 +672,18 @@ export class CodexAcpServer implements acp.Agent { return {}; } + private parseLegacySetSessionModelParams(params: Record): 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 { const currentModelId = ModelId.fromString(sessionState.currentModelId); return [ @@ -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) => ({ @@ -722,7 +745,7 @@ export class CodexAcpServer implements acp.Agent { request: acp.LoadSessionRequest ): Promise<{ sessionId: SessionId; - modelState: SessionModelState; + modelState: LegacySessionModelState; modeState: SessionModeState; thread: Thread; }> { @@ -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 { diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 6924d739..c9e346d6 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -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"}); }); @@ -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({}); diff --git a/src/__tests__/CodexACPAgent/e2e/acp-e2e-session-persistence.test.ts b/src/__tests__/CodexACPAgent/e2e/acp-e2e-session-persistence.test.ts index 037e3631..95ada735 100644 --- a/src/__tests__/CodexACPAgent/e2e/acp-e2e-session-persistence.test.ts +++ b/src/__tests__/CodexACPAgent/e2e/acp-e2e-session-persistence.test.ts @@ -1,4 +1,5 @@ import {afterEach, expect, it} from "vitest"; +import {legacySetSessionModel, type LegacyLoadSessionResponse} from "../../../AcpExtensions"; import { createAuthenticatedFixture, describeE2E, @@ -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, @@ -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( diff --git a/src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts b/src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts index 9dc2f179..84b8ee6e 100644 --- a/src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts +++ b/src/__tests__/CodexACPAgent/e2e/acp-e2e.test.ts @@ -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, @@ -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(), }); diff --git a/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts b/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts index 7ec42d85..583f9501 100644 --- a/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts +++ b/src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts @@ -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"); @@ -20,7 +21,7 @@ export interface TestSkill { export interface SpawnedAgentFixture { readonly connection: acp.ClientSideConnection; readonly workspaceDir: string; - createSession(mcpServers?: acp.McpServer[]): Promise; + createSession(mcpServers?: acp.McpServer[]): Promise; restart(): Promise; writeSkill(skill: TestSkill, rootDir?: string): void; setPermissionResponder(responder: PermissionResponder): void; @@ -166,11 +167,11 @@ class SpawnedAgentFixtureImpl implements SpawnedAgentFixture { return this.paths.workspaceDir; } - async createSession(mcpServers: acp.McpServer[] = []): Promise { + async createSession(mcpServers: acp.McpServer[] = []): Promise { return await this.connection.newSession({ cwd: this.workspaceDir, mcpServers, - }); + }) as LegacyNewSessionResponse; } async restart(): Promise { diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index e01c9b31..64f4e125 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -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"}; @@ -43,7 +44,7 @@ async function createSession(currentModelId: string, availableModels: Array { @@ -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]); diff --git a/src/__tests__/CodexACPAgent/session-delete.test.ts b/src/__tests__/CodexACPAgent/session-delete.test.ts index 9b551da2..4f8b5f82 100644 --- a/src/__tests__/CodexACPAgent/session-delete.test.ts +++ b/src/__tests__/CodexACPAgent/session-delete.test.ts @@ -21,7 +21,7 @@ describe("ACP session delete", () => { it("archives sessions that are not active locally", async () => { const fixture = createCodexMockTestFixture(); - await expect(fixture.getCodexAcpAgent().unstable_deleteSession({sessionId})).resolves.toEqual({}); + await expect(fixture.getCodexAcpAgent().deleteSession({sessionId})).resolves.toEqual({}); await expect(fixture.getCodexConnectionDump([])).toMatchFileSnapshot( "data/session-delete-unknown-local.json" @@ -31,7 +31,7 @@ describe("ACP session delete", () => { it("closes local session resources before archiving", async () => { const {fixture, codexAcpAgent} = await createSession(); - await expect(codexAcpAgent.unstable_deleteSession({sessionId})).resolves.toEqual({}); + await expect(codexAcpAgent.deleteSession({sessionId})).resolves.toEqual({}); await expect(fixture.getCodexConnectionDump([])).toMatchFileSnapshot( "data/session-delete-idle.json" @@ -45,7 +45,7 @@ describe("ACP session delete", () => { await codexAcpAgent.closeSession({sessionId}); fixture.clearCodexConnectionDump(); - await expect(codexAcpAgent.unstable_deleteSession({sessionId})).resolves.toEqual({}); + await expect(codexAcpAgent.deleteSession({sessionId})).resolves.toEqual({}); await expect(fixture.getCodexConnectionDump([])).toMatchFileSnapshot( "data/session-delete-unknown-local.json" @@ -56,7 +56,7 @@ describe("ACP session delete", () => { const {fixture, codexAcpAgent} = await createSession(); codexAcpAgent.getSessionState(sessionId).currentTurnId = "turn-id"; - await expect(codexAcpAgent.unstable_deleteSession({sessionId})).resolves.toEqual({}); + await expect(codexAcpAgent.deleteSession({sessionId})).resolves.toEqual({}); await expect(fixture.getCodexConnectionDump([])).toMatchFileSnapshot( "data/session-delete-active-turn.json" @@ -73,7 +73,7 @@ describe("ACP session delete", () => { vi.spyOn(codexAcpClient, "deleteSession").mockReturnValue(archive.promise); const resumeSessionSpy = vi.spyOn(codexAcpClient, "resumeSession"); - const deletePromise = codexAcpAgent.unstable_deleteSession({sessionId}); + const deletePromise = codexAcpAgent.deleteSession({sessionId}); await vi.waitFor(() => { expect(codexAcpClient.deleteSession).toHaveBeenCalledWith(sessionId); }); @@ -95,7 +95,7 @@ describe("ACP session delete", () => { vi.spyOn(codexAcpClient, "deleteSession").mockReturnValue(archive.promise); const loadSessionSpy = vi.spyOn(codexAcpClient, "loadSession"); - const deletePromise = codexAcpAgent.unstable_deleteSession({sessionId}); + const deletePromise = codexAcpAgent.deleteSession({sessionId}); await vi.waitFor(() => { expect(codexAcpClient.deleteSession).toHaveBeenCalledWith(sessionId); });