persona

package
v0.28.2 Latest Latest
Warning

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

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

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

Constants

This section is empty.

Variables

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

func LoadSeeds(reg *Registry, cfg *config.Config) []string

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.

func (*Handler) Mount

func (h *Handler) Mount(mux *http.ServeMux)

Mount wires all persona/role/sequence endpoints onto mux.

type HandlerOptions

type HandlerOptions struct {
	Registry    *Registry
	AllowWrites bool
}

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 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 NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) DeletePersona

func (r *Registry) DeletePersona(id string) error

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 (r *Registry) DeleteRole(id string) error

func (*Registry) DeleteSequence

func (r *Registry) DeleteSequence(id string) error

func (*Registry) GetPersona

func (r *Registry) GetPersona(id string) (Persona, error)

GetPersona returns a copy by ID.

func (*Registry) GetRole

func (r *Registry) GetRole(id string) (Role, error)

func (*Registry) GetSequence

func (r *Registry) GetSequence(id string) (Sequence, error)

func (*Registry) HydrateFrom

func (r *Registry) HydrateFrom(ctx context.Context, p Persister) error

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

func (r *Registry) ListPersonas() []Persona

ListPersonas returns copies sorted by ID. Empty slice when the registry is empty — never nil.

func (*Registry) ListRoles

func (r *Registry) ListRoles() []Role

func (*Registry) ListSequences

func (r *Registry) ListSequences() []Sequence

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

func (r *Registry) UpsertPersona(p Persona) (Persona, error)

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

func (r *Registry) UpsertRole(role Role) (Role, error)

func (*Registry) UpsertSequence

func (r *Registry) UpsertSequence(seq Sequence) (Sequence, error)

func (*Registry) WithClock

func (r *Registry) WithClock(fn func() time.Time) *Registry

WithClock overrides the time source used for CreatedAt/UpdatedAt. Tests use a mutable clock so they can assert exact timestamps.

func (*Registry) WithPersister

func (r *Registry) WithPersister(p Persister) *Registry

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.

Jump to

Keyboard shortcuts

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