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 @@ -50,6 +50,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
46 changes: 39 additions & 7 deletions src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ export class CodexAcpClient {
const response = await this.codexClient.threadResume({
config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []),
cwd: request.cwd,
modelProvider: await this.getResumeModelProvider(),
...(await this.resumeModelProviderParams()),
threadId: request.sessionId,
});
onSubscribed?.();
Expand All @@ -403,6 +403,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 All @@ -417,7 +419,7 @@ export class CodexAcpClient {
const response = await this.codexClient.threadResume({
config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []),
cwd: request.cwd,
modelProvider: await this.getResumeModelProvider(),
...(await this.resumeModelProviderParams()),
threadId: request.sessionId,
});
onSubscribed?.();
Expand All @@ -431,6 +433,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 @@ -458,6 +462,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 @@ -611,10 +617,17 @@ export class CodexAcpClient {
return this.gatewayConfig?.modelProvider ?? this.modelProvider;
}

private async getResumeModelProvider(): Promise<string> {
// Prefer an explicit/gateway provider, then the provider persisted in Codex config.
// Keep OpenAI as the final fallback for ChatGPT-authenticated sessions without a configured provider.
return (await this.getCurrentModelProvider()) ?? "openai";
/**
* Resume-time provider override, as `thread/resume` params.
*
* Prefer an explicit/gateway provider, then the provider persisted in Codex config.
* When neither is configured the field is omitted entirely: supplying one makes the
* app-server re-resolve the thread's model and reasoning effort from config, which
* discards the picks stored on the thread itself.
*/
private async resumeModelProviderParams(): Promise<{modelProvider?: string}> {
const modelProvider = await this.getCurrentModelProvider();
return modelProvider ? {modelProvider} : {};
}

private async refreshSkills(
Expand Down Expand Up @@ -755,7 +768,7 @@ export class CodexAcpClient {

async sendPrompt(
request: acp.PromptRequest,
agentMode: AgentMode,
getAgentMode: () => AgentMode,
modelId: ModelId,
serviceTier: ServiceTier | null,
disableSummary: boolean,
Expand All @@ -770,6 +783,7 @@ export class CodexAcpClient {
if (shouldCancel?.()) {
return null;
}
const agentMode = getAgentMode();
return await this.codexClient.runTurn({
threadId: request.sessionId,
input: input,
Expand All @@ -789,6 +803,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 @@ -966,6 +997,7 @@ export type SessionMetadata = {
sessionId: string,
currentModelId: string,
models: Model[],
agentMode?: AgentMode,
collaborationMode: ModeKind,
modelProvider?: string | null,
currentServiceTier?: ServiceTier | null,
Expand Down
43 changes: 30 additions & 13 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,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 @@ -647,6 +647,7 @@ export class CodexAcpServer {
availableModelCount: modelState.availableModels.length
});
return {
sessionId,
models: modelState,
modes: modeState,
...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId)),
Expand Down Expand Up @@ -853,7 +854,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 @@ -878,16 +879,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 @@ -913,12 +914,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 @@ -930,7 +938,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 @@ -942,16 +950,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 @@ -987,6 +1001,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 @@ -1446,7 +1464,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 @@ -2189,7 +2207,6 @@ export class CodexAcpServer {
if (!sessionState.supportedInputModalities.includes("image") && effectiveParams.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 @@ -2198,7 +2215,7 @@ export class CodexAcpServer {
const sendPromptPromise = this.runWithProcessCheck(
() => this.codexAcpClient.sendPrompt(
effectiveParams,
agentMode,
() => sessionState.agentMode,
modelId,
serviceTier,
disableSummary,
Expand Down Expand Up @@ -2288,7 +2305,7 @@ export class CodexAcpServer {
const implementationPromise = this.runWithProcessCheck(
() => this.codexAcpClient.sendPrompt(
implementationRequest,
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,
CancelLoginAccountParams,
CancelLoginAccountResponse,
ConfigReadParams,
Expand Down Expand Up @@ -65,6 +67,7 @@ import type {
TurnStartResponse,
TurnSteerParams,
TurnSteerResponse,
SandboxPolicy,
CommandExecutionRequestApprovalParams,
CommandExecutionRequestApprovalResponse,
FileChangeRequestApprovalParams,
Expand Down Expand Up @@ -534,7 +537,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 @@ -980,9 +983,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
Loading