Skip to main content

lib/jido_character.ex

defmodule Jido.Character do
  @moduledoc """
  Extensible character definition for AI agents.

  ## Direct API

      {:ok, bob} = Jido.Character.new(%{name: "Bob"})
      {:ok, bob} = Jido.Character.update(bob, %{identity: %{role: "Assistant"}})
      {:ok, bob} = Jido.Character.validate(bob)
      context = Jido.Character.to_context(bob)
      prompt = Jido.Character.to_system_prompt(bob)

  ## Raising Variants

      bob = Jido.Character.new!(%{name: "Bob"})
      bob = Jido.Character.update!(bob, %{identity: %{role: "Assistant"}})

  ## Module-Based Characters

      defmodule MyApp.Characters.Alice do
        use Jido.Character,
          defaults: %{name: "Alice"}
      end

      {:ok, alice} = MyApp.Characters.Alice.new()

  ## Functions

  - `new/1`, `new!/1` - Create a new character from a map of attributes
  - `update/2`, `update!/2` - Update an existing character with new attributes
  - `validate/1` - Validate a character map
  - `to_context/2` - Convert a character to a ReqLLM.Context struct
  - `to_system_prompt/2` - Generate a system prompt string from a character
  - `evolve/2`, `evolve!/2` - Evolve a character over simulated time

  ## Error Handling

  Functions returning `{:ok, t()} | {:error, errors()}` use Zoi validation errors.
  The `errors()` type is a list of `Zoi.Error.t()` structs with detailed information:

      case Jido.Character.new(%{}) do
        {:ok, char} -> char
        {:error, errors} ->
          Enum.each(errors, fn error ->
            IO.puts("Field \#{inspect(error.path)}: \#{error.message}")
          end)
      end

  Use the `!` variants (`new!/1`, `update!/2`) when you prefer exceptions over tuples.
  """

  @typedoc """
  A character map with the following fields:

  - `:id` (required) - Unique identifier (String.t())
  - `:name` - Character name (String.t() | nil)
  - `:description` - Character description (String.t() | nil)
  - `:identity` - Who the character is (Schema.Identity.t() | nil)
  - `:personality` - How the character behaves (Schema.Personality.t() | nil)
  - `:voice` - How the character communicates (Schema.Voice.t() | nil)
  - `:memory` - What the character remembers (Schema.Memory.t() | nil)
  - `:knowledge` - Permanent facts the character knows ([Schema.KnowledgeItem.t()])
  - `:instructions` - Behavioral instructions ([String.t()])
  - `:extensions` - Custom extension data (map())
  - `:created_at` - Creation timestamp (DateTime.t() | nil)
  - `:updated_at` - Last update timestamp (DateTime.t() | nil)
  - `:version` - Version number (non_neg_integer())
  """
  @type t :: %{
          required(:id) => String.t(),
          optional(:name) => String.t() | nil,
          optional(:description) => String.t() | nil,
          optional(:identity) => Jido.Character.Schema.Identity.t() | nil,
          optional(:personality) => Jido.Character.Schema.Personality.t() | nil,
          optional(:voice) => Jido.Character.Schema.Voice.t() | nil,
          optional(:memory) => Jido.Character.Schema.Memory.t() | nil,
          optional(:knowledge) => [Jido.Character.Schema.KnowledgeItem.t()],
          optional(:instructions) => [String.t()],
          optional(:extensions) => map(),
          optional(:created_at) => DateTime.t() | nil,
          optional(:updated_at) => DateTime.t() | nil,
          optional(:version) => non_neg_integer()
        }

  @type errors :: [Zoi.Error.t()]

  alias Jido.Character.Schema
  alias Jido.Character.Renderer

  @spec validate(map()) :: {:ok, t()} | {:error, errors()}
  def validate(attrs) when is_map(attrs) do
    schema = Schema.character()

    case Zoi.parse(schema, attrs) do
      {:ok, parsed} -> {:ok, parsed}
      {:error, errors} -> {:error, errors}
    end
  end

  @spec new(map()) :: {:ok, t()} | {:error, errors()}
  def new(attrs \\ %{})

  def new(attrs) when is_map(attrs) do
    attrs
    |> normalize_keys()
    |> ensure_id()
    |> ensure_timestamps(:create)
    |> ensure_version()
    |> validate()
  end

  @doc """
  Creates a new character, raising on validation errors.

  ## Examples

      bob = Jido.Character.new!(%{name: "Bob"})

  Raises `ArgumentError` if validation fails.
  """
  @spec new!(map()) :: t()
  def new!(attrs \\ %{}) do
    case new(attrs) do
      {:ok, character} -> character
      {:error, errors} -> raise ArgumentError, format_errors("Invalid character", errors)
    end
  end

  defp normalize_keys(attrs) do
    Map.new(attrs, fn
      {k, v} when is_binary(k) ->
        atom_key = safe_to_existing_atom(k)
        {atom_key || k, v}

      {k, v} ->
        {k, v}
    end)
  end

  defp safe_to_existing_atom(str) do
    String.to_existing_atom(str)
  rescue
    ArgumentError -> nil
  end

  defp ensure_id(attrs) do
    case Map.get(attrs, :id) do
      nil -> Map.put(attrs, :id, Uniq.UUID.uuid7())
      _id -> attrs
    end
  end

  defp ensure_timestamps(attrs, :create) do
    now = DateTime.utc_now()

    attrs
    |> Map.put_new(:created_at, now)
    |> Map.put(:updated_at, now)
  end

  defp ensure_timestamps(attrs, :update) do
    Map.put(attrs, :updated_at, DateTime.utc_now())
  end

  defp ensure_version(attrs) do
    Map.put_new(attrs, :version, 1)
  end

  @spec update(t(), map()) :: {:ok, t()} | {:error, errors()}
  def update(%{} = character, %{} = attrs) do
    attrs = normalize_keys(attrs)

    base = %{
      id: character.id,
      created_at: Map.get(character, :created_at)
    }

    merged =
      character
      |> deep_merge(attrs)
      |> Map.merge(base)

    merged =
      merged
      |> bump_version(character)
      |> ensure_timestamps(:update)

    validate(merged)
  end

  @doc """
  Updates a character, raising on validation errors.

  ## Examples

      bob = Jido.Character.update!(bob, %{name: "Robert"})

  Raises `ArgumentError` if validation fails.
  """
  @spec update!(t(), map()) :: t()
  def update!(character, attrs) do
    case update(character, attrs) do
      {:ok, updated} -> updated
      {:error, errors} -> raise ArgumentError, format_errors("Invalid update", errors)
    end
  end

  defp bump_version(attrs, %{version: version}) when is_integer(version) do
    Map.put(attrs, :version, version + 1)
  end

  defp bump_version(attrs, _), do: Map.put_new(attrs, :version, 1)

  defp deep_merge(map1, map2) when is_map(map1) and is_map(map2) do
    Map.merge(map1, map2, fn _k, v1, v2 ->
      cond do
        is_struct(v1) and is_map(v2) and not is_struct(v2) ->
          struct(v1.__struct__, deep_merge(Map.from_struct(v1), v2))

        is_map(v1) and is_map(v2) and not is_struct(v1) and not is_struct(v2) ->
          deep_merge(v1, v2)

        true ->
          v2
      end
    end)
  end

  defp format_errors(prefix, errors) when is_list(errors) do
    details =
      errors
      |> Enum.map(fn
        %{path: path, message: msg} when path != [] ->
          "#{Enum.join(path, ".")}: #{msg}"

        %{message: msg} ->
          msg

        other ->
          inspect(other)
      end)
      |> Enum.join(", ")

    "#{prefix}: #{details}"
  end

  @spec to_context(t(), keyword()) :: ReqLLM.Context.t()
  def to_context(character, opts \\ [])
  def to_context(%{} = character, opts), do: Renderer.to_context(character, opts)

  @spec to_system_prompt(t(), keyword()) :: String.t()
  def to_system_prompt(character, opts \\ [])
  def to_system_prompt(%{} = character, opts), do: Renderer.to_system_prompt(character, opts)

  # ---------------------------------------------------------------------------
  # Pipe-Friendly Helpers
  # ---------------------------------------------------------------------------

  @doc """
  Add knowledge to a character.

  ## Examples

      # String shorthand
      {:ok, char} = Jido.Character.add_knowledge(char, "Expert in Elixir")

      # With options
      {:ok, char} = Jido.Character.add_knowledge(char, "Expert in Elixir", category: "skills", importance: 0.9)

      # Multiple items
      {:ok, char} = Jido.Character.add_knowledge(char, ["Knows Elixir", "Knows Python"])

      # Full map
      {:ok, char} = Jido.Character.add_knowledge(char, %{content: "Expert in Elixir", importance: 0.9})
  """
  @spec add_knowledge(t(), String.t() | map() | [String.t() | map()], keyword()) ::
          {:ok, t()} | {:error, errors()}
  def add_knowledge(character, content, opts \\ [])

  def add_knowledge(character, content, opts) when is_binary(content) do
    item = build_knowledge_item(content, opts)
    append_to_list(character, :knowledge, [item])
  end

  def add_knowledge(character, %{} = item, _opts) do
    append_to_list(character, :knowledge, [item])
  end

  def add_knowledge(character, items, opts) when is_list(items) do
    normalized =
      Enum.map(items, fn
        content when is_binary(content) -> build_knowledge_item(content, opts)
        %{} = item -> item
      end)

    append_to_list(character, :knowledge, normalized)
  end

  defp build_knowledge_item(content, opts) do
    %{content: content}
    |> maybe_put(:category, opts[:category])
    |> maybe_put(:importance, opts[:importance])
  end

  @doc """
  Add an instruction to a character.

  ## Examples

      {:ok, char} = Jido.Character.add_instruction(char, "Always be helpful")

      # Multiple
      {:ok, char} = Jido.Character.add_instruction(char, ["Be helpful", "Be concise"])
  """
  @spec add_instruction(t(), String.t() | [String.t()]) :: {:ok, t()} | {:error, errors()}
  def add_instruction(character, instruction) when is_binary(instruction) do
    append_to_list(character, :instructions, [instruction])
  end

  def add_instruction(character, instructions) when is_list(instructions) do
    append_to_list(character, :instructions, instructions)
  end

  @doc """
  Add a memory entry to a character.

  Memory entries are subject to the character's memory capacity. When adding
  an entry would exceed capacity, the oldest entries are dropped to make room.

  ## Examples

      # String shorthand
      {:ok, char} = Jido.Character.add_memory(char, "User prefers brief answers")

      # With options
      {:ok, char} = Jido.Character.add_memory(char, "Important event", importance: 0.9, category: "events")

      # Full map
      {:ok, char} = Jido.Character.add_memory(char, %{content: "User said hello", importance: 0.5})
  """
  @spec add_memory(t(), String.t() | map(), keyword()) :: {:ok, t()} | {:error, errors()}
  def add_memory(character, content, opts \\ [])

  def add_memory(character, content, opts) when is_binary(content) do
    entry = build_memory_entry(content, opts)
    append_memory_entry(character, entry)
  end

  def add_memory(character, %{} = entry, _opts) do
    append_memory_entry(character, entry)
  end

  defp build_memory_entry(content, opts) do
    %{content: content, timestamp: DateTime.utc_now()}
    |> maybe_put(:importance, opts[:importance])
    |> maybe_put(:decay_rate, opts[:decay_rate])
    |> maybe_put(:category, opts[:category])
  end

  defp append_memory_entry(character, entry) do
    memory = Map.get(character, :memory, %{entries: [], capacity: 100})
    entries = Map.get(memory, :entries, [])
    capacity = Map.get(memory, :capacity, 100)

    new_entries = entries ++ [entry]

    trimmed_entries =
      if length(new_entries) > capacity do
        Enum.drop(new_entries, length(new_entries) - capacity)
      else
        new_entries
      end

    updated_memory = Map.put(memory, :entries, trimmed_entries)
    update(character, %{memory: updated_memory})
  end

  @doc """
  Add a trait to a character's personality.

  ## Examples

      # String shorthand
      {:ok, char} = Jido.Character.add_trait(char, "curious")

      # With intensity
      {:ok, char} = Jido.Character.add_trait(char, "analytical", intensity: 0.9)

      # Multiple
      {:ok, char} = Jido.Character.add_trait(char, ["curious", "patient"])
  """
  @spec add_trait(t(), String.t() | map() | [String.t() | map()], keyword()) ::
          {:ok, t()} | {:error, errors()}
  def add_trait(character, trait, opts \\ [])

  def add_trait(character, trait, opts) when is_binary(trait) do
    item = if intensity = opts[:intensity], do: %{name: trait, intensity: intensity}, else: trait
    append_personality_field(character, :traits, [item])
  end

  def add_trait(character, %{} = trait, _opts) do
    append_personality_field(character, :traits, [trait])
  end

  def add_trait(character, traits, opts) when is_list(traits) do
    normalized =
      Enum.map(traits, fn
        trait when is_binary(trait) ->
          if intensity = opts[:intensity], do: %{name: trait, intensity: intensity}, else: trait

        %{} = trait ->
          trait
      end)

    append_personality_field(character, :traits, normalized)
  end

  @doc """
  Add a value to a character's personality.

  ## Examples

      {:ok, char} = Jido.Character.add_value(char, "accuracy")
      {:ok, char} = Jido.Character.add_value(char, ["accuracy", "clarity"])
  """
  @spec add_value(t(), String.t() | [String.t()]) :: {:ok, t()} | {:error, errors()}
  def add_value(character, value) when is_binary(value) do
    append_personality_field(character, :values, [value])
  end

  def add_value(character, values) when is_list(values) do
    append_personality_field(character, :values, values)
  end

  @doc """
  Add a quirk to a character's personality.

  ## Examples

      {:ok, char} = Jido.Character.add_quirk(char, "Uses analogies frequently")
      {:ok, char} = Jido.Character.add_quirk(char, ["Uses analogies", "Asks clarifying questions"])
  """
  @spec add_quirk(t(), String.t() | [String.t()]) :: {:ok, t()} | {:error, errors()}
  def add_quirk(character, quirk) when is_binary(quirk) do
    append_personality_field(character, :quirks, [quirk])
  end

  def add_quirk(character, quirks) when is_list(quirks) do
    append_personality_field(character, :quirks, quirks)
  end

  @doc """
  Add an expression to a character's voice.

  ## Examples

      {:ok, char} = Jido.Character.add_expression(char, "Let me think about that...")
      {:ok, char} = Jido.Character.add_expression(char, ["Let me think...", "Interesting point!"])
  """
  @spec add_expression(t(), String.t() | [String.t()]) :: {:ok, t()} | {:error, errors()}
  def add_expression(character, expression) when is_binary(expression) do
    append_voice_field(character, :expressions, [expression])
  end

  def add_expression(character, expressions) when is_list(expressions) do
    append_voice_field(character, :expressions, expressions)
  end

  @doc """
  Add a fact to a character's identity.

  ## Examples

      {:ok, char} = Jido.Character.add_fact(char, "Has a PhD in Computer Science")
      {:ok, char} = Jido.Character.add_fact(char, ["Has a PhD", "Worked at 3 startups"])
  """
  @spec add_fact(t(), String.t() | [String.t()]) :: {:ok, t()} | {:error, errors()}
  def add_fact(character, fact) when is_binary(fact) do
    append_identity_field(character, :facts, [fact])
  end

  def add_fact(character, facts) when is_list(facts) do
    append_identity_field(character, :facts, facts)
  end

  # ---------------------------------------------------------------------------
  # Private Helpers for Appending
  # ---------------------------------------------------------------------------

  defp maybe_put(map, _key, nil), do: map
  defp maybe_put(map, key, value), do: Map.put(map, key, value)

  defp append_to_list(character, field, items) do
    existing = Map.get(character, field, [])
    update(character, %{field => existing ++ items})
  end

  defp append_personality_field(character, field, items) do
    personality = Map.get(character, :personality, %{})
    existing = Map.get(personality, field, [])
    updated_personality = Map.put(personality, field, existing ++ items)
    update(character, %{personality: updated_personality})
  end

  defp append_voice_field(character, field, items) do
    voice = Map.get(character, :voice, %{})
    existing = Map.get(voice, field, [])
    updated_voice = Map.put(voice, field, existing ++ items)
    update(character, %{voice: updated_voice})
  end

  defp append_identity_field(character, field, items) do
    identity = Map.get(character, :identity, %{})
    existing = Map.get(identity, field, [])
    updated_identity = Map.put(identity, field, existing ++ items)
    update(character, %{identity: updated_identity})
  end

  # ---------------------------------------------------------------------------
  # Evolution
  # ---------------------------------------------------------------------------

  @doc """
  Evolve a character over a period of simulated time.

  Time is relative: pass the delta since the last evolution call.

  ## Options

    * `:days` - number of days to advance (default: 0)
    * `:years` - number of years to advance (default: 0)
    * `:age?` - whether to update integer `identity.age` (default: true)
    * `:memory?` - whether to apply memory decay (default: true)
    * `:memory_prune_below` - drop memories whose decayed importance
        falls below this threshold (default: 0.05, set to nil to disable)

  ## Examples

      # Age by one year
      {:ok, older} = Jido.Character.evolve(char, years: 1)

      # Age by 30 days, decay memories
      {:ok, evolved} = Jido.Character.evolve(char, days: 30)

      # Only decay memories, don't age
      {:ok, evolved} = Jido.Character.evolve(char, days: 7, age?: false)

      # Disable memory pruning
      {:ok, evolved} = Jido.Character.evolve(char, days: 30, memory_prune_below: nil)
  """
  @spec evolve(t(), keyword()) :: {:ok, t()} | {:error, errors()}
  def evolve(%{} = character, opts \\ []) do
    days = opts[:days] || 0
    years = opts[:years] || 0
    total_days = days + years * 365

    if total_days <= 0 do
      {:ok, character}
    else
      age? = Keyword.get(opts, :age?, true)
      mem? = Keyword.get(opts, :memory?, true)

      attrs =
        %{}
        |> maybe_put_evolved_age(character, total_days, age?)
        |> maybe_put_evolved_memory(character, total_days, mem?, opts)

      if map_size(attrs) == 0 do
        {:ok, character}
      else
        update(character, attrs)
      end
    end
  end

  @doc """
  Evolve a character, raising on validation errors.

  ## Examples

      older = Jido.Character.evolve!(char, years: 1)

  Raises `ArgumentError` if validation fails.
  """
  @spec evolve!(t(), keyword()) :: t()
  def evolve!(character, opts \\ []) do
    case evolve(character, opts) do
      {:ok, evolved} -> evolved
      {:error, errors} -> raise ArgumentError, format_errors("Evolution failed", errors)
    end
  end

  defp maybe_put_evolved_age(attrs, _character, _total_days, false), do: attrs

  defp maybe_put_evolved_age(attrs, character, total_days, true) do
    identity = Map.get(character, :identity, %{})

    case Map.get(identity, :age) do
      age when is_integer(age) ->
        delta_years = trunc(Float.floor(total_days / 365))

        if delta_years > 0 do
          new_identity = Map.put(identity, :age, age + delta_years)
          Map.put(attrs, :identity, new_identity)
        else
          attrs
        end

      _string_or_nil ->
        attrs
    end
  end

  defp maybe_put_evolved_memory(attrs, _character, _total_days, false, _opts), do: attrs

  defp maybe_put_evolved_memory(attrs, character, total_days, true, opts) do
    memory = Map.get(character, :memory, %{entries: [], capacity: 100})
    entries = Map.get(memory, :entries, [])

    if entries == [] do
      attrs
    else
      min_imp = Keyword.get(opts, :memory_prune_below, 0.05)

      evolved_entries =
        entries
        |> Enum.map(&decay_entry(&1, total_days))
        |> maybe_prune_entries(min_imp)

      if evolved_entries == entries do
        attrs
      else
        new_memory = Map.put(memory, :entries, evolved_entries)
        Map.put(attrs, :memory, new_memory)
      end
    end
  end

  defp decay_entry(entry, total_days) do
    importance = Map.get(entry, :importance, 0.5)
    decay_rate = Map.get(entry, :decay_rate, 0.1)

    effective = importance * :math.pow(1.0 - decay_rate, total_days)
    Map.put(entry, :importance, effective)
  end

  defp maybe_prune_entries(entries, nil), do: entries

  defp maybe_prune_entries(entries, min_importance) do
    Enum.filter(entries, fn entry -> Map.get(entry, :importance, 0.5) >= min_importance end)
  end

  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do
      alias Jido.Character
      alias Jido.Character.Definition

      @jido_character_definition %Definition{
        module: __MODULE__,
        extensions: Keyword.get(opts, :extensions, []),
        defaults: Keyword.get(opts, :defaults, %{}),
        adapter: Keyword.get(opts, :adapter, Jido.Character.Persistence.Memory),
        adapter_opts: Keyword.get(opts, :adapter_opts, []),
        renderer: Keyword.get(opts, :renderer, Jido.Character.Context.Renderer),
        renderer_opts: Keyword.get(opts, :renderer_opts, [])
      }

      @doc "Return this character module's definition."
      @spec definition() :: Definition.t()
      def definition, do: @jido_character_definition

      @doc "Return configured extensions."
      @spec extensions() :: [atom()]
      def extensions, do: @jido_character_definition.extensions

      @doc "Return default attributes."
      @spec defaults() :: map()
      def defaults, do: @jido_character_definition.defaults

      @doc "Return configured persistence adapter."
      @spec adapter() :: module()
      def adapter, do: @jido_character_definition.adapter

      @doc "Return configured adapter options."
      @spec adapter_opts() :: keyword()
      def adapter_opts, do: @jido_character_definition.adapter_opts

      @doc "Return configured renderer module."
      @spec renderer() :: module()
      def renderer, do: @jido_character_definition.renderer

      @doc "Return configured renderer options."
      @spec renderer_opts() :: keyword()
      def renderer_opts, do: @jido_character_definition.renderer_opts

      @doc "Create a new character instance using defaults and overrides."
      @spec new(map()) :: {:ok, Character.t()} | {:error, Character.errors()}
      def new(attrs \\ %{}) do
        merged = Map.merge(defaults(), attrs)
        Character.new(merged)
      end

      @doc "Create a new character instance, raising on error."
      @spec new!(map()) :: Character.t()
      def new!(attrs \\ %{}) do
        merged = Map.merge(defaults(), attrs)
        Character.new!(merged)
      end

      @doc "Update an existing character immutably."
      @spec update(Character.t(), map()) :: {:ok, Character.t()} | {:error, Character.errors()}
      def update(character, attrs), do: Character.update(character, attrs)

      @doc "Update an existing character, raising on error."
      @spec update!(Character.t(), map()) :: Character.t()
      def update!(character, attrs), do: Character.update!(character, attrs)

      @doc "Validate character attributes."
      @spec validate(map()) :: {:ok, Character.t()} | {:error, Character.errors()}
      def validate(attrs), do: Character.validate(attrs)

      @doc "Render this character to LLM context."
      @spec to_context(Character.t(), keyword()) :: ReqLLM.Context.t()
      def to_context(character, opts \\ []) do
        module_opts = [
          renderer: @jido_character_definition.renderer,
          renderer_opts: @jido_character_definition.renderer_opts
        ]

        merged_opts = Keyword.merge(module_opts, opts)
        Character.to_context(character, merged_opts)
      end

      @doc "Render this character to a system prompt string."
      @spec to_system_prompt(Character.t(), keyword()) :: String.t()
      def to_system_prompt(character, opts \\ []) do
        module_opts = [
          renderer: @jido_character_definition.renderer,
          renderer_opts: @jido_character_definition.renderer_opts
        ]

        merged_opts = Keyword.merge(module_opts, opts)
        Character.to_system_prompt(character, merged_opts)
      end

      @doc "Persist this character using the configured adapter."
      @spec save(Character.t()) :: {:ok, Character.t()} | {:error, term()}
      def save(character) do
        adapter().save(@jido_character_definition, character)
      end

      @doc "Add knowledge to a character."
      @spec add_knowledge(Character.t(), String.t() | map() | [String.t() | map()], keyword()) ::
              {:ok, Character.t()} | {:error, Character.errors()}
      def add_knowledge(character, content, opts \\ []),
        do: Character.add_knowledge(character, content, opts)

      @doc "Add an instruction to a character."
      @spec add_instruction(Character.t(), String.t() | [String.t()]) ::
              {:ok, Character.t()} | {:error, Character.errors()}
      def add_instruction(character, instruction), do: Character.add_instruction(character, instruction)

      @doc "Add a memory entry to a character."
      @spec add_memory(Character.t(), String.t() | map(), keyword()) ::
              {:ok, Character.t()} | {:error, Character.errors()}
      def add_memory(character, content, opts \\ []), do: Character.add_memory(character, content, opts)

      @doc "Add a trait to a character's personality."
      @spec add_trait(Character.t(), String.t() | map() | [String.t() | map()], keyword()) ::
              {:ok, Character.t()} | {:error, Character.errors()}
      def add_trait(character, trait, opts \\ []), do: Character.add_trait(character, trait, opts)

      @doc "Add a value to a character's personality."
      @spec add_value(Character.t(), String.t() | [String.t()]) ::
              {:ok, Character.t()} | {:error, Character.errors()}
      def add_value(character, value), do: Character.add_value(character, value)

      @doc "Add a quirk to a character's personality."
      @spec add_quirk(Character.t(), String.t() | [String.t()]) ::
              {:ok, Character.t()} | {:error, Character.errors()}
      def add_quirk(character, quirk), do: Character.add_quirk(character, quirk)

      @doc "Add an expression to a character's voice."
      @spec add_expression(Character.t(), String.t() | [String.t()]) ::
              {:ok, Character.t()} | {:error, Character.errors()}
      def add_expression(character, expression), do: Character.add_expression(character, expression)

      @doc "Add a fact to a character's identity."
      @spec add_fact(Character.t(), String.t() | [String.t()]) ::
              {:ok, Character.t()} | {:error, Character.errors()}
      def add_fact(character, fact), do: Character.add_fact(character, fact)

      @doc "Evolve a character over a period of simulated time."
      @spec evolve(Character.t(), keyword()) :: {:ok, Character.t()} | {:error, Character.errors()}
      def evolve(character, opts \\ []), do: Character.evolve(character, opts)

      @doc "Evolve a character, raising on error."
      @spec evolve!(Character.t(), keyword()) :: Character.t()
      def evolve!(character, opts \\ []), do: Character.evolve!(character, opts)

      defoverridable new: 1,
                     new!: 1,
                     update: 2,
                     update!: 2,
                     validate: 1,
                     to_context: 2,
                     to_system_prompt: 2,
                     save: 1,
                     renderer: 0,
                     renderer_opts: 0,
                     add_knowledge: 3,
                     add_instruction: 2,
                     add_memory: 3,
                     add_trait: 3,
                     add_value: 2,
                     add_quirk: 2,
                     add_expression: 2,
                     add_fact: 2,
                     evolve: 2,
                     evolve!: 2
    end
  end
end