Skip to content

Commit 52d637a

Browse files
Add plan and goal command actions (#293)
* Add plan mode command action * Advertise goal composer action * Expose goal state and controls * Harden plan and goal session state Restore collaboration mode when sessions are loaded and publish authoritative goal snapshots without allowing stale reads to overwrite live updates. Keep goal control responses typed and preserve stable goal identity for UI replay. * Namespace goal control ACP extension
1 parent 616c085 commit 52d637a

27 files changed

Lines changed: 797 additions & 56 deletions

src/AcpExtensions.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
} from "@agentclientprotocol/sdk";
88

99
export const LEGACY_SET_SESSION_MODEL_METHOD = "session/set_model";
10+
export const GOAL_CONTROL_METHOD = "_codex/session/goal_control";
1011

1112
export type LegacySessionModel = {
1213
modelId: string;
@@ -42,11 +43,13 @@ export type ExtMethodRequest =
4243
AuthenticationStatusRequest
4344
| AuthenticationLogoutRequest
4445
| LegacySetSessionModelExtRequest
46+
| GoalControlExtRequest
4547

4648
export function isExtMethodRequest(request: { method: string, params: Record<string, unknown> }): request is ExtMethodRequest {
4749
return request.method === "authentication/status"
4850
|| request.method === "authentication/logout"
49-
|| request.method === LEGACY_SET_SESSION_MODEL_METHOD;
51+
|| request.method === LEGACY_SET_SESSION_MODEL_METHOD
52+
|| request.method === GOAL_CONTROL_METHOD;
5053
}
5154

5255
export type AuthenticationStatusRequest = { method: "authentication/status", params: {} }
@@ -60,6 +63,16 @@ export type LegacySetSessionModelExtRequest = {
6063
params: LegacySetSessionModelRequest;
6164
}
6265

66+
export type GoalControlRequest = {
67+
sessionId: SessionId;
68+
action: "pause" | "clear";
69+
}
70+
71+
export type GoalControlExtRequest = {
72+
method: typeof GOAL_CONTROL_METHOD;
73+
params: GoalControlRequest;
74+
}
75+
6376
export async function legacySetSessionModel(
6477
connection: Pick<ClientContext, "request">,
6578
params: LegacySetSessionModelRequest,

src/CodexAcpClient.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ import type {
3333
SkillsListResponse,
3434
SandboxPolicy,
3535
Thread,
36+
ThreadGoal,
3637
ThreadGoalStatus,
3738
ThreadSourceKind,
3839
TurnCompletedNotification,
3940
UserInput,
4041
} from "./app-server/v2";
4142
import packageJson from "../package.json";
4243
import type {AuthenticationStatusResponse} from "./AcpExtensions";
44+
import {createCodexCollaborationMode} from "./CollaborationModeConfig";
45+
import type {ModeKind} from "./app-server/ModeKind";
4346

4447
/**
4548
* Well-known provider id for the client-configurable custom LLM gateway.
@@ -84,7 +87,10 @@ export class CodexAcpClient {
8487

8588
async initialize(request: acp.InitializeRequest): Promise<void> {
8689
await this.codexClient.initialize({
87-
capabilities: null,
90+
capabilities: {
91+
experimentalApi: true,
92+
requestAttestation: false,
93+
},
8894
clientInfo: {
8995
name: request.clientInfo?.name ?? this.defaultClientInfo.name,
9096
version: request.clientInfo?.version ?? this.defaultClientInfo.version,
@@ -330,6 +336,7 @@ export class CodexAcpClient {
330336
sessionId: request.sessionId,
331337
currentModelId: currentModelId,
332338
models: codexModels,
339+
collaborationMode: this.getCollaborationMode(response.thread.id),
333340
modelProvider: response.modelProvider,
334341
currentServiceTier: response.serviceTier as ServiceTier ?? null,
335342
additionalDirectories,
@@ -357,6 +364,7 @@ export class CodexAcpClient {
357364
sessionId: request.sessionId,
358365
currentModelId: currentModelId,
359366
models: codexModels,
367+
collaborationMode: this.getCollaborationMode(response.thread.id),
360368
modelProvider: response.modelProvider,
361369
currentServiceTier: response.serviceTier as ServiceTier ?? null,
362370
thread: historyResponse.thread,
@@ -383,6 +391,7 @@ export class CodexAcpClient {
383391
sessionId: response.thread.id,
384392
currentModelId: currentModelId,
385393
models: codexModels,
394+
collaborationMode: this.getCollaborationMode(response.thread.id),
386395
modelProvider: response.modelProvider,
387396
currentServiceTier: response.serviceTier as ServiceTier ?? null,
388397
additionalDirectories,
@@ -417,6 +426,11 @@ export class CodexAcpClient {
417426
await this.codexClient.runCompact({threadId: sessionId});
418427
}
419428

429+
async getGoal(sessionId: string): Promise<ThreadGoal | null> {
430+
const response = await this.codexClient.threadGoalGet({threadId: sessionId});
431+
return response?.goal ?? null;
432+
}
433+
420434
async setGoal(
421435
sessionId: string,
422436
objective: string,
@@ -429,11 +443,18 @@ export class CodexAcpClient {
429443
}, onTurnStarted);
430444
}
431445

432-
async setGoalStatus(sessionId: string, status: ThreadGoalStatus): Promise<void> {
446+
async setGoalStatus(sessionId: string, status: ThreadGoalStatus): Promise<ThreadGoal> {
447+
let updatedGoal: ThreadGoal | null = null;
433448
await this.codexClient.runGoalSet({
434449
threadId: sessionId,
435450
status,
451+
}, undefined, undefined, (goal) => {
452+
updatedGoal = goal;
436453
});
454+
if (updatedGoal === null) {
455+
throw new Error(`Goal update for session ${sessionId} returned no goal`);
456+
}
457+
return updatedGoal;
437458
}
438459

439460
async resumeGoal(
@@ -679,6 +700,17 @@ export class CodexAcpClient {
679700
}, onTurnStarted);
680701
}
681702

703+
async setCollaborationMode(sessionId: string, mode: ModeKind, currentModelId: string): Promise<void> {
704+
await this.codexClient.threadSettingsUpdate({
705+
threadId: sessionId,
706+
collaborationMode: createCodexCollaborationMode(mode, currentModelId),
707+
});
708+
}
709+
710+
private getCollaborationMode(sessionId: string): ModeKind {
711+
return this.codexClient.getThreadSettings(sessionId)?.collaborationMode.mode ?? "default";
712+
}
713+
682714
resolveTurnInterrupted(params: { threadId: string, turnId: string }): void {
683715
this.codexClient.resolveTurnInterrupted(params.threadId, params.turnId);
684716
}
@@ -845,6 +877,7 @@ export type SessionMetadata = {
845877
sessionId: string,
846878
currentModelId: string,
847879
models: Model[],
880+
collaborationMode: ModeKind,
848881
modelProvider?: string | null,
849882
currentServiceTier?: ServiceTier | null,
850883
additionalDirectories: string[],

0 commit comments

Comments
 (0)