From bcd89786a07677153affe1786f1309074372b13b Mon Sep 17 00:00:00 2001 From: Makito Date: Wed, 15 Apr 2026 02:56:04 +0900 Subject: [PATCH] fix(stage-pages,stage-shared,stage-ui): auto fit IO tracer --- .../io-tracer/components/io-tracer-chart.vue | 17 ++--- packages/stage-shared/src/perf/io-trace.ts | 1 - .../src/composables/use-io-trace-bridge.ts | 66 ++----------------- packages/stage-ui/src/stores/chat.ts | 12 ++-- .../stage-ui/src/stores/devtools/io-tracer.ts | 13 ++-- 5 files changed, 24 insertions(+), 85 deletions(-) diff --git a/packages/stage-pages/src/pages/devtools/io-tracer/components/io-tracer-chart.vue b/packages/stage-pages/src/pages/devtools/io-tracer/components/io-tracer-chart.vue index 7ebfea7e1..12a6631fe 100644 --- a/packages/stage-pages/src/pages/devtools/io-tracer/components/io-tracer-chart.vue +++ b/packages/stage-pages/src/pages/devtools/io-tracer/components/io-tracer-chart.vue @@ -466,10 +466,10 @@ function autoFit() { hasUserInteracted = false } -watch(() => props.turns, () => { - if (visibleSpans.value.length > 0 && !hasUserInteracted) +watch(() => visibleSpans.value.length, (count) => { + if (count > 0 && !hasUserInteracted) autoFit() -}, { deep: false }) +}) defineExpose({ autoFit }) @@ -491,12 +491,7 @@ function spanDuration(span: IOSpan): string { function spanLabel(span: IOSpan): string { const subsystemLabel = SUBSYSTEM_CONFIG_MAP.get(span.subsystem)?.label ?? '' - const text = span.meta.text - if (text && typeof text === 'string') { - const short = text.length > 30 ? `${text.slice(0, 30)}…` : text - return `${subsystemLabel} · ${short}` - } - return `${subsystemLabel} · ${span.name}` + return `${subsystemLabel} / ${span.name}` } @@ -818,8 +813,8 @@ function spanLabel(span: IOSpan): string {
{{ hoveredSpan.span.meta.text.length > 80 ? `${hoveredSpan.span.meta.text.slice(0, 80)}…` : hoveredSpan.span.meta.text }}
-
- chunk: {{ hoveredSpan.span.meta.reason }} +
+ chunk: {{ hoveredSpan.span.meta.chunk_reason }}
diff --git a/packages/stage-shared/src/perf/io-trace.ts b/packages/stage-shared/src/perf/io-trace.ts index 22e2ebc24..bd23edefb 100644 --- a/packages/stage-shared/src/perf/io-trace.ts +++ b/packages/stage-shared/src/perf/io-trace.ts @@ -10,7 +10,6 @@ export const IOSpanNames = { InteractionTurn: 'Interaction turn', SpeechRecognition: 'Speech recognition', LLMInference: 'LLM inference', - TTSSegment: 'TTS segment', TTSSynthesis: 'TTS synthesis', AudioPlayback: 'Audio playback', } as const diff --git a/packages/stage-ui/src/composables/use-io-trace-bridge.ts b/packages/stage-ui/src/composables/use-io-trace-bridge.ts index dd7ce896a..69c36fcc1 100644 --- a/packages/stage-ui/src/composables/use-io-trace-bridge.ts +++ b/packages/stage-ui/src/composables/use-io-trace-bridge.ts @@ -2,66 +2,29 @@ import type { Span } from '@opentelemetry/api' import type { createSpeechPipeline } from '@proj-airi/pipelines-audio' import { IOAttributes, IOSpanNames, IOSubsystems } from '@proj-airi/stage-shared' -import { onScopeDispose, watch } from 'vue' +import { onScopeDispose } from 'vue' import { activeTurnSpan, startSpan } from './use-io-tracer' export function useIOTraceBridge(pipeline: ReturnType) { const cleanupFns: (() => void)[] = [] - const segmentSpans = new Map() const synthesisSpans = new Map() const playbackSpans = new Map() const segmentReasons = new Map() - let currentParent: Span | undefined - let hadSegments = false - - const stopWatch = watch(activeTurnSpan, (newSpan) => { - if (newSpan) { - currentParent = newSpan - hadSegments = false - } - }, { immediate: true }) - - function tryCloseTurn() { - if (hadSegments && segmentSpans.size === 0) { - activeTurnSpan.value?.end() - activeTurnSpan.value = undefined - } - } - - function closeSegment(segmentId: string) { - const segSpan = segmentSpans.get(segmentId) - if (segSpan) { - segSpan.end() - segmentSpans.delete(segmentId) - } - } - cleanupFns.push(pipeline.on('onSegment', (segment) => { segmentReasons.set(segment.segmentId, segment.reason) })) cleanupFns.push(pipeline.on('onTtsRequest', (request) => { - let ttsSegmentSpan = segmentSpans.get(request.segmentId) - if (!ttsSegmentSpan) { - hadSegments = true - ttsSegmentSpan = startSpan(IOSpanNames.TTSSegment, currentParent, { - [IOAttributes.Subsystem]: IOSubsystems.TTS, - [IOAttributes.TTSSegmentId]: request.segmentId, - [IOAttributes.TTSText]: request.text, - [IOAttributes.TTSChunkReason]: segmentReasons.get(request.segmentId) ?? '', - }) - segmentReasons.delete(request.segmentId) - segmentSpans.set(request.segmentId, ttsSegmentSpan) - } - - const ttsSynthesisSpan = startSpan(IOSpanNames.TTSSynthesis, ttsSegmentSpan, { + const ttsSynthesisSpan = startSpan(IOSpanNames.TTSSynthesis, activeTurnSpan.value, { [IOAttributes.Subsystem]: IOSubsystems.TTS, [IOAttributes.TTSSegmentId]: request.segmentId, [IOAttributes.TTSText]: request.text, + [IOAttributes.TTSChunkReason]: segmentReasons.get(request.segmentId) ?? '', }) + segmentReasons.delete(request.segmentId) synthesisSpans.set(request.segmentId, ttsSynthesisSpan) })) @@ -74,8 +37,7 @@ export function useIOTraceBridge(pipeline: ReturnType { - const segSpan = segmentSpans.get(event.item.segmentId) - const playbackSpan = startSpan(IOSpanNames.AudioPlayback, segSpan, { + const playbackSpan = startSpan(IOSpanNames.AudioPlayback, activeTurnSpan.value, { [IOAttributes.Subsystem]: IOSubsystems.Playback, [IOAttributes.TTSSegmentId]: event.item.segmentId, [IOAttributes.TTSText]: event.item.text, @@ -89,8 +51,6 @@ export function useIOTraceBridge(pipeline: ReturnType { @@ -101,34 +61,22 @@ export function useIOTraceBridge(pipeline: ReturnType { - closeSegment(event.item.segmentId) - tryCloseTurn() })) cleanupFns.push(pipeline.on('onIntentCancel', () => { - for (const [segmentId, span] of segmentSpans) { - span.setAttribute(IOAttributes.TTSCanceled, true) - span.end() - segmentSpans.delete(segmentId) - } for (const [segmentId, span] of synthesisSpans) { + span.setAttribute(IOAttributes.TTSCanceled, true) span.end() synthesisSpans.delete(segmentId) } for (const [segmentId, span] of playbackSpans) { + span.setAttribute(IOAttributes.TTSCanceled, true) span.end() playbackSpans.delete(segmentId) } - tryCloseTurn() })) onScopeDispose(() => { - stopWatch() for (const cleanup of cleanupFns) cleanup() }) diff --git a/packages/stage-ui/src/stores/chat.ts b/packages/stage-ui/src/stores/chat.ts index 5c0f8def2..98eb523d4 100644 --- a/packages/stage-ui/src/stores/chat.ts +++ b/packages/stage-ui/src/stores/chat.ts @@ -449,23 +449,19 @@ export const useChatOrchestratorStore = defineStore('chat-orchestrator', () => { toolCalls: sessionMessagesForSend.filter(msg => msg.role === 'tool') as ToolMessage[], }, streamingMessageContext) - // TODO: Close turn span for zero-segment responses (tool-only, empty). - // Currently the bridge's tryCloseTurn() only fires after TTS playback, - // so turns without segments stay open until the next interaction. - if (isForegroundSession()) { streamingMessage.value = { role: 'assistant', content: '', slices: [], tool_results: [] } } } catch (error) { - if (!hadExistingTurn && activeTurnSpan.value) { - activeTurnSpan.value.end() - activeTurnSpan.value = undefined - } console.error('Error sending message:', error) throw error } finally { + if (!hadExistingTurn && activeTurnSpan.value) { + activeTurnSpan.value.end() + activeTurnSpan.value = undefined + } sending.value = false } } diff --git a/packages/stage-ui/src/stores/devtools/io-tracer.ts b/packages/stage-ui/src/stores/devtools/io-tracer.ts index e8334bdd8..75cab71e2 100644 --- a/packages/stage-ui/src/stores/devtools/io-tracer.ts +++ b/packages/stage-ui/src/stores/devtools/io-tracer.ts @@ -111,16 +111,17 @@ export const useIOTracerStore = defineStore('devtools:io-tracer', () => { const subsystem = readable.attributes[IOAttributes.Subsystem] as IOSubsystem | undefined if (!subsystem) { - if (readable.name === IOSpanNames.TTSSegment) { - const turn = getOrCreateTurn() - const text = readable.attributes[IOAttributes.TTSText] - if (typeof text === 'string' && !turn.outputText) - turn.outputText = text - } notifyUpdate() return } + if (readable.name === IOSpanNames.TTSSynthesis) { + const turn = getOrCreateTurn() + const text = readable.attributes[IOAttributes.TTSText] + if (typeof text === 'string' && !turn.outputText) + turn.outputText = text + } + const turn = getOrCreateTurn() const meta = attrsToMeta(readable.attributes)