fix(omp): honor hidden custom messages (#2280)

This commit is contained in:
Byeonghoon Yoo
2026-07-24 00:41:36 +08:00
committed by GitHub
parent 8e063f0dfc
commit 5e47cff58e
7 changed files with 116 additions and 13 deletions
@@ -260,6 +260,23 @@ describe("OMP agent client and session", () => {
expect(omp.completedTurnCount()).toBe(1); expect(omp.completedTurnCount()).toBe(1);
}); });
test("omits live custom messages when display is false", async () => {
const omp = new OmpHarness();
await omp.start();
await expect(
omp.runPromptAfterExtensionNotice("hello OMP", "model turn completed", false),
).resolves.toMatchObject({ finalText: expect.stringContaining("model turn completed") });
expect(omp.timeline()).toEqual([
{ type: "user_message", text: "hello OMP", messageId: "user-1" },
{
type: "assistant_message",
text: "model turn completed",
messageId: "omp-assistant-1",
},
]);
});
test("does not complete a queued model turn from OMP's local-only hint", async () => { test("does not complete a queued model turn from OMP's local-only hint", async () => {
const omp = new OmpHarness(); const omp = new OmpHarness();
await omp.start(); await omp.start();
@@ -66,6 +66,7 @@ import {
} from "./provider-config.js"; } from "./provider-config.js";
export { formatOmpVersionSupport, resolveOmpDiagnosticPaths } from "./provider-config.js"; export { formatOmpVersionSupport, resolveOmpDiagnosticPaths } from "./provider-config.js";
import { OmpSubagentCardTracker, type OmpSubagentCardScheduler } from "./subagent-card-tracker.js"; import { OmpSubagentCardTracker, type OmpSubagentCardScheduler } from "./subagent-card-tracker.js";
import { shouldDisplayOmpCustomMessage } from "./custom-message.js";
import { getUserMessageText } from "./message-history.js"; import { getUserMessageText } from "./message-history.js";
import { materializeProviderImage } from "../provider-image-output.js"; import { materializeProviderImage } from "../provider-image-output.js";
import { OmpCliRuntime } from "./cli-runtime.js"; import { OmpCliRuntime } from "./cli-runtime.js";
@@ -2064,15 +2065,17 @@ export class OmpAgentSession implements AgentSession {
return; return;
} }
if (event.message.role === "custom") { if (event.message.role === "custom") {
const text = getUserMessageText(event.message.content); if (shouldDisplayOmpCustomMessage(event.message)) {
if (text) { const text = getUserMessageText(event.message.content);
const advisorItem = mapOmpAdvisorMessageToToolCall(event.message, text); if (text) {
this.emit({ const advisorItem = mapOmpAdvisorMessageToToolCall(event.message, text);
type: "timeline", this.emit({
provider: this.provider, type: "timeline",
turnId, provider: this.provider,
item: advisorItem ?? { type: "assistant_message", text }, turnId,
}); item: advisorItem ?? { type: "assistant_message", text },
});
}
} }
if (!this.activeTurnHasUserMessage) { if (!this.activeTurnHasUserMessage) {
this.completeTurn(turnId, []); this.completeTurn(turnId, []);
@@ -0,0 +1,7 @@
import type { OmpAgentMessage } from "./rpc-types.js";
type OmpCustomMessage = Extract<OmpAgentMessage, { role: "custom" }>;
export function shouldDisplayOmpCustomMessage(message: OmpCustomMessage): boolean {
return Reflect.get(message, "display") !== false;
}
@@ -209,6 +209,54 @@ describe("OMP history mapper", () => {
]); ]);
}); });
test("omits replayed custom messages only when display is false", async () => {
await expect(
collectHistory(
[
{ role: "user", content: "first prompt" },
{ role: "custom", content: "hidden reminder", display: false },
{ role: "custom", content: "visible explicit custom", display: true },
{ role: "custom", content: "visible legacy custom" },
{
role: "assistant",
content: [{ type: "text", text: "assistant reply" }],
responseId: "assistant-history",
},
],
[{ id: "entry-user-1", text: "first prompt" }],
),
).resolves.toEqual([
{
type: "timeline",
provider: "omp",
item: {
type: "user_message",
text: "first prompt",
messageId: "entry-user-1",
},
},
{
type: "timeline",
provider: "omp",
item: { type: "assistant_message", text: "visible explicit custom" },
},
{
type: "timeline",
provider: "omp",
item: { type: "assistant_message", text: "visible legacy custom" },
},
{
type: "timeline",
provider: "omp",
item: {
type: "assistant_message",
text: "assistant reply",
messageId: "assistant-history",
},
},
]);
});
test("suppresses replayed raw todo tool calls through the OMP detail hook", async () => { test("suppresses replayed raw todo tool calls through the OMP detail hook", async () => {
await expect( await expect(
collectHistory([ collectHistory([
@@ -1,5 +1,6 @@
import type { AgentStreamEvent, AgentTimelineItem, ToolCallDetail } from "../../agent-sdk-types.js"; import type { AgentStreamEvent, AgentTimelineItem, ToolCallDetail } from "../../agent-sdk-types.js";
import type { OmpAgentMessage, OmpImageContent, OmpTextContent } from "./rpc-types.js"; import type { OmpAgentMessage, OmpImageContent, OmpTextContent } from "./rpc-types.js";
import { shouldDisplayOmpCustomMessage } from "./custom-message.js";
import { import {
extractTextFromToolResult, extractTextFromToolResult,
mapToolDetail, mapToolDetail,
@@ -117,6 +118,9 @@ export class OmpHistoryMapper {
private mapCustomMessage( private mapCustomMessage(
message: Extract<OmpAgentMessage, { role: "custom" }>, message: Extract<OmpAgentMessage, { role: "custom" }>,
): AgentStreamEvent[] { ): AgentStreamEvent[] {
if (!shouldDisplayOmpCustomMessage(message)) {
return [];
}
const text = getUserMessageText(message.content); const text = getUserMessageText(message.content);
const mappedEvent = text ? this.hooks.mapCustomMessage?.(message, text, this.provider) : null; const mappedEvent = text ? this.hooks.mapCustomMessage?.(message, text, this.provider) : null;
if (mappedEvent) { if (mappedEvent) {
@@ -201,16 +201,25 @@ export class OmpHarness {
return { completion }; return { completion };
} }
async runPromptAfterExtensionNotice(input: string, output: string): Promise<unknown> { async runPromptAfterExtensionNotice(
input: string,
output: string,
display?: boolean,
): Promise<unknown> {
const session = this.requireSession(); const session = this.requireSession();
const promptStarted = this.omp.latestSession().nextPrompt(); const promptStarted = this.omp.latestSession().nextPrompt();
const run = session.run(input); const run = session.run(input);
await promptStarted; await promptStarted;
const runtime = this.omp.latestSession(); const runtime = this.omp.latestSession();
const message = {
role: "custom" as const,
content: "extension inventory changed",
...(display === undefined ? {} : { display }),
};
runtime.beginTurn(); runtime.beginTurn();
runtime.acceptPrompt(input, "user-1"); runtime.acceptPrompt(input, "user-1");
runtime.acceptCustomMessage("extension inventory changed"); runtime.emit({ type: "message_end", message });
runtime.finishTurn({ role: "custom", content: "extension inventory changed" }); runtime.finishTurn(message);
runtime.beginTurn(); runtime.beginTurn();
runtime.streamAssistantText(output); runtime.streamAssistantText(output);
runtime.finishTurn(); runtime.finishTurn();
@@ -1,8 +1,9 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { parseToolArgs, parseToolResult } from "./tool-call-detail.js"; import { parseToolArgs, parseToolResult } from "./tool-call-detail.js";
import { OmpAvailableCommandsUpdateEventSchema } from "./rpc-types.js"; import { OmpAgentMessageSchema, OmpAvailableCommandsUpdateEventSchema } from "./rpc-types.js";
import { mapOmpToolDetail } from "./tool-call-mapper.js"; import { mapOmpToolDetail } from "./tool-call-mapper.js";
import { shouldDisplayOmpCustomMessage } from "./custom-message.js";
describe("OMP 17 RPC compatibility", () => { describe("OMP 17 RPC compatibility", () => {
test("parses source-attributed command updates", () => { test("parses source-attributed command updates", () => {
@@ -16,6 +17,20 @@ describe("OMP 17 RPC compatibility", () => {
]); ]);
}); });
test("keeps non-false custom display metadata backward compatible", () => {
const message = OmpAgentMessageSchema.parse({
role: "custom",
content: "visible custom message",
display: null,
});
expect(message).toMatchObject({ display: null });
if (message.role !== "custom") {
throw new Error("Expected a custom OMP message");
}
expect(shouldDisplayOmpCustomMessage(message)).toBe(true);
});
test("maps subscribed custom tool events without assuming built-in names", () => { test("maps subscribed custom tool events without assuming built-in names", () => {
const event = { const event = {
type: "tool_execution_start", type: "tool_execution_start",