Documentation
¶
Overview ¶
Package persona provides the Voice Agent persona / role / sequence catalog for the Server-Target. A Persona is a long-lived customer-facing identity that binds a voice, default role, and metadata. A Role is a behavioural policy (system prompt, thinking, VAD) that can be assigned to any Persona. A Sequence is an optional multi-step workflow the Voice Agent follows in a given session.
Storage follows the TOML-seed + optional-store-override pattern documented in the server plan: deploy/config/server.example.toml carries immutable defaults committed to the repo, and DB entries (M5b, pending) override by ID at runtime.
Index ¶
- Variables
- func LoadSeeds(reg *Registry, cfg *config.Config) []string
- type Handler
- type HandlerOptions
- type Persister
- type Persona
- type PostgresPersister
- func (p *PostgresPersister) DeletePersona(ctx context.Context, id string) error
- func (p *PostgresPersister) DeleteRole(ctx context.Context, id string) error
- func (p *PostgresPersister) DeleteSequence(ctx context.Context, id string) error
- func (p *PostgresPersister) LoadPersonas(ctx context.Context) ([]Persona, error)
- func (p *PostgresPersister) LoadRoles(ctx context.Context) ([]Role, error)
- func (p *PostgresPersister) LoadSequences(ctx context.Context) ([]Sequence, error)
- func (p *PostgresPersister) SavePersona(ctx context.Context, entity Persona) error
- func (p *PostgresPersister) SaveRole(ctx context.Context, r Role) error
- func (p *PostgresPersister) SaveSequence(ctx context.Context, s Sequence) error
- type Registry
- func (r *Registry) DeletePersona(id string) error
- func (r *Registry) DeleteRole(id string) error
- func (r *Registry) DeleteSequence(id string) error
- func (r *Registry) GetPersona(id string) (Persona, error)
- func (r *Registry) GetRole(id string) (Role, error)
- func (r *Registry) GetSequence(id string) (Sequence, error)
- func (r *Registry) HydrateFrom(ctx context.Context, p Persister) error
- func (r *Registry) ListPersonas() []Persona
- func (r *Registry) ListRoles() []Role
- func (r *Registry) ListSequences() []Sequence
- func (r *Registry) Resolve(personaID, roleID, sequenceID string, stepIndex int) (ResolvedSession, error)
- func (r *Registry) UpsertPersona(p Persona) (Persona, error)
- func (r *Registry) UpsertRole(role Role) (Role, error)
- func (r *Registry) UpsertSequence(seq Sequence) (Sequence, error)
- func (r *Registry) WithClock(fn func() time.Time) *Registry
- func (r *Registry) WithPersister(p Persister) *Registry
- type ResolvedSession
- type Role
- type SQLitePersister
- func (p *SQLitePersister) DeletePersona(ctx context.Context, id string) error
- func (p *SQLitePersister) DeleteRole(ctx context.Context, id string) error
- func (p *SQLitePersister) DeleteSequence(ctx context.Context, id string) error
- func (p *SQLitePersister) LoadPersonas(ctx context.Context) ([]Persona, error)
- func (p *SQLitePersister) LoadRoles(ctx context.Context) ([]Role, error)
- func (p *SQLitePersister) LoadSequences(ctx context.Context) ([]Sequence, error)
- func (p *SQLitePersister) SavePersona(ctx context.Context, entity Persona) error
- func (p *SQLitePersister) SaveRole(ctx context.Context, r Role) error
- func (p *SQLitePersister) SaveSequence(ctx context.Context, s Sequence) error
- type Sequence
- type SequenceStep
Constants ¶
This section is empty.
Variables ¶
var ( ErrPersonaNotFound = errors.New("persona: persona not found") ErrRoleNotFound = errors.New("persona: role not found") ErrSequenceNotFound = errors.New("persona: sequence not found") ErrAlreadyExists = errors.New("persona: entity with this id already exists") ErrStepNotFound = errors.New("persona: sequence step not found") // ErrPersist wraps any error returned by the durable Persister so // callers can distinguish validation failures (400) from // persistence failures (500). Use errors.Is(err, ErrPersist). ErrPersist = errors.New("persona: persist failed") )
Errors returned by the registry. Exposed so handlers can map to HTTP statuses cleanly.
Functions ¶
func LoadSeeds ¶
LoadSeeds populates the registry from the TOML [[personas]], [[roles]], and [[sequences]] arrays in the server config. Seeds are always tagged Source="toml" so admin overrides (M5b, via store writes) can be distinguished via the Source field.
Returns human-readable notes suitable for startup logging. Errors are reported per-entry; one invalid seed never blocks the others.
Types ¶
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler exposes CRUD endpoints for personas, roles, and sequences. Read endpoints are open to any authenticated caller; write endpoints require middleware.Identity.Role == "admin".
Storage is the in-memory Registry; when a Persister is attached (M5b SQLite, optional Postgres later), writes round-trip through it before committing to memory and persistence failures are propagated to the HTTP response (T22 — caller sees 500, not silent 200).
func New ¶
func New(opts HandlerOptions) (*Handler, error)
New constructs a Handler. Registry is required.
type HandlerOptions ¶
HandlerOptions configures a Handler.
type Persister ¶
type Persister interface {
SavePersona(ctx context.Context, p Persona) error
DeletePersona(ctx context.Context, id string) error
LoadPersonas(ctx context.Context) ([]Persona, error)
SaveRole(ctx context.Context, r Role) error
DeleteRole(ctx context.Context, id string) error
LoadRoles(ctx context.Context) ([]Role, error)
SaveSequence(ctx context.Context, s Sequence) error
DeleteSequence(ctx context.Context, id string) error
LoadSequences(ctx context.Context) ([]Sequence, error)
}
Persister is the optional durability contract the Registry uses to persist admin-authored changes. Production deployments satisfy this via the SQLite-backed store (internal/server/persona/sqlite_persister.go); unit tests can supply an in-memory fake.
The interface is deliberately coarse — one Save/Delete per entity — because the Registry itself is the source of truth for validation and ordering. The Persister only has to round-trip bytes.
type Persona ¶
type Persona struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
Description string `json:"description,omitempty"`
Voice string `json:"voice,omitempty"`
Locale string `json:"locale,omitempty"`
DefaultRole string `json:"default_role,omitempty"`
DefaultSequence string `json:"default_sequence,omitempty"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Source records where the entry came from ("toml" or "store"). Clients
// can use this to distinguish deploy-time defaults from admin-authored
// overrides.
Source string `json:"source,omitempty"`
}
Persona is a long-lived Voice Agent identity bound to a voice and a default role. The ID is stable and safe to reference from clients (e.g. kombify-AI selects a persona_id when creating a Voice Agent session).
type PostgresPersister ¶ added in v0.29.0
type PostgresPersister struct {
// contains filtered or unexported fields
}
PostgresPersister implements Persister against the Postgres store schema.
func NewPostgresPersister ¶ added in v0.29.0
func NewPostgresPersister(db *sql.DB) *PostgresPersister
NewPostgresPersister wraps an already-open *sql.DB. The caller owns the connection lifecycle; the persister never closes it.
func (*PostgresPersister) DeletePersona ¶ added in v0.29.0
func (p *PostgresPersister) DeletePersona(ctx context.Context, id string) error
func (*PostgresPersister) DeleteRole ¶ added in v0.29.0
func (p *PostgresPersister) DeleteRole(ctx context.Context, id string) error
func (*PostgresPersister) DeleteSequence ¶ added in v0.29.0
func (p *PostgresPersister) DeleteSequence(ctx context.Context, id string) error
func (*PostgresPersister) LoadPersonas ¶ added in v0.29.0
func (p *PostgresPersister) LoadPersonas(ctx context.Context) ([]Persona, error)
func (*PostgresPersister) LoadRoles ¶ added in v0.29.0
func (p *PostgresPersister) LoadRoles(ctx context.Context) ([]Role, error)
func (*PostgresPersister) LoadSequences ¶ added in v0.29.0
func (p *PostgresPersister) LoadSequences(ctx context.Context) ([]Sequence, error)
func (*PostgresPersister) SavePersona ¶ added in v0.29.0
func (p *PostgresPersister) SavePersona(ctx context.Context, entity Persona) error
func (*PostgresPersister) SaveRole ¶ added in v0.29.0
func (p *PostgresPersister) SaveRole(ctx context.Context, r Role) error
func (*PostgresPersister) SaveSequence ¶ added in v0.29.0
func (p *PostgresPersister) SaveSequence(ctx context.Context, s Sequence) error
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the in-memory catalog of personas, roles, and sequences. It is safe for concurrent reads + writes (RWMutex). When a Persister is attached via WithPersister, admin-authored writes round-trip through the persistence layer (M5b); TOML seeds remain memory-only.
func (*Registry) DeletePersona ¶
DeletePersona removes the entry. Returns ErrPersonaNotFound when missing, or a persistence error when the durable delete fails (the in-memory entry stays in place in that case so the registry is consistent with storage).
func (*Registry) DeleteRole ¶
func (*Registry) DeleteSequence ¶
func (*Registry) GetPersona ¶
GetPersona returns a copy by ID.
func (*Registry) HydrateFrom ¶
HydrateFrom populates the registry with entries previously persisted by the given Persister. Entries are tagged Source="store" so they can be distinguished from TOML seeds in the API output.
This is called from the bootstrap BEFORE TOML seeds are loaded, then the seeds overlay the stored entries — TOML seeds act as defaults that a persisted override can replace. If you want TOML to win, invert the order in bootstrap.
func (*Registry) ListPersonas ¶
ListPersonas returns copies sorted by ID. Empty slice when the registry is empty — never nil.
func (*Registry) ListSequences ¶
func (*Registry) Resolve ¶
func (r *Registry) Resolve(personaID, roleID, sequenceID string, stepIndex int) (ResolvedSession, error)
Resolve composes a ResolvedSession from the requested persona / role / sequence IDs. Any empty ID falls back to the persona's default role and a nil sequence respectively. Missing IDs return a typed error so the WebSocket adapter can surface a clear error code to clients.
The caller (voiceagent adapter) supplies `stepIndex` — which step of the sequence to project into CurrentStep. 0 means the first step.
func (*Registry) UpsertPersona ¶
UpsertPersona inserts or replaces a persona. Returns a copy so callers can't mutate internal state. When a Persister is attached, the change is persisted FIRST and only committed to memory after success — a persist error means the in-memory state is unchanged and the caller sees a concrete error. Source="toml" entries skip the persister entirely so repo-committed seeds never round-trip to the store.
func (*Registry) WithClock ¶
WithClock overrides the time source used for CreatedAt/UpdatedAt. Tests use a mutable clock so they can assert exact timestamps.
func (*Registry) WithPersister ¶
WithPersister attaches a Persister to the Registry. Calls to UpsertPersona / UpsertRole / UpsertSequence (and their Delete siblings) will now round-trip through the persister after the in-memory mutation succeeds. Hydration is explicit — call HydrateFrom once at startup.
type ResolvedSession ¶
type ResolvedSession struct {
PersonaID string
RoleID string
SequenceID string
SequenceCompletion string
SequenceMaxTurns int
Voice string
Locale string
Model string
SystemPrompt string
RefinementPrompt string
Thinking string
AutomaticVAD bool
StartSensitivity string
EndSensitivity string
PrefixPaddingMs int32
SilenceDurationMs int32
ActivityHandling string
TurnCoverage string
CurrentStep *SequenceStep
StepID string
StepIndex int
StepCount int
StepInstruction string
StepExitCriteria string
StepMaxTurns int
}
ResolvedSession captures the fully composed config the Voice Agent WebSocket adapter hands to the live provider. It mirrors internal/server/voiceagent.LiveConfigFrame but stays in this package so the persona layer has no dependency on the voiceagent adapter.
type Role ¶
type Role struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
SystemPrompt string `json:"system_prompt"`
RefinementPrompt string `json:"refinement_prompt,omitempty"`
Locale string `json:"locale,omitempty"`
VocabularyHint string `json:"vocabulary_hint,omitempty"`
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
ThinkingEnabled bool `json:"thinking_enabled,omitempty"`
ThinkingLevel string `json:"thinking_level,omitempty"`
IncludeThoughts bool `json:"include_thoughts,omitempty"`
ThinkingBudget int `json:"thinking_budget,omitempty"`
AutomaticActivityDetection bool `json:"automatic_activity_detection,omitempty"`
VADStartSensitivity string `json:"vad_start_sensitivity,omitempty"`
VADEndSensitivity string `json:"vad_end_sensitivity,omitempty"`
VADPrefixPaddingMs int `json:"vad_prefix_padding_ms,omitempty"`
VADSilenceDurationMs int `json:"vad_silence_duration_ms,omitempty"`
ActivityHandling string `json:"activity_handling,omitempty"`
TurnCoverage string `json:"turn_coverage,omitempty"`
ContextCompressionEnabled bool `json:"context_compression_enabled,omitempty"`
ContextCompressionTriggerTk int64 `json:"context_compression_trigger_tokens,omitempty"`
ContextCompressionTargetTk int64 `json:"context_compression_target_tokens,omitempty"`
EnableAffectiveDialog bool `json:"enable_affective_dialog,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Source string `json:"source,omitempty"`
}
Role is a behavioural policy that can be assigned to any Persona. It owns the system prompt, thinking policy, and activity-detection defaults — the knobs that actually shape live model behaviour.
type SQLitePersister ¶
type SQLitePersister struct {
// contains filtered or unexported fields
}
SQLitePersister implements Persister against a *sql.DB whose schema was created by migration 007_personas.sql. Works for both the Framework's SQLite backend (modernc.org/sqlite) and, by extension, any SQL driver that speaks the same dialect (SQLite file paths, :memory:, etc.).
Postgres support requires a sibling file with pg-dialect placeholders ($1,$2,...) and would replace `ON CONFLICT(id) DO UPDATE SET ...` with the same shape — the schema in 007_personas.sql is already Postgres- compatible.
func NewSQLitePersister ¶
func NewSQLitePersister(db *sql.DB) *SQLitePersister
NewSQLitePersister wraps an already-open *sql.DB. The caller owns the connection lifecycle; the persister never closes it.
func (*SQLitePersister) DeletePersona ¶
func (p *SQLitePersister) DeletePersona(ctx context.Context, id string) error
func (*SQLitePersister) DeleteRole ¶
func (p *SQLitePersister) DeleteRole(ctx context.Context, id string) error
func (*SQLitePersister) DeleteSequence ¶
func (p *SQLitePersister) DeleteSequence(ctx context.Context, id string) error
func (*SQLitePersister) LoadPersonas ¶
func (p *SQLitePersister) LoadPersonas(ctx context.Context) ([]Persona, error)
func (*SQLitePersister) LoadRoles ¶
func (p *SQLitePersister) LoadRoles(ctx context.Context) ([]Role, error)
func (*SQLitePersister) LoadSequences ¶
func (p *SQLitePersister) LoadSequences(ctx context.Context) ([]Sequence, error)
func (*SQLitePersister) SavePersona ¶
func (p *SQLitePersister) SavePersona(ctx context.Context, entity Persona) error
func (*SQLitePersister) SaveRole ¶
func (p *SQLitePersister) SaveRole(ctx context.Context, r Role) error
func (*SQLitePersister) SaveSequence ¶
func (p *SQLitePersister) SaveSequence(ctx context.Context, s Sequence) error
type Sequence ¶
type Sequence struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
Description string `json:"description,omitempty"`
// Completion strategy: "all_steps" | "explicit_close" | "max_turns".
Completion string `json:"completion,omitempty"`
MaxTurns int `json:"max_turns,omitempty"`
Steps []SequenceStep `json:"steps"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Source string `json:"source,omitempty"`
}
Sequence is an optional multi-step workflow the Voice Agent follows in a session. Steps are ordered; advancing between them is driven by explicit client frames in v1 (M4) and by LLM-judged exit criteria in v1.1 (M5b+).
type SequenceStep ¶
type SequenceStep struct {
ID string `json:"id"`
Instruction string `json:"instruction"`
ExitCriteria string `json:"exit_criteria,omitempty"`
RequireTools []string `json:"require_tools,omitempty"`
MaxTurns int `json:"max_turns,omitempty"`
}
SequenceStep is a single step in a Sequence.