## Summary This PR gives the AIRI character skills, decisions, background tasks, and long-term memory. An external scene, such as a Discord channel, gets its own persistent session. The [ADR](https://github.com/chiba233/airi/blob/HoshinoYumeka/feat/cognitive-scheduler/docs/ai/adr/2026-10-10-skills-automations-and-memory.md) records the design. - **Skills.** A recipe works like a skill in a coding agent. The model loads its steps with `builtIn_useRecipe` and follows them. A keyword in the owner's message works like a slash command, so a style skill such as `i-have-adhd` keeps working in later replies. A recipe marked background runs as a task in its own session. Every recipe run can use every registered tool. - **Decisions.** A keyword invokes a decision recipe. The main model answers its question through `builtIn_judge` in the same turn and always picks one answer. Code then runs that answer's action: reply as usual, read without replying, add a hint, or use another recipe. - **Background tasks.** Each task runs in its own hidden session without voice, and tasks run at the same time. The chat lists open tasks, and the owner can stop them. The whole result reaches the conversation as a notice that history keeps. - **Long-term memory.** An index plus one entry per fact. The index lists every entry. The character reads, writes, and forgets entries with tools. Each entry belongs to one character card, and the owner can make an entry general. - **Scenes.** An input can name a scene `binding`, and the host keeps one persistent session for each binding. Scene replies and background tasks never speak on the host. Recipes do not start on their own. The Auto-run tab in Settings shows the idea in one sentence and a work-in-progress banner, so contributors can see where it goes. ## How prompts reach the model Every recipe prompt goes into one of the places below. Recipe steps never go into the system prompt of the owner conversation. | Prompt part | Where it goes | When | Text | | --- | --- | --- | --- | | Format rules and persona identity | System prompt | Every run | `composeSystemPrompt` | | Memory rules and the memory index | System prompt, after the identity | Memory is on | `composeMemoryPrompt`: the rules for the three memory tools, then one index line per entry | | Recipe list | Description of the `builtIn_useRecipe` tool | Every run in an owner conversation | `describeRecipesForRun`: usable recipes by name and purpose, the names of decision recipes, and proposals that wait. A call loads the steps. | | Skill invoked by keyword | Appended to the owner's message | The message has a keyword of the recipe | `[The owner used the skill "<name>". Follow its steps in this reply and in later replies while they fit.]` and then the steps. The text is stored with the message, so later turns keep it. | | Background skill invoked by keyword | Same place | Same | One line instead of the steps: the recipe runs in the background, so reply briefly and do not do its task. | | Decision invoked by keyword | Same place | Same | `judgmentSkillText`: the question and the answers, without what each answer leads to. The model calls `builtIn_judge` with one answer. | | `builtIn_judge` | Tool list of one turn | Only the turn whose owner message invoked a decision | The result gives the answer and the next step that code chose: reply, stay quiet, keep a hint in mind, or use another recipe. | | Recipe space | System prompt of the task session, after the identity | Every run of the task | `composeRecipeSpacePrompt`: `You run the recipe "<name>" in its own space ...`, then the recipe's purpose and steps. | | Task message | User message of the task session | The task starts | From a keyword: `The owner said: <message>`. From `builtIn_useRecipe`: the task that the model wrote. | | Task result | Notice in the owner conversation | The task ends | `[Notice from recipe:<name>, not a message from the owner.]`, then `The background task "<name>" finished.` and the whole result. The turn ends with `[Speak about this notice only if it fits now, or stay quiet.]` | Every task session gets every registered tool and the memory tools. A task session cannot start or propose recipes, and it cannot choose silence. ## Change map | Module | Before | After | Description | | --- | --- | --- | --- | | `core-agent` runtime | Identity stored in history. Every reply spoke. | Identity read per session at request time. Host send options for notices, background work, scenes, and invoked skills. The stream context carries `speaks`. | Scenes and tasks need their own identity, and they never speak for the owner. | | `core-agent` context registry | A module's entries stayed after it left. | `removeWriter` drops the entries of a module that leaves. | The prompt never keeps the observations of a module that is gone. | | `core-agent` recipes | None | Recipes, decisions with `judgeDecision`, and `MODEL_DECIDES_STEPS`. | Pure rules that the host and the tests share. | | `stage-ui` chat store | One send path for every input. | Recipe tools (`builtIn_useRecipe`, `builtIn_proposeRecipe`, `builtIn_judge`), memory tools, keyword skills, and background tasks with notices. A notice and the reply to it never sync to the cloud. | Recipes, memory, and tasks use the existing orchestrator, so every surface sends the same way. | | `stage-ui` chat surfaces | None | Recipe and judgment blocks, notices, and the background task list. | The owner sees which recipe ran, what a judgment decided, and which tasks are open. | | `stage-ui` character orchestrator | Every window that started it handled spark notifications. | Only the renderer leader handles them. The apps pass their synced Pinia runtime when they start it. | Several windows never handle the same notification. | | Settings | Long-term memory showed a work-in-progress notice. | Long-term memory lists recipes and memory entries. The owner writes, approves, and deletes recipes. The owner makes a memory general or forgets it. The Auto-run tab shows its idea and a work-in-progress banner. | The owner reviews what the model proposes and what it remembers. | | `plugin-protocol` | An input named only a session. | `input:text` can name a scene `binding` in `overrides`. | The host maps each binding to one persistent session. | | `discord-bot` | Sessions keyed by guild or DM. | Each text or voice channel binds to its own persistent session. | Each channel keeps its own history, apart from the owner's chat. | ## Boundaries and risks | Invariant or boundary | Failure mode | Protection | Evidence or gap | | --- | --- | --- | --- | | Model proposals need approval | A model saves a recipe that runs without the owner | A proposal is unapproved and disabled until the owner approves it. | `propose-recipe.test.ts`, `recipes.test.ts` | | A judgment always decides | The model hedges and no action runs | `builtIn_judge` takes one answer id and no confidence. Code runs that answer's action. | `judge-recipe.test.ts`, `recipe.test.ts` | | A notice is not owner speech | History stores a task result as owner speech | Notices are stored with their source and projected as notices | `chat-orchestrator-host-options.test.ts`, `chat-background-tasks.browser.test.ts` | | A notice stays on this device | Another device reads a notice as owner speech | `isCloudSyncableMessage` keeps a notice and its reply local, for live sends and for the backfill of a new cloud chat | `wire-message.test.ts` | | A task session stays hidden | Deleting a conversation selects a hidden task session | The replacement after a delete skips hidden sessions | `session-store.browser.test.ts` | | Only replies to the owner speak | A scene reply or a task reads aloud on the host | `speaks` per send, checked by the stage | `chat-orchestrator-host-options.test.ts`, `chat.contract.test.ts`. Live speech is not verified. | | Spark notifications run once | Several windows handle the same notification | Only the renderer leader handles them | Not verified live | | Scene privacy | A scene reply reaches every connected module, and a scene prompt reads every shared context | None. Both match `main`. | Known gap. A later PR handles it. | ## Verification All commands ran on48e0ad756. ```text pnpm -F @proj-airi/core-agent build # passed pnpm -F @proj-airi/core-agent exec vitest run src/runtime # 9 files, 176 tests passed pnpm -F @proj-airi/stage-ui exec vitest run --project node src/tools src/stores/chat.contract.test.ts src/stores/chat/prompt-recipe.test.ts src/stores/chat/context-store.test.ts src/stores/memory.test.ts src/stores/recipes.test.ts src/stores/character/orchestrator/index.test.ts # 14 files, 154 tests passed pnpm typecheck # 54 of 54 tasks passed pnpm exec eslint <files that this PR changes> # 0 errors ``` Browser tests and the full unit suite run in CI. Not verified: - Live chat with real models, Discord channel replies, and speech output. ## Visual changes All captures use stage-web at 1440x900, light theme, English, and the same seeded recipes and memories. Before is upstream `main` at778f767fc. | Before | After | | --- | --- | |  |  | | Settings / Long-term memory | Settings / Long-term memory | | Absent |  | | Settings / Recipes / Conversation | Settings / Recipes / Conversation | | Absent |  | | Settings / Recipes / Auto-run | Settings / Recipes / Auto-run | | Absent |  | | Settings / Memories | Settings / Memories | | Absent |  | | Chat / Recipe call, judgment, notice, and background tasks | Chat / Recipe call, judgment, notice, and background tasks | ## Rollout and follow-up - Recipes that start on their own are not part of this PR. The Auto-run tab keeps the idea open for discussion. - A later PR adds skills that the model writes as code. The code runs in a QuickJS sandbox (`quickjs-emscripten`). Settings show each code skill as a card that the model describes, and the owner can only delete it. This PR does not depend on it. - A later PR handles scene privacy: chat output that returns only to its source, and shared context that a scene cannot read. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
discord-bot
Allow アイリ to talk to you and many other users in Discord voice channels.
Channel sessions
Text messages and voice transcriptions both bind to discord:channel:<id>. Without a binding, a reply would join the owner's private session.
Getting started
git clone git@github.com:moeru-ai/airi.git
pnpm i
In Discord Developer Portal, create a new application and this will be the bot you will add to your server.
In the "Bot" tab, find "Privileged Gateway Intents" section, toggle on the following intents:
- "Server Members Intent"
- "Message Content Intent"
Now look above the "Privileged Gateway Intents" section, you will find the "Token" section, for newly created bots, click "Reset Token" to generate a new token, and copy the token for later use.
Note
If you ever forgot the token or lost it, you can always click "Reset Token" to generate a new token, but remember to update the token in your
.env.localfile, or configure through the UI as well.
Create a .env.local file:
cd integrations/discord-bot
cp .env .env.local
Fill-in the following credentials as configurations:
DISCORD_TOKEN=''
DISCORD_BOT_CLIENT_ID=''
OPENAI_MODEL=''
OPENAI_API_KEY=''
OPENAI_API_BASE_URL=''
ELEVENLABS_API_KEY=''
ELEVENLABS_API_BASE_URL=''
pnpm run -F @proj-airi/discord-bot start
Other similar projects
Acknowledgements
- Implementation of Audio handling and processing https://github.com/TheTrueSCP/CharacterAIVoice/blob/54d6a41b4e0eba9ad996c5f9ddcc6230277af2f8/src/VoiceHandler.js
- Example of usage https://github.com/discordjs/voice-examples/blob/da0c3b419107d41053501a4dddf3826ad53c03f7/radio-bot/src/bot.ts
- Excellent library https://github.com/discordjs/discord.js