Her character1 of 8
The soul file
On this page
- What it looks like
- The frontmatter
- The one way to get the frontmatter wrong
- The five sections
- Which copy is read
- From a clone
- From a wheel (pip install)
- What happens on upgrade
- Pointing at a different file
- The trap: a typo in a relative path loads the default instead
- The three templates
- Making a change take effect
soul/default.md is who she is. It is one markdown file, 293 lines in the version that ships, and it
is the only file you edit to change her character. Nothing about her personality is compiled in.
The parser is api/src/kotoba/soul/loader.py. It reads exactly two things: a block of key: value
lines at the top, and a set of ## Section headings below. Everything else in the file — the prose,
the comments, the horizontal rules — is either content of a section or ignored.
What it looks like
# SOUL
name: Kotoba
language: auto
voice_id: JTlYtJrcTzPC71hMLOxo
avatar_model: mao_pro/runtime/mao_pro.model3.json
---
## Personality
...
## How to address the user
...
## Emotional rules
...
## Tool voice patterns
...
## Quirks
...
The --- on its own line separates the frontmatter from the body. The parser takes the FIRST line
that is exactly --- as that separator. Everything above it is frontmatter; everything below is
the body.
The frontmatter
Four keys are read. Anything else on those lines is silently ignored — a mood: cheerful you invent
does nothing and raises nothing.
| Key | What it does | Default when absent |
|---|---|---|
name | Her name. Goes into the prompt's first sentence: You ARE Kotoba, a voice-first AI companion. | (unnamed — ask the user during onboarding) appears in the prompt |
language | Her reply language, and — when it is an ISO code — the transcriber's language too. See Language | auto |
voice_id | The ElevenLabs voice id | none |
avatar_model | Which Live2D model she wears, as <dir>/<entry> | mao_pro/runtime/mao_pro.model3.json |
A # comment at the end of a value is stripped: language: auto # detect stores auto.
The one way to get the frontmatter wrong
Do not fence the frontmatter YAML-style. Skills use --- above and below their frontmatter; the
soul file does not. Fencing it puts the separator on line 1, so the whole block lands in the body
and every key is lost — silently:
---
name: Aria ← this file boots as an UNNAMED companion on the DEFAULT language
language: es
---
Measured with parse_soul_markdown: that file yields name=None, language='auto'. There is no
warning. kotoba doctor will not catch it either — it checks that the file was found, not that it
parsed the way you meant.
The five sections
Only five headings are recognised. The heading text is matched case-insensitively against this list; a heading you invent is not an error, its content simply never reaches her.
| Heading | Column | Where it ends up |
|---|---|---|
## Personality | personality | System prompt, under ## Your personality |
## How to address the user | address_style | System prompt, under ## How you address the user |
## Emotional rules | emotional_rules | System prompt, under ## When to feel what |
## Tool voice patterns | tool_patterns | Not the prompt. Parsed into the runtime's spoken-line table — see Tool voice patterns |
## Quirks | quirks | System prompt, under ## Your quirks |
Four of the five go into the system prompt verbatim, byte for byte. Measured on the shipped file:
personality 3,392 chars → in the prompt
address_style 575 chars → in the prompt
emotional_rules 1,661 chars → in the prompt
quirks 773 chars → in the prompt
-----
6,401 chars of a 32,953-character system prompt (19.4%)
tool_patterns 7,854 chars → NOT in the prompt
So about one fifth of what she is told is yours. The other four fifths are written in
api/src/kotoba/soul/prompt.py — the speaking rules, the tool descriptions, the platform note, the
clock — and are not editable from the soul file. See Prompt versus code.
A section you delete is not an error either. It falls back to the loader's _DEFAULTS: an empty
string for four of them, and the literal word name for address_style.
Which copy is read
There are two identical copies of the file in the repository:
soul/default.md ← the one a clone reads
api/src/kotoba/data/soul/default.md ← the one a wheel carries inside the package
They are byte-identical, and a test keeps them that way. api/tests/test_her_two_souls_are_the_same_soul.py
compares every file under both trees and fails if one differs:
"They drift the moment somebody edits one, and nothing says so: the clone keeps working, and the wheel ships a personality that was corrected weeks ago."
If you are editing the repo, edit soul/default.md and copy it to the packaged path, or that
test goes red.
Never edit the packaged copy directly. Which file a running install actually reads depends on how you installed:
From a clone
resolve_soul_path() tries, in order:
<repo>/api/soul/default.md— does not exist in a normal checkout<repo>/soul/default.md— this is the one~/.kotoba/soul/default.md- the packaged copy inside
api/src/kotoba/data/soul/
Verified on this checkout:
$ kotoba doctor
ok soul /path/to/kotoba-companion/soul/default.md
Edit soul/default.md in your clone.
From a wheel (pip install)
There is no repo, so REPO_ROOT and API_DIR both resolve to ~/.kotoba. The order collapses to:
~/.kotoba/soul/default.md- the packaged copy inside site-packages
Before you run kotoba setup, the packaged copy is what loads. kotoba setup calls
seed_home_copies() before it asks its first question, which writes:
~/.kotoba/soul/default.md
~/.kotoba/soul/templates/assistant.md
~/.kotoba/soul/templates/companion.md
~/.kotoba/soul/templates/study.md
From then on ~/.kotoba/soul/default.md is what loads. Edit that one.
Whichever install you have, kotoba doctor prints the resolved path. That is the answer, not
this table.
What happens on upgrade
seed_home_copies() never overwrites a file that already exists. Measured: seed once, edit
name: Kotoba to name: Yuki, seed again — the file still says Yuki.
That cuts both ways:
- Your edits survive an upgrade.
pip install -Ureplaces the copy inside site-packages and leaves~/.kotoba/soul/default.mdalone. - You never receive an improved default. If a later version rewrites the personality, you will
not see it. Compare against the packaged copy yourself, or delete your home copy and re-run
kotoba setupto get the new one.
Skills behave differently, and worse — see Skills.
Pointing at a different file
SOUL_PATH overrides the default. It is read once, at process start, in core/engine.py:
soul = await sync_from_file(db, os.getenv("SOUL_PATH", "./soul/default.md"))
SOUL_PATH=/home/you/my-companion.md kotoba
The trap: a typo in a relative path loads the default instead
For any path other than the exact string ./soul/default.md, the candidate list ends with
<repo>/soul/default.md "as a last resort so she still boots". Measured:
SOUL_PATH=./soul/templates/stuudy.md → loads soul/default.md, silently
SOUL_PATH=/tmp/does-not-exist.md → raises FileNotFoundError
Use an absolute path if you want a typo to be loud. Either way, kotoba doctor prints the file that
actually resolved.
The three templates
soul/templates/ holds three alternative personalities, seeded into ~/.kotoba/soul/templates/ on a
wheel install. Point SOUL_PATH at one to use it.
| Template | Voice settings it suggests | Character |
|---|---|---|
companion.md | stability 0.4 · style 0.45 | Warmest and most expressive |
assistant.md | stability 0.6 · style 0.15 | Steadier, more even delivery |
study.md | stability 0.55 · style 0.25 | Clear for explaining, with some warmth |
All three parse cleanly and all three ship with name: blank — onboarding asks. Two things to know
before you switch:
- Those ElevenLabs voice numbers are comments. Nothing reads them. They tell you what to set in the ElevenLabs dashboard yourself.
- Each template defines voice patterns for only six tools (
web_search,web_extract,memory_write,session_search,todo,clarify) against the default file's 27. The rest fall back to each tool module's built-in lines, which is a supported path, not a bug.
Making a change take effect
The soul file is read once, at process start. sync_from_file runs inside core.engine.start(),
and every turn afterwards reads the database, not the file.
So: edit the file, then restart her. There is no hot reload, no watcher, and no /reload.
And restarting is not enough for two of the four frontmatter keys — see Changing who she is, which is the part that surprises everybody.
