Skip to content
Open
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
1 change: 1 addition & 0 deletions src/AcpExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type LegacyNewSessionResponse = NewSessionResponse & {
}

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

Expand Down
12 changes: 12 additions & 0 deletions src/AgentMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ export class AgentMode {
return match ?? null;
}

static fromSettings(
approvalPolicy: AskForApproval | undefined,
sandboxPolicy: SandboxPolicy | undefined,
): AgentMode | null {
if (!sandboxPolicy) return null;
const match = AgentMode.all().find(mode =>
mode.approvalPolicy === approvalPolicy
&& mode.sandboxPolicy.type === sandboxPolicy.type
);
return match ?? null;
}

static getInitialAgentMode(): AgentMode {
const predefinedAgentMode = process.env["INITIAL_AGENT_MODE"];
if (predefinedAgentMode) {
Expand Down
27 changes: 26 additions & 1 deletion src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ export class CodexAcpClient {
sessionId: request.sessionId,
currentModelId: currentModelId,
models: codexModels,
agentMode: AgentMode.fromSettings(response.approvalPolicy, response.sandbox)
?? AgentMode.getInitialAgentMode(),
collaborationMode: this.getCollaborationMode(response.thread.id),
modelProvider: response.modelProvider,
currentServiceTier: response.serviceTier as ServiceTier ?? null,
Expand Down Expand Up @@ -371,6 +373,8 @@ export class CodexAcpClient {
sessionId: request.sessionId,
currentModelId: currentModelId,
models: codexModels,
agentMode: AgentMode.fromSettings(response.approvalPolicy, response.sandbox)
?? AgentMode.getInitialAgentMode(),
collaborationMode: this.getCollaborationMode(response.thread.id),
modelProvider: response.modelProvider,
currentServiceTier: response.serviceTier as ServiceTier ?? null,
Expand Down Expand Up @@ -398,6 +402,8 @@ export class CodexAcpClient {
sessionId: response.thread.id,
currentModelId: currentModelId,
models: codexModels,
agentMode: AgentMode.fromSettings(response.approvalPolicy, response.sandbox)
?? AgentMode.getInitialAgentMode(),
collaborationMode: this.getCollaborationMode(response.thread.id),
modelProvider: response.modelProvider,
currentServiceTier: response.serviceTier as ServiceTier ?? null,
Expand Down Expand Up @@ -685,7 +691,7 @@ export class CodexAcpClient {

async sendPrompt(
request: acp.PromptRequest,
agentMode: AgentMode,
getAgentMode: () => AgentMode,
modelId: ModelId,
serviceTier: ServiceTier | null,
disableSummary: boolean,
Expand All @@ -700,6 +706,7 @@ export class CodexAcpClient {
if (shouldCancel?.()) {
return null;
}
const agentMode = getAgentMode();
return await this.codexClient.runTurn({
threadId: request.sessionId,
input: input,
Expand All @@ -719,6 +726,23 @@ export class CodexAcpClient {
});
}

async setAgentMode(sessionId: string, mode: AgentMode): Promise<void> {
await this.codexClient.threadSettingsUpdate({
threadId: sessionId,
approvalPolicy: mode.approvalPolicy,
sandboxPolicy: mode.sandboxPolicy,
});
}

async setModelAndEffort(sessionId: string, currentModelId: string): Promise<void> {
const modelId = ModelId.fromString(currentModelId);
await this.codexClient.threadSettingsUpdate({
threadId: sessionId,
model: modelId.model,
effort: modelId.effort as ReasoningEffort,
});
}

private getCollaborationMode(sessionId: string): ModeKind {
return this.codexClient.getThreadSettings(sessionId)?.collaborationMode.mode ?? "default";
}
Expand Down Expand Up @@ -897,6 +921,7 @@ export type SessionMetadata = {
sessionId: string,
currentModelId: string,
models: Model[],
agentMode?: AgentMode,
collaborationMode: ModeKind,
modelProvider?: string | null,
currentServiceTier?: ServiceTier | null,
Expand Down
41 changes: 29 additions & 12 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ export class CodexAcpServer {
availableModels: models,
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
agentMode: AgentMode.getInitialAgentMode(),
agentMode: sessionMetadata.agentMode ?? AgentMode.getInitialAgentMode(),
collaborationMode: sessionMetadata.collaborationMode,
currentTurnId: null,
lastTokenUsage: null,
Expand Down Expand Up @@ -538,6 +538,7 @@ export class CodexAcpServer {
availableModelCount: modelState.availableModels.length
});
return {
sessionId,
models: modelState,
modes: modeState,
...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId)),
Expand Down Expand Up @@ -728,7 +729,7 @@ export class CodexAcpServer {
const sessionState = this.sessions.get(_params.sessionId);
if (!sessionState) throw new Error(`Session ${_params.sessionId} not found`);

this.applyModeChange(sessionState, _params.modeId);
await this.applyModeChange(sessionState, _params.modeId);
return {};
}

Expand All @@ -753,16 +754,16 @@ export class CodexAcpServer {
this.applyFastModeChange(sessionState, params);
break;
case MODE_CONFIG_ID:
this.applyModeChange(sessionState, this.stringConfigValue(params));
await this.applyModeChange(sessionState, this.stringConfigValue(params));
break;
case COLLABORATION_MODE_CONFIG_ID:
await this.applyCollaborationModeChange(sessionState, this.stringConfigValue(params));
break;
case MODEL_CONFIG_ID:
this.applyModelChange(sessionState, this.stringConfigValue(params));
await this.applyModelChange(sessionState, this.stringConfigValue(params));
break;
case REASONING_EFFORT_CONFIG_ID:
this.applyReasoningEffortChange(sessionState, this.stringConfigValue(params));
await this.applyReasoningEffortChange(sessionState, this.stringConfigValue(params));
break;
default:
throw RequestError.invalidParams();
Expand All @@ -788,12 +789,19 @@ export class CodexAcpServer {
return params.value;
}

private applyModeChange(sessionState: SessionState, value: string): void {
private async applyModeChange(sessionState: SessionState, value: string): Promise<void> {
const newMode = AgentMode.find(value);
if (!newMode) {
throw RequestError.invalidParams();
}
const previousMode = sessionState.agentMode;
sessionState.agentMode = newMode;
try {
await this.codexAcpClient.setAgentMode(sessionState.sessionId, newMode);
} catch (error) {
sessionState.agentMode = previousMode;
throw error;
}
}

private async applyCollaborationModeChange(sessionState: SessionState, value: string): Promise<void> {
Expand All @@ -805,7 +813,7 @@ export class CodexAcpServer {
sessionState.collaborationMode = mode;
}

private applyModelChange(sessionState: SessionState, value: string): void {
private async applyModelChange(sessionState: SessionState, value: string): Promise<void> {
const model = sessionState.availableModels.find(m => m.id === value);
if (!model) {
const currentModel = ModelId.fromString(sessionState.currentModelId).model;
Expand All @@ -817,16 +825,22 @@ export class CodexAcpServer {
const currentEffort = ModelId.fromString(sessionState.currentModelId).effort;
const effort = findSupportedEffort(model.supportedReasoningEfforts, currentEffort)
?? model.defaultReasoningEffort;
await this.codexAcpClient.setModelAndEffort(
sessionState.sessionId,
ModelId.fromComponents(model, effort).toString(),
);
this.applyModelAndEffort(sessionState, model, effort);
}

private applyReasoningEffortChange(sessionState: SessionState, value: string): void {
private async applyReasoningEffortChange(sessionState: SessionState, value: string): Promise<void> {
const effort = findSupportedEffort(sessionState.supportedReasoningEfforts, value);
if (!effort) {
throw RequestError.invalidParams();
}
const {model} = ModelId.fromString(sessionState.currentModelId);
sessionState.currentModelId = ModelId.create(model, effort).toString();
const currentModelId = ModelId.create(model, effort).toString();
await this.codexAcpClient.setModelAndEffort(sessionState.sessionId, currentModelId);
sessionState.currentModelId = currentModelId;
}

private applyModelAndEffort(sessionState: SessionState, model: Model, effort: ReasoningEffort): void {
Expand Down Expand Up @@ -862,6 +876,10 @@ export class CodexAcpServer {
}

sessionState.availableModels = models;
await this.codexAcpClient.setModelAndEffort(
sessionState.sessionId,
ModelId.fromComponents(model, reasoningEffort).toString(),
);
this.applyModelAndEffort(sessionState, model, reasoningEffort);

return {};
Expand Down Expand Up @@ -1290,7 +1308,7 @@ export class CodexAcpServer {
availableModels: models,
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
agentMode: AgentMode.getInitialAgentMode(),
agentMode: sessionMetadata.agentMode ?? AgentMode.getInitialAgentMode(),
collaborationMode: sessionMetadata.collaborationMode,
currentTurnId: null,
lastTokenUsage: null,
Expand Down Expand Up @@ -1975,7 +1993,6 @@ export class CodexAcpServer {
if (!sessionState.supportedInputModalities.includes("image") && params.prompt.some(b => b.type === "image")) {
throw RequestError.invalidRequest("The current model does not support image input");
}
const agentMode = sessionState.agentMode;
const serviceTier = resolveFastServiceTier(
sessionState.fastModeEnabled,
sessionState.currentModelSupportsFast,
Expand All @@ -1984,7 +2001,7 @@ export class CodexAcpServer {
const sendPromptPromise = this.runWithProcessCheck(
() => this.codexAcpClient.sendPrompt(
params,
agentMode,
() => sessionState.agentMode,
modelId,
serviceTier,
disableSummary,
Expand Down
13 changes: 10 additions & 3 deletions src/CodexAppServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type {
ClientRequest,
InitializeParams,
InitializeResponse,
ReasoningEffort,
ServerNotification
} from "./app-server";
import type {
AskForApproval,
ConfigReadParams,
ConfigReadResponse,
GetAccountParams,
Expand Down Expand Up @@ -63,6 +65,7 @@ import type {
TurnStartResponse,
TurnSteerParams,
TurnSteerResponse,
SandboxPolicy,
CommandExecutionRequestApprovalParams,
CommandExecutionRequestApprovalResponse,
FileChangeRequestApprovalParams,
Expand Down Expand Up @@ -532,7 +535,7 @@ export class CodexAppServerClient {
return this.threadSettings.get(threadId);
}

async threadSettingsUpdate(params: ExperimentalThreadSettingsUpdateParams): Promise<void> {
async threadSettingsUpdate(params: ThreadSettingsUpdateParams): Promise<void> {
await this.connection.sendRequest("thread/settings/update", params);
}

Expand Down Expand Up @@ -974,9 +977,13 @@ type DistributiveOmit<T, K extends keyof any> = T extends any
? Omit<T, K>
: never;

export interface ExperimentalThreadSettingsUpdateParams {
export interface ThreadSettingsUpdateParams {
threadId: string;
collaborationMode: {
approvalPolicy?: AskForApproval;
sandboxPolicy?: SandboxPolicy;
model?: string;
effort?: ReasoningEffort;
collaborationMode?: {
mode: "default" | "plan";
settings: {
model: string;
Expand Down
8 changes: 7 additions & 1 deletion src/__tests__/CodexACPAgent/CodexAcpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ describe('ACP server test', { timeout: 40_000 }, () => {
});
});

it('restores collaboration mode for resumed and loaded sessions', async () => {
it('restores collaboration and agent modes for resumed and loaded sessions', async () => {
const mockFixture = createCodexMockTestFixture();
const codexAcpAgent = mockFixture.getCodexAcpAgent();
const codexAcpClient = mockFixture.getCodexAcpClient();
Expand Down Expand Up @@ -483,6 +483,8 @@ describe('ACP server test', { timeout: 40_000 }, () => {
modelProvider: "openai",
reasoningEffort: "medium",
serviceTier: null,
approvalPolicy: "never",
sandbox: {type: "dangerFullAccess"},
} as any;
});
vi.spyOn(codexAppServerClient, "threadRead").mockImplementation(async ({threadId}) => ({
Expand All @@ -505,8 +507,12 @@ describe('ACP server test', { timeout: 40_000 }, () => {

expect(codexAcpAgent.getSessionState("resume-id").collaborationMode).toBe("plan");
expect(codexAcpAgent.getSessionState("load-id").collaborationMode).toBe("plan");
expect(codexAcpAgent.getSessionState("resume-id").agentMode).toBe(AgentMode.AgentFullAccess);
expect(codexAcpAgent.getSessionState("load-id").agentMode).toBe(AgentMode.AgentFullAccess);
expect(resumed.configOptions?.find(option => option.id === "collaboration_mode")).toMatchObject({currentValue: "plan"});
expect(loaded.configOptions?.find(option => option.id === "collaboration_mode")).toMatchObject({currentValue: "plan"});
expect(resumed.configOptions?.find(option => option.id === "mode")).toMatchObject({currentValue: "agent-full-access"});
expect(loaded.configOptions?.find(option => option.id === "mode")).toMatchObject({currentValue: "agent-full-access"});
});

it('uses configured model provider when resuming sessions without an explicit provider', async () => {
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/CodexACPAgent/fast-mode-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe("Fast mode session config", () => {
currentServiceTier,
additionalDirectories: [],
});
vi.spyOn((codexAcpClient as any).codexClient, "threadSettingsUpdate").mockResolvedValue(undefined);

await codexAcpAgent.initialize({
protocolVersion: acp.PROTOCOL_VERSION,
Expand Down
3 changes: 2 additions & 1 deletion src/__tests__/CodexACPAgent/load-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,9 @@ describe("CodexACPAgent - loadSession", () => {
cwd: "/test/project",
mcpServers: [],
};
await codexAcpAgent.loadSession(loadParams);
const response = await codexAcpAgent.loadSession(loadParams);

expect(response.sessionId).toBe(thread.id);
expect(codexAppServerClient.threadRead).toHaveBeenCalledWith({
threadId: thread.id,
includeTurns: true,
Expand Down
Loading