Skip to content

Commit f1fb2f9

Browse files
committed
feat(agents): Improve AI span presentation with tool input preview and response model
For tool spans, parse the tool input parameters and display the first key's value next to the tool name (e.g. "search: weather in NYC"). For AI client spans, display the response model (e.g. "gpt-4o") instead of generic "chat"/"request", falling back to the original behavior. https://claude.ai/code/session_0113Szysfr5Bf4ZQZ59nkxgJ
1 parent b4ec6d9 commit f1fb2f9

File tree

5 files changed

+95
-3
lines changed

5 files changed

+95
-3
lines changed

static/app/views/insights/pages/agents/components/aiSpanList.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {t} from 'sentry/locale';
1313
import {getDuration} from 'sentry/utils/duration/getDuration';
1414
import {LLMCosts} from 'sentry/views/insights/pages/agents/components/llmCosts';
1515
import {
16+
getFirstToolInputValue,
1617
getGenAiOpType,
1718
getIsAiAgentNode,
1819
getNumberAttr,
@@ -475,6 +476,7 @@ function getSpanPresentation(
475476
case GenAiOperationType.AI_CLIENT: {
476477
const tokens = getNumberAttr(node, SpanFields.GEN_AI_USAGE_TOTAL_TOKENS);
477478
const cost = getNumberAttr(node, SpanFields.GEN_AI_COST_TOTAL_TOKENS);
479+
const responseModel = getStringAttr(node, SpanFields.GEN_AI_RESPONSE_MODEL);
478480
const tokenLabel = tokens ? (
479481
<Fragment>
480482
<Count value={tokens} />
@@ -484,7 +486,7 @@ function getSpanPresentation(
484486
return {
485487
icon: <IconChat size="md" />,
486488
color,
487-
title: description || op,
489+
title: responseModel || description || op,
488490
subtitle:
489491
tokenLabel && cost ? (
490492
<Fragment>
@@ -497,11 +499,12 @@ function getSpanPresentation(
497499
}
498500
case GenAiOperationType.TOOL: {
499501
const toolName = getStringAttr(node, SpanFields.GEN_AI_TOOL_NAME);
502+
const firstInputValue = getFirstToolInputValue(node);
500503
return {
501504
icon: <IconFix size="md" />,
502505
color,
503506
title: toolName || op,
504-
subtitle: toolName ? op : '',
507+
subtitle: firstInputValue || (toolName ? op : ''),
505508
};
506509
}
507510
case GenAiOperationType.HANDOFF:

static/app/views/insights/pages/agents/hooks/useAITrace.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const AI_TRACE_BASE_ATTRIBUTES = [
3535
SpanFields.GEN_AI_OPERATION_NAME,
3636
SpanFields.SPAN_STATUS,
3737
'status',
38+
'gen_ai.tool.call.arguments',
39+
'gen_ai.tool.input',
3840
];
3941

4042
export function useAITrace(traceSlug: string, timestamp?: number): UseAITraceResult {

static/app/views/insights/pages/agents/utils/aiTraceNodes.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,40 @@ export function getNumberAttr(node: AITraceSpanNode, field: string): number | un
139139
return undefined;
140140
}
141141

142+
const MAX_TOOL_INPUT_PREVIEW_LENGTH = 80;
143+
144+
/**
145+
* Parses tool input JSON and returns the value of the first key.
146+
* Used to show a preview of the tool input next to the tool name.
147+
*/
148+
export function getFirstToolInputValue(node: AITraceSpanNode): string | undefined {
149+
const toolInput =
150+
getStringAttr(node, 'gen_ai.tool.call.arguments') ||
151+
getStringAttr(node, 'gen_ai.tool.input');
152+
if (!toolInput) {
153+
return undefined;
154+
}
155+
156+
try {
157+
const parsed = JSON.parse(toolInput);
158+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
159+
const firstKey = Object.keys(parsed)[0];
160+
if (firstKey !== undefined) {
161+
const value = parsed[firstKey];
162+
const str = typeof value === 'string' ? value : JSON.stringify(value);
163+
if (str.length > MAX_TOOL_INPUT_PREVIEW_LENGTH) {
164+
return str.slice(0, MAX_TOOL_INPUT_PREVIEW_LENGTH) + '\u2026';
165+
}
166+
return str;
167+
}
168+
}
169+
} catch {
170+
// Invalid JSON, return undefined
171+
}
172+
173+
return undefined;
174+
}
175+
142176
export function hasError(node: AITraceSpanNode): boolean {
143177
if (node.errors.size > 0) {
144178
return true;

static/app/views/insights/pages/conversations/hooks/useConversation.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ interface ConversationApiSpan {
4545
'gen_ai.response.model'?: string;
4646
'gen_ai.response.object'?: string;
4747
'gen_ai.response.text'?: string;
48+
'gen_ai.tool.call.arguments'?: string;
49+
'gen_ai.tool.input'?: string;
4850
'gen_ai.tool.name'?: string;
4951
'gen_ai.usage.total_tokens'?: number;
5052
'span.name'?: string;
@@ -114,6 +116,8 @@ function createNodeFromApiSpan(
114116
[SpanFields.GEN_AI_RESPONSE_MODEL]: apiSpan['gen_ai.response.model'] ?? '',
115117
[SpanFields.GEN_AI_AGENT_NAME]: apiSpan['gen_ai.agent.name'] ?? '',
116118
[SpanFields.GEN_AI_TOOL_NAME]: apiSpan['gen_ai.tool.name'] ?? '',
119+
'gen_ai.tool.call.arguments': apiSpan['gen_ai.tool.call.arguments'] ?? '',
120+
'gen_ai.tool.input': apiSpan['gen_ai.tool.input'] ?? '',
117121
[SpanFields.GEN_AI_USAGE_TOTAL_TOKENS]: apiSpan['gen_ai.usage.total_tokens'] ?? 0,
118122
[SpanFields.GEN_AI_COST_TOTAL_TOKENS]: apiSpan['gen_ai.cost.total_tokens'] ?? 0,
119123
[SpanFields.SPAN_STATUS]: apiSpan['span.status'],

static/app/views/performance/newTraceDetails/traceRow/traceEAPSpanRow.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import React from 'react';
22
import {PlatformIcon} from 'platformicons';
33

44
import {ellipsize} from 'sentry/utils/string/ellipsize';
5+
import {
6+
getFirstToolInputValue,
7+
getStringAttr,
8+
} from 'sentry/views/insights/pages/agents/utils/aiTraceNodes';
9+
import {
10+
GenAiOperationType,
11+
getGenAiOperationTypeFromSpanOp,
12+
} from 'sentry/views/insights/pages/agents/utils/query';
13+
import {SpanFields} from 'sentry/views/insights/types';
514
import {TraceIcons} from 'sentry/views/performance/newTraceDetails/traceIcons';
615
import type {EapSpanNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode/eapSpanNode';
716
import {TraceBar} from 'sentry/views/performance/newTraceDetails/traceRow/traceBar';
@@ -13,6 +22,43 @@ import {
1322
type TraceRowProps,
1423
} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow';
1524

25+
/**
26+
* Returns an enriched description for AI spans when attributes are available.
27+
* - Tool spans: "toolName: firstInputValue"
28+
* - AI client spans: responseModel (e.g., "gpt-4o")
29+
* Falls back to undefined so the caller can use the default description.
30+
*/
31+
function getAIEnhancedDescription(node: EapSpanNode): string | undefined {
32+
const attrs = node.attributes;
33+
if (!attrs) {
34+
return undefined;
35+
}
36+
37+
const opType =
38+
(attrs[SpanFields.GEN_AI_OPERATION_TYPE] as string | undefined) ??
39+
getGenAiOperationTypeFromSpanOp(node.op);
40+
41+
if (!opType) {
42+
return undefined;
43+
}
44+
45+
if (opType === GenAiOperationType.TOOL) {
46+
const toolName = getStringAttr(node as any, SpanFields.GEN_AI_TOOL_NAME);
47+
const firstValue = getFirstToolInputValue(node as any);
48+
if (toolName && firstValue) {
49+
return `${toolName}: ${firstValue}`;
50+
}
51+
return toolName ?? undefined;
52+
}
53+
54+
if (opType === GenAiOperationType.AI_CLIENT) {
55+
const responseModel = getStringAttr(node as any, SpanFields.GEN_AI_RESPONSE_MODEL);
56+
return responseModel ?? undefined;
57+
}
58+
59+
return undefined;
60+
}
61+
1662
export function TraceEAPSpanRow(props: TraceRowProps<EapSpanNode>) {
1763
const spanId = props.node.id;
1864

@@ -22,7 +68,10 @@ export function TraceEAPSpanRow(props: TraceRowProps<EapSpanNode>) {
2268
<PlatformIcon platform={props.projects[props.node.projectSlug ?? ''] ?? 'default'} />
2369
);
2470

25-
const description = props.node.description || props.node.value.name;
71+
const description =
72+
getAIEnhancedDescription(props.node) ||
73+
props.node.description ||
74+
props.node.value.name;
2675

2776
return (
2877
<div

0 commit comments

Comments
 (0)