feat: show model persona and tool counts on Agent headings

This commit is contained in:
Soulter
2026-10-15 17:03:23 +08:00
parent debd123d90
commit 92cb1d9df3
9 changed files with 74 additions and 1 deletions
+2
View File
@@ -1,5 +1,7 @@
# Unreleased
- Add model and persona ID chips to Agent execution headings, show provider IDs on model hover, and count actual tool calls separately for each Agent while folded or expanded.
- Record successful send_message_to_user deliveries as response events on the originating trace, including message-chain outlines and destination UMO. Do not record failed sends as delivered responses; leave cron Agent instrumentation unchanged.
- Present trace-detail UMO and sender on separate icon-and-value rows instead of a dot-separated line, preserving UMO alias tooltips.
@@ -246,10 +246,27 @@ const timelineGroups = computed(() => {
const timelineRows = computed(() => {
const rows = [];
const agentErrors = new Map();
const agentMetadata = new Map();
for (const group of timelineGroups.value) {
for (const id of group.ancestors) {
agentErrors.set(id, (agentErrors.get(id) || 0) + group.records.filter((record) => record.fields.status === "error").length);
}
// Attribute metadata to the nearest Agent, including through plugin/tool spans.
const owner = group.ancestors.at(-1);
if (!owner) continue;
if (!agentMetadata.has(owner)) agentMetadata.set(owner, { models: [], personas: [], toolCalls: 0 });
const metadata = agentMetadata.get(owner);
for (const record of group.records) {
if (record.action === "astr_agent_prepare") {
const provider = record.fields.chat_provider;
if (provider?.model && !metadata.models.some((model) => model.id === provider.model && model.provider === provider.id)) {
metadata.models.push({ id: provider.model, provider: provider.id || "" });
}
const persona = record.fields.persona_id;
if (persona && !metadata.personas.includes(persona)) metadata.personas.push(persona);
}
if (record.action === "agent_tool_call") metadata.toolCalls += 1;
}
}
for (const group of timelineGroups.value) {
if (group.ancestors.some((id) => !expandedAgents.has(id))) continue;
@@ -258,7 +275,8 @@ const timelineRows = computed(() => {
if (group.records.length === 1) {
const agentId = group.agentId;
rows.push({ key: group.key, record: group.records[0], category: group.category, depth,
agentId, expanded: agentId ? expandedAgents.has(agentId) : detailsExpanded,
agentId, agentInfo: agentId ? agentMetadata.get(agentId) : null,
expanded: agentId ? expandedAgents.has(agentId) : detailsExpanded,
detailsExpanded, errors: agentId ? (agentErrors.get(agentId) || 0) + Number(group.records[0].fields.status === "error") : 0 });
continue;
}
@@ -1021,6 +1039,17 @@ onBeforeUnmount(() => {
: row.record.action === 'agent_start' ? tm('compact.agentExecution')
: row.record.action === 'event_received' ? tm('actions.eventReceived')
: recordLabel(row.record, Boolean(row.group)) }}</strong>
<template v-if="row.agentInfo">
<v-tooltip v-for="model in row.agentInfo.models" :key="model.id + ':' + model.provider" location="top" content-class="trace-tooltip">
<template #activator="{ props }">
<v-chip v-bind="props" class="timeline-metadata-chip" size="x-small" variant="tonal">{{ model.id }}</v-chip>
</template>
<div>{{ model.id }}</div>
<div v-if="model.provider">{{ tm("chips.provider", { id: model.provider }) }}</div>
</v-tooltip>
<v-chip v-for="persona in row.agentInfo.personas" :key="persona" class="timeline-metadata-chip" size="x-small" variant="tonal" :title="tm('chips.persona', { id: persona })">{{ persona }}</v-chip>
<v-chip v-if="row.agentInfo.toolCalls" size="x-small" variant="tonal">{{ tm("chips.toolCalls", { count: row.agentInfo.toolCalls }) }}</v-chip>
</template>
<v-chip v-if="row.category" size="x-small" variant="tonal">{{ tm(`handlerCategories.${row.category}`) }}</v-chip>
<span v-if="row.group" class="timeline-group-count">{{ tm("groups.count", { count: row.group.records.length }) }}</span>
<span v-if="row.record.action === 'agent_start'" class="timeline-duration">{{ formatDuration(row.record.completed ? row.record.fields.duration_ms || 0 : Math.max(0, (clockNow - row.record.time) * 1000)) }}</span>
@@ -1108,6 +1137,7 @@ onBeforeUnmount(() => {
.timeline-group-errors { color: rgb(var(--v-theme-error)); font-size: 11px; white-space: nowrap; }
.timeline-group-summary { align-items: center; display: flex; flex-wrap: wrap; gap: 6px 10px; }
.timeline-metadata-chip { max-width: 100%; vertical-align: middle; }
.timeline-title .timeline-metadata-chip { max-width: 200px; }
.timeline-metadata-chip :deep(.v-chip__content) { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.timeline-expansion-icon { color: var(--trace-muted); transition: transform 0.15s ease; }
.timeline-expansion-icon.is-collapsed { transform: rotate(-90deg); }
@@ -86,6 +86,9 @@
"spansHint": "Actual spans including the message root, excluding events. Collapsing does not change the count."
},
"chips": {
"provider": "Provider: {id}",
"persona": "Persona: {id}",
"toolCalls": "{count} tool calls",
"executionMode": "Execution mode: {name}",
"systemSender": "Sent by system",
"pluginSender": "Sent by plugin: {name}",
@@ -86,6 +86,9 @@
"spansHint": "メッセージのルートを含む実際の Span 数。イベントは含まず、折りたたんでも変わりません。"
},
"chips": {
"provider": "プロバイダー: {id}",
"persona": "ペルソナ: {id}",
"toolCalls": "ツール呼び出し {count} 回",
"executionMode": "実行方式: {name}",
"systemSender": "システムから送信",
"pluginSender": "プラグインから送信: {name}",
@@ -86,6 +86,9 @@
"spansHint": "Число Span, включая корневой Span сообщения, без событий. Сворачивание не меняет число."
},
"chips": {
"provider": "Провайдер: {id}",
"persona": "Персона: {id}",
"toolCalls": "Вызовов инструментов: {count}",
"executionMode": "Режим выполнения: {name}",
"systemSender": "Отправлено системой",
"pluginSender": "Отправлено плагином: {name}",
@@ -86,6 +86,9 @@
"spansHint": "实际 Span 数(含消息根 Span),不包含事件;折叠不影响计数。"
},
"chips": {
"provider": "提供商:{id}",
"persona": "人格:{id}",
"toolCalls": "{count} 次工具调用",
"executionMode": "执行方式:{name}",
"systemSender": "系统发送",
"pluginSender": "插件发送:{name}",
@@ -64,6 +64,31 @@ function handler(key, category = "listener", fields = {}) {
};
}
test("Agent title metadata remains visible when folded and stays isolated per Agent", (t) => {
const state = setup(t, [
{ key: "outer", action: "agent_start", operation_span_id: "outer", parent_span_id: "root", fields: {} },
{ key: "prepare", action: "astr_agent_prepare", owner_span_id: "outer", fields: { chat_provider: { id: "provider-a", model: "model-a" }, persona_id: "persona-a", tools: ["unused", "search"] } },
{ key: "prepare-again", action: "astr_agent_prepare", owner_span_id: "outer", fields: { chat_provider: { id: "provider-a", model: "model-a" }, persona_id: "persona-a" } },
{ key: "call", action: "agent_tool_call", operation_span_id: "tool", parent_span_id: "outer", fields: {} },
{ key: "result", action: "agent_tool_result", owner_span_id: "tool", fields: {} },
{ key: "inner", action: "agent_start", operation_span_id: "inner", parent_span_id: "outer", fields: {} },
{ key: "inner-prepare", action: "astr_agent_prepare", owner_span_id: "inner", fields: { chat_provider: { id: "provider-b", model: "model-b" }, persona_id: "persona-b" } },
{ key: "inner-tool", action: "agent_tool_call", operation_span_id: "inner-tool", parent_span_id: "inner", fields: {} },
{ key: "parallel", action: "agent_start", operation_span_id: "parallel", parent_span_id: "root", fields: {} },
]);
const outer = state.timelineRows.value.find((row) => row.agentId === "outer");
assert.equal(outer.expanded, false);
assert.deepEqual(JSON.parse(JSON.stringify(outer.agentInfo)), {
models: [{ id: "model-a", provider: "provider-a" }], personas: ["persona-a"], toolCalls: 1,
});
assert.equal(state.timelineRows.value.find((row) => row.agentId === "parallel").agentInfo, undefined);
state.expandedAgents.add("outer");
const inner = state.timelineRows.value.find((row) => row.agentId === "inner");
assert.equal(inner.agentInfo.models[0].id, "model-b");
assert.equal(inner.agentInfo.personas[0], "persona-b");
assert.equal(inner.agentInfo.toolCalls, 1);
});
test("collapse adjacent handlers by category without reordering separated records", (t) => {
const state = setup(t, [
{ key: "received", action: "event_received", fields: { platform_id: "qq" } },
+2
View File
@@ -172,6 +172,8 @@ The toolbar contains search, fullscreen, font-size controls, and `Install Pip pa
### Trace
The Agent execution heading shows chips for recorded model and persona IDs, plus the number of actual tool calls when nonzero. Hover over a model chip to see its provider ID. These remain visible when the execution chain is collapsed; nested and parallel Agents are counted separately, and unavailable metadata is omitted.
Below the detail title, UMO and sender appear on separate icon-and-value rows. UMO aliases retain their hover tooltip showing the full identifier.
Trace text and tooltips use the system UI font rather than Outfit. Running status is blue; completed and error statuses remain green and red.
+2
View File
@@ -174,6 +174,8 @@ ChatUI 支持以下常用能力:
包含 Agent 调用时,详情统计区增加无边框的 Tokens 卡片,展示本轮输入(含缓存输入)与输出的累计总量,以两位小数的 k/M 格式显示,悬浮提示保留精确数量;悬停或键盘聚焦可查看输入、输出及缓存命中明细。同一 Agent 的累计快照只取最新一份,多个 Agent 分别累计。没有已上报用量时显示“—”,不按 0 处理。
“Agent 执行”标题后显示已记录的模型 ID、人格 ID,以及非零的实际工具调用次数。模型标签悬停可查看提供商 ID,执行链折叠后标签仍可见;嵌套或并行 Agent 分别统计,缺失的信息不显示。
详情标题下的 UMO 和发送人分别以“图标 + 值”独立成行,不再使用点分隔;UMO 别名仍可悬停查看完整标识。
追踪区域和悬停提示使用系统默认字体,不使用 Outfit。运行中状态为蓝色,已完成和错误仍分别为绿色和红色。