Fix chunked assistant finalText assembly

This commit is contained in:
Mohamed Boudra
2026-02-11 12:12:58 +07:00
parent d904a72dfb
commit a09a6ca1b3
2 changed files with 125 additions and 5 deletions

View File

@@ -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<AgentRunResult> {
return {
sessionId: this.id,
finalText: "",
timeline: [],
};
}
async *stream(): AsyncGenerator<AgentStreamEvent> {
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<AgentStreamEvent> {}
async getRuntimeInfo() {
return {
provider: this.provider,
sessionId: this.id,
model: null,
modeId: null,
};
}
async getAvailableModes() {
return [];
}
async getCurrentMode() {
return null;
}
async setMode(): Promise<void> {}
getPendingPermissions() {
return [];
}
async respondToPermission(): Promise<void> {}
describePersistence() {
return {
provider: this.provider,
sessionId: this.id,
};
}
async interrupt(): Promise<void> {}
async close(): Promise<void> {}
}
class ChunkedAssistantClient implements AgentClient {
readonly provider = "codex" as const;
readonly capabilities = TEST_CAPABILITIES;
async isAvailable(): Promise<boolean> {
return true;
}
async createSession(): Promise<AgentSession> {
return new ChunkedAssistantSession();
}
async resumeSession(): Promise<AgentSession> {
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");

View File

@@ -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;