feat(xai): surface hosted X Search activity (#5050)
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
export function WebSearchRun({ run, turnActive }: { run: WebSearchRunModel; turnActive: boolean }) {
|
||||
const active = run.status === "running" && turnActive;
|
||||
const status = run.status === "running" && !turnActive ? "done" : run.status;
|
||||
const label = presentWebSearchAction(run.query, status);
|
||||
const label = presentWebSearchAction(run.query, status, run.target);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -77,6 +77,7 @@ const EXCLUDED_TOOLS = new Set([
|
||||
"terminal",
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"x_search",
|
||||
"write_file",
|
||||
]);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export function describeTraceLine(
|
||||
const query = traceFieldFromArgs(args, ["query", "q", "text"]) || args || trimmed;
|
||||
return {
|
||||
kind: "search",
|
||||
label: presentWebSearchAction(query, status),
|
||||
label: presentWebSearchAction(query, status, name === "x_search" ? "x" : "web"),
|
||||
detail: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { redactActivityText, safeActivityDetail } from "./activity-text";
|
||||
import { displayWebHost, formatCompactWebUrl, parseSafeActivityHttpUrl } from "./web-url";
|
||||
|
||||
export type WebSearchStatus = "running" | "done" | "error";
|
||||
export type WebSearchTarget = "web" | "x";
|
||||
|
||||
export interface WebSearchSource {
|
||||
title: string;
|
||||
@@ -16,6 +17,7 @@ export interface WebSearchSource {
|
||||
export interface WebSearchRunModel {
|
||||
key: string;
|
||||
query: string;
|
||||
target: WebSearchTarget;
|
||||
status: WebSearchStatus;
|
||||
sources: WebSearchSource[];
|
||||
error?: string;
|
||||
@@ -49,10 +51,11 @@ export function webSearchRunsByTraceLine(
|
||||
|
||||
function webSearchRunFromEvent(event: ToolProgressEvent): WebSearchRunModel | null {
|
||||
const name = compactToolName(toolEventName(event));
|
||||
if (name !== "web_search") return null;
|
||||
if (name !== "web_search" && name !== "x_search") return null;
|
||||
|
||||
const args = toolEventArguments(event);
|
||||
const query = stringField(args, ["query", "q", "text"]);
|
||||
const target: WebSearchTarget = name === "x_search" ? "x" : "web";
|
||||
const status: WebSearchStatus = event.phase === "error"
|
||||
? "error"
|
||||
: event.phase === "end"
|
||||
@@ -60,10 +63,11 @@ function webSearchRunFromEvent(event: ToolProgressEvent): WebSearchRunModel | nu
|
||||
: "running";
|
||||
|
||||
return {
|
||||
key: event.call_id ? `call:${event.call_id}` : formatToolCallTrace(event) ?? `web_search:${query}`,
|
||||
key: event.call_id ? `call:${event.call_id}` : formatToolCallTrace(event) ?? `${name}:${query}`,
|
||||
query,
|
||||
target,
|
||||
status,
|
||||
sources: status === "done" ? webSearchSources(event.result) : [],
|
||||
sources: status === "done" && target === "web" ? webSearchSources(event.result) : [],
|
||||
error: status === "error" ? readableError(event.error) : undefined,
|
||||
};
|
||||
}
|
||||
@@ -89,6 +93,7 @@ function presentWebSearchQuery(query: string): WebSearchQueryPresentation {
|
||||
export function presentWebSearchAction(
|
||||
query: string,
|
||||
status: WebSearchStatus,
|
||||
target: WebSearchTarget = "web",
|
||||
): string {
|
||||
const presentation = presentWebSearchQuery(query);
|
||||
const verb = status === "error"
|
||||
@@ -96,8 +101,11 @@ export function presentWebSearchAction(
|
||||
: status === "running"
|
||||
? "Searching"
|
||||
: "Searched";
|
||||
const target = [presentation.scope, presentation.query].filter(Boolean).join(" · ");
|
||||
return target ? `${verb} ${target}` : verb;
|
||||
const queryTarget = [presentation.scope, presentation.query].filter(Boolean).join(" · ");
|
||||
if (target === "x") {
|
||||
return queryTarget ? `${verb} X · ${queryTarget}` : `${verb} X`;
|
||||
}
|
||||
return queryTarget ? `${verb} ${queryTarget}` : verb;
|
||||
}
|
||||
|
||||
function mergeWebSearchRun(
|
||||
|
||||
@@ -961,6 +961,35 @@ describe("AgentActivityCluster", () => {
|
||||
expect(screen.getAllByTestId("activity-step")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("renders hosted X search as an explicit search activity", () => {
|
||||
const line = 'x_search({"query":"nanobot oauth"})';
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
id: "t-x-search",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: line,
|
||||
traces: [line],
|
||||
toolEvents: [{
|
||||
phase: "end",
|
||||
call_id: "x-search-1",
|
||||
name: "x_search",
|
||||
arguments: { query: "nanobot oauth" },
|
||||
result: { name: "x_semantic_search" },
|
||||
}],
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isTurnStreaming={false}
|
||||
hasBodyBelow={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Searched X · nanobot oauth")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Completed X search/i)).not.toBeInTheDocument();
|
||||
expect(screen.getAllByTestId("activity-step")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("redacts credentials from web search queries, titles, and links", () => {
|
||||
const query = "release notes access_token=signed-secret";
|
||||
const line = `web_search(${JSON.stringify({ query })})`;
|
||||
|
||||
@@ -32,6 +32,14 @@ describe("trace activity semantics", () => {
|
||||
expect(describeTrace('web_search({"query":"status test"})', status).label).toBe(label);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["running", "Searching X · status test"],
|
||||
["done", "Searched X · status test"],
|
||||
["error", "Could not search X · status test"],
|
||||
] as const)("identifies hosted X search activity for %s", (status, label) => {
|
||||
expect(describeTrace('x_search({"query":"status test"})', status).label).toBe(label);
|
||||
});
|
||||
|
||||
it("never exposes URL credentials, query secrets, or private-network links", () => {
|
||||
const publicResult = describeTrace(
|
||||
'web_fetch({"url":"https://user:password@example.com/docs?api_key=secret#section"})',
|
||||
|
||||
Reference in New Issue
Block a user