skill-sdk¶
skill-sdk (пакет skill_sdk) — SDK скиллов платформы: скилл пишется один
раз как функция Python, а SDK выводит из кода контракт v1, даёт контекст
вызова, хостит скилл по любому из трёх протоколов исполнителя (local,
http, mcp) и генерирует YAML для пакета каталога Control Plane. Для
разработчиков, которые добавляют в платформу новые действия для агентов и
процессов. Обоснование — TAI-ADR-0045; контракт скилла — CP-ADR-0056.
Статус
Версия 0.1.x, лицензия Apache-2.0. Пакет подключается path-зависимостью
соседней папкой, как остальные библиотеки платформы
(см. SDK и интеграции).
Что такое скилл¶
Скилл — именованное версионированное действие с формальным контрактом: JSON-схемы входа и выхода, класс побочных эффектов, уровень риска, идемпотентность, таймаут, политика повторов. Ядро хранит контракт версии неизменяемым; исполнитель (runner) вызывает реализацию скилла, проверяет вход и выход по схемам и публикует результат в задаче.
flowchart LR
Code["@skill в коде"] -->|"skill-sdk export"| Y["packages/пакет/skills/*.yaml"]
Y -->|"make bootstrap (пакеты каталога)"| CP[Control Plane: Skill name@version]
CP -->|задача с execution.skill| R[Исполнитель]
R -->|local / http / mcp| H[Хостинг скилла: skill-sdk]
H --> Code
Написать скилл¶
from typing import Literal
from pydantic import BaseModel
from skill_sdk import SkillContext, SkillError, skill
class MergeIn(BaseModel):
repository: str
branch: str
commit: str
target: str
class MergeOut(BaseModel):
merged: bool
sha: str | None = None
reason: Literal["conflict", "branch_moved", "already_merged"] | None = None
@skill(
"git.merge",
version="2",
side_effects="external_write",
risk="medium",
idempotency="natural",
timeout=300,
retry=(3, 30),
)
def merge(inputs: MergeIn, ctx: SkillContext) -> MergeOut:
"""Влить опубликованную ветку в целевую."""
...
if conflict:
return MergeOut(merged=False, reason="conflict") # предусмотренный исход — это выход
raise SkillError("git_unavailable", "remote недоступен", retryable=True) # сбой — ошибка
Контракт из кода¶
| Часть контракта | Откуда берётся |
|---|---|
| Схема входа | pydantic-модель первого аргумента (или inputs_schema=) |
| Схема выхода | pydantic-модель возвращаемого значения (или outputs_schema=) |
| Описание | первый абзац docstring (или description=) |
| Политика | аргументы декоратора |
| Реализация по умолчанию | local с entrypoint модуль:имя самой функции |
Схемы — JSON Schema 2020-12. Явные inputs_schema/outputs_schema нужны,
когда у уже опубликованной версии своя схема, которую модель не
воспроизводит.
Параметры декоратора @skill¶
| Параметр | Значения | По умолчанию | Смысл |
|---|---|---|---|
name |
строка | — | ключ скилла, например git.merge |
version |
строка | — | версия контракта; изменение контракта = новая версия |
side_effects |
none, external_read, external_write |
— | что скилл делает во внешнем мире |
risk |
low, medium, high |
— | уровень риска |
idempotency |
required, natural, none |
none |
повтор с тем же ключом не даёт второго эффекта |
timeout |
секунды | 60 |
таймаут вызова |
retry |
(maxAttempts, backoffSeconds) |
(1, 0) |
политика повторов исполнителя |
permissions, preconditions, postconditions, cost_model |
— | пусто | дополнительные части контракта v1 |
implementation |
Local(...), Http(...), Mcp(...) |
Local() |
как хостить (обычно задаётся при экспорте) |
SDK отвергает при импорте то, что отвергло бы ядро: неизвестные
значения side_effects/risk/idempotency, функцию не с сигнатурой
(inputs) или (inputs, ctx), отсутствие схемы входа или выхода и
external_write без идемпотентности с maxAttempts > 1 (повтор внешней
записи без идемпотентности — второй внешний эффект).
Исход против сбоя¶
| Ситуация | Как выразить |
|---|---|
| Исход, предусмотренный контрактом («конфликт», «не найдено») | вернуть выход с соответствующим полем |
| Сбой среды: сеть, лимит, недоступный сервис | SkillError(code, message, retryable=True) |
| Сбой, который повтор не исправит | SkillError(code, message, retryable=False) |
Код ошибки — ^[a-z0-9][a-z0-9_.-]{0,99}$. Любое другое исключение
реализации превращается в SkillError с кодом из атрибута code
исключения (если он валиден) или skill_error. Нарушение схемы —
input_contract_violation или output_contract_violation с перечнем ошибок
в details.
Контекст вызова SkillContext¶
Функция может быть синхронной или async; второй аргумент — контекст.
| Член | Назначение |
|---|---|
ctx.invocation_id, ctx.idempotency_key |
идентификатор вызова и ключ идемпотентности — повтор с тем же ключом не должен давать второй внешний эффект |
ctx.remaining() |
секунд до таймаута контракта (None, если хостинг его не знает) |
ctx.check_deadline() |
бросает повторяемый SkillError("timeout"), если время вышло |
ctx.log |
журнал с id вызова |
ctx.config(name, default=None) |
параметр хостинга (из окружения процесса) |
ctx.secret(name) |
секрет хостинга; нет значения — повторяемый config_missing (другой хост может его иметь) |
ctx.llm |
клиент platform-llm по конфигурации инсталляции; токены учитываются в стоимости сами |
ctx.add_cost(unit, amount) |
учесть своё потребление (запросы к API, страницы и т. п.) |
ctx.caller |
проверенный контекст вызывающего (TrustedAuthContext) для http/mcp-http |
Клиента Control Plane в контексте нет намеренно: скилл не заводит и не двигает задачи — это делают исходы approval и правила ядра.
LLM по умолчанию — OpenAICompatibleClient из переменных:
| Переменная | Смысл |
|---|---|
SKILL_LLM_BASE_URL |
база OpenAI-совместимого API |
SKILL_LLM_API_KEY |
ключ |
SKILL_LLM_MODELS |
модели через запятую — порядок ротации |
Другой провайдер — skill_sdk.configure_llm(factory).
Хостинг¶
| Протокол | Как запустить | Что видит исполнитель |
|---|---|---|
local |
пакет установлен рядом с демоном исполнителя, CONTROL_PLANE_SKILLS_LOCAL_PACKAGES=<пакет> |
исполнитель находит скиллы сам, вызывает __skill_invoke__ и получает {outputs, cost} |
http |
skill-sdk serve http <модуль> или skill_sdk.http.create_app(...) в своём ASGI |
POST /skills/{name}@{version} |
mcp |
skill-sdk serve mcp-stdio <модуль> или serve mcp-http |
инструмент MCP с именем скилла |
HTTP-протокол¶
POST /skills/git.merge@2
Authorization: Bearer <access token IAM audience скилла>
Content-Type: application/json
{"invocationId": "…", "idempotencyKey": "…", "inputs": {"repository": "…", "branch": "b", "commit": "abc1234", "target": "main"}}
| Ответ | Смысл |
|---|---|
200 |
тело — сами outputs без конверта; стоимость — в заголовке X-Skill-Cost |
422 |
неповторяемый SkillError: {"error": {"code", "message", "retryable", "details"}} |
503 |
повторяемый SkillError в том же конверте |
401 |
токен не прошёл проверку |
404 |
такого скилла на этом хостинге нет |
500 |
сбой реализации |
GET /skills — список скиллов хостинга.
MCP¶
Скилл — инструмент MCP-сервера; стоимость — в _meta["skill/cost"],
ошибка — результат с isError и единственным текстовым блоком
{"error": {…}} того же вида.
Аутентификация хостинга¶
http и mcp-http проверяют токен IAM audience скилла через
platform-auth-sdk:
| Переменная | Смысл |
|---|---|
SKILL_SDK_IAM_ISSUER |
issuer IAM (${TAIMEN_PUBLIC_URL}/iam) |
SKILL_SDK_AUDIENCE |
audience скилла (тот же, что в контракте реализации) |
SKILL_SDK_JWKS_URL |
JWKS IAM |
Без этих переменных хостинг не стартует. Флаг --allow-anonymous
(allow_anonymous=True) отключает проверку — только для локальной
разработки. Недоступный JWKS даёт 503, неверный токен — 401.
Пакет каталога¶
Скиллы попадают в Control Plane через пакеты каталога (TAI-ADR-0044, см. Пакеты каталога). YAML скиллов генерируется из кода и руками не правится:
# записать packages/selfdev/skills/*.yaml
skill-sdk export --package ../packages/selfdev taimen_selfdev
# CI: проверить, что код и YAML совпадают
skill-sdk export --package ../packages/selfdev --check taimen_selfdev
# хостинг по http: реализация в контракте, адрес из переменной окружения инсталляции
skill-sdk export --package ../packages/acme --protocol http \
--endpoint '${ACME_SKILLS_URL}' --audience acme-skills acme_skills
Параметр export |
Смысл |
|---|---|
--package |
каталог пакета (обязателен) |
--protocol |
local, http или mcp; по умолчанию — объявленный в коде |
--endpoint |
http: база сервиса, допускает ${VAR}; mcp: URL или stdio:<имя> |
--audience |
IAM audience, токен которого несёт исполнитель |
--check |
не писать, а сверить с файлами (для CI) |
Фрагмент сгенерированного файла:
# Сгенерировано skill-sdk из кода — правьте код и перегенерируйте (TAI-ADR-0045).
kind: Skill
key: adr.conformance_check
spec:
version: '1'
description: Сверить Accepted ADR с кодом по пробам (без LLM)
sideEffects: none
riskLevel: low
contract:
inputs:
$schema: https://json-schema.org/draft/2020-12/schema
type: object
required: [repository]
…
Контракт версии в ядре неизменяем: изменение схем или политики — новая
version в декораторе, новый файл и новый объект Skill в каталоге.
Прочие команды CLI¶
| Команда | Что делает |
|---|---|
skill-sdk list <модули…> |
скиллы в модулях |
skill-sdk contract <модуль:имя> |
контракт v1 одного скилла (JSON) |
skill-sdk invoke <модуль:имя> --input '<json>' |
вызвать локально с проверкой контракта (--input @файл — из файла) |
skill-sdk serve {http,mcp-http,mcp-stdio} <модули…> |
хостинг |
Цели — модуль:имя, модуль или пакет (со всеми подмодулями). Два разных
объявления одного name@version — ошибка.
Тесты скилла¶
from skill_sdk.testing import check_contract, invoke
def test_merge_contract():
check_contract(merge) # валидаторы ядра, если control-plane установлен рядом
def test_conflict_is_an_outcome():
result = invoke(merge, {"repository": "…", "branch": "b", "commit": "abc1234", "target": "main"})
assert result.outputs["reason"] == "conflict"
ainvoke — асинхронный вариант. check_contract прогоняет контракт через
те же валидаторы, что использует ядро, если пакет control-plane
доступен в окружении.
Установка¶
uv add skill-sdk # контракт, local, тесты, экспорт
uv add "skill-sdk[http]" # + ASGI-хостинг и проверка токена
uv add "skill-sdk[mcp]" # + MCP-сервер
uv add "skill-sdk[llm]" # + ctx.llm
uv add "skill-sdk[all]" # всё сразу
platform-auth-sdk и platform-llm подключаются соседними папками.