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
4 changes: 4 additions & 0 deletions src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ export class CodexAcpClient {
});
}

async compactSession(sessionId: string): Promise<TurnCompletedNotification> {
return await this.codexClient.runCompact({ threadId: sessionId });
}

async listSkills(params?: SkillsListParams): Promise<SkillsListResponse> {
return this.codexClient.listSkills(params ?? {});
}
Expand Down
74 changes: 47 additions & 27 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
ReasoningEffortOption,
Thread,
ThreadItem,
TurnCompletedNotification,
UserInput
} from "./app-server/v2";
import type {RateLimitsMap} from "./RateLimitsMap";
Expand Down Expand Up @@ -742,13 +743,20 @@ export class CodexAcpServer implements acp.Agent {
approvalHandler,
elicitationHandler);

if (await this.availableCommands.tryHandle(params.prompt, sessionState)) {
const commandResult = await this.availableCommands.tryHandle(params.prompt, sessionState);
if (commandResult) {
logger.log("Prompt handled by a command");
return {
stopReason: "end_turn",
usage: this.buildPromptUsage(sessionState.lastTokenUsage),
_meta: this.buildQuotaMeta(sessionState),
};
if (commandResult !== true) {
const interruptedResponse = await this.createInterruptedResponseIfNeeded(params.sessionId, commandResult, sessionState);
if (interruptedResponse) {
return interruptedResponse;
}
}
const error = eventHandler.getFailure();
if (error) {
throw error;
}
return this.createPromptResponse("end_turn", sessionState);
}

const modelId = ModelId.fromString(sessionState.currentModelId);
Expand All @@ -771,22 +779,9 @@ export class CodexAcpServer implements acp.Agent {
() => this.codexAcpClient.sendPrompt(params, agentMode, modelId, disableSummary, sessionState.cwd));

// Check if turn was interrupted (cancelled)
if (turnCompleted.turn.status === "interrupted") {
await this.connection.sessionUpdate({
sessionId: params.sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "*Conversation interrupted*"
}
}
});
return {
stopReason: "cancelled",
usage: this.buildPromptUsage(sessionState.lastTokenUsage),
_meta: this.buildQuotaMeta(sessionState),
};
const interruptedResponse = await this.createInterruptedResponseIfNeeded(params.sessionId, turnCompleted, sessionState);
if (interruptedResponse) {
return interruptedResponse;
}

const error = eventHandler.getFailure()
Expand All @@ -795,11 +790,7 @@ export class CodexAcpServer implements acp.Agent {
throw error;
}

return {
stopReason: "end_turn",
usage: this.buildPromptUsage(sessionState.lastTokenUsage),
_meta: this.buildQuotaMeta(sessionState),
};
return this.createPromptResponse("end_turn", sessionState);
} catch (err) {
logger.error(`Prompt for session ${params.sessionId} failed`, err);
throw err;
Expand Down Expand Up @@ -835,6 +826,35 @@ export class CodexAcpServer implements acp.Agent {
return toPromptUsage(lastTokenUsage);
}

private async createInterruptedResponseIfNeeded(
sessionId: string,
turnCompleted: TurnCompletedNotification,
sessionState: SessionState
): Promise<acp.PromptResponse | null> {
if (turnCompleted.turn.status !== "interrupted") {
return null;
}
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "*Conversation interrupted*"
}
}
});
return this.createPromptResponse("cancelled", sessionState);
}

private createPromptResponse(stopReason: acp.PromptResponse["stopReason"], sessionState: SessionState): acp.PromptResponse {
return {
stopReason,
usage: this.buildPromptUsage(sessionState.lastTokenUsage),
_meta: this.buildQuotaMeta(sessionState),
};
}

private async runWithProcessCheck<T>(operation: () => Promise<T>): Promise<T> {
try {
return await operation();
Expand Down
21 changes: 21 additions & 0 deletions src/CodexAppServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import type {
SkillsListResponse,
ThreadLoadedListParams,
ThreadLoadedListResponse,
ThreadCompactStartParams,
ThreadCompactStartResponse,
ThreadListParams,
ThreadListResponse,
ThreadReadParams,
Expand Down Expand Up @@ -181,6 +183,21 @@ export class CodexAppServerClient {
}
}

async runCompact(params: ThreadCompactStartParams): Promise<TurnCompletedNotification> {
let resolveTurnCompleted!: (event: TurnCompletedNotification) => void;
const turnCompleted = new Promise<TurnCompletedNotification>((resolve) => {
resolveTurnCompleted = resolve;
});
const releaseCapture = this.captureTurnCompletions(params.threadId, resolveTurnCompleted);

try {
await this.threadCompactStart(params);
return await turnCompleted;
} finally {
releaseCapture();
}
}

async turnInterrupt(params: TurnInterruptParams): Promise<TurnInterruptResponse> {
return await this.sendRequest({ method: "turn/interrupt", params: params });
}
Expand All @@ -205,6 +222,10 @@ export class CodexAppServerClient {
return await this.sendRequest({ method: "thread/read", params: params });
}

async threadCompactStart(params: ThreadCompactStartParams): Promise<ThreadCompactStartResponse> {
return await this.sendRequest({ method: "thread/compact/start", params });
}

async listMcpServerStatus(params: ListMcpServerStatusParams): Promise<ListMcpServerStatusResponse> {
return await this.sendRequest({ method: "mcpServerStatus/list", params });
}
Expand Down
13 changes: 11 additions & 2 deletions src/CodexCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {AgentSideConnection, AvailableCommand} from "@agentclientprotocol/s
import {ACPSessionConnection} from "./ACPSessionConnection";
import type {CodexAcpClient} from "./CodexAcpClient";
import type {RateLimitSnapshot, SkillsListEntry} from "./app-server/v2";
import type {TurnCompletedNotification} from "./app-server/v2";
import type {SessionState} from "./CodexAcpServer";
import type {RateLimitsMap} from "./RateLimitsMap";
import type {TokenCount} from "./TokenCount";
Expand Down Expand Up @@ -41,7 +42,7 @@ export class CodexCommands {
}
}

async tryHandle(prompt: acp.ContentBlock[], sessionState: SessionState): Promise<boolean> {
async tryHandle(prompt: acp.ContentBlock[], sessionState: SessionState): Promise<CommandHandlingResult | false> {
const command = this.parseCommand(prompt);
if (command) {
return this.handleCommand(command, sessionState);
Expand Down Expand Up @@ -91,6 +92,11 @@ export class CodexCommands {
description: "Display session configuration and token usage.",
input: null
},
{
name: "compact",
description: "Summarize conversation to prevent hitting the context limit.",
input: null
},
{
name: "logout",
description: "Sign out of Codex. This option is available when you are logged in via ChatGPT.",
Expand Down Expand Up @@ -119,10 +125,12 @@ export class CodexCommands {
};
}

async handleCommand(command: ParsedCommand, sessionState: SessionState): Promise<boolean> {
async handleCommand(command: ParsedCommand, sessionState: SessionState): Promise<CommandHandlingResult> {
const sessionId = sessionState.sessionId;

switch (command.name) {
case "compact":
return await this.runWithProcessCheck(() => this.codexAcpClient.compactSession(sessionId));
case "status": {
const session = new ACPSessionConnection(this.connection, sessionId);
const message = this.buildStatusMessage(sessionState);
Expand Down Expand Up @@ -355,3 +363,4 @@ export class CodexCommands {
}

type ParsedCommand = { name: string; input: string | null };
type CommandHandlingResult = true | TurnCompletedNotification;
16 changes: 14 additions & 2 deletions src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@ export class CodexEventHandler {
return await createMcpToolCallUpdate(event.item);
case "dynamicToolCall":
return await createDynamicToolCallUpdate(event.item);
case "contextCompaction":
return {
sessionUpdate: "tool_call",
toolCallId: event.item.id,
kind: "other",
title: "Compacting context",
status: "in_progress",
};
case "collabAgentToolCall":
case "userMessage":
case "hookPrompt":
Expand All @@ -237,7 +245,6 @@ export class CodexEventHandler {
case "imageGeneration":
case "enteredReviewMode":
case "exitedReviewMode":
case "contextCompaction":
case "plan":
return null;
}
Expand Down Expand Up @@ -272,6 +279,12 @@ export class CodexEventHandler {
text: summary
}
}
case "contextCompaction":
return {
sessionUpdate: "tool_call_update",
toolCallId: event.item.id,
status: "completed",
};
case "collabAgentToolCall":
case "userMessage":
case "hookPrompt":
Expand All @@ -281,7 +294,6 @@ export class CodexEventHandler {
case "imageGeneration":
case "enteredReviewMode":
case "exitedReviewMode":
case "contextCompaction":
case "plan":
return null;
}
Expand Down
Loading
Loading