turns

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: BSD-2-Clause Imports: 14 Imported by: 0

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

View Source
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

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

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) Cleanup

func (s *ExternalStop) Cleanup()

Cleanup stops the retry timer.

func (*ExternalStop) Process

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

Config is the turn-completion configuration this strategy applies.

func (*LLMTurnCompletionStop) Process added in v0.1.0

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

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

type MuteStrategy interface {
	ShouldMute(f frames.Frame) bool
}

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) Cleanup

func (s *SpeechTimeoutStop) Cleanup()

Cleanup stops the timers.

func (*SpeechTimeoutStop) Process

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

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

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

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

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) Cleanup

func (s *TurnAnalyzerStop) Cleanup()

Cleanup stops the timeout.

func (*TurnAnalyzerStop) Process

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

func (c *UserIdleController) Broadcast(ctx context.Context, build func() frames.Frame) error

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

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

type UserTurnProcessor struct {
	*processor.Base
	// contains filtered or unexported fields
}

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.

func (*UserTurnProcessor) Push

Push implements Emitter, so the idle controller's callback can reach the pipeline.

func (*UserTurnProcessor) Setup

Setup wires the controllers.

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.

func NewVADStart

func NewVADStart() *VADStart

NewVADStart builds a VAD-based start strategy.

func (*VADStart) Process

func (s *VADStart) Process(f frames.Frame) ProcessFrameResult

Process triggers the turn on a VAD speech-start.

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

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL