Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
Model,
ReasoningEffortOption,
Thread,
ThreadGoalStatus,
ThreadItem,
TurnCompletedNotification,
UserInput
Expand Down Expand Up @@ -69,7 +70,7 @@ import {resolveTerminalOutputMode, type TerminalOutputMode} from "./TerminalOutp

export interface ThreadGoalSnapshot {
objective: string;
status: string;
status: ThreadGoalStatus;
tokenBudget: number | null;
}

Expand Down
42 changes: 6 additions & 36 deletions src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,35 +273,9 @@ export class CodexEventHandler {
}
this.sessionState.currentGoal = goalSnapshot;

const status = this.formatThreadGoalStatus(event.goal.status);
const objective = goalSnapshot.objective;
const text = objective.includes("\n")
? `Goal updated (${status}):\n${objective}`
: `Goal updated (${status}): ${objective}`;
return {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: `\n\n${text}\n\n`,
},
};
}

private formatThreadGoalStatus(status: ThreadGoalUpdatedNotification["goal"]["status"]): string {
switch (status) {
case "active":
return "active";
case "paused":
return "paused";
case "budgetLimited":
return "budget limited";
case "blocked":
return "blocked";
case "usageLimited":
return "usage limited";
case "complete":
return "complete";
}
return this.createCodexSessionInfoUpdate({
goal: goalSnapshot,
});
}

private createThreadGoalClearedEvent(_event: ThreadGoalClearedNotification): UpdateSessionEvent | null {
Expand All @@ -310,13 +284,9 @@ export class CodexEventHandler {
}
this.sessionState.currentGoal = null;

return {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "\n\nGoal cleared.\n\n",
},
};
return this.createCodexSessionInfoUpdate({
goal: null,
});
}

private createThreadGoalSnapshot(event: ThreadGoalUpdatedNotification): ThreadGoalSnapshot {
Expand Down
17 changes: 16 additions & 1 deletion src/__tests__/CodexACPAgent/CodexAcpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1660,7 +1660,22 @@ describe('ACP server test', { timeout: 40_000 }, () => {
await expect(promptPromise).resolves.toEqual(expect.objectContaining({
stopReason: "end_turn",
}));
expect(mockFixture.getAcpConnectionDump([])).toContain("Goal updated (active): Ship the migration and keep tests green");
expect(mockFixture.getAcpConnectionEvents([])).toContainEqual(expect.objectContaining({
args: [expect.objectContaining({
update: {
sessionUpdate: "session_info_update",
_meta: {
codex: {
goal: {
objective: "Ship the migration and keep tests green",
status: "active",
tokenBudget: null,
},
},
},
},
})],
}));
});

it('completes goal slash command when a turn routes after the goal update', async () => {
Expand Down
9 changes: 5 additions & 4 deletions src/__tests__/CodexACPAgent/data/thread-goal-cleared.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
{
"sessionId": "test-session-id",
"update": {
"sessionUpdate": "agent_message_chunk",
"content": {
"type": "text",
"text": "\n\nGoal cleared.\n\n"
"sessionUpdate": "session_info_update",
"_meta": {
"codex": {
"goal": null
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
{
"sessionId": "test-session-id",
"update": {
"sessionUpdate": "agent_message_chunk",
"content": {
"type": "text",
"text": "\n\nGoal updated (budget limited):\nFirst task\nSecond task\n\n"
"sessionUpdate": "session_info_update",
"_meta": {
"codex": {
"goal": {
"objective": "First task\nSecond task",
"status": "budgetLimited",
"tokenBudget": 1000
}
}
}
}
}
Expand Down
13 changes: 9 additions & 4 deletions src/__tests__/CodexACPAgent/data/thread-goal-updated.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
{
"sessionId": "test-session-id",
"update": {
"sessionUpdate": "agent_message_chunk",
"content": {
"type": "text",
"text": "\n\nGoal updated (active): Ship the goal update\n\n"
"sessionUpdate": "session_info_update",
"_meta": {
"codex": {
"goal": {
"objective": "Ship the goal update",
"status": "active",
"tokenBudget": null
}
}
}
}
}
Expand Down
50 changes: 43 additions & 7 deletions src/__tests__/CodexACPAgent/thread-goal-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("CodexEventHandler - thread goal events", () => {
vi.clearAllMocks();
});

it("should send thread goal updates as agent messages", async () => {
it("should send thread goal updates as session metadata", async () => {
const goalUpdatedNotification: ServerNotification = {
method: "thread/goal/updated",
params: {
Expand All @@ -44,7 +44,7 @@ describe("CodexEventHandler - thread goal events", () => {
);
});

it("should format multiline thread goal updates", async () => {
it("should trim multiline thread goal objectives in session metadata", async () => {
const goalUpdatedNotification: ServerNotification = {
method: "thread/goal/updated",
params: {
Expand All @@ -70,7 +70,7 @@ describe("CodexEventHandler - thread goal events", () => {
);
});

it("should send thread goal cleared as an agent message", async () => {
it("should send thread goal cleared as session metadata", async () => {
const goalClearedNotification: ServerNotification = {
method: "thread/goal/cleared",
params: {
Expand Down Expand Up @@ -123,10 +123,21 @@ describe("CodexEventHandler - thread goal events", () => {

const events = mockFixture.getAcpConnectionEvents([]);
expect(events).toHaveLength(1);
expect(events[0]!.args[0].update.content.text).toBe("\n\nGoal updated (active): Ship the goal update\n\n");
expect(events[0]!.args[0].update).toEqual({
sessionUpdate: "session_info_update",
_meta: {
codex: {
goal: {
objective: "Ship the goal update",
status: "active",
tokenBudget: null,
},
},
},
});
});

it("should separate completed goal updates from preceding agent text", async () => {
it("should not append completed goal updates to preceding agent text", async () => {
const goalCompletedNotification: ServerNotification = {
method: "thread/goal/updated",
params: {
Expand Down Expand Up @@ -160,7 +171,25 @@ describe("CodexEventHandler - thread goal events", () => {

const events = mockFixture.getAcpConnectionEvents([]);
expect(events).toHaveLength(2);
expect(events[1]!.args[0].update.content.text).toBe("\n\nGoal updated (complete): tell me a joke\n\n");
expect(events[0]!.args[0].update).toEqual({
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "Because they kept losing interest in `any`.",
},
});
expect(events[1]!.args[0].update).toEqual({
sessionUpdate: "session_info_update",
_meta: {
codex: {
goal: {
objective: "tell me a joke",
status: "complete",
tokenBudget: null,
},
},
},
});
});

it("should suppress duplicate thread goal cleared notifications", async () => {
Expand All @@ -178,7 +207,14 @@ describe("CodexEventHandler - thread goal events", () => {

const events = mockFixture.getAcpConnectionEvents([]);
expect(events).toHaveLength(1);
expect(events[0]!.args[0].update.content.text).toBe("\n\nGoal cleared.\n\n");
expect(events[0]!.args[0].update).toEqual({
sessionUpdate: "session_info_update",
_meta: {
codex: {
goal: null,
},
},
});
});

function createSessionState(): SessionState {
Expand Down