diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md index 2d1b497b5..328733c90 100644 --- a/changelogs/unreleased.md +++ b/changelogs/unreleased.md @@ -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. diff --git a/dashboard/src/components/shared/TraceDisplayer.vue b/dashboard/src/components/shared/TraceDisplayer.vue index 4990e6a82..b903c8dac 100644 --- a/dashboard/src/components/shared/TraceDisplayer.vue +++ b/dashboard/src/components/shared/TraceDisplayer.vue @@ -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)) }} + + + + {{ model.id }} + + {{ model.id }} + {{ tm("chips.provider", { id: model.provider }) }} + + {{ persona }} + {{ tm("chips.toolCalls", { count: row.agentInfo.toolCalls }) }} + {{ tm(`handlerCategories.${row.category}`) }} {{ tm("groups.count", { count: row.group.records.length }) }} {{ formatDuration(row.record.completed ? row.record.fields.duration_ms || 0 : Math.max(0, (clockNow - row.record.time) * 1000)) }} @@ -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); } diff --git a/dashboard/src/i18n/locales/en-US/features/trace.json b/dashboard/src/i18n/locales/en-US/features/trace.json index aabc3615f..31aab76a6 100644 --- a/dashboard/src/i18n/locales/en-US/features/trace.json +++ b/dashboard/src/i18n/locales/en-US/features/trace.json @@ -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}", diff --git a/dashboard/src/i18n/locales/ja-JP/features/trace.json b/dashboard/src/i18n/locales/ja-JP/features/trace.json index 6c376c7a2..fa5fc5991 100644 --- a/dashboard/src/i18n/locales/ja-JP/features/trace.json +++ b/dashboard/src/i18n/locales/ja-JP/features/trace.json @@ -86,6 +86,9 @@ "spansHint": "メッセージのルートを含む実際の Span 数。イベントは含まず、折りたたんでも変わりません。" }, "chips": { + "provider": "プロバイダー: {id}", + "persona": "ペルソナ: {id}", + "toolCalls": "ツール呼び出し {count} 回", "executionMode": "実行方式: {name}", "systemSender": "システムから送信", "pluginSender": "プラグインから送信: {name}", diff --git a/dashboard/src/i18n/locales/ru-RU/features/trace.json b/dashboard/src/i18n/locales/ru-RU/features/trace.json index 912358672..28d943f0c 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/trace.json +++ b/dashboard/src/i18n/locales/ru-RU/features/trace.json @@ -86,6 +86,9 @@ "spansHint": "Число Span, включая корневой Span сообщения, без событий. Сворачивание не меняет число." }, "chips": { + "provider": "Провайдер: {id}", + "persona": "Персона: {id}", + "toolCalls": "Вызовов инструментов: {count}", "executionMode": "Режим выполнения: {name}", "systemSender": "Отправлено системой", "pluginSender": "Отправлено плагином: {name}", diff --git a/dashboard/src/i18n/locales/zh-CN/features/trace.json b/dashboard/src/i18n/locales/zh-CN/features/trace.json index 643cf5e5a..8fc4846d2 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/trace.json +++ b/dashboard/src/i18n/locales/zh-CN/features/trace.json @@ -86,6 +86,9 @@ "spansHint": "实际 Span 数(含消息根 Span),不包含事件;折叠不影响计数。" }, "chips": { + "provider": "提供商:{id}", + "persona": "人格:{id}", + "toolCalls": "{count} 次工具调用", "executionMode": "执行方式:{name}", "systemSender": "系统发送", "pluginSender": "插件发送:{name}", diff --git a/dashboard/tests/traceTimelineGroups.test.mjs b/dashboard/tests/traceTimelineGroups.test.mjs index 35ba121ee..5a18e9ce5 100644 --- a/dashboard/tests/traceTimelineGroups.test.mjs +++ b/dashboard/tests/traceTimelineGroups.test.mjs @@ -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" } }, diff --git a/docs/en/use/webui.md b/docs/en/use/webui.md index 0ecb9f963..e17c23239 100644 --- a/docs/en/use/webui.md +++ b/docs/en/use/webui.md @@ -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. diff --git a/docs/zh/use/webui.md b/docs/zh/use/webui.md index e3b6ebfd6..4126c0a5d 100644 --- a/docs/zh/use/webui.md +++ b/docs/zh/use/webui.md @@ -174,6 +174,8 @@ ChatUI 支持以下常用能力: 包含 Agent 调用时,详情统计区增加无边框的 Tokens 卡片,展示本轮输入(含缓存输入)与输出的累计总量,以两位小数的 k/M 格式显示,悬浮提示保留精确数量;悬停或键盘聚焦可查看输入、输出及缓存命中明细。同一 Agent 的累计快照只取最新一份,多个 Agent 分别累计。没有已上报用量时显示“—”,不按 0 处理。 +“Agent 执行”标题后显示已记录的模型 ID、人格 ID,以及非零的实际工具调用次数。模型标签悬停可查看提供商 ID,执行链折叠后标签仍可见;嵌套或并行 Agent 分别统计,缺失的信息不显示。 + 详情标题下的 UMO 和发送人分别以“图标 + 值”独立成行,不再使用点分隔;UMO 别名仍可悬停查看完整标识。 追踪区域和悬停提示使用系统默认字体,不使用 Outfit。运行中状态为蓝色,已完成和错误仍分别为绿色和红色。