Documentation
¶
Overview ¶
Package turns manages the user-turn lifecycle, ported from Pipecat's turns subsystem. A UserTurnProcessor drives a UserTurnController (which runs pluggable start and stop strategies) and a UserIdleController. Turn detection is decoupled: voice activity comes from a vad.Processor upstream as VADUser*SpeakingFrames, and the end-of-turn model lives inside a stop strategy; the subsystem reasons over frames, not raw audio (except the turn-analyzer stop strategy, which is fed InputAudioRawFrames).
Index ¶
- Constants
- type AlwaysUserMute
- type Config
- type ControllerHooks
- type Emitter
- type ExternalCompletionStop
- type ExternalStart
- type ExternalStartConfig
- type ExternalStop
- type ExternalStopConfig
- type ExternalStrategiesConfig
- type FirstSpeechUserMute
- type FunctionCallUserMute
- type IdleCallback
- type IdleConfig
- type LLMTurnCompletionStop
- type MinWordsStart
- type MinWordsStartConfig
- type MuteStrategy
- type MuteUntilFirstBotComplete
- type ProcessFrameResult
- type SpeechTimeoutConfig
- type SpeechTimeoutStop
- type StartStrategy
- type StartStrategyBase
- func (b *StartStrategyBase) Cleanup()
- func (b *StartStrategyBase) ResolvesProposedTurnStartFrames() bool
- func (b *StartStrategyBase) Setup(processor.Setup) error
- func (b *StartStrategyBase) TriggerResetAggregation()
- func (b *StartStrategyBase) TriggerStarted()
- func (b *StartStrategyBase) TriggerStartedOverriding(o StartedOverrides)
- func (b *StartStrategyBase) TurnStarted()
- func (b *StartStrategyBase) TurnStopped()
- type StartedOverrides
- type StopStrategy
- type StopStrategyBase
- func (b *StopStrategyBase) Broadcast(build func() frames.Frame)
- func (b *StopStrategyBase) Cleanup()
- func (b *StopStrategyBase) Push(f frames.Frame, dir processor.Direction)
- func (b *StopStrategyBase) ResolvesProposedTurnStopFrames() bool
- func (b *StopStrategyBase) Setup(processor.Setup) error
- func (b *StopStrategyBase) TriggerFinalized()
- func (b *StopStrategyBase) TriggerFinalizedOverriding(o StoppedOverrides)
- func (b *StopStrategyBase) TriggerInferenceTriggered()
- func (b *StopStrategyBase) TriggerStopped()
- func (b *StopStrategyBase) TriggerStoppedOverriding(o StoppedOverrides)
- func (b *StopStrategyBase) TurnStarted()
- func (b *StopStrategyBase) TurnStopped()
- type StoppedOverrides
- type TranscriptionStart
- type TranscriptionStartConfig
- type TurnAnalyzerConfig
- type TurnAnalyzerStop
- type UserIdleController
- func (c *UserIdleController) Broadcast(ctx context.Context, build func() frames.Frame) error
- func (c *UserIdleController) Cleanup()
- func (c *UserIdleController) Process(f frames.Frame)
- func (c *UserIdleController) Push(ctx context.Context, f frames.Frame, dir processor.Direction) error
- func (c *UserIdleController) Setup(ctx context.Context, emit Emitter)
- func (c *UserIdleController) Stop()
- type UserTurnController
- func (c *UserTurnController) Cleanup()
- func (c *UserTurnController) Locked(fn func())
- func (c *UserTurnController) Process(f frames.Frame)
- func (c *UserTurnController) ResolvesProposedTurnStartFrames() bool
- func (c *UserTurnController) ResolvesProposedTurnStopFrames() bool
- func (c *UserTurnController) SetHooks(h ControllerHooks)
- func (c *UserTurnController) Setup(ctx context.Context, st processor.Setup) error
- func (c *UserTurnController) Stop()
- func (c *UserTurnController) Strategies() UserTurnStrategies
- func (c *UserTurnController) UpdateStrategies(strategies UserTurnStrategies) error
- type UserTurnProcessor
- func (p *UserTurnProcessor) Cleanup(ctx context.Context) error
- func (p *UserTurnProcessor) Controller() *UserTurnController
- func (p *UserTurnProcessor) ProcessFrame(ctx context.Context, f frames.Frame, dir processor.Direction) error
- func (p *UserTurnProcessor) Push(ctx context.Context, f frames.Frame, dir processor.Direction) error
- func (p *UserTurnProcessor) Setup(ctx context.Context, s processor.Setup) error
- type UserTurnStartedParams
- type UserTurnStoppedParams
- type UserTurnStrategies
- type VADStart
- type WakePhraseStart
- type WakePhraseStartConfig
Constants ¶
const ( // EventUserTurnStarted fires when a user turn starts, carrying the // StartStrategy that decided it began. // // events.On(p.Events(), turns.EventUserTurnStarted, // func(ctx context.Context, s turns.StartStrategy) { … }) EventUserTurnStarted = "on_user_turn_started" // EventUserTurnInferenceTriggered fires when there is enough signal to start // LLM inference, carrying the StopStrategy that decided. It fires together // with EventUserTurnStopped for most strategies, and alone when a strategy // further down the chain gates finalization on someone else's verdict. EventUserTurnInferenceTriggered = "on_user_turn_inference_triggered" // EventUserTurnStopped fires when a user turn is semantically final, // carrying the StopStrategy that decided. It is nil when nothing decided: // the turn was closed because no strategy did. EventUserTurnStopped = "on_user_turn_stopped" // EventUserTurnStopTimeout fires when no stop strategy triggered before the // watchdog gave up. It carries no argument. EventUserTurnStopTimeout = "on_user_turn_stop_timeout" // EventUserTurnIdle fires when the user has been idle for the configured // timeout. It carries no argument. EventUserTurnIdle = "on_user_turn_idle" )
The events a UserTurnProcessor raises around each turn it decides.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AlwaysUserMute ¶
type AlwaysUserMute struct {
// contains filtered or unexported fields
}
AlwaysUserMute mutes the user whenever the bot is speaking.
func NewAlwaysUserMute ¶
func NewAlwaysUserMute() *AlwaysUserMute
NewAlwaysUserMute builds an always-while-bot-speaking mute strategy.
func (*AlwaysUserMute) ShouldMute ¶
func (s *AlwaysUserMute) ShouldMute(f frames.Frame) bool
ShouldMute reports muted while the bot speaks.
type Config ¶
type Config struct {
// Strategies are the start/stop strategy chains; empty chains use the
// defaults, which are VAD and transcription to start and a speech timeout to
// stop. For end-of-turn from a model, build Stop with NewTurnAnalyzerStop.
Strategies UserTurnStrategies
// StopTimeout is the watchdog that force-stops a stuck turn; 0 uses 5s.
StopTimeout time.Duration
// IdleTimeout enables the idle watchdog; a value <= 0 disables it.
IdleTimeout time.Duration
// OnIdle fires when the conversation goes idle. Required to enable idle.
OnIdle IdleCallback
// MuteStrategies suppress user input while engaged (e.g. while the bot
// speaks or a tool call runs). They are OR-reduced; empty means never mute.
MuteStrategies []MuteStrategy
}
Config configures turn taking, for the user aggregator that drives it or for a UserTurnProcessor of its own.
Run inside the aggregator, the strategies see the same frames in the same order as the aggregation, so a turn that ends on a finalized transcript ends with that transcript already folded into the user's message. Run in a processor of their own, the decision can instead be made once and shared by several aggregators.
type ControllerHooks ¶
type ControllerHooks struct {
// Started, Stopped, InferenceTriggered and ResetAggregation each carry the
// strategy that made the decision, so what the hooks report can name it.
Started func(ctx context.Context, s StartStrategy, params UserTurnStartedParams)
Stopped func(ctx context.Context, s StopStrategy, params UserTurnStoppedParams)
InferenceTriggered func(ctx context.Context, s StopStrategy)
StopTimeout func(ctx context.Context)
ResetAggregation func(ctx context.Context, s StartStrategy)
Push func(ctx context.Context, f frames.Frame, dir processor.Direction)
Broadcast func(ctx context.Context, build func() frames.Frame)
}
ControllerHooks are the callbacks the controller invokes upward (to the UserTurnProcessor). They all run with the controller's mutex held.
type Emitter ¶
type Emitter interface {
// Push sends a frame to the neighbor in dir.
Push(ctx context.Context, f frames.Frame, dir processor.Direction) error
// Broadcast builds one frame per direction and sends them both ways. It takes
// a constructor rather than a frame because the two directions must not share
// one instance: each is processed on its own goroutine, and a frame is owned
// by a single goroutine at a time.
Broadcast(ctx context.Context, build func() frames.Frame) error
}
Emitter lets a controller or strategy push frames into the pipeline. The UserTurnProcessor implements it. Broadcast sends a frame both downstream and upstream, which is how turn decisions and interruptions reach the whole pipeline.
type ExternalCompletionStop ¶
type ExternalCompletionStop struct {
StopStrategyBase
}
ExternalCompletionStop finalizes a turn when an external judge emits a UserTurnInferenceCompletedFrame. It is the base for LLM-gated completion.
func NewExternalCompletionStop ¶
func NewExternalCompletionStop() *ExternalCompletionStop
NewExternalCompletionStop builds an external-completion stop strategy.
func (*ExternalCompletionStop) Process ¶
func (s *ExternalCompletionStop) Process(f frames.Frame) ProcessFrameResult
Process finalizes the turn on a completion frame.
type ExternalStart ¶
type ExternalStart struct {
StartStrategyBase
}
ExternalStart takes its cue for the start of a turn from another processor rather than detecting it. It understands two signals, which differ in how much the emitter has already done:
- A ProposedUserStartedSpeakingFrame is a service with its own turn detection proposing a turn boundary. This strategy makes the decision, emitting the UserStartedSpeakingFrame and broadcasting the interruption itself. Embed it to adjust when, or whether, a proposal opens a turn.
- A UserStartedSpeakingFrame means the turn was already decided and announced elsewhere, typically by a shared turn processor fanning turns out to several aggregators. This strategy adopts that decision and emits nothing, so the turn is not announced twice.
A service that emits turn frames directly lands on the adopt path and keeps working, but it owns the interruption logic itself. Emitting proposals instead hands that job back to the pipeline.
func NewExternalStart ¶
func NewExternalStart(cfg ExternalStartConfig) *ExternalStart
NewExternalStart builds an external start strategy.
func (*ExternalStart) Process ¶
func (s *ExternalStart) Process(f frames.Frame) ProcessFrameResult
Process resolves a proposal, or adopts a turn start decided elsewhere.
func (*ExternalStart) ResolvesProposedTurnStartFrames ¶ added in v0.1.0
func (s *ExternalStart) ResolvesProposedTurnStartFrames() bool
ResolvesProposedTurnStartFrames reports that this strategy resolves proposals into turn starts.
type ExternalStartConfig ¶ added in v0.1.0
type ExternalStartConfig struct {
// EnableInterruptions broadcasts an interruption when a proposal opens a
// turn; nil defaults to true. It is ignored on the adopt path, where the
// emitter has already broadcast one.
EnableInterruptions *bool
}
ExternalStartConfig configures an ExternalStart strategy.
type ExternalStop ¶
type ExternalStop struct {
StopStrategyBase
// contains filtered or unexported fields
}
ExternalStop takes its cue for the end of a turn from another processor. It is the counterpart to ExternalStart and takes the same two signals:
- A ProposedUserStoppedSpeakingFrame is a service proposing that the turn has ended. This strategy decides, and emits the UserStoppedSpeakingFrame itself. It may also hold the turn open past the proposal, which is what WaitForTranscript does.
- A UserStoppedSpeakingFrame means the turn end was already decided and announced elsewhere. This strategy adopts that decision and emits nothing.
To shift the timing further, embed it and override the finalization both paths reach once they decide the turn is over.
func NewExternalStop ¶
func NewExternalStop(cfg ExternalStopConfig) *ExternalStop
NewExternalStop builds an external stop strategy.
func (*ExternalStop) Process ¶
func (s *ExternalStop) Process(f frames.Frame) ProcessFrameResult
Process records the external signals and the transcripts around them. It always continues, so the rest of the stop chain is evaluated.
func (*ExternalStop) ResolvesProposedTurnStopFrames ¶ added in v0.1.0
func (s *ExternalStop) ResolvesProposedTurnStopFrames() bool
ResolvesProposedTurnStopFrames reports that this strategy resolves proposals into turn stops.
func (*ExternalStop) Setup ¶ added in v0.1.0
func (s *ExternalStop) Setup(processor.Setup) error
Setup starts the timer that retries finalization while a turn is open, so a transcript arriving after the stop signal still ends the turn.
func (*ExternalStop) TurnStarted ¶ added in v0.1.0
func (s *ExternalStop) TurnStarted()
TurnStarted readies the strategy to detect the end of the turn now starting.
func (*ExternalStop) TurnStopped ¶ added in v0.1.0
func (s *ExternalStop) TurnStopped()
TurnStopped clears per-turn state once the turn has ended.
type ExternalStopConfig ¶
type ExternalStopConfig struct {
// Timeout is the short delay used internally to handle consecutive or
// slightly delayed transcriptions; 0 uses 500ms.
Timeout time.Duration
// WaitForTranscript holds the turn open until transcript text arrives after
// the external stop signal; nil defaults to true. Set it false when local
// turn detection is the intended driver of the conversation, so transcripts
// are off the latency critical path.
WaitForTranscript *bool
}
ExternalStopConfig configures an ExternalStop strategy.
type ExternalStrategiesConfig ¶ added in v0.1.0
type ExternalStrategiesConfig struct {
// EnableInterruptions broadcasts an interruption when a proposal opens a
// turn; nil defaults to true. A service routes its own should-interrupt
// setting here. It does not apply on the adopt path, where the emitter has
// already broadcast one.
EnableInterruptions *bool
}
ExternalStrategiesConfig configures ExternalStrategies.
type FirstSpeechUserMute ¶
type FirstSpeechUserMute struct {
// contains filtered or unexported fields
}
FirstSpeechUserMute mutes the user only during the bot's first speaking turn, allowing pre-speech input and never muting afterward.
func NewFirstSpeechUserMute ¶
func NewFirstSpeechUserMute() *FirstSpeechUserMute
NewFirstSpeechUserMute builds a first-speech mute strategy.
func (*FirstSpeechUserMute) ShouldMute ¶
func (s *FirstSpeechUserMute) ShouldMute(f frames.Frame) bool
ShouldMute reports muted only during the bot's first speech.
type FunctionCallUserMute ¶
type FunctionCallUserMute struct {
// contains filtered or unexported fields
}
FunctionCallUserMute mutes the user while any tool call is in flight.
func NewFunctionCallUserMute ¶
func NewFunctionCallUserMute() *FunctionCallUserMute
NewFunctionCallUserMute builds a function-call mute strategy.
func (*FunctionCallUserMute) ShouldMute ¶
func (s *FunctionCallUserMute) ShouldMute(f frames.Frame) bool
ShouldMute reports muted while one or more tool calls are running.
type IdleCallback ¶
type IdleCallback func(ctx context.Context, c *UserIdleController) error
IdleCallback runs when the conversation has stayed quiet past the configured timeout. It receives the controller so it can Push or Broadcast frames (a reminder, or an EndFrame to hang up). It runs off the frame path. Following Pipecat, it fires once per arming; escalation/retry, if wanted, is the caller's responsibility.
type IdleConfig ¶
type IdleConfig struct {
// Timeout is how long the conversation may stay quiet after the bot stops
// speaking before Callback fires. A value <= 0 disables idle detection.
Timeout time.Duration
// Callback fires on idle. A nil callback disables idle detection.
Callback IdleCallback
}
IdleConfig configures a UserIdleController.
type LLMTurnCompletionStop ¶ added in v0.1.0
type LLMTurnCompletionStop struct {
*ExternalCompletionStop
// contains filtered or unexported fields
}
LLMTurnCompletionStop finalizes a turn on the LLM's own verdict that the user had finished speaking.
It adds to ExternalCompletionStop the setup the marker protocol needs: on the StartFrame it configures the LLM service to gate its replies on the protocol and seeds the configuration the gating runs under. The update is marked to reach inactive services, so every LLM behind a switcher is configured rather than only the one in use at startup.
Finalization itself is inherited. The LLM service detects the completion marker in its own output, reports the turn complete, and the base turns that into the stop. On an incomplete marker the service re-prompts internally and reports nothing, so the turn stays open.
func NewLLMTurnCompletionStop ¶
func NewLLMTurnCompletionStop(cfg llm.UserTurnCompletionConfig) *LLMTurnCompletionStop
NewLLMTurnCompletionStop builds the stop strategy that finalizes a turn on the LLM's completion verdict. Pair it with deferred detectors via FilterIncompleteUserTurnStrategies.
func (*LLMTurnCompletionStop) Config ¶ added in v0.1.0
func (s *LLMTurnCompletionStop) Config() llm.UserTurnCompletionConfig
Config is the turn-completion configuration this strategy applies.
func (*LLMTurnCompletionStop) Process ¶ added in v0.1.0
func (s *LLMTurnCompletionStop) Process(f frames.Frame) ProcessFrameResult
Process configures the LLM on start and leaves finalization to the base.
type MinWordsStart ¶
type MinWordsStart struct {
StartStrategyBase
// contains filtered or unexported fields
}
MinWordsStart opens a turn only once enough words are heard, raising the bar for interrupting the bot.
func NewMinWordsStart ¶
func NewMinWordsStart(cfg MinWordsStartConfig) *MinWordsStart
NewMinWordsStart builds a min-words start strategy.
func (*MinWordsStart) Process ¶
func (s *MinWordsStart) Process(f frames.Frame) ProcessFrameResult
Process counts words and triggers once the threshold is met.
func (*MinWordsStart) TurnStarted ¶ added in v0.1.0
func (s *MinWordsStart) TurnStarted()
TurnStarted clears the bot-speaking flag: the turn that just started will have interrupted the bot, so the rest of it counts against the single-word threshold without waiting for the bot-stopped frame to catch up.
type MinWordsStartConfig ¶
type MinWordsStartConfig struct {
// MinWords is the word count required to open a turn while the bot is
// speaking (to gate barge-in); a single word suffices when the bot is silent.
MinWords int
// UseInterim counts interim transcripts too; nil defaults to true.
UseInterim *bool
}
MinWordsStartConfig configures a MinWordsStart strategy.
type MuteStrategy ¶
MuteStrategy decides whether user input should be suppressed right now. ShouldMute is called for every frame (so the strategy can track state) and returns the muted state as of that frame. The UserTurnProcessor OR-reduces all strategies and, while muted, drops the user-input frames before they reach turn detection — so the user can neither barge in nor pollute the context at the wrong moment. Strategies are driven only from the processor (under its mute mutex) and need no locking of their own.
type MuteUntilFirstBotComplete ¶
type MuteUntilFirstBotComplete struct {
// contains filtered or unexported fields
}
MuteUntilFirstBotComplete mutes the user from the start of the session until the bot finishes its first speech.
The mute is also released when the bot's first speaking turn fails before producing any audio, so that a failed opening leaves the user able to speak.
func NewMuteUntilFirstBotComplete ¶
func NewMuteUntilFirstBotComplete() *MuteUntilFirstBotComplete
NewMuteUntilFirstBotComplete builds a mute-until-first-bot-complete strategy.
func (*MuteUntilFirstBotComplete) ShouldMute ¶
func (s *MuteUntilFirstBotComplete) ShouldMute(f frames.Frame) bool
ShouldMute reports muted until the bot's first speech completes, or until that first turn fails before it starts.
type ProcessFrameResult ¶
type ProcessFrameResult int
ProcessFrameResult is what a strategy returns from Process to control the per-frame strategy loop.
const ( // Continue evaluates the next strategy in the chain. Continue ProcessFrameResult = iota // Stop short-circuits the remaining strategies for this frame. Stop )
type SpeechTimeoutConfig ¶
type SpeechTimeoutConfig struct {
// UserSpeechTimeout is the silence the user gets to resume after the VAD
// stops; 0 uses 600ms.
UserSpeechTimeout time.Duration
// WaitForTranscript holds the turn open until a transcript arrives; nil
// defaults to true.
WaitForTranscript *bool
}
SpeechTimeoutConfig configures a SpeechTimeoutStop strategy.
type SpeechTimeoutStop ¶
type SpeechTimeoutStop struct {
StopStrategyBase
// contains filtered or unexported fields
}
SpeechTimeoutStop ends a turn purely on silence timers after the VAD reports the user stopped — no model. It is the model-free default stop strategy.
func NewSpeechTimeoutStop ¶
func NewSpeechTimeoutStop(cfg SpeechTimeoutConfig) *SpeechTimeoutStop
NewSpeechTimeoutStop builds a speech-timeout stop strategy.
func (*SpeechTimeoutStop) Process ¶
func (s *SpeechTimeoutStop) Process(f frames.Frame) ProcessFrameResult
Process runs the silence timers and decides end-of-turn.
func (*SpeechTimeoutStop) TurnStarted ¶ added in v0.1.0
func (s *SpeechTimeoutStop) TurnStarted()
TurnStarted readies the strategy to detect the end of the turn now starting.
func (*SpeechTimeoutStop) TurnStopped ¶ added in v0.1.0
func (s *SpeechTimeoutStop) TurnStopped()
TurnStopped clears per-turn state once the turn has ended.
type StartStrategy ¶
type StartStrategy interface {
// Process examines one frame; returning Stop short-circuits the start chain
// for that frame. It runs with the shared mutex held.
Process(f frames.Frame) ProcessFrameResult
// TurnStarted readies per-turn state when a turn begins.
TurnStarted()
// TurnStopped clears per-turn state when a turn ends.
TurnStopped()
// Setup hands the strategy the pipeline's configuration, which it knows
// before any frame arrives.
Setup(s processor.Setup) error
// Cleanup releases resources (timers).
Cleanup()
// ResolvesProposedTurnStartFrames reports whether this strategy resolves
// proposals into turn starts.
ResolvesProposedTurnStartFrames() bool
// contains filtered or unexported methods
}
StartStrategy decides when the user's turn begins. Concrete strategies embed StartStrategyBase and implement Process.
func DefaultStartStrategies ¶
func DefaultStartStrategies() []StartStrategy
DefaultStartStrategies returns the default start chain: VAD onset, with transcription as a fallback for soft speech the VAD misses.
type StartStrategyBase ¶
type StartStrategyBase struct {
// EnableInterruptions broadcasts an InterruptionFrame on turn start.
EnableInterruptions bool
// EnableUserSpeakingFrames broadcasts a UserStartedSpeakingFrame on turn start.
EnableUserSpeakingFrames bool
// contains filtered or unexported fields
}
StartStrategyBase is embedded by every start strategy. It carries the open-turn flags and the trigger helpers.
func (*StartStrategyBase) Cleanup ¶
func (b *StartStrategyBase) Cleanup()
Cleanup is the default no-op.
func (*StartStrategyBase) ResolvesProposedTurnStartFrames ¶ added in v0.1.0
func (b *StartStrategyBase) ResolvesProposedTurnStartFrames() bool
ResolvesProposedTurnStartFrames reports whether this strategy resolves proposals into turn starts.
A ProposedUserStartedSpeakingFrame is a request for a decision, so a strategy that acts on one consumes it: the frame stops traveling, and no resolver further along decides the same turn a second time. Override to true in a strategy that handles that frame.
func (*StartStrategyBase) Setup ¶ added in v0.1.0
func (b *StartStrategyBase) Setup(processor.Setup) error
Setup is the default no-op.
func (*StartStrategyBase) TriggerResetAggregation ¶
func (b *StartStrategyBase) TriggerResetAggregation()
TriggerResetAggregation asks the user aggregator to drop the in-progress aggregation (e.g. pre-wake-phrase speech).
func (*StartStrategyBase) TriggerStarted ¶
func (b *StartStrategyBase) TriggerStarted()
TriggerStarted signals that the user's turn has begun, with the flags the strategy was built with.
func (*StartStrategyBase) TriggerStartedOverriding ¶ added in v0.1.0
func (b *StartStrategyBase) TriggerStartedOverriding(o StartedOverrides)
TriggerStartedOverriding signals that the user's turn has begun, overriding the strategy's configured flags for this turn.
func (*StartStrategyBase) TurnStarted ¶ added in v0.1.0
func (b *StartStrategyBase) TurnStarted()
TurnStarted is the default no-op.
func (*StartStrategyBase) TurnStopped ¶ added in v0.1.0
func (b *StartStrategyBase) TurnStopped()
TurnStopped is the default no-op.
type StartedOverrides ¶ added in v0.1.0
type StartedOverrides struct {
// EnableInterruptions overrides whether an interruption is broadcast for
// this turn. Set it false when something else in the pipeline has already
// broadcast one.
EnableInterruptions *bool
// EnableUserSpeakingFrames overrides whether a UserStartedSpeakingFrame is
// emitted for this turn. Set it false when something else in the pipeline
// has already emitted it.
EnableUserSpeakingFrames *bool
}
StartedOverrides overrides, for one turn, the flags the strategy was built with. A nil field keeps the configured value.
type StopStrategy ¶
type StopStrategy interface {
// Process examines one frame; returning Stop short-circuits the stop chain.
// Stop strategies usually return Continue and signal via Trigger*. It runs
// with the shared mutex held.
Process(f frames.Frame) ProcessFrameResult
// TurnStarted readies per-turn state when a turn begins.
TurnStarted()
// TurnStopped clears per-turn state, including any buffered speech, when a
// turn ends.
TurnStopped()
// Setup hands the strategy the pipeline's configuration, which it knows
// before any frame arrives.
Setup(s processor.Setup) error
// Cleanup releases resources (timers).
Cleanup()
// ResolvesProposedTurnStopFrames reports whether this strategy resolves
// proposals into turn stops.
ResolvesProposedTurnStopFrames() bool
// contains filtered or unexported methods
}
StopStrategy decides when the user's turn ends. Concrete strategies embed StopStrategyBase and implement Process.
func DefaultStopStrategies ¶
func DefaultStopStrategies() []StopStrategy
DefaultStopStrategies returns the default, model-free stop chain: a speech-timeout after VAD stop. For Smart-Turn, pass a chain built with NewTurnAnalyzerStop instead.
func Deferred ¶
func Deferred(inner StopStrategy) StopStrategy
Deferred wraps inner so it can drive inference-triggering but never finalize a turn; pair it with a finalizer such as ExternalCompletionStop.
type StopStrategyBase ¶
type StopStrategyBase struct {
// EnableUserSpeakingFrames broadcasts a UserStoppedSpeakingFrame on turn stop.
EnableUserSpeakingFrames bool
// contains filtered or unexported fields
}
StopStrategyBase is embedded by every stop strategy.
func (*StopStrategyBase) Broadcast ¶
func (b *StopStrategyBase) Broadcast(build func() frames.Frame)
Broadcast builds one frame per direction and sends them both ways. It takes a constructor so the two directions never share an instance.
func (*StopStrategyBase) Cleanup ¶
func (b *StopStrategyBase) Cleanup()
Cleanup is the default no-op.
func (*StopStrategyBase) Push ¶
func (b *StopStrategyBase) Push(f frames.Frame, dir processor.Direction)
Push sends a frame to the neighbor in dir.
func (*StopStrategyBase) ResolvesProposedTurnStopFrames ¶ added in v0.1.0
func (b *StopStrategyBase) ResolvesProposedTurnStopFrames() bool
ResolvesProposedTurnStopFrames reports whether this strategy resolves proposals into turn stops.
A ProposedUserStoppedSpeakingFrame is a request for a decision, so a strategy that acts on one consumes it: the frame stops traveling, and no resolver further along decides the same turn a second time. Override to true in a strategy that handles that frame, including one that holds the proposal for a while before deciding.
func (*StopStrategyBase) Setup ¶ added in v0.1.0
func (b *StopStrategyBase) Setup(processor.Setup) error
Setup is the default no-op.
func (*StopStrategyBase) TriggerFinalized ¶
func (b *StopStrategyBase) TriggerFinalized()
TriggerFinalized signals that the turn is semantically final.
func (*StopStrategyBase) TriggerFinalizedOverriding ¶ added in v0.1.0
func (b *StopStrategyBase) TriggerFinalizedOverriding(o StoppedOverrides)
TriggerFinalizedOverriding signals that the turn is semantically final, overriding the strategy's configured flags for this turn.
func (*StopStrategyBase) TriggerInferenceTriggered ¶
func (b *StopStrategyBase) TriggerInferenceTriggered()
TriggerInferenceTriggered signals that there is enough evidence to start LLM inference, without finalizing the turn.
func (*StopStrategyBase) TriggerStopped ¶
func (b *StopStrategyBase) TriggerStopped()
TriggerStopped fires inference-triggered then finalized, the usual "turn is over" signal.
To leave finalization to another strategy, so this one fires only the inference trigger, wrap it with Deferred rather than changing the call.
func (*StopStrategyBase) TriggerStoppedOverriding ¶ added in v0.1.0
func (b *StopStrategyBase) TriggerStoppedOverriding(o StoppedOverrides)
TriggerStoppedOverriding fires inference-triggered then finalized, overriding the strategy's configured flags for this turn.
func (*StopStrategyBase) TurnStarted ¶ added in v0.1.0
func (b *StopStrategyBase) TurnStarted()
TurnStarted is the default no-op.
func (*StopStrategyBase) TurnStopped ¶ added in v0.1.0
func (b *StopStrategyBase) TurnStopped()
TurnStopped is the default no-op.
type StoppedOverrides ¶ added in v0.1.0
type StoppedOverrides struct {
// EnableUserSpeakingFrames overrides whether a UserStoppedSpeakingFrame is
// emitted for this turn. Set it false when something else in the pipeline
// has already emitted it.
EnableUserSpeakingFrames *bool
}
StoppedOverrides overrides, for one turn, the flags the strategy was built with. A nil field keeps the configured value.
type TranscriptionStart ¶
type TranscriptionStart struct {
StartStrategyBase
// contains filtered or unexported fields
}
TranscriptionStart opens a turn on a transcript, a fallback for soft speech a VAD misses.
func NewTranscriptionStart ¶
func NewTranscriptionStart(cfg TranscriptionStartConfig) *TranscriptionStart
NewTranscriptionStart builds a transcription-based start strategy.
func (*TranscriptionStart) Process ¶
func (s *TranscriptionStart) Process(f frames.Frame) ProcessFrameResult
Process triggers the turn on a transcript.
type TranscriptionStartConfig ¶
type TranscriptionStartConfig struct {
// UseInterim also triggers on interim transcripts; nil defaults to true.
UseInterim *bool
}
TranscriptionStartConfig configures a TranscriptionStart strategy.
type TurnAnalyzerConfig ¶
type TurnAnalyzerConfig struct {
// Analyzer is the end-of-turn model (e.g. Smart Turn V3). Required.
Analyzer turn.Analyzer
// WaitForTranscript holds the turn open until a transcript arrives; nil
// defaults to true. Set false for realtime services that bypass STT.
WaitForTranscript *bool
}
TurnAnalyzerConfig configures a TurnAnalyzerStop strategy.
type TurnAnalyzerStop ¶
type TurnAnalyzerStop struct {
StopStrategyBase
// contains filtered or unexported fields
}
TurnAnalyzerStop ends a turn using an end-of-turn model fed the user's audio, gated on a finalized transcript (or a safety-net timeout). This is the Smart-Turn stop strategy.
func NewTurnAnalyzerStop ¶
func NewTurnAnalyzerStop(cfg TurnAnalyzerConfig) *TurnAnalyzerStop
NewTurnAnalyzerStop builds a Smart-Turn stop strategy.
func (*TurnAnalyzerStop) Process ¶
func (s *TurnAnalyzerStop) Process(f frames.Frame) ProcessFrameResult
Process feeds the analyzer and decides end-of-turn.
func (*TurnAnalyzerStop) Setup ¶ added in v0.1.0
func (s *TurnAnalyzerStop) Setup(st processor.Setup) error
Setup tells the analyzer the pipeline's input rate, which is known before any audio arrives.
func (*TurnAnalyzerStop) TurnStarted ¶ added in v0.1.0
func (s *TurnAnalyzerStop) TurnStarted()
TurnStarted resets the bookkeeping but keeps the analyzer's buffered pre-speech audio for the turn now beginning.
func (*TurnAnalyzerStop) TurnStopped ¶ added in v0.1.0
func (s *TurnAnalyzerStop) TurnStopped()
TurnStopped resets the bookkeeping and clears the analyzer's buffered speech.
type UserIdleController ¶
type UserIdleController struct {
// contains filtered or unexported fields
}
UserIdleController fires IdleConfig.Callback when the conversation stays quiet after the bot finishes speaking. It is owned by a UserTurnProcessor, which feeds it every frame plus synthetic user-speaking frames on its turn decisions. The timer arms on BotStoppedSpeakingFrame (only when no user turn is in progress and no tool calls are pending) and is canceled by bot/user speech onset or a tool call.
A UserIdleTimeoutUpdateFrame applies at once: it restarts a running timer with the new duration and, while the bot waits for the user to speak, arms the timer even when idle detection was disabled before.
func NewUserIdleController ¶
func NewUserIdleController(cfg IdleConfig) *UserIdleController
NewUserIdleController builds a user-idle controller.
func (*UserIdleController) Broadcast ¶
Broadcast sends a frame both downstream and upstream (for use from the callback), building a separate instance for each direction.
func (*UserIdleController) Cleanup ¶
func (c *UserIdleController) Cleanup()
Cleanup cancels any pending timer.
func (*UserIdleController) Process ¶
func (c *UserIdleController) Process(f frames.Frame)
Process updates idle state from one frame and arms/cancels the timer.
func (*UserIdleController) Push ¶
func (c *UserIdleController) Push(ctx context.Context, f frames.Frame, dir processor.Direction) error
Push sends a frame to the neighbor in dir (for use from the callback).
func (*UserIdleController) Setup ¶
func (c *UserIdleController) Setup(ctx context.Context, emit Emitter)
Setup records the session context and the emitter used by the callback.
func (*UserIdleController) Stop ¶ added in v0.1.0
func (c *UserIdleController) Stop()
Stop cancels any pending timer. Its owner calls it at the end of the session, so the timer cannot report an idleness that only means the session is over.
It has no Start to pair with: the timer is created in response to speech rather than at the start of the session, so there is nothing to bring up.
type UserTurnController ¶
type UserTurnController struct {
// contains filtered or unexported fields
}
UserTurnController runs the start and stop strategy chains and owns the user-turn state machine: double-start/stop guards and a stop-timeout watchdog. A single mutex serializes every state mutation — Process and all strategy timer callbacks acquire it — so strategies need no locking of their own.
func NewUserTurnController ¶
func NewUserTurnController(strategies UserTurnStrategies, stopTimeout time.Duration) *UserTurnController
NewUserTurnController builds a controller. A zero stopTimeout uses 5s; empty strategy lists fall back to the defaults.
func (*UserTurnController) Cleanup ¶
func (c *UserTurnController) Cleanup()
Cleanup stops the watchdog and cleans up the strategies.
func (*UserTurnController) Locked ¶ added in v0.1.0
func (c *UserTurnController) Locked(fn func())
Locked runs fn with the controller's lock held, so a caller can keep its own turn-scoped state in step with the decisions the strategies make.
A strategy decides a turn is over from a timer, and the whole stop sequence runs from that timer under this lock: the inference trigger and the finalization that follows it are one indivisible step. State a caller updates through here therefore cannot land in the middle of that step, only before or after it, which is what keeps a turn's end from being observed half-made.
fn must not push frames or re-enter the controller.
func (*UserTurnController) Process ¶
func (c *UserTurnController) Process(f frames.Frame)
Process taps one frame: it updates the speaking flag, re-arms the watchdog, and runs the start then stop strategy chains. It holds the mutex throughout, so the strategies' synchronous Trigger* callbacks run safely without re-locking.
func (*UserTurnController) ResolvesProposedTurnStartFrames ¶ added in v0.1.0
func (c *UserTurnController) ResolvesProposedTurnStartFrames() bool
ResolvesProposedTurnStartFrames reports whether any active start strategy resolves proposed turn starts.
A proposal is resolved once, so a caller holding this controller stops forwarding a ProposedUserStartedSpeakingFrame when this is true: passing it along would let a resolver further down the pipeline decide the same turn a second time.
func (*UserTurnController) ResolvesProposedTurnStopFrames ¶ added in v0.1.0
func (c *UserTurnController) ResolvesProposedTurnStopFrames() bool
ResolvesProposedTurnStopFrames is the end-of-turn counterpart to ResolvesProposedTurnStartFrames.
func (*UserTurnController) SetHooks ¶
func (c *UserTurnController) SetHooks(h ControllerHooks)
SetHooks installs the upward callbacks. Call before Setup.
func (*UserTurnController) Setup ¶
Setup records the session context and binds each strategy to the shared environment.
func (*UserTurnController) Stop ¶ added in v0.1.0
func (c *UserTurnController) Stop()
Stop tears the turn watchdog down, leaving the strategies alone. Its owner calls it at the end of the session: left running the watchdog reports what ending looks like rather than a turn that really stalled, since no turn finishes once the session is over. The strategies may be shared, so cleaning them up waits for Cleanup.
func (*UserTurnController) Strategies ¶ added in v0.1.0
func (c *UserTurnController) Strategies() UserTurnStrategies
Strategies are the chains the controller is currently running.
func (*UserTurnController) UpdateStrategies ¶ added in v0.1.0
func (c *UserTurnController) UpdateStrategies(strategies UserTurnStrategies) error
UpdateStrategies replaces the current strategies with the given ones. The chains that go are cleaned up and the ones that arrive are set up with the same pipeline configuration the controller was given, so a caller swapping them does not have to hand it over again.
Empty chains fall back to the defaults, as they do at construction.
type UserTurnProcessor ¶
UserTurnProcessor manages the user-turn lifecycle as a processor of its own.
It drives a UserTurnController, which runs the configured start and stop strategies, and a UserIdleController. What reaches the pipeline (the user speaking frames, the interruption) depends on which strategy decided and what it asked for.
Use it where the turn decision has to be made once and shared: several aggregators fanning off one turn, or a pipeline that wants the decision at a particular point rather than inside the aggregator. Where one aggregator owns the turn, configure the strategies on it instead with aggregators.WithTurns, which keeps the decision on the same frames as the aggregation.
func NewUserTurnProcessor ¶
func NewUserTurnProcessor(cfg Config) *UserTurnProcessor
NewUserTurnProcessor builds a turn processor from the given configuration.
func (*UserTurnProcessor) Cleanup ¶
func (p *UserTurnProcessor) Cleanup(ctx context.Context) error
Cleanup releases what the controllers hold. It is the one call that happens exactly once, which is why releasing a shared strategy belongs here rather than at the end of the session: an ordinary session ends and is then torn down, and a controller releasing what it took twice hands it back once more than it took it.
func (*UserTurnProcessor) Controller ¶ added in v0.1.0
func (p *UserTurnProcessor) Controller() *UserTurnController
Controller is the turn controller this processor drives, so a caller can read the strategies in force or replace them.
func (*UserTurnProcessor) ProcessFrame ¶
func (p *UserTurnProcessor) ProcessFrame( ctx context.Context, f frames.Frame, dir processor.Direction, ) error
ProcessFrame gives each frame to the controllers and forwards it.
The frame is forwarded before the controllers see it, so the decisions they raise are queued behind the frame that caused them rather than ahead of it.
type UserTurnStartedParams ¶
type UserTurnStartedParams struct {
// EnableInterruptions broadcasts an InterruptionFrame so the bot is barged
// in on turn start.
EnableInterruptions bool
// EnableUserSpeakingFrames broadcasts a UserStartedSpeakingFrame on turn
// start. External integrations disable this when they emit it themselves.
EnableUserSpeakingFrames bool
}
UserTurnStartedParams describes how a start strategy wants a turn opened.
func DefaultStartedParams ¶
func DefaultStartedParams() UserTurnStartedParams
DefaultStartedParams is the params a typical start strategy uses.
type UserTurnStoppedParams ¶
type UserTurnStoppedParams struct {
// EnableUserSpeakingFrames broadcasts a UserStoppedSpeakingFrame on turn
// stop.
EnableUserSpeakingFrames bool
}
UserTurnStoppedParams describes how a stop strategy wants a turn closed.
func DefaultStoppedParams ¶
func DefaultStoppedParams() UserTurnStoppedParams
DefaultStoppedParams is the params a typical stop strategy uses.
type UserTurnStrategies ¶
type UserTurnStrategies struct {
Start []StartStrategy
Stop []StopStrategy
// contains filtered or unexported fields
}
UserTurnStrategies holds the start and stop strategy chains a controller runs. Per frame, start strategies run in order until one returns Stop; then stop strategies run the same way (they usually return Continue and signal via their triggers).
func ExternalStrategies ¶
func ExternalStrategies(cfg ExternalStrategiesConfig) UserTurnStrategies
ExternalStrategies returns strategies driven by another component in the pipeline: a service with its own turn detection, or a shared turn processor fanning turns out to several aggregators.
What the aggregator emits depends on which signal drives the turn. A ProposedUserStarted/StoppedSpeakingFrame leaves the decision here, so the aggregator pushes the turn frames and broadcasts interruptions. A UserStarted/StoppedSpeakingFrame means the emitter already announced the turn, so the aggregator emits nothing and EnableInterruptions does not apply.
func FilterIncompleteUserTurnStrategies ¶
func FilterIncompleteUserTurnStrategies(detectors []StopStrategy, cfg llm.UserTurnCompletionConfig) UserTurnStrategies
FilterIncompleteUserTurnStrategies builds a stop chain gated on the LLM's own verdict of whether the user had finished speaking.
The detector chain is preserved but deferred, so it only triggers inference and leaves finalization to the LLM gate appended after it. Pass your detector stop strategies; empty uses the defaults. The LLM service is configured by the gate itself when the pipeline starts, so nothing else has to be set up.
func (UserTurnStrategies) ExternalInterruptions ¶ added in v0.1.0
func (s UserTurnStrategies) ExternalInterruptions() (enabled, isExternal bool)
ExternalInterruptions reports whether these chains were built by ExternalStrategies and, when they were, whether they broadcast an interruption as a proposal opens a turn.
type VADStart ¶
type VADStart struct {
StartStrategyBase
}
VADStart opens a user turn as soon as the VAD reports speech.
type WakePhraseStart ¶
type WakePhraseStart struct {
StartStrategyBase
// contains filtered or unexported fields
}
WakePhraseStart gates a turn behind a spoken wake phrase. Place it first in the start chain: while asleep it blocks the other start strategies; once awake it lets them run until an inactivity timeout puts it back to sleep.
Use SingleActivation to require the phrase before every turn.
func NewWakePhraseStart ¶
func NewWakePhraseStart(cfg WakePhraseStartConfig) *WakePhraseStart
NewWakePhraseStart builds a wake-phrase start strategy.
func (*WakePhraseStart) Cleanup ¶
func (s *WakePhraseStart) Cleanup()
Cleanup stops the inactivity timer.
func (*WakePhraseStart) Process ¶
func (s *WakePhraseStart) Process(f frames.Frame) ProcessFrameResult
Process matches the wake phrase while asleep and keeps the session alive while awake.
func (*WakePhraseStart) TurnStarted ¶ added in v0.1.0
func (s *WakePhraseStart) TurnStarted()
TurnStarted readies the strategy for a new turn.
In timeout mode it keeps the state and refreshes the timeout: a turn starting is the activity that keeps the strategy awake. In single-activation mode it does nothing, because the keepalive window opened when the phrase was detected is what puts the strategy back to sleep, and cutting it short here would block the very turn the phrase opened.
type WakePhraseStartConfig ¶
type WakePhraseStartConfig struct {
// Phrases are the wake phrases (case-insensitive, whitespace-flexible).
Phrases []string
// Timeout is how long the session stays awake without activity; 0 uses 10s.
// In timeout mode the timer resets on activity (user or bot speech). In
// single-activation mode it acts as a keepalive window: the strategy stays
// awake for this long after the phrase is detected, which is what lets the
// turn it opened run to completion before it sleeps again.
Timeout time.Duration
// SingleActivation requires the phrase again for every turn: the strategy
// returns to sleep once the keepalive window closes.
SingleActivation bool
// OnWakePhraseDetected is called with the phrase that matched. It runs with
// the turn lock held, so it must not block or push frames.
OnWakePhraseDetected func(phrase string)
// OnWakePhraseTimeout is called when the inactivity timeout expires and the
// strategy goes back to sleep. Same rules as OnWakePhraseDetected.
OnWakePhraseTimeout func()
}
WakePhraseStartConfig configures a WakePhraseStart strategy.