diff --git a/packages/server/src/server/agent/providers/omp/agent.test.ts b/packages/server/src/server/agent/providers/omp/agent.test.ts index fe2fc3749..35020ca45 100644 --- a/packages/server/src/server/agent/providers/omp/agent.test.ts +++ b/packages/server/src/server/agent/providers/omp/agent.test.ts @@ -260,6 +260,23 @@ describe("OMP agent client and session", () => { 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 () => { const omp = new OmpHarness(); await omp.start(); diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index eec64cee4..c0654a627 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -66,6 +66,7 @@ import { } from "./provider-config.js"; export { formatOmpVersionSupport, resolveOmpDiagnosticPaths } from "./provider-config.js"; import { OmpSubagentCardTracker, type OmpSubagentCardScheduler } from "./subagent-card-tracker.js"; +import { shouldDisplayOmpCustomMessage } from "./custom-message.js"; import { getUserMessageText } from "./message-history.js"; import { materializeProviderImage } from "../provider-image-output.js"; import { OmpCliRuntime } from "./cli-runtime.js"; @@ -2064,15 +2065,17 @@ export class OmpAgentSession implements AgentSession { return; } if (event.message.role === "custom") { - const text = getUserMessageText(event.message.content); - if (text) { - const advisorItem = mapOmpAdvisorMessageToToolCall(event.message, text); - this.emit({ - type: "timeline", - provider: this.provider, - turnId, - item: advisorItem ?? { type: "assistant_message", text }, - }); + if (shouldDisplayOmpCustomMessage(event.message)) { + const text = getUserMessageText(event.message.content); + if (text) { + const advisorItem = mapOmpAdvisorMessageToToolCall(event.message, text); + this.emit({ + type: "timeline", + provider: this.provider, + turnId, + item: advisorItem ?? { type: "assistant_message", text }, + }); + } } if (!this.activeTurnHasUserMessage) { this.completeTurn(turnId, []); diff --git a/packages/server/src/server/agent/providers/omp/custom-message.ts b/packages/server/src/server/agent/providers/omp/custom-message.ts new file mode 100644 index 000000000..e84095b4f --- /dev/null +++ b/packages/server/src/server/agent/providers/omp/custom-message.ts @@ -0,0 +1,7 @@ +import type { OmpAgentMessage } from "./rpc-types.js"; + +type OmpCustomMessage = Extract; + +export function shouldDisplayOmpCustomMessage(message: OmpCustomMessage): boolean { + return Reflect.get(message, "display") !== false; +} diff --git a/packages/server/src/server/agent/providers/omp/history-mapper.test.ts b/packages/server/src/server/agent/providers/omp/history-mapper.test.ts index 353f3c98f..db67f0cbe 100644 --- a/packages/server/src/server/agent/providers/omp/history-mapper.test.ts +++ b/packages/server/src/server/agent/providers/omp/history-mapper.test.ts @@ -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 () => { await expect( collectHistory([ diff --git a/packages/server/src/server/agent/providers/omp/message-history.ts b/packages/server/src/server/agent/providers/omp/message-history.ts index 04a6b2877..b030ec513 100644 --- a/packages/server/src/server/agent/providers/omp/message-history.ts +++ b/packages/server/src/server/agent/providers/omp/message-history.ts @@ -1,5 +1,6 @@ import type { AgentStreamEvent, AgentTimelineItem, ToolCallDetail } from "../../agent-sdk-types.js"; import type { OmpAgentMessage, OmpImageContent, OmpTextContent } from "./rpc-types.js"; +import { shouldDisplayOmpCustomMessage } from "./custom-message.js"; import { extractTextFromToolResult, mapToolDetail, @@ -117,6 +118,9 @@ export class OmpHistoryMapper { private mapCustomMessage( message: Extract, ): AgentStreamEvent[] { + if (!shouldDisplayOmpCustomMessage(message)) { + return []; + } const text = getUserMessageText(message.content); const mappedEvent = text ? this.hooks.mapCustomMessage?.(message, text, this.provider) : null; if (mappedEvent) { diff --git a/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts b/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts index c7e3c905e..00fed1ac8 100644 --- a/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts +++ b/packages/server/src/server/agent/providers/omp/test-utils/omp-harness.ts @@ -201,16 +201,25 @@ export class OmpHarness { return { completion }; } - async runPromptAfterExtensionNotice(input: string, output: string): Promise { + async runPromptAfterExtensionNotice( + input: string, + output: string, + display?: boolean, + ): Promise { const session = this.requireSession(); const promptStarted = this.omp.latestSession().nextPrompt(); const run = session.run(input); await promptStarted; const runtime = this.omp.latestSession(); + const message = { + role: "custom" as const, + content: "extension inventory changed", + ...(display === undefined ? {} : { display }), + }; runtime.beginTurn(); runtime.acceptPrompt(input, "user-1"); - runtime.acceptCustomMessage("extension inventory changed"); - runtime.finishTurn({ role: "custom", content: "extension inventory changed" }); + runtime.emit({ type: "message_end", message }); + runtime.finishTurn(message); runtime.beginTurn(); runtime.streamAssistantText(output); runtime.finishTurn(); diff --git a/packages/server/src/server/agent/providers/omp/v17-rpc-compat.test.ts b/packages/server/src/server/agent/providers/omp/v17-rpc-compat.test.ts index 5807945aa..8a472361d 100644 --- a/packages/server/src/server/agent/providers/omp/v17-rpc-compat.test.ts +++ b/packages/server/src/server/agent/providers/omp/v17-rpc-compat.test.ts @@ -1,8 +1,9 @@ import { describe, expect, test } from "vitest"; 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 { shouldDisplayOmpCustomMessage } from "./custom-message.js"; describe("OMP 17 RPC compatibility", () => { 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", () => { const event = { type: "tool_execution_start",