Skip to content

Commit 303bca9

Browse files
Add plan mode command action
1 parent 75893cd commit 303bca9

12 files changed

Lines changed: 261 additions & 10 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,
@@ -675,6 +680,13 @@ export class CodexAcpClient {
675680
}, onTurnStarted);
676681
}
677682

683+
async setCollaborationMode(sessionId: string, mode: ModeKind, currentModelId: string): Promise<void> {
684+
await this.codexClient.threadSettingsUpdate({
685+
threadId: sessionId,
686+
collaborationMode: createCodexCollaborationMode(mode, currentModelId),
687+
});
688+
}
689+
678690
resolveTurnInterrupted(params: { threadId: string, turnId: string }): void {
679691
this.codexClient.resolveTurnInterrupted(params.threadId, params.turnId);
680692
}

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,
@@ -87,6 +94,7 @@ export interface SessionState {
8794
supportedReasoningEfforts: Array<ReasoningEffortOption>,
8895
supportedInputModalities: Array<InputModality>,
8996
agentMode: AgentMode,
97+
collaborationMode: ModeKind,
9098
currentTurnId: string | null;
9199
lastTokenUsage: TokenCount | null;
92100
totalTokenUsage: TokenCount | null;
@@ -402,6 +410,7 @@ export class CodexAcpServer {
402410
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
403411
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
404412
agentMode: AgentMode.getInitialAgentMode(),
413+
collaborationMode: DEFAULT_COLLABORATION_MODE,
405414
currentTurnId: null,
406415
lastTokenUsage: null,
407416
totalTokenUsage: null,
@@ -685,13 +694,24 @@ export class CodexAcpServer {
685694
const sessionState = this.sessions.get(params.sessionId);
686695
if (!sessionState) throw new Error(`Session ${params.sessionId} not found`);
687696

697+
await this.applySessionConfigOption(sessionState, params);
698+
699+
return {
700+
configOptions: this.createSessionConfigOptions(sessionState),
701+
};
702+
}
703+
704+
private async applySessionConfigOption(sessionState: SessionState, params: acp.SetSessionConfigOptionRequest): Promise<void> {
688705
switch (params.configId) {
689706
case FAST_MODE_CONFIG_ID:
690707
this.applyFastModeChange(sessionState, params);
691708
break;
692709
case MODE_CONFIG_ID:
693710
this.applyModeChange(sessionState, this.stringConfigValue(params));
694711
break;
712+
case COLLABORATION_MODE_CONFIG_ID:
713+
await this.applyCollaborationModeChange(sessionState, this.stringConfigValue(params));
714+
break;
695715
case MODEL_CONFIG_ID:
696716
this.applyModelChange(sessionState, this.stringConfigValue(params));
697717
break;
@@ -701,10 +721,6 @@ export class CodexAcpServer {
701721
default:
702722
throw RequestError.invalidParams();
703723
}
704-
705-
return {
706-
configOptions: this.createSessionConfigOptions(sessionState),
707-
};
708724
}
709725

710726
private applyFastModeChange(sessionState: SessionState, params: acp.SetSessionConfigOptionRequest): void {
@@ -734,6 +750,15 @@ export class CodexAcpServer {
734750
sessionState.agentMode = newMode;
735751
}
736752

753+
private async applyCollaborationModeChange(sessionState: SessionState, value: string): Promise<void> {
754+
const mode = parseCollaborationMode(value);
755+
if (mode === null) {
756+
throw RequestError.invalidParams();
757+
}
758+
await this.codexAcpClient.setCollaborationMode(sessionState.sessionId, mode, sessionState.currentModelId);
759+
sessionState.collaborationMode = mode;
760+
}
761+
737762
private applyModelChange(sessionState: SessionState, value: string): void {
738763
const model = sessionState.availableModels.find(m => m.id === value);
739764
if (!model) {
@@ -812,6 +837,7 @@ export class CodexAcpServer {
812837
const currentModelId = ModelId.fromString(sessionState.currentModelId);
813838
const configOptions = [
814839
sessionState.agentMode.toConfigOption(),
840+
createCollaborationModeConfigOption(sessionState.collaborationMode),
815841
createModelConfigOption(sessionState.availableModels, currentModelId.model),
816842
];
817843
if (sessionState.supportedReasoningEfforts.length > 0) {
@@ -931,6 +957,7 @@ export class CodexAcpServer {
931957
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
932958
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
933959
agentMode: AgentMode.getInitialAgentMode(),
960+
collaborationMode: DEFAULT_COLLABORATION_MODE,
934961
currentTurnId: null,
935962
lastTokenUsage: null,
936963
totalTokenUsage: null,
@@ -1490,6 +1517,18 @@ export class CodexAcpServer {
14901517
sessionState.currentTurnId = turnId;
14911518
pendingTurnStart?.resolve(turnId);
14921519
},
1520+
setConfigOption: async (configId, value) => {
1521+
await this.applySessionConfigOption(sessionState, {
1522+
sessionId: sessionState.sessionId,
1523+
configId,
1524+
value,
1525+
});
1526+
const session = new ACPSessionConnection(this.connection, sessionState.sessionId);
1527+
await session.update({
1528+
sessionUpdate: "config_option_update",
1529+
configOptions: this.createSessionConfigOptions(sessionState),
1530+
});
1531+
},
14931532
});
14941533
void commandPromise.catch((err) => {
14951534
if (this.activePrompts.get(params.sessionId) !== activePrompt) {

src/CodexAppServerClient.ts

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

496+
async threadSettingsUpdate(params: ExperimentalThreadSettingsUpdateParams): Promise<void> {
497+
await this.connection.sendRequest("thread/settings/update", params);
498+
}
499+
496500
async threadList(params: ThreadListParams): Promise<ThreadListResponse> {
497501
return await this.sendRequest({ method: "thread/list", params: params });
498502
}
@@ -927,6 +931,18 @@ type DistributiveOmit<T, K extends keyof any> = T extends any
927931
? Omit<T, K>
928932
: never;
929933

934+
export interface ExperimentalThreadSettingsUpdateParams {
935+
threadId: string;
936+
collaborationMode: {
937+
mode: "default" | "plan";
938+
settings: {
939+
model: string;
940+
reasoning_effort: string | null;
941+
developer_instructions: string | null;
942+
};
943+
};
944+
}
945+
930946
type McpServerStartupSnapshot = {
931947
status: McpServerStartupState;
932948
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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('CodexACPAgent - initialize', () => {
8585
]));
8686
});
8787

88-
it('should not opt into experimental app-server capabilities for ACP elicitation support', async () => {
88+
it('enables experimental thread settings without requesting attestation', async () => {
8989
await agent.initialize({
9090
protocolVersion: acp.PROTOCOL_VERSION,
9191
clientCapabilities: {
@@ -94,7 +94,10 @@ describe('CodexACPAgent - initialize', () => {
9494
});
9595

9696
expect(mockCodexConnection.sendRequest).toHaveBeenCalledWith("initialize", expect.objectContaining({
97-
capabilities: null,
97+
capabilities: {
98+
experimentalApi: true,
99+
requestAttestation: false,
100+
},
98101
}));
99102
});
100103

0 commit comments

Comments
 (0)