# AGENTS.md — kokoro-chat Development Brief

Read this before writing any code.

---

## Project Overview

**kokoro-chat** is a Python character chat engine core extracted from roleplay-chat. It provides a protocol-based, dependency-injectable `ChatEngine` with zero framework dependencies. Any app (FastAPI, CLI, etc.) can use it by implementing the 9 storage protocols.

**Version:** 0.1.0 (Alpha)
**Python:** ≥ 3.11
**Repo:** `github.com/motosan-dev/kokoro-chat`

---

## Repository Structure

```
kokoro-chat/
├── kokoro_chat/
│   ├── __init__.py              # Re-exports: Character, ChatConfig, ChatEngine
│   ├── config.py                # ChatConfig dataclass + cost calculation
│   ├── character/
│   │   ├── model.py             # Character dataclass (30+ fields, from_db_model)
│   │   └── loader.py            # map_v2_yaml_to_db() — YAML V2 → DB kwargs
│   ├── attitude/
│   │   ├── levels.py            # Affection levels (stranger→married) + behaviors
│   │   └── state.py             # AttitudeState — decay, modifiers, snapshots
│   ├── lorebook/
│   │   └── engine.py            # LorebookEngine — keyword scan + token budget
│   ├── memory/
│   │   └── memo.py              # Stale memo filtering (3-day expiry)
│   ├── prompt/
│   │   ├── assembler.py         # System prompt assembly (static + full paths)
│   │   ├── sections.py          # Shared section builders (single source of truth)
│   │   ├── budget.py            # Token budget constants + estimator
│   │   └── formatter.py         # Conversation history formatting
│   ├── providers/
│   │   ├── attitude.py          # Attitude context provider
│   │   ├── persona.py           # Character persona context provider
│   │   └── recent_chat.py       # Recent chat context provider
│   ├── tools/
│   │   ├── attitude_tool.py     # update_attitude (affection/trust/mood)
│   │   ├── memo_tool.py         # upsert_memo (key-value user info)
│   │   ├── memory_tool.py       # save_memory (with visibility: room/private)
│   │   └── search_tool.py       # search_memories (semantic search)
│   ├── storage/
│   │   └── protocol.py          # 9 Protocol interfaces + value objects
│   └── engine/
│       ├── chat.py              # ChatEngine (orchestration only, ~660 lines)
│       ├── llm.py               # LLM client factory + bridge classes
│       ├── attitude_manager.py  # AttitudeManager (cache → store → decay)
│       ├── message_manager.py   # MessageManager (history + save + few-shot)
│       ├── summarizer.py        # Summarizer (turn counting + LLM compression)
│       └── background.py        # Background task prompts + parsers
├── pyproject.toml
└── README.md
```

---

## Coding Standards

### Python
- Python 3.11+, type hints on all public functions
- `async`-first: all storage protocols and engine methods are async
- `@runtime_checkable` on all Protocol classes
- `@dataclass` for value objects (Character, ChatConfig, AttitudeState, etc.)
- No `Any` types unless truly necessary — use `Protocol` or generics
- No framework dependencies (no FastAPI, Flask, SQLAlchemy, Redis imports)

### Error Handling
- Tools return result dataclasses with `success: bool` + `error: str | None`
- Engine methods log warnings with `exc_info=True` on failures
- Never `raise` in background tasks — log and continue

### Tests
- Test files: `tests/test_<module>.py`
- Use `pytest` + `pytest-asyncio` (asyncio_mode = "auto")
- Mock all storage protocols — never call real DBs or LLMs
- Parametrize with synthetic data where applicable

### Commits
- Format: `type(scope): description`
- Types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`
- Scope: module name (e.g. `engine`, `prompt`, `attitude`, `tools`, `storage`)

---

## Key Types

### Storage Protocols (`storage/protocol.py`)

```python
@runtime_checkable
class AttitudeStore(Protocol):
    async def get(self, user_id: str, character_id: str) -> AttitudeRecord | None: ...
    async def create(self, user_id: str, character_id: str, ...) -> None: ...
    async def save(self, user_id: str, character_id: str, ...) -> None: ...

@runtime_checkable
class MemoryVectorStore(Protocol):
    async def search(self, query, character_id, user_id, top_k, ...) -> list[MemorySearchResult]: ...
    async def add(self, character_id, user_id, text, importance, ...) -> str: ...
```

### ChatEngine (`engine/chat.py`)

Orchestration only — delegates to `AttitudeManager`, `MessageManager`, and `Summarizer`.

```python
class ChatEngine:
    def __init__(self, config, attitude_store, cache, chat_history, chat_writer,
                 memory_store, memo_store, rolling_summary_store, event_store, ...): ...

    async def chat_stream(self, user_message, character, user_id, ...) -> AsyncGenerator: ...
    async def chat(self, user_message, character, user_id, ...) -> dict: ...
    async def get_attitude(self, char_id, user_id) -> dict: ...
    async def trigger_event(self, character, user_id, event_description) -> dict: ...
```

### AttitudeManager (`engine/attitude_manager.py`)

Handles attitude lifecycle: cache lookup → store fetch/create → time decay application.

```python
class AttitudeManager:
    async def get(self, char_id, user_id) -> AttitudeState: ...         # cache → store → new
    async def save(self, char_id, user_id, state) -> None: ...
    async def decay_modifiers(self, char_id, user_id, state) -> None: ...
```

### MessageManager (`engine/message_manager.py`)

Handles conversation history: cache lookup → store fetch, message persistence, few-shot prefix building.

```python
class MessageManager:
    async def get_for_prompt(self, char_id, user_id, ...) -> list[ChatMessage]: ...
    async def save(self, char_id, user_id, message) -> None: ...
    async def build_few_shot_prefix(self, character) -> list[dict]: ...
```

### Summarizer (`engine/summarizer.py`)

Tracks turn count and triggers LLM-based compression into a rolling summary.

```python
class Summarizer:
    async def maybe_summarize(self, char_id, user_id, messages, character) -> None: ...
```

### Character (`character/model.py`)

```python
@dataclass
class Character:
    id: str; name: str; personality: str; background: str
    # V2 fields: description, personality_summary, scenario, system_prompt, mes_example
    # Kokoro extensions: speech_patterns_v2, affection_overrides, post_history_by_level, anti_patterns

    @classmethod
    def from_db_model(cls, db_char) -> "Character": ...
```

### AttitudeState (`attitude/state.py`)

```python
@dataclass
class AttitudeState:
    affection: int  # 0-100
    trust: int      # 0-100
    mood: str
    attitude_modifiers: list[dict]  # {trigger, effect, remaining}

    def apply_time_decay(self, now=None) -> bool: ...
    def apply_event_impact(self, event_description, impact) -> None: ...
    def snapshot(self) -> dict: ...
```

---

## Prompt Assembly

Two paths in `prompt/assembler.py`:

| Path | Function | Use Case |
|------|----------|----------|
| Static (slim) | `assemble_static_system_prompt()` | AgentLoop with tools — LLM fetches dynamic context via tool calls |
| Full | `assemble_system_prompt()` | Legacy — all dynamic context baked into system prompt |

Assembly order: platform rules → character persona → traits → speech patterns → examples → anti-patterns → secrets → relationship → chat mode → scene → lorebook → dynamic context → post_history

Token budget: 12000 tokens max. Degrades by dropping: memories → events → truncating memos.

---

## Tool System

All tools are pure async functions (no class state):

| Tool | Function | Store Used |
|------|----------|-----------|
| `update_attitude` | Adjust affection/trust/mood | `AttitudeStore` |
| `upsert_memo` | Save user personal info | `MemoStore` |
| `save_memory` | Save conversation memory (room/private) | `MemoryVectorStore` |
| `search_memories` | Semantic memory search | `MemoryVectorStore` |

Each returns a result dataclass with `success`, relevant data, and optional `error`.

---

## Before Committing

```bash
pytest -x -q
```

Tests must pass. No linter configured yet — follow coding standards manually.

---

## Before Releasing

Every release **must** include documentation and changelog updates. This is a hard gate — do not tag or publish without completing all items.

### Release Checklist

1. **Update CHANGELOG.md** — add new version section:
   - Use `### Added`, `### Changed`, `### Fixed`, `### Removed`
   - Follow [Keep a Changelog](https://keepachangelog.com/) format
   - Include PR/issue numbers where applicable

2. **Bump version**:
   - `pyproject.toml` → `version = "X.Y.Z"`

3. **Update documentation** — any file affected by this release:
   - `README.md` — new features, API changes, usage examples
   - `AGENTS.md` — coding standards, key types, repository structure
   - `CLAUDE.md` — architecture, patterns, critical rules

4. **Run tests**:
   ```bash
   pytest -x -q
   ```

5. **Commit, tag, and push**:
   ```bash
   git add -p  # review each change
   git commit -m "chore: release vX.Y.Z"
   git tag -a vX.Y.Z -m "vX.Y.Z — summary"
   git push origin main --tags
   ```

### What Goes in the CHANGELOG

| Category | When to use |
|----------|-------------|
| **Added** | New protocols, new tools, new engine features, new character fields |
| **Changed** | Breaking protocol changes, prompt assembly changes, dependency updates |
| **Fixed** | Bug fixes, prompt quality fixes, decay/budget calculation fixes |
| **Removed** | Deprecated protocols, removed features |

### What Docs to Update

| Change type | Files to update |
|-------------|----------------|
| New protocol or store | README.md (Storage Protocol) + AGENTS.md (Key Types) + CLAUDE.md (Storage Protocols table) |
| New tool | README.md (Features) + AGENTS.md (Tool System table) + CLAUDE.md (Architecture) |
| Character model change | README.md (Usage) + AGENTS.md (Key Types: Character) |
| Prompt assembly change | AGENTS.md (Prompt Assembly) + CLAUDE.md (Known Patterns) |
| Engine API change | README.md (Usage) + AGENTS.md (Key Types: ChatEngine) |
| Attitude system change | README.md (Features) + AGENTS.md (Key Types: AttitudeState) + CLAUDE.md (Attitude System) |
| Dependency change | README.md (Dependencies) + CLAUDE.md (Dependencies) + pyproject.toml |

### Tag Convention

| Format | Example |
|--------|---------|
| `vX.Y.Z` | `v0.2.0` |

### Publishing

No CI/CD yet. Manual publish:
```bash
pip install build twine
python -m build
twine upload dist/*
```

---

## Design Principles

1. **Zero framework coupling** — this is a library, not an app
2. **Protocol-based DI** — all storage is swappable via `typing.Protocol`
3. **Async-native** — no sync wrappers, no blocking I/O
4. **Graceful degradation** — prompt assembly never crashes, just drops less important context
5. **Chinese-first** — default prompts, attitude behaviors, and platform rules are Traditional Chinese
