Skills — the capability layer
What a skill is#
A skill is one capability of the single consciousness. There is one loop — the consciousness — and a skill plugs into it by doing any subset of these:
| Hook | What it does |
|---|---|
| perceive | pushes Perception objects onto the bus — this is a sense |
tools() | tools armed only while the skill is active |
context_section | static prompt rules mounted while active |
context_for(batch) | prompt content computed from the current batch (e.g. recall) |
live_state() | volatile state injected into every frame (e.g. where she is in the game) |
left_undone(batch, acted) | a word for her when a turn is ending with this skill's world still waiting; the turn gets one more step (Minecraft: she only talked, her hands are empty) |
start() / stop() | owns infrastructure — a subprocess, a WebSocket, a poller |
File: src/core/skills/base.py
class Skill:
name: str # unique id, e.g. "voice:discord"
skill_name: Optional[str] = None # config/UI toggle key; None = core, always on
def __init__(self, config, bus, expression, context=None)
def initialize(self) -> None # one-time setup, no connections
async def start(self) -> None # gated by `enabled`
async def stop(self) -> None
enabled reads config.skills[skill_name].enabled. The UI is the single
source of truth — Bea can never arm a capability by herself. A skill with
skill_name = None is core: always on, never shown as a toggle.
SkillRegistry#
The catalog. It has no loop and no scheduler: the consciousness iterates it when it builds a prompt or dispatches a tool.
registry.all() # every skill
registry.active() # the ones currently running
registry.toggleable() # the ones with a config key (what the UI lists)
registry.get(name) # by `name`
registry.get_by_key(key) # by `skill_name`
registry.context_sections() # static rules from active skills
registry.dynamic_context(batch)
registry.tools() # every tool from every active skill
Everything is built in AIVtuberBrain._build_consciousness() and started by
Consciousness.start(), which calls start() on each one.
The skills#
| Skill | name | toggle | What it does |
|---|---|---|---|
ChatSurface | chat:ui | — (core) | text from the dashboard; the author is the owner |
StreamPlanSkill | plan | — (core) | today's objectives; contributes nothing while the plan is empty |
VoiceSurface | voice:discord | discord | Discord voice + text; owns the node subprocess |
TelegramSkill | chat:telegram | telegram | Telegram, in-process |
TwitchSkill | chat:twitch | twitch | Twitch chat, read anonymously |
DonationSkill | donation | donations | donations via webhook |
MinecraftSurface | game:mc | minecraft | the game body |
IdleSurface | idle | monologue | the passage of time |
MemorySkill | memory | memory | long-term recall and the diary |
SocialMemory | social | social_memory | who people are |
DreamSkill | dream | dream | sleep, self-lore, consolidation |
WebSkill | web | web | searching, and reading pages |
Tools by owner#
speak and stay_silent belong to the mind itself (src/core/mind/tools.py),
not to a skill. Everything else is armed by whichever skill is active:
| Owner | Tools |
|---|---|
| the mind | speak, stay_silent |
plan | objective_started, objective_done, objective_dropped |
voice:discord | discord_send_message, discord_reply, discord_react, discord_send_dm, discord_list_voice_channels, discord_join_voice, discord_leave_voice, discord_summon |
chat:telegram | telegram_send_message |
chat:twitch | twitch_say |
donation | recall_donors |
game:mc | every game tool: find_block, craft_item, move_to, attack_entity, build_template, mc_chat, stop_moving, … (full list in minecraft) |
social | remember_person, recall_person |
dream | go_to_sleep |
web | web_search, web_fetch |
memory | none — recall is injected every turn via context_for |
Some of those are armed by the skill's own state rather than by its toggle:
discord_leave_voice only while she is in a call, the objective_* tools only
while there is a plan, recall_donors only once somebody has donated, the game
tools only while the mod is connected. So the registry is assembled from the
live skills on every read rather than cached — the whole set costs
microseconds, and a prompt naming a tool the schema does not carry is a call
she invents. unarmed (src/core/mind/operating.py) logs an ERROR when the
two disagree.
Written answers go through the unified tools — send_message(platform, channel, text), react, say_nothing — with the destination in the
arguments. send_message names, and accepts, only the platforms something can
actually deliver to: minecraft is typed into with mc_chat and a donation has
no channel to answer in, so neither is a destination. There is no speak on a
written channel: answering out loud is impossible by construction, not by a
rule in the prompt.
Two shapes of skill#
A platform (Discord, Telegram, Twitch) extends PlatformSkill
(src/core/skills/platform.py) instead of Skill. It only owes three things —
platform, a way to build an Author, and a way to send text — and inherits
perception building, the humanizer delivery and the conversation tools.
Everything else extends Skill directly.
Adding a skill#
- Create
src/core/skills/my_skill/surface.py:
from src.core.skills.base import Skill
class MySkill(Skill):
name = "my:skill"
skill_name = "my_skill" # the config/UI toggle key
def initialize(self) -> None:
self.setting = self.config.skills.get("my_skill", {}).get("setting", "default")
def tools(self):
if not self.active:
return []
return [...]
- Add it to the tuple in
AIVtuberBrain._build_consciousness(). - Add its block to
config.example.jsonunderskills.
It now appears in the dashboard's Skills page and can be toggled at runtime.
If it is a text platform, extend PlatformSkill instead and the roster, person
cards and attention priorities work with no extra code —
they are all keyed on Author and conversation_key.