fix(frontend): Agent Tools & Metadata not available for V1 conversations (#12180)

Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
This commit is contained in:
yunbae
2026-01-01 00:08:09 +09:00
committed by GitHub
parent 2734a5a52d
commit b7d5f903cf
11 changed files with 171 additions and 19 deletions

View File

@@ -0,0 +1,68 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import React from "react";
import { adaptSystemMessage } from "#/utils/system-message-adapter";
import { EventState } from "#/stores/use-event-store";
import { SystemMessageModal } from "#/components/features/conversation-panel/system-message-modal";
import { ToolsContextMenu } from "#/components/features/controls/tools-context-menu";
const v1Event: EventState["events"] = [
{
id: "v1-id",
timestamp: "2025-12-30T12:00:00Z",
source: "agent",
system_prompt: {
type: "text",
text: "v1 prompt",
},
tools: [
{
type: "function",
function: {
name: "bash",
description: "Execute bash",
parameters: {},
},
},
],
},
];
const adaptedResult = adaptSystemMessage(v1Event);
vi.mock("#/hooks/query/use-active-conversation", () => ({
useActiveConversation: () => ({ data: { conversation_version: "V1" } }),
}));
vi.mock("#/hooks/use-user-providers", () => ({
useUserProviders: () => ({ providers: ["test"] }),
}));
describe("SystemMessage UI Rendering", () => {
it("should render the 'Show Agent Tools' button in the context menu", () => {
render(
<ToolsContextMenu
onClose={() => {}}
onShowSkills={() => {}}
onShowAgentTools={() => {}}
/>,
);
expect(screen.getByTestId("show-agent-tools-button")).toBeInTheDocument();
});
it("should display the adapted v1 system prompt content correctly", () => {
render(
<SystemMessageModal
isOpen
onClose={() => {}}
systemMessage={adaptedResult}
/>,
);
const messageElement = screen.getByText("v1 prompt");
expect(messageElement).toBeDefined();
expect(messageElement).toBeVisible();
});
});

View File

@@ -0,0 +1,37 @@
import { describe, it, expect } from "vitest";
import { adaptSystemMessage } from "#/utils/system-message-adapter";
import { EventState } from "#/stores/use-event-store";
const v1Event: EventState["events"] = [
{
id: "v1-id",
timestamp: "2025-12-30T12:00:00Z",
source: "agent",
system_prompt: {
type: "text",
text: "v1 prompt",
},
tools: [
{
type: "function",
function: {
name: "bash",
description: "Execute bash",
parameters: {},
},
},
],
},
];
describe("adaptSystemMessage", () => {
it("should correctly adapt the v1 system_prompt event structure", () => {
const result = adaptSystemMessage(v1Event);
expect(result).not.toBeNull();
expect(result?.content).toBe("v1 prompt");
});
it("should return null when no system message is present in events", () => {
expect(adaptSystemMessage([])).toBeNull();
});
});

View File

@@ -61,7 +61,7 @@ export function Tools() {
<SystemMessageModal
isOpen={systemModalVisible}
onClose={() => setSystemModalVisible(false)}
systemMessage={systemMessage ? systemMessage.args : null}
systemMessage={systemMessage || null}
/>
{/* Skills Modal */}

View File

@@ -4,16 +4,12 @@ import { ModalBody } from "#/components/shared/modals/modal-body";
import { SystemMessageHeader } from "./system-message-modal/system-message-header";
import { TabNavigation } from "./system-message-modal/tab-navigation";
import { TabContent } from "./system-message-modal/tab-content";
import { SystemMessageForModal } from "#/utils/system-message-adapter";
interface SystemMessageModalProps {
isOpen: boolean;
onClose: () => void;
systemMessage: {
content: string;
tools: Array<Record<string, unknown>> | null;
openhands_version: string | null;
agent_class: string | null;
} | null;
systemMessage: SystemMessageForModal | null;
}
export function SystemMessageModal({

View File

@@ -1,12 +1,13 @@
import { SystemMessageContent } from "./system-message-content";
import { ToolsList } from "./tools-list";
import { EmptyToolsState } from "./empty-tools-state";
import { ChatCompletionToolParam } from "#/types/v1/core";
interface TabContentProps {
activeTab: "system" | "tools";
systemMessage: {
content: string;
tools: Array<Record<string, unknown>> | null;
tools: Array<Record<string, unknown>> | ChatCompletionToolParam[] | null;
};
expandedTools: Record<number, boolean>;
onToggleTool: (index: number) => void;

View File

@@ -1,6 +1,7 @@
import { Typography } from "#/ui/typography";
import { ToolParameters } from "./tool-parameters";
import { ToggleButton } from "./toggle-button";
import { ChatCompletionToolParam } from "#/types/v1/core";
interface FunctionData {
name?: string;
@@ -17,7 +18,7 @@ interface ToolData {
}
interface ToolItemProps {
tool: Record<string, unknown>;
tool: Record<string, unknown> | ChatCompletionToolParam;
index: number;
isExpanded: boolean;
onToggle: (index: number) => void;

View File

@@ -1,7 +1,8 @@
import { ChatCompletionToolParam } from "#/types/v1/core";
import { ToolItem } from "./tool-item";
interface ToolsListProps {
tools: Array<Record<string, unknown>>;
tools: Array<Record<string, unknown>> | ChatCompletionToolParam[];
expandedTools: Record<number, boolean>;
onToggleTool: (index: number) => void;
}

View File

@@ -214,7 +214,7 @@ export function ConversationName() {
<SystemMessageModal
isOpen={systemModalVisible}
onClose={() => setSystemModalVisible(false)}
systemMessage={systemMessage ? systemMessage.args : null}
systemMessage={systemMessage || null}
/>
{/* Skills Modal */}

View File

@@ -4,7 +4,6 @@ import { usePostHog } from "posthog-js/react";
import { useParams, useNavigate } from "react-router";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import useMetricsStore from "#/stores/metrics-store";
import { isSystemMessage, isActionOrObservation } from "#/types/core/guards";
import { ConversationStatus } from "#/types/conversation-status";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useDeleteConversation } from "./mutation/use-delete-conversation";
@@ -18,9 +17,13 @@ import {
} from "#/utils/custom-toast-handlers";
import { I18nKey } from "#/i18n/declaration";
import { useEventStore } from "#/stores/use-event-store";
import { isV0Event } from "#/types/v1/type-guards";
import { useActiveConversation } from "./query/use-active-conversation";
import { useDownloadConversation } from "./use-download-conversation";
import {
adaptSystemMessage,
SystemMessageForModal,
} from "#/utils/system-message-adapter";
interface UseConversationNameContextMenuProps {
conversationId?: string;
@@ -56,10 +59,8 @@ export function useConversationNameContextMenu({
React.useState(false);
const { mutateAsync: downloadConversation } = useDownloadConversation();
const systemMessage = events
.filter(isV0Event)
.filter(isActionOrObservation)
.find(isSystemMessage);
const systemMessage: SystemMessageForModal | null =
adaptSystemMessage(events);
const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();

View File

@@ -5,14 +5,14 @@ import { OpenHandsParsedEvent } from "#/types/core";
import { isV1Event } from "#/types/v1/type-guards";
// While we transition to v1 events, our store can handle both v0 and v1 events
type OHEvent = (OpenHandsEvent | OpenHandsParsedEvent) & {
export type OHEvent = (OpenHandsEvent | OpenHandsParsedEvent) & {
isFromPlanningAgent?: boolean;
};
const getEventId = (event: OHEvent): string | number | undefined =>
"id" in event ? event.id : undefined;
interface EventState {
export interface EventState {
events: OHEvent[];
eventIds: Set<string | number>;
uiEvents: OHEvent[];

View File

@@ -0,0 +1,47 @@
import { OHEvent } from "#/stores/use-event-store";
import { isActionOrObservation, isSystemMessage } from "#/types/core/guards";
import { ChatCompletionToolParam } from "#/types/v1/core";
import {
isSystemPromptEvent,
isV0Event,
isV1Event,
} from "#/types/v1/type-guards";
export interface SystemMessageForModal {
content: string;
tools: ChatCompletionToolParam[] | Record<string, unknown>[] | null;
openhands_version: string | null;
agent_class: string | null;
}
export function adaptSystemMessage(
events: OHEvent[],
): SystemMessageForModal | null {
let systemMessage: SystemMessageForModal | null = null;
const v0SystemMessage = events
.filter(isV0Event)
.filter(isActionOrObservation)
.find(isSystemMessage);
// V1 System Prompt Event
const v1SystemPromptEvent = events
.filter(isV1Event)
.find(isSystemPromptEvent);
if (v0SystemMessage) {
systemMessage = v0SystemMessage.args;
} else if (v1SystemPromptEvent) {
systemMessage = {
content: v1SystemPromptEvent.system_prompt.text,
tools: v1SystemPromptEvent.tools ?? null,
openhands_version: null,
agent_class: null,
};
}
if (systemMessage) {
return systemMessage;
}
return null;
}