This guide provides comprehensive instructions for developing and customizing AIr-Friends. For architectural details and design decisions, see DESIGN.md.
- Deno 2.x or higher
- dumb-init - Required for wrapping agent subprocesses with proper signal forwarding and zombie process reaping
- Discord Bot Token (for Discord integration)
- Misskey Access Token (for Misskey integration)
- OpenCode CLI - The ACP-compliant CLI agent that powers AIr-Friends
- For OpenCode CLI: GEMINI_API_KEY, OPENCODE_API_KEY, or OPENROUTER_API_KEY for provider access
-
Clone the repository
git clone https://github.com/jim60105/AIr-Friends.git cd AIr-Friends -
Set up environment variables
cp .env.example .env # Edit .env with your credentials and configuration -
Optional: Configure the bot
All the necessary configuration can be done through environment variables. However, if you prefer using a YAML config file, copy the example config:
cp config.example.yaml config.yaml # Edit config.yaml as needed -
Run in development mode
deno task dev
-
Run in production mode
deno task start
-
Run with YOLO mode (auto-approve all permissions)
deno run --allow-net --allow-read --allow-write --allow-env --allow-run src/main.ts --yolo
Warning
YOLO mode auto-approves ALL permission requests from the ACP agent. Only use this in trusted container environments or for testing purposes.
| Task | Description | Command |
|---|---|---|
dev |
Development mode with hot reload | deno task dev |
start |
Production mode | deno task start |
test |
Run tests | deno task test |
fmt |
Format code | deno task fmt |
lint |
Lint code | deno task lint |
check |
Type check | deno task check |
AIr-Friends/
├── src/
│ ├── main.ts # Entry point
│ ├── bootstrap.ts # Application bootstrap
│ ├── shutdown.ts # Graceful shutdown handler
│ ├── healthcheck.ts # Health check server
│ ├── acp/ # ACP Client integration
│ │ ├── agent-connector.ts
│ │ ├── agent-factory.ts
│ │ ├── client.ts
│ │ └── types.ts
│ ├── core/ # Core logic (agent, memory, workspace)
│ │ ├── agent-core.ts
│ │ ├── session-orchestrator.ts
│ │ ├── workspace-manager.ts
│ │ ├── memory-store.ts
│ │ ├── context-assembler.ts
│ │ ├── message-handler.ts
│ │ ├── reply-dispatcher.ts
│ │ ├── reply-policy.ts
│ │ ├── config-loader.ts
│ │ ├── template-renderer.ts
│ │ ├── error-handler.ts
│ │ ├── event-router.ts
│ │ ├── model-router.ts
│ │ ├── rate-limiter.ts
│ │ ├── spontaneous-scheduler.ts
│ │ ├── spontaneous-target.ts
│ │ ├── channel-lurk-scheduler.ts
│ │ ├── self-research-scheduler.ts
│ │ ├── memory-maintenance-scheduler.ts
│ │ ├── reminder-scheduler.ts
│ │ ├── reminder-store.ts
│ │ ├── scheduler-state-store.ts
│ │ ├── audit-logger.ts
│ │ ├── audit-retention.ts
│ │ ├── audit-retention-scheduler.ts
│ │ ├── git-backup-service.ts
│ │ ├── git-backup-scheduler.ts
│ │ ├── git-credential-setup.ts
│ │ └── skill-installer.ts
│ ├── platforms/ # Platform adapters (Discord, Misskey)
│ │ ├── platform-adapter.ts
│ │ ├── platform-registry.ts
│ │ ├── discord/
│ │ └── misskey/
│ ├── skills/ # Skill handlers
│ │ ├── registry.ts
│ │ ├── index.ts
│ │ ├── memory-handler.ts
│ │ ├── reply-handler.ts
│ │ ├── context-handler.ts
│ │ ├── file-handler.ts
│ │ ├── reaction-handler.ts
│ │ ├── reminder-handler.ts
│ │ └── types.ts
│ ├── skill-api/ # HTTP API for shell skills
│ │ ├── server.ts
│ │ └── session-registry.ts
│ ├── types/ # TypeScript type definitions
│ │ ├── audit.ts
│ │ ├── config.ts
│ │ ├── context.ts
│ │ ├── errors.ts
│ │ ├── events.ts
│ │ ├── logger.ts
│ │ ├── memory.ts
│ │ ├── platform.ts
│ │ ├── reminder.ts
│ │ ├── template.ts
│ │ └── workspace.ts
│ └── utils/ # Utility functions
│ ├── env.ts
│ ├── logger.ts
│ ├── gelf-transport.ts
│ ├── hash.ts
│ ├── metrics.ts
│ ├── path-validator.ts
│ ├── rss-fetcher.ts
│ ├── text-search.ts
│ └── token-counter.ts
├── skills/ # Shell-based skill scripts
│ ├── memory-save/
│ ├── memory-search/
│ ├── memory-patch/
│ ├── memory-stats/
│ ├── memory-export/
│ ├── fetch-context/
│ ├── get-message/
│ ├── send-reply/
│ ├── edit-reply/
│ ├── send-file/
│ ├── react-message/
│ ├── set-reminder/
│ ├── list-reminders/
│ ├── cancel-reminder/
│ ├── self-research/
│ ├── agent-browser/
│ ├── chinese-content-writing-guideline/
│ └── lib/ # Shared skill client library
├── prompts/ # Bot prompt files (template system)
│ ├── system_reply.md # Normal message reply system prompt
│ ├── character_name.md # Replaces {{character_name}}
│ ├── character_info.md # Replaces {{character_info}}
│ └── ... # Any .md file becomes a placeholder source
├── config/ # Configuration examples
├── docs/ # Documentation & BDD features
│ ├── DESIGN.md # Design document
│ ├── DEVELOPMENT.md # This file
│ ├── SKILLS_IMPLEMENTATION.md
│ └── features/ # Gherkin feature specs
└── tests/ # Test files
For more details on the architecture, see DESIGN.md.
For development guide for AI agents working on this codebase, see AGENTS.md.
Configuration is loaded from config.yaml (YAML format). See config.example.yaml for a complete example.
Platform & Auth:
| Variable | Description |
|---|---|
DISCORD_ENABLED |
Enable Discord integration (true/false) |
DISCORD_TOKEN |
Discord bot token |
MISSKEY_ENABLED |
Enable Misskey integration (true/false) |
MISSKEY_HOST |
Misskey instance host |
MISSKEY_TOKEN |
Misskey access token |
GEMINI_API_KEY |
API key for the OpenCode Gemini provider |
OPENCODE_API_KEY |
OpenCode API key |
OPENROUTER_API_KEY |
OpenRouter API key |
Agent & Model:
| Variable | Description |
|---|---|
AGENT_MODEL |
LLM model identifier (e.g., "gpt-5-mini") |
AGENT_DEFAULT_TYPE |
Default ACP agent type (opencode) |
AGENT_SKILLS_DIR |
Skills directory path (default: "skills") |
AGENT_EXTERNAL_SKILLS |
External skills to install at startup (JSON string, e.g. [{"repo":"owner/repo","skill":"name"}]) |
AGENT_AUTO_APPROVE_SKILLS |
Skill names to auto-approve in restricted mode (comma-separated) |
AGENT_MCP_SERVERS |
External MCP servers (JSON string array) |
MODEL_ROUTING_ENABLED |
Enable model routing (true/false, default: false) |
MODEL_ROUTING_RULES |
Model routing rules as JSON string |
Agent Sandbox:
| Variable | Description |
|---|---|
AGENT_SANDBOX_FILTER_ENV |
Filter subprocess env vars to allowed list only (true/false, default: true) |
AGENT_SANDBOX_NETWORK_ISOLATION |
Enable Linux network namespace isolation (true/false, default: false) |
AGENT_SANDBOX_ALLOWED_ENV_VARS |
Additional env var names to pass through filter (comma-separated) |
AGENT_SANDBOX_ALLOWED_WRITE_EXTENSIONS |
Allowed file extensions for agent writes (comma-separated, default: .md,.txt) |
Agent Idle Timeout:
| Variable | Description |
|---|---|
AGENT_IDLE_TIMEOUT_ENABLED |
Enable idle timeout detection (true/false, default: true) |
AGENT_IDLE_TIMEOUT_MS |
Idle timeout in ms (default: 300000 = 5 min) |
AGENT_IDLE_TIMEOUT_CHECK_INTERVAL_MS |
Check interval in ms (default: 30000 = 30s) |
Agent Connect Timeout:
| Variable | Description |
|---|---|
AGENT_CONNECT_TIMEOUT_MS |
Max time to wait for the ACP handshake during connect() (default: 30000 = 30s). A WARN is logged at 80% elapsed as an early signal before the hard timeout fires. This default is convention-based (matches the idle-timeout default), not measured from production connect latency — revisit once real data accumulates post-rollout. |
Agent Git Credential Store:
| Variable | Description |
|---|---|
AGENT_GIT_CREDENTIAL_ENABLED |
Enable git credential store for agent subprocesses (true/false, default: false) |
AGENT_GIT_CREDENTIAL_HOST |
Override git host for credential store (default: from gitBackup.remoteUrl or github.com) |
Dry Run / Debug Mode:
| Variable | Description |
|---|---|
DRY_RUN_ENABLED |
Enable dry run mode (true/false, default: false) |
DRY_RUN_OUTPUT_PATH |
Output directory for assembled prompts (default: ./data/dry-run/) |
DRY_RUN_MOCK_REPLY |
Mock reply text (empty = no reply) |
Reply Policy & Channels:
| Variable | Description |
|---|---|
REPLY_POLICY |
Reply policy mode (all/public/channels) (REPLY_TO accepted as alias) |
CHANNELS |
Channels entries (JSON array, replaces config) |
Discord Platform:
| Variable | Description |
|---|---|
DISCORD_SPONTANEOUS_ENABLED |
Enable spontaneous posting (true/false, default: false) |
DISCORD_SPONTANEOUS_MIN_INTERVAL_MS |
Min interval between posts in ms (default: 10800000) |
DISCORD_SPONTANEOUS_MAX_INTERVAL_MS |
Max interval between posts in ms (default: 43200000) |
DISCORD_SPONTANEOUS_CONTEXT_FETCH_PROBABILITY |
Probability of fetching recent messages (0.0-1.0, default: 0.5) |
DISCORD_TYPING_INDICATOR_ENABLED |
Show typing indicator while processing (true/false, default: false) |
DISCORD_CHANNEL_LURK_ENABLED |
Enable channel lurk reply (true/false, default: false) |
DISCORD_CHANNEL_LURK_INTERVAL_MS |
Channel lurk check interval in ms (default: 1800000) |
Misskey Platform:
| Variable | Description |
|---|---|
MISSKEY_SPONTANEOUS_ENABLED |
Enable spontaneous posting (true/false, default: false) |
MISSKEY_SPONTANEOUS_MIN_INTERVAL_MS |
Min interval between posts in ms (default: 10800000) |
MISSKEY_SPONTANEOUS_MAX_INTERVAL_MS |
Max interval between posts in ms (default: 43200000) |
MISSKEY_SPONTANEOUS_CONTEXT_FETCH_PROBABILITY |
Probability of fetching recent messages (0.0-1.0, default: 0.5) |
Rate Limiting:
| Variable | Description |
|---|---|
RATE_LIMIT_ENABLED |
Enable rate limiting (true/false, default: false) |
RATE_LIMIT_MAX_REQUESTS_PER_WINDOW |
Max requests per sliding window per user (default: 10) |
RATE_LIMIT_WINDOW_MS |
Sliding window duration in ms (default: 600000) |
RATE_LIMIT_COOLDOWN_MS |
Cooldown period in ms after limit exceeded (default: 600000) |
Self-Research:
| Variable | Description |
|---|---|
SELF_RESEARCH_ENABLED |
Enable self-research (true/false, default: false) |
SELF_RESEARCH_MODEL |
LLM model for self-research (separate from chat) |
SELF_RESEARCH_RSS_FEEDS |
RSS feed sources as JSON string |
SELF_RESEARCH_MIN_INTERVAL_MS |
Min interval between research sessions (default: 43200000) |
SELF_RESEARCH_MAX_INTERVAL_MS |
Max interval between research sessions (default: 86400000) |
Memory Maintenance:
| Variable | Description |
|---|---|
MEMORY_MAINTENANCE_ENABLED |
Enable memory maintenance (true/false, default: false) |
MEMORY_MAINTENANCE_MODEL |
LLM model for memory maintenance |
MEMORY_MAINTENANCE_MIN_MEMORY_COUNT |
Min enabled memories before maintenance runs (default: 50) |
MEMORY_MAINTENANCE_INTERVAL_MS |
Interval between maintenance runs in ms (default: 604800000) |
Scheduled Reminders:
| Variable | Description |
|---|---|
REMINDERS_ENABLED |
Enable scheduled reminders (true/false, default: false) |
REMINDERS_MAX_PER_USER |
Max active reminders per user (default: 20) |
REMINDERS_MIN_INTERVAL_MS |
Minimum reminder delay from now in ms (default: 60000) |
REMINDERS_PERSIST_PATH |
Reminder persistence file name (default: reminders.jsonl) |
REMINDERS_CHECK_INTERVAL_MS |
How often to check for due reminders in ms (default: 30000) |
Git Backup:
| Variable | Description |
|---|---|
GIT_BACKUP_ENABLED |
Enable Git backup (true/false, default: false) |
GIT_BACKUP_REMOTE_URL |
Remote Git repository URL (HTTPS) |
GIT_BACKUP_INTERVAL_MS |
Backup interval in ms (default: 3600000 = 1 hour) |
GIT_BACKUP_AUTHOR_NAME |
Git commit author name |
GIT_BACKUP_AUTHOR_EMAIL |
Git commit author email |
GIT_BACKUP_AUTH_USER |
Git backup HTTPS auth username (default: authorEmail) |
GIT_BACKUP_AUTH_PASSWORD |
Git backup HTTPS auth password/token (default: GITHUB_TOKEN) |
Send File Skill:
| Variable | Description |
|---|---|
SKILL_SEND_FILE_ENABLED |
Enable send-file skill (true/false, default: false) |
SKILL_SEND_FILE_MAX_SIZE_MB |
File size limit in MB (0 = platform default) |
SKILL_SEND_FILE_ALLOWED_EXTENSIONS |
Allowed file extensions whitelist (comma-separated) |
SKILL_SEND_FILE_MAX_FILES_PER_INVOCATION |
Max files per send-file invocation (default: 10) |
SKILL_SEND_FILE_MAX_TOTAL_SIZE_MB |
Max aggregate size per invocation in MB (default: 50) |
Metrics & Health:
| Variable | Description |
|---|---|
METRICS_ENABLED |
Enable Prometheus metrics endpoint (true/false, default: false) |
METRICS_PATH |
Metrics endpoint path (default: /metrics) |
HEALTH_PORT |
Port for health check / metrics endpoint |
Session Audit Log:
| Variable | Description |
|---|---|
AUDIT_ENABLED |
Enable audit logging (true/false, default: false) |
AUDIT_RETENTION_DAYS |
Log retention in days (default: 7) |
AUDIT_HASH_CONTENT |
SHA-256 hash user content in audit entries (true/false, default: true) |
AUDIT_INCLUDED_PHASES |
Only record these phases (comma-separated, empty = all) |
Logging & Environment:
| Variable | Description |
|---|---|
LOG_LEVEL |
Logging level (DEBUG/INFO/WARN/ERROR) |
DENO_ENV |
Environment name (dev/prod) |
GELF_ENABLED |
Enable GELF log output (true/false, default: false) |
GELF_ENDPOINT |
GELF endpoint URL |
GELF_HOSTNAME |
Source hostname in GELF messages (default: air-friends) |
GELF_PROTOCOL |
GELF transport protocol: http/tcp/udp (default: http) |
GELF_COMPRESS |
Enable GZIP compression for UDP transport (true/false) |
AIr-Friends can centrally control whether an incoming event is processed by AgentCore using a top-level replyPolicy and a channels list:
all: reply to all events in public channels and DMs.public: always reply in public channels; for DMs, reply only if account/channel is configured withrateLimitBypass.channels: reply only when account/channel is listed in thechannelsconfig (default).
Channel config entry format:
{platform}/account/{account_ID}
{platform}/channel/{channel_ID}
Example configuration:
replyPolicy: "channels"
channels:
- id: "discord/account/123456789012345678"
enabled: true
spontaneousPost: false
channelLurk: false
rateLimitBypass: false
- id: "discord/channel/987654321098765432"
enabled: true
spontaneousPost: true
channelLurk: true
rateLimitBypass: false
- id: "misskey/account/abcdef1234567890"
enabled: true
spontaneousPost: false
channelLurk: false
rateLimitBypass: trueEnvironment variable overrides:
REPLY_POLICY=public # (REPLY_TO is still accepted as an alias)
CHANNELS='[{"id":"discord/account/12345678901234567","enabled":true}]' # JSON array replaces configAIr-Friends supports dynamic model selection based on user identity, channel, or session type. This allows operators to fine-tune API costs and response quality per context.
- Rules are evaluated in array order (first-match wins)
- The first matching rule determines the model
- If no rule matches, the system falls back to
agent.model(or section-specific model for self-research/memory-maintenance) - When
modelRouting.enabledisfalse(default), routing is skipped entirely — backward compatible
Via config.yaml:
agent:
model: "gpt-5-mini" # default fallback model
modelRouting:
enabled: true
rules:
# Specific account + research keywords → research model
- match:
channel: "discord/account/12345678901234567"
contentKeywords: ["研究", "research"]
model: "openrouter/google/gemini-2.5-pro"
# Any message with research keywords → research model
- match:
contentKeywords: ["研究", "research", "論文", "paper"]
model: "openrouter/google/gemini-2.5-pro"
# Premium model for a specific user (any content)
- match: { channel: "discord/account/12345678901234567" }
model: "openrouter/deepseek/deepseek-v3.2"
# Cheaper model for spontaneous posts
- match: { sessionType: "spontaneous" }
model: "openrouter/deepseek/deepseek-v3.2"
# Premium model for self-research
- match: { sessionType: "self-research" }
model: "openrouter/anthropic/claude-opus-4.8"Via environment variables:
MODEL_ROUTING_ENABLED=true
MODEL_ROUTING_RULES='[{"match":{"channel":"discord/account/12345678901234567","contentKeywords":["研究","research"]},"model":"openrouter/google/gemini-2.5-pro"},{"match":{"sessionType":"spontaneous"},"model":"openrouter/deepseek/deepseek-v3.2"}]'Each rule's match object supports multiple conditions combined with AND logic. All specified conditions must match for the rule to apply:
| Field | Example | Description |
|---|---|---|
channel |
"discord/account/12345678901234567" |
Match a specific channel entry |
sessionType |
"message" |
Match a session type |
contentKeywords |
["研究", "research"] |
Match message content containing any keyword (OR within array, case-insensitive). Only effective for sessionType: "message" |
Valid sessionType values: "message", "spontaneous", "self-research", "memory-maintenance", "reminder", "channelLurk"
For self-research and memory-maintenance sessions, the fallback chain is:
modelRouting rules (if enabled & matched)
→ section-specific model (selfResearch.model / memoryMaintenance.model)
→ agent.model
For message and spontaneous sessions:
modelRouting rules (if enabled & matched)
→ agent.model
AIr-Friends supports sending structured log messages to a GELF (Graylog Extended Log Format) compatible server via HTTP. This enables centralized log management using tools like Graylog or Grafana Loki.
Via config.yaml:
logging:
level: "INFO"
gelf:
enabled: true
endpoint: "http://graylog.example.com:12202/gelf"
hostname: "my-bot-instance"Via environment variables:
GELF_ENABLED=true
GELF_ENDPOINT=http://graylog.example.com:12202/gelf
GELF_HOSTNAME=my-bot-instance- Log messages are sent asynchronously via HTTP POST to the configured endpoint
- The GELF transport uses fire-and-forget pattern — log sending never blocks the main execution flow
- Failed sends are logged to stderr and silently discarded
- Each request has a 5-second timeout to prevent hanging connections
- All log levels (DEBUG through FATAL) are mapped to corresponding Syslog severity levels
- Context data from log entries is automatically flattened into GELF additional fields
- Sensitive data is already sanitized before reaching the GELF transport
{
"version": "1.1",
"host": "air-friends",
"short_message": "Configuration loaded successfully",
"timestamp": 1735689600.000,
"level": 6,
"_module": "ConfigLoader",
"_log_level": "INFO",
"_enabledPlatforms": "[\"discord\"]"
}When running in a container, configure GELF via environment variables in your compose.yml:
services:
air-friends:
image: ghcr.io/jim60105/air-friends:latest
environment:
- GELF_ENABLED=true
- GELF_ENDPOINT=http://graylog:12202/gelf
- GELF_HOSTNAME=air-friends-productionThe container includes a pre-configured opencode.json that automatically sets up OpenCode CLI with:
- Gemini Provider: Uses
GEMINI_API_KEYenvironment variable - Only Necessary Tools Enabled: enable bash, disable edit and write
- Auto-compaction: Enabled for better token management
- Auto-update: Disabled (container should be rebuilt for updates)
The configuration defines a dual-agent setup: a build agent (default) for restricted mode with granular permission whitelisting ("*": "deny" + specific allows), and a yolo agent for unrestricted mode ("*": "allow"). When YOLO mode is enabled, the system switches to the yolo agent via ACP setSessionMode("yolo").
The configuration file is located at ~/.config/opencode/opencode.json inside the container. OpenCode automatically enables its providers when their respective keys are available as environment variables:
- OpenRouter provider: Uses
OPENROUTER_API_KEY - Gemini provider: Uses
GEMINI_API_KEY - OpenCode's own hosted API: Uses
OPENCODE_API_KEY
To override the system prompt, set the prompt path in opencode.json to point at prompts/system_prompt_override.md.
You can customize OpenCode behavior by mounting your own opencode.json configuration file:
podman run -d --rm \
-v data:/app/data \
-v ./config.yaml:/app/config.yaml:ro \
-v ./my-opencode.json:/home/deno/.config/opencode/opencode.json:ro \
--env-file .env \
--name air-friends \
ghcr.io/jim60105/air-friends:latestFor more information about OpenCode configuration, see the OpenCode documentation.
I recommend checking out my blog post, "🤖 AI Can Cosplay Too? A Beginner's Guide to LLM Character Role-Playing", for tips on setting up your character.
The system prompt (prompts/system_reply.md) uses Vento as its template engine. Vento is a JavaScript-based template engine that uses {{ }} syntax for both interpolation and control flow.
- Variable interpolation:
{{ variableName }}outputs the value of a variable - Conditionals:
{{ if condition }}...{{ else }}...{{ /if }} - Loops:
{{ for item of collection }}...{{ /for }} - Include:
{{ include "./filename.md" }}to include other template files - Set:
{{ set varName }}...{{ /set }}to assign content to a variable - JavaScript expressions: Any valid JS expression works inside
{{ }} - Comments:
{{# This is a comment #}}(not included in output) - Trimming:
{{- ... -}}removes surrounding whitespace
For complete Vento documentation, visit https://vento.js.org/
The following variables are available in all prompt templates:
| Variable | Type | Description | Example |
|---|---|---|---|
isDm |
boolean |
Whether this is a direct message conversation | true |
platform |
string |
Platform name | "discord", "misskey" |
userId |
string |
User's platform ID | "560842157351763989" |
channelId |
string |
Channel/conversation ID | "873618490202931231" |
guildId |
string |
Server/guild ID (empty string if N/A) | "" |
sessionId |
string |
Current skill API session ID | "sess_abc123" |
agentType |
string |
ACP agent type ("opencode") |
"opencode" |
model |
string |
Model identifier for the current session | "claude-opus-4.6" |
yolo |
boolean |
Whether YOLO mode is enabled (bypasses permission restrictions) | true |
canWriteAgentWorkspace |
boolean |
Whether this session allows writing to agent workspace | false |
Special prompt variables (only available in specific prompt types):
| Variable | Available In | Description |
|---|---|---|
rssItems |
system_self_research.md |
Formatted RSS feed items |
workspaceKey |
system_memory_maintenance.md |
User workspace identifier |
memoriesDump |
system_memory_maintenance.md |
JSON dump of enabled memories |
minMemoryCount |
system_memory_maintenance.md |
Minimum memory count threshold for maintenance |
recentMessagesFetched |
system_spontaneous.md |
Whether recent messages were fetched |
importantMemories |
system_spontaneous.md |
Formatted important memories text |
recentMessages |
system_spontaneous.md |
Formatted recent messages text |
availableEmojis |
system_spontaneous.md |
Formatted available emojis text |
userContextMessage |
system_reply.md |
Pre-formatted user context message |
reminderMessage |
system_reminder.md |
Reminder message content |
reminderCreatedAt |
system_reminder.md |
Reminder creation timestamp |
reminderScheduledAt |
system_reminder.md |
Reminder scheduled timestamp |
DM-specific instructions:
{{ if isDm }}
This is a private conversation. You can discuss personal topics freely.
{{ else }}
This is a public channel. Be mindful of other participants.
{{ /if }}Platform-specific formatting:
{{ if platform === "discord" }}
Use Discord Markdown for formatting (bold, italic, code blocks).
Message limit: 2000 characters.
{{ else if platform === "misskey" }}
Use MFM (Misskey Flavored Markdown) for formatting.
{{ /if }}YOLO-conditional agent permissions:
{{ if yolo }}
You have full unrestricted access to all tools and commands.
{{ else }}
You are in restricted mode. Only registered skill scripts are permitted.
{{ /if }}Including fragment files:
{{- set myVariable }}{{ include "./my_fragment.md" }}{{ /set -}}
Hello, I am {{ myVariable }}.Using JavaScript expressions:
{{ new Date().toLocaleDateString("zh-TW") }}
{{ isDm ? "私訊模式" : "公開頻道模式" }}To customize the bot's character, edit individual fragment files (e.g., character_name.md, character_info.md) without touching system_reply.md. You can also override any prompt file by mounting your custom version:
# compose.yml
volumes:
- ./my-prompts/system_reply.md:/app/prompts/system_reply.md:ro,Z
- ./my-prompts/character_name.md:/app/prompts/character_name.md:ro,Z- Only override the files you need; others keep their container defaults
- Fragment files (e.g.,
character_name.md) can be plain text or use Vento syntax - Your custom templates have access to all the template variables listed above
If you have custom prompt files using the old {{placeholder}} syntax, you need to update them:
| Old Syntax | New Syntax |
|---|---|
{{character_name}} |
{{ include "./character_name.md" }} |
{{character_info}} |
{{ include "./character_info.md" }} |
For fragment values used multiple times, use set to load once:
{{- set charName }}{{ include "./character_name.md" }}{{ /set -}}
Hello, I am {{ charName }}. {{ charName }} is my name.Warning
This is a breaking change. The old {{placeholder}} syntax is no longer supported. In Vento, {{placeholder}} is interpreted as a variable reference, not a fragment include. If the variable is undefined, it will output an empty string or an error depending on the template engine configuration.
When running AIr-Friends in a container, you can customize the bot's character by mounting your own prompt files without rebuilding the container image:
-
Copy the default prompts to your local directory:
# The default prompts are included in the repository # You can copy them to customize: cp -r prompts/ my-custom-prompts/
-
Edit the prompt files in your local directory:
Edit
my-custom-prompts/character_name.md,my-custom-prompts/character_info.md, etc. to customize your bot's character. -
Mount your custom prompt files when running the container:
Using
podman run:podman run -d --rm \ -v data:/app/data \ -v ./config.yaml:/app/config.yaml:ro \ -v ./my-custom-prompts/character_name.md:/app/prompts/character_name.md:ro \ -v ./my-custom-prompts/character_info.md:/app/prompts/character_info.md:ro \ --env-file .env \ --name air-friends \ ghcr.io/jim60105/air-friends:latest
Using
compose.yml:volumes: # Mount only the prompt files you want to override - ./prompts/character_name.md:/app/prompts/character_name.md:ro,Z - ./prompts/character_info.md:/app/prompts/character_info.md:ro,Z
Tip
Only the files you mount will be overridden. Files you don't mount keep their container defaults, so there's no need to provide all prompt files.
-
Restart the container to apply the changes:
podman compose down && podman compose up -d
The container includes default prompts that will be used if you don't mount any custom prompt files.
Run the test suite:
deno task testCoverage Requirement: Test coverage MUST be over 75%. CI enforces this threshold — PRs that drop below 75% coverage will not pass. All PRs will only be merged after all CI checks succeed.
During development, data is stored under ./data/ (configurable via workspace.repoPath):
data/
├── workspaces/ # Per-user workspaces
│ └── {platform}/{userId}/ # Each user's memory files
└── agent-workspace/ # Agent's global knowledge workspace
├── README.md # Usage guide
├── notes/ # Knowledge notes by topic
│ ├── _index.md # Notes index
│ └── {topic}.md # Individual notes
└── journal/ # Daily reflections
└── {YYYY-MM-DD}.md # Daily entries
The agent workspace is automatically created on first use by WorkspaceManager.getOrCreateAgentWorkspace().
For more information about testing practices and guidelines, see DESIGN.md.
- DESIGN.md - Detailed design document with architecture and data flow
- SKILLS_IMPLEMENTATION.md - Skills implementation guide
- features/ - BDD feature specifications (Gherkin)
- misskey/ - Misskey integration documentation
- AGENTS.md - Development guide for AI agents
Please ensure your code follows the project's coding standards:
- Run
deno fmtbefore committing - Run
deno lintto check for issues - Ensure all tests pass with
deno test - Ensure test coverage is over 75%
- All CI checks must pass before a PR can be merged
- Follow the architecture patterns described in DESIGN.md