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)