lifecycle

package
v0.40.7 Latest Latest
Warning

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

Go to latest
Published: May 28, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package lifecycle owns mode start/stop orchestration and refcounted shared dependencies for SpeechKit hosts.

SpeechKit exposes three strict modes (Dictation, Assist, Voice Agent). A host that toggles a mode off at runtime should release everything that mode kept alive — Genkit runtime, TTS router, STT router, audio capture, the Voice Agent session manager, etc. Without a coordination layer, hosts either (a) leak goroutines and provider state forever, or (b) bake mode gating into ad-hoc if/else bootstrap code that is impossible to keep symmetric between Device-Target and Server-Target.

This package provides that coordination layer as a framework-neutral, dependency-free public API. It knows nothing about audio, STT, TTS, or LLM concretely — those live in their own internal packages and are surfaced here through SharedDepKey identifiers + factory callbacks registered by the host.

Two-layer model

  • Registry orchestrates mode runtimes (which modes are on / off). Hosts call Registry.Apply with a target set; the registry diffs current state, stops modes that should be off, starts modes that should be on, and emits TransitionEvent values to subscribers.

  • SharedDepRegistry holds refcounted shared dependencies. A mode runtime declares its dependency keys via [ModeRuntime.Requires]; the registry acquires them on Start and releases them on Stop. A dep is lazily constructed on the first Acquire and torn down when its last holder releases.

Mode runtime contract

A ModeRuntime implementation is responsible for translating an Deps map (delivered by the registry on Start) into whatever live state the mode needs (handlers, hot loops, background workers). Implementations MUST be idempotent on Start and Stop and MUST tear down all goroutines they spawned before returning from Stop — see [TestRegistryLeak] for the enforcement contract.

Audit events

Hosts wire audit logging on top of Registry.Subscribe — the lifecycle package does not depend on the audit package. The canonical event names are documented in docs/compliance/audit-event-catalog.md under "Mode lifecycle":

  • speechkit.mode.start (Outcome=success | failure)
  • speechkit.mode.stop (Outcome=success | failure)
  • speechkit.mode.failed (transition aborted by a dependency)

Stability

pkg/speechkit/lifecycle is part of the OSS public surface and follows the same semver discipline as the rest of pkg/speechkit. Before v1.0 the API may still evolve — see CHANGELOG.md for breaking-change calls.

Index

Constants

This section is empty.

Variables

View Source
var ErrModeAlreadyRegistered = errors.New("lifecycle: mode runtime already registered")

ErrModeAlreadyRegistered is returned by Registry.Register when a runtime with the same Name() is registered twice. Re-registering is not supported — hosts should construct the registry once at startup.

View Source
var ErrModeUnknown = errors.New("lifecycle: mode not registered")

ErrModeUnknown is returned by single-mode operations (Start, Stop) when the given key has no registered runtime.

View Source
var ErrNoFactory = errors.New("lifecycle: no factory registered for shared dep")

ErrNoFactory is returned by SharedDepRegistry.Acquire when no factory has been registered for the requested key. Hosts that wire the registry should register every key consumed by any mode they also register; this error indicates a wiring bug, not a runtime failure.

Functions

This section is empty.

Types

type Deps

type Deps struct {
	Values map[SharedDepKey]any
}

Deps is the dependency bundle delivered to [ModeRuntime.Start] by the registry. The Values map contains exactly the keys this runtime declared via [ModeRuntime.Requires], with values produced by the factories registered in SharedDepRegistry.

Helpers below provide typed access; ModeRuntime implementations should never reach into Values directly without a type assertion.

func (Deps) Get

func (d Deps) Get(key SharedDepKey) (any, bool)

Get returns the value associated with key and whether the key was present. Callers that depend on a key MUST have declared it via Requires() — a missing key is a programmer error in the host wiring.

func (Deps) MustGet

func (d Deps) MustGet(key SharedDepKey) any

MustGet returns the value or panics. Use sparingly — only in test fakes and in ModeRuntime.Start where the missing-key path is a non-recoverable wiring bug.

type Diff

type Diff struct {
	ToStart []ModeKey
	ToStop  []ModeKey
}

Diff describes the work Registry.Apply needs to do to reach a target state. ToStop is processed before ToStart so refcounted shared deps released by stopping a mode become available to a mode being started in the same transition.

func (Diff) IsEmpty

func (d Diff) IsEmpty() bool

IsEmpty reports whether the diff would perform no work.

type ModeKey

type ModeKey string

ModeKey identifies one of the three SpeechKit strict modes.

Hosts may extend the value space (e.g. for experimental modes) but the three canonical keys below are guaranteed to be recognised by the Device-Target and Server-Target adapter layers.

const (
	ModeDictation  ModeKey = "dictation"
	ModeAssist     ModeKey = "assist"
	ModeVoiceAgent ModeKey = "voiceagent"
)

type ModeRuntime

type ModeRuntime interface {
	// Name returns the mode identity. The registry uses Name() to key
	// runtimes; duplicate registrations are rejected at Register time.
	Name() ModeKey

	// Start brings the runtime to Running. The deps map carries values
	// for every key returned by Requires(). Returns error if any
	// startup step fails; the registry treats this as a Failed
	// transition and releases any shared deps it acquired on the
	// runtime's behalf.
	Start(ctx context.Context, deps Deps) error

	// Stop tears the runtime down to Stopped. Idempotent. MUST drain
	// goroutines, close channels, and free any state derived from
	// borrowed deps before returning.
	Stop(ctx context.Context) error

	// Status returns the current lifecycle state. The registry treats
	// this as authoritative for diff computations and observability —
	// implementations must keep it consistent with actual goroutine /
	// resource ownership.
	Status() Status

	// Requires returns the SharedDepKeys this runtime needs as Deps
	// inputs. The slice is consumed at every Start; implementations
	// should return a stable copy or a pre-built constant.
	Requires() []SharedDepKey
}

ModeRuntime is the contract every SpeechKit mode implements so the Registry can orchestrate its lifecycle.

Implementations MUST:

  • Be idempotent: Start on a Running runtime returns nil; Stop on a Stopped runtime returns nil.
  • Terminate every goroutine they spawned before Stop returns.
  • Treat the deps map as borrowed: don't retain references past Stop.
  • Return the same Requires() list across the lifetime of the instance. The registry caches this set; mid-life changes are undefined behaviour.

The contract is verified by [TestRegistryLeak] in registry_leak_test.go.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry orchestrates mode runtimes and their shared dependencies.

Typical wiring:

sharedDeps := lifecycle.NewSharedDepRegistry()
sharedDeps.Register("audio.capture", audioFactory)
sharedDeps.Register("stt.router", sttFactory)
// ... etc

registry := lifecycle.NewRegistry(sharedDeps)
_ = registry.Register(dictationRuntime)
_ = registry.Register(assistRuntime)
_ = registry.Register(voiceAgentRuntime)

// At startup or when settings change:
if err := registry.Apply(ctx, lifecycle.Target{
    lifecycle.ModeDictation:  true,
    lifecycle.ModeAssist:     false,
    lifecycle.ModeVoiceAgent: false,
}); err != nil { ... }

Registry is safe for concurrent use; Apply calls serialise on an internal mutex so transitions never interleave.

func NewRegistry

func NewRegistry(sharedDeps *SharedDepRegistry) *Registry

NewRegistry returns an empty registry wired to the given shared-dep registry. Passing nil for sharedDeps creates an empty SharedDepRegistry — useful for tests that don't exercise shared deps.

func (*Registry) Apply

func (r *Registry) Apply(ctx context.Context, target Target) error

Apply transitions every registered mode to the state described by target. Stops run first, then starts. If a start fails, the transition aborts after the failing mode; modes earlier in the ToStart order remain Running. Use TransitionEvent subscribers to observe per-mode outcomes.

Returns the first error encountered. Callers that want all-or- nothing semantics should compute the rollback themselves from subscriber events; the registry deliberately does not auto-roll-back because partial state may be desirable (e.g. Dictation stays up even if Voice Agent failed to acquire Gemini Live).

func (*Registry) Diff

func (r *Registry) Diff(target Target) Diff

Diff computes what Apply would do without performing it. Useful for tests and for UIs that want to preview the transition before committing.

func (*Registry) Register

func (r *Registry) Register(rt ModeRuntime) error

Register adds a mode runtime to the registry. Returns ErrModeAlreadyRegistered when the same Name() is used twice.

func (*Registry) SetClock

func (r *Registry) SetClock(clock func() time.Time)

SetClock overrides the timestamp source for TransitionEvent. Tests use this to inject a deterministic clock; production leaves the default time.Now.

func (*Registry) Shutdown

func (r *Registry) Shutdown(ctx context.Context) error

Shutdown stops every Running mode and releases all shared deps. Hosts should call this on process exit before tearing down the rest of the application. Returns the first stop error, if any — remaining modes are still attempted.

func (*Registry) Snapshot

func (r *Registry) Snapshot() SnapshotView

Snapshot returns a point-in-time view of the registry state.

func (*Registry) Subscribe

func (r *Registry) Subscribe(buffer int) (<-chan TransitionEvent, func())

Subscribe returns a channel that receives every TransitionEvent. Buffer is the channel buffer size; events are dropped (with no error signal) if the consumer is slower than the producer — drain in a goroutine. Use the returned unsubscribe func to stop receiving and allow garbage collection of the channel.

type ReleaseFunc

type ReleaseFunc func() error

ReleaseFunc is returned by SharedDepRegistry.Acquire; calling it decrements the refcount and triggers teardown when the count hits zero. ReleaseFunc is safe to call multiple times — second and subsequent calls are no-ops.

type SharedDepFactory

type SharedDepFactory func(ctx context.Context) (value any, teardown func() error, err error)

SharedDepFactory produces a value plus a teardown callback the registry will invoke when the last holder releases the value.

The teardown MAY be nil for stateless values (e.g. a pre-built catalog struct). Non-nil teardown errors are surfaced to the releasing caller; the registry still drops the entry so a fresh Acquire rebuilds from scratch.

type SharedDepKey

type SharedDepKey string

SharedDepKey identifies a refcounted shared dependency in SharedDepRegistry. Values are opaque strings — the lifecycle package does not interpret them. Hosts choose stable names that match their own module organisation (e.g. "audio.capture", "stt.router", "ai.genkit"). Two ModeRuntime implementations that declare the same key MUST be able to consume the same underlying value type.

type SharedDepRegistry

type SharedDepRegistry struct {
	// contains filtered or unexported fields
}

SharedDepRegistry refcounts shared dependencies so multiple modes can share expensive state (Genkit runtime, STT router, audio capture) without duplicating it, and so a dep is released deterministically when no mode is using it any more.

Construction sequence:

  1. NewSharedDepRegistry creates an empty registry.
  2. Host calls [Register] for every key it expects modes to claim.
  3. Mode runtimes call [Acquire] from their Start methods.
  4. Each Acquire returns a ReleaseFunc the runtime stashes and invokes from its Stop method.

The registry is safe for concurrent use.

func NewSharedDepRegistry

func NewSharedDepRegistry() *SharedDepRegistry

NewSharedDepRegistry returns an empty registry. Register factories on the returned value before any mode tries to acquire.

func (*SharedDepRegistry) Acquire

Acquire returns the value associated with key, lazily constructing it via the registered factory if this is the first holder. The caller MUST invoke the returned ReleaseFunc exactly once when it no longer needs the value (typically from ModeRuntime.Stop).

Concurrent Acquire calls for the same key block on the registry mutex; the factory runs exactly once even under contention.

func (*SharedDepRegistry) Register

func (r *SharedDepRegistry) Register(key SharedDepKey, factory SharedDepFactory)

Register associates a factory with a key. Re-registering the same key replaces the factory — useful in tests; production wiring should register each key exactly once.

func (*SharedDepRegistry) Snapshot

func (r *SharedDepRegistry) Snapshot() []SharedDepStatus

Snapshot returns the current set of live shared deps sorted by key. Useful for OTel metrics emission and for debugging "why is this goroutine still running" investigations.

type SharedDepStatus

type SharedDepStatus struct {
	Key  SharedDepKey
	Refs int
}

SharedDepStatus is a read-only view of one entry for observability.

type SnapshotView

type SnapshotView struct {
	Modes      map[ModeKey]Status
	SharedDeps []SharedDepStatus
}

Snapshot returns the current Status of every registered mode, plus the active shared-dep refcounts. Used by /readyz handlers and OTel metric collection.

type Status

type Status string

Status is a mode runtime's observable lifecycle state.

const (
	// StatusStopped means the mode has never started, or has been cleanly
	// torn down. No goroutines, no live shared-dep references.
	StatusStopped Status = "stopped"

	// StatusStarting means a Start call is in flight but has not yet
	// returned. Shared deps are being acquired or the runtime is wiring up.
	StatusStarting Status = "starting"

	// StatusRunning means the mode is live and serving requests.
	StatusRunning Status = "running"

	// StatusStopping means a Stop call is in flight. The runtime is
	// draining and will release its shared deps once Stop returns.
	StatusStopping Status = "stopping"

	// StatusFailed means the last transition aborted with an error. The
	// runtime is back at rest from the registry's point of view; a fresh
	// Start may be attempted.
	StatusFailed Status = "failed"

	// StatusDisabled means an operator has explicitly turned the mode off
	// (e.g. config flag), distinct from Stopped (transient) — disabled
	// modes never auto-start and do not block readiness checks.
	// See internal/server/core/health.go for the analogous distinction
	// at the HTTP /readyz surface.
	StatusDisabled Status = "disabled"
)

func (Status) IsActive

func (s Status) IsActive() bool

IsActive reports whether the runtime is participating in serving traffic. Starting and Running are active; everything else is not.

func (Status) IsTerminal

func (s Status) IsTerminal() bool

IsTerminal reports whether the runtime is at rest — neither serving nor in the middle of a transition. Hosts can take fresh action (Start, retry, surface error) only on terminal statuses.

func (Status) Rank

func (s Status) Rank() int

Rank orders statuses for "worst-status-wins" aggregation across multiple modes. Higher rank = more attention required. The ordering mirrors core/health.go statusRank semantics so dashboards display consistent overall state.

Stopped  / Disabled → 0
Starting / Stopping → 1
Running             → 2 (not "worst" — Running is the goal)
Failed              → 3

Note that Running ranks above Starting because callers that compare statuses across modes want to detect transitions (Starting) as a distinct "interesting" state without elevating it above a steady Running mode. Failed always wins.

func (Status) String

func (s Status) String() string

String implements fmt.Stringer.

type Target

type Target map[ModeKey]bool

Target is the desired-state input to Registry.Apply. A mode whose key is true should be running; a mode whose key is false (or absent) should be stopped. Modes not registered with the registry are silently ignored — Target may safely include keys for modes the current host build does not ship.

type TransitionEvent

type TransitionEvent struct {
	Mode      ModeKey
	From      Status
	To        Status
	Err       error // populated when To == StatusFailed
	Timestamp time.Time
}

TransitionEvent is published by Registry.Subscribe consumers. One event is emitted per status change.

Hosts wire the audit-log on top of this stream — see docs/compliance/audit-event-catalog.md "Mode lifecycle" for the canonical event-name mapping.

Jump to

Keyboard shortcuts

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