Command Palette

Search for a command to run...

UnylyUnyly
обзор Saelспецификация v0.2▸ github source ↗

Sael Protocol — Spec v1.0

Source-available спецификация стримингового протокола связи AI-агентов с инструментами (BUSL-1.1, спека CC BY-NC-ND). Замена MCP с совместимым adapter-уровнем.

Версия

v1.0 — Stable

Статус

stable, 2026-06-06

Лицензия

BUSL-1.1

Поддерживает

Fasad Salatov (Unyly)

Sael — это streaming-first протокол для соединения AI-агентов (LLM-клиентов) с инструментами (tool servers). Он заменяет Model Context Protocol (MCP) в production-сценариях где MCP даёт сбой: композиция, стриминг, подписки, backpressure, capability-based security и multi-agent — встроены в протокол, а не сделаны костылями поверх.

▸ v1.0 — stable (2026-06-06)

  • Stability guarantee — никаких breaking changes через v1.x
  • Federation — server-to-server routing через mesh discovery
  • MessagePack binary frames — opt-in через subprotocol
  • Расширенный filter — matches/in/notIn, parens, !, arithmetic, nested.field
  • Schema registry — $ref к https://schemas.sael.dev/
  • Python SDK — [email protected]
  • Conformance test suite — 78 tests (Go 35 + TS 21 + Python 22)
  • IETF draft alignment (sael-protocol-00)

▸ v0.2 — production-grade (2026-06-05)

  • Криптографически подписанные capability-токены (SCT, HMAC-SHA256)
  • Bearer-аутентификация в handshake
  • Восстановление сессии после disconnect (RSM)
  • Heartbeat (HBT/HBA) — detect dead connections
  • Валидация input через JSON Schema (server-side)
  • Cost-трекинг в RES/END
  • Distributed tracing (W3C trace_id/span_id)
  • Версионирование инструментов (name@version)
  • Поле версии протокола бамп 1 → 2

▸ v0.1 (2026-04-15)

Первоначальный draft: streaming, композиция, подписки, backpressure, capabilities.

MCP от Anthropic шипнул в 2024 и стал де-факто стандартом для AI-tooling. Технически — это JSON-RPC поверх stdio/SSE, спроектированный для desktop-app мира. На production-нагрузках видны архитектурные трещины:

  • Нет нативного стриминга

    Long-running tool calls (LLM-стримы, file uploads, log tails) — через SSE-костыли.

  • Stateless каждый call

    Каждый tool invocation пересоздаёт контекст. Жжёт токены, увеличивает latency.

  • Нет композиции

    "Get repos → filter → summarize → post to Slack" — N round-trips с полным payload.

  • Нет подписок

    Реактивные workflow (notify on new PR) — внешние poll-loops.

  • Нет backpressure

    Поток запросов может DoS-нуть tool server. Нет graceful degradation.

  • Нет multi-agent

    Agent-to-agent (Claude → Gemini) требует кастомных мостов.

  • Нет capability model

    Tool может делать что угодно. Клиент не может ограничить.

Это не баги, а следствия выбора JSON-RPC как подложки. Чтобы починить — нужен новый протокол. Sael — это он.

Streaming-native

Двунаправленные стримы — first-class. Каждый call может стримить.

Композиция

Pipe syntax (tool₁ | filter | tool₂) — server-side execution.

Stateful сессии

Открой канал, держи контекст — экономь токены и latency.

Подписки

Подпишись на события. Сервер пушит. Никакого polling.

Backpressure

Встроенный flow control. Серверы троттлят gracefully.

Типизация

JSON Schema повсюду. Композиция — type-checked.

Multi-agent

Agent IDs в протоколе. Прямые agent-to-agent вызовы.

Capability-based

Вызовы требуют capability-токены. Агент не может что не разрешено.

MCP-совместимость

Adapter оборачивает существующие MCP-сервера. Zero migration cost.

Sael использует WebSocket как основной транспорт. WebSocket даёт двунаправленный стриминг, работает в каждом браузере и на любом сервере, переживает mobile-network handovers.

endpoint
ws://server:port/sael
wss://server:port/sael   (TLS, recommended)

Для server-to-server federation допустим plain TCP с тем же framing — меньше overhead. Опциональный QUIC резервируется для v0.3+.

Каждое сообщение в линии — это frame: 4-байтовый header + JSON payload.

wire
┌─────────┬─────────┬─────────────────────────────┐
│ version │  kind   │       payload (JSON)        │
│ 1 byte  │ 3 bytes │       variable length       │
└─────────┴─────────┴─────────────────────────────┘
  • version — версия протокола, сейчас 0x01
  • kind — 3-буквенный ASCII opcode (см. ниже)
  • payload — UTF-8 JSON, может быть пустым

По умолчанию — WebSocket text-frames с JSON. MessagePack включается opt-in через WS-subprotocol x-quark-msgpack: фреймы идут binary-frames, формат компактнее (−12% на типичном payload, больше на числовых). Реализовано в эталонном сервере.

channel — это persistent stateful соединение между клиентом (AI-агентом) и Sael-сервером. Открывается на connect, закрывается на disconnect.

Внутри канала сохраняется состояние:

  • Capability grants (валидны до конца канала или revoke)
  • Подписки (живут пока не отписался или канал закрыт)
  • Открытые tool-стримы (живы пока не completed или cancelled)

Один канал может нести несколько одновременных вызовов / стримов / подписок, различаемых через seq.

Sael v0.2 ввёл криптографически подписанные capability-токены — Sael Capability Tokens (SCT). Похоже на JWT, но spec-defined для Sael.

Формат токена

format
qct.v1.<base64url(payload)>.<base64url(signature)>

signature = HMAC-SHA256(secret, "v1." + base64url(payload))

Payload

json
{
  "iss": "https://issuer.example.com",
  "sub": "[email protected]",
  "iat": 1690000000,
  "nbf": 1690000000,
  "exp": 1700000000,
  "scope": ["github:read:*", "slack:notify:#dev"],
  "client_id": "claude-desktop",
  "max_cost_usd": 5.00
}
  • iss — issuer URL (required)
  • sub — subject (user/principal) (required)
  • iat — issued at, Unix seconds
  • nbf — not before, optional
  • exp — expires, Unix seconds (required)
  • scope — array of capability strings (required)
  • client_id — restricts which client (optional)
  • max_cost_usd — ceiling on total cost (optional)

Capability-строки

examples
github:read:*                          # read anything on GitHub
github:write:repo:owner/name           # write to specific repo
slack:notify:#general                  # notify a specific Slack channel
slack:notify:*                         # notify any Slack channel
*:read                                 # read anything anywhere

Capability "a:b:c" grant'ит exact match И descendants если grant'нут "a:b:c:*".

Использование

json
{
  "kind": "HEY",
  "v": 2,
  "auth": { "type": "bearer", "token": "qct.v1.eyJ..." },
  "agent": { "id": "claude-desktop", "kind": "llm", "name": "Claude" }
}

Серверная верификация

  1. Парсим SCT (split по ".")
  2. Верифицируем HMAC-подпись
  3. Проверяем iat <= now, nbf <= now, exp > now
  4. Если client_id задан — проверяем что совпадает с agent.id
  5. Сохраняем scope как granted capabilities канала

При неудаче — ERR { code: "AUTH_INVALID" } + close.

почему важно

Без подписанных токенов AI-агент мог выдумать любые capabilities и сервер их принимал. SCT означает что только legitимный issuer может выдать токен. Это foundation для enterprise/compliance use cases (audit trails, SOC2, GDPR).

клиент → сервер

HEYhandshake (capabilities, версия, identity агента)
LSTсписок доступных инструментов
INVвызов инструмента (one-shot или stream)
SUBподписка на stream / topic
UNSотписка
CANотменить in-flight инвокацию
ACKbackpressure ack (window update)
BYEgraceful close

сервер → клиент

HEYответ handshake (server capabilities)
LSTответ со списком инструментов
RESone-shot ответ
STRchunk стрима
ENDстрим завершён
ERRошибка (invocation, subscription, session)
EVTсобытие подписки
WINbackpressure window grant
BYEserver-initiated close

Каждый канал стартует с обмена HEY.

json· клиент
{
  "kind": "HEY",
  "v": 1,
  "agent": {
    "id": "claude-desktop-3.7-mac",
    "kind": "llm",
    "name": "Claude Desktop"
  },
  "supports": ["streaming", "subscribe", "compose", "capabilities"]
}
json· сервер
{
  "kind": "HEY",
  "v": 1,
  "server": {
    "id": "github-tools-v2",
    "name": "GitHub Tools",
    "version": "2.1.0"
  },
  "supports": ["streaming", "subscribe", "compose", "capabilities"],
  "tools": 12,
  "topics": 4
}

Если версии не совпадают, сервер отвечает ERR и закрывает канал.

Обе стороны обмениваются heartbeat'ами для определения мёртвых соединений (firewall timeouts, mobile NAT drops).

json
// Client (every 30s)
{ "kind": "HBT", "ts": 1700000000 }

// Server
{ "kind": "HBA", "ts": 1700000000 }

▸ client

Не получил HBA в 60s → reconnect

▸ server

Не получил HBT в 90s → close (state хранится TTL)

После disconnect (drop, mobile sleep) клиент переподключается и шлёт RSM:

json· клиент → сервер
{
  "kind": "RSM",
  "v": 2,
  "session_id": "ses_a7b3c9d1",
  "last_seq_received": 42
}

Сервер:

  • Если session жив — реплеит буферизованные frames с seq > 42, потом продолжает
  • Если истёк — ERR { code: "SESSION_EXPIRED" }, клиент fallback на свежий HEY

Subscriptions и capability grants переживают resume. Открытые tool-streams отменяются (клиент должен re-INV если нужно).

Серверы ОБЯЗАНЫ буферизовать последние 64 outgoing frames на сессию.

mobile-friendly

Айфон в кармане → 4G переключается → WebSocket падает → юзер открывает приложение → client автоматически делает RSM → последние 30s пропущенных push-событий приходят разом. Никаких потерь.

Инструменты регистрируются на сервере при старте. Клиенты discoverят через LST:

json· ответ сервера на LST
{
  "kind": "LST",
  "seq": 1,
  "tools": [
    {
      "name": "github.list_repos",
      "description": "List repos for a user/org",
      "input": { "type": "object", "properties": { "owner": { "type": "string" } } },
      "output": { "type": "array", "items": { "$ref": "#/types/Repo" } },
      "effects": ["network", "read"],
      "cost": { "estimate": 0.0001, "currency": "USD" },
      "streaming": true,
      "requires_capability": "github:read"
    }
  ]
}

Tool schemas используют JSON Schema Draft 2020-12 с двумя расширениями:

  • effects — массив из pure | read | write | network | money | irreversible | cost
  • cost — оценённая стоимость вызова (помогает AI бюджетировать)
  • requires_capability — capability которая нужна для вызова

One-shot

json· клиент → сервер
{
  "kind": "INV",
  "seq": 2,
  "tool": "github.list_repos",
  "input": { "owner": "anthropic" }
}
json· сервер → клиент
{
  "kind": "RES",
  "seq": 2,
  "output": [{ "name": "claude-code", "stars": 12000, "owner": "anthropic" }]
}

Streaming

Когда у инструмента в spec streaming: true, результаты приходят чанками STR, завершается END.

json
// → INV { "seq": 3, "tool": "logs.tail", "input": { "file": "app.log" } }
// ← STR { "seq": 3, "data": { "line": "GET /api 200" } }
// ← STR { "seq": 3, "data": { "line": "GET /api 200" } }
// ← STR { "seq": 3, "data": { "line": "GET /api 500" } }
// ← END { "seq": 3 }

Если у инструмента delta: true, чанки приходят как merge-patch в поле delta (RFC 7386): сначала snapshot, затем инкрементальные патчи. Клиент держит локальное состояние и применяет патчи — трафик пропорционален изменению, а не размеру состояния. Замер: 60 апдейтов 30-метричного дашборда — 28.1 KB целиком против 3.6 KB дельтами (−87%).

json
// → INV { "seq": 4, "tool": "demo.dashboard" }
// ← STR { "seq": 4, "delta": { "metric_0": 0, "metric_1": 10, ... } }  // snapshot
// ← STR { "seq": 4, "delta": { "metric_3": 42 } }                       // patch
// ← STR { "seq": 4, "delta": { "metric_7": 99 } }                       // patch
// ← END { "seq": 4 }

Это killer-фича Sael vs MCP.

Один INV может описать pipeline. Сервер выполняет всю цепочку, отправляя клиенту только финальный результат. Никаких round-trips между шагами.

json· pipeline
{
  "kind": "INV",
  "seq": 4,
  "pipeline": [
    { "tool": "github.list_repos", "input": { "owner": "anthropic" } },
    { "filter": "stars > 100" },
    { "map": ["name"] },
    { "tool": "slack.notify", "input_bind": { "items": "$prev", "channel": "#dev" } }
  ]
}

Стадии:

  • toolвызов tool, output течёт в следующую стадию
  • filter — CEL/SQL-like выражение
  • mapпроекция полей
  • reduceагрегация
  • input_bindбайнд предыдущей output в input следующего tool
  • parallelfan-out: N независимых под-pipeline исполняются конкурентно, результат — массив по порядку
json· parallel fan-out
{
  "kind": "INV",
  "seq": 5,
  "pipeline": [
    { "tool": "data.fetch", "input": { "n": 20 } },
    { "parallel": [
        [ { "tool": "enrich.summary",   "input_bind": { "data": "$prev" } } ],
        [ { "tool": "enrich.sentiment", "input_bind": { "data": "$prev" } } ],
        [ { "tool": "enrich.translate", "input_bind": { "data": "$prev" } } ]
    ] }
  ]
}

Каждая ветка стартует от одного и того же $prev и исполняется в своей горутине. Wall-clock = самая медленная ветка, не сумма. Замер (6 веток × 50 мс): 7 round-trips / 312 мс у MCP против 1 round-trip / 53 мс у Sael.

эффект

4 HTTP round-trips превращаются в один. ~10× снижение latency в production workloads.

json· подписка
{
  "kind": "SUB",
  "seq": 5,
  "topic": "github.pr_opened",
  "filter": { "repo": "anthropic/claude-code" }
}

Сервер отвечает RES с subscription id, потом стримит EVT:

json
{ "kind": "EVT", "seq": 5, "data": { "pr": 123, "title": "Fix typo" } }

Пока клиент не пошлёт UNS с тем же seq, или канал не закроется.

Когда сервер перегружен, он шлёт WIN с меньшим окном. Клиент НЕ ДОЛЖЕН слать больше чем window outstanding requests.

json
{ "kind": "WIN", "window": 3 }

Дефолтное окно: 64. Сервер может сжать в любой момент. Клиент обязан respect.

Sael v0.1 ставит минимальную capability model. Capabilities — это строки которые tool декларирует, а юзер grant'ит.

Tool декларирует: requires_capability: "github:write:repo:foo/bar"

Клиент (после согласия юзера) в HEY:

json
"capabilities": [
  "github:read:*",
  "github:write:repo:foo/bar",
  "slack:notify:#dev"
]

Сервер валидирует capability против granted списка на каждом INV. Если missing — ERR с кодом MISSING_CAPABILITY. В v0.2 будут cryptographically signed grants для audit/compliance.

json
{
  "kind": "ERR",
  "seq": 4,
  "code": "MISSING_CAPABILITY",
  "message": "Tool github.write_issue requires github:write",
  "stage": 1
}

Стандартные коды ошибок:

MISSING_CAPABILITYcapability не выдана
TOOL_NOT_FOUNDимя tool не существует
INVALID_INPUTinput не матчит schema
TIMEOUTпревышен бюджет выполнения
INTERNALserver-side ошибка
BACKPRESSUREслишком много in-flight requests
RATE_LIMITEDupstream API rate limit
CANCELLEDклиент отменил

Каждый frame может содержать tracing metadata по стандарту W3C Trace Context:

json
{
  "kind": "INV",
  "seq": 5,
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "parent_span_id": "00f067aa0ba902b1",
  "tool": "github.list_repos"
}
  • trace_id — 32 hex chars, идентифицирует distributed trace
  • span_id — 16 hex chars, идентифицирует span внутри trace
  • parent_span_id — связывает span с родителем

Сервер пропагирует trace_id ко всем child-операциям (pipeline stages, federation). OpenTelemetry collectors могут ingest'ить Sael traces через sidecar который читает frames и emit'ит spans.

примеры использования

  • Debugging: какие именно вызовы делал AI агент в этой сессии
  • Latency analysis: где в pipeline узкие места
  • Compliance: full audit trail кто-что-когда
  • Cost attribution: какая фича сколько потратила

Sael поставляется с MCP-Sael adapter. Любой существующий MCP-сервер можно обернуть:

diagram
[AI agent] ──Sael──> [Sael adapter] ──MCP──> [legacy MCP server]

Adapter:

  • Конвертит Sael INV → MCP tools/call
  • Конвертит MCP ответы → Sael RES
  • Теряет streaming/composition/subscriptions (MCP не поддерживает)
  • Логает warning когда advanced feature запрошен

Это значит zero migration cost. Клиенты начинают использовать Sael, существующие MCPs работают через adapter, авторы мигрируют на native Sael когда захотят новые фичи.

Reference implementations в этом репо:

Go/backend/internal/sael/
TypeScript/packages/sael-client/

Go server

go
srv := sael.NewServer()
srv.RegisterTool(sael.Tool{
  Name: "echo.upper",
  Description: "Echo text in uppercase",
  Handler: func(ctx context.Context, in map[string]any) (any, error) {
    return strings.ToUpper(in["text"].(string)), nil
  },
})
http.Handle("/sael/ws", srv)

TypeScript client

ts
import { Sael } from '@fasad_salatov/sael-client'

const ch = await Sael.connect('wss://server/sael/ws', {
  agent: { id: 'my-bot', kind: 'llm', name: 'My Bot' },
})

const repos = await ch.invoke('github.list_repos', { owner: 'anthropic' })

for await (const log of ch.stream('logs.tail', { file: 'app.log' })) {
  console.log(log.line)
}

const filtered = await ch.pipeline([
  { tool: 'github.list_repos', input: { owner: 'anthropic' } },
  { filter: 'stars > 100' },
  { map: ['name'] },
])
  1. v0.1Apr 2026initial draft, reference impls, MCP adapter
  2. v0.2Jun 5, 2026SCT auth, session resume, heartbeat, validation, cost tracking, tracing
  3. v1.0Jun 6, 2026stable. Federation, MessagePack, extended filter, schema registry, Python SDK.▸ now
  4. v1.1Q3 2026QUIC transport, mesh routing improvements
  5. v1.2Q4 2026WebRTC P2P для browser-to-browser AI агентов
  6. v1.3Q1 2027WASM pipeline stages (sandboxed user code)
  7. v2.0Q3 2027Asymmetric SCT signing (RSA/ECDSA), full CEL adoption, capability delegation chains

обсуждение

Спека source-available (CC BY-NC-ND), код — BUSL-1.1. Контрибутить через issues или PR на GitHub (с передачей прав, см. CONTRIBUTING). Коммерческая лицензия, вопросы и интеграции — email или Telegram.