fix(omp): complete turns when agent_end omits messages (#2261)

* fix(omp): complete turns after empty agent_end

* test(omp): remove timing flush from empty end regression
This commit is contained in:
Byeonghoon Yoo
2026-07-22 21:42:16 +02:00
committed by GitHub
parent 5dfe50ca74
commit 35f5171477
4 changed files with 58 additions and 5 deletions
@@ -132,6 +132,20 @@ describe("OMP agent client and session", () => {
]); ]);
}); });
test("completes a streamed assistant turn when agent_end omits messages", async () => {
const omp = new OmpHarness();
await omp.start();
const { completion } = await omp.startPromptWithEmptyAgentEnd(
"hello OMP",
"empty terminal payload recovered",
);
await expect(completion).resolves.toMatchObject({
finalText: "empty terminal payload recovered",
});
expect(omp.completedTurnCount()).toBe(1);
});
test("does not accept a follow-up until OMP reports stable idle", async () => { test("does not accept a follow-up until OMP reports stable idle", async () => {
const omp = new OmpHarness(); const omp = new OmpHarness();
await omp.start(); await omp.start();
@@ -899,6 +899,7 @@ export class OmpAgentSession implements AgentSession {
private activeTurnId: string | null = null; private activeTurnId: string | null = null;
private activeClientMessageId: string | null = null; private activeClientMessageId: string | null = null;
private activeAssistantMessageId: string | null = null; private activeAssistantMessageId: string | null = null;
private activeTurnTerminalAssistantMessage: OmpAgentMessage | null = null;
private activeTurnStarted = false; private activeTurnStarted = false;
private activeTurnHasUserMessage = false; private activeTurnHasUserMessage = false;
private activeNoTurnPromptText: string | null = null; private activeNoTurnPromptText: string | null = null;
@@ -987,6 +988,7 @@ export class OmpAgentSession implements AgentSession {
this.activeTurnId = turnId; this.activeTurnId = turnId;
this.activeClientMessageId = options?.clientMessageId ?? null; this.activeClientMessageId = options?.clientMessageId ?? null;
this.activeAssistantMessageId = null; this.activeAssistantMessageId = null;
this.activeTurnTerminalAssistantMessage = null;
this.activeTurnStarted = false; this.activeTurnStarted = false;
this.activeTurnHasUserMessage = false; this.activeTurnHasUserMessage = false;
this.activePromptRequestId = null; this.activePromptRequestId = null;
@@ -1017,6 +1019,7 @@ export class OmpAgentSession implements AgentSession {
this.activeTurnStarted = false; this.activeTurnStarted = false;
this.activeTurnHasUserMessage = false; this.activeTurnHasUserMessage = false;
this.activeAssistantMessageId = null; this.activeAssistantMessageId = null;
this.activeTurnTerminalAssistantMessage = null;
this.clearNoTurnBuffers(); this.clearNoTurnBuffers();
if (isOmpRequestAbortError(error)) { if (isOmpRequestAbortError(error)) {
this.emit({ this.emit({
@@ -1151,6 +1154,7 @@ export class OmpAgentSession implements AgentSession {
this.activeTurnStarted = false; this.activeTurnStarted = false;
this.activeTurnHasUserMessage = false; this.activeTurnHasUserMessage = false;
this.activeAssistantMessageId = null; this.activeAssistantMessageId = null;
this.activeTurnTerminalAssistantMessage = null;
this.clearNoTurnBuffers(); this.clearNoTurnBuffers();
this.emit({ this.emit({
type: "turn_canceled", type: "turn_canceled",
@@ -1773,6 +1777,7 @@ export class OmpAgentSession implements AgentSession {
this.activeClientMessageId = null; this.activeClientMessageId = null;
this.activeTurnStarted = false; this.activeTurnStarted = false;
this.activeTurnHasUserMessage = false; this.activeTurnHasUserMessage = false;
this.activeTurnTerminalAssistantMessage = null;
this.clearNoTurnBuffers(); this.clearNoTurnBuffers();
this.emit({ this.emit({
type: "turn_failed", type: "turn_failed",
@@ -1857,17 +1862,25 @@ export class OmpAgentSession implements AgentSession {
}, },
}); });
return; return;
case "agent_end": case "agent_end": {
const messages = event.messages ?? [];
let terminalMessages: OmpAgentMessage[] | null = null;
if (messages.some((message) => message.role === "assistant")) {
terminalMessages = messages;
} else if (this.activeTurnTerminalAssistantMessage) {
terminalMessages = [this.activeTurnTerminalAssistantMessage];
}
// OMP can end an internal extension-notice cycle before it starts the // OMP can end an internal extension-notice cycle before it starts the
// model turn for the same prompt. That cycle has no assistant message // model turn for the same prompt. Ignore only cycles where neither the
// and is not the foreground turn's terminal event. // terminal payload nor the live stream contained an assistant message.
if (!(event.messages ?? []).some((message) => message.role === "assistant")) { if (!terminalMessages) {
return; return;
} }
// A state request is processed after OMP's RPC loop becomes promptable, // A state request is processed after OMP's RPC loop becomes promptable,
// so do not advertise Paseo idle until it reports that transition. // so do not advertise Paseo idle until it reports that transition.
void this.completeTurnAfterProviderIdle(turnId, event.messages ?? []); void this.completeTurnAfterProviderIdle(turnId, terminalMessages);
return; return;
}
default: default:
return; return;
} }
@@ -1983,6 +1996,9 @@ export class OmpAgentSession implements AgentSession {
): void { ): void {
if (event.message.role === "assistant") { if (event.message.role === "assistant") {
this.activeAssistantMessageId = null; this.activeAssistantMessageId = null;
if (turnId) {
this.activeTurnTerminalAssistantMessage = event.message;
}
return; return;
} }
if (event.message.role === "custom") { if (event.message.role === "custom") {
@@ -2097,6 +2113,7 @@ export class OmpAgentSession implements AgentSession {
this.activeTurnId = null; this.activeTurnId = null;
this.activeClientMessageId = null; this.activeClientMessageId = null;
this.activeAssistantMessageId = null; this.activeAssistantMessageId = null;
this.activeTurnTerminalAssistantMessage = null;
this.activeTurnStarted = false; this.activeTurnStarted = false;
this.activeTurnHasUserMessage = false; this.activeTurnHasUserMessage = false;
this.clearNoTurnBuffers(); this.clearNoTurnBuffers();
@@ -413,6 +413,12 @@ export class FakeOmpSession implements OmpRuntimeSession {
this.emit({ type: "agent_end", messages: this.messages }); this.emit({ type: "agent_end", messages: this.messages });
} }
finishTurnWithEmptyAgentEnd(message: OmpAgentMessage = { role: "assistant", content: [] }): void {
this.messages = [...this.messages, message];
this.emit({ type: "message_end", message });
this.emit({ type: "agent_end", messages: [] });
}
beginTurn(): void { beginTurn(): void {
this.emit({ type: "turn_start" }); this.emit({ type: "turn_start" });
} }
@@ -173,6 +173,22 @@ export class OmpHarness {
return await run; return await run;
} }
async startPromptWithEmptyAgentEnd(
input: string,
output: string,
): Promise<{ completion: Promise<unknown> }> {
const session = this.requireSession();
const promptStarted = this.omp.latestSession().nextPrompt();
const completion = session.run(input);
await promptStarted;
const runtime = this.omp.latestSession();
runtime.beginTurn();
runtime.acceptPrompt(input, "user-1");
runtime.streamAssistantText(output);
runtime.finishTurnWithEmptyAgentEnd();
return { completion };
}
async runPromptAfterExtensionNotice(input: string, output: string): Promise<unknown> { async runPromptAfterExtensionNotice(input: string, output: string): Promise<unknown> {
const session = this.requireSession(); const session = this.requireSession();
const promptStarted = this.omp.latestSession().nextPrompt(); const promptStarted = this.omp.latestSession().nextPrompt();