@pipedog/core 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/NOTICE +9 -0
- package/dist/adapters/base_llm_adapter.d.ts +266 -0
- package/dist/adapters/base_llm_adapter.js +489 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/schemas/direct_function.d.ts +202 -0
- package/dist/adapters/schemas/direct_function.js +302 -0
- package/dist/adapters/schemas/function_schema.d.ts +67 -0
- package/dist/adapters/schemas/function_schema.js +72 -0
- package/dist/adapters/schemas/index.d.ts +2 -0
- package/dist/adapters/schemas/index.js +8 -0
- package/dist/adapters/schemas/tools_schema.d.ts +57 -0
- package/dist/adapters/schemas/tools_schema.js +79 -0
- package/dist/adapters/services/index.d.ts +2 -0
- package/dist/adapters/services/index.js +8 -0
- package/dist/adapters/services/open_ai_adapter.d.ts +147 -0
- package/dist/adapters/services/open_ai_adapter.js +250 -0
- package/dist/audio/dtmf/dtmf_data.d.ts +3 -0
- package/dist/audio/dtmf/dtmf_data.js +22 -0
- package/dist/audio/dtmf/index.d.ts +2 -0
- package/dist/audio/dtmf/index.js +8 -0
- package/dist/audio/dtmf/types.d.ts +31 -0
- package/dist/audio/dtmf/types.js +35 -0
- package/dist/audio/dtmf/utils.d.ts +13 -0
- package/dist/audio/dtmf/utils.js +62 -0
- package/dist/audio/filters/base_audio_filter.d.ts +51 -0
- package/dist/audio/filters/base_audio_filter.js +17 -0
- package/dist/audio/filters/index.d.ts +2 -0
- package/dist/audio/filters/index.js +8 -0
- package/dist/audio/index.d.ts +2 -0
- package/dist/audio/index.js +8 -0
- package/dist/audio/krisp_instance.d.ts +95 -0
- package/dist/audio/krisp_instance.js +229 -0
- package/dist/audio/mixers/base_audio_mixer.d.ts +56 -0
- package/dist/audio/mixers/base_audio_mixer.js +16 -0
- package/dist/audio/mixers/index.d.ts +2 -0
- package/dist/audio/mixers/index.js +8 -0
- package/dist/audio/resamplers/base_audio_resampler.d.ts +46 -0
- package/dist/audio/resamplers/base_audio_resampler.js +35 -0
- package/dist/audio/resamplers/engine.d.ts +60 -0
- package/dist/audio/resamplers/engine.js +89 -0
- package/dist/audio/resamplers/index.d.ts +2 -0
- package/dist/audio/resamplers/index.js +8 -0
- package/dist/audio/resamplers/resampy_resampler.d.ts +10 -0
- package/dist/audio/resamplers/resampy_resampler.js +15 -0
- package/dist/audio/resamplers/sinc_engine.d.ts +10 -0
- package/dist/audio/resamplers/sinc_engine.js +235 -0
- package/dist/audio/resamplers/soxr_resampler.d.ts +58 -0
- package/dist/audio/resamplers/soxr_resampler.js +79 -0
- package/dist/audio/resamplers/soxr_stream_resampler.d.ts +78 -0
- package/dist/audio/resamplers/soxr_stream_resampler.js +159 -0
- package/dist/audio/turn/base_turn_analyzer.d.ts +84 -0
- package/dist/audio/turn/base_turn_analyzer.js +61 -0
- package/dist/audio/turn/index.d.ts +2 -0
- package/dist/audio/turn/index.js +8 -0
- package/dist/audio/turn/smart_turn/_whisper_features.d.ts +30 -0
- package/dist/audio/turn/smart_turn/_whisper_features.js +225 -0
- package/dist/audio/turn/smart_turn/base_smart_turn.d.ts +94 -0
- package/dist/audio/turn/smart_turn/base_smart_turn.js +239 -0
- package/dist/audio/turn/smart_turn/local_smart_turn_v3.d.ts +76 -0
- package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +151 -0
- package/dist/audio/utils.d.ts +224 -0
- package/dist/audio/utils.js +634 -0
- package/dist/audio/vad/data/index.d.ts +2 -0
- package/dist/audio/vad/data/index.js +8 -0
- package/dist/audio/vad/index.d.ts +2 -0
- package/dist/audio/vad/index.js +8 -0
- package/dist/audio/vad/vad_analyzer.d.ts +134 -0
- package/dist/audio/vad/vad_analyzer.js +249 -0
- package/dist/audio/vad/vad_controller.d.ts +143 -0
- package/dist/audio/vad/vad_controller.js +228 -0
- package/dist/audio/volume.d.ts +38 -0
- package/dist/audio/volume.js +75 -0
- package/dist/bus/adapters/base.d.ts +35 -0
- package/dist/bus/adapters/base.js +19 -0
- package/dist/bus/adapters/index.d.ts +10 -0
- package/dist/bus/adapters/index.js +15 -0
- package/dist/bus/adapters/llm_context_adapter.d.ts +36 -0
- package/dist/bus/adapters/llm_context_adapter.js +82 -0
- package/dist/bus/adapters/tools_schema_adapter.d.ts +25 -0
- package/dist/bus/adapters/tools_schema_adapter.js +70 -0
- package/dist/bus/bridge_processor.d.ts +111 -0
- package/dist/bus/bridge_processor.js +166 -0
- package/dist/bus/bus.d.ts +110 -0
- package/dist/bus/bus.js +200 -0
- package/dist/bus/index.d.ts +22 -0
- package/dist/bus/index.js +27 -0
- package/dist/bus/local/async_queue.d.ts +12 -0
- package/dist/bus/local/async_queue.js +21 -0
- package/dist/bus/local/index.d.ts +3 -0
- package/dist/bus/local/index.js +8 -0
- package/dist/bus/messages.d.ts +378 -0
- package/dist/bus/messages.js +357 -0
- package/dist/bus/network/index.d.ts +16 -0
- package/dist/bus/network/index.js +21 -0
- package/dist/bus/queue.d.ts +27 -0
- package/dist/bus/queue.js +51 -0
- package/dist/bus/serializers/base.d.ts +25 -0
- package/dist/bus/serializers/base.js +14 -0
- package/dist/bus/serializers/index.d.ts +9 -0
- package/dist/bus/serializers/index.js +14 -0
- package/dist/bus/serializers/json.d.ts +77 -0
- package/dist/bus/serializers/json.js +367 -0
- package/dist/bus/subscriber.d.ts +46 -0
- package/dist/bus/subscriber.js +27 -0
- package/dist/bus/ui/index.d.ts +3 -0
- package/dist/bus/ui/index.js +8 -0
- package/dist/bus/ui/messages.d.ts +186 -0
- package/dist/bus/ui/messages.js +171 -0
- package/dist/classifiers/base_classifier.d.ts +263 -0
- package/dist/classifiers/base_classifier.js +346 -0
- package/dist/classifiers/cloudflare/clef/classifier.d.ts +70 -0
- package/dist/classifiers/cloudflare/clef/classifier.js +204 -0
- package/dist/classifiers/cloudflare/clef/client.d.ts +104 -0
- package/dist/classifiers/cloudflare/clef/client.js +234 -0
- package/dist/classifiers/cloudflare/clef/index.d.ts +2 -0
- package/dist/classifiers/cloudflare/clef/index.js +8 -0
- package/dist/classifiers/cloudflare/index.d.ts +2 -0
- package/dist/classifiers/cloudflare/index.js +8 -0
- package/dist/classifiers/http_client.d.ts +70 -0
- package/dist/classifiers/http_client.js +161 -0
- package/dist/classifiers/index.d.ts +2 -0
- package/dist/classifiers/index.js +8 -0
- package/dist/classifiers/jev/classifier.d.ts +8 -0
- package/dist/classifiers/jev/classifier.js +13 -0
- package/dist/classifiers/jev/client.d.ts +8 -0
- package/dist/classifiers/jev/client.js +13 -0
- package/dist/classifiers/jev/index.d.ts +2 -0
- package/dist/classifiers/jev/index.js +8 -0
- package/dist/classifiers/llm/classifier.d.ts +72 -0
- package/dist/classifiers/llm/classifier.js +304 -0
- package/dist/classifiers/llm/index.d.ts +2 -0
- package/dist/classifiers/llm/index.js +8 -0
- package/dist/classifiers/openai/decisions/classifier.d.ts +77 -0
- package/dist/classifiers/openai/decisions/classifier.js +241 -0
- package/dist/classifiers/openai/decisions/client.d.ts +108 -0
- package/dist/classifiers/openai/decisions/client.js +234 -0
- package/dist/classifiers/openai/decisions/index.d.ts +2 -0
- package/dist/classifiers/openai/decisions/index.js +8 -0
- package/dist/classifiers/openai/index.d.ts +2 -0
- package/dist/classifiers/openai/index.js +8 -0
- package/dist/classifiers/typesafe/index.d.ts +2 -0
- package/dist/classifiers/typesafe/index.js +8 -0
- package/dist/classifiers/typesafe/jev/classifier.d.ts +70 -0
- package/dist/classifiers/typesafe/jev/classifier.js +220 -0
- package/dist/classifiers/typesafe/jev/client.d.ts +105 -0
- package/dist/classifiers/typesafe/jev/client.js +233 -0
- package/dist/classifiers/typesafe/jev/index.d.ts +2 -0
- package/dist/classifiers/typesafe/jev/index.js +8 -0
- package/dist/clocks/base_clock.d.ts +13 -0
- package/dist/clocks/base_clock.js +14 -0
- package/dist/clocks/index.d.ts +2 -0
- package/dist/clocks/index.js +8 -0
- package/dist/clocks/system_clock.d.ts +15 -0
- package/dist/clocks/system_clock.js +25 -0
- package/dist/frames/frames.d.ts +2533 -0
- package/dist/frames/frames.js +2485 -0
- package/dist/frames/index.d.ts +2 -0
- package/dist/frames/index.js +8 -0
- package/dist/frames/protobufs/frames_pb2.d.ts +229 -0
- package/dist/frames/protobufs/frames_pb2.js +466 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +56 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.js +8 -0
- package/dist/metrics/metrics.d.ts +276 -0
- package/dist/metrics/metrics.js +254 -0
- package/dist/observers/base_observer.d.ts +173 -0
- package/dist/observers/base_observer.js +151 -0
- package/dist/observers/error_observer.d.ts +75 -0
- package/dist/observers/error_observer.js +87 -0
- package/dist/observers/function_call_observer.d.ts +157 -0
- package/dist/observers/function_call_observer.js +222 -0
- package/dist/observers/index.d.ts +2 -0
- package/dist/observers/index.js +8 -0
- package/dist/observers/loggers/debug_log_observer.d.ts +91 -0
- package/dist/observers/loggers/debug_log_observer.js +228 -0
- package/dist/observers/loggers/index.d.ts +2 -0
- package/dist/observers/loggers/index.js +8 -0
- package/dist/observers/loggers/llm_log_observer.d.ts +24 -0
- package/dist/observers/loggers/llm_log_observer.js +110 -0
- package/dist/observers/loggers/metrics_log_observer.d.ts +85 -0
- package/dist/observers/loggers/metrics_log_observer.js +186 -0
- package/dist/observers/loggers/transcription_log_observer.d.ts +23 -0
- package/dist/observers/loggers/transcription_log_observer.js +74 -0
- package/dist/observers/service_metrics_observer.d.ts +168 -0
- package/dist/observers/service_metrics_observer.js +236 -0
- package/dist/observers/speaking_observer.d.ts +104 -0
- package/dist/observers/speaking_observer.js +150 -0
- package/dist/observers/startup_timing_observer.d.ts +221 -0
- package/dist/observers/startup_timing_observer.js +365 -0
- package/dist/observers/turn_tracking_observer.d.ts +78 -0
- package/dist/observers/turn_tracking_observer.js +189 -0
- package/dist/observers/user_bot_latency_observer.d.ts +411 -0
- package/dist/observers/user_bot_latency_observer.js +974 -0
- package/dist/pipeline/base_pipeline.d.ts +49 -0
- package/dist/pipeline/base_pipeline.js +101 -0
- package/dist/pipeline/capabilities.d.ts +76 -0
- package/dist/pipeline/capabilities.js +97 -0
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +8 -0
- package/dist/pipeline/job_context.d.ts +317 -0
- package/dist/pipeline/job_context.js +450 -0
- package/dist/pipeline/job_decorator.d.ts +54 -0
- package/dist/pipeline/job_decorator.js +92 -0
- package/dist/pipeline/llm_switcher.d.ts +84 -0
- package/dist/pipeline/llm_switcher.js +107 -0
- package/dist/pipeline/llm_with_backend.d.ts +435 -0
- package/dist/pipeline/llm_with_backend.js +806 -0
- package/dist/pipeline/parallel_pipeline.d.ts +83 -0
- package/dist/pipeline/parallel_pipeline.js +200 -0
- package/dist/pipeline/pipeline.d.ts +126 -0
- package/dist/pipeline/pipeline.js +196 -0
- package/dist/pipeline/service_switcher.d.ts +287 -0
- package/dist/pipeline/service_switcher.js +509 -0
- package/dist/pipeline/sync_parallel_pipeline.d.ts +192 -0
- package/dist/pipeline/sync_parallel_pipeline.js +387 -0
- package/dist/pipeline/worker.d.ts +821 -0
- package/dist/pipeline/worker.js +1656 -0
- package/dist/pipeline/worker_observer.d.ts +131 -0
- package/dist/pipeline/worker_observer.js +274 -0
- package/dist/pipeline/worker_ready_decorator.d.ts +45 -0
- package/dist/pipeline/worker_ready_decorator.js +76 -0
- package/dist/processors/aggregators/async_tool_messages.d.ts +146 -0
- package/dist/processors/aggregators/async_tool_messages.js +291 -0
- package/dist/processors/aggregators/dtmf_aggregator.d.ts +61 -0
- package/dist/processors/aggregators/dtmf_aggregator.js +144 -0
- package/dist/processors/aggregators/gated.d.ts +43 -0
- package/dist/processors/aggregators/gated.js +86 -0
- package/dist/processors/aggregators/gated_llm_context.d.ts +42 -0
- package/dist/processors/aggregators/gated_llm_context.js +86 -0
- package/dist/processors/aggregators/index.d.ts +2 -0
- package/dist/processors/aggregators/index.js +8 -0
- package/dist/processors/aggregators/llm_context.d.ts +370 -0
- package/dist/processors/aggregators/llm_context.js +580 -0
- package/dist/processors/aggregators/llm_context_summarizer.d.ts +219 -0
- package/dist/processors/aggregators/llm_context_summarizer.js +447 -0
- package/dist/processors/aggregators/llm_response.d.ts +37 -0
- package/dist/processors/aggregators/llm_response.js +71 -0
- package/dist/processors/aggregators/llm_response_universal.d.ts +966 -0
- package/dist/processors/aggregators/llm_response_universal.js +2394 -0
- package/dist/processors/aggregators/llm_text_processor.d.ts +53 -0
- package/dist/processors/aggregators/llm_text_processor.js +102 -0
- package/dist/processors/aggregators/sentence.d.ts +39 -0
- package/dist/processors/aggregators/sentence.js +69 -0
- package/dist/processors/async_generator.d.ts +42 -0
- package/dist/processors/async_generator.js +76 -0
- package/dist/processors/audio/audio_buffer_processor.d.ts +241 -0
- package/dist/processors/audio/audio_buffer_processor.js +565 -0
- package/dist/processors/audio/index.d.ts +2 -0
- package/dist/processors/audio/index.js +8 -0
- package/dist/processors/audio/vad_processor.d.ts +67 -0
- package/dist/processors/audio/vad_processor.js +103 -0
- package/dist/processors/consumer_processor.d.ts +49 -0
- package/dist/processors/consumer_processor.js +85 -0
- package/dist/processors/filters/frame_filter.d.ts +39 -0
- package/dist/processors/filters/frame_filter.js +52 -0
- package/dist/processors/filters/function_filter.d.ts +59 -0
- package/dist/processors/filters/function_filter.js +78 -0
- package/dist/processors/filters/identity_filter.d.ts +33 -0
- package/dist/processors/filters/identity_filter.js +37 -0
- package/dist/processors/filters/index.d.ts +2 -0
- package/dist/processors/filters/index.js +8 -0
- package/dist/processors/filters/null_filter.d.ts +34 -0
- package/dist/processors/filters/null_filter.js +47 -0
- package/dist/processors/filters/wake_notifier_filter.d.ts +48 -0
- package/dist/processors/filters/wake_notifier_filter.js +49 -0
- package/dist/processors/frame_processor.d.ts +360 -0
- package/dist/processors/frame_processor.js +932 -0
- package/dist/processors/frameworks/index.d.ts +2 -0
- package/dist/processors/frameworks/index.js +8 -0
- package/dist/processors/frameworks/rtvi/frames.d.ts +241 -0
- package/dist/processors/frameworks/rtvi/frames.js +259 -0
- package/dist/processors/frameworks/rtvi/index.d.ts +6 -0
- package/dist/processors/frameworks/rtvi/index.js +11 -0
- package/dist/processors/frameworks/rtvi/models.d.ts +1741 -0
- package/dist/processors/frameworks/rtvi/models.js +1807 -0
- package/dist/processors/frameworks/rtvi/observer.d.ts +340 -0
- package/dist/processors/frameworks/rtvi/observer.js +1113 -0
- package/dist/processors/frameworks/rtvi/processor.d.ts +182 -0
- package/dist/processors/frameworks/rtvi/processor.js +676 -0
- package/dist/processors/idle_frame_processor.d.ts +44 -0
- package/dist/processors/idle_frame_processor.js +86 -0
- package/dist/processors/index.d.ts +2 -0
- package/dist/processors/index.js +8 -0
- package/dist/processors/logger.d.ts +42 -0
- package/dist/processors/logger.js +87 -0
- package/dist/processors/metrics/frame_processor_metrics.d.ts +172 -0
- package/dist/processors/metrics/frame_processor_metrics.js +372 -0
- package/dist/processors/metrics/index.d.ts +2 -0
- package/dist/processors/metrics/index.js +8 -0
- package/dist/processors/producer_processor.d.ts +68 -0
- package/dist/processors/producer_processor.js +78 -0
- package/dist/processors/text_transformer.d.ts +30 -0
- package/dist/processors/text_transformer.js +48 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +8 -0
- package/dist/registry/registry.d.ts +69 -0
- package/dist/registry/registry.js +149 -0
- package/dist/registry/types.d.ts +47 -0
- package/dist/registry/types.js +39 -0
- package/dist/serializers/base_serializer.d.ts +94 -0
- package/dist/serializers/base_serializer.js +82 -0
- package/dist/serializers/index.d.ts +2 -0
- package/dist/serializers/index.js +8 -0
- package/dist/services/ai_service.d.ts +145 -0
- package/dist/services/ai_service.js +262 -0
- package/dist/services/anthropic/index.d.ts +2 -0
- package/dist/services/anthropic/index.js +7 -0
- package/dist/services/assemblyai/index.d.ts +2 -0
- package/dist/services/assemblyai/index.js +7 -0
- package/dist/services/asyncai/index.d.ts +2 -0
- package/dist/services/asyncai/index.js +7 -0
- package/dist/services/aws/index.d.ts +2 -0
- package/dist/services/aws/index.js +7 -0
- package/dist/services/azure/index.d.ts +2 -0
- package/dist/services/azure/index.js +7 -0
- package/dist/services/baseten/index.d.ts +2 -0
- package/dist/services/baseten/index.js +7 -0
- package/dist/services/bland/index.d.ts +2 -0
- package/dist/services/bland/index.js +7 -0
- package/dist/services/camb/index.d.ts +2 -0
- package/dist/services/camb/index.js +7 -0
- package/dist/services/cartesia/index.d.ts +2 -0
- package/dist/services/cartesia/index.js +7 -0
- package/dist/services/cerebras/index.d.ts +2 -0
- package/dist/services/cerebras/index.js +7 -0
- package/dist/services/crusoe/index.d.ts +2 -0
- package/dist/services/crusoe/index.js +7 -0
- package/dist/services/deepgram/index.d.ts +2 -0
- package/dist/services/deepgram/index.js +7 -0
- package/dist/services/deepseek/index.d.ts +2 -0
- package/dist/services/deepseek/index.js +7 -0
- package/dist/services/elevenlabs/index.d.ts +2 -0
- package/dist/services/elevenlabs/index.js +7 -0
- package/dist/services/fal/index.d.ts +2 -0
- package/dist/services/fal/index.js +7 -0
- package/dist/services/fireworks/index.d.ts +2 -0
- package/dist/services/fireworks/index.js +7 -0
- package/dist/services/fish/index.d.ts +2 -0
- package/dist/services/fish/index.js +7 -0
- package/dist/services/funasr/index.d.ts +3 -0
- package/dist/services/funasr/index.js +8 -0
- package/dist/services/gladia/index.d.ts +2 -0
- package/dist/services/gladia/index.js +7 -0
- package/dist/services/google/index.d.ts +2 -0
- package/dist/services/google/index.js +7 -0
- package/dist/services/gradium/index.d.ts +2 -0
- package/dist/services/gradium/index.js +7 -0
- package/dist/services/grok/index.d.ts +2 -0
- package/dist/services/grok/index.js +7 -0
- package/dist/services/groq/index.d.ts +2 -0
- package/dist/services/groq/index.js +7 -0
- package/dist/services/heygen/index.d.ts +2 -0
- package/dist/services/heygen/index.js +7 -0
- package/dist/services/hume/index.d.ts +2 -0
- package/dist/services/hume/index.js +7 -0
- package/dist/services/image_service.d.ts +52 -0
- package/dist/services/image_service.js +60 -0
- package/dist/services/inception/index.d.ts +2 -0
- package/dist/services/inception/index.js +7 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +8 -0
- package/dist/services/inworld/index.d.ts +2 -0
- package/dist/services/inworld/index.js +7 -0
- package/dist/services/keenable/index.d.ts +2 -0
- package/dist/services/keenable/index.js +7 -0
- package/dist/services/kokoro/index.d.ts +2 -0
- package/dist/services/kokoro/index.js +7 -0
- package/dist/services/llm_service.d.ts +1106 -0
- package/dist/services/llm_service.js +2231 -0
- package/dist/services/mem0/index.d.ts +2 -0
- package/dist/services/mem0/index.js +7 -0
- package/dist/services/meta/index.d.ts +2 -0
- package/dist/services/meta/index.js +7 -0
- package/dist/services/minimax/index.d.ts +2 -0
- package/dist/services/minimax/index.js +7 -0
- package/dist/services/mistral/index.d.ts +2 -0
- package/dist/services/mistral/index.js +7 -0
- package/dist/services/moondream/index.d.ts +2 -0
- package/dist/services/moondream/index.js +7 -0
- package/dist/services/moonshine/index.d.ts +2 -0
- package/dist/services/moonshine/index.js +7 -0
- package/dist/services/nebius/index.d.ts +2 -0
- package/dist/services/nebius/index.js +7 -0
- package/dist/services/neuphonic/index.d.ts +2 -0
- package/dist/services/neuphonic/index.js +7 -0
- package/dist/services/novita/index.d.ts +2 -0
- package/dist/services/novita/index.js +7 -0
- package/dist/services/nvidia/index.d.ts +2 -0
- package/dist/services/nvidia/index.js +11 -0
- package/dist/services/ollama/index.d.ts +2 -0
- package/dist/services/ollama/index.js +7 -0
- package/dist/services/openai/index.d.ts +2 -0
- package/dist/services/openai/index.js +7 -0
- package/dist/services/openclaw/index.d.ts +14 -0
- package/dist/services/openclaw/index.js +19 -0
- package/dist/services/openrouter/index.d.ts +2 -0
- package/dist/services/openrouter/index.js +7 -0
- package/dist/services/perplexity/index.d.ts +2 -0
- package/dist/services/perplexity/index.js +7 -0
- package/dist/services/piper/index.d.ts +2 -0
- package/dist/services/piper/index.js +7 -0
- package/dist/services/pocket_tts/index.d.ts +2 -0
- package/dist/services/pocket_tts/index.js +7 -0
- package/dist/services/qwen/index.d.ts +2 -0
- package/dist/services/qwen/index.js +7 -0
- package/dist/services/resembleai/index.d.ts +2 -0
- package/dist/services/resembleai/index.js +7 -0
- package/dist/services/rime/index.d.ts +2 -0
- package/dist/services/rime/index.js +7 -0
- package/dist/services/sambanova/index.d.ts +2 -0
- package/dist/services/sambanova/index.js +7 -0
- package/dist/services/sarvam/index.d.ts +2 -0
- package/dist/services/sarvam/index.js +7 -0
- package/dist/services/settings.d.ts +356 -0
- package/dist/services/settings.js +437 -0
- package/dist/services/simli/index.d.ts +2 -0
- package/dist/services/simli/index.js +7 -0
- package/dist/services/smallest/index.d.ts +2 -0
- package/dist/services/smallest/index.js +7 -0
- package/dist/services/soniox/index.d.ts +2 -0
- package/dist/services/soniox/index.js +7 -0
- package/dist/services/speechify/index.d.ts +2 -0
- package/dist/services/speechify/index.js +7 -0
- package/dist/services/speechmatics/index.d.ts +2 -0
- package/dist/services/speechmatics/index.js +7 -0
- package/dist/services/stt_latency.d.ts +62 -0
- package/dist/services/stt_latency.js +71 -0
- package/dist/services/stt_service.d.ts +601 -0
- package/dist/services/stt_service.js +1044 -0
- package/dist/services/tavus/index.d.ts +2 -0
- package/dist/services/tavus/index.js +7 -0
- package/dist/services/together/index.d.ts +2 -0
- package/dist/services/together/index.js +7 -0
- package/dist/services/tts_service.d.ts +909 -0
- package/dist/services/tts_service.js +2018 -0
- package/dist/services/ultravox/index.d.ts +2 -0
- package/dist/services/ultravox/index.js +9 -0
- package/dist/services/vision_service.d.ts +56 -0
- package/dist/services/vision_service.js +62 -0
- package/dist/services/websocket_service.d.ts +213 -0
- package/dist/services/websocket_service.js +654 -0
- package/dist/services/whisper/index.d.ts +2 -0
- package/dist/services/whisper/index.js +9 -0
- package/dist/services/xai/index.d.ts +2 -0
- package/dist/services/xai/index.js +9 -0
- package/dist/services/xtts/index.d.ts +2 -0
- package/dist/services/xtts/index.js +9 -0
- package/dist/tests/index.d.ts +2 -0
- package/dist/tests/index.js +8 -0
- package/dist/tests/utils.d.ts +123 -0
- package/dist/tests/utils.js +212 -0
- package/dist/transcriptions/index.d.ts +2 -0
- package/dist/transcriptions/index.js +8 -0
- package/dist/transcriptions/language.d.ts +376 -0
- package/dist/transcriptions/language.js +543 -0
- package/dist/transports/base_input.d.ts +149 -0
- package/dist/transports/base_input.js +338 -0
- package/dist/transports/base_output.d.ts +432 -0
- package/dist/transports/base_output.js +1231 -0
- package/dist/transports/base_transport.d.ts +223 -0
- package/dist/transports/base_transport.js +167 -0
- package/dist/transports/daily/index.d.ts +2 -0
- package/dist/transports/daily/index.js +9 -0
- package/dist/transports/heygen/index.d.ts +2 -0
- package/dist/transports/heygen/index.js +9 -0
- package/dist/transports/index.d.ts +2 -0
- package/dist/transports/index.js +8 -0
- package/dist/transports/lemonslice/index.d.ts +2 -0
- package/dist/transports/lemonslice/index.js +9 -0
- package/dist/transports/livekit/index.d.ts +2 -0
- package/dist/transports/livekit/index.js +9 -0
- package/dist/transports/local/index.d.ts +2 -0
- package/dist/transports/local/index.js +9 -0
- package/dist/transports/moq/index.d.ts +2 -0
- package/dist/transports/moq/index.js +9 -0
- package/dist/transports/smallwebrtc/index.d.ts +2 -0
- package/dist/transports/smallwebrtc/index.js +9 -0
- package/dist/transports/tavus/index.d.ts +2 -0
- package/dist/transports/tavus/index.js +9 -0
- package/dist/transports/video_in_sampler.d.ts +157 -0
- package/dist/transports/video_in_sampler.js +258 -0
- package/dist/transports/vonage/index.d.ts +2 -0
- package/dist/transports/vonage/index.js +9 -0
- package/dist/transports/websocket/index.d.ts +2 -0
- package/dist/transports/websocket/index.js +9 -0
- package/dist/transports/whatsapp/index.d.ts +2 -0
- package/dist/transports/whatsapp/index.js +9 -0
- package/dist/turns/eager_end_of_turn_mixin.d.ts +70 -0
- package/dist/turns/eager_end_of_turn_mixin.js +122 -0
- package/dist/turns/empty_user_turn.d.ts +55 -0
- package/dist/turns/empty_user_turn.js +53 -0
- package/dist/turns/index.d.ts +2 -0
- package/dist/turns/index.js +8 -0
- package/dist/turns/speculation_gate.d.ts +169 -0
- package/dist/turns/speculation_gate.js +294 -0
- package/dist/turns/types.d.ts +33 -0
- package/dist/turns/types.js +35 -0
- package/dist/turns/user_idle_controller.d.ts +94 -0
- package/dist/turns/user_idle_controller.js +187 -0
- package/dist/turns/user_mute/always_user_mute_strategy.d.ts +15 -0
- package/dist/turns/user_mute/always_user_mute_strategy.js +29 -0
- package/dist/turns/user_mute/base_user_mute_strategy.d.ts +39 -0
- package/dist/turns/user_mute/base_user_mute_strategy.js +45 -0
- package/dist/turns/user_mute/first_speech_user_mute_strategy.d.ts +28 -0
- package/dist/turns/user_mute/first_speech_user_mute_strategy.js +52 -0
- package/dist/turns/user_mute/function_call_user_mute_strategy.d.ts +25 -0
- package/dist/turns/user_mute/function_call_user_mute_strategy.js +50 -0
- package/dist/turns/user_mute/index.d.ts +6 -0
- package/dist/turns/user_mute/index.js +11 -0
- package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.d.ts +43 -0
- package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.js +76 -0
- package/dist/turns/user_start/base_user_turn_start_strategy.d.ts +135 -0
- package/dist/turns/user_start/base_user_turn_start_strategy.js +134 -0
- package/dist/turns/user_start/external_user_turn_start_strategy.d.ts +45 -0
- package/dist/turns/user_start/external_user_turn_start_strategy.js +64 -0
- package/dist/turns/user_start/index.d.ts +8 -0
- package/dist/turns/user_start/index.js +16 -0
- package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.d.ts +133 -0
- package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.js +243 -0
- package/dist/turns/user_start/min_words_user_turn_start_strategy.d.ts +60 -0
- package/dist/turns/user_start/min_words_user_turn_start_strategy.js +102 -0
- package/dist/turns/user_start/transcription_user_turn_start_strategy.d.ts +29 -0
- package/dist/turns/user_start/transcription_user_turn_start_strategy.js +43 -0
- package/dist/turns/user_start/vad_user_turn_start_strategy.d.ts +20 -0
- package/dist/turns/user_start/vad_user_turn_start_strategy.js +31 -0
- package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.d.ts +141 -0
- package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.js +238 -0
- package/dist/turns/user_stop/base_user_turn_stop_strategy.d.ts +174 -0
- package/dist/turns/user_stop/base_user_turn_stop_strategy.js +170 -0
- package/dist/turns/user_stop/deferred_user_turn_stop_strategy.d.ts +81 -0
- package/dist/turns/user_stop/deferred_user_turn_stop_strategy.js +107 -0
- package/dist/turns/user_stop/eager_match_policy.d.ts +36 -0
- package/dist/turns/user_stop/eager_match_policy.js +50 -0
- package/dist/turns/user_stop/eager_user_turn_stop_strategy.d.ts +84 -0
- package/dist/turns/user_stop/eager_user_turn_stop_strategy.js +154 -0
- package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.d.ts +45 -0
- package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.js +55 -0
- package/dist/turns/user_stop/external_user_turn_stop_strategy.d.ts +115 -0
- package/dist/turns/user_stop/external_user_turn_stop_strategy.js +205 -0
- package/dist/turns/user_stop/index.d.ts +10 -0
- package/dist/turns/user_stop/index.js +15 -0
- package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.d.ts +44 -0
- package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.js +59 -0
- package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.d.ts +140 -0
- package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.js +303 -0
- package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.d.ts +135 -0
- package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.js +332 -0
- package/dist/turns/user_turn_completion_mixin.d.ts +147 -0
- package/dist/turns/user_turn_completion_mixin.js +591 -0
- package/dist/turns/user_turn_controller.d.ts +134 -0
- package/dist/turns/user_turn_controller.js +335 -0
- package/dist/turns/user_turn_processor.d.ts +87 -0
- package/dist/turns/user_turn_processor.js +180 -0
- package/dist/turns/user_turn_strategies.d.ts +187 -0
- package/dist/turns/user_turn_strategies.js +222 -0
- package/dist/utils/async_tool_cancellation.d.ts +35 -0
- package/dist/utils/async_tool_cancellation.js +78 -0
- package/dist/utils/asyncio/index.d.ts +2 -0
- package/dist/utils/asyncio/index.js +7 -0
- package/dist/utils/asyncio/task_manager.d.ts +49 -0
- package/dist/utils/asyncio/task_manager.js +105 -0
- package/dist/utils/base_object.d.ts +76 -0
- package/dist/utils/base_object.js +154 -0
- package/dist/utils/context/aggregated_frame_sequencer.d.ts +491 -0
- package/dist/utils/context/aggregated_frame_sequencer.js +803 -0
- package/dist/utils/context/audio_context_manager.d.ts +131 -0
- package/dist/utils/context/audio_context_manager.js +233 -0
- package/dist/utils/context/index.d.ts +2 -0
- package/dist/utils/context/index.js +7 -0
- package/dist/utils/context/llm_context_summarization.d.ts +258 -0
- package/dist/utils/context/llm_context_summarization.js +511 -0
- package/dist/utils/context/text_segment_map.d.ts +611 -0
- package/dist/utils/context/text_segment_map.js +1132 -0
- package/dist/utils/context/word_completion_tracker.d.ts +273 -0
- package/dist/utils/context/word_completion_tracker.js +441 -0
- package/dist/utils/context/word_timestamp_tracker.d.ts +72 -0
- package/dist/utils/context/word_timestamp_tracker.js +98 -0
- package/dist/utils/deprecation.d.ts +28 -0
- package/dist/utils/deprecation.js +75 -0
- package/dist/utils/env.d.ts +38 -0
- package/dist/utils/env.js +62 -0
- package/dist/utils/errors.d.ts +75 -0
- package/dist/utils/errors.js +202 -0
- package/dist/utils/file_resolver.d.ts +126 -0
- package/dist/utils/file_resolver.js +314 -0
- package/dist/utils/file_storage.d.ts +115 -0
- package/dist/utils/file_storage.js +178 -0
- package/dist/utils/frame_queue.d.ts +36 -0
- package/dist/utils/frame_queue.js +66 -0
- package/dist/utils/http.d.ts +39 -0
- package/dist/utils/http.js +53 -0
- package/dist/utils/image.d.ts +14 -0
- package/dist/utils/image.js +63 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/jpeg_encoder.d.ts +14 -0
- package/dist/utils/jpeg_encoder.js +315 -0
- package/dist/utils/log_config.d.ts +34 -0
- package/dist/utils/log_config.js +76 -0
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.js +91 -0
- package/dist/utils/mixin.d.ts +14 -0
- package/dist/utils/mixin.js +63 -0
- package/dist/utils/network.d.ts +73 -0
- package/dist/utils/network.js +90 -0
- package/dist/utils/security/allowed_origins.d.ts +16 -0
- package/dist/utils/security/allowed_origins.js +34 -0
- package/dist/utils/security/index.d.ts +2 -0
- package/dist/utils/security/index.js +7 -0
- package/dist/utils/security/redaction.d.ts +27 -0
- package/dist/utils/security/redaction.js +123 -0
- package/dist/utils/security/ssrf.d.ts +79 -0
- package/dist/utils/security/ssrf.js +327 -0
- package/dist/utils/shared.d.ts +35 -0
- package/dist/utils/shared.js +136 -0
- package/dist/utils/startup.d.ts +17 -0
- package/dist/utils/startup.js +130 -0
- package/dist/utils/string.d.ts +116 -0
- package/dist/utils/string.js +460 -0
- package/dist/utils/sync/base_notifier.d.ts +25 -0
- package/dist/utils/sync/base_notifier.js +16 -0
- package/dist/utils/sync/event_notifier.d.ts +38 -0
- package/dist/utils/sync/event_notifier.js +51 -0
- package/dist/utils/sync/index.d.ts +2 -0
- package/dist/utils/sync/index.js +7 -0
- package/dist/utils/text/alnum_utils.d.ts +109 -0
- package/dist/utils/text/alnum_utils.js +262 -0
- package/dist/utils/text/base_text_aggregator.d.ts +146 -0
- package/dist/utils/text/base_text_aggregator.js +102 -0
- package/dist/utils/text/base_text_filter.d.ts +54 -0
- package/dist/utils/text/base_text_filter.js +50 -0
- package/dist/utils/text/index.d.ts +2 -0
- package/dist/utils/text/index.js +7 -0
- package/dist/utils/text/markdown_text_filter.d.ts +147 -0
- package/dist/utils/text/markdown_text_filter.js +522 -0
- package/dist/utils/text/markup_utils.d.ts +54 -0
- package/dist/utils/text/markup_utils.js +156 -0
- package/dist/utils/text/pattern_pair_aggregator.d.ts +251 -0
- package/dist/utils/text/pattern_pair_aggregator.js +478 -0
- package/dist/utils/text/phonemes.d.ts +60 -0
- package/dist/utils/text/phonemes.js +172 -0
- package/dist/utils/text/simple_text_aggregator.d.ts +135 -0
- package/dist/utils/text/simple_text_aggregator.js +254 -0
- package/dist/utils/text/skip_tags_aggregator.d.ts +85 -0
- package/dist/utils/text/skip_tags_aggregator.js +147 -0
- package/dist/utils/text/transforms/acronyms.d.ts +21 -0
- package/dist/utils/text/transforms/acronyms.js +31 -0
- package/dist/utils/text/transforms/currency.d.ts +16 -0
- package/dist/utils/text/transforms/currency.js +56 -0
- package/dist/utils/text/transforms/dates.d.ts +18 -0
- package/dist/utils/text/transforms/dates.js +102 -0
- package/dist/utils/text/transforms/email.d.ts +17 -0
- package/dist/utils/text/transforms/email.js +35 -0
- package/dist/utils/text/transforms/index.d.ts +37 -0
- package/dist/utils/text/transforms/index.js +42 -0
- package/dist/utils/text/transforms/numbers.d.ts +25 -0
- package/dist/utils/text/transforms/numbers.js +59 -0
- package/dist/utils/text/transforms/percentages.d.ts +16 -0
- package/dist/utils/text/transforms/percentages.js +32 -0
- package/dist/utils/text/transforms/phone.d.ts +21 -0
- package/dist/utils/text/transforms/phone.js +39 -0
- package/dist/utils/text/transforms/pronunciations.d.ts +65 -0
- package/dist/utils/text/transforms/pronunciations.js +112 -0
- package/dist/utils/text/transforms/replacements.d.ts +39 -0
- package/dist/utils/text/transforms/replacements.js +46 -0
- package/dist/utils/text/transforms/strip_markdown.d.ts +25 -0
- package/dist/utils/text/transforms/strip_markdown.js +60 -0
- package/dist/utils/text/transforms/units.d.ts +22 -0
- package/dist/utils/text/transforms/units.js +81 -0
- package/dist/utils/text/transforms/voice_formatter.d.ts +86 -0
- package/dist/utils/text/transforms/voice_formatter.js +98 -0
- package/dist/utils/text/unicode_digits.d.ts +3 -0
- package/dist/utils/text/unicode_digits.js +34 -0
- package/dist/utils/text/word_timestamp_utils.d.ts +26 -0
- package/dist/utils/text/word_timestamp_utils.js +52 -0
- package/dist/utils/time.d.ts +13 -0
- package/dist/utils/time.js +31 -0
- package/dist/utils/tracing/index.d.ts +3 -0
- package/dist/utils/tracing/index.js +8 -0
- package/dist/utils/tracing/service_attributes.d.ts +198 -0
- package/dist/utils/tracing/service_attributes.js +434 -0
- package/dist/utils/tracing/service_decorators.d.ts +214 -0
- package/dist/utils/tracing/service_decorators.js +1662 -0
- package/dist/utils/tracing/setup.d.ts +107 -0
- package/dist/utils/tracing/setup.js +160 -0
- package/dist/utils/tracing/tracing_context.d.ts +55 -0
- package/dist/utils/tracing/tracing_context.js +97 -0
- package/dist/utils/tracing/turn_trace_observer.d.ts +95 -0
- package/dist/utils/tracing/turn_trace_observer.js +233 -0
- package/dist/utils/types.d.ts +80 -0
- package/dist/utils/types.js +108 -0
- package/dist/utils/utils.d.ts +5 -0
- package/dist/utils/utils.js +22 -0
- package/dist/utils/yaml.d.ts +179 -0
- package/dist/utils/yaml.js +556 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +7 -0
- package/dist/workers/base_worker.d.ts +772 -0
- package/dist/workers/base_worker.js +1298 -0
- package/dist/workers/index.d.ts +2 -0
- package/dist/workers/index.js +8 -0
- package/dist/workers/llm/backend_llm_worker.d.ts +552 -0
- package/dist/workers/llm/backend_llm_worker.js +1055 -0
- package/dist/workers/llm/index.d.ts +6 -0
- package/dist/workers/llm/index.js +11 -0
- package/dist/workers/llm/llm_context_worker.d.ts +66 -0
- package/dist/workers/llm/llm_context_worker.js +72 -0
- package/dist/workers/llm/llm_worker.d.ts +180 -0
- package/dist/workers/llm/llm_worker.js +323 -0
- package/dist/workers/llm/tool_decorator.d.ts +68 -0
- package/dist/workers/llm/tool_decorator.js +60 -0
- package/dist/workers/proxy/index.d.ts +3 -0
- package/dist/workers/proxy/index.js +9 -0
- package/dist/workers/proxy/websocket/client.d.ts +110 -0
- package/dist/workers/proxy/websocket/client.js +196 -0
- package/dist/workers/proxy/websocket/index.d.ts +3 -0
- package/dist/workers/proxy/websocket/index.js +10 -0
- package/dist/workers/runner.d.ts +268 -0
- package/dist/workers/runner.js +489 -0
- package/dist/workers/ui/index.d.ts +22 -0
- package/dist/workers/ui/index.js +27 -0
- package/dist/workers/ui/ui_event_decorator.d.ts +43 -0
- package/dist/workers/ui/ui_event_decorator.js +75 -0
- package/dist/workers/ui/ui_job_context.d.ts +12 -0
- package/dist/workers/ui/ui_job_context.js +17 -0
- package/dist/workers/ui/ui_prompts.d.ts +21 -0
- package/dist/workers/ui/ui_prompts.js +41 -0
- package/dist/workers/ui/ui_tools.d.ts +32 -0
- package/dist/workers/ui/ui_tools.js +112 -0
- package/dist/workers/ui/ui_worker.d.ts +571 -0
- package/dist/workers/ui/ui_worker.js +1156 -0
- package/package.json +61 -3
- package/README.md +0 -5
|
@@ -0,0 +1,2394 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/processors/aggregators/llm_response_universal.py
|
|
6
|
+
/**
|
|
7
|
+
* LLM response aggregators for handling conversation context and message aggregation.
|
|
8
|
+
*
|
|
9
|
+
* This module provides aggregators that process and accumulate LLM responses,
|
|
10
|
+
* user inputs, and conversation context. These aggregators handle the flow
|
|
11
|
+
* between speech-to-text, LLM processing, and text-to-speech components in
|
|
12
|
+
* conversational AI pipelines.
|
|
13
|
+
*
|
|
14
|
+
* Porting notes: the deprecated `LLMUserAggregatorParams.filterIncompleteUserTurns`
|
|
15
|
+
* / `userTurnCompletionConfig` and `LLMAssistantAggregatorParams.enableContextSummarization`
|
|
16
|
+
* / `contextSummarizationConfig` fields are not ported.
|
|
17
|
+
*/
|
|
18
|
+
import { rethrowIfCancelled, sleep } from "@pipedog/asyncio";
|
|
19
|
+
import { VADController } from "../../audio/vad/vad_controller.js";
|
|
20
|
+
import { AggregatedTextFrame, AssistantImageRawFrame, BotStartedSpeakingFrame, BotStoppedSpeakingFrame, CancelFrame, EagerEndOfTurnCancelFrame, EagerTranscriptionFrame, EndFrame, FunctionCallCancelFrame, FunctionCallInProgressFrame, FunctionCallResultFrame, FunctionCallsStartedFrame, InputAudioRawFrame, InterimTranscriptionFrame, InterruptionFrame, LLMAssistantPushAggregationFrame, LLMContextAssistantTimestampFrame, LLMContextAssistantTurnFrame, LLMContextFrame, LLMContextUserTurnFrame, LLMFullResponseEndFrame, LLMFullResponseStartFrame, LLMMarkerFrame, LLMMessagesAppendFrame, LLMMessagesTransformFrame, LLMMessagesUpdateFrame, LLMRunFrame, LLMServiceMetadataFrame, LLMSetToolChoiceFrame, LLMSetToolsFrame, LLMThoughtEndFrame, LLMThoughtStartFrame, LLMThoughtTextFrame, ProposedUserStartedSpeakingFrame, ProposedUserStoppedSpeakingFrame, ServiceMetadataFrame, STTMetadataFrame, StartFrame, TextFrame, TranscriptionFrame, TranslationFrame, TTSStartedFrame, TTSTextFrame, UserBackchannelFrame, UserFileRawFrame, UserImageRawFrame, UserMuteStartedFrame, UserMuteStoppedFrame, UserSpeakingFrame, UserStartedSpeakingFrame, UserStoppedSpeakingFrame, VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame, } from "../../frames/frames.js";
|
|
21
|
+
import { DEFAULT_TTFS_P99 } from "../../services/stt_latency.js";
|
|
22
|
+
import { EmptyUserTurnConfig } from "../../turns/empty_user_turn.js";
|
|
23
|
+
import { UserIdleController } from "../../turns/user_idle_controller.js";
|
|
24
|
+
import { TranscriptionUserTurnStartStrategy } from "../../turns/user_start/transcription_user_turn_start_strategy.js";
|
|
25
|
+
import { USER_TURN_COMPLETE_MARKER, USER_TURN_INCOMPLETE_LONG_MARKER, USER_TURN_INCOMPLETE_SHORT_MARKER, } from "../../turns/user_turn_completion_mixin.js";
|
|
26
|
+
import { UserTurnController } from "../../turns/user_turn_controller.js";
|
|
27
|
+
import { ExternalUserTurnStrategies, UserTurnStrategies, } from "../../turns/user_turn_strategies.js";
|
|
28
|
+
import { logger } from "../../utils/logger.js";
|
|
29
|
+
import { concatenateAggregatedText, TextPartForConcatenation } from "../../utils/string.js";
|
|
30
|
+
import { TextType } from "../../utils/text/base_text_aggregator.js";
|
|
31
|
+
import { timeNowIso8601 } from "../../utils/time.js";
|
|
32
|
+
import { FrameDirection, FrameProcessor, } from "../frame_processor.js";
|
|
33
|
+
import * as asyncToolMessages from "./async_tool_messages.js";
|
|
34
|
+
import { isGiven, LLMContext, LLMSpecificMessage, } from "./llm_context.js";
|
|
35
|
+
import { LLMContextSummarizer } from "./llm_context_summarizer.js";
|
|
36
|
+
/**
|
|
37
|
+
* Python `json.dumps(value, ensure_ascii=False)`: same output as
|
|
38
|
+
* `JSON.stringify`, but with Python's `", "` and `": "` separators so
|
|
39
|
+
* contexts are identical across ports.
|
|
40
|
+
*/
|
|
41
|
+
function jsonDumps(value) {
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
return "null";
|
|
44
|
+
if (value === null || typeof value !== "object") {
|
|
45
|
+
return JSON.stringify(value) ?? "null";
|
|
46
|
+
}
|
|
47
|
+
const withToJson = value;
|
|
48
|
+
if (typeof withToJson.toJSON === "function") {
|
|
49
|
+
return jsonDumps(withToJson.toJSON());
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
return `[${value.map((v) => jsonDumps(v)).join(", ")}]`;
|
|
53
|
+
}
|
|
54
|
+
const entries = Object.entries(value)
|
|
55
|
+
.filter(([, v]) => v !== undefined && typeof v !== "function")
|
|
56
|
+
.map(([k, v]) => `${JSON.stringify(k)}: ${jsonDumps(v)}`);
|
|
57
|
+
return `{${entries.join(", ")}}`;
|
|
58
|
+
}
|
|
59
|
+
/** Python truthiness for a function call result (empty dicts/lists/strings are falsy). */
|
|
60
|
+
function isTruthy(value) {
|
|
61
|
+
if (value === null || value === undefined || value === false || value === 0 || value === "") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value))
|
|
65
|
+
return value.length > 0;
|
|
66
|
+
if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
67
|
+
return Object.keys(value).length > 0;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** Python `repr()` of a list of strings, as used in log lines. */
|
|
72
|
+
function reprList(items) {
|
|
73
|
+
return `[${items.map((i) => `'${i}'`).join(", ")}]`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parameters for configuring LLM user aggregation behavior.
|
|
77
|
+
*
|
|
78
|
+
* See `LLMUserAggregatorParamsOptions` for the fields.
|
|
79
|
+
*/
|
|
80
|
+
export class LLMUserAggregatorParams {
|
|
81
|
+
addToolChangeMessages;
|
|
82
|
+
audioIdleTimeout;
|
|
83
|
+
userTurnStrategies;
|
|
84
|
+
userMuteStrategies;
|
|
85
|
+
userTurnStopTimeout;
|
|
86
|
+
userIdleTimeout;
|
|
87
|
+
vadAnalyzer;
|
|
88
|
+
emptyUserTurn;
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
this.addToolChangeMessages = options.addToolChangeMessages ?? false;
|
|
91
|
+
this.audioIdleTimeout = options.audioIdleTimeout ?? 1.0;
|
|
92
|
+
this.userTurnStrategies = options.userTurnStrategies ?? null;
|
|
93
|
+
this.userMuteStrategies = options.userMuteStrategies ?? [];
|
|
94
|
+
this.userTurnStopTimeout = options.userTurnStopTimeout ?? 5.0;
|
|
95
|
+
this.userIdleTimeout = options.userIdleTimeout ?? 0;
|
|
96
|
+
this.vadAnalyzer = options.vadAnalyzer ?? null;
|
|
97
|
+
this.emptyUserTurn =
|
|
98
|
+
options.emptyUserTurn === undefined ? new EmptyUserTurnConfig() : options.emptyUserTurn;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parameters for configuring LLM assistant aggregation behavior.
|
|
103
|
+
*
|
|
104
|
+
* See `LLMAssistantAggregatorParamsOptions` for the fields.
|
|
105
|
+
*/
|
|
106
|
+
export class LLMAssistantAggregatorParams {
|
|
107
|
+
enableAutoContextSummarization;
|
|
108
|
+
autoContextSummarizationConfig;
|
|
109
|
+
addToolChangeMessages;
|
|
110
|
+
constructor(options = {}) {
|
|
111
|
+
this.enableAutoContextSummarization = options.enableAutoContextSummarization ?? false;
|
|
112
|
+
this.autoContextSummarizationConfig = options.autoContextSummarizationConfig ?? null;
|
|
113
|
+
this.addToolChangeMessages = options.addToolChangeMessages ?? false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A user turn stopped message containing a user transcript update.
|
|
118
|
+
*
|
|
119
|
+
* A message in a conversation transcript containing the user content. This
|
|
120
|
+
* is the aggregated transcript that is then used in the context.
|
|
121
|
+
*
|
|
122
|
+
* - `content`: The message content/text. `null` in realtime mode
|
|
123
|
+
* (`realtimeServiceMode: true`) when fired from a user-turn-stop frame: in
|
|
124
|
+
* realtime mode the user message isn't finalized until the assistant
|
|
125
|
+
* response start acts as the effective end-of-turn signal. Subscribers that
|
|
126
|
+
* need the finalized text should listen to `on_user_turn_message_added`
|
|
127
|
+
* instead.
|
|
128
|
+
* - `timestamp`: When the user turn started.
|
|
129
|
+
* - `userId`: Optional identifier for the user.
|
|
130
|
+
*/
|
|
131
|
+
export class UserTurnStoppedMessage {
|
|
132
|
+
content;
|
|
133
|
+
timestamp;
|
|
134
|
+
userId;
|
|
135
|
+
constructor(options) {
|
|
136
|
+
this.content = options.content;
|
|
137
|
+
this.timestamp = options.timestamp;
|
|
138
|
+
this.userId = options.userId ?? null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A message accompanying `on_user_turn_message_added`.
|
|
143
|
+
*
|
|
144
|
+
* Fired when a user message is written to the LLM context. In cascade mode
|
|
145
|
+
* (`realtimeServiceMode: false`) this coincides with `on_user_turn_stopped`.
|
|
146
|
+
* In realtime mode (`realtimeServiceMode: true`) the write is triggered by
|
|
147
|
+
* the assistant response start (or session end), so this event fires
|
|
148
|
+
* decoupled from user-turn-end frames. `content` is always populated.
|
|
149
|
+
*
|
|
150
|
+
* - `content`: The aggregated user transcript.
|
|
151
|
+
* - `timestamp`: When the user turn started.
|
|
152
|
+
* - `userId`: Optional identifier for the user.
|
|
153
|
+
*/
|
|
154
|
+
export class UserTurnMessageAddedMessage {
|
|
155
|
+
content;
|
|
156
|
+
timestamp;
|
|
157
|
+
userId;
|
|
158
|
+
constructor(options) {
|
|
159
|
+
this.content = options.content;
|
|
160
|
+
this.timestamp = options.timestamp;
|
|
161
|
+
this.userId = options.userId ?? null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A message accompanying `on_user_backchannel`.
|
|
166
|
+
*
|
|
167
|
+
* A short acknowledgment the user said while the bot talks, such as "mhm",
|
|
168
|
+
* which stays out of the LLM context.
|
|
169
|
+
*
|
|
170
|
+
* - `content`: What the user said.
|
|
171
|
+
* - `timestamp`: When the user said it.
|
|
172
|
+
* - `userId`: Optional identifier for the user.
|
|
173
|
+
*/
|
|
174
|
+
export class UserBackchannelMessage {
|
|
175
|
+
content;
|
|
176
|
+
timestamp;
|
|
177
|
+
userId;
|
|
178
|
+
constructor(options) {
|
|
179
|
+
this.content = options.content;
|
|
180
|
+
this.timestamp = options.timestamp;
|
|
181
|
+
this.userId = options.userId ?? null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* An assistant turn stopped message containing an assistant transcript update.
|
|
186
|
+
*
|
|
187
|
+
* A message in a conversation transcript containing the assistant content.
|
|
188
|
+
* This is the aggregated transcript that is then used in the context.
|
|
189
|
+
*
|
|
190
|
+
* - `content`: The message content/text. May be empty if the LLM returned
|
|
191
|
+
* zero tokens (e.g. turn was interrupted before any tokens were received or
|
|
192
|
+
* pushed).
|
|
193
|
+
* - `interrupted`: Whether the assistant turn was interrupted.
|
|
194
|
+
* - `timestamp`: When the assistant turn started.
|
|
195
|
+
*/
|
|
196
|
+
export class AssistantTurnStoppedMessage {
|
|
197
|
+
content;
|
|
198
|
+
interrupted;
|
|
199
|
+
timestamp;
|
|
200
|
+
constructor(options) {
|
|
201
|
+
this.content = options.content;
|
|
202
|
+
this.interrupted = options.interrupted;
|
|
203
|
+
this.timestamp = options.timestamp;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* A message accompanying `on_assistant_backchannel`.
|
|
208
|
+
*
|
|
209
|
+
* A short acknowledgment the assistant said while the user talks, such as
|
|
210
|
+
* "Mm-hmm.".
|
|
211
|
+
*
|
|
212
|
+
* - `content`: What the assistant said.
|
|
213
|
+
* - `timestamp`: When the assistant started saying it.
|
|
214
|
+
*/
|
|
215
|
+
export class AssistantBackchannelMessage {
|
|
216
|
+
content;
|
|
217
|
+
timestamp;
|
|
218
|
+
constructor(options) {
|
|
219
|
+
this.content = options.content;
|
|
220
|
+
this.timestamp = options.timestamp;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* An assistant thought message containing an assistant thought update.
|
|
225
|
+
*
|
|
226
|
+
* A message in a conversation transcript containing the assistant thought
|
|
227
|
+
* content.
|
|
228
|
+
*
|
|
229
|
+
* - `content`: The message content/text.
|
|
230
|
+
* - `timestamp`: When the thought started.
|
|
231
|
+
*/
|
|
232
|
+
export class AssistantThoughtMessage {
|
|
233
|
+
content;
|
|
234
|
+
timestamp;
|
|
235
|
+
constructor(options) {
|
|
236
|
+
this.content = options.content;
|
|
237
|
+
this.timestamp = options.timestamp;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Base LLM aggregator that uses an LLMContext for conversation storage.
|
|
242
|
+
*
|
|
243
|
+
* This aggregator maintains conversation state using an LLMContext and pushes
|
|
244
|
+
* LLMContextFrame objects as aggregation frames. It provides common
|
|
245
|
+
* functionality for context-based conversation management.
|
|
246
|
+
*/
|
|
247
|
+
export class LLMContextAggregator extends FrameProcessor {
|
|
248
|
+
// Developer-role messages appended to the context when tools are added/
|
|
249
|
+
// removed via `LLMSetToolsFrame` (only when `addToolChangeMessages` is
|
|
250
|
+
// enabled on the aggregator's params). `{function_names}` is substituted
|
|
251
|
+
// with a sorted, comma-separated, backtick-wrapped list.
|
|
252
|
+
static TOOL_ACTIVATION_MESSAGE_TEMPLATE = "The following function(s) have just been added and may now be called: " +
|
|
253
|
+
"{function_names}. Any previously available functions remain available.";
|
|
254
|
+
static TOOL_DEACTIVATION_MESSAGE_TEMPLATE = "The following function(s) have just been removed and should not be called: " +
|
|
255
|
+
"{function_names}. Any previously available functions remain available. " +
|
|
256
|
+
"The removed function(s) may become available again later, in which case " +
|
|
257
|
+
"you will be informed.";
|
|
258
|
+
_context;
|
|
259
|
+
_role;
|
|
260
|
+
_addToolChangeMessages;
|
|
261
|
+
_aggregation = [];
|
|
262
|
+
/** Initialize the context response aggregator. */
|
|
263
|
+
constructor(options) {
|
|
264
|
+
super(options);
|
|
265
|
+
this._context = options.context;
|
|
266
|
+
this._role = options.role;
|
|
267
|
+
this._addToolChangeMessages = options.addToolChangeMessages ?? false;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Append a developer message describing tool add/remove deltas.
|
|
271
|
+
*
|
|
272
|
+
* No-op unless `addToolChangeMessages` was enabled on the aggregator, and
|
|
273
|
+
* no-op when the diff against the currently advertised tools is empty.
|
|
274
|
+
* Custom (LLM-specific) tools are ignored — only standard tools are diffed.
|
|
275
|
+
*
|
|
276
|
+
* Both aggregators call this on every `LLMSetToolsFrame` they handle.
|
|
277
|
+
* Whichever aggregator handles the frame first computes a real diff against
|
|
278
|
+
* the shared context and adds the announcement; by the time the other
|
|
279
|
+
* aggregator sees it (if at all), the context already reflects the new
|
|
280
|
+
* tools, so its diff is empty and no duplicate message is added. This is
|
|
281
|
+
* order-independent: it works whether the frame flows downstream (user
|
|
282
|
+
* aggregator first) or upstream (assistant aggregator first, and consumed
|
|
283
|
+
* without being forwarded).
|
|
284
|
+
*/
|
|
285
|
+
_maybeAddToolChangeMessages(newTools) {
|
|
286
|
+
if (!this._addToolChangeMessages) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const names = (tools) => {
|
|
290
|
+
if (!isGiven(tools)) {
|
|
291
|
+
return new Set();
|
|
292
|
+
}
|
|
293
|
+
return new Set(tools.standardTools.map((s) => s.name));
|
|
294
|
+
};
|
|
295
|
+
const oldNames = names(this._context.tools);
|
|
296
|
+
const newNames = names(newTools);
|
|
297
|
+
const added = [...newNames].filter((n) => !oldNames.has(n));
|
|
298
|
+
const removed = [...oldNames].filter((n) => !newNames.has(n));
|
|
299
|
+
if (added.length === 0 && removed.length === 0) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const ctor = this.constructor;
|
|
303
|
+
const parts = [];
|
|
304
|
+
if (added.length > 0) {
|
|
305
|
+
const list = added
|
|
306
|
+
.sort()
|
|
307
|
+
.map((n) => `\`${n}\``)
|
|
308
|
+
.join(", ");
|
|
309
|
+
parts.push(ctor.TOOL_ACTIVATION_MESSAGE_TEMPLATE.replaceAll("{function_names}", list));
|
|
310
|
+
}
|
|
311
|
+
if (removed.length > 0) {
|
|
312
|
+
const list = removed
|
|
313
|
+
.sort()
|
|
314
|
+
.map((n) => `\`${n}\``)
|
|
315
|
+
.join(", ");
|
|
316
|
+
parts.push(ctor.TOOL_DEACTIVATION_MESSAGE_TEMPLATE.replaceAll("{function_names}", list));
|
|
317
|
+
}
|
|
318
|
+
this._context.addMessage({ role: "developer", content: parts.join(" ") });
|
|
319
|
+
}
|
|
320
|
+
/** Messages from the LLM context. */
|
|
321
|
+
get messages() {
|
|
322
|
+
return this._context.getMessages();
|
|
323
|
+
}
|
|
324
|
+
/** The role for this aggregator. */
|
|
325
|
+
get role() {
|
|
326
|
+
return this._role;
|
|
327
|
+
}
|
|
328
|
+
/** The LLMContext instance used by this aggregator. */
|
|
329
|
+
get context() {
|
|
330
|
+
return this._context;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Create a context frame with the current context.
|
|
334
|
+
*
|
|
335
|
+
* @returns LLMContextFrame containing the current context.
|
|
336
|
+
*/
|
|
337
|
+
_getContextFrame() {
|
|
338
|
+
return new LLMContextFrame({ context: this._context });
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Push a context frame in the specified direction.
|
|
342
|
+
*
|
|
343
|
+
* @param direction The direction to push the frame (upstream or downstream).
|
|
344
|
+
*/
|
|
345
|
+
async pushContextFrame(direction = FrameDirection.DOWNSTREAM) {
|
|
346
|
+
const frame = this._getContextFrame();
|
|
347
|
+
await this.pushFrame(frame, direction);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Add messages to the context.
|
|
351
|
+
*
|
|
352
|
+
* @param messages Messages to add to the conversation context.
|
|
353
|
+
*/
|
|
354
|
+
addMessages(messages) {
|
|
355
|
+
this._context.addMessages(messages);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Set the context messages.
|
|
359
|
+
*
|
|
360
|
+
* @param messages Messages to replace the current context messages.
|
|
361
|
+
*/
|
|
362
|
+
setMessages(messages) {
|
|
363
|
+
this._context.setMessages(messages);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Transform the context messages using a provided function.
|
|
367
|
+
*
|
|
368
|
+
* @param transform A function that takes the current list of messages and
|
|
369
|
+
* returns a modified list of messages to set in the context.
|
|
370
|
+
*/
|
|
371
|
+
transformMessages(transform) {
|
|
372
|
+
this._context.transformMessages(transform);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Set tools in the context.
|
|
376
|
+
*
|
|
377
|
+
* @param tools List of tool definitions to set in the context.
|
|
378
|
+
*/
|
|
379
|
+
setTools(tools) {
|
|
380
|
+
this._context.setTools(tools);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Set tool choice in the context.
|
|
384
|
+
*
|
|
385
|
+
* @param toolChoice Tool choice configuration for the context.
|
|
386
|
+
*/
|
|
387
|
+
setToolChoice(toolChoice) {
|
|
388
|
+
this._context.setToolChoice(toolChoice);
|
|
389
|
+
}
|
|
390
|
+
/** Reset the aggregation state. */
|
|
391
|
+
async reset() {
|
|
392
|
+
this._aggregation = [];
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get the current aggregation as a string.
|
|
396
|
+
*
|
|
397
|
+
* @returns The concatenated aggregation string.
|
|
398
|
+
*/
|
|
399
|
+
aggregationString() {
|
|
400
|
+
return concatenateAggregatedText(this._aggregation);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* User LLM aggregator that aggregates user input during active user turns.
|
|
405
|
+
*
|
|
406
|
+
* This aggregator uses a turn controller and operates within turn boundaries
|
|
407
|
+
* defined by the controller's configured user turn strategies. User turn
|
|
408
|
+
* start strategies indicate when a user turn begins, while user turn stop
|
|
409
|
+
* strategies signal when the user turn has ended.
|
|
410
|
+
*
|
|
411
|
+
* The aggregator collects and aggregates speech-to-text transcriptions that
|
|
412
|
+
* occur while a user turn is active and pushes the final aggregation when the
|
|
413
|
+
* user turn is finished.
|
|
414
|
+
*
|
|
415
|
+
* Event handlers available:
|
|
416
|
+
*
|
|
417
|
+
* - on_user_turn_started: Called when the user turn starts
|
|
418
|
+
* - on_user_turn_stopped: Called when the user turn ends
|
|
419
|
+
* - on_user_turn_stop_timeout: Called when no user turn stop strategy triggers
|
|
420
|
+
* - on_user_turn_idle: Called when the user has been idle for the configured timeout
|
|
421
|
+
* - on_user_turn_message_added: Called when a user message is written to
|
|
422
|
+
* context. In realtime mode (`realtimeServiceMode: true`) the write is
|
|
423
|
+
* triggered by the assistant response start rather than the user-turn-end
|
|
424
|
+
* frame, so this fires decoupled from `on_user_turn_stopped` — subscribe
|
|
425
|
+
* here for the finalized user text.
|
|
426
|
+
* - on_user_mute_started: Called when the user becomes muted
|
|
427
|
+
* - on_user_mute_stopped: Called when the user becomes unmuted
|
|
428
|
+
* - on_user_backchannel: Called when the user acknowledges the bot without
|
|
429
|
+
* interrupting it, from a `UserBackchannelFrame`
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* aggregator.eventHandler("on_user_turn_started", async (aggregator, strategy: BaseUserTurnStartStrategy) => {});
|
|
433
|
+
* aggregator.eventHandler(
|
|
434
|
+
* "on_user_turn_stopped",
|
|
435
|
+
* async (aggregator, strategy: BaseUserTurnStopStrategy, message: UserTurnStoppedMessage) => {},
|
|
436
|
+
* );
|
|
437
|
+
* aggregator.eventHandler("on_user_turn_stop_timeout", async (aggregator) => {});
|
|
438
|
+
* aggregator.eventHandler("on_user_turn_idle", async (aggregator) => {});
|
|
439
|
+
* // In realtime mode the user message is written when the assistant response
|
|
440
|
+
* // starts, not at user-turn-end — subscribe here for the finalized text.
|
|
441
|
+
* aggregator.eventHandler(
|
|
442
|
+
* "on_user_turn_message_added",
|
|
443
|
+
* async (aggregator, message: UserTurnMessageAddedMessage) => {},
|
|
444
|
+
* );
|
|
445
|
+
* aggregator.eventHandler("on_user_mute_started", async (aggregator) => {});
|
|
446
|
+
* aggregator.eventHandler("on_user_mute_stopped", async (aggregator) => {});
|
|
447
|
+
* aggregator.eventHandler("on_user_backchannel", async (aggregator, message: UserBackchannelMessage) => {});
|
|
448
|
+
*/
|
|
449
|
+
export class LLMUserAggregator extends LLMContextAggregator {
|
|
450
|
+
_params;
|
|
451
|
+
// Realtime-mode wiring. null (the default) means auto-configure from
|
|
452
|
+
// service metadata: cascade behavior until a realtime service announces
|
|
453
|
+
// itself, at which point _handleLlmServiceMetadata flips this to true.
|
|
454
|
+
// Explicit true/false force the mode on or off (legacy context writes).
|
|
455
|
+
_realtimeServiceMode;
|
|
456
|
+
_userIsMuted = false;
|
|
457
|
+
_userTurnStartTimestamp = "";
|
|
458
|
+
// One-shot guard: whether the first realtime LLM service metadata frame has
|
|
459
|
+
// been handled this session — see _handleLlmServiceMetadata.
|
|
460
|
+
_realtimeMetadataHandled = false;
|
|
461
|
+
// Realtime-mode deferred-flush state. Realtime mode treats the assistant
|
|
462
|
+
// response start as the "user turn ended" signal for context-writing
|
|
463
|
+
// purposes, then waits up to this many seconds for any in-flight
|
|
464
|
+
// `TranscriptionFrame` to land before flushing — mirroring the cascade-mode
|
|
465
|
+
// pattern of waiting up to `ttfsP99Latency` after
|
|
466
|
+
// `UserStoppedSpeakingFrame`. Captured from the most recent
|
|
467
|
+
// `STTMetadataFrame` (which a realtime LLM service may broadcast for its
|
|
468
|
+
// internal STT pass), falling back to `DEFAULT_TTFS_P99`.
|
|
469
|
+
_ttfsP99Latency = null;
|
|
470
|
+
_realtimeHandoffFlushTask = null;
|
|
471
|
+
// Full transcript across the user turn. Each
|
|
472
|
+
// `_onUserTurnInferenceTriggered` push captures only the new segment since
|
|
473
|
+
// the previous push (pushAggregation resets `_aggregation` after writing to
|
|
474
|
+
// context); we accumulate those segments here so the eventual
|
|
475
|
+
// `on_user_turn_stopped` event surfaces the full turn transcript even when
|
|
476
|
+
// several inferences fire before finalization.
|
|
477
|
+
_fullUserTurnAggregation = null;
|
|
478
|
+
// Whether the current user turn interrupted the bot.
|
|
479
|
+
_userTurnInterruptedBot = false;
|
|
480
|
+
_consecutiveEmptyUserTurnRecoveries = 0;
|
|
481
|
+
// Pair-internal: read by the paired LLMAssistantAggregator.
|
|
482
|
+
_userTurnController;
|
|
483
|
+
_userIdleController;
|
|
484
|
+
// VAD controller
|
|
485
|
+
_vadController = null;
|
|
486
|
+
/**
|
|
487
|
+
* Initialize the user context aggregator.
|
|
488
|
+
*
|
|
489
|
+
* @param context The LLM context for conversation storage.
|
|
490
|
+
*/
|
|
491
|
+
constructor(context, options = {}) {
|
|
492
|
+
const params = options.params || new LLMUserAggregatorParams();
|
|
493
|
+
super({
|
|
494
|
+
...options,
|
|
495
|
+
context,
|
|
496
|
+
role: "user",
|
|
497
|
+
addToolChangeMessages: params.addToolChangeMessages,
|
|
498
|
+
});
|
|
499
|
+
this._params = params;
|
|
500
|
+
this._registerEventHandler("on_user_turn_started");
|
|
501
|
+
this._registerEventHandler("on_user_turn_stopped");
|
|
502
|
+
this._registerEventHandler("on_user_turn_stop_timeout");
|
|
503
|
+
this._registerEventHandler("on_user_turn_idle");
|
|
504
|
+
this._registerEventHandler("on_user_turn_inference_triggered");
|
|
505
|
+
this._registerEventHandler("on_user_turn_message_added");
|
|
506
|
+
this._registerEventHandler("on_user_mute_started");
|
|
507
|
+
this._registerEventHandler("on_user_mute_stopped");
|
|
508
|
+
this._registerEventHandler("on_user_backchannel");
|
|
509
|
+
this._realtimeServiceMode = options._realtimeServiceMode ?? null;
|
|
510
|
+
const userTurnStrategies = this._params.userTurnStrategies || new UserTurnStrategies();
|
|
511
|
+
// Realtime-mode strategy mutation: drop the transcription-based start
|
|
512
|
+
// strategy and flip waitForTranscript on stop strategies that expose it,
|
|
513
|
+
// so turn-stop fires as soon as VAD / the turn analyzer / external frames
|
|
514
|
+
// report end-of-speech.
|
|
515
|
+
//
|
|
516
|
+
// This only runs here when realtime mode was explicitly enabled. When
|
|
517
|
+
// it's auto-configured (the default — _realtimeServiceMode is null until a
|
|
518
|
+
// realtime service announces itself), the mutation is deferred to
|
|
519
|
+
// _handleLlmServiceMetadata, which also applies it on top of any
|
|
520
|
+
// service-recommended external strategies.
|
|
521
|
+
if (this._realtimeServiceMode) {
|
|
522
|
+
this._applyRealtimeModeStrategyMutations(userTurnStrategies, this._params.userTurnStrategies !== null);
|
|
523
|
+
this._disableEmptyUserTurnRecovery();
|
|
524
|
+
}
|
|
525
|
+
this._userTurnController = new UserTurnController({
|
|
526
|
+
userTurnStrategies,
|
|
527
|
+
userTurnStopTimeout: this._params.userTurnStopTimeout,
|
|
528
|
+
});
|
|
529
|
+
this._userTurnController.addEventHandler("on_push_frame", (controller, frame, direction) => this._onPushFrame(controller, frame, direction));
|
|
530
|
+
this._userTurnController.addEventHandler("on_broadcast_frame", (controller, frameCls, frameOptions) => this._onBroadcastFrame(controller, frameCls, frameOptions));
|
|
531
|
+
this._userTurnController.addEventHandler("on_user_turn_speculation_cancelled", (controller) => this._onUserTurnSpeculationCancelled(controller));
|
|
532
|
+
this._userTurnController.addEventHandler("on_user_turn_started", (controller, strategy, params) => this._onUserTurnStarted(controller, strategy, params));
|
|
533
|
+
this._userTurnController.addEventHandler("on_user_turn_inference_triggered", (controller, strategy, speculation) => this._onUserTurnInferenceTriggered(controller, strategy, speculation));
|
|
534
|
+
this._userTurnController.addEventHandler("on_user_turn_stopped", (controller, strategy, params) => this._onUserTurnStopped(controller, strategy, params));
|
|
535
|
+
this._userTurnController.addEventHandler("on_user_turn_stop_timeout", (controller) => this._onUserTurnStopTimeout(controller));
|
|
536
|
+
this._userTurnController.addEventHandler("on_reset_aggregation", (controller, strategy) => this._onResetAggregation(controller, strategy));
|
|
537
|
+
this._userIdleController = new UserIdleController({
|
|
538
|
+
userIdleTimeout: this._params.userIdleTimeout,
|
|
539
|
+
});
|
|
540
|
+
this._userIdleController.addEventHandler("on_user_turn_idle", (controller) => this._onUserTurnIdle(controller));
|
|
541
|
+
// VAD controller
|
|
542
|
+
if (this._params.vadAnalyzer) {
|
|
543
|
+
this._vadController = new VADController(this._params.vadAnalyzer, {
|
|
544
|
+
audioIdleTimeout: this._params.audioIdleTimeout,
|
|
545
|
+
});
|
|
546
|
+
this._vadController.addEventHandler("on_speech_started", (controller) => this._onVadSpeechStarted(controller));
|
|
547
|
+
this._vadController.addEventHandler("on_speech_stopped", (controller) => this._onVadSpeechStopped(controller));
|
|
548
|
+
this._vadController.addEventHandler("on_speech_activity", (controller) => this._onVadSpeechActivity(controller));
|
|
549
|
+
this._vadController.addEventHandler("on_push_frame", (controller, frame, direction) => this._onPushFrame(controller, frame, direction));
|
|
550
|
+
this._vadController.addEventHandler("on_broadcast_frame", (controller, frameCls, frameOptions) => this._onBroadcastFrame(controller, frameCls, frameOptions));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Set up the aggregator.
|
|
555
|
+
*
|
|
556
|
+
* @param setup Configuration object containing setup parameters.
|
|
557
|
+
*/
|
|
558
|
+
async setup(setup) {
|
|
559
|
+
await super.setup(setup);
|
|
560
|
+
if (this._vadController) {
|
|
561
|
+
await this._vadController.setup(setup);
|
|
562
|
+
}
|
|
563
|
+
await this._userTurnController.setup(setup);
|
|
564
|
+
await this._userIdleController.setup(setup);
|
|
565
|
+
for (const s of this._params.userMuteStrategies) {
|
|
566
|
+
await s.setup(setup);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/** Release this aggregator's resources at teardown. */
|
|
570
|
+
async cleanup() {
|
|
571
|
+
await super.cleanup();
|
|
572
|
+
await this._cancelRealtimeHandoffFlushTask();
|
|
573
|
+
if (this._vadController) {
|
|
574
|
+
await this._vadController.cleanup();
|
|
575
|
+
}
|
|
576
|
+
await this._userTurnController.cleanup();
|
|
577
|
+
await this._userIdleController.cleanup();
|
|
578
|
+
for (const s of this._params.userMuteStrategies) {
|
|
579
|
+
await s.cleanup();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Process frames for user speech aggregation and context management.
|
|
584
|
+
*
|
|
585
|
+
* @param frame The frame to process.
|
|
586
|
+
* @param direction The direction of frame flow in the pipeline.
|
|
587
|
+
*/
|
|
588
|
+
async processFrame(frame, direction) {
|
|
589
|
+
await super.processFrame(frame, direction);
|
|
590
|
+
if (await this._maybeMuteFrame(frame)) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (frame instanceof StartFrame) {
|
|
594
|
+
await this.pushFrame(frame, direction);
|
|
595
|
+
await this._start(frame);
|
|
596
|
+
}
|
|
597
|
+
else if (frame instanceof EndFrame) {
|
|
598
|
+
// Push EndFrame before stop(), because stop() waits on the task to
|
|
599
|
+
// finish and the task finishes when EndFrame is processed.
|
|
600
|
+
await this.pushFrame(frame, direction);
|
|
601
|
+
await this._stop(frame);
|
|
602
|
+
}
|
|
603
|
+
else if (frame instanceof CancelFrame) {
|
|
604
|
+
await this._cancel(frame);
|
|
605
|
+
await this.pushFrame(frame, direction);
|
|
606
|
+
}
|
|
607
|
+
else if (frame instanceof TranscriptionFrame) {
|
|
608
|
+
await this._handleTranscription(frame);
|
|
609
|
+
}
|
|
610
|
+
else if (frame instanceof InterimTranscriptionFrame ||
|
|
611
|
+
frame instanceof TranslationFrame ||
|
|
612
|
+
frame instanceof EagerTranscriptionFrame) {
|
|
613
|
+
// Interim transcriptions, translations and eager end-of-turn
|
|
614
|
+
// transcripts are consumed here and not pushed downstream, same as
|
|
615
|
+
// final TranscriptionFrame. The turn strategies still see them: the
|
|
616
|
+
// controller is fed every frame below.
|
|
617
|
+
}
|
|
618
|
+
else if (frame instanceof UserBackchannelFrame) {
|
|
619
|
+
const message = new UserBackchannelMessage({
|
|
620
|
+
content: frame.text,
|
|
621
|
+
timestamp: frame.timestamp,
|
|
622
|
+
userId: frame.userId,
|
|
623
|
+
});
|
|
624
|
+
await this._callEventHandler("on_user_backchannel", message);
|
|
625
|
+
await this.pushFrame(frame, direction);
|
|
626
|
+
}
|
|
627
|
+
else if (frame instanceof LLMRunFrame) {
|
|
628
|
+
await this._handleLlmRun(frame);
|
|
629
|
+
}
|
|
630
|
+
else if (frame instanceof LLMMessagesAppendFrame) {
|
|
631
|
+
await this._handleLlmMessagesAppend(frame);
|
|
632
|
+
}
|
|
633
|
+
else if (frame instanceof LLMMessagesUpdateFrame) {
|
|
634
|
+
await this._handleLlmMessagesUpdate(frame);
|
|
635
|
+
}
|
|
636
|
+
else if (frame instanceof LLMMessagesTransformFrame) {
|
|
637
|
+
await this._handleLlmMessagesTransform(frame);
|
|
638
|
+
}
|
|
639
|
+
else if (frame instanceof LLMSetToolsFrame) {
|
|
640
|
+
// Normalize and validate (a plain list of direct functions /
|
|
641
|
+
// FunctionSchema objects becomes a ToolsSchema) so the tool-change diff
|
|
642
|
+
// and setTools see a consistent type.
|
|
643
|
+
const normalizedTools = LLMContext._normalizeAndValidateTools(frame.tools);
|
|
644
|
+
this._maybeAddToolChangeMessages(normalizedTools);
|
|
645
|
+
this.setTools(normalizedTools);
|
|
646
|
+
// Push the LLMSetToolsFrame as well, since speech-to-speech LLM
|
|
647
|
+
// services (like OpenAI Realtime) may need to know about tool changes;
|
|
648
|
+
// unlike text-based LLM services they won't just "pick up the change"
|
|
649
|
+
// on the next LLM run, as the LLM is continuously running.
|
|
650
|
+
await this.pushFrame(frame, direction);
|
|
651
|
+
}
|
|
652
|
+
else if (frame instanceof LLMSetToolChoiceFrame) {
|
|
653
|
+
this.setToolChoice(frame.toolChoice);
|
|
654
|
+
}
|
|
655
|
+
else if (frame instanceof ServiceMetadataFrame) {
|
|
656
|
+
await this._handleServiceMetadata(frame);
|
|
657
|
+
await this.pushFrame(frame, direction);
|
|
658
|
+
}
|
|
659
|
+
else if (frame instanceof ProposedUserStartedSpeakingFrame) {
|
|
660
|
+
// A proposal is resolved once. Forwarding one our own strategies
|
|
661
|
+
// resolve would let a resolver further down the pipeline decide the
|
|
662
|
+
// same turn a second time.
|
|
663
|
+
if (!this._userTurnController.resolvesProposedTurnStartFrames) {
|
|
664
|
+
await this.pushFrame(frame, direction);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (frame instanceof ProposedUserStoppedSpeakingFrame) {
|
|
668
|
+
if (!this._userTurnController.resolvesProposedTurnStopFrames) {
|
|
669
|
+
await this.pushFrame(frame, direction);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
await this.pushFrame(frame, direction);
|
|
674
|
+
}
|
|
675
|
+
if (this._vadController) {
|
|
676
|
+
await this._vadController.processFrame(frame);
|
|
677
|
+
}
|
|
678
|
+
await this._userTurnController.processFrame(frame);
|
|
679
|
+
await this._userIdleController.processFrame(frame);
|
|
680
|
+
}
|
|
681
|
+
/** Push the current aggregation. */
|
|
682
|
+
async pushAggregation() {
|
|
683
|
+
return await this._pushAggregation();
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Write the aggregated user turn to the context.
|
|
687
|
+
*
|
|
688
|
+
* @param options.runLlm Whether to emit the LLMContextFrame that runs
|
|
689
|
+
* inference. False when the response is already generated — an eager end
|
|
690
|
+
* of turn that held — and only the context write is still owed.
|
|
691
|
+
* @returns The text written, or "" when there was nothing to write.
|
|
692
|
+
*/
|
|
693
|
+
async _pushAggregation(options = {}) {
|
|
694
|
+
const runLlm = options.runLlm ?? true;
|
|
695
|
+
if (this._aggregation.length === 0) {
|
|
696
|
+
return "";
|
|
697
|
+
}
|
|
698
|
+
const aggregation = this.aggregationString();
|
|
699
|
+
await this.reset();
|
|
700
|
+
this._context.addMessage({ role: this.role, content: aggregation });
|
|
701
|
+
await this.broadcastFrame(LLMContextUserTurnFrame, {
|
|
702
|
+
text: aggregation,
|
|
703
|
+
timestamp: this._userTurnStartTimestamp,
|
|
704
|
+
});
|
|
705
|
+
if (runLlm) {
|
|
706
|
+
await this.pushContextFrame();
|
|
707
|
+
}
|
|
708
|
+
const message = new UserTurnMessageAddedMessage({
|
|
709
|
+
content: aggregation,
|
|
710
|
+
timestamp: this._userTurnStartTimestamp,
|
|
711
|
+
});
|
|
712
|
+
await this._callEventHandler("on_user_turn_message_added", message);
|
|
713
|
+
return aggregation;
|
|
714
|
+
}
|
|
715
|
+
async _start(_frame) {
|
|
716
|
+
await this._startControllers();
|
|
717
|
+
}
|
|
718
|
+
async _stop(_frame) {
|
|
719
|
+
if (this._realtimeServiceMode) {
|
|
720
|
+
// Realtime mode: cancel any pending deferred handoff flush and commit
|
|
721
|
+
// trailing user content directly. The on_user_turn_stopped event
|
|
722
|
+
// already fired (if turn frames were emitted), so don't re-fire it from
|
|
723
|
+
// session end.
|
|
724
|
+
await this._cancelRealtimeHandoffFlushTask();
|
|
725
|
+
await this.pushAggregation();
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
await this._maybeEmitUserTurnStopped({ onSessionEnd: true });
|
|
729
|
+
}
|
|
730
|
+
await this._stopControllers();
|
|
731
|
+
}
|
|
732
|
+
async _cancel(_frame) {
|
|
733
|
+
// See _stop — same realtime-mode vs cascade dispatch.
|
|
734
|
+
if (this._realtimeServiceMode) {
|
|
735
|
+
await this._cancelRealtimeHandoffFlushTask();
|
|
736
|
+
await this.pushAggregation();
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
await this._maybeEmitUserTurnStopped({ onSessionEnd: true });
|
|
740
|
+
}
|
|
741
|
+
await this._stopControllers();
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Mutate turn strategies for realtime mode.
|
|
745
|
+
*
|
|
746
|
+
* Drops `TranscriptionUserTurnStartStrategy` from the start strategies
|
|
747
|
+
* (transcripts shouldn't start a turn when the realtime service drives the
|
|
748
|
+
* conversation) and flips `waitForTranscript = false` on stop strategies
|
|
749
|
+
* that expose the flag, so end-of-turn fires as soon as VAD / the turn
|
|
750
|
+
* analyzer reports end-of-speech.
|
|
751
|
+
*/
|
|
752
|
+
_applyRealtimeModeStrategyMutations(userTurnStrategies, areUserProvidedCustomStrategies) {
|
|
753
|
+
const startStrategies = userTurnStrategies.start ?? [];
|
|
754
|
+
const dropped = [];
|
|
755
|
+
const keptStart = [];
|
|
756
|
+
for (const s of startStrategies) {
|
|
757
|
+
if (s instanceof TranscriptionUserTurnStartStrategy) {
|
|
758
|
+
dropped.push(s.constructor.name);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
keptStart.push(s);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
userTurnStrategies.start = keptStart;
|
|
765
|
+
const flipped = [];
|
|
766
|
+
for (const s of userTurnStrategies.stop ?? []) {
|
|
767
|
+
// The flag is a property on some stop strategies, not on the base.
|
|
768
|
+
const withFlag = s;
|
|
769
|
+
if (withFlag.waitForTranscript) {
|
|
770
|
+
try {
|
|
771
|
+
withFlag.waitForTranscript = false;
|
|
772
|
+
flipped.push(s.constructor.name);
|
|
773
|
+
}
|
|
774
|
+
catch (e) {
|
|
775
|
+
rethrowIfCancelled(e);
|
|
776
|
+
// Strategy exposes the property but no setter — skip.
|
|
777
|
+
if (!(e instanceof TypeError))
|
|
778
|
+
throw e;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (dropped.length === 0 && flipped.length === 0) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const msg = `${this}: realtime mode — mutated turn strategies: ` +
|
|
786
|
+
`dropped ${dropped.length > 0 ? reprList(dropped) : "no"} start strategy(ies); set ` +
|
|
787
|
+
`wait_for_transcript=False on ${flipped.length > 0 ? reprList(flipped) : "no"} stop strategy(ies).`;
|
|
788
|
+
if (areUserProvidedCustomStrategies) {
|
|
789
|
+
logger.warning(msg);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
logger.debug(msg);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Turn off empty user turn recovery for realtime mode.
|
|
797
|
+
*
|
|
798
|
+
* A realtime LLM service hears the user's audio directly, so an empty
|
|
799
|
+
* transcript doesn't mean the model missed the speech.
|
|
800
|
+
*/
|
|
801
|
+
_disableEmptyUserTurnRecovery() {
|
|
802
|
+
if (this._params.emptyUserTurn === null) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
this._params.emptyUserTurn = null;
|
|
806
|
+
logger.debug(`${this}: realtime mode — empty user turn recovery disabled; the realtime ` +
|
|
807
|
+
"LLM service hears the user's audio directly.");
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Dispatch a service metadata frame.
|
|
811
|
+
*
|
|
812
|
+
* Any service may recommend user turn strategies via the base
|
|
813
|
+
* `userTurnStrategies` field, so apply that first — regardless of service
|
|
814
|
+
* type or realtime mode — then run type-specific handling.
|
|
815
|
+
*/
|
|
816
|
+
async _handleServiceMetadata(frame) {
|
|
817
|
+
await this._handleServiceUserTurnStrategies(frame.serviceName, frame.userTurnStrategies);
|
|
818
|
+
if (frame instanceof STTMetadataFrame) {
|
|
819
|
+
await this._handleSttMetadata(frame);
|
|
820
|
+
}
|
|
821
|
+
else if (frame instanceof LLMServiceMetadataFrame) {
|
|
822
|
+
await this._handleLlmServiceMetadata(frame);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Handle an `STTMetadataFrame` broadcast by an STT service at start.
|
|
827
|
+
*
|
|
828
|
+
* Captures the STT TTFS P99 so the realtime-mode deferred handoff flush can
|
|
829
|
+
* size itself to the real transcript-arrival latency. Any recommended user
|
|
830
|
+
* turn strategies were already applied by `_handleServiceMetadata`.
|
|
831
|
+
*/
|
|
832
|
+
async _handleSttMetadata(frame) {
|
|
833
|
+
this._ttfsP99Latency = frame.ttfsP99Latency;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Apply user turn strategies a service recommends via its metadata frame.
|
|
837
|
+
*
|
|
838
|
+
* Honored only when the user did not pass their own `userTurnStrategies`,
|
|
839
|
+
* which always wins. `updateStrategies` is idempotent, so this is safe to
|
|
840
|
+
* re-run on a later broadcast (e.g. a ServiceSwitcher switch).
|
|
841
|
+
*/
|
|
842
|
+
async _handleServiceUserTurnStrategies(serviceName, userTurnStrategies) {
|
|
843
|
+
if (userTurnStrategies === null || userTurnStrategies === undefined) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (this._params.userTurnStrategies !== null) {
|
|
847
|
+
logger.debug(`${this}: ignoring user turn strategies recommended by ` +
|
|
848
|
+
`\`${serviceName}\`; using the user-provided strategies.`);
|
|
849
|
+
this._warnOnDiscardedInterruptionSetting(serviceName, userTurnStrategies);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
logger.debug(`${this}: applying user turn strategies recommended by \`${serviceName}\`.`);
|
|
853
|
+
await this._userTurnController.updateStrategies(userTurnStrategies);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Warn when (maybe unintentionally) overruling a service silently turns interruptions back on.
|
|
857
|
+
*
|
|
858
|
+
* A service recommends turn strategies by putting them on its
|
|
859
|
+
* `ServiceMetadataFrame.userTurnStrategies`, and one configured with
|
|
860
|
+
* `shouldInterrupt: false` carries that setting there. User-provided
|
|
861
|
+
* strategies discard the recommendation whole — so the pipeline would
|
|
862
|
+
* start interrupting with no indication why.
|
|
863
|
+
*
|
|
864
|
+
* Passing `ExternalUserTurnStrategies` explicitly was a safe way to opt out
|
|
865
|
+
* of the recommendation until 1.8.0, because the external start strategy
|
|
866
|
+
* never interrupted no matter how it was constructed. It broadcasts the
|
|
867
|
+
* interruption now, so explicit strategies can contradict the service, and
|
|
868
|
+
* this warning names the two ways to reconcile them.
|
|
869
|
+
*
|
|
870
|
+
* Only that container is checked. A `UserTurnStrategies` assembled by hand
|
|
871
|
+
* around a bare `ExternalUserTurnStartStrategy` contradicts the service the
|
|
872
|
+
* same way and goes unwarned — not expected to come up often enough to
|
|
873
|
+
* justify inspecting the individual strategies.
|
|
874
|
+
*/
|
|
875
|
+
_warnOnDiscardedInterruptionSetting(serviceName, recommended) {
|
|
876
|
+
if (!(recommended instanceof ExternalUserTurnStrategies)) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (recommended.enableInterruptions) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const provided = this._params.userTurnStrategies;
|
|
883
|
+
if (!(provided instanceof ExternalUserTurnStrategies) || !provided.enableInterruptions) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
logger.warning(`${this}: \`${serviceName}\` asked for interruptions to stay off, but the ` +
|
|
887
|
+
"user-provided `ExternalUserTurnStrategies` leaves them on, so the bot will " +
|
|
888
|
+
"be interrupted when the user starts speaking. Drop `user_turn_strategies` to " +
|
|
889
|
+
"use the service's recommendation, or pass " +
|
|
890
|
+
"`ExternalUserTurnStrategies(enable_interruptions=False)`.");
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Handle an `LLMServiceMetadataFrame` broadcast by an LLM service.
|
|
894
|
+
*
|
|
895
|
+
* Non-realtime services need no configuration. For a realtime service,
|
|
896
|
+
* auto-configure `realtimeServiceMode` (once) when the user left it unset
|
|
897
|
+
* (`null`); an explicit `true`/`false` is honored as-is (`false` opts out
|
|
898
|
+
* silently). When the mode is active, strip transcript dependence from the
|
|
899
|
+
* active strategies — including any external strategies
|
|
900
|
+
* `_handleServiceMetadata` just adopted.
|
|
901
|
+
*
|
|
902
|
+
* The strategy mutation re-runs on every broadcast (so a re-adopted
|
|
903
|
+
* recommendation is re-mutated, e.g. after a ServiceSwitcher switch); only
|
|
904
|
+
* the auto-configuration + log is one-shot. `updateStrategies` is
|
|
905
|
+
* idempotent, so re-applying is safe.
|
|
906
|
+
*/
|
|
907
|
+
async _handleLlmServiceMetadata(frame) {
|
|
908
|
+
if (!frame.isRealtimeService) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
// Auto-configure + log once per session.
|
|
912
|
+
if (!this._realtimeMetadataHandled) {
|
|
913
|
+
this._realtimeMetadataHandled = true;
|
|
914
|
+
if (this._realtimeServiceMode === null) {
|
|
915
|
+
this._realtimeServiceMode = true;
|
|
916
|
+
logger.debug(`${this}: detected realtime service \`${frame.serviceName}\`; ` +
|
|
917
|
+
"auto-enabled realtime_service_mode.");
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (!this._realtimeServiceMode) {
|
|
921
|
+
// Explicitly disabled — honor it silently; the user opted out.
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
this._disableEmptyUserTurnRecovery();
|
|
925
|
+
const strategies = this._userTurnController.userTurnStrategies;
|
|
926
|
+
this._applyRealtimeModeStrategyMutations(strategies, this._params.userTurnStrategies !== null);
|
|
927
|
+
await this._userTurnController.updateStrategies(strategies);
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Commit the user message in realtime mode, allowing for late transcripts.
|
|
931
|
+
*
|
|
932
|
+
* Called by the paired assistant half when the assistant response starts —
|
|
933
|
+
* realtime mode treats that as the user turn's end signal for
|
|
934
|
+
* context-writing purposes (in place of `UserStoppedSpeakingFrame`).
|
|
935
|
+
* Realtime services may deliver the finalized user `TranscriptionFrame` in
|
|
936
|
+
* pieces, and some deliver it only after the assistant starts responding,
|
|
937
|
+
* so we always defer the flush by `ttfsP99Latency` to let any in-flight
|
|
938
|
+
* transcript land. The `_realtimeHandoffFlushImmediate` failsafe on
|
|
939
|
+
* `LLMFullResponseEndFrame` catches any stragglers.
|
|
940
|
+
*
|
|
941
|
+
* Pair-internal: called by the paired LLMAssistantAggregator.
|
|
942
|
+
*/
|
|
943
|
+
async _realtimeHandoffFlush() {
|
|
944
|
+
// Cancel any prior deferred flush task — a new turn supersedes it.
|
|
945
|
+
await this._cancelRealtimeHandoffFlushTask();
|
|
946
|
+
this._realtimeHandoffFlushTask = this.createTask(() => this._realtimeDeferredHandoffFlush(), `${this}::realtime_handoff_flush`);
|
|
947
|
+
}
|
|
948
|
+
/** Wait one `ttfsP99Latency` window, then flush whatever has arrived. */
|
|
949
|
+
async _realtimeDeferredHandoffFlush() {
|
|
950
|
+
const wait = this._ttfsP99Latency !== null ? this._ttfsP99Latency : DEFAULT_TTFS_P99;
|
|
951
|
+
await sleep(wait);
|
|
952
|
+
if (this._aggregation.length > 0) {
|
|
953
|
+
await this.pushAggregation();
|
|
954
|
+
this._userTurnStartTimestamp = "";
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Failsafe flush invoked when the assistant response ends.
|
|
959
|
+
*
|
|
960
|
+
* By the time the assistant response has fully completed, even slow
|
|
961
|
+
* transcripts should have landed. Cancel any pending deferred flush and
|
|
962
|
+
* write the user message now so it precedes the assistant message in
|
|
963
|
+
* context.
|
|
964
|
+
*
|
|
965
|
+
* Pair-internal: called by the paired LLMAssistantAggregator.
|
|
966
|
+
*/
|
|
967
|
+
async _realtimeHandoffFlushImmediate() {
|
|
968
|
+
await this._cancelRealtimeHandoffFlushTask();
|
|
969
|
+
if (this._aggregation.length > 0) {
|
|
970
|
+
await this.pushAggregation();
|
|
971
|
+
this._userTurnStartTimestamp = "";
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
async _cancelRealtimeHandoffFlushTask() {
|
|
975
|
+
if (this._realtimeHandoffFlushTask === null) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (!this._realtimeHandoffFlushTask.done()) {
|
|
979
|
+
await this.cancelTask(this._realtimeHandoffFlushTask);
|
|
980
|
+
}
|
|
981
|
+
this._realtimeHandoffFlushTask = null;
|
|
982
|
+
}
|
|
983
|
+
async _startControllers() {
|
|
984
|
+
if (this._vadController) {
|
|
985
|
+
await this._vadController.start();
|
|
986
|
+
}
|
|
987
|
+
await this._userTurnController.start();
|
|
988
|
+
}
|
|
989
|
+
async _stopControllers() {
|
|
990
|
+
// At session end the controllers' timers can only report what ending
|
|
991
|
+
// looks like: no audio arriving, no turn finishing, the user idle. They
|
|
992
|
+
// stop here, while what they hold (the VAD analyzer, the turn strategies)
|
|
993
|
+
// may be shared and is released in cleanup() instead.
|
|
994
|
+
await this._cancelRealtimeHandoffFlushTask();
|
|
995
|
+
if (this._vadController) {
|
|
996
|
+
await this._vadController.stop();
|
|
997
|
+
}
|
|
998
|
+
await this._userTurnController.stop();
|
|
999
|
+
await this._userIdleController.stop();
|
|
1000
|
+
}
|
|
1001
|
+
async _maybeMuteFrame(frame) {
|
|
1002
|
+
// Lifecycle frames should never be muted and should not trigger mute
|
|
1003
|
+
// state changes. Evaluating mute strategies on StartFrame would broadcast
|
|
1004
|
+
// UserMuteStartedFrame before StartFrame reaches downstream processors.
|
|
1005
|
+
if (frame instanceof StartFrame || frame instanceof EndFrame || frame instanceof CancelFrame) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
const shouldMuteFrame = this._userIsMuted &&
|
|
1009
|
+
(frame instanceof InterruptionFrame ||
|
|
1010
|
+
frame instanceof VADUserStartedSpeakingFrame ||
|
|
1011
|
+
frame instanceof VADUserStoppedSpeakingFrame ||
|
|
1012
|
+
frame instanceof ProposedUserStartedSpeakingFrame ||
|
|
1013
|
+
frame instanceof ProposedUserStoppedSpeakingFrame ||
|
|
1014
|
+
frame instanceof UserStartedSpeakingFrame ||
|
|
1015
|
+
frame instanceof UserStoppedSpeakingFrame ||
|
|
1016
|
+
frame instanceof InputAudioRawFrame ||
|
|
1017
|
+
frame instanceof InterimTranscriptionFrame ||
|
|
1018
|
+
frame instanceof TranscriptionFrame ||
|
|
1019
|
+
frame instanceof EagerTranscriptionFrame ||
|
|
1020
|
+
frame instanceof UserBackchannelFrame);
|
|
1021
|
+
if (shouldMuteFrame) {
|
|
1022
|
+
logger.trace(`${frame.name} suppressed - user currently muted`);
|
|
1023
|
+
}
|
|
1024
|
+
let shouldMuteNextTime = false;
|
|
1025
|
+
for (const s of this._params.userMuteStrategies) {
|
|
1026
|
+
// Every strategy sees the frame, so no short-circuiting.
|
|
1027
|
+
const mute = await s.processFrame(frame);
|
|
1028
|
+
shouldMuteNextTime = shouldMuteNextTime || Boolean(mute);
|
|
1029
|
+
}
|
|
1030
|
+
if (shouldMuteNextTime !== this._userIsMuted) {
|
|
1031
|
+
logger.debug(`${this}: user is now ${shouldMuteNextTime ? "muted" : "unmuted"}`);
|
|
1032
|
+
this._userIsMuted = shouldMuteNextTime;
|
|
1033
|
+
// Emit mute state change events
|
|
1034
|
+
if (this._userIsMuted) {
|
|
1035
|
+
await this._callEventHandler("on_user_mute_started");
|
|
1036
|
+
await this.broadcastFrame(UserMuteStartedFrame);
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
await this._callEventHandler("on_user_mute_stopped");
|
|
1040
|
+
await this.broadcastFrame(UserMuteStoppedFrame);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return shouldMuteFrame;
|
|
1044
|
+
}
|
|
1045
|
+
async _handleLlmRun(_frame) {
|
|
1046
|
+
await this.pushContextFrame();
|
|
1047
|
+
}
|
|
1048
|
+
async _handleLlmMessagesAppend(frame) {
|
|
1049
|
+
this.addMessages(frame.messages);
|
|
1050
|
+
if (frame.runLlm) {
|
|
1051
|
+
await this.pushContextFrame();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
async _handleLlmMessagesUpdate(frame) {
|
|
1055
|
+
this.setMessages(frame.messages);
|
|
1056
|
+
if (frame.runLlm) {
|
|
1057
|
+
await this.pushContextFrame();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async _handleLlmMessagesTransform(frame) {
|
|
1061
|
+
this.transformMessages(frame.transform);
|
|
1062
|
+
if (frame.runLlm) {
|
|
1063
|
+
await this.pushContextFrame();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async _handleTranscription(frame) {
|
|
1067
|
+
const text = frame.text;
|
|
1068
|
+
// Make sure we really have some text.
|
|
1069
|
+
if (!text.trim()) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
// In realtime mode with a service that doesn't emit user-turn frames
|
|
1073
|
+
// (Gemini Live, AWS Nova Sonic, Ultravox), `_onUserTurnStarted` never
|
|
1074
|
+
// fires, so seed `_userTurnStartTimestamp` here from the first transcript
|
|
1075
|
+
// so the eventual `on_user_turn_message_added` event carries one.
|
|
1076
|
+
// Harmless in cascade mode — turn frames set it before this runs (the
|
|
1077
|
+
// timestamp check is a no-op).
|
|
1078
|
+
if (!this._userTurnStartTimestamp) {
|
|
1079
|
+
this._userTurnStartTimestamp = timeNowIso8601();
|
|
1080
|
+
}
|
|
1081
|
+
// Transcriptions never include inter-part spaces (so far).
|
|
1082
|
+
this._aggregation.push(new TextPartForConcatenation({
|
|
1083
|
+
text,
|
|
1084
|
+
includesInterPartSpaces: frame.includesInterFrameSpaces,
|
|
1085
|
+
}));
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Broadcasts a frame upstream and queues it for internal processing.
|
|
1089
|
+
*
|
|
1090
|
+
* Queues the frame so it flows through `processFrame` and is handled
|
|
1091
|
+
* internally (e.g. by the `UserTurnController`). The upstream frame is
|
|
1092
|
+
* pushed directly.
|
|
1093
|
+
*
|
|
1094
|
+
* @param frameCls The class of the frame to be broadcasted.
|
|
1095
|
+
* @param options Options to be passed to the frame's constructor.
|
|
1096
|
+
*/
|
|
1097
|
+
async _queuedBroadcastFrame(frameCls, options) {
|
|
1098
|
+
await this.queueFrame(new frameCls(options));
|
|
1099
|
+
await this.pushFrame(new frameCls(options), FrameDirection.UPSTREAM);
|
|
1100
|
+
}
|
|
1101
|
+
async _onPushFrame(_controller, frame, direction = FrameDirection.DOWNSTREAM) {
|
|
1102
|
+
await this.queueFrame(frame, direction);
|
|
1103
|
+
}
|
|
1104
|
+
async _onBroadcastFrame(_controller, frameCls, options) {
|
|
1105
|
+
await this._queuedBroadcastFrame(frameCls, options);
|
|
1106
|
+
}
|
|
1107
|
+
async _onUserTurnSpeculationCancelled(_controller) {
|
|
1108
|
+
// Broadcast rather than queued, so it reaches a downstream gate ahead of
|
|
1109
|
+
// the turn end that follows it. Queued, it would arrive after, and the
|
|
1110
|
+
// gate would release the response this withdraws.
|
|
1111
|
+
await this.broadcastFrame(EagerEndOfTurnCancelFrame);
|
|
1112
|
+
}
|
|
1113
|
+
async _onVadSpeechStarted(controller) {
|
|
1114
|
+
const analyzer = controller._vadAnalyzer;
|
|
1115
|
+
await this._queuedBroadcastFrame(VADUserStartedSpeakingFrame, {
|
|
1116
|
+
startSecs: analyzer.params.startSecs,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
async _onVadSpeechStopped(controller) {
|
|
1120
|
+
const analyzer = controller._vadAnalyzer;
|
|
1121
|
+
await this._queuedBroadcastFrame(VADUserStoppedSpeakingFrame, {
|
|
1122
|
+
stopSecs: analyzer.params.stopSecs,
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
async _onVadSpeechActivity(_controller) {
|
|
1126
|
+
await this._queuedBroadcastFrame(UserSpeakingFrame);
|
|
1127
|
+
}
|
|
1128
|
+
async _onUserTurnStarted(_controller, strategy, params) {
|
|
1129
|
+
logger.debug(`${this}: User started speaking (strategy: ${strategy})`);
|
|
1130
|
+
this._userTurnStartTimestamp = timeNowIso8601();
|
|
1131
|
+
this._fullUserTurnAggregation = null;
|
|
1132
|
+
// Unless the bot is waiting for the user, it's thinking, speaking or
|
|
1133
|
+
// running a function call, and the interruption below cancels that.
|
|
1134
|
+
this._userTurnInterruptedBot =
|
|
1135
|
+
params.enableInterruptions && !this._userIdleController.waitingForUser;
|
|
1136
|
+
if (params.enableUserSpeakingFrames) {
|
|
1137
|
+
await this.broadcastFrame(UserStartedSpeakingFrame);
|
|
1138
|
+
}
|
|
1139
|
+
await this._userIdleController.processFrame(new UserStartedSpeakingFrame());
|
|
1140
|
+
if (params.enableInterruptions) {
|
|
1141
|
+
await this.broadcastInterruption();
|
|
1142
|
+
}
|
|
1143
|
+
await this._callEventHandler("on_user_turn_started", strategy);
|
|
1144
|
+
}
|
|
1145
|
+
async _onUserTurnInferenceTriggered(_controller, strategy, speculation = null) {
|
|
1146
|
+
if (this._realtimeServiceMode) {
|
|
1147
|
+
// Realtime mode: the assistant response start, not turn frames, is the
|
|
1148
|
+
// signal that the user turn has ended for context-writing purposes.
|
|
1149
|
+
// Fire the event without pushing aggregation here.
|
|
1150
|
+
logger.debug(`${this}: User turn inference triggered (strategy: ${strategy}) ` +
|
|
1151
|
+
"[realtime mode: event-only, no context push]");
|
|
1152
|
+
await this._callEventHandler("on_user_turn_inference_triggered", strategy);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (speculation) {
|
|
1156
|
+
logger.debug(`${this}: User turn inference triggered speculatively (strategy: ${strategy})`);
|
|
1157
|
+
await this._runSpeculativeInference(speculation);
|
|
1158
|
+
await this._callEventHandler("on_user_turn_inference_triggered", strategy);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
logger.debug(`${this}: User turn inference triggered (strategy: ${strategy})`);
|
|
1162
|
+
// Push aggregation now: this writes the user message segment to the
|
|
1163
|
+
// context and emits LLMContextFrame, which kicks LLM inference.
|
|
1164
|
+
// Concatenate the segment into `_fullUserTurnAggregation` so multiple
|
|
1165
|
+
// inferences in the same turn don't lose earlier segments from the
|
|
1166
|
+
// eventual `on_user_turn_stopped` event.
|
|
1167
|
+
const segment = await this.pushAggregation();
|
|
1168
|
+
if (segment) {
|
|
1169
|
+
if (this._fullUserTurnAggregation) {
|
|
1170
|
+
this._fullUserTurnAggregation = `${this._fullUserTurnAggregation} ${segment}`.trim();
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
this._fullUserTurnAggregation = segment;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
await this._callEventHandler("on_user_turn_inference_triggered", strategy);
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Run an inference for a turn that hasn't ended yet.
|
|
1180
|
+
*
|
|
1181
|
+
* The turn is still open, so the context must not record it. The inference
|
|
1182
|
+
* runs against a provisional copy carrying the speculated turn text:
|
|
1183
|
+
* nothing here mutates the real context, and `_aggregation` keeps
|
|
1184
|
+
* accumulating for whenever the turn does end. The response is held
|
|
1185
|
+
* downstream until the turn is confirmed, and discarded if it isn't.
|
|
1186
|
+
*
|
|
1187
|
+
* @param speculation The speculation to run, from the stop strategy that
|
|
1188
|
+
* started it.
|
|
1189
|
+
*/
|
|
1190
|
+
async _runSpeculativeInference(speculation) {
|
|
1191
|
+
const provisional = new LLMContext({
|
|
1192
|
+
messages: [...this._context.messages, { role: this.role, content: speculation.text }],
|
|
1193
|
+
tools: this._context.tools,
|
|
1194
|
+
toolChoice: this._context.toolChoice,
|
|
1195
|
+
});
|
|
1196
|
+
await this.pushFrame(new LLMContextFrame({ context: provisional, speculation: true }));
|
|
1197
|
+
}
|
|
1198
|
+
async _onUserTurnStopped(_controller, strategy, params) {
|
|
1199
|
+
logger.debug(`${this}: User stopped speaking (strategy: ${strategy})`);
|
|
1200
|
+
if (params.enableUserSpeakingFrames) {
|
|
1201
|
+
await this.broadcastFrame(UserStoppedSpeakingFrame);
|
|
1202
|
+
}
|
|
1203
|
+
await this._userIdleController.processFrame(new UserStoppedSpeakingFrame());
|
|
1204
|
+
if (this._realtimeServiceMode) {
|
|
1205
|
+
// Realtime mode: the user message isn't finalized at turn-stop time —
|
|
1206
|
+
// the assistant response start is the effective end-of-turn signal,
|
|
1207
|
+
// and the user message is written then. Content is null here;
|
|
1208
|
+
// subscribers wanting the finalized text use on_user_turn_message_added
|
|
1209
|
+
// instead.
|
|
1210
|
+
const message = new UserTurnStoppedMessage({
|
|
1211
|
+
content: null,
|
|
1212
|
+
timestamp: this._userTurnStartTimestamp,
|
|
1213
|
+
});
|
|
1214
|
+
await this._callEventHandler("on_user_turn_stopped", strategy, message);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
// A turn end that confirms a speculation has its response already: the
|
|
1218
|
+
// context write is all that's left, and running inference again would
|
|
1219
|
+
// answer the same turn twice.
|
|
1220
|
+
await this._maybeEmitUserTurnStopped({
|
|
1221
|
+
strategy,
|
|
1222
|
+
runLlm: !params.confirmsSpeculation,
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
async _onResetAggregation(_controller, strategy) {
|
|
1226
|
+
logger.debug(`${this}: Resetting aggregation (strategy: ${strategy})`);
|
|
1227
|
+
await this.reset();
|
|
1228
|
+
}
|
|
1229
|
+
async _onUserTurnStopTimeout(_controller) {
|
|
1230
|
+
await this._callEventHandler("on_user_turn_stop_timeout");
|
|
1231
|
+
}
|
|
1232
|
+
async _onUserTurnIdle(_controller) {
|
|
1233
|
+
await this._callEventHandler("on_user_turn_idle");
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Maybe emit user turn stopped event.
|
|
1237
|
+
*
|
|
1238
|
+
* Earlier inference triggers in the same turn have already pushed their
|
|
1239
|
+
* segments to the context and accumulated them into
|
|
1240
|
+
* `this._fullUserTurnAggregation`. Any aggregation that arrived after the
|
|
1241
|
+
* last inference trigger is flushed here so end-of-turn content is never
|
|
1242
|
+
* lost from the public event.
|
|
1243
|
+
*
|
|
1244
|
+
* @param options.strategy The strategy that triggered the turn stop.
|
|
1245
|
+
* @param options.onSessionEnd If true, only emit if there's unemitted
|
|
1246
|
+
* content (avoids duplicate events when session ends).
|
|
1247
|
+
* @param options.runLlm Whether the context write should run inference.
|
|
1248
|
+
*/
|
|
1249
|
+
async _maybeEmitUserTurnStopped(options = {}) {
|
|
1250
|
+
const strategy = options.strategy ?? null;
|
|
1251
|
+
const onSessionEnd = options.onSessionEnd ?? false;
|
|
1252
|
+
const runLlm = options.runLlm ?? true;
|
|
1253
|
+
const segment = await this._pushAggregation({ runLlm });
|
|
1254
|
+
const fullAggregation = this._fullUserTurnAggregation;
|
|
1255
|
+
this._fullUserTurnAggregation = null;
|
|
1256
|
+
let content;
|
|
1257
|
+
if (segment && fullAggregation) {
|
|
1258
|
+
content = `${fullAggregation} ${segment}`.trim();
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
content = fullAggregation || segment;
|
|
1262
|
+
}
|
|
1263
|
+
if (!onSessionEnd || content) {
|
|
1264
|
+
const message = new UserTurnStoppedMessage({
|
|
1265
|
+
content,
|
|
1266
|
+
timestamp: this._userTurnStartTimestamp,
|
|
1267
|
+
});
|
|
1268
|
+
await this._callEventHandler("on_user_turn_stopped", strategy, message);
|
|
1269
|
+
this._userTurnStartTimestamp = "";
|
|
1270
|
+
}
|
|
1271
|
+
const interruptedBot = this._userTurnInterruptedBot;
|
|
1272
|
+
this._userTurnInterruptedBot = false;
|
|
1273
|
+
if (content) {
|
|
1274
|
+
this._consecutiveEmptyUserTurnRecoveries = 0;
|
|
1275
|
+
}
|
|
1276
|
+
else if (!onSessionEnd) {
|
|
1277
|
+
// An empty turn doesn't run the LLM, so the bot stays silent unless a
|
|
1278
|
+
// recovery runs it. Without one, restart the idle timer, which this
|
|
1279
|
+
// turn's start cancelled.
|
|
1280
|
+
if (!(await this._maybeRecoverEmptyUserTurn(interruptedBot))) {
|
|
1281
|
+
await this._userIdleController.waitForUser();
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Run the LLM for a user turn that ended with no transcript.
|
|
1287
|
+
*
|
|
1288
|
+
* @param interruptedBot Whether the turn interrupted a response in progress.
|
|
1289
|
+
* @returns Whether the LLM was asked to respond.
|
|
1290
|
+
*/
|
|
1291
|
+
async _maybeRecoverEmptyUserTurn(interruptedBot) {
|
|
1292
|
+
const config = this._params.emptyUserTurn;
|
|
1293
|
+
if (!config) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
const prompt = interruptedBot ? config.interruptedPrompt : config.idlePrompt;
|
|
1297
|
+
if (!prompt) {
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
if (this._consecutiveEmptyUserTurnRecoveries >= config.maxConsecutiveRecoveries) {
|
|
1301
|
+
logger.debug(`${this}: Empty user turn left unanswered (too many in a row)`);
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
// A pending function call result runs the LLM itself, and a muted user
|
|
1305
|
+
// shouldn't be prompted to speak.
|
|
1306
|
+
if (this._userIsMuted || this._userIdleController.functionCallsInProgress) {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
logger.debug(`${this}: Empty user turn (${interruptedBot ? "interrupted" : "idle"}), running LLM`);
|
|
1310
|
+
this._consecutiveEmptyUserTurnRecoveries += 1;
|
|
1311
|
+
this._context.addMessage({ role: "developer", content: prompt });
|
|
1312
|
+
await this.pushContextFrame();
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Assistant LLM aggregator that processes bot responses and function calls.
|
|
1318
|
+
*
|
|
1319
|
+
* This aggregator handles the complex logic of processing assistant
|
|
1320
|
+
* responses including:
|
|
1321
|
+
*
|
|
1322
|
+
* - Text frame aggregation between response start/end markers
|
|
1323
|
+
* - Function call lifecycle management
|
|
1324
|
+
* - Context updates with timestamps
|
|
1325
|
+
* - Tool execution and result handling
|
|
1326
|
+
* - Interruption handling during responses
|
|
1327
|
+
*
|
|
1328
|
+
* The aggregator manages function calls in progress and coordinates between
|
|
1329
|
+
* text generation and tool execution phases of LLM responses.
|
|
1330
|
+
*
|
|
1331
|
+
* Event handlers available:
|
|
1332
|
+
*
|
|
1333
|
+
* - on_assistant_turn_started: Called when the assistant turn starts
|
|
1334
|
+
* - on_assistant_turn_stopped: Called when the assistant turn ends
|
|
1335
|
+
* - on_assistant_thought: Called when an assistant thought is available
|
|
1336
|
+
* - on_assistant_backchannel: Called when the assistant starts saying text of
|
|
1337
|
+
* type `backchannel`, such as a `BotBackchannelFrame`
|
|
1338
|
+
* - on_summary_applied: Called when a context summarization is applied
|
|
1339
|
+
*
|
|
1340
|
+
* @example
|
|
1341
|
+
* aggregator.eventHandler("on_assistant_turn_started", async (aggregator) => {});
|
|
1342
|
+
* aggregator.eventHandler(
|
|
1343
|
+
* "on_assistant_turn_stopped",
|
|
1344
|
+
* async (aggregator, message: AssistantTurnStoppedMessage) => {},
|
|
1345
|
+
* );
|
|
1346
|
+
* aggregator.eventHandler("on_assistant_thought", async (aggregator, message: AssistantThoughtMessage) => {});
|
|
1347
|
+
* aggregator.eventHandler(
|
|
1348
|
+
* "on_assistant_backchannel",
|
|
1349
|
+
* async (aggregator, message: AssistantBackchannelMessage) => {},
|
|
1350
|
+
* );
|
|
1351
|
+
* aggregator.eventHandler(
|
|
1352
|
+
* "on_summary_applied",
|
|
1353
|
+
* async (aggregator, summarizer: LLMContextSummarizer, event: SummaryAppliedEvent) => {},
|
|
1354
|
+
* );
|
|
1355
|
+
*/
|
|
1356
|
+
export class LLMAssistantAggregator extends LLMContextAggregator {
|
|
1357
|
+
_params;
|
|
1358
|
+
// Realtime-mode wiring. null (the default) auto-configures from service
|
|
1359
|
+
// metadata, mirroring the user half (see LLMUserAggregator constructor).
|
|
1360
|
+
_realtimeServiceMode;
|
|
1361
|
+
_pairedUserAggregator;
|
|
1362
|
+
_functionCallsInProgress = new Map();
|
|
1363
|
+
_functionCallsImageResults = new Map();
|
|
1364
|
+
// Markers seen on LLMMarkerFrames, so a service configured with markers
|
|
1365
|
+
// other than the defaults still gets them stripped from transcripts.
|
|
1366
|
+
_seenTurnMarkers = new Set();
|
|
1367
|
+
_contextUpdatedTasks = new Set();
|
|
1368
|
+
_userSpeaking = false;
|
|
1369
|
+
_botSpeaking = false;
|
|
1370
|
+
// When a function call result arrives while the bot is speaking, we defer
|
|
1371
|
+
// the LLM re-invocation until the bot stops speaking. This flag is set to
|
|
1372
|
+
// true in that case so that `BotStoppedSpeakingFrame` knows to push a
|
|
1373
|
+
// context frame. Multiple results arriving in the same speaking window are
|
|
1374
|
+
// bundled into a single deferred push.
|
|
1375
|
+
_pushContextOnBotStoppedSpeaking = false;
|
|
1376
|
+
// A function call result asked to run inference and the push hasn't
|
|
1377
|
+
// happened yet: it was held for results still queued, or for a user who is
|
|
1378
|
+
// speaking. Whichever result is handled once the way is clear makes the
|
|
1379
|
+
// push, whatever its own `runLlm` says. A run that starts meanwhile, such
|
|
1380
|
+
// as the one the user's turn brings, settles it: that run sees the context
|
|
1381
|
+
// as it stands. A user turn with nothing in it brings no run, so the push
|
|
1382
|
+
// stays owed until the next arrival.
|
|
1383
|
+
_contextPushOwed = false;
|
|
1384
|
+
_assistantTurnStartTimestamp = "";
|
|
1385
|
+
_thoughtAppendToContext = false;
|
|
1386
|
+
_thoughtLlm = "";
|
|
1387
|
+
_thoughtAggregation = [];
|
|
1388
|
+
_thoughtStartTime = "";
|
|
1389
|
+
_summarizer;
|
|
1390
|
+
/**
|
|
1391
|
+
* Initialize the assistant context aggregator.
|
|
1392
|
+
*
|
|
1393
|
+
* @param context The LLM context for conversation storage.
|
|
1394
|
+
*/
|
|
1395
|
+
constructor(context, options = {}) {
|
|
1396
|
+
const params = options.params || new LLMAssistantAggregatorParams();
|
|
1397
|
+
super({
|
|
1398
|
+
...options,
|
|
1399
|
+
context,
|
|
1400
|
+
role: "assistant",
|
|
1401
|
+
addToolChangeMessages: params.addToolChangeMessages,
|
|
1402
|
+
});
|
|
1403
|
+
this._params = params;
|
|
1404
|
+
this._realtimeServiceMode = options._realtimeServiceMode ?? null;
|
|
1405
|
+
this._pairedUserAggregator = options._pairedUserAggregator ?? null;
|
|
1406
|
+
// Context summarization — always create the summarizer so that manually
|
|
1407
|
+
// pushed LLMSummarizeContextFrame frames are always handled.
|
|
1408
|
+
// Auto-triggering based on thresholds is only enabled when
|
|
1409
|
+
// enableAutoContextSummarization is true.
|
|
1410
|
+
this._summarizer = new LLMContextSummarizer({
|
|
1411
|
+
context: this._context,
|
|
1412
|
+
config: this._params.autoContextSummarizationConfig,
|
|
1413
|
+
autoTrigger: this._params.enableAutoContextSummarization,
|
|
1414
|
+
});
|
|
1415
|
+
this._summarizer.addEventHandler("on_request_summarization", (summarizer, frame) => this._onRequestSummarization(summarizer, frame));
|
|
1416
|
+
this._summarizer.addEventHandler("on_summary_applied", (summarizer, event) => this._onSummaryApplied(summarizer, event));
|
|
1417
|
+
this._registerEventHandler("on_assistant_turn_started");
|
|
1418
|
+
this._registerEventHandler("on_assistant_turn_stopped");
|
|
1419
|
+
this._registerEventHandler("on_assistant_thought");
|
|
1420
|
+
this._registerEventHandler("on_assistant_backchannel");
|
|
1421
|
+
this._registerEventHandler("on_summary_applied");
|
|
1422
|
+
}
|
|
1423
|
+
/** Whether there are any function calls currently in progress. */
|
|
1424
|
+
get hasFunctionCallsInProgress() {
|
|
1425
|
+
return this._functionCallsInProgress.size > 0;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Whether a synchronous call is in flight, one whose result the model waits for.
|
|
1429
|
+
*
|
|
1430
|
+
* A synchronous call leaves the context without its result until it
|
|
1431
|
+
* returns, so the model cannot run in the meantime; an asynchronous one
|
|
1432
|
+
* (`cancelOnInterruption: false`) has a placeholder result in the context
|
|
1433
|
+
* and the model runs on. A call announced but not yet started counts as
|
|
1434
|
+
* synchronous.
|
|
1435
|
+
*/
|
|
1436
|
+
get hasSynchronousFunctionCallsInProgress() {
|
|
1437
|
+
for (const frame of this._functionCallsInProgress.values()) {
|
|
1438
|
+
if (frame === null || frame.cancelOnInterruption) {
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Set up the aggregator.
|
|
1446
|
+
*
|
|
1447
|
+
* @param setup Configuration object containing setup parameters.
|
|
1448
|
+
*/
|
|
1449
|
+
async setup(setup) {
|
|
1450
|
+
await super.setup(setup);
|
|
1451
|
+
if (this._summarizer) {
|
|
1452
|
+
await this._summarizer.setup(setup);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/** Release this aggregator's resources at teardown. */
|
|
1456
|
+
async cleanup() {
|
|
1457
|
+
await super.cleanup();
|
|
1458
|
+
await this._cleanup();
|
|
1459
|
+
}
|
|
1460
|
+
/** Reset the aggregation state. */
|
|
1461
|
+
async reset() {
|
|
1462
|
+
await super.reset();
|
|
1463
|
+
await this._resetThoughtAggregation(); // Just to be safe
|
|
1464
|
+
this._pushContextOnBotStoppedSpeaking = false;
|
|
1465
|
+
this._contextPushOwed = false;
|
|
1466
|
+
}
|
|
1467
|
+
/** Reset the thought aggregation state. */
|
|
1468
|
+
async _resetThoughtAggregation() {
|
|
1469
|
+
this._thoughtAppendToContext = false;
|
|
1470
|
+
this._thoughtLlm = "";
|
|
1471
|
+
this._thoughtAggregation = [];
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Process frames for assistant response aggregation and function call management.
|
|
1475
|
+
*
|
|
1476
|
+
* @param frame The frame to process.
|
|
1477
|
+
* @param direction The direction of frame flow in the pipeline.
|
|
1478
|
+
*/
|
|
1479
|
+
async processFrame(frame, direction) {
|
|
1480
|
+
await super.processFrame(frame, direction);
|
|
1481
|
+
if (frame instanceof StartFrame) {
|
|
1482
|
+
// Push StartFrame before start(), because we want StartFrame to be
|
|
1483
|
+
// processed by every processor before any other frame is processed.
|
|
1484
|
+
await this.pushFrame(frame, direction);
|
|
1485
|
+
await this._start(frame);
|
|
1486
|
+
}
|
|
1487
|
+
else if (frame instanceof InterruptionFrame) {
|
|
1488
|
+
await this._handleInterruptions(frame);
|
|
1489
|
+
await this.pushFrame(frame, direction);
|
|
1490
|
+
}
|
|
1491
|
+
else if (frame instanceof EndFrame || frame instanceof CancelFrame) {
|
|
1492
|
+
await this._handleEndOrCancel(frame);
|
|
1493
|
+
await this.pushFrame(frame, direction);
|
|
1494
|
+
}
|
|
1495
|
+
else if (frame instanceof LLMAssistantPushAggregationFrame) {
|
|
1496
|
+
await this._handlePushAggregation();
|
|
1497
|
+
}
|
|
1498
|
+
else if (frame instanceof TTSStartedFrame) {
|
|
1499
|
+
await this._handleTtsStarted(frame);
|
|
1500
|
+
await this.pushFrame(frame, direction);
|
|
1501
|
+
}
|
|
1502
|
+
else if (frame instanceof LLMFullResponseStartFrame) {
|
|
1503
|
+
this._contextPushOwed = false;
|
|
1504
|
+
await this._handleLlmStart(frame);
|
|
1505
|
+
}
|
|
1506
|
+
else if (frame instanceof LLMFullResponseEndFrame) {
|
|
1507
|
+
await this._handleLlmEnd(frame);
|
|
1508
|
+
}
|
|
1509
|
+
else if (frame instanceof TextFrame) {
|
|
1510
|
+
await this._handleText(frame);
|
|
1511
|
+
}
|
|
1512
|
+
else if (frame instanceof LLMMarkerFrame) {
|
|
1513
|
+
await this._handleMarkerFrame(frame);
|
|
1514
|
+
}
|
|
1515
|
+
else if (frame instanceof LLMThoughtStartFrame) {
|
|
1516
|
+
await this._handleThoughtStart(frame);
|
|
1517
|
+
}
|
|
1518
|
+
else if (frame instanceof LLMThoughtTextFrame) {
|
|
1519
|
+
await this._handleThoughtText(frame);
|
|
1520
|
+
}
|
|
1521
|
+
else if (frame instanceof LLMThoughtEndFrame) {
|
|
1522
|
+
await this._handleThoughtEnd(frame);
|
|
1523
|
+
}
|
|
1524
|
+
else if (frame instanceof LLMRunFrame) {
|
|
1525
|
+
await this._handleLlmRun(frame);
|
|
1526
|
+
}
|
|
1527
|
+
else if (frame instanceof LLMMessagesAppendFrame) {
|
|
1528
|
+
await this._handleLlmMessagesAppend(frame);
|
|
1529
|
+
}
|
|
1530
|
+
else if (frame instanceof LLMMessagesUpdateFrame) {
|
|
1531
|
+
await this._handleLlmMessagesUpdate(frame);
|
|
1532
|
+
}
|
|
1533
|
+
else if (frame instanceof LLMMessagesTransformFrame) {
|
|
1534
|
+
await this._handleLlmMessagesTransform(frame);
|
|
1535
|
+
}
|
|
1536
|
+
else if (frame instanceof LLMSetToolsFrame) {
|
|
1537
|
+
// Normalize and validate (a plain list of direct functions /
|
|
1538
|
+
// FunctionSchema objects becomes a ToolsSchema) so the tool-change diff
|
|
1539
|
+
// and setTools see a consistent type.
|
|
1540
|
+
const normalizedTools = LLMContext._normalizeAndValidateTools(frame.tools);
|
|
1541
|
+
this._maybeAddToolChangeMessages(normalizedTools);
|
|
1542
|
+
this.setTools(normalizedTools);
|
|
1543
|
+
}
|
|
1544
|
+
else if (frame instanceof LLMSetToolChoiceFrame) {
|
|
1545
|
+
this.setToolChoice(frame.toolChoice);
|
|
1546
|
+
}
|
|
1547
|
+
else if (frame instanceof FunctionCallsStartedFrame) {
|
|
1548
|
+
await this._handleFunctionCallsStarted(frame);
|
|
1549
|
+
}
|
|
1550
|
+
else if (frame instanceof FunctionCallInProgressFrame) {
|
|
1551
|
+
await this._handleFunctionCallInProgress(frame);
|
|
1552
|
+
}
|
|
1553
|
+
else if (frame instanceof FunctionCallResultFrame) {
|
|
1554
|
+
await this._handleFunctionCallResult(frame);
|
|
1555
|
+
}
|
|
1556
|
+
else if (frame instanceof FunctionCallCancelFrame) {
|
|
1557
|
+
await this._handleFunctionCallCancel(frame);
|
|
1558
|
+
}
|
|
1559
|
+
else if (frame instanceof UserImageRawFrame) {
|
|
1560
|
+
await this._handleUserImageFrame(frame);
|
|
1561
|
+
}
|
|
1562
|
+
else if (frame instanceof UserFileRawFrame) {
|
|
1563
|
+
await this._handleUserFileFrame(frame);
|
|
1564
|
+
}
|
|
1565
|
+
else if (frame instanceof AssistantImageRawFrame) {
|
|
1566
|
+
await this._handleAssistantImageFrame(frame);
|
|
1567
|
+
}
|
|
1568
|
+
else if (frame instanceof UserStartedSpeakingFrame) {
|
|
1569
|
+
this._userSpeaking = true;
|
|
1570
|
+
await this.pushFrame(frame, direction);
|
|
1571
|
+
}
|
|
1572
|
+
else if (frame instanceof UserStoppedSpeakingFrame) {
|
|
1573
|
+
this._userSpeaking = false;
|
|
1574
|
+
await this.pushFrame(frame, direction);
|
|
1575
|
+
}
|
|
1576
|
+
else if (frame instanceof BotStartedSpeakingFrame) {
|
|
1577
|
+
this._botSpeaking = true;
|
|
1578
|
+
await this.pushFrame(frame, direction);
|
|
1579
|
+
}
|
|
1580
|
+
else if (frame instanceof BotStoppedSpeakingFrame) {
|
|
1581
|
+
this._botSpeaking = false;
|
|
1582
|
+
await this.pushFrame(frame, direction);
|
|
1583
|
+
if (this._pushContextOnBotStoppedSpeaking && !this._userSpeaking) {
|
|
1584
|
+
logger.debug(`${this}: Bot stopped speaking — pushing deferred context frame!`);
|
|
1585
|
+
await this.pushContextFrame(FrameDirection.UPSTREAM);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
else if (frame instanceof LLMServiceMetadataFrame) {
|
|
1589
|
+
// Auto-configure realtime mode on the assistant half too — the
|
|
1590
|
+
// broadcast reaches both halves. The assistant only needs the flag for
|
|
1591
|
+
// its trailing flush; the user half owns the strategy mutations.
|
|
1592
|
+
if (frame.isRealtimeService && this._realtimeServiceMode === null) {
|
|
1593
|
+
this._realtimeServiceMode = true;
|
|
1594
|
+
// The mode just auto-enabled: the trailing flush needs the
|
|
1595
|
+
// back-reference to the user half (always wired by the pair — this
|
|
1596
|
+
// surfaces unsupported direct construction instead of silently
|
|
1597
|
+
// dropping user messages).
|
|
1598
|
+
this._requirePairedUserAggregator();
|
|
1599
|
+
}
|
|
1600
|
+
await this.pushFrame(frame, direction);
|
|
1601
|
+
}
|
|
1602
|
+
else if (frame instanceof ProposedUserStartedSpeakingFrame) {
|
|
1603
|
+
// A broadcast sends a copy each way, so a proposal from a service
|
|
1604
|
+
// sitting between the two halves — a realtime LLM — reaches the user
|
|
1605
|
+
// half as the upstream copy while this copy travels on. A proposal
|
|
1606
|
+
// should be resolved once, so stop it here if the user half resolves
|
|
1607
|
+
// it. No standard pipeline has a resolver downstream of this half, so
|
|
1608
|
+
// this guards the invariant rather than fixing an observed escape.
|
|
1609
|
+
const user = this._pairedUserAggregator;
|
|
1610
|
+
if (!user?._userTurnController.resolvesProposedTurnStartFrames) {
|
|
1611
|
+
await this.pushFrame(frame, direction);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
else if (frame instanceof ProposedUserStoppedSpeakingFrame) {
|
|
1615
|
+
const user = this._pairedUserAggregator;
|
|
1616
|
+
if (!user?._userTurnController.resolvesProposedTurnStopFrames) {
|
|
1617
|
+
await this.pushFrame(frame, direction);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
await this.pushFrame(frame, direction);
|
|
1622
|
+
}
|
|
1623
|
+
// Pass frames to summarizer for monitoring
|
|
1624
|
+
if (this._summarizer) {
|
|
1625
|
+
await this._summarizer.processFrame(frame);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
async _start(_frame) {
|
|
1629
|
+
if (this._realtimeServiceMode) {
|
|
1630
|
+
this._requirePairedUserAggregator();
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Throw if realtime mode is active without a paired user aggregator.
|
|
1635
|
+
*
|
|
1636
|
+
* Realtime mode needs the assistant half to hold a back-reference to the
|
|
1637
|
+
* user half so it can flush the user message on `LLMFullResponseStartFrame`.
|
|
1638
|
+
* The pair wires this unconditionally (so the halves are always configured
|
|
1639
|
+
* together); this guards unsupported direct construction, surfacing the
|
|
1640
|
+
* missing flush loudly instead of silently dropping user messages. Checked
|
|
1641
|
+
* at `StartFrame` for an explicit mode and again when the mode auto-enables.
|
|
1642
|
+
*
|
|
1643
|
+
* @throws Error (upstream: RuntimeError) when no user aggregator is paired.
|
|
1644
|
+
*/
|
|
1645
|
+
_requirePairedUserAggregator() {
|
|
1646
|
+
if (this._pairedUserAggregator === null) {
|
|
1647
|
+
throw new Error(`${this}: realtime_service_mode is enabled but this assistant ` +
|
|
1648
|
+
"aggregator has no paired user aggregator. Build both aggregators " +
|
|
1649
|
+
"with LLMContextAggregatorPair(context) rather than constructing " +
|
|
1650
|
+
"them individually, so realtime mode is configured once and shared " +
|
|
1651
|
+
"across both halves.");
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
/** Push the current assistant aggregation with timestamp. */
|
|
1655
|
+
async pushAggregation() {
|
|
1656
|
+
const aggregation = await this._addAggregationToContext();
|
|
1657
|
+
if (!aggregation) {
|
|
1658
|
+
return "";
|
|
1659
|
+
}
|
|
1660
|
+
// Push context frame
|
|
1661
|
+
await this.pushContextFrame();
|
|
1662
|
+
// Push timestamp frame with current time
|
|
1663
|
+
const timestampFrame = new LLMContextAssistantTimestampFrame({ timestamp: timeNowIso8601() });
|
|
1664
|
+
await this.pushFrame(timestampFrame);
|
|
1665
|
+
return aggregation;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Commit the held assistant aggregation as a context message, pushing no frames.
|
|
1669
|
+
*
|
|
1670
|
+
* For flushes in the middle of an open assistant turn (a user file or image
|
|
1671
|
+
* arriving mid-reply), where the downstream context and timestamp frames —
|
|
1672
|
+
* and pushContextFrame()'s side effects — belong to the turn's real end,
|
|
1673
|
+
* not to this commit.
|
|
1674
|
+
*
|
|
1675
|
+
* @returns The committed aggregation, or an empty string if none was held.
|
|
1676
|
+
*/
|
|
1677
|
+
async _addAggregationToContext() {
|
|
1678
|
+
if (this._aggregation.length === 0) {
|
|
1679
|
+
return "";
|
|
1680
|
+
}
|
|
1681
|
+
const aggregation = this.aggregationString();
|
|
1682
|
+
await this.reset();
|
|
1683
|
+
this._context.addMessage({ role: "assistant", content: aggregation });
|
|
1684
|
+
return aggregation;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Push a context frame in the specified direction.
|
|
1688
|
+
*
|
|
1689
|
+
* @param direction The direction to push the frame (upstream or downstream).
|
|
1690
|
+
*/
|
|
1691
|
+
async pushContextFrame(direction = FrameDirection.DOWNSTREAM) {
|
|
1692
|
+
await super.pushContextFrame(direction);
|
|
1693
|
+
this._pushContextOnBotStoppedSpeaking = false;
|
|
1694
|
+
this._contextPushOwed = false;
|
|
1695
|
+
}
|
|
1696
|
+
async _handleLlmRun(_frame) {
|
|
1697
|
+
await this.pushContextFrame(FrameDirection.UPSTREAM);
|
|
1698
|
+
}
|
|
1699
|
+
async _handleLlmMessagesAppend(frame) {
|
|
1700
|
+
// An LLMMessagesAppendFrame with runLlm=true is treated as a
|
|
1701
|
+
// FunctionCallResultFrame with runLlm=true is: held while the bot speaks,
|
|
1702
|
+
// owed while the user speaks, and bundled with the appends and results
|
|
1703
|
+
// queued behind it. Run at once, a message appended mid-sentence runs the
|
|
1704
|
+
// model on a context that lacks the answer in progress, and the model
|
|
1705
|
+
// answers it again.
|
|
1706
|
+
this.addMessages(frame.messages);
|
|
1707
|
+
if (frame.runLlm) {
|
|
1708
|
+
this._contextPushOwed = true;
|
|
1709
|
+
}
|
|
1710
|
+
if (this._contextPushOwed && !this._userSpeaking) {
|
|
1711
|
+
await this._maybePushContext();
|
|
1712
|
+
}
|
|
1713
|
+
else if (this._contextPushOwed) {
|
|
1714
|
+
logger.debug(`${this}: User is speaking — context frame push owed until the turn ends.`);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
async _handleLlmMessagesUpdate(frame) {
|
|
1718
|
+
this.setMessages(frame.messages);
|
|
1719
|
+
if (frame.runLlm) {
|
|
1720
|
+
await this.pushContextFrame(FrameDirection.UPSTREAM);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
async _handleLlmMessagesTransform(frame) {
|
|
1724
|
+
this.transformMessages(frame.transform);
|
|
1725
|
+
if (frame.runLlm) {
|
|
1726
|
+
await this.pushContextFrame(FrameDirection.UPSTREAM);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
async _handleInterruptions(_frame) {
|
|
1730
|
+
await this._triggerAssistantTurnStopped({ interrupted: true });
|
|
1731
|
+
await this.reset();
|
|
1732
|
+
}
|
|
1733
|
+
async _handleEndOrCancel(frame) {
|
|
1734
|
+
await this._triggerAssistantTurnStopped({ interrupted: frame instanceof CancelFrame });
|
|
1735
|
+
await this._cleanup();
|
|
1736
|
+
}
|
|
1737
|
+
async _cleanup() {
|
|
1738
|
+
// Cancel any in-flight onContextUpdated callback tasks and tear down the
|
|
1739
|
+
// summarizer. Idempotent so it is safe to run from both the
|
|
1740
|
+
// EndFrame/CancelFrame handler and cleanup().
|
|
1741
|
+
for (const task of [...this._contextUpdatedTasks]) {
|
|
1742
|
+
await this.cancelTask(task);
|
|
1743
|
+
}
|
|
1744
|
+
this._contextUpdatedTasks.clear();
|
|
1745
|
+
if (this._summarizer) {
|
|
1746
|
+
await this._summarizer.cleanup();
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async _handleFunctionCallsStarted(frame) {
|
|
1750
|
+
const functionNames = frame.functionCalls.map((f) => `${f.functionName}:${f.toolCallId}`);
|
|
1751
|
+
logger.debug(`${this} FunctionCallsStartedFrame: ${reprList(functionNames)}`);
|
|
1752
|
+
for (const functionCall of frame.functionCalls) {
|
|
1753
|
+
this._functionCallsInProgress.set(functionCall.toolCallId, null);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
async _handleFunctionCallInProgress(frame) {
|
|
1757
|
+
logger.debug(`${this} FunctionCallInProgressFrame: [${frame.functionName}:${frame.toolCallId}]`);
|
|
1758
|
+
// Update context with the in-progress function call
|
|
1759
|
+
this._context.addMessage({
|
|
1760
|
+
role: "assistant",
|
|
1761
|
+
tool_calls: [
|
|
1762
|
+
{
|
|
1763
|
+
id: frame.toolCallId,
|
|
1764
|
+
function: {
|
|
1765
|
+
name: frame.functionName,
|
|
1766
|
+
arguments: jsonDumps(frame.arguments),
|
|
1767
|
+
},
|
|
1768
|
+
type: "function",
|
|
1769
|
+
},
|
|
1770
|
+
],
|
|
1771
|
+
});
|
|
1772
|
+
const isAsync = !frame.cancelOnInterruption;
|
|
1773
|
+
if (isAsync) {
|
|
1774
|
+
this._context.addMessage(asyncToolMessages.buildStartedMessage(frame.toolCallId));
|
|
1775
|
+
}
|
|
1776
|
+
else {
|
|
1777
|
+
this._context.addMessage({
|
|
1778
|
+
role: "tool",
|
|
1779
|
+
content: "IN_PROGRESS",
|
|
1780
|
+
tool_call_id: frame.toolCallId,
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
this._functionCallsInProgress.set(frame.toolCallId, frame);
|
|
1784
|
+
}
|
|
1785
|
+
async _handleFunctionCallResult(frame) {
|
|
1786
|
+
logger.debug(`${this} FunctionCallResultFrame: [${frame.functionName}:${frame.toolCallId}]`);
|
|
1787
|
+
if (!this._functionCallsInProgress.has(frame.toolCallId)) {
|
|
1788
|
+
logger.warning(`FunctionCallResultFrame tool_call_id [${frame.toolCallId}] is not running`);
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
const inProgressFrame = this._functionCallsInProgress.get(frame.toolCallId) ?? null;
|
|
1792
|
+
if (inProgressFrame === null) {
|
|
1793
|
+
// Started, but its in-progress frame hasn't arrived: there's nothing
|
|
1794
|
+
// yet to attach the result to.
|
|
1795
|
+
logger.warning(`FunctionCallResultFrame tool_call_id [${frame.toolCallId}] is not in progress`);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
const groupId = inProgressFrame.groupId;
|
|
1799
|
+
const properties = frame.properties;
|
|
1800
|
+
const isFinal = frame.properties ? frame.properties.isFinal : true;
|
|
1801
|
+
if (isFinal) {
|
|
1802
|
+
await this._handleFunctionCallFinished(frame, inProgressFrame);
|
|
1803
|
+
}
|
|
1804
|
+
else {
|
|
1805
|
+
await this._handleFunctionCallIntermediateResult(frame, inProgressFrame);
|
|
1806
|
+
}
|
|
1807
|
+
let runLlm = false;
|
|
1808
|
+
// Append any images that were generated by function calls.
|
|
1809
|
+
const imageFrame = this._functionCallsImageResults.get(frame.toolCallId);
|
|
1810
|
+
if (imageFrame !== undefined) {
|
|
1811
|
+
this._functionCallsImageResults.delete(frame.toolCallId);
|
|
1812
|
+
// If an image frame has been added to the context, let's run inference.
|
|
1813
|
+
runLlm = await this._maybeAppendImageToContext(imageFrame);
|
|
1814
|
+
}
|
|
1815
|
+
// Run inference if the function call result requires it.
|
|
1816
|
+
if (isTruthy(frame.result)) {
|
|
1817
|
+
if (properties && properties.runLlm !== null && properties.runLlm !== undefined) {
|
|
1818
|
+
// If the tool call result has a runLlm property, use it.
|
|
1819
|
+
runLlm = properties.runLlm;
|
|
1820
|
+
}
|
|
1821
|
+
else if (frame.runLlm !== null && frame.runLlm !== undefined) {
|
|
1822
|
+
// If the frame is indicating we should run the LLM, do it.
|
|
1823
|
+
runLlm = frame.runLlm;
|
|
1824
|
+
}
|
|
1825
|
+
else {
|
|
1826
|
+
// Run the LLM when this is the last function call in the group to
|
|
1827
|
+
// complete. If groupId is set, only consider sibling calls; otherwise
|
|
1828
|
+
// always execute as soon as we receive the result.
|
|
1829
|
+
if (groupId) {
|
|
1830
|
+
runLlm = ![...this._functionCallsInProgress.values()].some((f) => f !== null &&
|
|
1831
|
+
f.groupId === groupId &&
|
|
1832
|
+
// We are now able to receive "updates", so the current frame
|
|
1833
|
+
// can still be in the in progress list, and we need to ignore it.
|
|
1834
|
+
f.toolCallId !== frame.toolCallId);
|
|
1835
|
+
}
|
|
1836
|
+
else {
|
|
1837
|
+
runLlm = true;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (runLlm) {
|
|
1842
|
+
this._contextPushOwed = true;
|
|
1843
|
+
}
|
|
1844
|
+
if (this._contextPushOwed && !this._userSpeaking) {
|
|
1845
|
+
await this._maybePushContext();
|
|
1846
|
+
}
|
|
1847
|
+
// Call the `onContextUpdated` callback once the function call result is
|
|
1848
|
+
// added to the context. Also, run this in a separate task to make sure we
|
|
1849
|
+
// don't block the pipeline.
|
|
1850
|
+
const onContextUpdated = properties?.onContextUpdated;
|
|
1851
|
+
if (onContextUpdated) {
|
|
1852
|
+
const workerName = `${frame.functionName}:${frame.toolCallId}:on_context_updated`;
|
|
1853
|
+
const task = this.createTask(() => onContextUpdated(), workerName);
|
|
1854
|
+
this._contextUpdatedTasks.add(task);
|
|
1855
|
+
task.addDoneCallback((t) => this._contextUpdatedTaskFinished(t));
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Decide whether to push a context frame after a function call settles or a message is appended.
|
|
1860
|
+
*
|
|
1861
|
+
* Push an `LLMContextFrame` upstream (with care to avoid duplicate pushes
|
|
1862
|
+
* while results or appends are queued or the bot is still speaking).
|
|
1863
|
+
* Cascade LLMs use the context frame to re-run inference with the new tool
|
|
1864
|
+
* result in scope. Realtime LLMs read the new tool result out of the
|
|
1865
|
+
* context the same way — they don't get function results from
|
|
1866
|
+
* `FunctionCallResultFrame` directly — so the same push is load-bearing for
|
|
1867
|
+
* both modes.
|
|
1868
|
+
*/
|
|
1869
|
+
async _maybePushContext() {
|
|
1870
|
+
if (this.hasQueuedFrame(FunctionCallResultFrame) ||
|
|
1871
|
+
this.hasQueuedFrame(LLMMessagesAppendFrame)) {
|
|
1872
|
+
// Another result or append is already queued. Defer the context push to
|
|
1873
|
+
// bundle them all into a single LLM call instead of triggering one
|
|
1874
|
+
// inference pass per frame. The push is owed until it happens, so the
|
|
1875
|
+
// last frame in the queue makes it whether or not that frame asks to
|
|
1876
|
+
// run — a burst can end with an intermediate result, or an
|
|
1877
|
+
// LLMMessagesAppendFrame with runLlm=false, that doesn't.
|
|
1878
|
+
logger.debug(`${this}: More results or appends queued — deferring context frame push.`);
|
|
1879
|
+
}
|
|
1880
|
+
else if (this._botSpeaking) {
|
|
1881
|
+
// Defer the context frame push until the bot finishes speaking. If
|
|
1882
|
+
// multiple function call results arrive while the bot is speaking, they
|
|
1883
|
+
// all accumulate in the context and a single push is performed once
|
|
1884
|
+
// speaking stops, preventing the LLM from running multiple times and
|
|
1885
|
+
// producing duplicated responses. This should be an edge case, since it
|
|
1886
|
+
// would require a FunctionCallResultFrame being queued between an LLM
|
|
1887
|
+
// response start and end frame.
|
|
1888
|
+
logger.debug(`${this}: Bot is speaking — deferring context frame push.`);
|
|
1889
|
+
this._pushContextOnBotStoppedSpeaking = true;
|
|
1890
|
+
}
|
|
1891
|
+
else {
|
|
1892
|
+
logger.debug(`${this}: Pushing context frame!`);
|
|
1893
|
+
await this.pushContextFrame(FrameDirection.UPSTREAM);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Handle an intermediate result for an async function call.
|
|
1898
|
+
*
|
|
1899
|
+
* Injects an intermediate developer message into the context without
|
|
1900
|
+
* removing the call from the in-progress map.
|
|
1901
|
+
*/
|
|
1902
|
+
async _handleFunctionCallIntermediateResult(frame, _inProgressFrame) {
|
|
1903
|
+
if (!isTruthy(frame.result)) {
|
|
1904
|
+
logger.warning(`${this} result_callback called with is_final=False but no result!`);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
const result = jsonDumps(frame.result);
|
|
1908
|
+
this._context.addMessage(asyncToolMessages.buildIntermediateResultMessage(frame.toolCallId, result));
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Handle the final result of a function call.
|
|
1912
|
+
*
|
|
1913
|
+
* Removes the call from the in-progress map, updates the context, and
|
|
1914
|
+
* triggers LLM inference when appropriate.
|
|
1915
|
+
*/
|
|
1916
|
+
async _handleFunctionCallFinished(frame, inProgressFrame) {
|
|
1917
|
+
const deferred = this._isDeferred(inProgressFrame);
|
|
1918
|
+
this._functionCallsInProgress.delete(frame.toolCallId);
|
|
1919
|
+
const result = isTruthy(frame.result) ? jsonDumps(frame.result) : "COMPLETED";
|
|
1920
|
+
if (deferred) {
|
|
1921
|
+
// The conversation moved on while the call ran, so the LLM is told
|
|
1922
|
+
// about the result in a developer message rather than in the tool
|
|
1923
|
+
// message it has already seen.
|
|
1924
|
+
this._context.addMessage(asyncToolMessages.buildFinalResultMessage(frame.toolCallId, result));
|
|
1925
|
+
}
|
|
1926
|
+
else {
|
|
1927
|
+
this._updateFunctionCallResult(frame.functionName, frame.toolCallId, result);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
async _handleFunctionCallCancel(frame) {
|
|
1931
|
+
logger.debug(`${this} FunctionCallCancelFrame: [${frame.functionName}:${frame.toolCallId}]`);
|
|
1932
|
+
const functionCall = this._functionCallsInProgress.get(frame.toolCallId);
|
|
1933
|
+
if (!functionCall) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
// Update context with the function call cancellation. An async call is
|
|
1937
|
+
// settled with a developer message, the same channel its results use,
|
|
1938
|
+
// whether or not the conversation moved on, since the notice says the
|
|
1939
|
+
// call did not complete and asks for the user to be told.
|
|
1940
|
+
if (functionCall.cancelOnInterruption) {
|
|
1941
|
+
this._updateFunctionCallResult(frame.functionName, frame.toolCallId, "CANCELLED");
|
|
1942
|
+
}
|
|
1943
|
+
else {
|
|
1944
|
+
this._context.addMessage(asyncToolMessages.buildCancelledMessage(frame.toolCallId));
|
|
1945
|
+
}
|
|
1946
|
+
const groupId = functionCall.groupId;
|
|
1947
|
+
this._functionCallsInProgress.delete(frame.toolCallId);
|
|
1948
|
+
if (!frame.runLlm || this._userSpeaking) {
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
// Hold off while siblings from the same LLM response are still running:
|
|
1952
|
+
// whichever of them settles last runs inference, with this cancellation
|
|
1953
|
+
// already in the context.
|
|
1954
|
+
if (groupId &&
|
|
1955
|
+
[...this._functionCallsInProgress.values()].some((f) => f !== null && f.groupId === groupId)) {
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
await this._maybePushContext();
|
|
1959
|
+
}
|
|
1960
|
+
async _handleUserImageFrame(frame) {
|
|
1961
|
+
let imageAppended = false;
|
|
1962
|
+
// Check if this image is a result of a function call.
|
|
1963
|
+
if (frame.request?.toolCallId && this._functionCallsInProgress.has(frame.request.toolCallId)) {
|
|
1964
|
+
this._functionCallsImageResults.set(frame.request.toolCallId, frame);
|
|
1965
|
+
// Call the resultCallback if provided. This signals that the image has
|
|
1966
|
+
// been retrieved and the function call can now complete.
|
|
1967
|
+
if (frame.request.resultCallback) {
|
|
1968
|
+
const resultCallback = frame.request.resultCallback;
|
|
1969
|
+
await resultCallback(null);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
else {
|
|
1973
|
+
// Commit any in-progress assistant aggregation before appending, so the
|
|
1974
|
+
// image message lands after it in the context. Context-only: the turn
|
|
1975
|
+
// stays open and its frames fire at the turn's real end.
|
|
1976
|
+
await this._addAggregationToContext();
|
|
1977
|
+
imageAppended = await this._maybeAppendImageToContext(frame);
|
|
1978
|
+
}
|
|
1979
|
+
if (imageAppended && frame.runLlm !== false) {
|
|
1980
|
+
await this.pushContextFrame(FrameDirection.UPSTREAM);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async _handleUserFileFrame(frame) {
|
|
1984
|
+
// TODO: Should this have a similar function-call check like _handleUserImageFrame?
|
|
1985
|
+
if (!frame.appendToContext) {
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
// Commit any in-progress assistant aggregation before appending, so the
|
|
1989
|
+
// file message lands after it in the context. The order matters beyond
|
|
1990
|
+
// chronology: LLMContext.removeInvalidFileMessage() only considers
|
|
1991
|
+
// messages after the last assistant message, so a file message written
|
|
1992
|
+
// before a later-committed assistant message could never be cleaned up if
|
|
1993
|
+
// the provider rejects it. Context-only: the assistant turn stays open,
|
|
1994
|
+
// any remaining reply text commits at the turn's end, and the turn's
|
|
1995
|
+
// frames fire there.
|
|
1996
|
+
await this._addAggregationToContext();
|
|
1997
|
+
logger.debug(`${this} Appending UserFileRawFrame to LLM context (format: ${frame.format})`);
|
|
1998
|
+
await this._context.addFileFrameMessage({
|
|
1999
|
+
type: frame.type,
|
|
2000
|
+
format: frame.format,
|
|
2001
|
+
text: frame.text,
|
|
2002
|
+
file: frame.file,
|
|
2003
|
+
name: frame.filename,
|
|
2004
|
+
// TODO: pass customOptions through to adapters via the universal message
|
|
2005
|
+
});
|
|
2006
|
+
if (frame.runLlm !== false) {
|
|
2007
|
+
await this.pushContextFrame(FrameDirection.UPSTREAM);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
async _handleAssistantImageFrame(frame) {
|
|
2011
|
+
logger.debug(`${this} Appending AssistantImageRawFrame to LLM context (size: (${frame.size.join(", ")}))`);
|
|
2012
|
+
if (frame.originalData && frame.originalMimeType) {
|
|
2013
|
+
await this._context.addImageFrameMessage({
|
|
2014
|
+
format: frame.originalMimeType,
|
|
2015
|
+
size: frame.size, // Technically doesn't matter, since already encoded
|
|
2016
|
+
image: frame.originalData,
|
|
2017
|
+
role: "assistant",
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
if (frame.format === null) {
|
|
2022
|
+
throw new Error("AssistantImageRawFrame.format must not be null");
|
|
2023
|
+
}
|
|
2024
|
+
await this._context.addImageFrameMessage({
|
|
2025
|
+
format: frame.format,
|
|
2026
|
+
size: frame.size,
|
|
2027
|
+
image: frame.image,
|
|
2028
|
+
role: "assistant",
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
async _handleLlmStart(_frame) {
|
|
2033
|
+
// Realtime mode treats LLMFullResponseStartFrame as the user turn's end
|
|
2034
|
+
// signal for context-writing purposes — see _realtimeHandleLlmStart.
|
|
2035
|
+
if (this._realtimeServiceMode) {
|
|
2036
|
+
await this._realtimeHandleLlmStart();
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
await this._triggerAssistantTurnStarted();
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Realtime mode: commit the paired user message, then start the assistant turn.
|
|
2043
|
+
*
|
|
2044
|
+
* Realtime mode treats `LLMFullResponseStartFrame` as the user turn's end
|
|
2045
|
+
* signal for context-writing purposes (in place of
|
|
2046
|
+
* `UserStoppedSpeakingFrame`). The paired user aggregator may defer the
|
|
2047
|
+
* actual write for one `ttfsP99Latency` window to allow a late-arriving
|
|
2048
|
+
* transcript to land.
|
|
2049
|
+
*/
|
|
2050
|
+
async _realtimeHandleLlmStart() {
|
|
2051
|
+
if (this._pairedUserAggregator !== null) {
|
|
2052
|
+
await this._pairedUserAggregator._realtimeHandoffFlush();
|
|
2053
|
+
}
|
|
2054
|
+
await this._triggerAssistantTurnStarted();
|
|
2055
|
+
}
|
|
2056
|
+
async _handleLlmEnd(_frame) {
|
|
2057
|
+
// Realtime mode failsafe: by the time the assistant response fully
|
|
2058
|
+
// completes, even slow transcripts should have landed. Cancel any
|
|
2059
|
+
// still-pending deferred handoff flush and commit the user message now so
|
|
2060
|
+
// it precedes the assistant message in context.
|
|
2061
|
+
if (this._realtimeServiceMode && this._pairedUserAggregator !== null) {
|
|
2062
|
+
await this._pairedUserAggregator._realtimeHandoffFlushImmediate();
|
|
2063
|
+
}
|
|
2064
|
+
await this._triggerAssistantTurnStopped();
|
|
2065
|
+
}
|
|
2066
|
+
async _handleTtsStarted(frame) {
|
|
2067
|
+
// If this TTS output will be written to context and we don't already have
|
|
2068
|
+
// an open assistant turn, open one. This handles the case of
|
|
2069
|
+
// TTSSpeakFrame-driven utterances that have no surrounding LLM response
|
|
2070
|
+
// frames to signal assistant turn boundaries, while rightly deferring to
|
|
2071
|
+
// any earlier LLM-driven turn start.
|
|
2072
|
+
if (frame.appendToContext && !this._assistantTurnStartTimestamp) {
|
|
2073
|
+
await this._triggerAssistantTurnStarted();
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
async _handlePushAggregation() {
|
|
2077
|
+
// LLMAssistantPushAggregationFrame is emitted at the end of a
|
|
2078
|
+
// TTSSpeakFrame-driven utterance to commit the spoken text.
|
|
2079
|
+
await this._triggerAssistantTurnStopped();
|
|
2080
|
+
}
|
|
2081
|
+
async _handleText(frame) {
|
|
2082
|
+
// Skip TextFrame types not intended to build the assistant context
|
|
2083
|
+
if (frame instanceof TranscriptionFrame ||
|
|
2084
|
+
frame instanceof TranslationFrame ||
|
|
2085
|
+
frame instanceof InterimTranscriptionFrame ||
|
|
2086
|
+
frame instanceof EagerTranscriptionFrame) {
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
// A backchannel is reported once, by the segment the TTS will speak,
|
|
2090
|
+
// which the output transport releases as its audio starts.
|
|
2091
|
+
if (frame instanceof AggregatedTextFrame &&
|
|
2092
|
+
!(frame instanceof TTSTextFrame) &&
|
|
2093
|
+
frame.textType === TextType.BACKCHANNEL &&
|
|
2094
|
+
frame.willBeSpoken) {
|
|
2095
|
+
const message = new AssistantBackchannelMessage({
|
|
2096
|
+
content: frame.text,
|
|
2097
|
+
timestamp: timeNowIso8601(),
|
|
2098
|
+
});
|
|
2099
|
+
await this._callEventHandler("on_assistant_backchannel", message);
|
|
2100
|
+
}
|
|
2101
|
+
if (!frame.appendToContext) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
// Make sure we really have text (spaces count, too!)
|
|
2105
|
+
if (frame.text.length === 0) {
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
const text = frame instanceof AggregatedTextFrame && frame.rawText ? frame.rawText : frame.text;
|
|
2109
|
+
this._aggregation.push(new TextPartForConcatenation({
|
|
2110
|
+
text,
|
|
2111
|
+
includesInterPartSpaces: frame.includesInterFrameSpaces,
|
|
2112
|
+
}));
|
|
2113
|
+
}
|
|
2114
|
+
async _handleMarkerFrame(frame) {
|
|
2115
|
+
this._seenTurnMarkers.add(frame.marker);
|
|
2116
|
+
if (frame.appendToContextImmediately) {
|
|
2117
|
+
// Stand-alone marker: write it to the context now as its own assistant
|
|
2118
|
+
// message. Used when the marker is the entire assistant turn — e.g. the
|
|
2119
|
+
// ◐ / ○ incomplete-turn signals, where the spoken response is
|
|
2120
|
+
// suppressed and the marker is the only artifact.
|
|
2121
|
+
this._context.addMessage({ role: "assistant", content: frame.marker });
|
|
2122
|
+
await this.pushContextFrame();
|
|
2123
|
+
const timestampFrame = new LLMContextAssistantTimestampFrame({
|
|
2124
|
+
timestamp: timeNowIso8601(),
|
|
2125
|
+
});
|
|
2126
|
+
await this.pushFrame(timestampFrame);
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
// Marker is part of an in-progress assistant response. Append it to the
|
|
2130
|
+
// running aggregation so `pushAggregation` writes marker + text as a
|
|
2131
|
+
// single context message — e.g. the ● complete-turn signal that prefixes
|
|
2132
|
+
// the spoken response, producing "● <response>" in context. Markers are
|
|
2133
|
+
// stripped from the transcript via `_maybeStripTurnCompletionMarkers` so
|
|
2134
|
+
// consumers see clean text.
|
|
2135
|
+
this._aggregation.push(new TextPartForConcatenation({ text: frame.marker, includesInterPartSpaces: false }));
|
|
2136
|
+
}
|
|
2137
|
+
async _handleThoughtStart(frame) {
|
|
2138
|
+
await this._resetThoughtAggregation();
|
|
2139
|
+
this._thoughtAppendToContext = frame.appendToContext;
|
|
2140
|
+
// frame.llm is required when frame.appendToContext is true, which is the
|
|
2141
|
+
// only case where we read this value, so we can safely default to an
|
|
2142
|
+
// empty string here for typing purposes.
|
|
2143
|
+
this._thoughtLlm = frame.llm || "";
|
|
2144
|
+
this._thoughtStartTime = timeNowIso8601();
|
|
2145
|
+
}
|
|
2146
|
+
async _handleThoughtText(frame) {
|
|
2147
|
+
// Make sure we really have text (spaces count, too!)
|
|
2148
|
+
if (frame.text.length === 0) {
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
this._thoughtAggregation.push(new TextPartForConcatenation({
|
|
2152
|
+
text: frame.text,
|
|
2153
|
+
includesInterPartSpaces: frame.includesInterFrameSpaces,
|
|
2154
|
+
}));
|
|
2155
|
+
}
|
|
2156
|
+
async _handleThoughtEnd(frame) {
|
|
2157
|
+
const thought = concatenateAggregatedText(this._thoughtAggregation);
|
|
2158
|
+
if (this._thoughtAppendToContext) {
|
|
2159
|
+
const llm = this._thoughtLlm;
|
|
2160
|
+
this._context.addMessage(new LLMSpecificMessage({
|
|
2161
|
+
llm,
|
|
2162
|
+
message: {
|
|
2163
|
+
type: "thought",
|
|
2164
|
+
text: thought,
|
|
2165
|
+
signature: frame.signature,
|
|
2166
|
+
},
|
|
2167
|
+
}));
|
|
2168
|
+
}
|
|
2169
|
+
const message = new AssistantThoughtMessage({
|
|
2170
|
+
content: thought,
|
|
2171
|
+
timestamp: this._thoughtStartTime,
|
|
2172
|
+
});
|
|
2173
|
+
await this._resetThoughtAggregation();
|
|
2174
|
+
await this._callEventHandler("on_assistant_thought", message);
|
|
2175
|
+
}
|
|
2176
|
+
async _maybeAppendImageToContext(frame) {
|
|
2177
|
+
if (!frame.appendToContext) {
|
|
2178
|
+
return false;
|
|
2179
|
+
}
|
|
2180
|
+
logger.debug(`${this} Appending UserImageRawFrame to LLM context (size: (${frame.size.join(", ")}))`);
|
|
2181
|
+
if (frame.format === null) {
|
|
2182
|
+
throw new Error("UserImageRawFrame.format must not be null");
|
|
2183
|
+
}
|
|
2184
|
+
await this._context.addImageFrameMessage({
|
|
2185
|
+
format: frame.format,
|
|
2186
|
+
size: frame.size,
|
|
2187
|
+
image: frame.image,
|
|
2188
|
+
text: frame.text,
|
|
2189
|
+
});
|
|
2190
|
+
return true;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Whether a call's final result must be delivered as a deferred message.
|
|
2194
|
+
*
|
|
2195
|
+
* An async call (`cancelOnInterruption: false`) whose result arrives before
|
|
2196
|
+
* the conversation moved on is indistinguishable from a synchronous one,
|
|
2197
|
+
* and settles in place: its "started" placeholder becomes the tool result,
|
|
2198
|
+
* and the LLM sees an ordinary call. The context alone decides. The result
|
|
2199
|
+
* is deferred when a user or developer message follows the placeholder:
|
|
2200
|
+
* something the user said, new task instructions, or an intermediate update
|
|
2201
|
+
* from this call. Assistant messages are ignored, so filler spoken with a
|
|
2202
|
+
* `TTSSpeakFrame` while the call runs does not defer it, whether the tool
|
|
2203
|
+
* handler or an event handler spoke it. The same holds for text the model
|
|
2204
|
+
* wrote after seeing the placeholder with no user turn in between, as when
|
|
2205
|
+
* an ungrouped sibling's result runs inference; the result still settles in
|
|
2206
|
+
* place. Other calls' placeholders, results, and deferred messages are
|
|
2207
|
+
* ignored too. A placeholder that is no longer in the context, because the
|
|
2208
|
+
* context was rebuilt while the call ran, also defers.
|
|
2209
|
+
*
|
|
2210
|
+
* @param inProgressFrame The call's in-progress frame.
|
|
2211
|
+
* @returns True when the result goes into a developer message.
|
|
2212
|
+
*/
|
|
2213
|
+
_isDeferred(inProgressFrame) {
|
|
2214
|
+
if (inProgressFrame.cancelOnInterruption) {
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
const toolCallId = inProgressFrame.toolCallId;
|
|
2218
|
+
let afterPlaceholder = false;
|
|
2219
|
+
for (const message of this._context.messages) {
|
|
2220
|
+
if (message instanceof LLMSpecificMessage) {
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
if (!afterPlaceholder) {
|
|
2224
|
+
afterPlaceholder = message.role === "tool" && message.tool_call_id === toolCallId;
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const role = message.role;
|
|
2228
|
+
if (role === "tool" || role === "assistant") {
|
|
2229
|
+
// A sibling's placeholder or result, or assistant output.
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
const payload = asyncToolMessages.parseMessage(message);
|
|
2233
|
+
if (payload !== null && payload.toolCallId !== toolCallId) {
|
|
2234
|
+
// A sibling's deferred update or result.
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
return !afterPlaceholder;
|
|
2240
|
+
}
|
|
2241
|
+
_updateFunctionCallResult(_functionName, toolCallId, result) {
|
|
2242
|
+
for (const message of this._context.getMessages()) {
|
|
2243
|
+
if (!(message instanceof LLMSpecificMessage) &&
|
|
2244
|
+
message.role === "tool" &&
|
|
2245
|
+
message.tool_call_id &&
|
|
2246
|
+
message.tool_call_id === toolCallId) {
|
|
2247
|
+
message.content = result;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
_contextUpdatedTaskFinished(task) {
|
|
2252
|
+
this._contextUpdatedTasks.delete(task);
|
|
2253
|
+
}
|
|
2254
|
+
async _triggerAssistantTurnStarted() {
|
|
2255
|
+
this._assistantTurnStartTimestamp = timeNowIso8601();
|
|
2256
|
+
await this._callEventHandler("on_assistant_turn_started");
|
|
2257
|
+
}
|
|
2258
|
+
async _triggerAssistantTurnStopped(options = {}) {
|
|
2259
|
+
const interrupted = options.interrupted ?? false;
|
|
2260
|
+
if (!this._assistantTurnStartTimestamp) {
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
let aggregation = await this.pushAggregation();
|
|
2264
|
+
if (aggregation) {
|
|
2265
|
+
// Strip turn completion markers from the transcript
|
|
2266
|
+
aggregation = this._maybeStripTurnCompletionMarkers(aggregation);
|
|
2267
|
+
}
|
|
2268
|
+
const message = new AssistantTurnStoppedMessage({
|
|
2269
|
+
content: aggregation,
|
|
2270
|
+
interrupted,
|
|
2271
|
+
timestamp: this._assistantTurnStartTimestamp,
|
|
2272
|
+
});
|
|
2273
|
+
await this._callEventHandler("on_assistant_turn_stopped", message);
|
|
2274
|
+
if (aggregation) {
|
|
2275
|
+
await this.broadcastFrame(LLMContextAssistantTurnFrame, {
|
|
2276
|
+
text: aggregation,
|
|
2277
|
+
timestamp: this._assistantTurnStartTimestamp,
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
this._assistantTurnStartTimestamp = "";
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Strip turn completion markers from assistant transcript.
|
|
2284
|
+
*
|
|
2285
|
+
* Turn completion markers are used internally for turn completion detection
|
|
2286
|
+
* and shouldn't appear in the final transcript. Both the default markers
|
|
2287
|
+
* and any seen on this turn's marker frames are removed, so a custom marker
|
|
2288
|
+
* set is stripped too.
|
|
2289
|
+
*/
|
|
2290
|
+
_maybeStripTurnCompletionMarkers(text) {
|
|
2291
|
+
let markerFound = false;
|
|
2292
|
+
const markers = new Set([
|
|
2293
|
+
USER_TURN_COMPLETE_MARKER,
|
|
2294
|
+
USER_TURN_INCOMPLETE_SHORT_MARKER,
|
|
2295
|
+
USER_TURN_INCOMPLETE_LONG_MARKER,
|
|
2296
|
+
...this._seenTurnMarkers,
|
|
2297
|
+
]);
|
|
2298
|
+
for (const marker of markers) {
|
|
2299
|
+
if (text.includes(marker)) {
|
|
2300
|
+
text = text.replaceAll(marker, "");
|
|
2301
|
+
markerFound = true;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
// Only strip whitespace if we removed a marker
|
|
2305
|
+
return markerFound ? text.trim() : text;
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Handle summarization request from the summarizer.
|
|
2309
|
+
*
|
|
2310
|
+
* Push the request frame UPSTREAM to the LLM service for processing.
|
|
2311
|
+
*
|
|
2312
|
+
* @param summarizer The summarizer that generated the request.
|
|
2313
|
+
* @param frame The summarization request frame to broadcast.
|
|
2314
|
+
*/
|
|
2315
|
+
async _onRequestSummarization(_summarizer, frame) {
|
|
2316
|
+
await this.pushFrame(frame, FrameDirection.UPSTREAM);
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Handle summary applied event from the summarizer.
|
|
2320
|
+
*
|
|
2321
|
+
* Forwards the event to any registered `on_summary_applied` handlers.
|
|
2322
|
+
*
|
|
2323
|
+
* @param summarizer The summarizer that applied the summary.
|
|
2324
|
+
* @param event The summary applied event.
|
|
2325
|
+
*/
|
|
2326
|
+
async _onSummaryApplied(summarizer, event) {
|
|
2327
|
+
await this._callEventHandler("on_summary_applied", summarizer, event);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
/** Pair of LLM context aggregators for updating context with user and assistant messages. */
|
|
2331
|
+
export class LLMContextAggregatorPair {
|
|
2332
|
+
_user;
|
|
2333
|
+
_assistant;
|
|
2334
|
+
/**
|
|
2335
|
+
* Initialize the LLM context aggregator pair.
|
|
2336
|
+
*
|
|
2337
|
+
* @param context The context to be managed by the aggregators.
|
|
2338
|
+
*/
|
|
2339
|
+
constructor(context, options = {}) {
|
|
2340
|
+
const userParams = options.userParams || new LLMUserAggregatorParams();
|
|
2341
|
+
const assistantParams = options.assistantParams || new LLMAssistantAggregatorParams();
|
|
2342
|
+
const addToolChangeMessages = options.addToolChangeMessages ?? null;
|
|
2343
|
+
if (addToolChangeMessages !== null) {
|
|
2344
|
+
userParams.addToolChangeMessages = addToolChangeMessages;
|
|
2345
|
+
assistantParams.addToolChangeMessages = addToolChangeMessages;
|
|
2346
|
+
}
|
|
2347
|
+
const realtimeServiceMode = options.realtimeServiceMode ?? null;
|
|
2348
|
+
this._user = new LLMUserAggregator(context, {
|
|
2349
|
+
params: userParams,
|
|
2350
|
+
_realtimeServiceMode: realtimeServiceMode,
|
|
2351
|
+
});
|
|
2352
|
+
// Wire the assistant→user back-reference unconditionally: realtime mode
|
|
2353
|
+
// may be auto-configured later (realtimeServiceMode=null), so the
|
|
2354
|
+
// reference must already exist when it flips on. Realtime mode treats the
|
|
2355
|
+
// assistant response start as the user turn's end signal — the assistant
|
|
2356
|
+
// half triggers a (possibly deferred) flush of the user half so the user
|
|
2357
|
+
// message lands in context before the assistant turn starts. The user
|
|
2358
|
+
// side has nothing to flush back, and the behavior stays gated on the
|
|
2359
|
+
// resolved realtime flag, so this reference is inert in cascade mode.
|
|
2360
|
+
this._assistant = new LLMAssistantAggregator(context, {
|
|
2361
|
+
params: assistantParams,
|
|
2362
|
+
_realtimeServiceMode: realtimeServiceMode,
|
|
2363
|
+
_pairedUserAggregator: this._user,
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Get the user context aggregator.
|
|
2368
|
+
*
|
|
2369
|
+
* @returns The user context aggregator instance.
|
|
2370
|
+
*/
|
|
2371
|
+
user() {
|
|
2372
|
+
return this._user;
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Get the assistant context aggregator.
|
|
2376
|
+
*
|
|
2377
|
+
* @returns The assistant context aggregator instance.
|
|
2378
|
+
*/
|
|
2379
|
+
assistant() {
|
|
2380
|
+
return this._assistant;
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Allow destructuring of the aggregator pair.
|
|
2384
|
+
*
|
|
2385
|
+
* @example
|
|
2386
|
+
* const pair = new LLMContextAggregatorPair(context); // The instance
|
|
2387
|
+
* const [user, assistant] = new LLMContextAggregatorPair(context); // Destructured
|
|
2388
|
+
*/
|
|
2389
|
+
*[Symbol.iterator]() {
|
|
2390
|
+
yield this._user;
|
|
2391
|
+
yield this._assistant;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
//# sourceMappingURL=llm_response_universal.js.map
|