Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
hasPassthroughJson,
isPlaceholderMessage,
isOnlyJsonMessage,
isSystemRole,
shouldRenderMessage,
parseToolCallsFromMessage,
} from "./chat-message-utils";
Expand Down Expand Up @@ -313,4 +314,35 @@ describe("chat-message-utils", () => {
).toEqual(directToolCalls);
});
});

describe("isSystemRole", () => {
it("returns true for system", () => {
expect(isSystemRole("system")).toBe(true);
});

it("returns true for developer", () => {
expect(isSystemRole("developer")).toBe(true);
});

it("returns true for tool_definitions", () => {
expect(isSystemRole("tool_definitions")).toBe(true);
});

it("is case-insensitive", () => {
expect(isSystemRole("System")).toBe(true);
expect(isSystemRole("DEVELOPER")).toBe(true);
});

it("returns false for user/assistant/tool", () => {
expect(isSystemRole("user")).toBe(false);
expect(isSystemRole("assistant")).toBe(false);
expect(isSystemRole("tool")).toBe(false);
});

it("returns false for empty / undefined / null", () => {
expect(isSystemRole("")).toBe(false);
expect(isSystemRole(undefined)).toBe(false);
expect(isSystemRole(null)).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,25 @@ export function parseToolCallsFromMessage(
: [];
}

/**
* Roles that carry system-level instructions, tool catalogs, skills, or
* operator rules rather than user/assistant turns. These messages bloat the
* Preview tab with content that is rarely what reviewers want to see.
*
* The full payload (including these messages) remains visible in the JSON and
* JSON Beta views — this is a render-layer filter only.
*/
const SYSTEM_ROLES = new Set(["system", "developer", "tool_definitions"]);

/**
* Check if a ChatML role represents a system/developer/instruction message.
* Case-insensitive to accommodate upstream role casing variations.
*/
export function isSystemRole(role: string | undefined | null): boolean {
if (!role) return false;
return SYSTEM_ROLES.has(role.toLowerCase());
}

/**
* Check if message has thinking content.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
extractAdditionalInput,
} from "@/src/utils/chatml";
import type { ChatMlMessageSchema } from "@/src/components/schemas/ChatMlSchema";
import { isSystemRole } from "../components/chat-message-utils";

// ChatML message type from schema
export type ChatMlMessage = z.infer<typeof ChatMlMessageSchema>;
Expand Down Expand Up @@ -46,6 +47,19 @@ function parseToolCallsFromMessage(
: [];
}

export interface UseChatMLParserOptions {
/**
* When true, drop messages whose role is system/developer/tool_definitions
* from the rendered chat transcript. The underlying trace payload is
* untouched — JSON views still display the full input/output.
*
* Defaults to true: every current caller is a trace/observation detail or
* preview surface where leaking multi-KB system prompts makes Preview
* useless for scanning conversations (see 2BB-289).
*/
hideSystemMessages?: boolean;
}

/**
* Hook to parse input/output into ChatML format and extract tool information.
*
Expand All @@ -67,7 +81,9 @@ export function useChatMLParser(
preParsedInput?: unknown,
preParsedOutput?: unknown,
preParsedMetadata?: unknown,
options: UseChatMLParserOptions = {},
): ChatMLParserResult {
const { hideSystemMessages = true } = options;
// Use pre-parsed data if available (from Web Worker), otherwise parse synchronously
// This eliminates ~100ms of duplicate parsing when data comes from useParsedObservation
const parsedInput =
Expand Down Expand Up @@ -95,28 +111,37 @@ export function useChatMLParser(
const outputClean = cleanLegacyOutput(parsedOutput, parsedOutput);

// Combine messages
const messages = combineInputOutputMessages(
const combinedMessages = combineInputOutputMessages(
inResult,
outResult,
outputClean,
);

// Extract all unique tools from messages (no numbering yet)
// Optionally hide system/developer/tool_definitions messages from the
// pretty render (Preview tab). Tools are extracted from ALL messages
// (pre-filter) so the SectionToolDefinitions catalog stays populated even
// when the only carrier of `tools` was a system message.
const rawInputMessageCount = inResult.success ? inResult.data.length : 0;
const toolsMap = new Map<string, ToolDefinition>();
const messages: typeof combinedMessages = [];
let inputMessageCount = 0;

for (const message of messages) {
combinedMessages.forEach((message, i) => {
if (message.tools && Array.isArray(message.tools)) {
for (const tool of message.tools) {
if (!toolsMap.has(tool.name)) {
toolsMap.set(tool.name, tool);
}
}
}
}

if (hideSystemMessages && isSystemRole(message.role)) return;
messages.push(message);
if (i < rawInputMessageCount) inputMessageCount++;
});

// Count tool call invocations
// Only number tool calls from OUTPUT messages (current invocation), not input (history)
const inputMessageCount = inResult.success ? inResult.data.length : 0;
let toolCallCounter = 0;
const messageToToolCallNumbers = new Map<number, number[]>();
const toolCallCounts = new Map<string, number>();
Expand Down Expand Up @@ -184,7 +209,13 @@ export function useChatMLParser(
toolNameToDefinitionNumber,
inputMessageCount,
};
}, [parsedInput, parsedOutput, parsedMetadata, observationName]);
}, [
parsedInput,
parsedOutput,
parsedMetadata,
observationName,
hideSystemMessages,
]);
}

// Re-export for use in ChatMessage
Expand Down
Loading