Перейти к содержанию

Клиенты сервисов

Канонические Python-клиенты платформы: control-plane-client для Control Plane и platform-memory-client для memory-service. Статья описывает подключение, разрешение credential, обмен PAT на access token, повторы и идемпотентность, обработку ошибок и типовые сценарии. Для разработчиков исполнителей, коннекторов, демо и вертикальных пакетов.

Не пишите свой клиент

Логика обмена токенов, повторов под тем же Idempotency-Key и разбора конверта ошибок уже есть в каноне (TAI-ADR-0030). Если чего-то не хватает, добавьте метод в клиент рядом с сервером, а не обёртку «по памяти о контракте» в своём репозитории.

control-plane-client

Пакет control_plane_client (дистрибутив control-plane-client)
Где лежит control-plane/client
Зависимости только httpx
Протокол control-harness/2; базовый путь {server}/api/v1
[tool.uv.sources]
control-plane-client = { path = "../control-plane/client", editable = true }

Быстрый старт

from control_plane_client import ControlPlaneClient, IamCredential

credential = IamCredential(
    "https://platform.example.com/iam",   # IAM
    "<iam-tenant-id>",
    audience="control-plane",
    scopes=("control-plane:read", "control-plane:write"),
)

async with ControlPlaneClient("https://platform.example.com", credential,
                              user_agent="acme-connector/0.1") as cp:
    ctx = await cp.get_context()
    task = await cp.create_task(...)       # Idempotency-Key генерируется сам

Второй аргумент ControlPlaneClient — строка (статический API-ключ) или CredentialProvider (объект с token(), refresh(), refreshable). user_agent ставится перед токеном SDK: acme-connector/0.1 control-plane-client/<версия>.

Credential: откуда берётся PAT

Control Plane не принимает PAT как Bearer: PAT предъявляется только IAM, а сервис получает короткоживущий access token своего audience. IamCredential делает обмен POST {iam}/api/v1/platform-access-tokens:exchange с телом {token, audience, scopes}, кэширует токен и обменивает заново за refresh_margin_seconds (по умолчанию 30 с) до истечения.

Порядок поиска PAT:

Источник Когда используется
аргумент platform_access_token= (строка или callable) явная передача; callable читается при каждом обмене — ротация файла подхватывается без перезапуска; локальное хранилище не читается, tenant может быть пустым
IAM_PLATFORM_ACCESS_TOKEN только вместе с IAM_CREDENTIAL_MODE=environment (или ci); без режима — ошибка iam_environment_mode_required
Keychain macOS если не отключён IAM_NO_KEYCHAIN=1
файл ~/.config/iam/credentials.json учитывается XDG_CONFIG_HOME

Запись в локальном хранилище адресуется тройкой issuer|tenant|principal. Если на машине несколько credential одного tenant'а (несколько исполнителей), процесс обязан назвать себя переменной IAM_PRINCIPAL; иначе — отказ iam_credential_ambiguous, а не работа под чужой identity.

Процесс в контейнере с PAT в файле:

from pathlib import Path
from control_plane_client import IamCredential

pat_file = Path("/run/secrets/agent.pat")
credential = IamCredential(
    "http://iam-service:8010", "",
    audience="control-plane",
    scopes=("control-plane:read", "control-plane:write"),
    platform_access_token=lambda: pat_file.read_text(),
)

Одному процессу нужны два сервиса — два IamCredential над одним PAT с разными audience (правило «один токен — один audience»).

Конфигурация из окружения

resolve_credential(server_url) выбирает credential харнесса: сначала IAM, затем API-ключ.

Переменная Смысл
CONTROL_PLANE_IAM_URL адрес IAM; без него IAM-режим не включается
CONTROL_PLANE_IAM_TENANT tenant IAM; URL без tenant'а — ошибка конфигурации, а не молчаливый откат
CONTROL_PLANE_IAM_AUDIENCE audience, по умолчанию control-plane
CONTROL_PLANE_IAM_SCOPES scopes через пробел или запятую, например "control-plane:read control-plane:write"
IAM_PRINCIPAL какой principal этот процесс, если на машине их несколько
CONTROL_PLANE_API_KEY legacy API-ключ (только если IAM не настроен)

Значения со пробелами в env-файлах, которые читает shell (source), заключайте в кавычки.

Команды, повторы и идемпотентность

Свойство Поведение
Создающие и action-команды (create_task, claim_task, start_run, succeed_run, fail_run, create_artifact, request_approval, …) клиент ставит Idempotency-Key и повторяет запрос при транспортном сбое (всего до 3 попыток, паузы 0,5 с и 1 с) тем же ключом — повтор HTTP-запроса никогда не становится второй бизнес-командой
Свой ключ idempotency_key= у create_task, succeed_run, fail_run, create_artifact, request_approval — для потребителей, которые повторяют команду по своему состоянию
401 при refreshable credential один переобмен и один повтор с тем же ключом; второй 401 — настоящий отказ
Оптимистичная блокировка update_task(..., expected_version=), complete_task(..., version=) отправляют If-Match: "task-<version>"
Чтения без повторов

Ошибки

Ответы Control Plane приходят в конверте {"error": {"code", "message", "details", "requestId"}} и превращаются в исключения по коду, а при незнакомом коде — по статусу:

Исключение Коды / статус Что делать
AuthenticationError invalid_credentials, 401 проверить PAT, binding
PermissionDeniedError permission_denied, 403 не хватает права в binding'е
NotEligibleError not_eligible нет роли/capability для задачи
NotFoundError not_found, 404 —
ValidationError 422, 428, unsupported_protocol_version исправить запрос
StaleClaimError stale_claim прекратить запись: владение задачей потеряно
ClaimConflictError task_already_claimed, task_claimed, claim_conflict, … задача занята
VersionConflictError version_conflict перечитать задачу, повторить с новой версией
IdempotencyConflictError idempotency_key_reused, idempotency_in_flight ключ использован с другим телом или команда ещё выполняется
SessionExpiredError session_expired, session_not_active открыть новую сессию
ApprovalRequiredError, TaskNotReadyError, BudgetExceededError, SkillUnavailableError, RunNotActiveError, CancelledError одноимённые коды см. Исполнение — claims и runs
TransportError ответа нет повторить позже
IamCredentialError iam_unreachable, iam_invalid_token, iam_audience_not_allowed, iam_exchange_failed, iam_not_authenticated, iam_credential_ambiguous проблема обмена в IAM

Все исключения наследуют ControlPlaneError (code, message, status, details).

Аренды: HeartbeatRunner

Сессия и claim — аренды с TTL. HeartbeatRunner продлевает их в фоне:

from control_plane_client import HeartbeatRunner

runner = HeartbeatRunner(cp, session_id=session["id"], claim_id=claim["id"],
                         interval_seconds=60, max_transport_failures=3)
runner.start()
try:
    # … в цикле работы:
    if runner.error is not None:
        ...  # владение потеряно — прекратить авторитетные записи
finally:
    await runner.stop()

Доменная ошибка (аренда истекла, владение потеряно) терминальна и сохраняется в error; транспортная ошибка повторяется на следующем тике и становится терминальной только после max_transport_failures подряд.

Привязка каталога к проекту

find_project_config() ищет вверх от текущего каталога .control-plane/config.json — несекретную привязку рабочей копии к серверу, tenant'у, workspace, проекту и репозиторию; write_project_config() её записывает. Секретов в этом файле нет, его можно коммитить.

Группы методов

Группа Методы (выборочно)
контекст и сессии get_context, open_session, heartbeat_session, close_session
работа list_available_work, list_tasks, get_task, get_task_transitions, get_claimability
задачи create_task, update_task, complete_task, add_task_relation, add_task_comment, edit_task_comment
цели create_goal, list_goals, get_goal, update_goal, list_goal_work
claims и runs claim_task, heartbeat_claim, release_claim, start_run, succeed_run, fail_run, suspend_run, prepare_handoff, continue_after_handoff, launch_child_run
трасса create_checkpoint, record_action, finish_action
артефакты create_artifact, get_artifact, list_artifacts
approvals request_approval, list_approvals, approve, reject, get_approval_outcome
инструменты search_tools, describe_tool, get_harness_manifest

Полный контракт — OpenAPI Control Plane (/openapi.json, см. API Control Plane).

platform-memory-client

Пакет platform_memory_client (дистрибутив platform-memory-client)
Где лежит memory-service/client
Зависимости httpx, pydantic (движок памяти не подтягивается)
Классы MemoryClient (синхронный), AsyncMemoryClient (asyncio)

Кто ходит в память напрямую

Исполнители получают контекст задачи через Control Plane (контекст харнесса и run'а), а не из памяти напрямую — см. Контекст задачи и память. Клиент памяти нужен сервисам с собственным грантом на namespace: коннекторам загрузки знаний, демо, сервисам пакетов.

Credential

Bearer — статический ключ-грант на namespace или access token IAM audience memory-service. Параметр token принимает строку или callable (вызывается перед каждым запросом); асинхронный клиент — ещё и объект с async token(), то есть IamCredential подключается напрямую:

from control_plane_client.iam import IamCredential
from platform_memory_client import AsyncMemoryClient

cred = IamCredential(
    "http://iam-service:8010", "",
    audience="memory-service",
    scopes=("memory:read", "memory:write"),
    platform_access_token=lambda: pat,
)
async with AsyncMemoryClient("http://memory-service:8077", token=cred) as mem:
    result = await mem.query("как получить пропуск для посетителя?", namespaces=["kb"])

Методы

Метод Путь Назначение
healthz() GET /healthz живость и статистика графа
recall(query, budget=, hops=, namespaces=, …) POST /recall поиск с раскрытием связей
search(...) POST /search гибридный поиск
query(question, k=8, hops=1, synthesize=, namespaces=, …) POST /api/brain/query ответ с источниками (опционально — синтез LLM)
retain(content, type=, external_id=, title=, links=, provenance=, run_id=, namespace=) POST /retain идемпотентная запись факта или решения
audit(...) POST /audit аудит записей
retain_document(...), delete_document(key) /api/brain/documents загрузка и удаление документа
nodes(...), node(key), source(key), delete_node(key) /api/brain/nodes, /api/brain/sources узлы графа и источники
observe(...), observe_batch(...) POST /api/memory/observations наблюдения
context(request, namespaces=, run_id=, …) POST /api/memory/context собрать ограниченный ContextPack без синтеза
register_package, packages, package /api/memory/packages доменные пакеты видов (scope memory:service)
namespace_kinds, set_namespace_kinds /api/memory/namespaces/{ns}/kinds виды namespace
reconcile(...), typed_context(...) /api/memory/reconcile, /api/memory/context/typed сверка и типизированный контекст

Параметры allowed_namespaces / allowed_scopes у чтений сужают видимость: для обычного вызывающего они пересекаются с тем, что сервер и так разрешает, и никогда не расширяют её; пустой список не видит ничего. run_id передаётся заголовком X-Run-Id для корреляции.

Ошибки

Исключение Когда
MemoryServiceError ответ 4xx/5xx; поля status_code, detail; свойства unavailable (5xx) и not_found
MemoryTransportError ответа нет (status_code == 0)

Подробно о контракте памяти — API памяти.

Сценарий: коннектор, который заводит задачи

import asyncio
from pathlib import Path
from control_plane_client import ControlPlaneClient, IamCredential, ConflictError

async def main() -> None:
    cred = IamCredential(
        "http://iam-service:8010", "",
        scopes=("control-plane:read", "control-plane:write"),
        platform_access_token=lambda: Path("/run/secrets/connector.pat").read_text(),
    )
    async with ControlPlaneClient("http://control-plane-api:8000", cred,
                                  user_agent="acme-connector/0.1") as cp:
        for item in await fetch_new_items():
            try:
                await cp.create_task(
                    ...,                                 # поля задачи по OpenAPI
                    idempotency_key=f"acme:{item.id}",   # повтор не создаст дубль
                )
            except ConflictError:
                continue

asyncio.run(main())

Principal коннектора — вида agent или service с IAM-binding'ом в Control Plane (права tasks.read, tasks.write, при необходимости events.read), PAT — в файле с правами 0600.

См. также