diff --git a/packages/stage-pages/src/pages/devtools/io-tracer/io-tracer-types.ts b/packages/stage-pages/src/pages/devtools/io-tracer/io-tracer-types.ts
index 683fdc575..fd3072a62 100644
--- a/packages/stage-pages/src/pages/devtools/io-tracer/io-tracer-types.ts
+++ b/packages/stage-pages/src/pages/devtools/io-tracer/io-tracer-types.ts
@@ -13,6 +13,7 @@ export interface SubsystemConfig {
export const SUBSYSTEM_CONFIGS: SubsystemConfig[] = [
{ subsystem: IOSubsystems.ASR, label: 'ASR', color: '#3b82f6', bgColor: '#3b82f618', icon: 'i-lucide:mic' },
{ subsystem: IOSubsystems.LLM, label: 'LLM', color: '#a855f7', bgColor: '#a855f718', icon: 'i-lucide:brain' },
+ { subsystem: IOSubsystems.StreamingControl, label: 'Streaming Control', color: '#06b6d4', bgColor: '#06b6d418', icon: 'i-lucide:radio-tower' },
{ subsystem: IOSubsystems.TTS, label: 'TTS', color: '#22c55e', bgColor: '#22c55e18', icon: 'i-lucide:audio-lines' },
{ subsystem: IOSubsystems.Playback, label: 'Playback', color: '#f87171', bgColor: '#f8717118', icon: 'i-lucide:play' },
]
diff --git a/packages/stage-shared/src/perf/io-trace.ts b/packages/stage-shared/src/perf/io-trace.ts
index bd23edefb..0781c46e7 100644
--- a/packages/stage-shared/src/perf/io-trace.ts
+++ b/packages/stage-shared/src/perf/io-trace.ts
@@ -1,6 +1,7 @@
export const IOSubsystems = {
ASR: 'asr',
LLM: 'llm',
+ StreamingControl: 'streaming-control',
TTS: 'tts',
Playback: 'playback',
} as const
@@ -10,6 +11,7 @@ export const IOSpanNames = {
InteractionTurn: 'Interaction turn',
SpeechRecognition: 'Speech recognition',
LLMInference: 'LLM inference',
+ StreamingControlDispatch: 'Streaming control dispatch',
TTSSynthesis: 'TTS synthesis',
AudioPlayback: 'Audio playback',
} as const
@@ -22,10 +24,22 @@ export const IOAttributes = {
// Non-standard
Subsystem: `${customPrefix}.subsystem`,
+ TooltipKeys: `${customPrefix}.tooltip.keys`,
LLM_TTFT: `${customPrefix}.llm.time_to_first_token`,
ASRText: `${customPrefix}.asr.text`,
ASRAbort: `${customPrefix}.asr.abort`,
LLMTextLength: `${customPrefix}.llm.text_length`,
+ StreamingControlCallName: `${customPrefix}.streaming_control.call_name`,
+ StreamingControlHandlerCount: `${customPrefix}.streaming_control.handler_count`,
+ StreamingControlMatched: `${customPrefix}.streaming_control.matched`,
+ StreamingControlParameter: `${customPrefix}.streaming_control.parameter`,
+ StreamingControlParsed: `${customPrefix}.streaming_control.parsed`,
+ StreamingControlParserName: `${customPrefix}.streaming_control.parser_name`,
+ StreamingControlReason: `${customPrefix}.streaming_control.reason`,
+ StreamingControlRawToken: `${customPrefix}.streaming_control.raw_token`,
+ StreamingControlTokenLength: `${customPrefix}.streaming_control.token_length`,
+ StreamingControlTokenType: `${customPrefix}.streaming_control.token_type`,
+ StreamingControlTurnId: `${customPrefix}.streaming_control.turn_id`,
TTSSegmentId: `${customPrefix}.tts.segment_id`,
TTSText: `${customPrefix}.tts.text`,
TTSChunkReason: `${customPrefix}.tts.chunk_reason`,
@@ -38,8 +52,26 @@ export const IOEvents = {
// Non-standard
LLMFirstToken: `${customPrefix}.llm.first_token`,
ASRSentenceEnd: `${customPrefix}.asr.sentence_end`,
+ StreamingControlHandlerEnd: `${customPrefix}.streaming_control.handler_end`,
+ StreamingControlHandlerError: `${customPrefix}.streaming_control.handler_error`,
+ StreamingControlHandlerStart: `${customPrefix}.streaming_control.handler_start`,
+ StreamingControlParsed: `${customPrefix}.streaming_control.parsed`,
+ StreamingControlRejected: `${customPrefix}.streaming_control.rejected`,
+ StreamingControlSignalHandlerError: `${customPrefix}.streaming_control.signal_handler_error`,
} as const
+/**
+ * Event captured inside an IO tracing span.
+ */
+export interface IOSpanEvent {
+ /** OTel event name. */
+ name: string
+ /** Event timestamp in milliseconds. */
+ timeTs: number
+ /** Event attributes normalized for the devtools UI. */
+ meta: Record
+}
+
export interface IOSpan {
id: string
traceId: string
@@ -51,6 +83,8 @@ export interface IOSpan {
subsystem: IOSubsystem
name: string
meta: Record
+ /** OTel events attached to the span. */
+ events?: IOSpanEvent[]
}
export interface IOTurn {
diff --git a/packages/stage-ui/src/stores/devtools/io-tracer.ts b/packages/stage-ui/src/stores/devtools/io-tracer.ts
index 75cab71e2..ca9fe0489 100644
--- a/packages/stage-ui/src/stores/devtools/io-tracer.ts
+++ b/packages/stage-ui/src/stores/devtools/io-tracer.ts
@@ -14,7 +14,11 @@ const MAX_TURNS = 50
function attrsToMeta(attrs: Attributes): Record {
const meta: Record = {}
for (const [key, value] of Object.entries(attrs)) {
- const shortKey = key.includes('.') ? key.split('.').at(-1)! : key
+ const shortKey = key === IOAttributes.TooltipKeys
+ ? 'tooltipKeys'
+ : key.includes('.')
+ ? key.split('.').at(-1)!
+ : key
meta[shortKey] = value
}
return meta
@@ -124,6 +128,11 @@ export const useIOTracerStore = defineStore('devtools:io-tracer', () => {
const turn = getOrCreateTurn()
const meta = attrsToMeta(readable.attributes)
+ const events = readable.events.map(event => ({
+ name: event.name,
+ timeTs: hrTimeToMilliseconds(event.time),
+ meta: attrsToMeta(event.attributes ?? {}),
+ }))
for (const event of readable.events) {
const eventAttrs = event.attributes ?? {}
@@ -153,6 +162,7 @@ export const useIOTracerStore = defineStore('devtools:io-tracer', () => {
startTs: startMs,
endTs: endMs,
meta,
+ events,
}
turn.spans.push(ioSpan)