Skip to content

Commit 380035a

Browse files
Add plan mode command action
1 parent f3f1b3c commit 380035a

12 files changed

Lines changed: 272 additions & 8 deletions

src/CodexAcpClient.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import type {
4040
} from "./app-server/v2";
4141
import packageJson from "../package.json";
4242
import type {AuthenticationStatusResponse} from "./AcpExtensions";
43+
import {createCodexCollaborationMode} from "./CollaborationModeConfig";
44+
import type {ModeKind} from "./app-server/ModeKind";
4345

4446
/**
4547
* Well-known provider id for the client-configurable custom LLM gateway.
@@ -84,7 +86,10 @@ export class CodexAcpClient {
8486

8587
async initialize(request: acp.InitializeRequest): Promise<void> {
8688
await this.codexClient.initialize({
87-
capabilities: null,
89+
capabilities: {
90+
experimentalApi: true,
91+
requestAttestation: false,
92+
},
8893
clientInfo: {
8994
name: request.clientInfo?.name ?? this.defaultClientInfo.name,
9095
version: request.clientInfo?.version ?? this.defaultClientInfo.version,
@@ -679,6 +684,13 @@ export class CodexAcpClient {
679684
}, onTurnStarted);
680685
}
681686

687+
async setCollaborationMode(sessionId: string, mode: ModeKind, currentModelId: string): Promise<void> {
688+
await this.codexClient.threadSettingsUpdate({
689+
threadId: sessionId,
690+
collaborationMode: createCodexCollaborationMode(mode, currentModelId),
691+
});
692+
}
693+
682694
resolveTurnInterrupted(params: { threadId: string, turnId: string }): void {
683695
this.codexClient.resolveTurnInterrupted(params.threadId, params.turnId);
684696
}

src/CodexAcpServer.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ import type {
2020
import type {RateLimitsMap} from "./RateLimitsMap";
2121
import {ModelId} from "./ModelId";
2222
import {AgentMode, MODE_CONFIG_ID} from "./AgentMode";
23+
import {
24+
COLLABORATION_MODE_CONFIG_ID,
25+
createCollaborationModeConfigOption,
26+
DEFAULT_COLLABORATION_MODE,
27+
parseCollaborationMode,
28+
} from "./CollaborationModeConfig";
29+
import type {ModeKind} from "./app-server/ModeKind";
2330
import {
2431
createModelConfigOption,
2532
createReasoningEffortConfigOption,
@@ -88,6 +95,7 @@ export interface SessionState {
8895
supportedReasoningEfforts: Array<ReasoningEffortOption>,
8996
supportedInputModalities: Array<InputModality>,
9097
agentMode: AgentMode,
98+
collaborationMode: ModeKind,
9199
currentTurnId: string | null;
92100
lastTokenUsage: TokenCount | null;
93101
totalTokenUsage: TokenCount | null;
@@ -405,6 +413,7 @@ export class CodexAcpServer {
405413
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
406414
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
407415
agentMode: AgentMode.getInitialAgentMode(),
416+
collaborationMode: DEFAULT_COLLABORATION_MODE,
408417
currentTurnId: null,
409418
lastTokenUsage: null,
410419
totalTokenUsage: null,
@@ -690,13 +699,24 @@ export class CodexAcpServer {
690699
const sessionState = this.sessions.get(params.sessionId);
691700
if (!sessionState) throw new Error(`Session ${params.sessionId} not found`);
692701

702+
await this.applySessionConfigOption(sessionState, params);
703+
704+
return {
705+
configOptions: this.createSessionConfigOptions(sessionState),
706+
};
707+
}
708+
709+
private async applySessionConfigOption(sessionState: SessionState, params: acp.SetSessionConfigOptionRequest): Promise<void> {
693710
switch (params.configId) {
694711
case FAST_MODE_CONFIG_ID:
695712
this.applyFastModeChange(sessionState, params);
696713
break;
697714
case MODE_CONFIG_ID:
698715
this.applyModeChange(sessionState, this.stringConfigValue(params));
699716
break;
717+
case COLLABORATION_MODE_CONFIG_ID:
718+
await this.applyCollaborationModeChange(sessionState, this.stringConfigValue(params));
719+
break;
700720
case MODEL_CONFIG_ID:
701721
this.applyModelChange(sessionState, this.stringConfigValue(params));
702722
break;
@@ -706,10 +726,6 @@ export class CodexAcpServer {
706726
default:
707727
throw RequestError.invalidParams();
708728
}
709-
710-
return {
711-
configOptions: this.createSessionConfigOptions(sessionState),
712-
};
713729
}
714730

715731
private applyFastModeChange(sessionState: SessionState, params: acp.SetSessionConfigOptionRequest): void {
@@ -739,6 +755,15 @@ export class CodexAcpServer {
739755
sessionState.agentMode = newMode;
740756
}
741757

758+
private async applyCollaborationModeChange(sessionState: SessionState, value: string): Promise<void> {
759+
const mode = parseCollaborationMode(value);
760+
if (mode === null) {
761+
throw RequestError.invalidParams();
762+
}
763+
await this.codexAcpClient.setCollaborationMode(sessionState.sessionId, mode, sessionState.currentModelId);
764+
sessionState.collaborationMode = mode;
765+
}
766+
742767
private applyModelChange(sessionState: SessionState, value: string): void {
743768
const model = sessionState.availableModels.find(m => m.id === value);
744769
if (!model) {
@@ -817,6 +842,7 @@ export class CodexAcpServer {
817842
const currentModelId = ModelId.fromString(sessionState.currentModelId);
818843
const configOptions = [
819844
sessionState.agentMode.toConfigOption(),
845+
createCollaborationModeConfigOption(sessionState.collaborationMode),
820846
createModelConfigOption(sessionState.availableModels, currentModelId.model),
821847
];
822848
if (sessionState.supportedReasoningEfforts.length > 0) {
@@ -936,6 +962,7 @@ export class CodexAcpServer {
936962
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
937963
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
938964
agentMode: AgentMode.getInitialAgentMode(),
965+
collaborationMode: DEFAULT_COLLABORATION_MODE,
939966
currentTurnId: null,
940967
lastTokenUsage: null,
941968
totalTokenUsage: null,
@@ -1549,6 +1576,18 @@ export class CodexAcpServer {
15491576
sessionState.currentTurnId = turnId;
15501577
pendingTurnStart?.resolve(turnId);
15511578
},
1579+
setConfigOption: async (configId, value) => {
1580+
await this.applySessionConfigOption(sessionState, {
1581+
sessionId: sessionState.sessionId,
1582+
configId,
1583+
value,
1584+
});
1585+
const session = new ACPSessionConnection(this.connection, sessionState.sessionId);
1586+
await session.update({
1587+
sessionUpdate: "config_option_update",
1588+
configOptions: this.createSessionConfigOptions(sessionState),
1589+
});
1590+
},
15521591
});
15531592
void commandPromise.catch((err) => {
15541593
if (this.activePrompts.get(params.sessionId) !== activePrompt) {

src/CodexAppServerClient.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,10 @@ export class CodexAppServerClient {
513513
return await this.sendRequest({ method: "thread/resume", params: params });
514514
}
515515

516+
async threadSettingsUpdate(params: ExperimentalThreadSettingsUpdateParams): Promise<void> {
517+
await this.connection.sendRequest("thread/settings/update", params);
518+
}
519+
516520
async threadList(params: ThreadListParams): Promise<ThreadListResponse> {
517521
return await this.sendRequest({ method: "thread/list", params: params });
518522
}
@@ -947,6 +951,18 @@ type DistributiveOmit<T, K extends keyof any> = T extends any
947951
? Omit<T, K>
948952
: never;
949953

954+
export interface ExperimentalThreadSettingsUpdateParams {
955+
threadId: string;
956+
collaborationMode: {
957+
mode: "default" | "plan";
958+
settings: {
959+
model: string;
960+
reasoning_effort: string | null;
961+
developer_instructions: string | null;
962+
};
963+
};
964+
}
965+
950966
type McpServerStartupSnapshot = {
951967
status: McpServerStartupState;
952968
error: string | null;

src/CodexCommands.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import type {RateLimitsMap} from "./RateLimitsMap";
88
import type {TokenCount} from "./TokenCount";
99
import {logger} from "./Logger";
1010
import {createAgentTextMessageChunk} from "./ContentChunks";
11+
import {
12+
COLLABORATION_MODE_CONFIG_ID,
13+
DEFAULT_COLLABORATION_MODE,
14+
PLAN_COLLABORATION_MODE,
15+
} from "./CollaborationModeConfig";
1116

1217
type ParsedSlashCommand = {
1318
name: string;
@@ -21,6 +26,7 @@ export type CommandHandleResult =
2126
export type CommandHandleOptions = {
2227
onTurnStartPending?: () => void;
2328
onTurnStarted?: (turnId: string, threadId: string) => void;
29+
setConfigOption?: (configId: string, value: string) => Promise<void>;
2430
};
2531

2632
export type LogoutHandler = () => void | Promise<void>;
@@ -94,6 +100,20 @@ export class CodexCommands {
94100
*/
95101
private getBuiltinCommands(): AvailableCommand[] {
96102
return [
103+
{
104+
name: "plan",
105+
description: "Turn plan mode on.",
106+
input: null,
107+
_meta: {
108+
commandAction: {
109+
kind: "setConfigOption",
110+
configId: COLLABORATION_MODE_CONFIG_ID,
111+
value: PLAN_COLLABORATION_MODE,
112+
resetValue: DEFAULT_COLLABORATION_MODE,
113+
presentation: "state",
114+
},
115+
},
116+
},
97117
{
98118
name: "mcp",
99119
description: "List configured Model Context Protocol (MCP) tools.",
@@ -173,6 +193,14 @@ export class CodexCommands {
173193

174194
const sessionId = sessionState.sessionId;
175195
switch (commandName) {
196+
case "plan": {
197+
if (command.rest.length > 0) {
198+
await this.sendCommandUsageMessage(commandName, "no arguments", sessionId);
199+
return { handled: true };
200+
}
201+
await options.setConfigOption?.(COLLABORATION_MODE_CONFIG_ID, PLAN_COLLABORATION_MODE);
202+
return { handled: options.setConfigOption !== undefined };
203+
}
176204
case "compact": {
177205
await this.runWithProcessCheck(() => this.codexAcpClient.runCompact(sessionId));
178206
return { handled: true };

src/CollaborationModeConfig.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type * as acp from "@agentclientprotocol/sdk";
2+
import type {ReasoningEffort} from "./app-server";
3+
import type {ModeKind} from "./app-server/ModeKind";
4+
import {ModelId} from "./ModelId";
5+
6+
export const COLLABORATION_MODE_CONFIG_ID = "collaboration_mode";
7+
export const DEFAULT_COLLABORATION_MODE: ModeKind = "default";
8+
export const PLAN_COLLABORATION_MODE: ModeKind = "plan";
9+
10+
export function createCollaborationModeConfigOption(currentValue: ModeKind): acp.SessionConfigOption {
11+
return {
12+
id: COLLABORATION_MODE_CONFIG_ID,
13+
name: "Collaboration mode",
14+
description: "How Codex collaborates for subsequent turns",
15+
category: "collaboration_mode",
16+
type: "select",
17+
currentValue,
18+
options: [
19+
{value: DEFAULT_COLLABORATION_MODE, name: "Default"},
20+
{value: PLAN_COLLABORATION_MODE, name: "Plan", description: "Plan before making changes"},
21+
],
22+
};
23+
}
24+
25+
export function parseCollaborationMode(value: unknown): ModeKind | null {
26+
if (value === DEFAULT_COLLABORATION_MODE) return DEFAULT_COLLABORATION_MODE;
27+
if (value === PLAN_COLLABORATION_MODE) return PLAN_COLLABORATION_MODE;
28+
return null;
29+
}
30+
31+
export function createCodexCollaborationMode(mode: ModeKind, currentModelId: string) {
32+
const modelId = ModelId.fromString(currentModelId);
33+
return {
34+
mode,
35+
settings: {
36+
model: modelId.model,
37+
reasoning_effort: modelId.effort as ReasoningEffort | null,
38+
developer_instructions: null,
39+
},
40+
};
41+
}

src/__tests__/CodexACPAgent/data/available-commands-build-in.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
"update": {
77
"sessionUpdate": "available_commands_update",
88
"availableCommands": [
9+
{
10+
"name": "plan",
11+
"description": "Turn plan mode on.",
12+
"input": null,
13+
"_meta": {
14+
"commandAction": {
15+
"kind": "setConfigOption",
16+
"configId": "collaboration_mode",
17+
"value": "plan",
18+
"resetValue": "default",
19+
"presentation": "state"
20+
}
21+
}
22+
},
923
{
1024
"name": "mcp",
1125
"description": "List configured Model Context Protocol (MCP) tools.",

src/__tests__/CodexACPAgent/data/available-commands-skills.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
"update": {
77
"sessionUpdate": "available_commands_update",
88
"availableCommands": [
9+
{
10+
"name": "plan",
11+
"description": "Turn plan mode on.",
12+
"input": null,
13+
"_meta": {
14+
"commandAction": {
15+
"kind": "setConfigOption",
16+
"configId": "collaboration_mode",
17+
"value": "plan",
18+
"resetValue": "default",
19+
"presentation": "state"
20+
}
21+
}
22+
},
923
{
1024
"name": "mcp",
1125
"description": "List configured Model Context Protocol (MCP) tools.",

src/__tests__/CodexACPAgent/data/load-session-history.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
"update": {
77
"sessionUpdate": "available_commands_update",
88
"availableCommands": [
9+
{
10+
"name": "plan",
11+
"description": "Turn plan mode on.",
12+
"input": null,
13+
"_meta": {
14+
"commandAction": {
15+
"kind": "setConfigOption",
16+
"configId": "collaboration_mode",
17+
"value": "plan",
18+
"resetValue": "default",
19+
"presentation": "state"
20+
}
21+
}
22+
},
923
{
1024
"name": "mcp",
1125
"description": "List configured Model Context Protocol (MCP) tools.",

src/__tests__/CodexACPAgent/data/load-session-response-item-history-fallback.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
"update": {
77
"sessionUpdate": "available_commands_update",
88
"availableCommands": [
9+
{
10+
"name": "plan",
11+
"description": "Turn plan mode on.",
12+
"input": null,
13+
"_meta": {
14+
"commandAction": {
15+
"kind": "setConfigOption",
16+
"configId": "collaboration_mode",
17+
"value": "plan",
18+
"resetValue": "default",
19+
"presentation": "state"
20+
}
21+
}
22+
},
923
{
1024
"name": "mcp",
1125
"description": "List configured Model Context Protocol (MCP) tools.",

src/__tests__/CodexACPAgent/initialize.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ describe('CodexACPAgent - initialize', () => {
8585
]));
8686
});
8787

88+
it('enables experimental thread settings without requesting attestation', async () => {
89+
await agent.initialize({
90+
protocolVersion: acp.PROTOCOL_VERSION,
91+
clientCapabilities: {
92+
elicitation: { form: {}, url: {} },
93+
},
94+
});
95+
96+
expect(mockCodexConnection.sendRequest).toHaveBeenCalledWith("initialize", expect.objectContaining({
97+
capabilities: {
98+
experimentalApi: true,
99+
requestAttestation: false,
100+
},
101+
}));
102+
});
103+
88104
it('should advertise API key auth with the legacy metadata method', () => {
89105
expect(getCodexAuthMethods()).toEqual(expect.arrayContaining([
90106
expect.objectContaining({

0 commit comments

Comments
 (0)