Documentation
¶
Index ¶
- Constants
- Variables
- func ComputeDepth(bufferMS, frameMS int) int
- func EndSilenceWindows() int
- func EventLogEnabled() bool
- func MarkEchoTimeout(clip []byte) time.Duration
- func NewSTTService(client *STTClient, input STTInput, output STTOutput) *sttService
- func StartSTTService(ctx context.Context, svc *sttService)
- func TransitionHandlerDefined(state SessionState, source InputSource) bool
- func TurnEndSilenceWindows() int
- func ValidateVAD(factory func() (VADDetector, error)) error
- type BufferedChan
- type ControlEvent
- type ControlOutKind
- type ControlOutMessage
- type DatasetRow
- type DecisionEvent
- type DecisionRecorder
- type FileDecisionRecorder
- type InputSource
- type Message
- type ProdTurnSink
- type RecordingLabel
- type ReplayResult
- type RowLabel
- type STTClient
- type STTInput
- type STTOutput
- type STTPassKind
- type STTRequest
- type STTResult
- type STTRouter
- type STTSegment
- type ScoreReport
- type ServiceInput
- type ServiceOutput
- type Session
- type SessionOption
- func WithClock(after func(time.Duration) <-chan time.Time) SessionOption
- func WithCloseFunc(f func()) SessionOption
- func WithDecisionClock(now func() time.Time) SessionOption
- func WithDecisionRecorder(r DecisionRecorder) SessionOption
- func WithFileDecisionRecorderFromEnv(dir, streamSID, callSID, label string, cfg VADConfig, live io.Writer) SessionOption
- func WithMaxSilenceMS(ms int) SessionOption
- func WithMaxTurnMS(ms int) SessionOption
- func WithMaxUtteranceMS(ms int) SessionOption
- func WithSTTInput(in STTInput) SessionOption
- func WithSTTOutput(out STTOutput) SessionOption
- func WithSimTurnMS(ms int) SessionOption
- func WithTranscriptAgentLabel(label string) SessionOption
- func WithTranscriptOutput(dir, sid string, live io.Writer) SessionOption
- func WithTurnEndPolicy(p TurnEndPolicy) SessionOption
- func WithTurnSink(sink TurnSink) SessionOption
- func WithTwilioControlInput(in TwilioControlPlaneInput) SessionOption
- func WithTwilioControlOutput(out TwilioControlPlaneOutput) SessionOption
- func WithTwilioDataInput(in TwilioDataPlaneInput) SessionOption
- func WithTwilioDataOutput(out TwilioDataPlaneOutput) SessionOption
- func WithVADConfig(cfg VADConfig) SessionOption
- func WithVADEventObserver(f func(ev VADEvent, emitted bool)) SessionOption
- func WithVADFactory(f func() (VADDetector, error)) SessionOption
- type SessionState
- type StopwordPolicy
- type TimerCompletion
- type TimerFacility
- type TransportType
- type TurnEndPolicy
- type TurnSink
- type TurnTrigger
- type TwilioControlPlaneInput
- type TwilioControlPlaneOutput
- type TwilioDataPlaneInput
- type TwilioDataPlaneOutput
- type VADClient
- type VADConfig
- type VADDetector
- type VADEvent
- type VADInput
- type VADKind
- type VADOutput
- type Verifier
- type VerifierOutcome
Constants ¶
const ( DecisionTypeVAD = "vad" DecisionTypeCap = "cap" DecisionTypeSTTDispatch = "stt_dispatch" DecisionTypeSTTResult = "stt_result" DecisionKindSpeechStart = "speech-start" DecisionKindSilence = "silence" DecisionKindEndOfUtter = "end-of-utterance" DecisionKindTurnEnd = "turn-end" DecisionKindUtteranceCap = "utterance-cap" DecisionKindTurnCap = "turn-cap" DecisionKindIdleTimeout = "idle-timeout" DecisionParamSpeechThresh = "SpeechThresh" DecisionParamSilenceThresh = "SilenceThresh" DecisionParamEndSilence = "EndSilenceMS" DecisionParamTurnEndSilence = "TurnEndSilenceMS" DecisionParamMaxUtterance = "MaxUtteranceMS" DecisionParamMaxTurn = "MaxTurnMS" DecisionParamMaxSilence = "MaxSilenceMS" )
Decision event types and the labels M1 emits. Kept as named constants so the producer (state.go) and any consumer read the same strings.
const DataPlaneBufferMS = 80
DataPlaneBufferMS is the target buffer depth in milliseconds for the data plane (audio frames and recognition results). This buffer absorbs jitter between frame-driven services (audio in, VAD, STT) and the message-driven control plane (LLM, TTS, Twilio out).
const HarnessVersion = "SOP-153-v1"
HarnessVersion tags every row this build of the harness produces (DoD: "rows carry provenance: recording id, VAD config, harness version").
const MarkEchoGraceMS = 500
MarkEchoGraceMS is the only tunable knob in the mark-echo timeout derivation: the margin added atop the farewell clip's own playout duration to give Twilio a chance to echo the mark back (SOP-125 Observable behavior #4).
const MaxSilenceMS = 15000
MaxSilenceMS is how long the session waits, hearing no VAD speech events, before starting call termination (SOP-125 Observable behavior #3).
The clock runs from the caller's most recent speech onset, not their first (see withSpeechReset), so this is dead air after they genuinely stop -- 20s of it read as the engine having failed to notice, and 10s left no room to pause and think mid-call.
const MaxTurnMS = 60000
MaxTurnMS caps a whole turn (SOP-161) -- the caller's entire stretch of speaking before the engine takes her turn, spanning any pauses in between. Unlike MaxUtteranceMS (which resets on every end-of-utterance), this timer runs from the turn's first speech onset until completeTurn, so a caller who breaks a long ramble into several sub-cap utterances is still bounded. 60s is a deliberately conservative start (tightening is a later tuning pass). Overridable per-process by AATOOLKIT_MAX_TURN_MS, and per-session by WithMaxTurnMS.
const MaxUtteranceMS = 45000
MaxUtteranceMS caps a single continuous utterance (SOP-156). While the caller speaks without reaching end-of-utterance, the utterance timer runs; on expiry the engine plays the forced-stop clip and hangs up. 45s is long enough for any genuine turn and short enough that a caller who holds the line open (a phone by a speaker) cannot run up an unbounded Twilio bill. Overridable per-process by AATOOLKIT_MAX_UTTERANCE_MS, and per-session by WithMaxUtteranceMS.
const MuLawFrameMS = 20
MuLawFrameMS is the duration of a single Twilio μ-law audio frame. Twilio sends 160-sample μ-law frames at 8 kHz = 20 ms per frame.
const SampleRateHz = 8000
SampleRateHz is the sample rate of μ-law audio from Twilio.
Variables ¶
var AllSources = []InputSource{ SourceTwilioData, SourceTwilioControl, SourceVADEvent, SourceSTTResult, SourceIdleTimer, SourceMarkEchoTimer, SourceUtteranceTimer, SourceSimTurnTimer, SourceTurnTimer, }
AllSources enumerates every InputSource, in declaration order. Used by TestTransitionTableIsTotal to iterate the full state space.
var AllStates = []SessionState{ StateIdle, StateListening, StateAwaitingFullResult, StateSpeaking, StateTerminating, StateAwaitingMarkEcho, StateClosed, }
AllStates enumerates every SessionState, in declaration order. Used by TestTransitionTableIsTotal to iterate the full state space.
var FrameMS = MuLawFrameMS
FrameMS derives the frame duration in milliseconds from the static sample rate and the standard Twilio frame size. It is computed once at init to allow future flexibility if frame size changes.
Functions ¶
func ComputeDepth ¶
ComputeDepth returns the buffer depth required for the given buffer duration (in milliseconds) and frame size (in milliseconds). depth = ceil(bufferMS / frameMS).
func EndSilenceWindows ¶
func EndSilenceWindows() int
EndSilenceWindows is how many consecutive silence windows the VAD needs to see before it declares end-of-utterance, at the production defaults.
Test seam only. A test that needs to feed "enough silence for the caller to have finished" would otherwise hardcode the count -- EndSilenceMS divided by the window duration -- and that number rots silently the moment either value moves, failing as "end of utterance never fired" instead of pointing at the default that changed. Production code never needs this: vadMachine derives it from its own config.
func EventLogEnabled ¶
func EventLogEnabled() bool
EventLogEnabled reports whether decision-event recording is switched on via AATOOLKIT_EVENT_LOG. Truthy values: 1/true/yes/on (case-insensitive). One definition shared by every wiring site (twilio live path, probeset replay).
func MarkEchoTimeout ¶
MarkEchoTimeout derives how long to wait for Twilio's mark-echo after sending the farewell clip: the clip's own playout duration (μ-law is 1 byte/sample at SampleRateHz) plus MarkEchoGraceMS. Genuinely derived from clip length -- not a hardcoded constant -- so a different clip yields a different timeout (see TestMarkEchoTimeoutDerivedFromClip).
func NewSTTService ¶
NewSTTService returns an sttService reading from input and writing to output. The returned service has a cancel channel (CancelChan) that callers can use to mark requests as abandoned before transcription.
func StartSTTService ¶
StartSTTService runs the sttService's main loop until ctx is cancelled. This is a seam to allow main.go to start the STT service without exporting the sttService.run method.
func TransitionHandlerDefined ¶
func TransitionHandlerDefined(state SessionState, source InputSource) bool
TransitionHandlerDefined reports whether the transition table has an explicit (non-nil) entry for (state, source). Exported so TestTransitionTableIsTotal can assert totality without reaching into package-private table internals.
func TurnEndSilenceWindows ¶
func TurnEndSilenceWindows() int
TurnEndSilenceWindows is how many consecutive silence windows the VAD needs to see, since the start of the current trailing-silence run, before it declares turn-end, at the production defaults.
Test seam only, same rationale as EndSilenceWindows: a test that must feed "enough silence to end the turn" derives the count from config rather than hardcoding it.
func ValidateVAD ¶
func ValidateVAD(factory func() (VADDetector, error)) error
ValidateVAD is the engine's startup self-test for the VAD pipeline: it constructs a detector from the given factory and runs one real inference, failing hard (a non-nil error) if the detector can't be built or produces an implausible result. A consumer calls this once before accepting calls, passing the same factory it wires via WithVADFactory (e.g. NewVADClient(url).Detector()) — since SOP-147 the detector is the out-of- process sidecar, so the self-test also confirms the sidecar answers.
Types ¶
type BufferedChan ¶
type BufferedChan[T any] struct { // contains filtered or unexported fields }
BufferedChan[T] is a concrete service channel implementation that wraps a buffered Go channel. The buffer depth is derived from DataPlaneBufferMS and FrameMS: depth = ceil(DataPlaneBufferMS / FrameMS).
func NewBufferedChan ¶
func NewBufferedChan[T any](depth int) *BufferedChan[T]
NewBufferedChan creates a new BufferedChan with the specified depth.
func (*BufferedChan[T]) Channel ¶
func (bc *BufferedChan[T]) Channel() <-chan T
Channel returns the underlying chan T (read-only view for receivers). Callers can range over this or use it as the receive side of select.
type ControlEvent ¶
ControlEvent is a Twilio control-plane signal (e.g. "stop") routed to a Session's controlIn field, decoupled from twilio.Frame to avoid an import cycle (internal/telephony/twilio already imports internal/telephony). Twilio-side adapters translate twilio.Frame into ControlEvent.
type ControlOutKind ¶
type ControlOutKind string
ControlOutKind identifies which Twilio control-plane message a ControlOutMessage carries: EncodeMark or EncodeClear (SOP-125). EncodeStop is never sent here -- it's a client-side function (twilio/stream.go), not part of the server's outbound vocabulary; the server ends a call by closing the WebSocket instead.
const ( ControlOutMark ControlOutKind = "mark" ControlOutClear ControlOutKind = "clear" )
type ControlOutMessage ¶
type ControlOutMessage struct {
Kind ControlOutKind
MarkName string
}
ControlOutMessage is the send-side counterpart to ControlEvent: a Twilio control-plane message a Session writes out via TwilioControlPlaneOutput.
type DatasetRow ¶
type DatasetRow struct {
RecordingID string `json:"recording_id"`
Utterances []string `json:"utterances"`
Label RowLabel `json:"label"`
EndSilenceMS int `json:"end_silence_ms"`
HarnessVersion string `json:"harness_version"`
}
DatasetRow is one cumulative-prefix row of a replayed recording's FullPass transcripts, labelled structurally rather than by hand.
func RowsFromUtterances ¶
func RowsFromUtterances(recordingID string, recLabel RecordingLabel, utterances []string, endSilenceMS int) []DatasetRow
RowsFromUtterances builds one cumulative-prefix DatasetRow per utterance in utterances (u1, u1+u2, ... u1..un), each labelled structurally from recLabel: every prefix short of the full recording is `incomplete` by construction -- the caller demonstrably kept talking past it, which is not a judgment call. The final, full-recording prefix is `complete`, unless recLabel is `truncated`, in which case the recording never reached a confirmed end and every row -- including the terminal one -- is `incomplete` (ticket SOP-153 "Why": labels are structural, not hand-written; re-running at a different --end-silence-ms produces a different, equally correct set because the utterances it structures over change with the VAD config).
type DecisionEvent ¶
type DecisionEvent struct {
Seq int `json:"seq"`
AudioMS int `json:"audio_ms"`
Type string `json:"type"`
Kind string `json:"kind,omitempty"`
Param string `json:"param,omitempty"`
ParamValue any `json:"param_value,omitempty"`
Prob float32 `json:"prob,omitempty"`
SilenceCount int `json:"silence_count,omitempty"`
RequestID int `json:"request_id,omitempty"`
Effect string `json:"effect,omitempty"`
// STT round-trip fields (SOP-167). Text is the delivered transcript;
// LatencyMS is result_time - dispatch_time from the injected clock;
// AudioBytes is the dispatched mu-law buffer length; STTDurSec is whisper's
// own reported audio duration. All omitempty so non-STT events stay compact.
Text string `json:"text,omitempty"`
LatencyMS int `json:"latency_ms,omitempty"`
AudioBytes int `json:"audio_bytes,omitempty"`
STTDurSec float64 `json:"stt_audio_sec,omitempty"`
}
DecisionEvent is one recorded decision. The shape is deliberately flat and JSON-friendly (one object per JSONL line); fields not relevant to a given event type are omitted. M1 populates only the VAD end-of-utterance shape; later milestones add further event types (STT, caps) and any fields they need.
AudioMS is a position in the INPUT audio (derived from the monotonic VAD-window clock), never wall-clock, so the record is stable under replay.
type DecisionRecorder ¶
type DecisionRecorder interface {
Record(ev DecisionEvent)
Close() error
}
DecisionRecorder receives one structured DecisionEvent per parameterized choice the voice-input path makes (M1: end-of-utterance only). It exists so the parameters we set for dealing with voice input can be evaluated after the fact: each event ties a parameter and its value to a position in the input audio and the effect the choice had.
Implementations must return promptly from Record: it is called synchronously from the session's single sequencer loop and must not block it (same contract as TurnSink). Close flushes any buffered state and is idempotent.
type FileDecisionRecorder ¶
type FileDecisionRecorder struct {
// contains filtered or unexported fields
}
FileDecisionRecorder buffers events in memory, feeds each one live to an io.Writer as it arrives, and on Close flushes the buffer to <streamSID>.events.jsonl plus a <streamSID>.events.header.json in dir. It mirrors the audio tap's lifecycle (per-stream, buffer, write-on-close) and is written to the same directory so events sit beside the audio they describe.
func NewFileDecisionRecorder ¶
func NewFileDecisionRecorder(dir, streamSID, callSID, label string, cfg VADConfig, live io.Writer) *FileDecisionRecorder
NewFileDecisionRecorder builds a recorder that writes into dir. A nil return (dir == "") lets the caller decide, per the tap convention, to wire nothing rather than a disabled object. live is where each event is echoed as it arrives; nil defaults to os.Stderr.
func (*FileDecisionRecorder) Close ¶
func (r *FileDecisionRecorder) Close() error
Close writes the header and the buffered events, then marks the recorder closed so a second Close (or a late Record) is a no-op. Both files are keyed by streamSID to line up with the tap's <streamSID>.in.ulaw / .json naming.
func (*FileDecisionRecorder) Record ¶
func (r *FileDecisionRecorder) Record(ev DecisionEvent)
type InputSource ¶
type InputSource int
InputSource identifies which channel or timer fired to produce a transitionEvent. It is the second axis of the (state, source) transition table.
const ( SourceTwilioData InputSource = iota SourceTwilioControl SourceVADEvent SourceSTTResult SourceIdleTimer SourceMarkEchoTimer SourceUtteranceTimer SourceSimTurnTimer SourceTurnTimer )
func (InputSource) String ¶
func (s InputSource) String() string
type Message ¶
type Message struct {
Text string
Transport TransportType
SessionID string
}
Message is a typed turn at the policy seam: the compiled harness produces one per user utterance; the interpreted policy reads it and adapts response style to the transport.
type ProdTurnSink ¶
type ProdTurnSink struct {
CallSID string
// contains filtered or unexported fields
}
ProdTurnSink is the production TurnSink: it logs turn completions with structured logging (turn number, text, timestamp). Satisfies the TurnSink interface defined in vad.go.
func (*ProdTurnSink) OnEndOfUtterance ¶
func (p *ProdTurnSink) OnEndOfUtterance()
func (*ProdTurnSink) OnSpeechStart ¶
func (p *ProdTurnSink) OnSpeechStart()
func (*ProdTurnSink) OnTurnComplete ¶
func (p *ProdTurnSink) OnTurnComplete(text string, trigger TurnTrigger)
type RecordingLabel ¶
type RecordingLabel string
RecordingLabel is the vocabulary a captured recording is tagged with at capture time (AATOOLKIT_TAP_LABEL, twilio/tap.go's sidecar "label" field), read back here to derive each recording's dataset rows structurally instead of by hand.
const ( LabelCompleteTurn RecordingLabel = "complete-turn" LabelTruncated RecordingLabel = "truncated" LabelGreeting RecordingLabel = "greeting" LabelHowAreYou RecordingLabel = "how-are-you" LabelDoneEnding RecordingLabel = "done-ending" )
type ReplayResult ¶
type ReplayResult struct {
Text string `json:"text"`
}
ReplayResult is one utterance's FullPass transcript from a replay run (ticket SOP-153 Observable behavior #1: "emits the ordered FullPass transcripts as JSON" -- one per utterance, the same granularity `build` structures its cumulative-prefix rows over, not one per fused turn).
func Replay ¶
func Replay(ctx context.Context, callSID string, audioStream io.Reader, sttClient *STTClient, opts ...SessionOption) ([]ReplayResult, error)
Replay drives audioStream (typically a captured .ulaw's bytes) through a production Session -- the same VAD+STT path a live call runs, not a copy -- and returns the ordered FullPass turn transcripts.
sttClient is the same STTClient production wires (internal/telephony/stt.go); callers pass a real one (NewSTTClient(sttBaseURL)) to drive a genuine whisper sidecar, or one pointed at an httptest server for deterministic tests -- either way it is the identical STTClient.Transcribe code path, never a stand-in.
opts overrides Session construction after Replay's own defaults (turn sink, data/control/STT wiring, StopwordPolicy) are applied, so a caller can inject e.g. WithVADConfig to change --end-silence-ms, or WithVADFactory in tests that don't need the real Silero model.
Replay is deterministic: for fixed audioStream bytes, a fixed sttClient response set, and fixed opts, two calls produce byte-identical results, every run -- no wall-clock sleep gates any part of this function.
type RowLabel ¶
type RowLabel string
RowLabel is a dataset row's structurally-derived completeness label.
type STTClient ¶
type STTClient struct {
// contains filtered or unexported fields
}
STTClient transcribes μ-law telephony audio via an OpenAI-shaped /v1/audio/transcriptions endpoint (the whisper sidecar). It holds one keep-alive http.Client so chunked clips reuse a single connection.
func NewSTTClient ¶
NewSTTClient returns a client posting to baseURL (e.g. http://127.0.0.1:7789).
func (*STTClient) Transcribe ¶
func (c *STTClient) Transcribe(ctx context.Context, id string, mulawBytes []byte) (STTResult, error)
Transcribe returns the full verbose_json decode of a μ-law clip. Clips longer than 30s are split into <=30s chunks transcribed in order; their text is joined and the first chunk's Language/Duration/Segments are kept (whisper's chunk-level detail beyond joined text is not defined across a multi-chunk clip -- callers needing per-chunk segments should not send clips over maxClipBytes).
type STTInput ¶
type STTInput = ServiceInput[STTRequest]
STTInput is the receive side of the channel sttService reads STTRequests from (the SOP-116 ServiceInput[T] pattern).
type STTOutput ¶
type STTOutput = ServiceOutput[STTResult]
STTOutput is the send side of the channel sttService writes STTResults to (the SOP-116 ServiceOutput[T] pattern).
type STTPassKind ¶
type STTPassKind string
STTPassKind identifies which recognition pass a request or result belongs to. FullPass is the sole remaining pass: a transcription over the full utterance.
const (
FullPass STTPassKind = "full"
)
type STTRequest ¶
type STTRequest struct {
SessionID string
RequestID int
Kind STTPassKind
Audio []byte
}
STTRequest is the unit of work sent to sttService: a μ-law clip tagged with the session/request identity it must be correlated back to.
type STTResult ¶
type STTResult struct {
SessionID string
RequestID int
Kind STTPassKind
Text string
Language string
Duration float64
Segments []STTSegment
}
STTResult is the lossless whisper verbose_json record for one STTRequest, tagged with the same correlation identity the request carried (Charter R9).
type STTRouter ¶
type STTRouter struct {
// contains filtered or unexported fields
}
STTRouter fans the single sttService result stream out to per-session channels, keyed by SessionID. It exists because sttOut is process-global (one sttService for all calls): sessions receiving from it directly race each other, so a result can be delivered to -- and dropped by -- the wrong session, or sit orphaned in the shared buffer until an unrelated later call drains it (both observed live; see handleSTTResult's SessionID guard, which with the router in place becomes a true invariant check rather than load-bearing routing).
Lifecycle: Register a session's channel before its call starts consuming results, Deregister after the call ends. Results for a SessionID with no registered route are logged and dropped -- with per-session routing that is the correct fate for a result whose call has already hung up.
func NewSTTRouter ¶
NewSTTRouter returns a router that reads from results (the channel sttService writes to). Call Run to start dispatching.
func (*STTRouter) Deregister ¶
Deregister removes sessionID's route, but only if route is still the one registered under it. Results arriving after this are logged and dropped. Deregistering an unknown SessionID, or one whose route has since been replaced, is a no-op.
The identity check is what makes duplicate SessionIDs merely bad rather than silently destructive. Two calls sharing a CallSID means the second Register replaces the first's route; when the first call then ends, a key-only delete would remove the *second*, live call's route, and every result for it from then on would be dropped as "unregistered" -- a call going deaf for the rest of its life because an unrelated one hung up. Comparing identity means an ending call can only ever remove its own route.
func (*STTRouter) Register ¶
Register creates, stores, and returns the per-session result channel for sessionID -- the value to wire into the session via WithSTTOutput. Registering a SessionID that is already registered replaces the old route (logged loudly: it means two live calls share a CallSID, which Twilio guarantees against -- but twilio-cli is a stand-in that mints its own).
The returned route is also the handle to Deregister with: see there.
func (*STTRouter) Run ¶
Run reads results until ctx is cancelled or the results channel errors, dispatching each to its session's registered route. The send is non-blocking: a full route (a session that stopped draining) drops the result with a loud log rather than stalling every other session's delivery.
type STTSegment ¶
type STTSegment struct {
Text string
Start float64
End float64
AvgLogprob float64
NoSpeechProb float64
}
STTSegment is one segment of a whisper verbose_json transcription: a timed span of text with the model's confidence for that span.
type ScoreReport ¶
type ScoreReport struct {
PromptFile string `json:"prompt_file"`
EndSilenceMS int `json:"end_silence_ms"`
RowCount int `json:"row_count"`
Outcomes map[VerifierOutcome]int `json:"outcomes"`
}
ScoreReport is score's output: which prompt ran, at which VAD config, over how many rows, and how many rows landed in each of the four outcomes. Deliberately carries no accuracy field (DoD: "score reports the four outcomes, not accuracy").
func Score ¶
func Score(ctx context.Context, promptFile string, rows []DatasetRow, endSilenceMS int, verify Verifier) (ScoreReport, error)
Score runs verify over every row and tallies the four named outcomes.
type ServiceInput ¶
type ServiceInput[T any] interface { Channel() <-chan T Send(ctx context.Context, val T) error Recv(ctx context.Context) (T, error) }
ServiceInput[T] is the generic interface for a service that accepts typed input. It wraps a channel and provides context-aware send/recv.
type ServiceOutput ¶
type ServiceOutput[T any] interface { Channel() <-chan T Send(ctx context.Context, val T) error Recv(ctx context.Context) (T, error) }
ServiceOutput[T] is the generic interface for a service that produces typed output. It wraps a channel and provides context-aware send/recv.
type Session ¶
Session is the per-call coordination point. It owns the service-interface inputs that drive the pipeline, the call's identity and conversation history, and the cancel func that tears the call down. A single sequencer goroutine per session drives the entire pipeline via one select loop (see run) -- there is no separate VAD-event dispatcher goroutine; turn-taking dispatch happens inline from the transition table.
A Session is driven by one owning goroutine: Start and Close are not safe to call concurrently with each other.
func NewSession ¶
func NewSession(ctx context.Context, callSID string, opts ...SessionOption) *Session
NewSession builds a session for callSID with a cancel derived from ctx. It does not start the sequencer or the VAD goroutine — call Start for that.
func (*Session) Close ¶
func (s *Session) Close()
Close cancels the session and, if the sequencer was started, waits for its goroutines to exit before releasing the VAD service. Safe to call on a session that was never started.
func (*Session) Closed ¶
func (s *Session) Closed() <-chan struct{}
Closed returns a channel that closes when the session transitions to StateClosed. Safe to call from any goroutine.
func (*Session) Start ¶
Start spawns the sequencer and the VAD-forward goroutine for this session. It is idempotent: calling it again after the first success is a no-op returning nil.
Start constructs the session's vadDetector via its vadFactory (required — wired with WithVADFactory; there is no in-process default since SOP-147), failing hard: a missing factory or a factory error is returned and nothing is started — no goroutines are spawned, so a caller that gets an error can retry or abandon the session cleanly.
func (*Session) State ¶
func (s *Session) State() SessionState
State returns the session's current SessionState. Safe to call from any goroutine, including concurrently with the running sequencer.
func (*Session) TurnActive ¶
TurnActive reports whether a turn is currently in progress. Safe to call from any goroutine: turnActive is written only from the sequencer goroutine, always followed later in the same event's handling by setState's Lock/Unlock (called unconditionally after every transition, run()), so acquiring stateMu here -- the same mutex State() uses -- establishes happens-before for turnActive's most recent write too, the same way it does for state itself.
type SessionOption ¶
type SessionOption func(*Session)
SessionOption configures optional Session behavior at construction time.
func WithClock ¶
func WithClock(after func(time.Duration) <-chan time.Time) SessionOption
WithClock replaces the passage of time for this session's timers (idle, utterance, markEcho) with after. nil, the default, means the wall clock.
Test seam only, and the reason it exists: a timer-driven transition can otherwise only be observed by sleeping past its deadline. Such a test asserts on the scheduler rather than the code, and -- worse -- silently stops proving anything the moment the deadline it was meant to outlast grows past the sleep. With a fake clock the test fires the timer itself and the assertion is exact.
func WithCloseFunc ¶
func WithCloseFunc(f func()) SessionOption
WithCloseFunc overrides how the session tears down its underlying transport once Closed via the termination flow. Production wires this to the Twilio WebSocket's close; tests inject a fake to observe it was called without a real connection.
func WithDecisionClock ¶
func WithDecisionClock(now func() time.Time) SessionOption
WithDecisionClock replaces the monotonic clock the decision recorder reads to time STT round-trip latency (SOP-167) with now. nil, the default, means the wall clock (time.Now). Distinct from WithClock: that seam supplies relative durations for timers; this one supplies absolute instants for latency. Test seam -- a fake now advanced by a known delta makes the recorded latency exact.
func WithDecisionRecorder ¶
func WithDecisionRecorder(r DecisionRecorder) SessionOption
WithDecisionRecorder wires the DecisionRecorder that receives one DecisionEvent per parameterized voice-input choice (M1: end-of-utterance). Unset, a session uses a no-op recorder (NewSession default) and records nothing. The session owns the recorder's lifecycle: Close flushes it.
func WithFileDecisionRecorderFromEnv ¶
func WithFileDecisionRecorderFromEnv(dir, streamSID, callSID, label string, cfg VADConfig, live io.Writer) SessionOption
WithFileDecisionRecorderFromEnv returns a SessionOption that wires a FileDecisionRecorder writing into dir when AATOOLKIT_EVENT_LOG is on and dir is non-empty; otherwise it is a no-op option. It folds the enable-flag gate and the nil-recorder check that the live (twilio) and replay (probeset) wiring sites would otherwise each repeat.
func WithMaxSilenceMS ¶
func WithMaxSilenceMS(ms int) SessionOption
WithMaxSilenceMS overrides MaxSilenceMS for this session. Test seam only: MaxSilenceMS's real multi-second default is impractical to wait out in a test.
func WithMaxTurnMS ¶
func WithMaxTurnMS(ms int) SessionOption
WithMaxTurnMS overrides MaxTurnMS for this session (SOP-161). Test seam and the live-testing knob behind AATOOLKIT_MAX_TURN_MS.
func WithMaxUtteranceMS ¶
func WithMaxUtteranceMS(ms int) SessionOption
WithMaxUtteranceMS overrides MaxUtteranceMS for this session (SOP-156). Test seam and the live-testing knob behind AATOOLKIT_MAX_UTTERANCE_MS.
func WithSTTInput ¶
func WithSTTInput(in STTInput) SessionOption
WithSTTInput wires in the STT service's request input as the destination for this session's dispatched full-pass STTRequests (SOP-124).
func WithSTTOutput ¶
func WithSTTOutput(out STTOutput) SessionOption
WithSTTOutput wires in the STT service's result output as this session's source of recognition results.
func WithSimTurnMS ¶
func WithSimTurnMS(ms int) SessionOption
WithSimTurnMS enables sim-turn bed playback (SOP-157) for the configured duration (ms). <= 0 disables it (production default).
func WithTranscriptAgentLabel ¶
func WithTranscriptAgentLabel(label string) SessionOption
WithTranscriptAgentLabel sets the response role label the transcript summary prints (SOP-168). Empty or unset falls back to the generic "agent"; a consumer injects its own product name here so the engine never embeds it.
func WithTranscriptOutput ¶
func WithTranscriptOutput(dir, sid string, live io.Writer) SessionOption
WithTranscriptOutput enables the end-of-call conversation transcript summary (SOP-168). At Close the session renders each turn's utterances bracketed and: prints the summary to live (nil = no print), and writes <sid>.transcript.txt into dir (dir == "" = no file). Live and replay wiring pass the same dir/sid as the audio tap / decision record so the transcript sits beside them.
func WithTurnEndPolicy ¶
func WithTurnEndPolicy(p TurnEndPolicy) SessionOption
WithTurnEndPolicy injects the policy that decides whether a FullPass transcript closes the current turn. Without a policy, turns are only flushed on call end.
func WithTurnSink ¶
func WithTurnSink(sink TurnSink) SessionOption
WithTurnSink overrides the TurnSink that receives this session's VAD boundary events. Start defaults to a logging TurnSink when none is given.
func WithTwilioControlInput ¶
func WithTwilioControlInput(in TwilioControlPlaneInput) SessionOption
WithTwilioControlInput wires in the Twilio control-plane demux output (SOP-120) as this session's source of control-plane signals.
func WithTwilioControlOutput ¶
func WithTwilioControlOutput(out TwilioControlPlaneOutput) SessionOption
WithTwilioControlOutput wires in the destination for the outbound mark sent alongside the farewell clip (SOP-125).
func WithTwilioDataInput ¶
func WithTwilioDataInput(in TwilioDataPlaneInput) SessionOption
WithTwilioDataInput wires in the Twilio data-plane demux output (SOP-120) as this session's source of inbound media payloads.
func WithTwilioDataOutput ¶
func WithTwilioDataOutput(out TwilioDataPlaneOutput) SessionOption
WithTwilioDataOutput wires in the destination for the farewell clip's outbound audio frames (SOP-125).
func WithVADConfig ¶
func WithVADConfig(cfg VADConfig) SessionOption
WithVADConfig overrides this session's VAD config (e.g. EndSilenceMS). Unset (zero-value) fields are filled from defaultVADConfig() by Session.Start via withDefaults, same as an entirely omitted config.
Production never calls this -- every live call runs defaultVADConfig(), which is why vad.go's DefaultVADConfig doc says the config is process-wide, not per-session. This option exists for SOP-153's replay harness: `probeset replay --end-silence-ms N` needs to drive one Session at a caller-chosen threshold per invocation without mutating the package-wide default (which would race concurrent replays and leak into any live call sharing the process).
func WithVADEventObserver ¶
func WithVADEventObserver(f func(ev VADEvent, emitted bool)) SessionOption
WithVADEventObserver registers f to be called once per inference window this session's VAD processes, settling that window's fate (see newVADService's onWindow param in vad.go): emitted=false fires as soon as the window is known to have produced no VADEvent; emitted=true and the real event fire only once that event is guaranteed-delivered onto this session's VAD output channel, i.e. once run()'s select loop is guaranteed to see it, not merely once it was computed.
Production never calls this either. It exists alongside WithVADConfig for SOP-153's replay harness: Replay needs to know every window it fed has been fully accounted for -- including any VADEndOfUtterance it produced having actually reached the sequencer -- before it can safely conclude no more STT dispatches are coming and end the call. A real completion signal in place of a fixed sleep.
func WithVADFactory ¶
func WithVADFactory(f func() (VADDetector, error)) SessionOption
WithVADFactory overrides how Start constructs the vadDetector used by this session's VAD goroutine. Production code doesn't need this — Start defaults to NewSileroDetector — but tests use it to inject a fake detector without touching the real ONNX model.
type SessionState ¶
type SessionState int
SessionState is a state in the session's total (state, source) transition table (charter: single select loop, SOP-123). AwaitingFullResult, Terminating, and AwaitingMarkEcho exist in the enum ahead of the logic that drives them -- their transition handlers are stubs that log "not yet implemented" (SOP-115/G,H own that logic; out of scope here).
const ( StateIdle SessionState = iota StateListening StateAwaitingFullResult StateSpeaking StateTerminating StateAwaitingMarkEcho StateClosed )
func (SessionState) String ¶
func (s SessionState) String() string
type StopwordPolicy ¶
type StopwordPolicy struct{}
StopwordPolicy closes the turn when the normalized transcript equals exactly "done". Normalization: lowercase, trim whitespace, strip trailing punctuation (.!?,). Matching is exact after normalization.
func (StopwordPolicy) IsEndOfTurn ¶
func (StopwordPolicy) IsEndOfTurn(transcript string) bool
type TimerCompletion ¶
TimerCompletion represents the result of a completed timer.
type TimerFacility ¶
type TimerFacility struct {
// contains filtered or unexported fields
}
TimerFacility manages multiple named timers with generation counters. It is NOT thread-safe for direct manipulation, but uses channels for safe coordination between goroutines.
func NewTimerFacility ¶
func NewTimerFacility(ctx context.Context) *TimerFacility
NewTimerFacility creates a new timer facility with the given context. When the context is cancelled, all armed timers' goroutines will exit.
func NewTimerFacilityWithClock ¶
func NewTimerFacilityWithClock(ctx context.Context, after func(time.Duration) <-chan time.Time) *TimerFacility
NewTimerFacilityWithClock builds a facility whose timers wait via after instead of the wall clock. Production passes time.After (NewTimerFacility); tests pass a fake so a timer fires exactly when the test says it does, rather than when the scheduler gets round to it.
func (*TimerFacility) Arm ¶
Arm starts a timer with the given name and duration. It returns a generation counter (timerId). If a timer with this name is already armed, it cancels the previous one first. Returns -1 if the facility's context has been cancelled.
func (*TimerFacility) Cancel ¶
func (tf *TimerFacility) Cancel(name string)
Cancel cancels the timer with the given name. If the timer's completion is already pending, this prevents it from being sent. Cancelling a non-existent timer is a no-op. Post-teardown calls are silently ignored.
func (*TimerFacility) Completions ¶
func (tf *TimerFacility) Completions() <-chan TimerCompletion
Completions returns a read-only channel that receives TimerCompletion events when armed timers fire.
func (*TimerFacility) IsCurrent ¶
func (tf *TimerFacility) IsCurrent(completion TimerCompletion) bool
IsCurrent checks whether the given completion's TimerID is the current generation for its name. This allows consumers to distinguish stale fires from current ones. Returns false if the facility's context is cancelled.
type TransportType ¶
type TransportType string
TransportType identifies the channel over which a conversation turn arrived. It is an open string enum — future transport additions do not require recompilation.
const ( TransportVoice TransportType = "voice" TransportSMS TransportType = "sms" TransportOutbound TransportType = "outbound" )
type TurnEndPolicy ¶
TurnEndPolicy decides whether a FullPass transcript closes the current turn. If IsEndOfTurn returns true the accumulated buffer is flushed (excluding the triggering utterance) and the stopword is consumed. A session with no policy injected still flushes on call end.
type TurnSink ¶
type TurnSink interface {
OnSpeechStart()
OnEndOfUtterance()
OnTurnComplete(text string, trigger TurnTrigger)
}
OnTurnComplete delivers the fused text of a completed turn (SOP-150): multiple utterances joined with a single space, each part trimmed, plus the trigger that ended the turn (SOP-162). Implementations must return promptly: they are called synchronously from the session's single transition-table dispatch loop (state.go's handleSpeechOnset/ dispatchFullPass) and must not block it.
type TurnTrigger ¶
type TurnTrigger string
TurnSink receives turn-taking boundaries dispatched from a session's VAD events. OnSpeechStart fires when the caller starts talking (SOP-95 wires this as a no-op stub — barge-in behavior beyond that is out of scope); OnEndOfUtterance fires when the caller's utterance is judged complete. TurnTrigger names which of completeTurn's seven call sites (state.go, six distinct trigger values — silence-turn-end covers two of the seven: the immediate and the deferred-while-a-pass-was-in-flight paths) ended a turn (SOP-162 DoD: "trigger type" in every TurnSink.OnTurnComplete log line).
const ( TriggerStopword TurnTrigger = "stopword" TriggerSilenceTurnEnd TurnTrigger = "silence-turn-end" TriggerIdleTimeout TurnTrigger = "idle-timeout" TriggerUtteranceCap TurnTrigger = "utterance-cap" TriggerTurnCap TurnTrigger = "turn-cap" TriggerCallEnd TurnTrigger = "call-end" )
type TwilioControlPlaneInput ¶
type TwilioControlPlaneInput = ServiceInput[ControlEvent]
TwilioControlPlaneInput is the receive side of the channel a Session reads Twilio control-plane signals from (SOP-120's demux control plane, adapted).
type TwilioControlPlaneOutput ¶
type TwilioControlPlaneOutput = ServiceOutput[ControlOutMessage]
TwilioControlPlaneOutput is the send side of the channel a Session writes outbound control-plane messages to (mark/clear). The concrete implementation (internal/telephony/twilio/output.go) encodes each message via EncodeMark/EncodeClear and writes it to the WebSocket.
type TwilioDataPlaneInput ¶
type TwilioDataPlaneInput = ServiceInput[[]byte]
TwilioDataPlaneInput is the receive side of the channel a Session reads inbound media payloads from (SOP-120's demux data plane, adapted).
type TwilioDataPlaneOutput ¶
type TwilioDataPlaneOutput = ServiceOutput[[]byte]
TwilioDataPlaneOutput is the send side of the channel a Session writes outbound media frames to (SOP-116 ServiceOutput pattern). The concrete implementation (internal/telephony/twilio/output.go) encodes each payload via EncodeMedia and writes it to the WebSocket; this package only sees the generic interface to avoid an import cycle (twilio already imports telephony).
type VADClient ¶
type VADClient struct {
// contains filtered or unexported fields
}
VADClient posts inference windows to the VAD sidecar's octet-stream endpoint (scripts/vad_server.py). It holds one keep-alive http.Client so a call's stream of windows reuses a single connection, mirroring STTClient.
func NewVADClient ¶
NewVADClient returns a client posting to baseURL (e.g. http://127.0.0.1:7790).
func (*VADClient) Detector ¶
func (c *VADClient) Detector() func() (VADDetector, error)
Detector returns a vadFactory (the WithVADFactory shape) that builds a fresh httpSileroDetector with zeroed state per call — one per session, so no session's recurrent state leaks into another's.
type VADConfig ¶
type VADConfig = vadConfig
VADConfig is an exported alias for vadConfig, so the audio tap (SOP-152, in package twilio) can record the config this process runs. Same need and same shape as the VADDetector alias above: name the type outward without handing out construction. An alias rather than a parallel struct keeps one definition of the fields.
func DefaultVADConfig ¶
func DefaultVADConfig() VADConfig
DefaultVADConfig returns the VAD config every live call runs. It is process-wide for production traffic: no live call passes WithVADConfig, so vadCfg is filled from defaultVADConfig() via withDefaults() for every real session. The tap records it so a later replay (SOP-153) knows which thresholds produced a recording's live log -- defaultVADConfig() is a moving target (f5cad49 took EndSilenceMS 700 -> 1050) and the value cannot be recovered after the fact. SOP-153's replay harness is the one caller that does override per-session, via WithVADConfig (session.go), so it can drive `--end-silence-ms` without touching this package-wide default.
type VADDetector ¶
type VADDetector = vadDetector
VADDetector is an exported alias for vadDetector — identical type, just spellable from outside the package. WithVADFactory's parameter needs an exported name so external test packages (telephony_test) can supply a custom factory without this package exposing detector construction.
type VADEvent ¶
type VADEvent struct {
Kind VADKind
Prob float32 // detector probability that produced this event
VoicedCount int // voiced windows accumulated since speech-start
SilenceCount int // consecutive sub-SilenceThresh windows since speech
WindowIndex int // monotonic per-utterance window counter (resets at each speech onset)
// StreamWindowIndex is a monotonic, never-reset window counter over the
// whole stream (0 at the first window, +1 per window processed). Unlike
// WindowIndex it does not reset at speech onset, so AudioMS =
// StreamWindowIndex * windowMS gives a stable position in the input audio
// for a DecisionEvent -- see decision.go.
StreamWindowIndex int
SessionID string // set by the vadService wrapper at construction time
}
VADEvent is a voice-activity boundary emitted on a vadService's VADOutput. It is a lossless record of the vadMachine's full state at the moment of emission (charter R9) and carries SessionID on every result (charter R10).
type VADInput ¶
type VADInput = ServiceOutput[[]byte]
VADInput is the frames-in plane of a vadService (SOP-116 pattern): callers Send raw μ-law frames for the wrapped vadMachine to consume.
type VADKind ¶
type VADKind string
VADKind is the kind of voice-activity event emitted by the VAD goroutine.
type VADOutput ¶
type VADOutput = ServiceInput[VADEvent]
VADOutput is the events-out plane of a vadService (SOP-116 pattern): callers Recv the VADEvents the wrapped vadMachine emits.
type Verifier ¶
type Verifier func(ctx context.Context, promptFile string, row DatasetRow) (VerifierOutcome, error)
Verifier runs promptFile's prompt over one dataset row and reports which of the four outcomes it produced. cmd/probeset's real score wiring supplies one backed by an actual model call; tests supply a fixed fake, so Score's own aggregation logic is exercised independent of any live prompt call.
type VerifierOutcome ¶
type VerifierOutcome string
VerifierOutcome is one of the four outcomes score reports for a row, in place of raw accuracy (ticket SOP-153 Observable behavior #5: accuracy averages over an asymmetry where one error truncates a caller and the other costs a re-ask).
const ( OutcomeProceed VerifierOutcome = "proceed" OutcomeSpuriousRepair VerifierOutcome = "spurious_repair" OutcomeRepairFires VerifierOutcome = "repair_fires" OutcomePartialAccepted VerifierOutcome = "partial_accepted" )