Building an Agent
🔵 The complete path to creating an agent — just YAML and, optionally, scripts. First — the philosophy, then — the mechanics.
4.1 Philosophy
Before writing agent.yaml, it's worth understanding what you're actually creating.
An agent is a description, not code
In Agent OS, an agent is assembled from YAML: personality, tools, hooks, budget. Code (Rhai/JS) is only needed when you require a custom tool or hook. This is declarative assembly: you describe an employee in a "job description", rather than writing their "brain". All the heavy lifting — the turn loop, tool-call handling, token accounting, isolation, logging, planning — is done by the core.
Five questions that define an agent
Any agent is an answer to five questions. Each is a separate facet, a separate place in the config:
| Question | Field | What it is |
|---|---|---|
| Who is he? | instructions |
role, personality, behavior rules, and response format |
| What can he do? | tools / tool_files |
built-in and YAML/script tools |
| How does he behave in the lifecycle? | hooks / hook_files |
memory, cache, escalation, context injections |
| How much is he allowed? | budget / budget_group |
token limits (this is money) |
| Where do requests come from? | telegram, email, widget, API |
entry channels |
Separation of concerns
Each facet lives in its own file, so they can be changed independently:
swap out a tool without touching the prompt; rewrite the prompt without touching the hooks;
move the limits into budgets.yaml without touching the agent at all. This is the main
point of the structure:
config/agents/<name>/
├── agent.yaml # personality + bindings
├── tools/*.yaml # capabilities
├── hooks/*.yaml # lifecycle
├── bench/scenarios.yaml # quality control
└── tests/*.yaml # tool tests
The agent as an employee
A useful metaphor: instructions — the job description (what to do and how to
respond), tools — the skills and tools at the workplace, hooks — reflexes
and rules (memory, caching of frequent items, handoff to a human), budget — limits of
authority, channels — communication lines.
Principles of a good agent
- Groundedness. Demand sources and citations, forbid making things up, verify
with a tool. The
ngu-agentexample forces citing a source;trace-agent— "never guess, always verify with a call". - Clear role and format. Fix the language and structure of the response right in the instructions.
- Economy. Budgets are money: cache (
faq_cache) and context limits cut costs. - Observability. Cover the agent with benchmarks (
bench/) — otherwise regressions go unnoticed.
Template → process → session
You describe a template (recipe). The core creates a process on the first request (lazy spawn), and the user's conversation is a session. Change the template — new sessions get the new behavior; already running processes keep the old system prompt.
4.2 Directory structure
The server finds agents on its own: any subfolder of config/agents/ containing
agent.yaml (or agent.yml) becomes a template. tool_files and
hook_files are resolved relative to the agent folder.
4.3 agent.yaml — reference
name: my-agent
welcome: |
👋 Привет! Чем помочь?
instructions: |
You are a helpful assistant.
Use my_tool to answer questions about X.
Answer in Russian.
tools: [http_fetch]
tool_files:
- tools/my_tool.yaml
hooks: [] # имена хуков, зарегистрированных при старте
hook_files:
- hooks/faq_cache.yaml
route_files: # HTTP-маршруты, которые добавляет агент (http_route / static_route)
- routes/api.yaml
max_context_tokens: 32000
max_output_tokens: 8192 # потолок ответа за inference (перекрывает kernel.max_output_tokens)
model: "" # именованная модель из server.yaml → models:
output_schema: null # JSON Schema структурированного завершения
budget: # опциональное перекрытие бюджета
refill_per_sec: 30
max_bucket: 10000
max_total: 200000
budget_group: my-shared
budget_messages:
session_exhausted: "Лимит диалога исчерпан. Начните новый."
user_exhausted: "Лимит на сегодня исчерпан."
group_exhausted: "Общий лимит исчерпан. Попробуйте позже."
rate_limited: "Слишком много запросов. Подождите."
inherit_budget: false
durable: false # true = сессия переживает рестарт (opt-in)
supervision: null # политика авто-рестарта (opt-in), см. ниже
parent: null
attachments: # что виджет принимает от пользователя (файлы/скриншоты/камера)
enabled: true # false = виджет скрывает кнопку вложений
file: true # загрузка файлов (по умолч. true)
screenshot: true # снимок экрана (по умолч. true)
camera: true # фото с камеры (по умолч. true)
accept: "image/*,.pdf" # маска accept для файлового инпута (опционально)
max_size: 10485760 # макс. размер файла в байтах (опционально)
login: # вход в виджете (см. §19.1)
enabled: false # показывать кнопку «Войти»
required: false # требовать вход (анонимные отклоняются, 401)
providers: [yandex] # OIDC-провайдеры (пусто = все)
allow_users: false # разрешить вход логином/паролем из auth.users
prompt: "Войти через Яндекс"
widget: # оформление виджета (см. §19.1)
show_close: true # кнопка «закрыть» (×); false — для инлайн-встраивания
starters: [] # вопросы-кнопки под приветствием нового диалога
persist: true # сохранять диалог при закрытии/перезагрузке (по умолч. true)
history_limit: 40 # сколько завершённых обменов хранить в истории
tool_visuals: final # визуалы тулов: final|always|never (см. §19)
cron: [] # задания по расписанию этого агента
telegram:
token_env: MY_TG_BOT_TOKEN
allowed_user_ids: []
show_typing: true
show_tool_calls: true
email:
imap_host: imap.example.com
imap_port: 993
imap_username: support@example.com
imap_password_env: EMAIL_PASSWORD
allowed_senders: []
smtp:
host: smtp.example.com
username: support@example.com
password_env: SMTP_PASSWORD
from: support@example.com
notify: []
poll_secs: 60
vk:
token_env: VK_GROUP_TOKEN
group_id: 123456789
allowed_user_ids: []
show_typing: true
show_tool_calls: true
poll_wait: 25
Field reference
| Field | Required | Default | Description |
|---|---|---|---|
name |
yes | — | unique template name |
instructions |
yes | — | system prompt |
welcome |
no | — | widget greeting |
tools |
no | [] |
built-in tools (http_fetch, fs_read, job_done) |
tool_files |
no | [] |
paths to YAML tools (relative to the agent folder) |
hooks |
no | [] |
hook names (registered at startup) |
hook_files |
no | [] |
paths to YAML hooks (relative to the agent folder) |
route_files |
no | [] |
paths to YAML HTTP routes http_route/static_route (relative to the agent folder; see §21) |
max_context_tokens |
no | 32000 |
context window limit |
max_output_tokens |
no | global kernel.max_output_tokens (4096) |
cap on response length per single inference. Overrides the global one and, if needed, raises the process's per-turn budget |
model |
no | empty | named model from server.yaml → models: (empty = default provider) |
output_schema |
no | — | completion JSON Schema: replaces the job_done parameters |
budget |
no | — | {refill_per_sec, max_bucket, max_total} |
budget_group |
no | — | name of the shared budget group |
budget_messages |
no | — | exhaustion messages (4 levels) |
inherit_budget |
no | false |
children inherit the parent's group |
durable |
no | false |
session survives a core restart: the process, budget group, and session are saved at the turn boundary and restored on boot (requires logging.durable_db_path; see §27.2) |
supervision |
no | null |
auto-restart policy on crash (see below) |
sandbox |
no | null |
per-agent sandbox policy (limits/FS roots), overrides the global server.yaml → sandbox (see §3.2) |
parent |
no | — | parent name (process tree) |
attachments |
no | — | attachments from the user: {enabled, file, screenshot, camera, accept, max_size} — enables the attachments button in the widget and acceptance of attachments from channels (email/telegram/vk, §19) |
login |
no | — | widget entry: {enabled, required, providers, allow_users, prompt} — mandatory OIDC authorization and/or auth.users (§19.1) |
admin_only |
no | false |
admins only: a chat request to the agent must carry a valid admin credential (bearer token or admin login cookie), otherwise 403. When authentication is disabled, the gate is open. For powerful agents (e.g. coder, which edits files and runs commands) |
cron |
no | [] |
scheduled jobs that launch this agent |
telegram |
no | — | {token_env, allowed_user_ids, show_typing, show_tool_calls} |
email |
no | — | inbound email channel (see below) |
vk |
no | — | inbound community VK bot (see below) |
yandex_messenger |
no | — | inbound Yandex Messenger bot (Bot API, polling) |
Supervision (auto-restart)
Optional: the agent is restarted on crash (non-transient inference error or unexpected termination with exit≠0).
supervision:
restart: on_failure # never | on_failure | always
max_restarts: 3 # бюджет перезапусков в окне
window_secs: 300
backoff_secs: 5 # задержка перед рестартом
resume: from_template # from_template | from_checkpoint
replay_input: false # opt-in: переиграть вход, на котором упало
on_exhausted: terminate # terminate | quarantine
poison_threshold: 3 # одинаковых ошибок до карантина (0 = выкл)
- A restart recreates the process and rebinds the session to the new pid; the budget is counted per session (survives restarts).
- By default the new agent waits for the next inbound message; with
replay_input: truethe input on which it crashed is replayed (forfrom_checkpoint— "from the boundary", forfrom_template— reinject + turn). - Budget exhaustion →
on_exhausted(terminateorquarantine— the process is parked and no longer restarted). - Events:
process_restarted,supervisor_gave_up.
Input/output channels
A template can bind channels — inbound sources that route
messages to this agent: widget, Telegram bot, VK bot, Yandex Messenger bot,
email, HTTP API, webhook. In agent.yaml these are the fields telegram, vk,
yandex_messenger and email. A full breakdown of all channels (widget embedding,
configuration of Telegram/VK/Yandex Messenger/email, sinks) — in §19.
Structured output (output_schema)
Set output_schema so that the agent returns a fixed shape instead of
free text. The core replaces the job_done parameters with this schema (adding
job_done if it is not in tools:), and the model must finish with a call to
job_done(...) with a suitable value:
output_schema:
type: object
properties:
needs_action: { type: boolean }
summary: { type: string }
required: [needs_action, summary]
The arguments of job_done are validated before completion; an invalid call
is rejected (the model sees the error and can correct itself in the same cycle).
The validated value is available to the caller (e.g. subagent_tool puts
it into data of the result).
4.4 Step by step
Step 1 — skeleton
mkdir -p config/agents/mybot/tools config/agents/mybot/hooks config/agents/mybot/bench config/agents/mybot/tests
Write config/agents/mybot/agent.yaml (see §4.3).
Step 2 — tool
Create config/agents/mybot/tools/my_tool.yaml (see
tool reference) and list it in tool_files:.
Test without a server:
agent-os tool config/agents/mybot/tools/my_tool.yaml '{"query":"..."}'
Step 3 — hook (optional)
Create config/agents/mybot/hooks/<hook>.yaml (see 06-hooks-reference) and
list it in hook_files:. Test without a server:
agent-os hook config/agents/mybot/hooks/<hook>.yaml user "Привет"
Step 4 — budget group
Add the group to config/budgets.yaml and bind the template:
budget_groups:
- name: mybot-shared
refill_per_sec: 30
max_bucket: 30000
max_total: 3000000
agents:
- template: my-agent
group: mybot-shared
Step 5 — benchmark scenarios
Create config/agents/mybot/bench/scenarios.yaml:
name: "MyBot"
model: "my-agent"
scenarios:
- name: "Простой вопрос"
turns:
- user: "Как мне ...?"
assert:
contains: [ожидаемое, слово]
min_links: 1
valid_urls: true
Check types: contains (substrings, case-insensitive), min_links /
min_urls (number of markdown links), valid_urls (all links http/https).
Run (see 09-operations):
agent-os bench --port=3001 --agent=my-agent --max=3
Step 6 — tool tests (optional)
Fixture tests (no network) in config/agents/mybot/tests/my_tool_tests.yaml:
kind: site_tool_tests
tool: tools/my_tool.yaml
tests:
- name: "извлекает заголовок"
input: { url: "https://example.com/p/1" }
fixture: { productData: { title: "Widget", price: 99 } }
assert:
- { path: /title, equals: "Widget" }
- { path: /price, equals: 99 }
Live tests (real HTTP: kind: site_tool_live_tests,
archive_tool_live_tests, rss_tool_live_tests) instead of fixture: set
fetch: true. How to run them — in 09-operations.
4.5 Instruction practices
- Name which tool to call when, including multi-step
chains and lookup by slug/id (a live example is
config/agents/drom/agent.yaml). - Require copying
links[]from the tool output into the response; forbid bare URLs. - Fix the language and format of the response.
- Use
__GEOIP__to substitute the user's city (seedrom). - Keep the agent "grounded": "never guess, always verify with a call"
(see
trace-agent).
4.6 Live reload
With live_reload: true (default), you can edit agent.yaml, tools
and hooks on the fly: new sessions pick up the new instructions, re-registered
tools/hooks take effect from the next call. Already running processes
keep the original system prompt.