diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index f11b64832..6a3b6ea9b 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -452,6 +452,121 @@ describe("AgentManager", () => { expect(refreshed?.runtimeInfo?.model).toBe("gpt-5.2-codex"); }); + test("runAgent assembles finalText from trailing assistant chunks", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const expectedFinalText = + "```json\n{\"message\":\"Reserve space for archive button in sidebar agent list\"}\n```"; + + class ChunkedAssistantSession implements AgentSession { + readonly provider = "codex" as const; + readonly capabilities = TEST_CAPABILITIES; + readonly id = randomUUID(); + + async run(): Promise { + return { + sessionId: this.id, + finalText: "", + timeline: [], + }; + } + + async *stream(): AsyncGenerator { + yield { type: "turn_started", provider: this.provider }; + yield { + type: "timeline", + provider: this.provider, + item: { + type: "assistant_message", + text: "```json\n{\"message\":\"Reserve space for archive button in side", + }, + }; + yield { + type: "timeline", + provider: this.provider, + item: { + type: "assistant_message", + text: "bar agent list\"}\n```", + }, + }; + yield { type: "turn_completed", provider: this.provider }; + } + + async *streamHistory(): AsyncGenerator {} + + async getRuntimeInfo() { + return { + provider: this.provider, + sessionId: this.id, + model: null, + modeId: null, + }; + } + + async getAvailableModes() { + return []; + } + + async getCurrentMode() { + return null; + } + + async setMode(): Promise {} + + getPendingPermissions() { + return []; + } + + async respondToPermission(): Promise {} + + describePersistence() { + return { + provider: this.provider, + sessionId: this.id, + }; + } + + async interrupt(): Promise {} + + async close(): Promise {} + } + + class ChunkedAssistantClient implements AgentClient { + readonly provider = "codex" as const; + readonly capabilities = TEST_CAPABILITIES; + + async isAvailable(): Promise { + return true; + } + + async createSession(): Promise { + return new ChunkedAssistantSession(); + } + + async resumeSession(): Promise { + return new ChunkedAssistantSession(); + } + } + + const manager = new AgentManager({ + clients: { + codex: new ChunkedAssistantClient(), + }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000113", + }); + + const snapshot = await manager.createAgent({ + provider: "codex", + cwd: workdir, + }); + + const result = await manager.runAgent(snapshot.id, "generate commit message"); + expect(result.finalText).toBe(expectedFinalText); + }); + test("listAgents excludes internal agents", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 18caed02a..abf7a8425 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -558,9 +558,6 @@ export class AgentManager { for await (const event of events) { if (event.type === "timeline") { timeline.push(event.item); - if (event.item.type === "assistant_message") { - finalText = event.item.text; - } } else if (event.type === "turn_completed") { usage = event.usage; } else if (event.type === "turn_failed") { @@ -570,6 +567,8 @@ export class AgentManager { } } + finalText = this.getLastAssistantMessageFromTimeline(timeline) ?? ""; + const agent = this.requireAgent(agentId); const sessionId = agent.persistence?.sessionId; if (!sessionId) { @@ -869,10 +868,16 @@ export class AgentManager { return null; } + return this.getLastAssistantMessageFromTimeline(agent.timeline); + } + + private getLastAssistantMessageFromTimeline( + timeline: readonly AgentTimelineItem[] + ): string | null { // Collect the last contiguous assistant messages (Claude streams chunks) const chunks: string[] = []; - for (let i = agent.timeline.length - 1; i >= 0; i--) { - const item = agent.timeline[i]; + for (let i = timeline.length - 1; i >= 0; i--) { + const item = timeline[i]; if (item.type !== "assistant_message") { if (chunks.length) { break;