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

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 = выкл)

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


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.